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