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