Using Twitter API With C

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.

This will give you keys such as:

consumer key: xvz1evFS4wEEPTGEFPHBog
consumer secret: kAcSOqF21Fu85e7zjz7ZN2U4ZRhfV3WpwPAoE3Z7kBw

2. Obtain the access tokens

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.

In the end, you should have two keys like this:

access token: k370773112-GmHxMAgYyLbNEtIKZeRNFsMKPR9EyMZeS9weJAEb
access token secret: LswwdoUaIvS8ltyTt5jkRh4J50vUPVVHtR2YPi5kE

3. Percent-encode the status

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/* Does character c need percent-encoding or is it an unreserved character? */
static int need_percent_encoding(char c)
{
    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). */
static char *percent_encode(char *str)
{
    for (unsigned int i = 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. */
            char tmp[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] = '%';
        }
    }

    return str;
}

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
static char *base64_encode(const char *msg, size_t msg_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_t signature_len = msg_len * 4 * 3 + 1;

    char *encoded = malloc(signature_len);
    if (encoded == NULL) {
        return NULL;
    }

    /* From https://www.openssl.org/docs/crypto/BIO_f_base64.html */
    BIO *b64 = BIO_new(BIO_f_base64());
    if (b64 == NULL) {
        free(encoded);
        return NULL;
    }

    /* 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);
        return NULL;
    }

    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);

    return encoded;
}

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/* Generate nonce used as oauth_nonce. */
static char *generate_nonce(void)
{
    char nonce[32];

    if (!RAND_bytes((unsigned char *)nonce, sizeof(nonce))) {
        return NULL;
    }

    char *encoded_nonce = base64_encode(nonce, sizeof(nonce));
    if (encoded_nonce == NULL) {
        return NULL;
    }

    size_t nonce_len = strlen(encoded_nonce);
    for (size_t i = 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';
        }
    }

    return encoded_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):

1
2
3
time_t t = time(NULL);
char timestamp[11];
snprintf(timestamp, sizeof(timestamp), "%zu", (size_t)t);

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static unsigned char *hmac_sha1_encode(const char *data, const char *hmac_key,
                                       unsigned int *result_len)
{
    *result_len = 20; /* Should be always 20 bytes. */
    unsigned char *result = malloc(*result_len);
    if (result == NULL) {
        return NULL;
    }

    HMAC_CTX ctx;

    /* 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, (const unsigned char *)data, strlen(data));
    HMAC_Final(&ctx, result, result_len);
    HMAC_CTX_cleanup(&ctx);

    return result;
}

Now we are ready to build the signature:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/* Compute the signature from given parameters as required by the OAuth. */
static char *compute_signature(const char *timestamp, const char *nonce, const char *status,
                               const char *consumer_key, const char *auth_token,
                               const char *hmac_key)
{
    char *signature_base = malloc(buf_size);
    if (signature_base == NULL) {
        return NULL;
    }

    /* Encode the status again. */
    char encoded_status[strlen(status) * 3 + 1];
    snprintf(encoded_status, sizeof(encoded_status), "%s", status);
    percent_encode(encoded_status);

    int ret = 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);
        return NULL;
    }

    unsigned int encoded_len;
    char *hmac_encoded = (char *)hmac_sha1_encode(signature_base, hmac_key,
                                                  &encoded_len);
    free(signature_base);

    if (hmac_encoded == NULL) {
        return NULL;
    }

    char *encoded = base64_encode(hmac_encoded, encoded_len);
    free(hmac_encoded);

    if (encoded == NULL) {
        return NULL;
    }

    return percent_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:

kAcSOqF21Fu85e7zjz7ZN2U4ZRhfV3WpwPAoE3Z7kBw&LswwdoUaIvS8ltyTt5jkRh4J50vUPVVHtR2YPi5kE

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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
static char *create_post(const char *timestamp, const char *nonce, const char *status,
                         const char *signature, const char *consumer_key,
                         const char *auth_token)
{
    char *post = malloc(buf_size);
    if (post == NULL) {
        return NULL;
    }

    int ret = snprintf(post, buf_size, "POST /1.1/statuses/update.json HTTP/1.1\r\n"
                       "User-Agent: LightBot\r\n"
                       "Host: api.twitter.com\r\n"
                       "Content-Type: application/x-www-form-urlencoded\r\n"
                       "Authorization: OAuth oauth_consumer_key=\"%s\", oauth_nonce=\"%s\", "
                       "oauth_signature=\"%s\", oauth_signature_method=\"HMAC-SHA1\", "
                       "oauth_timestamp=\"%s\", oauth_token=\"%s\", oauth_version=\"1.0\"\r\n"
                       "Content-Length: %zu\r\n\r\n"
                       "status=%s",
                       consumer_key, nonce, signature, timestamp, auth_token,
                       strlen("status=") + strlen(status), status);
    if (ret < 0 || (size_t)ret >= buf_size) {
        free(post);
        return NULL;
    }

    return post;
}

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
/**
 * 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.
 */
static int send_to_twitter(const char *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;
    }

    int ret;

    char buf[1024] = { 0 };
    BIO_puts(bio, post);

    int read_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;
        } else if (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);

    return ret;
}

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.

Comments