New upstream release, includes a new --proxy options and honours the
[debian/bti.git] / bti.c
1 /*
2  * Copyright (C) 2008 Greg Kroah-Hartman <greg@kroah.com>
3  *
4  * This program is free software; you can redistribute it and/or modify it
5  * under the terms of the GNU General Public License as published by the
6  * Free Software Foundation version 2 of the License.
7  *
8  * This program is distributed in the hope that it will be useful, but
9  * WITHOUT ANY WARRANTY; without even the implied warranty of
10  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
11  * General Public License for more details.
12  *
13  * You should have received a copy of the GNU General Public License along
14  * with this program; if not, write to the Free Software Foundation, Inc.,
15  * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
16  */
17
18 #include <stdio.h>
19 #include <stdlib.h>
20 #include <stddef.h>
21 #include <string.h>
22 #include <getopt.h>
23 #include <errno.h>
24 #include <ctype.h>
25 #include <fcntl.h>
26 #include <unistd.h>
27 #include <sys/stat.h>
28 #include <curl/curl.h>
29 #include "bti_version.h"
30
31
32 #define zalloc(size)    calloc(size, 1)
33
34 #define dbg(format, arg...)                                             \
35         do {                                                            \
36                 if (debug)                                              \
37                         printf("%s: " format , __func__ , ## arg );     \
38         } while (0)
39
40
41 static int debug = 0;
42
43 enum host {
44         HOST_TWITTER = 0,
45         HOST_IDENTICA = 1,
46 };
47
48 struct session {
49         char *password;
50         char *account;
51         char *tweet;
52         char *proxy;
53         int bash;
54         enum host host;
55 };
56
57 struct bti_curl_buffer {
58         char *data;
59         int length;
60 };
61
62 static void display_help(void)
63 {
64         fprintf(stdout, "bti - send tweet to twitter\n");
65         fprintf(stdout, "Version: " BTI_VERSION "\n");
66         fprintf(stdout, "Usage:\n");
67         fprintf(stdout, "  bti [options]\n");
68         fprintf(stdout, "options are:\n");
69         fprintf(stdout, "  --account accountname\n");
70         fprintf(stdout, "  --password password\n");
71         fprintf(stdout, "  --proxy PROXY:PORT\n");
72         fprintf(stdout, "  --host HOST\n");
73         fprintf(stdout, "  --bash\n");
74         fprintf(stdout, "  --debug\n");
75         fprintf(stdout, "  --version\n");
76         fprintf(stdout, "  --help\n");
77 }
78
79 static void display_version(void)
80 {
81         fprintf(stdout, "bti - version %s\n", BTI_VERSION);
82 }
83
84 static char *get_string_from_stdin(void)
85 {
86         char *temp;
87         char *string;
88
89         string = zalloc(1000);
90         if (!string)
91                 return NULL;
92
93         if (!fgets(string, 999, stdin))
94                 return NULL;
95         temp = strchr(string, '\n');
96         *temp = '\0';
97         return string;
98 }
99
100 static struct session *session_alloc(void)
101 {
102         struct session *session;
103
104         session = zalloc(sizeof(*session));
105         if (!session)
106                 return NULL;
107         return session;
108 }
109
110 static void session_free(struct session *session)
111 {
112         if (!session)
113                 return;
114         free(session->password);
115         free(session->account);
116         free(session->tweet);
117         free(session->proxy);
118         free(session);
119 }
120
121 static struct bti_curl_buffer *bti_curl_buffer_alloc(void)
122 {
123         struct bti_curl_buffer *buffer;
124
125         buffer = zalloc(sizeof(*buffer));
126         if (!buffer)
127                 return NULL;
128
129         /* start out with a data buffer of 1 byte to
130          * make the buffer fill logic simpler */
131         buffer->data = zalloc(1);
132         if (!buffer->data) {
133                 free(buffer);
134                 return NULL;
135         }
136         buffer->length = 0;
137         return buffer;
138 }
139
140 static void bti_curl_buffer_free(struct bti_curl_buffer *buffer)
141 {
142         if (!buffer)
143                 return;
144         free(buffer->data);
145         free(buffer);
146 }
147
148 static const char *twitter_url = "https://twitter.com/statuses/update.xml";
149 static const char *identica_url = "http://identi.ca/api/statuses/update.xml";
150
151 static CURL *curl_init(void)
152 {
153         CURL *curl;
154
155         curl = curl_easy_init();
156         if (!curl) {
157                 fprintf(stderr, "Can not init CURL!\n");
158                 return NULL;
159         }
160         /* some ssl sanity checks on the connection we are making */
161         curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);
162         curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0);
163         return curl;
164 }
165
166 size_t curl_callback(void *buffer, size_t size, size_t nmemb, void *userp)
167 {
168         struct bti_curl_buffer *curl_buf = userp;
169         size_t buffer_size = size * nmemb;
170         char *temp;
171
172         if ((!buffer) || (!buffer_size) || (!curl_buf))
173                 return -EINVAL;
174
175         /* add to the data we already have */
176         temp = zalloc(curl_buf->length + buffer_size + 1);
177         if (!temp)
178                 return -ENOMEM;
179
180         memcpy(temp, curl_buf->data, curl_buf->length);
181         free(curl_buf->data);
182         curl_buf->data = temp;
183         memcpy(&curl_buf->data[curl_buf->length], (char *)buffer, buffer_size);
184         curl_buf->length += buffer_size;
185
186         dbg("%s\n", curl_buf->data);
187
188         return buffer_size;
189 }
190
191 static int send_tweet(struct session *session)
192 {
193         char user_password[500];
194         char data[500];
195         struct bti_curl_buffer *curl_buf;
196         CURL *curl = NULL;
197         CURLcode res;
198         struct curl_httppost *formpost = NULL;
199         struct curl_httppost *lastptr = NULL;
200         struct curl_slist *slist = NULL;
201
202         if (!session)
203                 return -EINVAL;
204
205         curl_buf = bti_curl_buffer_alloc();
206         if (!curl_buf)
207                 return -ENOMEM;
208
209         snprintf(user_password, sizeof(user_password), "%s:%s",
210                  session->account, session->password);
211         snprintf(data, sizeof(data), "status=\"%s\"", session->tweet);
212
213         curl = curl_init();
214         if (!curl)
215                 return -EINVAL;
216
217         curl_formadd(&formpost, &lastptr,
218                      CURLFORM_COPYNAME, "status",
219                      CURLFORM_COPYCONTENTS, session->tweet,
220                      CURLFORM_END);
221
222         curl_easy_setopt(curl, CURLOPT_HTTPPOST, formpost);
223
224         switch (session->host) {
225         case HOST_TWITTER:
226                 curl_easy_setopt(curl, CURLOPT_URL, twitter_url);
227                 /*
228                  * twitter doesn't like the "Expect: 100-continue" header
229                  * anymore, so turn it off.
230                  */
231                 slist = curl_slist_append(slist, "Expect:");
232                 curl_easy_setopt(curl, CURLOPT_HTTPHEADER, slist);
233                 break;
234         case HOST_IDENTICA:
235                 curl_easy_setopt(curl, CURLOPT_URL, identica_url);
236                 break;
237         }
238
239         if (session->proxy)
240                 curl_easy_setopt(curl, CURLOPT_PROXY, session->proxy);
241
242         if (debug)
243                 curl_easy_setopt(curl, CURLOPT_VERBOSE, 1);
244         curl_easy_setopt(curl, CURLOPT_USERPWD, user_password);
245
246         dbg("user_password = %s\n", user_password);
247         dbg("data = %s\n", data);
248         dbg("proxy = %s\n", session->proxy);
249
250         curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_callback);
251         curl_easy_setopt(curl, CURLOPT_WRITEDATA, curl_buf);
252         res = curl_easy_perform(curl);
253         if (res && !session->bash) {
254                 fprintf(stderr, "error(%d) trying to send tweet\n", res);
255                 return -EINVAL;
256         }
257
258         curl_easy_cleanup(curl);
259         curl_formfree(formpost);
260         bti_curl_buffer_free(curl_buf);
261         return 0;
262 }
263
264 static void parse_configfile(struct session *session)
265 {
266         FILE *config_file;
267         char *line = NULL;
268         size_t len = 0;
269         char *account = NULL;
270         char *password = NULL;
271         char *host = NULL;
272         char *proxy = NULL;
273         char *file;
274         char *home = getenv("HOME");
275
276         /* config file is ~/.bti  */
277         file = alloca(strlen(home) + 7);
278
279         sprintf(file, "%s/.bti", home);
280
281         config_file = fopen(file, "r");
282
283         /* No error if file does not exist or is unreadable.  */
284         if (config_file == NULL)
285                 return;
286
287         do {
288                 ssize_t n = getline(&line, &len, config_file);
289                 if (n < 0)
290                         break;
291                 if (line[n - 1] == '\n')
292                         line[n - 1] = '\0';
293                 /* Parse file.  Format is the usual value pairs:
294                    account=name
295                    passwort=value
296                    # is a comment character
297                 */
298                 *strchrnul(line, '#') = '\0';
299                 char *c = line;
300                 while (isspace(*c))
301                         c++;
302                 /* Ignore blank lines.  */
303                 if (c[0] == '\0')
304                         continue;
305
306                 if (!strncasecmp(c, "account", 7) && (c[7] == '=')) {
307                         c += 8;
308                         if (c[0] != '\0')
309                                 account = strdup(c);
310                 } else if (!strncasecmp(c, "password", 8) &&
311                            (c[8] == '=')) {
312                         c += 9;
313                         if (c[0] != '\0')
314                                 password = strdup(c);
315                 } else if (!strncasecmp(c, "host", 4) &&
316                            (c[4] == '=')) {
317                         c += 5;
318                         if (c[0] != '\0')
319                                 host = strdup(c);
320                 } else if (!strncasecmp(c, "proxy", 5) &&
321                            (c[5] == '=')) {
322                         c += 6;
323                         if (c[0] != '\0')
324                                 proxy = strdup(c);
325                 }
326         } while (!feof(config_file));
327
328         if (password)
329                 session->password = password;
330         if (account)
331                 session->account = account;
332         if (host) {
333                 if (strcasecmp(host, "twitter") == 0)
334                         session->host = HOST_TWITTER;
335                 if (strcasecmp(host, "identica") == 0)
336                         session->host = HOST_IDENTICA;
337                 free(host);
338         }
339         if (proxy) {
340                 if (session->proxy)
341                         free(session->proxy);
342                 session->proxy = proxy;
343         }
344
345         /* Free buffer and close file.  */
346         free(line);
347         fclose(config_file);
348 }
349
350 int main(int argc, char *argv[], char *envp[])
351 {
352         static const struct option options[] = {
353                 { "debug", 0, NULL, 'd' },
354                 { "account", 1, NULL, 'a' },
355                 { "password", 1, NULL, 'p' },
356                 { "host", 1, NULL, 'H' },
357                 { "proxy", 1, NULL, 'P' },
358                 { "help", 0, NULL, 'h' },
359                 { "bash", 0, NULL, 'b' },
360                 { "version", 0, NULL, 'v' },
361                 { }
362         };
363         struct session *session;
364         pid_t child;
365         char *tweet;
366         int retval;
367         int option;
368         char *http_proxy;
369 #if 0
370         char *home = getenv("HOME");
371         char *pwd = getenv("PWD");
372         char *dir;
373 #endif
374         session = session_alloc();
375         if (!session) {
376                 fprintf(stderr, "no more memory...\n");
377                 return -1;
378         }
379
380         curl_global_init(CURL_GLOBAL_ALL);
381
382         /* Set environment variables first, before reading command line options
383          * or config file values. */
384         http_proxy = getenv("http_proxy");
385         if (http_proxy) {
386                 if (session->proxy)
387                         free(session->proxy);
388                 session->proxy = strdup(http_proxy);
389                 dbg("http_proxy = %s\n", session->proxy);
390         }
391
392         parse_configfile(session);
393
394         while (1) {
395                 option = getopt_long_only(argc, argv, "dqe:p:P:H:a:h",
396                                           options, NULL);
397                 if (option == -1)
398                         break;
399                 switch (option) {
400                 case 'd':
401                         debug = 1;
402                         break;
403                 case 'a':
404                         if (session->account)
405                                 free(session->account);
406                         session->account = strdup(optarg);
407                         dbg("account = %s\n", session->account);
408                         break;
409                 case 'p':
410                         if (session->password)
411                                 free(session->password);
412                         session->password = strdup(optarg);
413                         dbg("password = %s\n", session->password);
414                         break;
415                 case 'P':
416                         if (session->proxy)
417                                 free(session->proxy);
418                         session->proxy = strdup(optarg);
419                         dbg("proxy = %s\n", session->proxy);
420                         break;
421                 case 'H':
422                         if (strcasecmp(optarg, "twitter") == 0)
423                                 session->host = HOST_TWITTER;
424                         if (strcasecmp(optarg, "identica") == 0)
425                                 session->host = HOST_IDENTICA;
426                         dbg("host = %d\n", session->host);
427                         break;
428                 case 'b':
429                         session->bash= 1;
430                         break;
431                 case 'h':
432                         display_help();
433                         goto exit;
434                 case 'v':
435                         display_version();
436                         goto exit;
437                 default:
438                         display_help();
439                         goto exit;
440                 }
441         }
442
443         if (!session->account) {
444                 fprintf(stdout, "Enter twitter account: ");
445                 session->account = get_string_from_stdin();
446         }
447
448         if (!session->password) {
449                 fprintf(stdout, "Enter twitter password: ");
450                 session->password = get_string_from_stdin();
451         }
452 #if 0
453         /* get the current working directory basename */
454         if (strcmp(pwd, home) == 0)
455                 dir = "~";
456         else {
457                 dir = strrchr(pwd, '/');
458                 if (dir)
459                         dir++;
460                 else
461                         dir = "?";
462         }
463 #endif
464         tweet = get_string_from_stdin();
465         if (!tweet || strlen(tweet) == 0) {
466                 dbg("no tweet?\n");
467                 return -1;
468         }
469
470 //      session->tweet = zalloc(strlen(tweet) + strlen(dir) + 10);
471         session->tweet = zalloc(strlen(tweet) + 10);
472
473         /* if --bash is specified, add the "PWD $ " to
474          * the start of the tweet. */
475         if (session->bash)
476 //              sprintf(session->tweet, "%s $ %s", dir, tweet);
477                 sprintf(session->tweet, "$ %s", tweet);
478         else
479                 sprintf(session->tweet, "%s", tweet);
480         free(tweet);
481
482         dbg("account = %s\n", session->account);
483         dbg("password = %s\n", session->password);
484         dbg("tweet = %s\n", session->tweet);
485         dbg("host = %d\n", session->host);
486
487         /* fork ourself so that the main shell can get on
488          * with it's life as we try to connect and handle everything
489          */
490         if (session->bash) {
491                 child = fork();
492                         if (child) {
493                         dbg("child is %d\n", child);
494                         exit(0);
495                 }
496         }
497
498         retval = send_tweet(session);
499         if (retval && !session->bash) {
500                 fprintf(stderr, "tweet failed\n");
501                 return -1;
502         }
503
504         session_free(session);
505 exit:
506         return 0;
507 }