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