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