I wanted once to implement simple twitter integration with one C application.
Even though everything can be found in https://dev.twitter.com, having no
experience in the API (or OAuth, or OpenSSL), it can take some time to get used
to. Here is a small guide of posting a tweet using C and OpenSSL.
1. Obtain the consumer tokens
First you’ll need consumer key and secret for your twitter application. This
will identify your application to twitter.
To receive keys for your application, you need to create it first at
https://apps.twitter.com. Application will start
with read-only permissions, which you want to change to read and write to be
able to post updates. That will require you to give your phone number to
Twitter.
These tokens gives your application a permission to post updates on behalf of a
twitter user. Basically the account needs to give permissions for an application
and let it post on its behalf.
There are a few ways to do this. It needs to be done only once per account and
application, so I skipped some corners here: I used Tweepy
which could generate the tokens quite easily. If you want to do this yourself,
PIN-based authentication would be the way to go.
If the account that you are posting with is the also the owner of the application,
you can also generate the access tokens directly from the application management.
Let’s say that you want to post Hello from Aatosjalo.com!. First the message
needs to be
percent-encoded. So all
reserved characters should be transformed to %<byte0><byte1>, e.g. ASCII
character (0x20) is percent-encoded to %20. In C, that could be something
like the following:
123456789101112131415161718192021222324252627
/* Does character c need percent-encoding or is it an unreserved character? */staticintneed_percent_encoding(charc){return!(isalnum((int)c)||c=='-'||c=='.'||c=='_'||c=='~');}/* Percent-encode (or URL encode) given string. There should be enough room in * the str (three times uncoded characters). */staticchar*percent_encode(char*str){for(unsignedinti=0;str[i]!='\0';i++){if(need_percent_encoding(str[i])){/* Make room for two characters. */memmove(str+i+3,str+i+1,strlen(str)-i+1);/* Write '%' and two bytes of which the character consists of. */chartmp[2];snprintf(tmp,2,"%X",str[i]>>4);str[i+1]=tmp[0];snprintf(tmp,2,"%X",str[i]&0xf);str[i+2]=tmp[0];str[i]='%';}}returnstr;}
Then you should have your status something like:
status: Hello%20from%20Aatosjalo.com%21
4. Learn Base 64 Encoding
Twitter expects a certain signature with each status update that needs to be
Base 64 encoded. I’m no expert
with OpenSSL, but following a few examples I came up with:
staticchar*base64_encode(constchar*msg,size_tmsg_len){/* This could be improved. Currently it allocates bit too much. Times three * is required for the percent encoding but times four is just overestimate * of base64 encoding. */size_tsignature_len=msg_len*4*3+1;char*encoded=malloc(signature_len);if(encoded==NULL){returnNULL;}/* From https://www.openssl.org/docs/crypto/BIO_f_base64.html */BIO*b64=BIO_new(BIO_f_base64());if(b64==NULL){free(encoded);returnNULL;}/* Bio base64 writes to a stream for some reason, so open a stream to the * buffer. */FILE*stream=fmemopen(encoded,signature_len,"w");if(stream==NULL){free(encoded);BIO_free_all(b64);returnNULL;}BIO*bio=BIO_new_fp(stream,BIO_NOCLOSE);BIO_push(b64,bio);BIO_set_flags(b64,BIO_FLAGS_BASE64_NO_NL);BIO_write(b64,msg,(int)msg_len);BIO_flush(b64);BIO_free_all(b64);fclose(stream);returnencoded;}
5. Create the signature
Twitter users OAuth and HTTPS for its APIs. This is already documented quite
nicely in
Creating Signatures,
so I won’t repeat everything here.
First you’ll need a nonce that is used in the signature. It should be relatively
random and you shouldn’t use a nonce twice (even for prototyping). If you are
using OpenSSL, you could do something like this:
12345678910111213141516171819202122232425
/* Generate nonce used as oauth_nonce. */staticchar*generate_nonce(void){charnonce[32];if(!RAND_bytes((unsignedchar*)nonce,sizeof(nonce))){returnNULL;}char*encoded_nonce=base64_encode(nonce,sizeof(nonce));if(encoded_nonce==NULL){returnNULL;}size_tnonce_len=strlen(encoded_nonce);for(size_ti=0;i<nonce_len;i++){if(!isalnum((int)encoded_nonce[i])){/* Replace non-alphanumerics by arbitrary 'a' since they should not * be present in the nonce. */encoded_nonce[i]='a';}}returnencoded_nonce;}
Nonce shouldn’t have any non-alphanumeric characters, but maybe you can figure out nicer
way to get rid of those yourself.
Signature also expects a timestamp, but that can be retrieved quite easily with
time(2):
Now we should have all the parameters for the signature. Next we have to hash
it with
HMAC-SHA1.
One simple implementation of that using OpenSSL would be:
1234567891011121314151617181920
staticunsignedchar*hmac_sha1_encode(constchar*data,constchar*hmac_key,unsignedint*result_len){*result_len=20;/* Should be always 20 bytes. */unsignedchar*result=malloc(*result_len);if(result==NULL){returnNULL;}HMAC_CTXctx;/* Example from http://stackoverflow.com/a/245335. */HMAC_CTX_init(&ctx);HMAC_Init_ex(&ctx,hmac_key,(int)strlen(hmac_key),EVP_sha1(),NULL);HMAC_Update(&ctx,(constunsignedchar*)data,strlen(data));HMAC_Final(&ctx,result,result_len);HMAC_CTX_cleanup(&ctx);returnresult;}
/* Compute the signature from given parameters as required by the OAuth. */staticchar*compute_signature(constchar*timestamp,constchar*nonce,constchar*status,constchar*consumer_key,constchar*auth_token,constchar*hmac_key){char*signature_base=malloc(buf_size);if(signature_base==NULL){returnNULL;}/* Encode the status again. */charencoded_status[strlen(status)*3+1];snprintf(encoded_status,sizeof(encoded_status),"%s",status);percent_encode(encoded_status);intret=snprintf(signature_base,buf_size,"POST&https%%3A%%2F%%2Fapi.twitter.com""%%2F1.1%%2Fstatuses%%2Fupdate.json&""oauth_consumer_key%%3D%s%%26oauth_nonce%%3D%s""%%26oauth_signature_method%%3DHMAC-SHA1""%%26oauth_timestamp%%3D%s%%26oauth_token%%3D%s""%%26oauth_version%%3D1.0""%%26status%%3D%s",consumer_key,nonce,timestamp,auth_token,encoded_status);if(ret<0||(size_t)ret>buf_size){free(signature_base);returnNULL;}unsignedintencoded_len;char*hmac_encoded=(char*)hmac_sha1_encode(signature_base,hmac_key,&encoded_len);free(signature_base);if(hmac_encoded==NULL){returnNULL;}char*encoded=base64_encode(hmac_encoded,encoded_len);free(hmac_encoded);if(encoded==NULL){returnNULL;}returnpercent_encode(encoded);}
The parameters should be quite self-explanatory expect the HMAC key. HMAC key is
formed from the consumer secret and access token secret by concatenating them with
&. For example, HMAC key in our example would be simply:
You should note that parameter status is already percent-encoded once, but for
the signature it should be encoded again.
After these, we need to build our signature_base. It should have the HTTP
request method (here POST), URL that we are going to use
(https://api.twitter.com/1.1/statuses/update.json) and then the parameters
(extra % comes from escaping percent in printf). For more information, see
Creating Signatures.
Now we should have everything and we can do the HMAC-SHA1 hashing. The hash
should finally be base64- and percent-encoded and then we have our signature that
we will use in our HTTP POST.
6. Create the POST
Next we have to create the POST, but we should now have all parameters for that
so it should be easy.
We are posting a status update and the API documentation for that can be found
here. In short,
the URL is https://api.twitter.com/1.1/statuses/update.json and the only
required parameter is status. Example of updating status can be found
here.
The main ‘new’ thing here is the Authorization parameter which is required by the
OAuth. Otherwise it’s quite straightforward: HTTP method, URL, protocol version,
user agent (which is just the name of the application) etc.
7. Sending
Only thing left is to open a SSL connection to twitter, send the POST and check
the result. A crude example of doing so:
/** * Posts given POST to api.twitter.com. * * From https://thunked.org/programming/openssl-tutorial-client-t11.html and * example in https://www.openssl.org/docs/crypto/BIO_f_ssl.html. * * This doesn't verify the server certificate, i.e. it will accept certificates * signed by any CA. */staticintsend_to_twitter(constchar*post){SSL_CTX*ctx=SSL_CTX_new(SSLv23_client_method());if(ctx==NULL){printf("Error creating SSL_CTX\n");return-1;}BIO*bio=BIO_new_ssl_connect(ctx);if(bio==NULL){printf("Error creating BIO!\n");SSL_CTX_free(ctx);return-1;}SSL*ssl;BIO_get_ssl(bio,&ssl);if(ssl==NULL){printf("BIO_get_ssl failed\n");BIO_free_all(bio);SSL_CTX_free(ctx);return-1;}SSL_set_mode(ssl,SSL_MODE_AUTO_RETRY);BIO_set_conn_hostname(bio,"api.twitter.com:https");if(BIO_do_connect(bio)<=0){printf("Failed to connect!");BIO_free_all(bio);SSL_CTX_free(ctx);return-1;}if(BIO_do_handshake(bio)<=0){printf("Failed to do SSL handshake!");BIO_free_all(bio);SSL_CTX_free(ctx);return-1;}intret;charbuf[1024]={0};BIO_puts(bio,post);intread_bytes=BIO_read(bio,buf,sizeof(buf)-1);if(read_bytes>0){buf[read_bytes]='\0';if(strstr(buf,"HTTP/1.1 200 OK")!=NULL){ret=0;}elseif(strstr(buf,"HTTP/1.1 403 Forbidden")!=NULL){/* Twitter doesn't allow consecutive duplicates and will respond * with 403 in such case. */printf("Twitter responded with 403!\n");ret=-2;}else{printf("Error occurred! Received:\n""%s\n",buf);ret=-1;}}else{printf("Read failed!\n");ret=-1;}BIO_free_all(bio);SSL_CTX_free(ctx);returnret;}
Interesting return codes here are 200 and 403. Former will be sent if the update
was successful, but the latter will be sent if we have reached rate limit or send
a duplicate update as seen in the description of the API.
Now you should have your tweet posted on your account! As always, the full code
can be found here.