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