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