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