]> diplodocus.org Git - nmh/blob - sbr/oauth.c
c4509fda4d96c64393f4a648c7cd634b56ad73e8
[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,
594 user_creds->alloc * sizeof *user_creds->creds);
595 }
596 creds = user_creds->creds+user_creds->len;
597 user_creds->len++;
598 creds->user = getcpy(user);
599 creds->access_token = creds->refresh_token = NULL;
600 creds->expires_at = 0;
601 return creds;
602 }
603
604 static void
605 free_user_creds(struct user_creds *user_creds)
606 {
607 mh_oauth_cred *cred;
608 size_t i;
609 cred = user_creds->creds;
610 for (i = 0; i < user_creds->len; i++) {
611 free(cred[i].user);
612 free(cred[i].access_token);
613 free(cred[i].refresh_token);
614 }
615 free(user_creds->creds);
616 free(user_creds);
617 }
618
619 static bool
620 load_creds(struct user_creds **result, FILE *fp, mh_oauth_ctx *ctx)
621 {
622 bool success = false;
623 char name[NAMESZ], value_buf[BUFSIZ];
624 int state;
625 m_getfld_state_t getfld_ctx;
626
627 struct user_creds *user_creds;
628 NEW(user_creds);
629 user_creds->alloc = 16;
630 user_creds->len = 0;
631 user_creds->creds = mh_xmalloc(user_creds->alloc * sizeof *user_creds->creds);
632
633 getfld_ctx = m_getfld_state_init(fp);
634 for (;;) {
635 int size = sizeof value_buf;
636 switch (state = m_getfld2(&getfld_ctx, name, value_buf, &size)) {
637 case FLD:
638 case FLDPLUS: {
639 char **save, *expire;
640 time_t *expires_at = NULL;
641 if (has_prefix(name, "access-")) {
642 const char *user = name + 7;
643 mh_oauth_cred *creds = find_or_alloc_user_creds(user_creds,
644 user);
645 save = &creds->access_token;
646 } else if (has_prefix(name, "refresh-")) {
647 const char *user = name + 8;
648 mh_oauth_cred *creds = find_or_alloc_user_creds(user_creds,
649 user);
650 save = &creds->refresh_token;
651 } else if (has_prefix(name, "expire-")) {
652 const char *user = name + 7;
653 mh_oauth_cred *creds = find_or_alloc_user_creds(user_creds,
654 user);
655 expires_at = &creds->expires_at;
656 save = &expire;
657 } else {
658 set_err_details(ctx, MH_OAUTH_CRED_FILE, "unexpected field");
659 break;
660 }
661
662 if (state == FLD) {
663 *save = trimcpy(value_buf);
664 } else {
665 char *tmp = getcpy(value_buf);
666 while (state == FLDPLUS) {
667 size = sizeof value_buf;
668 state = m_getfld2(&getfld_ctx, name, value_buf, &size);
669 tmp = add(value_buf, tmp);
670 }
671 *save = trimcpy(tmp);
672 free(tmp);
673 }
674 if (expires_at != NULL) {
675 errno = 0;
676 *expires_at = strtol(expire, NULL, 10);
677 free(expire);
678 if (errno != 0) {
679 set_err_details(ctx, MH_OAUTH_CRED_FILE,
680 "invalid expiration time");
681 break;
682 }
683 expires_at = NULL;
684 }
685 continue;
686 }
687
688 case BODY:
689 case FILEEOF:
690 success = true;
691 break;
692
693 default:
694 /* Not adding details for LENERR/FMTERR because m_getfld2 already
695 * wrote advise message to stderr. */
696 set_err(ctx, MH_OAUTH_CRED_FILE);
697 break;
698 }
699 break;
700 }
701 m_getfld_state_destroy(&getfld_ctx);
702
703 if (success) {
704 *result = user_creds;
705 } else {
706 free_user_creds(user_creds);
707 }
708
709 return success;
710 }
711
712 static bool
713 save_user(FILE *fp, const char *user, const char *access, const char *refresh,
714 long expires_at)
715 {
716 if (access != NULL) {
717 if (fprintf(fp, "access-%s: %s\n", user, access) < 0) return false;
718 }
719 if (refresh != NULL) {
720 if (fprintf(fp, "refresh-%s: %s\n", user, refresh) < 0) return false;
721 }
722 if (expires_at > 0) {
723 if (fprintf(fp, "expire-%s: %ld\n", user, (long)expires_at) < 0) {
724 return false;
725 }
726 }
727 return true;
728 }
729
730 bool
731 mh_oauth_cred_save(FILE *fp, mh_oauth_cred *cred, const char *user)
732 {
733 struct user_creds *user_creds;
734 int fd = fileno(fp);
735 size_t i;
736
737 /* Load existing creds if any. */
738 if (!load_creds(&user_creds, fp, cred->ctx)) {
739 return false;
740 }
741
742 if (fchmod(fd, S_IRUSR | S_IWUSR) < 0) goto err;
743 if (ftruncate(fd, 0) < 0) goto err;
744 if (fseek(fp, 0, SEEK_SET) < 0) goto err;
745
746 /* Write all creds except for this user. */
747 for (i = 0; i < user_creds->len; i++) {
748 mh_oauth_cred *c = &user_creds->creds[i];
749 if (strcmp(c->user, user) == 0) continue;
750 if (!save_user(fp, c->user, c->access_token, c->refresh_token,
751 c->expires_at)) {
752 goto err;
753 }
754 }
755
756 /* Write updated creds for this user. */
757 if (!save_user(fp, user, cred->access_token, cred->refresh_token,
758 cred->expires_at)) {
759 goto err;
760 }
761
762 free_user_creds(user_creds);
763
764 return true;
765
766 err:
767 free_user_creds(user_creds);
768 set_err(cred->ctx, MH_OAUTH_CRED_FILE);
769 return false;
770 }
771
772 mh_oauth_cred *
773 mh_oauth_cred_load(FILE *fp, mh_oauth_ctx *ctx, const char *user)
774 {
775 mh_oauth_cred *creds, *result = NULL;
776 struct user_creds *user_creds;
777 size_t i;
778
779 if (!load_creds(&user_creds, fp, ctx)) {
780 return NULL;
781 }
782
783 /* Search user_creds for this user. If we don't find it, return NULL.
784 * If we do, free fields of all structs except this one, moving this one to
785 * the first struct if necessary. When we return it, it just looks like one
786 * struct to the caller, and the whole array is freed later. */
787 creds = user_creds->creds;
788 for (i = 0; i < user_creds->len; i++) {
789 if (strcmp(creds[i].user, user) == 0) {
790 result = creds;
791 if (i > 0) {
792 result->access_token = creds[i].access_token;
793 result->refresh_token = creds[i].refresh_token;
794 result->expires_at = creds[i].expires_at;
795 }
796 } else {
797 free(creds[i].access_token);
798 free(creds[i].refresh_token);
799 }
800 free(creds[i].user);
801 }
802
803 /* No longer need user_creds. result just uses its creds member. */
804 free(user_creds);
805
806 if (result == NULL) {
807 set_err_details(ctx, MH_OAUTH_CRED_USER_NOT_FOUND, user);
808 return NULL;
809 }
810
811 result->ctx = ctx;
812 result->user = NULL;
813
814 return result;
815 }
816
817 const char *
818 mh_oauth_sasl_client_response(size_t *res_len,
819 const char *user, const mh_oauth_cred *cred)
820 {
821 char **p;
822
823 p = &cred->ctx->sasl_client_res;
824 free(*p);
825 *p = concat("user=", user, "\1auth=Bearer ", cred->access_token, "\1\1", NULL);
826 *res_len = strlen(*p);
827 return *p;
828 }
829
830 /*******************************************************************************
831 * building URLs and making HTTP requests with libcurl
832 */
833
834 /*
835 * Build null-terminated URL in the array pointed to by s. If the URL doesn't
836 * fit within size (including the terminating null byte), return false without *
837 * building the entire URL. Some of URL may already have been written into the
838 * result array in that case.
839 */
840 static bool
841 make_query_url(char *s, size_t size, CURL *curl, const char *base_url, ...)
842 {
843 bool result = false;
844 size_t len;
845 char *prefix;
846 va_list ap;
847 const char *name;
848
849 if (base_url == NULL) {
850 len = 0;
851 prefix = "";
852 } else {
853 len = strlen(base_url);
854 if (len > size - 1) /* Less one for NUL. */
855 return false;
856 strcpy(s, base_url);
857 prefix = "?";
858 }
859
860 va_start(ap, base_url);
861 for (name = va_arg(ap, char *); name != NULL; name = va_arg(ap, char *)) {
862 char *name_esc = curl_easy_escape(curl, name, 0);
863 char *val_esc = curl_easy_escape(curl, va_arg(ap, char *), 0);
864 /* prefix + name_esc + '=' + val_esc + '\0' must fit within size */
865 size_t new_len = len
866 + strlen(prefix)
867 + strlen(name_esc)
868 + 1 /* '=' */
869 + strlen(val_esc);
870 if (new_len + 1 > size) {
871 free(name_esc);
872 free(val_esc);
873 goto out;
874 }
875 sprintf(s + len, "%s%s=%s", prefix, name_esc, val_esc);
876 free(name_esc);
877 free(val_esc);
878 len = new_len;
879 prefix = "&";
880 }
881
882 result = true;
883
884 out:
885 va_end(ap);
886 return result;
887 }
888
889 static int
890 debug_callback(CURL *handle, curl_infotype type, char *data,
891 size_t size, void *userptr)
892 {
893 FILE *fp = userptr;
894 NMH_UNUSED(handle);
895
896 switch (type) {
897 case CURLINFO_HEADER_IN:
898 case CURLINFO_DATA_IN:
899 fputs("< ", fp);
900 break;
901 case CURLINFO_HEADER_OUT:
902 case CURLINFO_DATA_OUT:
903 fputs("> ", fp);
904 break;
905 default:
906 return 0;
907 }
908 fwrite(data, 1, size, fp);
909 if (data[size - 1] != '\n') {
910 putc('\n', fp);
911 }
912 fflush(fp);
913 return 0;
914 }
915
916 static size_t
917 write_callback(const char *ptr, size_t size, size_t nmemb, void *userdata)
918 {
919 struct curl_ctx *ctx = userdata;
920 size_t new_len;
921
922 if (ctx->too_big) {
923 return 0;
924 }
925
926 size *= nmemb;
927 new_len = ctx->res_len + size;
928 if (new_len > sizeof ctx->res_body) {
929 ctx->too_big = true;
930 return 0;
931 }
932
933 memcpy(ctx->res_body + ctx->res_len, ptr, size);
934 ctx->res_len = new_len;
935
936 return size;
937 }
938
939 static bool
940 post(struct curl_ctx *ctx, const char *url, const char *req_body)
941 {
942 CURL *curl = ctx->curl;
943 CURLcode status;
944
945 ctx->too_big = false;
946 ctx->res_len = 0;
947
948 if (ctx->log != NULL) {
949 curl_easy_setopt(curl, CURLOPT_VERBOSE, (long)1);
950 curl_easy_setopt(curl, CURLOPT_DEBUGFUNCTION, debug_callback);
951 curl_easy_setopt(curl, CURLOPT_DEBUGDATA, ctx->log);
952 }
953
954 if (curl_easy_setopt(curl, CURLOPT_URL, url) != CURLE_OK) {
955 return false;
956 }
957
958 curl_easy_setopt(curl, CURLOPT_POSTFIELDS, req_body);
959 curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
960 curl_easy_setopt(curl, CURLOPT_WRITEDATA, ctx);
961
962 if (has_prefix(url, "http://127.0.0.1:")) {
963 /* Hack: on Cygwin, curl doesn't fail to connect with ECONNREFUSED.
964 Instead, it waits to timeout. So set a really short timeout, but
965 just on localhost (for convenience of the user, and the test
966 suite). */
967 curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 2L);
968 }
969
970 status = curl_easy_perform(curl);
971 /* first check for error from callback */
972 if (ctx->too_big) {
973 return false;
974 }
975 /* now from curl */
976 if (status != CURLE_OK) {
977 return false;
978 }
979
980 return curl_easy_getinfo(curl,
981 CURLINFO_RESPONSE_CODE, &ctx->res_code) == CURLE_OK &&
982 curl_easy_getinfo(curl,
983 CURLINFO_CONTENT_TYPE, &ctx->content_type) == CURLE_OK;
984 }
985
986 /*******************************************************************************
987 * JSON processing
988 */
989
990 /* We need 2 for each key/value pair plus 1 for the enclosing object, which
991 * means we only need 9 for Gmail. Clients must not fail if the server returns
992 * more, though, e.g. for protocol extensions. */
993 #define JSMN_TOKENS 16
994
995 /*
996 * Parse JSON, store pointer to array of jsmntok_t in tokens.
997 *
998 * Returns whether parsing is successful.
999 *
1000 * Even in that case, tokens has been allocated and must be freed.
1001 */
1002 static bool
1003 parse_json(jsmntok_t **tokens, size_t *tokens_len,
1004 const char *input, size_t input_len, FILE *log)
1005 {
1006 jsmn_parser p;
1007 jsmnerr_t r;
1008
1009 *tokens_len = JSMN_TOKENS;
1010 *tokens = mh_xmalloc(*tokens_len * sizeof **tokens);
1011
1012 jsmn_init(&p);
1013 while ((r = jsmn_parse(&p, input, input_len,
1014 *tokens, *tokens_len)) == JSMN_ERROR_NOMEM) {
1015 *tokens_len = 2 * *tokens_len;
1016 if (log != NULL) {
1017 fprintf(log, "* need more jsmntok_t! allocating %ld\n",
1018 (long)*tokens_len);
1019 }
1020 /* Don't need to limit how much we allocate; we already limited the size
1021 of the response body. */
1022 *tokens = mh_xrealloc(*tokens, *tokens_len * sizeof **tokens);
1023 }
1024
1025 return r > 0;
1026 }
1027
1028 /*
1029 * Search input and tokens for the value identified by null-terminated name.
1030 *
1031 * If found, allocate a null-terminated copy of the value and store the address
1032 * in val. val is left untouched if not found.
1033 */
1034 static void
1035 get_json_string(char **val, const char *input, const jsmntok_t *tokens,
1036 const char *name)
1037 {
1038 /* number of top-level tokens (not counting object/list children) */
1039 int token_count = tokens[0].size * 2;
1040 /* number of tokens to skip when we encounter objects and lists */
1041 /* We only look for top-level strings. */
1042 int skip_tokens = 0;
1043 /* whether the current token represents a field name */
1044 /* The next token will be the value. */
1045 bool is_key = true;
1046
1047 int i;
1048 for (i = 1; i <= token_count; i++) {
1049 const char *key;
1050 int key_len;
1051 if (tokens[i].type == JSMN_ARRAY || tokens[i].type == JSMN_OBJECT) {
1052 /* We're not interested in any array or object children; skip. */
1053 int children = tokens[i].size;
1054 if (tokens[i].type == JSMN_OBJECT) {
1055 /* Object size counts key/value pairs, skip both. */
1056 children *= 2;
1057 }
1058 /* Add children to token_count. */
1059 token_count += children;
1060 if (skip_tokens == 0) {
1061 /* This token not already skipped; skip it. */
1062 /* Would already be skipped if child of object or list. */
1063 skip_tokens++;
1064 }
1065 /* Skip this token's children. */
1066 skip_tokens += children;
1067 }
1068 if (skip_tokens > 0) {
1069 skip_tokens--;
1070 /* When we finish with the object or list, we'll have a key. */
1071 is_key = true;
1072 continue;
1073 }
1074 if (is_key) {
1075 is_key = false;
1076 continue;
1077 }
1078 key = input + tokens[i - 1].start;
1079 key_len = tokens[i - 1].end - tokens[i - 1].start;
1080 if (strncmp(key, name, key_len) == 0) {
1081 int val_len = tokens[i].end - tokens[i].start;
1082 *val = mh_xmalloc(val_len + 1);
1083 memcpy(*val, input + tokens[i].start, val_len);
1084 (*val)[val_len] = '\0';
1085 return;
1086 }
1087 is_key = true;
1088 }
1089 }
1090
1091 /*
1092 * Parse input as JSON, extracting specified string values.
1093 *
1094 * Variadic arguments are pairs of null-terminated strings indicating the value
1095 * to extract from the JSON and addresses into which pointers to null-terminated
1096 * copies of the values are written. These must be followed by one NULL pointer
1097 * to indicate the end of pairs.
1098 *
1099 * The extracted strings are copies which caller must free. If any name is not
1100 * found, the address to store the value is not touched.
1101 *
1102 * Returns non-zero if parsing is successful.
1103 *
1104 * When parsing failed, no strings have been copied.
1105 *
1106 * log may be used for debug-logging if not NULL.
1107 */
1108 static bool
1109 get_json_strings(const char *input, size_t input_len, FILE *log, ...)
1110 {
1111 bool result = false;
1112 jsmntok_t *tokens;
1113 size_t tokens_len;
1114 va_list ap;
1115 const char *name;
1116
1117 if (!parse_json(&tokens, &tokens_len, input, input_len, log)) {
1118 goto out;
1119 }
1120
1121 if (tokens->type != JSMN_OBJECT || tokens->size == 0) {
1122 goto out;
1123 }
1124
1125 result = true;
1126
1127 va_start(ap, log);
1128 for (name = va_arg(ap, char *); name != NULL; name = va_arg(ap, char *)) {
1129 get_json_string(va_arg(ap, char **), input, tokens, name);
1130 }
1131
1132 out:
1133 va_end(ap);
1134 free(tokens);
1135 return result;
1136 }
1137 #endif