]> git.piffa.net Git - arduino/blob - books/pdummies/Libraries/Twitter/Twitter.cpp
first commit
[arduino] / books / pdummies / Libraries / Twitter / Twitter.cpp
1 /*
2  * Twitter.cpp
3  *
4  * Author: Markku Rossi <mtr@iki.fi>
5  *
6  * Copyright (c) 2011-2012 Markku Rossi
7  *
8  * This program is free software: you can redistribute it and/or
9  * modify it under the terms of the GNU General Public License as
10  * published by the Free Software Foundation, either version 3 of the
11  * License, or (at your option) any later version.
12  *
13  * This program is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
16  * General Public License for more details.
17  *
18  * You should have received a copy of the GNU General Public License
19  * along with this program.  If not, see
20  * <http://www.gnu.org/licenses/>.
21  *
22  */
23
24 #include "Twitter.h"
25
26 const static char hex_table[] PROGMEM = "0123456789ABCDEF";
27 const static char base64_table[] PROGMEM
28 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
29
30 const static char s_jan[] PROGMEM = "Jan";
31 const static char s_feb[] PROGMEM = "Feb";
32 const static char s_mar[] PROGMEM = "Mar";
33 const static char s_apr[] PROGMEM = "Apr";
34 const static char s_may[] PROGMEM = "May";
35 const static char s_jun[] PROGMEM = "Jun";
36 const static char s_jul[] PROGMEM = "Jul";
37 const static char s_aug[] PROGMEM = "Aug";
38 const static char s_sep[] PROGMEM = "Sep";
39 const static char s_oct[] PROGMEM = "Oct";
40 const static char s_nov[] PROGMEM = "Nov";
41 const static char s_dec[] PROGMEM = "Dec";
42
43 const static char *months[] PROGMEM =
44   {
45     s_jan, s_feb, s_mar, s_apr, s_may, s_jun,
46     s_jul, s_aug, s_sep, s_oct, s_nov, s_dec,
47   };
48
49 Twitter::Twitter(char *buffer, size_t buffer_len)
50   : basetime(0L),
51     last_millis(0L),
52     timestamp(0),
53     buffer(buffer),
54     buffer_len(buffer_len),
55     server(0),
56     uri(0),
57     port(0)
58 {
59 }
60
61 void
62 Twitter::set_twitter_endpoint(const prog_char server[], const prog_char uri[],
63                               IPAddress ip, uint16_t port, bool proxy)
64 {
65   this->server = server;
66   this->uri = uri;
67   this->ip = ip;
68   this->port = port;
69
70   this->proxy = proxy ? 1 : 0;
71 }
72
73 void
74 Twitter::set_client_id(const prog_char consumer_key[],
75                        const prog_char consumer_secret[])
76 {
77   this->consumer_key = consumer_key;
78   this->consumer_secret = consumer_secret;
79 }
80
81 void
82 Twitter::set_account_id(const prog_char access_token[],
83                         const prog_char token_secret[])
84 {
85   this->access_token.pgm = access_token;
86   this->token_secret.pgm = token_secret;
87
88   this->access_token_pgm = 1;
89 }
90
91 void
92 Twitter::set_account_id(int access_token, int token_secret)
93 {
94   this->access_token.eeprom = access_token;
95   this->token_secret.eeprom = token_secret;
96
97   this->access_token_pgm = 0;
98 }
99
100 bool
101 Twitter::is_ready(void)
102 {
103   if (basetime)
104     return true;
105
106   return query_time();
107 }
108
109 unsigned long
110 Twitter::get_time(void)
111 {
112   unsigned long now = millis();
113
114   if (now < last_millis)
115     /* Our internal clock wrapped around. */
116     basetime += 0xffffffffL / 1000L;
117
118   last_millis = now;
119
120   return basetime + now / 1000L;
121 }
122
123 bool
124 Twitter::query_time(void)
125 {
126   EthernetClient http;
127
128   if (!http.connect(ip, port))
129     {
130       println(PSTR("query_time: could not connect to server"));
131       return false;
132     }
133
134   http_print(&http, PSTR("HEAD "));
135
136   if (proxy)
137     {
138       http_print(&http, PSTR("http://"));
139       http_print(&http, server);
140     }
141
142   http_println(&http, PSTR("/ HTTP/1.1"));
143
144   http_print(&http, PSTR("Host: "));
145   http_print(&http, server);
146   http_newline(&http);
147
148   http_println(&http, PSTR("Connection: close"));
149
150   http_newline(&http);
151
152   while (http.connected())
153     {
154       if (!read_line(&http, buffer, buffer_len))
155         break;
156
157       if (buffer[0] == '\0')
158         break;
159
160       if (process_date_header(buffer))
161         break;
162     }
163
164   http.stop();
165
166   return basetime != 0L;
167 }
168
169 bool
170 Twitter::process_date_header(char *buffer)
171 {
172   if ((buffer[0] != 'D' && buffer[0] != 'd')
173       || (buffer[1] != 'A' && buffer[1] != 'a')
174       || (buffer[2] != 'T' && buffer[2] != 't')
175       || (buffer[3] != 'E' && buffer[3] != 'e')
176       || (buffer[4] != ':'))
177     return false;
178
179   unsigned long now = parse_date(buffer + 5);
180
181   if (now != 0)
182     {
183       /* We managed to parse the date header, now update system
184          basetime. */
185       last_millis = millis();
186       basetime = now - last_millis / 1000L;
187     }
188
189   return true;
190 }
191
192 long
193 Twitter::parse_date(char *date)
194 {
195   tmElements_t tm;
196   char *cp;
197   char *end;
198
199   memset(&tm, 0, sizeof(tm));
200
201   /* We could use sscanf to parse the date string but it adds 26 bytes
202      to our SRAM consumption so let's use this adhoc parser. */
203
204   for (cp = date; *cp && *cp != ','; cp++)
205     ;
206
207   if (*cp != ',')
208     return 0;
209
210   tm.Day = strtol(++cp, &end, 10);
211
212   if (end == cp)
213     return 0;
214
215   cp = end;
216
217   for (; *cp && *cp == ' '; cp++)
218     ;
219   if (!*cp)
220     return 0;
221
222   tm.Month = parse_month(cp, &end);
223   if (tm.Month <= 0)
224     return 0;
225
226   cp = end + 1;
227
228   tm.Year = strtol(cp, &end, 10) - 1970;
229
230   if (end == cp)
231     return 0;
232
233   cp = end + 1;
234
235   tm.Hour = strtol(cp, &end, 10);
236
237   if (end == cp)
238     return 0;
239
240   cp = end + 1;
241
242   tm.Minute = strtol(cp, &end, 10);
243
244   if (end == cp)
245     return 0;
246
247   cp = end + 1;
248
249   tm.Second = strtol(cp, &end, 10);
250
251   if (end == cp)
252     return 0;
253
254   return makeTime(tm);
255 }
256
257 int
258 Twitter::parse_month(char *str, char **end)
259 {
260   char *cp;
261   int i;
262
263   for (cp = str; *cp && *cp != ' '; cp++)
264     ;
265
266   if (!*cp)
267     return 0;
268
269   *cp = '\0';
270
271   *end = cp;
272
273   for (i = 0; i < 12; i++)
274     if (strcmp_P(str, (char *) pgm_read_word(&(months[i]))) == 0)
275       return i + 1;
276
277   return 0;
278 }
279
280 bool
281 Twitter::post_status(const char *message)
282 {
283   char *cp;
284   int i;
285
286   timestamp = get_time();
287   create_nonce();
288
289   compute_authorization(message);
290
291   /* Post message to twitter. */
292
293   EthernetClient http;
294
295   if (!http.connect(ip, port))
296     {
297       println(PSTR("Could not connect to server"));
298       return false;
299     }
300
301   http_print(&http, PSTR("POST "));
302
303   if (proxy)
304     {
305       http_print(&http, PSTR("http://"));
306       http_print(&http, server);
307     }
308
309   http_print(&http, uri);
310   http_println(&http, PSTR(" HTTP/1.1"));
311
312   http_print(&http, PSTR("Host: "));
313   http_print(&http, server);
314   http_newline(&http);
315
316   http_println(&http,
317                PSTR("Content-Type: application/x-www-form-urlencoded"));
318   http_println(&http, PSTR("Connection: close"));
319
320   /* Authorization header. */
321   http_print(&http, PSTR("Authorization: OAuth oauth_consumer_key=\""));
322
323   url_encode_pgm(buffer, consumer_key);
324   http.write(buffer);
325
326   http_print(&http, PSTR("\",oauth_signature_method=\"HMAC-SHA1"));
327   http_print(&http, PSTR("\",oauth_timestamp=\""));
328
329   sprintf(buffer, "%ld", timestamp);
330   http.write(buffer);
331
332   http_print(&http, PSTR("\",oauth_nonce=\""));
333
334   hex_encode(buffer, nonce, sizeof(nonce));
335   http.write(buffer);
336
337   http_print(&http, PSTR("\",oauth_version=\"1.0\",oauth_token=\""));
338
339   if (access_token_pgm)
340     url_encode_pgm(buffer, access_token.pgm);
341   else
342     url_encode_eeprom(buffer, access_token.eeprom);
343
344   http.write(buffer);
345
346   http_print(&http, PSTR("\",oauth_signature=\""));
347
348   cp = base64_encode(buffer, signature, HASH_LENGTH);
349   url_encode(cp + 1, buffer);
350
351   http.write(cp + 1);
352
353   http_println(&http, PSTR("\""));
354
355   /* Encode content. */
356   cp = url_encode(buffer, "status");
357   *cp++ = '=';
358   cp = url_encode(cp, message);
359
360   int content_length = cp - buffer;
361   sprintf(cp + 1, "%d", content_length);
362
363   http_print(&http, PSTR("Content-Length: "));
364   http.write(cp + 1);
365   http_newline(&http);
366
367   /* Header-body separator. */
368   http_newline(&http);
369
370   /* And finally content. */
371   http.write(buffer);
372
373   /* Read response status line. */
374   if (!read_line(&http, buffer, buffer_len) || buffer[0] == '\0')
375     {
376       http.stop();
377       return false;
378     }
379
380   int response_code;
381
382   /* HTTP/1.1 200 Success */
383   for (i = 0; buffer[i] && buffer[i] != ' '; i++)
384     ;
385   if (buffer[i])
386     response_code = atoi(buffer + i + 1);
387   else
388     response_code = 0;
389
390   bool success = (200 <= response_code && response_code < 300);
391
392   if (!success)
393     Serial.println(buffer);
394
395   /* Skip header. */
396   while (true)
397     {
398       if (!read_line(&http, buffer, buffer_len))
399         {
400           http.stop();
401           return false;
402         }
403
404       if (buffer[0] == '\0')
405         break;
406
407       /* Update our system basetime from the response `Date'
408          header. */
409       process_date_header(buffer);
410     }
411
412   /* Handle content. */
413   while (http.connected())
414     {
415       while (http.available() > 0)
416         {
417           uint8_t byte = http.read();
418
419           if (!success)
420             Serial.write(byte);
421         }
422       delay(100);
423     }
424
425   http.stop();
426
427   if (!success)
428     println(PSTR(""));
429
430   return success;
431 }
432
433 char *
434 Twitter::url_encode(char *buffer, char ch)
435 {
436   if ('0' <= ch && ch <= '9'
437       || 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z'
438       || ch == '-' || ch == '.' || ch == '_' || ch == '~')
439     {
440       *buffer++ = ch;
441     }
442   else
443     {
444       int val = ((int) ch) & 0xff;
445
446       snprintf(buffer, 4, "%%%02X", val);
447       buffer += 3;
448     }
449
450   *buffer = '\0';
451
452   return buffer;
453 }
454
455 char *
456 Twitter::url_encode(char *buffer, const char *data)
457 {
458   char ch;
459
460   while ((ch = *data++))
461     buffer = url_encode(buffer, ch);
462
463   return buffer;
464 }
465
466 char *
467 Twitter::url_encode_pgm(char *buffer, const prog_char data[])
468 {
469   char ch;
470
471   while ((ch = pgm_read_byte(data++)))
472     buffer = url_encode(buffer, ch);
473
474   return buffer;
475 }
476
477 char *
478 Twitter::url_encode_eeprom(char *buffer, int address)
479 {
480   char ch;
481
482   while ((ch = EEPROM.read(address++)))
483     buffer = url_encode(buffer, ch);
484
485   return buffer;
486 }
487
488 char *
489 Twitter::hex_encode(char *buffer, const uint8_t *data, size_t data_len)
490 {
491   size_t i;
492
493   for (i = 0; i < data_len; i++)
494     {
495       *buffer++ = (char) pgm_read_byte(hex_table + (data[i] >> 4));
496       *buffer++ = (char) pgm_read_byte(hex_table + (data[i] & 0x0f));
497     }
498
499   *buffer = '\0';
500
501   return buffer;
502 }
503
504 char *
505 Twitter::base64_encode(char *buffer, const uint8_t *data, size_t data_len)
506 {
507   int i, len;
508
509   for (i = 0, len = data_len; len > 0; i += 3, len -= 3)
510     {
511       unsigned long val;
512
513       val = (unsigned long) data[i] << 16;
514       if (len > 1)
515         val |= (unsigned long) data[i + 1] << 8;
516       if (len > 2)
517         val |= data[i + 2];
518
519       *buffer++ = (char) pgm_read_byte(base64_table + (val >> 18));
520       *buffer++ = (char) pgm_read_byte(base64_table + ((val >> 12) & 0x3f));
521       *buffer++ = (char) pgm_read_byte(base64_table + ((val >> 6) & 0x3f));
522       *buffer++ = (char) pgm_read_byte(base64_table + (val & 0x3f));
523     }
524
525   for (; len < 0; len++)
526     buffer[len] = '=';
527
528   *buffer = '\0';
529
530   return buffer;
531 }
532
533 void
534 Twitter::create_nonce(void)
535 {
536   int i;
537
538   /* Nonce must be unique for the request timestamp value, so let's
539      use the timestamp as our random seed. */
540   srandom(timestamp);
541
542   for (i = 0; i < sizeof(nonce); i++)
543     nonce[i] = (uint8_t) random();
544 }
545
546 void
547 Twitter::compute_authorization(const char *message)
548 {
549   char *cp = buffer;
550
551   /* Compute key and init HMAC. */
552
553   cp = url_encode_pgm(buffer, consumer_secret);
554   *cp++ = '&';
555
556   if (access_token_pgm)
557     cp = url_encode_pgm(cp, token_secret.pgm);
558   else
559     cp = url_encode_eeprom(cp, token_secret.eeprom);
560
561   Sha1.initHmac((uint8_t *) buffer, cp - buffer);
562
563   auth_add_pgm(PSTR("POST&http%3A%2F%2F"));
564   auth_add_pgm(server);
565
566   url_encode_pgm(buffer, uri);
567   auth_add(buffer);
568
569   auth_add('&');
570
571   auth_add_pgm(PSTR("oauth_consumer_key"));
572   auth_add_value_separator();
573   url_encode_pgm(buffer, consumer_key);
574   auth_add(buffer);
575
576   cp = hex_encode(buffer, nonce, sizeof(nonce));
577
578   auth_add_param(PSTR("oauth_nonce"), buffer, cp + 1);
579
580   auth_add_param(PSTR("oauth_signature_method"), "HMAC-SHA1", buffer);
581
582   sprintf(buffer, "%ld", timestamp);
583   auth_add_param(PSTR("oauth_timestamp"), buffer, cp + 1);
584
585   auth_add_param_separator();
586
587   auth_add_pgm(PSTR("oauth_token"));
588   auth_add_value_separator();
589   if (access_token_pgm)
590     url_encode_pgm(buffer, access_token.pgm);
591   else
592     url_encode_eeprom(buffer, access_token.eeprom);
593   auth_add(buffer);
594
595   auth_add_param(PSTR("oauth_version"), "1.0", buffer);
596
597   cp = url_encode(buffer, message);
598   auth_add_param(PSTR("status"), buffer, cp + 1);
599
600   signature = Sha1.resultHmac();
601 }
602
603 void
604 Twitter::auth_add(char ch)
605 {
606   Sha1.write(ch);
607 }
608
609 void
610 Twitter::auth_add(const char *str)
611 {
612   Sha1.print(str);
613 }
614
615 void
616 Twitter::auth_add_pgm(const prog_char str[])
617 {
618   uint8_t c;
619
620   while ((c = pgm_read_byte(str++)))
621     Sha1.write(c);
622 }
623
624 void
625 Twitter::auth_add_param(const prog_char key[], const char *value, char *workbuf)
626 {
627   /* Add separator.  We know that this method is not used to add the
628      first parameter. */
629   auth_add_param_separator();
630
631   auth_add_pgm(key);
632
633   auth_add_value_separator();
634
635   url_encode(workbuf, value);
636   auth_add(workbuf);
637 }
638
639 void
640 Twitter::auth_add_param_separator(void)
641 {
642   auth_add_pgm(PSTR("%26"));
643 }
644
645 void
646 Twitter::auth_add_value_separator(void)
647 {
648   auth_add_pgm(PSTR("%3D"));
649 }
650
651 void
652 Twitter::http_print(Client *client, const prog_char str[])
653 {
654   uint8_t c;
655
656   if (!client || !str)
657     return;
658
659   while ((c = pgm_read_byte(str++)))
660     client->write(c);
661 }
662
663 void
664 Twitter::http_println(Client *client, const prog_char str[])
665 {
666   http_print(client, str);
667   http_newline(client);
668 }
669
670 void
671 Twitter::http_newline(Client *client)
672 {
673   client->write('\r');
674   client->write('\n');
675 }
676
677 void
678 Twitter::println(const prog_char str[])
679 {
680   uint8_t c;
681
682   if (!str)
683     return;
684
685   while ((c = pgm_read_byte(str++)))
686     Serial.write(c);
687
688   Serial.write('\r');
689   Serial.write('\n');
690 }
691
692 bool
693 Twitter::read_line(Client *client, char *buffer, size_t buflen)
694 {
695   size_t pos = 0;
696
697   while (client->connected())
698     {
699       while (client->available() > 0)
700         {
701           uint8_t byte = client->read();
702
703           if (byte == '\n')
704             {
705               /* EOF found. */
706               if (pos < buflen)
707                 {
708                   if (pos > 0 && buffer[pos - 1] == '\r')
709                     pos--;
710
711                   buffer[pos] = '\0';
712                 }
713               else
714                 {
715                   buffer[buflen - 1] = '\0';
716                 }
717
718               return true;
719             }
720
721           if (pos < buflen)
722             buffer[pos++] = byte;
723         }
724
725       delay(100);
726     }
727
728   return false;
729 }