1 /* oauth.c -- OAuth 2.0 implementation for XOAUTH2 in SMTP and POP3.
2  *
3  * This code is Copyright (c) 2014, by the authors of nmh.  See the
4  * COPYRIGHT file in the root directory of the nmh distribution for
5  * complete copyright information.
6  */
7 
8 #include <h/mh.h>
9 
10 #ifdef OAUTH_SUPPORT
11 
12 #include <sys/stat.h>
13 
14 #include <stdarg.h>
15 #include <stdio.h>
16 #include <stdlib.h>
17 #include <string.h>
18 #include <strings.h>
19 #include <time.h>
20 #include <unistd.h>
21 
22 #include <curl/curl.h>
23 #include <thirdparty/jsmn/jsmn.h>
24 
25 #include <h/oauth.h>
26 #include <h/utils.h>
27 #include "lock_file.h"
28 
29 #define JSON_TYPE "application/json"
30 
31 /* We pretend access tokens expire 60 seconds earlier than they actually do to
32  * allow for separate processes to use and refresh access tokens.  The process
33  * that uses the access token (post) has an error if the token is expired; the
34  * process that refreshes the access token (send) must have already refreshed if
35  * the expiration is close.
36  *
37  * 60s is arbitrary, and hopefully is enough to allow for clock skew.
38  * Currently only Gmail supports XOAUTH2, and seems to always use a token
39  * life-time of 3600s, but that is not guaranteed.  It is possible for Gmail to
40  * issue an access token with a life-time so short that even after send
41  * refreshes it, it's already expired when post tries to use it, but that seems
42  * unlikely. */
43 #define EXPIRY_FUDGE 60
44 
45 /* maximum size for HTTP response bodies
46  * (not counting header and not null-terminated) */
47 #define RESPONSE_BODY_MAX 8192
48 
49 /* Maximum size for URLs and URI-encoded query strings, null-terminated.
50  *
51  * Actual maximum we need is based on the size of tokens (limited by
52  * RESPONSE_BODY_MAX), code user copies from a web page (arbitrarily large), and
53  * various service parameters (all arbitrarily large).  In practice, all these
54  * are just tens of bytes.  It's not hard to change this to realloc as needed,
55  * but we should still have some limit, so why not this one?
56  */
57 #define URL_MAX 8192
58 
59 struct mh_oauth_cred {
60     mh_oauth_ctx *ctx;
61 
62     /* opaque access token ([1] 1.4) in null-terminated string */
63     char *access_token;
64     /* opaque refresh token ([1] 1.5) in null-terminated string */
65     char *refresh_token;
66 
67     /* time at which the access token expires, or 0 if unknown */
68     time_t expires_at;
69 
70     /* Ignoring token_type ([1] 7.1) because
71      * https://developers.google.com/accounts/docs/OAuth2InstalledApp says
72      * "Currently, this field always has the value Bearer". */
73 
74     /* only filled while loading cred files, otherwise NULL */
75     char *user;
76 };
77 
78 struct mh_oauth_ctx {
79     struct mh_oauth_service_info svc;
80     CURL *curl;
81     FILE *log;
82 
83     char buf[URL_MAX];
84 
85     char *cred_fn;
86     char *sasl_client_res;
87     char *user_agent;
88 
89     mh_oauth_err_code err_code;
90 
91     /* If any detailed message about the error is available, this points to it.
92      * May point to err_buf, or something else. */
93     const char *err_details;
94 
95     /* Pointer to buffer mh_oauth_err_get_string allocates. */
96     char *err_formatted;
97 
98     /* Ask libcurl to store errors here. */
99     char err_buf[CURL_ERROR_SIZE];
100 };
101 
102 struct curl_ctx {
103     /* inputs */
104 
105     CURL *curl;
106     /* NULL or a file handle to have curl log diagnostics to */
107     FILE *log;
108 
109     /* outputs */
110 
111     /* Whether the response was too big; if so, the rest of the output fields
112      * are undefined. */
113     boolean too_big;
114 
115     /* HTTP response code */
116     long res_code;
117 
118     /* NULL or null-terminated value of Content-Type response header field */
119     const char *content_type;
120 
121     /* number of bytes in the response body */
122     size_t res_len;
123 
124     /* response body; NOT null-terminated */
125     char res_body[RESPONSE_BODY_MAX];
126 };
127 
128 static boolean get_json_strings(const char *, size_t, FILE *, ...);
129 static boolean make_query_url(char *, size_t, CURL *, const char *, ...);
130 static boolean post(struct curl_ctx *, const char *, const char *);
131 
132 int
mh_oauth_do_xoauth(const char * user,const char * svc,unsigned char ** oauth_res,size_t * oauth_res_len,FILE * log)133 mh_oauth_do_xoauth(const char *user, const char *svc, unsigned char **oauth_res,
134 		   size_t *oauth_res_len, FILE *log)
135 {
136     mh_oauth_ctx *ctx;
137     mh_oauth_cred *cred;
138     char *fn;
139     int failed_to_lock = 0;
140     FILE *fp;
141     char *client_res;
142 
143     if (!mh_oauth_new (&ctx, svc)) adios(NULL, mh_oauth_get_err_string(ctx));
144 
145     if (log != NULL) mh_oauth_log_to(stderr, ctx);
146 
147     fn = mh_xstrdup(mh_oauth_cred_fn(svc));
148     fp = lkfopendata(fn, "r+", &failed_to_lock);
149     if (fp == NULL) {
150         if (errno == ENOENT) {
151             adios(NULL, "no credentials -- run mhlogin -saslmech xoauth2 -authservice %s", svc);
152         }
153         adios(fn, "failed to open");
154     }
155     if (failed_to_lock) {
156         adios(fn, "failed to lock");
157     }
158 
159     if ((cred = mh_oauth_cred_load(fp, ctx, user)) == NULL) {
160         adios(NULL, mh_oauth_get_err_string(ctx));
161     }
162 
163     if (!mh_oauth_access_token_valid(time(NULL), cred)) {
164         if (!mh_oauth_refresh(cred)) {
165             if (mh_oauth_get_err_code(ctx) == MH_OAUTH_NO_REFRESH) {
166                 adios(NULL, "no valid credentials -- run mhlogin -saslmech xoauth2 -authservice %s", svc);
167             }
168             if (mh_oauth_get_err_code(ctx) == MH_OAUTH_BAD_GRANT) {
169                 adios(NULL, "credentials rejected -- run mhlogin -saslmech xoauth2 -authservice %s", svc);
170             }
171             inform("error refreshing OAuth2 token");
172             adios(NULL, mh_oauth_get_err_string(ctx));
173         }
174 
175         fseek(fp, 0, SEEK_SET);
176         if (!mh_oauth_cred_save(fp, cred, user)) {
177             adios(NULL, mh_oauth_get_err_string(ctx));
178         }
179     }
180 
181     if (lkfclosedata(fp, fn) < 0) {
182         adios(fn, "failed to close");
183     }
184     free(fn);
185 
186     /* XXX writeBase64raw modifies the source buffer!  make a copy */
187     client_res = mh_xstrdup(mh_oauth_sasl_client_response(oauth_res_len, user,
188                                                       cred));
189     mh_oauth_cred_free(cred);
190     mh_oauth_free(ctx);
191 
192     *oauth_res = (unsigned char *) client_res;
193 
194     return OK;
195 }
196 
197 static boolean
is_json(const char * content_type)198 is_json(const char *content_type)
199 {
200     return content_type != NULL
201         && strncasecmp(content_type, JSON_TYPE, LEN(JSON_TYPE)) == 0;
202 }
203 
204 static void
set_err_details(mh_oauth_ctx * ctx,mh_oauth_err_code code,const char * details)205 set_err_details(mh_oauth_ctx *ctx, mh_oauth_err_code code, const char *details)
206 {
207     ctx->err_code = code;
208     ctx->err_details = details;
209 }
210 
211 static void
set_err(mh_oauth_ctx * ctx,mh_oauth_err_code code)212 set_err(mh_oauth_ctx *ctx, mh_oauth_err_code code)
213 {
214     set_err_details(ctx, code, NULL);
215 }
216 
217 static void
set_err_http(mh_oauth_ctx * ctx,const struct curl_ctx * curl_ctx)218 set_err_http(mh_oauth_ctx *ctx, const struct curl_ctx *curl_ctx)
219 {
220     char *error = NULL;
221     mh_oauth_err_code code;
222     /* 5.2. Error Response says error response should use status code 400 and
223      * application/json body.  If Content-Type matches, try to parse the body
224      * regardless of the status code. */
225     if (curl_ctx->res_len > 0
226         && is_json(curl_ctx->content_type)
227         && get_json_strings(curl_ctx->res_body, curl_ctx->res_len, ctx->log,
228                             "error", &error, (void *)NULL)
229         && error != NULL) {
230         if (strcmp(error, "invalid_grant") == 0) {
231             code = MH_OAUTH_BAD_GRANT;
232         } else {
233             /* All other errors indicate a bug, not anything the user did. */
234             code = MH_OAUTH_REQUEST_BAD;
235         }
236     } else {
237         code = MH_OAUTH_RESPONSE_BAD;
238     }
239     set_err(ctx, code);
240     free(error);
241 }
242 
243 static char *
make_user_agent(void)244 make_user_agent(void)
245 {
246     const char *curl = curl_version_info(CURLVERSION_NOW)->version;
247     return concat(user_agent, " libcurl/", curl, NULL);
248 }
249 
250 boolean
mh_oauth_new(mh_oauth_ctx ** result,const char * svc_name)251 mh_oauth_new(mh_oauth_ctx **result, const char *svc_name)
252 {
253     mh_oauth_ctx *ctx;
254 
255     NEW(ctx);
256     *result = ctx;
257     ctx->curl = NULL;
258 
259     ctx->log = NULL;
260     ctx->cred_fn = ctx->sasl_client_res = ctx->err_formatted = NULL;
261 
262     if (!mh_oauth_get_service_info(svc_name, &ctx->svc, ctx->err_buf,
263 				   sizeof(ctx->err_buf))) {
264 	set_err_details(ctx, MH_OAUTH_BAD_PROFILE, ctx->err_buf);
265         return FALSE;
266     }
267 
268     ctx->curl = curl_easy_init();
269     if (ctx->curl == NULL) {
270         set_err(ctx, MH_OAUTH_CURL_INIT);
271         return FALSE;
272     }
273     curl_easy_setopt(ctx->curl, CURLOPT_ERRORBUFFER, ctx->err_buf);
274 
275     ctx->user_agent = make_user_agent();
276 
277     if (curl_easy_setopt(ctx->curl, CURLOPT_USERAGENT,
278                          ctx->user_agent) != CURLE_OK) {
279         set_err_details(ctx, MH_OAUTH_CURL_INIT, ctx->err_buf);
280         return FALSE;
281     }
282 
283     return TRUE;
284 }
285 
286 void
mh_oauth_free(mh_oauth_ctx * ctx)287 mh_oauth_free(mh_oauth_ctx *ctx)
288 {
289     free(ctx->svc.name);
290     free(ctx->svc.scope);
291     free(ctx->svc.client_id);
292     free(ctx->svc.client_secret);
293     free(ctx->svc.auth_endpoint);
294     free(ctx->svc.token_endpoint);
295     free(ctx->svc.redirect_uri);
296     free(ctx->cred_fn);
297     free(ctx->sasl_client_res);
298     free(ctx->err_formatted);
299     free(ctx->user_agent);
300 
301     if (ctx->curl != NULL) {
302         curl_easy_cleanup(ctx->curl);
303     }
304     free(ctx);
305 }
306 
307 const char *
mh_oauth_svc_display_name(const mh_oauth_ctx * ctx)308 mh_oauth_svc_display_name(const mh_oauth_ctx *ctx)
309 {
310     return ctx->svc.display_name;
311 }
312 
313 void
mh_oauth_log_to(FILE * log,mh_oauth_ctx * ctx)314 mh_oauth_log_to(FILE *log, mh_oauth_ctx *ctx)
315 {
316     ctx->log = log;
317 }
318 
319 mh_oauth_err_code
mh_oauth_get_err_code(const mh_oauth_ctx * ctx)320 mh_oauth_get_err_code(const mh_oauth_ctx *ctx)
321 {
322     return ctx->err_code;
323 }
324 
325 const char *
mh_oauth_get_err_string(mh_oauth_ctx * ctx)326 mh_oauth_get_err_string(mh_oauth_ctx *ctx)
327 {
328     const char *base;
329 
330     free(ctx->err_formatted);
331 
332     switch (ctx->err_code) {
333     case MH_OAUTH_BAD_PROFILE:
334         base = "incomplete OAuth2 service definition";
335         break;
336     case MH_OAUTH_CURL_INIT:
337         base = "error initializing libcurl";
338         break;
339     case MH_OAUTH_REQUEST_INIT:
340         base = "local error initializing HTTP request";
341         break;
342     case MH_OAUTH_POST:
343         base = "error making HTTP request to OAuth2 authorization endpoint";
344         break;
345     case MH_OAUTH_RESPONSE_TOO_BIG:
346         base = "refusing to process response body larger than 8192 bytes";
347         break;
348     case MH_OAUTH_RESPONSE_BAD:
349         base = "invalid response";
350         break;
351     case MH_OAUTH_BAD_GRANT:
352         base = "bad grant (authorization code or refresh token)";
353         break;
354     case MH_OAUTH_REQUEST_BAD:
355         base = "bad OAuth request; re-run with -snoop and send REDACTED output"
356             " to nmh-workers";
357         break;
358     case MH_OAUTH_NO_REFRESH:
359         base = "no refresh token";
360         break;
361     case MH_OAUTH_CRED_USER_NOT_FOUND:
362         base = "user not found in cred file";
363         break;
364     case MH_OAUTH_CRED_FILE:
365         base = "error loading cred file";
366         break;
367     default:
368         base = "unknown error";
369     }
370     if (ctx->err_details == NULL) {
371         return ctx->err_formatted = mh_xstrdup(base);
372     }
373 
374     ctx->err_formatted = concat(base, ": ", ctx->err_details, NULL);
375     return ctx->err_formatted;
376 }
377 
378 const char *
mh_oauth_get_authorize_url(mh_oauth_ctx * ctx)379 mh_oauth_get_authorize_url(mh_oauth_ctx *ctx)
380 {
381     /* [1] 4.1.1 Authorization Request */
382     if (!make_query_url(ctx->buf, sizeof ctx->buf, ctx->curl,
383                         ctx->svc.auth_endpoint,
384                         "response_type", "code",
385                         "client_id", ctx->svc.client_id,
386                         "redirect_uri", ctx->svc.redirect_uri,
387                         "scope", ctx->svc.scope,
388                         (void *)NULL)) {
389         set_err(ctx, MH_OAUTH_REQUEST_INIT);
390         return NULL;
391     }
392     return ctx->buf;
393 }
394 
395 static boolean
cred_from_response(mh_oauth_cred * cred,const char * content_type,const char * input,size_t input_len)396 cred_from_response(mh_oauth_cred *cred, const char *content_type,
397                    const char *input, size_t input_len)
398 {
399     boolean result = FALSE;
400     char *access_token, *expires_in, *refresh_token;
401     const mh_oauth_ctx *ctx = cred->ctx;
402 
403     if (!is_json(content_type)) {
404         return FALSE;
405     }
406 
407     access_token = expires_in = refresh_token = NULL;
408     if (!get_json_strings(input, input_len, ctx->log,
409                           "access_token", &access_token,
410                           "expires_in", &expires_in,
411                           "refresh_token", &refresh_token,
412                           (void *)NULL)) {
413         goto out;
414     }
415 
416     if (access_token == NULL) {
417         /* Response is invalid, but if it has a refresh token, we can try. */
418         if (refresh_token == NULL) {
419             goto out;
420         }
421     }
422 
423     result = TRUE;
424 
425     free(cred->access_token);
426     cred->access_token = access_token;
427     access_token = NULL;
428 
429     cred->expires_at = 0;
430     if (expires_in != NULL) {
431         long e;
432         errno = 0;
433         e = strtol(expires_in, NULL, 10);
434         if (errno == 0) {
435             if (e > 0) {
436                 cred->expires_at = time(NULL) + e;
437             }
438         } else if (ctx->log != NULL) {
439             fprintf(ctx->log, "* invalid expiration: %s\n", expires_in);
440         }
441     }
442 
443     /* [1] 6 Refreshing an Access Token says a new refresh token may be issued
444      * in refresh responses. */
445     if (refresh_token != NULL) {
446         free(cred->refresh_token);
447         cred->refresh_token = refresh_token;
448         refresh_token = NULL;
449     }
450 
451   out:
452     free(refresh_token);
453     free(expires_in);
454     free(access_token);
455     return result;
456 }
457 
458 static boolean
do_access_request(mh_oauth_cred * cred,const char * req_body)459 do_access_request(mh_oauth_cred *cred, const char *req_body)
460 {
461     mh_oauth_ctx *ctx = cred->ctx;
462     struct curl_ctx curl_ctx;
463 
464     curl_ctx.curl = ctx->curl;
465     curl_ctx.log = ctx->log;
466     if (!post(&curl_ctx, ctx->svc.token_endpoint, req_body)) {
467         if (curl_ctx.too_big) {
468             set_err(ctx, MH_OAUTH_RESPONSE_TOO_BIG);
469         } else {
470             set_err_details(ctx, MH_OAUTH_POST, ctx->err_buf);
471         }
472         return FALSE;
473     }
474 
475     if (curl_ctx.res_code != 200) {
476         set_err_http(ctx, &curl_ctx);
477         return FALSE;
478     }
479 
480     if (!cred_from_response(cred, curl_ctx.content_type, curl_ctx.res_body,
481                             curl_ctx.res_len)) {
482         set_err(ctx, MH_OAUTH_RESPONSE_BAD);
483         return FALSE;
484     }
485 
486     return TRUE;
487 }
488 
489 mh_oauth_cred *
mh_oauth_authorize(const char * code,mh_oauth_ctx * ctx)490 mh_oauth_authorize(const char *code, mh_oauth_ctx *ctx)
491 {
492     mh_oauth_cred *result;
493 
494     if (!make_query_url(ctx->buf, sizeof ctx->buf, ctx->curl, NULL,
495                         "code", code,
496                         "grant_type", "authorization_code",
497                         "redirect_uri", ctx->svc.redirect_uri,
498                         "client_id", ctx->svc.client_id,
499                         "client_secret", ctx->svc.client_secret,
500                         (void *)NULL)) {
501         set_err(ctx, MH_OAUTH_REQUEST_INIT);
502         return NULL;
503     }
504 
505     NEW(result);
506     result->ctx = ctx;
507     result->access_token = result->refresh_token = NULL;
508 
509     if (!do_access_request(result, ctx->buf)) {
510         free(result);
511         return NULL;
512     }
513 
514     return result;
515 }
516 
517 boolean
mh_oauth_refresh(mh_oauth_cred * cred)518 mh_oauth_refresh(mh_oauth_cred *cred)
519 {
520     boolean result;
521     mh_oauth_ctx *ctx = cred->ctx;
522 
523     if (cred->refresh_token == NULL) {
524         set_err(ctx, MH_OAUTH_NO_REFRESH);
525         return FALSE;
526     }
527 
528     if (!make_query_url(ctx->buf, sizeof ctx->buf, ctx->curl, NULL,
529                         "grant_type", "refresh_token",
530                         "refresh_token", cred->refresh_token,
531                         "client_id", ctx->svc.client_id,
532                         "client_secret", ctx->svc.client_secret,
533                         (void *)NULL)) {
534         set_err(ctx, MH_OAUTH_REQUEST_INIT);
535         return FALSE;
536     }
537 
538     result = do_access_request(cred, ctx->buf);
539 
540     if (result && cred->access_token == NULL) {
541         set_err_details(ctx, MH_OAUTH_RESPONSE_BAD, "no access token");
542         return FALSE;
543     }
544 
545     return result;
546 }
547 
548 boolean
mh_oauth_access_token_valid(time_t t,const mh_oauth_cred * cred)549 mh_oauth_access_token_valid(time_t t, const mh_oauth_cred *cred)
550 {
551     return cred->access_token != NULL && t + EXPIRY_FUDGE < cred->expires_at;
552 }
553 
554 void
mh_oauth_cred_free(mh_oauth_cred * cred)555 mh_oauth_cred_free(mh_oauth_cred *cred)
556 {
557     free(cred->refresh_token);
558     free(cred->access_token);
559     free(cred);
560 }
561 
562 /* for loading multi-user cred files */
563 struct user_creds {
564     mh_oauth_cred *creds;
565 
566     /* number of allocated mh_oauth_cred structs above points to */
567     size_t alloc;
568 
569     /* number that are actually filled in and used */
570     size_t len;
571 };
572 
573 /* If user has an entry in user_creds, return pointer to it.  Else allocate a
574  * new struct in user_creds and return pointer to that. */
575 static mh_oauth_cred *
find_or_alloc_user_creds(struct user_creds user_creds[],const char * user)576 find_or_alloc_user_creds(struct user_creds user_creds[], const char *user)
577 {
578     mh_oauth_cred *creds = user_creds->creds;
579     size_t i;
580     for (i = 0; i < user_creds->len; i++) {
581         if (strcmp(creds[i].user, user) == 0) {
582             return &creds[i];
583         }
584     }
585     if (user_creds->alloc == user_creds->len) {
586         user_creds->alloc *= 2;
587         user_creds->creds = mh_xrealloc(user_creds->creds, user_creds->alloc);
588     }
589     creds = user_creds->creds+user_creds->len;
590     user_creds->len++;
591     creds->user = getcpy(user);
592     creds->access_token = creds->refresh_token = NULL;
593     creds->expires_at = 0;
594     return creds;
595 }
596 
597 static void
free_user_creds(struct user_creds * user_creds)598 free_user_creds(struct user_creds *user_creds)
599 {
600     mh_oauth_cred *cred;
601     size_t i;
602     cred = user_creds->creds;
603     for (i = 0; i < user_creds->len; i++) {
604         free(cred[i].user);
605         free(cred[i].access_token);
606         free(cred[i].refresh_token);
607     }
608     free(user_creds->creds);
609     free(user_creds);
610 }
611 
612 static boolean
load_creds(struct user_creds ** result,FILE * fp,mh_oauth_ctx * ctx)613 load_creds(struct user_creds **result, FILE *fp, mh_oauth_ctx *ctx)
614 {
615     boolean success = FALSE;
616     char name[NAMESZ], value_buf[BUFSIZ];
617     int state;
618     m_getfld_state_t getfld_ctx = 0;
619 
620     struct user_creds *user_creds;
621     NEW(user_creds);
622     user_creds->alloc = 4;
623     user_creds->len = 0;
624     user_creds->creds = mh_xmalloc(user_creds->alloc * sizeof *user_creds->creds);
625 
626     for (;;) {
627 	int size = sizeof value_buf;
628 	switch (state = m_getfld(&getfld_ctx, name, value_buf, &size, fp)) {
629         case FLD:
630         case FLDPLUS: {
631             char **save, *expire;
632             time_t *expires_at = NULL;
633             if (has_prefix(name, "access-")) {
634                 const char *user = name + 7;
635                 mh_oauth_cred *creds = find_or_alloc_user_creds(user_creds,
636                                                                 user);
637                 save = &creds->access_token;
638             } else if (has_prefix(name, "refresh-")) {
639                 const char *user = name + 8;
640                 mh_oauth_cred *creds = find_or_alloc_user_creds(user_creds,
641                                                                 user);
642                 save = &creds->refresh_token;
643             } else if (has_prefix(name, "expire-")) {
644                 const char *user = name + 7;
645                 mh_oauth_cred *creds = find_or_alloc_user_creds(user_creds,
646                                                                 user);
647                 expires_at = &creds->expires_at;
648                 save = &expire;
649             } else {
650                 set_err_details(ctx, MH_OAUTH_CRED_FILE, "unexpected field");
651                 break;
652             }
653 
654             if (state == FLD) {
655                 *save = trimcpy(value_buf);
656             } else {
657                 char *tmp = getcpy(value_buf);
658                 while (state == FLDPLUS) {
659                     size = sizeof value_buf;
660                     state = m_getfld(&getfld_ctx, name, value_buf, &size, fp);
661                     tmp = add(value_buf, tmp);
662                 }
663                 *save = trimcpy(tmp);
664                 free(tmp);
665             }
666             if (expires_at != NULL) {
667                 errno = 0;
668                 *expires_at = strtol(expire, NULL, 10);
669                 free(expire);
670                 if (errno != 0) {
671                     set_err_details(ctx, MH_OAUTH_CRED_FILE,
672                                     "invalid expiration time");
673                     break;
674                 }
675                 expires_at = NULL;
676             }
677             continue;
678         }
679 
680         case BODY:
681         case FILEEOF:
682             success = TRUE;
683             break;
684 
685         default:
686             /* Not adding details for LENERR/FMTERR because m_getfld already
687              * wrote advise message to stderr. */
688             set_err(ctx, MH_OAUTH_CRED_FILE);
689             break;
690 	}
691 	break;
692     }
693     m_getfld_state_destroy(&getfld_ctx);
694 
695     if (success) {
696         *result = user_creds;
697     } else {
698         free_user_creds(user_creds);
699     }
700 
701     return success;
702 }
703 
704 static boolean
save_user(FILE * fp,const char * user,const char * access,const char * refresh,long expires_at)705 save_user(FILE *fp, const char *user, const char *access, const char *refresh,
706           long expires_at)
707 {
708     if (access != NULL) {
709         if (fprintf(fp, "access-%s: %s\n", user, access) < 0) return FALSE;
710     }
711     if (refresh != NULL) {
712         if (fprintf(fp, "refresh-%s: %s\n", user, refresh) < 0) return FALSE;
713     }
714     if (expires_at > 0) {
715         if (fprintf(fp, "expire-%s: %ld\n", user, (long)expires_at) < 0) {
716             return FALSE;
717         }
718     }
719     return TRUE;
720 }
721 
722 boolean
mh_oauth_cred_save(FILE * fp,mh_oauth_cred * cred,const char * user)723 mh_oauth_cred_save(FILE *fp, mh_oauth_cred *cred, const char *user)
724 {
725     struct user_creds *user_creds;
726     int fd = fileno(fp);
727     size_t i;
728 
729     /* Load existing creds if any. */
730     if (!load_creds(&user_creds, fp, cred->ctx)) {
731         return FALSE;
732     }
733 
734     if (fchmod(fd, S_IRUSR | S_IWUSR) < 0) goto err;
735     if (ftruncate(fd, 0) < 0) goto err;
736     if (fseek(fp, 0, SEEK_SET) < 0) goto err;
737 
738     /* Write all creds except for this user. */
739     for (i = 0; i < user_creds->len; i++) {
740         mh_oauth_cred *c = &user_creds->creds[i];
741         if (strcmp(c->user, user) == 0) continue;
742         if (!save_user(fp, c->user, c->access_token, c->refresh_token,
743                        c->expires_at)) {
744             goto err;
745         }
746     }
747 
748     /* Write updated creds for this user. */
749     if (!save_user(fp, user, cred->access_token, cred->refresh_token,
750                    cred->expires_at)) {
751         goto err;
752     }
753 
754     free_user_creds(user_creds);
755 
756     return TRUE;
757 
758   err:
759     free_user_creds(user_creds);
760     set_err(cred->ctx, MH_OAUTH_CRED_FILE);
761     return FALSE;
762 }
763 
764 mh_oauth_cred *
mh_oauth_cred_load(FILE * fp,mh_oauth_ctx * ctx,const char * user)765 mh_oauth_cred_load(FILE *fp, mh_oauth_ctx *ctx, const char *user)
766 {
767     mh_oauth_cred *creds, *result = NULL;
768     struct user_creds *user_creds;
769     size_t i;
770 
771     if (!load_creds(&user_creds, fp, ctx)) {
772         return NULL;
773     }
774 
775     /* Search user_creds for this user.  If we don't find it, return NULL.
776      * If we do, free fields of all structs except this one, moving this one to
777      * the first struct if necessary.  When we return it, it just looks like one
778      * struct to the caller, and the whole array is freed later. */
779     creds = user_creds->creds;
780     for (i = 0; i < user_creds->len; i++) {
781         if (strcmp(creds[i].user, user) == 0) {
782             result = creds;
783             if (i > 0) {
784                 result->access_token = creds[i].access_token;
785                 result->refresh_token = creds[i].refresh_token;
786                 result->expires_at = creds[i].expires_at;
787             }
788         } else {
789             free(creds[i].access_token);
790             free(creds[i].refresh_token);
791         }
792         free(creds[i].user);
793     }
794 
795     /* No longer need user_creds.  result just uses its creds member. */
796     free(user_creds);
797 
798     if (result == NULL) {
799         set_err_details(ctx, MH_OAUTH_CRED_USER_NOT_FOUND, user);
800         return NULL;
801     }
802 
803     result->ctx = ctx;
804     result->user = NULL;
805 
806     return result;
807 }
808 
809 const char *
mh_oauth_sasl_client_response(size_t * res_len,const char * user,const mh_oauth_cred * cred)810 mh_oauth_sasl_client_response(size_t *res_len,
811                               const char *user, const mh_oauth_cred *cred)
812 {
813     char **p;
814 
815     p = &cred->ctx->sasl_client_res;
816     free(*p);
817     *p = concat("user=", user, "\1auth=Bearer ", cred->access_token, "\1\1", NULL);
818     *res_len = strlen(*p);
819     return *p;
820 }
821 
822 /*******************************************************************************
823  * building URLs and making HTTP requests with libcurl
824  */
825 
826 /*
827  * Build null-terminated URL in the array pointed to by s.  If the URL doesn't
828  * fit within size (including the terminating null byte), return FALSE without *
829  * building the entire URL.  Some of URL may already have been written into the
830  * result array in that case.
831  */
832 static boolean
make_query_url(char * s,size_t size,CURL * curl,const char * base_url,...)833 make_query_url(char *s, size_t size, CURL *curl, const char *base_url, ...)
834 {
835     boolean result = FALSE;
836     size_t len;
837     char *prefix;
838     va_list ap;
839     const char *name;
840 
841     if (base_url == NULL) {
842         len = 0;
843         prefix = "";
844     } else {
845         len = strlen(base_url);
846         if (len > size - 1) /* Less one for NUL. */
847             return FALSE;
848         strcpy(s, base_url);
849         prefix = "?";
850     }
851 
852     va_start(ap, base_url);
853     for (name = va_arg(ap, char *); name != NULL; name = va_arg(ap, char *)) {
854         char *name_esc = curl_easy_escape(curl, name, 0);
855         char *val_esc = curl_easy_escape(curl, va_arg(ap, char *), 0);
856         /* prefix + name_esc + '=' + val_esc + '\0' must fit within size */
857         size_t new_len = len
858           + strlen(prefix)
859           + strlen(name_esc)
860           + 1 /* '=' */
861           + strlen(val_esc);
862         if (new_len + 1 > size) {
863             free(name_esc);
864             free(val_esc);
865             goto out;
866         }
867         sprintf(s + len, "%s%s=%s", prefix, name_esc, val_esc);
868         free(name_esc);
869         free(val_esc);
870         len = new_len;
871         prefix = "&";
872     }
873 
874     result = TRUE;
875 
876   out:
877     va_end(ap);
878     return result;
879 }
880 
881 static int
debug_callback(CURL * handle,curl_infotype type,char * data,size_t size,void * userptr)882 debug_callback(CURL *handle, curl_infotype type, char *data,
883                size_t size, void *userptr)
884 {
885     FILE *fp = userptr;
886     NMH_UNUSED(handle);
887 
888     switch (type) {
889     case CURLINFO_HEADER_IN:
890     case CURLINFO_DATA_IN:
891         fputs("< ", fp);
892         break;
893     case CURLINFO_HEADER_OUT:
894     case CURLINFO_DATA_OUT:
895         fputs("> ", fp);
896         break;
897     default:
898         return 0;
899     }
900     fwrite(data, 1, size, fp);
901     if (data[size - 1] != '\n') {
902         putc('\n', fp);
903     }
904     fflush(fp);
905     return 0;
906 }
907 
908 static size_t
write_callback(const char * ptr,size_t size,size_t nmemb,void * userdata)909 write_callback(const char *ptr, size_t size, size_t nmemb, void *userdata)
910 {
911     struct curl_ctx *ctx = userdata;
912     size_t new_len;
913 
914     if (ctx->too_big) {
915         return 0;
916     }
917 
918     size *= nmemb;
919     new_len = ctx->res_len + size;
920     if (new_len > sizeof ctx->res_body) {
921       ctx->too_big = TRUE;
922       return 0;
923     }
924 
925     memcpy(ctx->res_body + ctx->res_len, ptr, size);
926     ctx->res_len = new_len;
927 
928     return size;
929 }
930 
931 static boolean
post(struct curl_ctx * ctx,const char * url,const char * req_body)932 post(struct curl_ctx *ctx, const char *url, const char *req_body)
933 {
934     CURL *curl = ctx->curl;
935     CURLcode status;
936 
937     ctx->too_big = FALSE;
938     ctx->res_len = 0;
939 
940     if (ctx->log != NULL) {
941         curl_easy_setopt(curl, CURLOPT_VERBOSE, (long)1);
942         curl_easy_setopt(curl, CURLOPT_DEBUGFUNCTION, debug_callback);
943         curl_easy_setopt(curl, CURLOPT_DEBUGDATA, ctx->log);
944     }
945 
946     if ((status = curl_easy_setopt(curl, CURLOPT_URL, url)) != CURLE_OK) {
947         return FALSE;
948     }
949 
950     curl_easy_setopt(curl, CURLOPT_POSTFIELDS, req_body);
951     curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
952     curl_easy_setopt(curl, CURLOPT_WRITEDATA, ctx);
953 
954     if (has_prefix(url, "http://127.0.0.1:")) {
955         /* Hack:  on Cygwin, curl doesn't fail to connect with ECONNREFUSED.
956            Instead, it waits to timeout.  So set a really short timeout, but
957            just on localhost (for convenience of the user, and the test
958            suite). */
959         curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 2L);
960     }
961 
962     status = curl_easy_perform(curl);
963     /* first check for error from callback */
964     if (ctx->too_big) {
965         return FALSE;
966     }
967     /* now from curl */
968     if (status != CURLE_OK) {
969         return FALSE;
970     }
971 
972     if ((status = curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE,
973                                     &ctx->res_code)) != CURLE_OK
974         || (status = curl_easy_getinfo(curl, CURLINFO_CONTENT_TYPE,
975                                        &ctx->content_type)) != CURLE_OK) {
976         return FALSE;
977     }
978 
979     return TRUE;
980 }
981 
982 /*******************************************************************************
983  * JSON processing
984  */
985 
986 /* We need 2 for each key/value pair plus 1 for the enclosing object, which
987  * means we only need 9 for Gmail.  Clients must not fail if the server returns
988  * more, though, e.g. for protocol extensions. */
989 #define JSMN_TOKENS 16
990 
991 /*
992  * Parse JSON, store pointer to array of jsmntok_t in tokens.
993  *
994  * Returns whether parsing is successful.
995  *
996  * Even in that case, tokens has been allocated and must be freed.
997  */
998 static boolean
parse_json(jsmntok_t ** tokens,size_t * tokens_len,const char * input,size_t input_len,FILE * log)999 parse_json(jsmntok_t **tokens, size_t *tokens_len,
1000            const char *input, size_t input_len, FILE *log)
1001 {
1002     jsmn_parser p;
1003     jsmnerr_t r;
1004 
1005     *tokens_len = JSMN_TOKENS;
1006     *tokens = mh_xmalloc(*tokens_len * sizeof **tokens);
1007 
1008     jsmn_init(&p);
1009     while ((r = jsmn_parse(&p, input, input_len,
1010                            *tokens, *tokens_len)) == JSMN_ERROR_NOMEM) {
1011         *tokens_len = 2 * *tokens_len;
1012         if (log != NULL) {
1013             fprintf(log, "* need more jsmntok_t! allocating %ld\n",
1014                     (long)*tokens_len);
1015         }
1016         /* Don't need to limit how much we allocate; we already limited the size
1017            of the response body. */
1018         *tokens = mh_xrealloc(*tokens, *tokens_len * sizeof **tokens);
1019     }
1020     if (r <= 0) {
1021         return FALSE;
1022     }
1023 
1024     return TRUE;
1025 }
1026 
1027 /*
1028  * Search input and tokens for the value identified by null-terminated name.
1029  *
1030  * If found, allocate a null-terminated copy of the value and store the address
1031  * in val.  val is left untouched if not found.
1032  */
1033 static void
get_json_string(char ** val,const char * input,const jsmntok_t * tokens,const char * name)1034 get_json_string(char **val, const char *input, const jsmntok_t *tokens,
1035                 const char *name)
1036 {
1037     /* number of top-level tokens (not counting object/list children) */
1038     int token_count = tokens[0].size * 2;
1039     /* number of tokens to skip when we encounter objects and lists */
1040     /* We only look for top-level strings. */
1041     int skip_tokens = 0;
1042     /* whether the current token represents a field name */
1043     /* The next token will be the value. */
1044     boolean is_key = TRUE;
1045 
1046     int i;
1047     for (i = 1; i <= token_count; i++) {
1048         const char *key;
1049         int key_len;
1050         if (tokens[i].type == JSMN_ARRAY || tokens[i].type == JSMN_OBJECT) {
1051             /* We're not interested in any array or object children; skip. */
1052             int children = tokens[i].size;
1053             if (tokens[i].type == JSMN_OBJECT) {
1054                 /* Object size counts key/value pairs, skip both. */
1055                 children *= 2;
1056             }
1057             /* Add children to token_count. */
1058             token_count += children;
1059             if (skip_tokens == 0) {
1060                 /* This token not already skipped; skip it. */
1061                 /* Would already be skipped if child of object or list. */
1062                 skip_tokens++;
1063             }
1064             /* Skip this token's children. */
1065             skip_tokens += children;
1066         }
1067         if (skip_tokens > 0) {
1068             skip_tokens--;
1069             /* When we finish with the object or list, we'll have a key. */
1070             is_key = TRUE;
1071             continue;
1072         }
1073         if (is_key) {
1074             is_key = FALSE;
1075             continue;
1076         }
1077         key = input + tokens[i - 1].start;
1078         key_len = tokens[i - 1].end - tokens[i - 1].start;
1079         if (strncmp(key, name, key_len) == 0) {
1080             int val_len = tokens[i].end - tokens[i].start;
1081             *val = mh_xmalloc(val_len + 1);
1082             memcpy(*val, input + tokens[i].start, val_len);
1083             (*val)[val_len] = '\0';
1084             return;
1085         }
1086         is_key = TRUE;
1087     }
1088 }
1089 
1090 /*
1091  * Parse input as JSON, extracting specified string values.
1092  *
1093  * Variadic arguments are pairs of null-terminated strings indicating the value
1094  * to extract from the JSON and addresses into which pointers to null-terminated
1095  * copies of the values are written.  These must be followed by one NULL pointer
1096  * to indicate the end of pairs.
1097  *
1098  * The extracted strings are copies which caller must free.  If any name is not
1099  * found, the address to store the value is not touched.
1100  *
1101  * Returns non-zero if parsing is successful.
1102  *
1103  * When parsing failed, no strings have been copied.
1104  *
1105  * log may be used for debug-logging if not NULL.
1106  */
1107 static boolean
get_json_strings(const char * input,size_t input_len,FILE * log,...)1108 get_json_strings(const char *input, size_t input_len, FILE *log, ...)
1109 {
1110     boolean result = FALSE;
1111     jsmntok_t *tokens;
1112     size_t tokens_len;
1113     va_list ap;
1114     const char *name;
1115 
1116     if (!parse_json(&tokens, &tokens_len, input, input_len, log)) {
1117         goto out;
1118     }
1119 
1120     if (tokens->type != JSMN_OBJECT || tokens->size == 0) {
1121         goto out;
1122     }
1123 
1124     result = TRUE;
1125 
1126     va_start(ap, log);
1127     for (name = va_arg(ap, char *); name != NULL; name = va_arg(ap, char *)) {
1128         get_json_string(va_arg(ap, char **), input, tokens, name);
1129     }
1130 
1131   out:
1132     va_end(ap);
1133     free(tokens);
1134     return result;
1135 }
1136 #endif
1137