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