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