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