1 /* oauth.c -- OAuth 2.0 implementation for XOAUTH2 in SMTP and POP3.
3 * This code is Copyright (c) 2014, by the authors of nmh. See the
4 * COPYRIGHT file in the root directory of the nmh distribution for
5 * complete copyright information.
22 #include <curl/curl.h>
23 #include <thirdparty/jsmn/jsmn.h>
27 #include "lock_file.h"
29 #define JSON_TYPE "application/json"
31 /* We pretend access tokens expire 60 seconds earlier than they actually do to
32 * allow for separate processes to use and refresh access tokens. The process
33 * that uses the access token (post) has an error if the token is expired; the
34 * process that refreshes the access token (send) must have already refreshed if
35 * the expiration is close.
37 * 60s is arbitrary, and hopefully is enough to allow for clock skew.
38 * Currently only Gmail supports XOAUTH2, and seems to always use a token
39 * life-time of 3600s, but that is not guaranteed. It is possible for Gmail to
40 * issue an access token with a life-time so short that even after send
41 * refreshes it, it's already expired when post tries to use it, but that seems
43 #define EXPIRY_FUDGE 60
45 /* maximum size for HTTP response bodies
46 * (not counting header and not null-terminated) */
47 #define RESPONSE_BODY_MAX 8192
49 /* Maximum size for URLs and URI-encoded query strings, null-terminated.
51 * Actual maximum we need is based on the size of tokens (limited by
52 * RESPONSE_BODY_MAX), code user copies from a web page (arbitrarily large), and
53 * various service parameters (all arbitrarily large). In practice, all these
54 * are just tens of bytes. It's not hard to change this to realloc as needed,
55 * but we should still have some limit, so why not this one?
59 struct mh_oauth_cred
{
62 /* opaque access token ([1] 1.4) in null-terminated string */
64 /* opaque refresh token ([1] 1.5) in null-terminated string */
67 /* time at which the access token expires, or 0 if unknown */
70 /* Ignoring token_type ([1] 7.1) because
71 * https://developers.google.com/accounts/docs/OAuth2InstalledApp says
72 * "Currently, this field always has the value Bearer". */
74 /* only filled while loading cred files, otherwise NULL */
79 struct mh_oauth_service_info svc
;
86 char *sasl_client_res
;
89 mh_oauth_err_code err_code
;
91 /* If any detailed message about the error is available, this points to it.
92 * May point to err_buf, or something else. */
93 const char *err_details
;
95 /* Pointer to buffer mh_oauth_err_get_string allocates. */
98 /* Ask libcurl to store errors here. */
99 char err_buf
[CURL_ERROR_SIZE
];
106 /* NULL or a file handle to have curl log diagnostics to */
111 /* Whether the response was too big; if so, the rest of the output fields
115 /* HTTP response code */
118 /* NULL or null-terminated value of Content-Type response header field */
119 const char *content_type
;
121 /* number of bytes in the response body */
124 /* response body; NOT null-terminated */
125 char res_body
[RESPONSE_BODY_MAX
];
128 static bool get_json_strings(const char *, size_t, FILE *, ...) ENDNULL
;
129 static bool make_query_url(char *, size_t, CURL
*, const char *, ...) ENDNULL
;
130 static bool post(struct curl_ctx
*, const char *, const char *);
133 mh_oauth_do_xoauth(const char *user
, const char *svc
, unsigned char **oauth_res
,
134 size_t *oauth_res_len
, FILE *log
)
139 int failed_to_lock
= 0;
143 if (!mh_oauth_new (&ctx
, svc
))
144 adios(NULL
, "%s", mh_oauth_get_err_string(ctx
));
146 if (log
!= NULL
) mh_oauth_log_to(stderr
, ctx
);
148 fn
= mh_oauth_cred_fn(svc
);
149 fp
= lkfopendata(fn
, "r+", &failed_to_lock
);
151 if (errno
== ENOENT
) {
152 adios(NULL
, "no credentials -- run mhlogin -saslmech xoauth2 -authservice %s", svc
);
154 adios(fn
, "failed to open");
156 if (failed_to_lock
) {
157 adios(fn
, "failed to lock");
160 if ((cred
= mh_oauth_cred_load(fp
, ctx
, user
)) == NULL
) {
161 adios(NULL
, "%s", mh_oauth_get_err_string(ctx
));
164 if (!mh_oauth_access_token_valid(time(NULL
), cred
)) {
165 if (!mh_oauth_refresh(cred
)) {
166 if (mh_oauth_get_err_code(ctx
) == MH_OAUTH_NO_REFRESH
) {
167 adios(NULL
, "no valid credentials -- run mhlogin -saslmech xoauth2 -authservice %s", svc
);
169 if (mh_oauth_get_err_code(ctx
) == MH_OAUTH_BAD_GRANT
) {
170 adios(NULL
, "credentials rejected -- run mhlogin -saslmech xoauth2 -authservice %s", svc
);
172 inform("error refreshing OAuth2 token");
173 adios(NULL
, "%s", mh_oauth_get_err_string(ctx
));
176 fseek(fp
, 0, SEEK_SET
);
177 if (!mh_oauth_cred_save(fp
, cred
, user
)) {
178 adios(NULL
, "%s", mh_oauth_get_err_string(ctx
));
182 if (lkfclosedata(fp
, fn
) < 0) {
183 adios(fn
, "failed to close");
187 /* XXX writeBase64raw modifies the source buffer! make a copy */
188 client_res
= mh_xstrdup(mh_oauth_sasl_client_response(oauth_res_len
, user
,
190 mh_oauth_cred_free(cred
);
193 *oauth_res
= (unsigned char *) client_res
;
199 is_json(const char *content_type
)
201 return content_type
!= NULL
202 && strncasecmp(content_type
, JSON_TYPE
, LEN(JSON_TYPE
)) == 0;
206 set_err_details(mh_oauth_ctx
*ctx
, mh_oauth_err_code code
, const char *details
)
208 ctx
->err_code
= code
;
209 ctx
->err_details
= details
;
213 set_err(mh_oauth_ctx
*ctx
, mh_oauth_err_code code
)
215 set_err_details(ctx
, code
, NULL
);
219 set_err_http(mh_oauth_ctx
*ctx
, const struct curl_ctx
*curl_ctx
)
222 mh_oauth_err_code code
;
223 /* 5.2. Error Response says error response should use status code 400 and
224 * application/json body. If Content-Type matches, try to parse the body
225 * regardless of the status code. */
226 if (curl_ctx
->res_len
> 0
227 && is_json(curl_ctx
->content_type
)
228 && get_json_strings(curl_ctx
->res_body
, curl_ctx
->res_len
, ctx
->log
,
229 "error", &error
, NULL
)
231 if (strcmp(error
, "invalid_grant") == 0) {
232 code
= MH_OAUTH_BAD_GRANT
;
234 /* All other errors indicate a bug, not anything the user did. */
235 code
= MH_OAUTH_REQUEST_BAD
;
238 code
= MH_OAUTH_RESPONSE_BAD
;
245 make_user_agent(void)
247 const char *curl
= curl_version_info(CURLVERSION_NOW
)->version
;
248 return concat(user_agent
, " libcurl/", curl
, NULL
);
252 mh_oauth_new(mh_oauth_ctx
**result
, const char *svc_name
)
261 ctx
->cred_fn
= ctx
->sasl_client_res
= ctx
->err_formatted
= NULL
;
263 if (!mh_oauth_get_service_info(svc_name
, &ctx
->svc
, ctx
->err_buf
,
264 sizeof(ctx
->err_buf
))) {
265 set_err_details(ctx
, MH_OAUTH_BAD_PROFILE
, ctx
->err_buf
);
269 ctx
->curl
= curl_easy_init();
270 if (ctx
->curl
== NULL
) {
271 set_err(ctx
, MH_OAUTH_CURL_INIT
);
274 curl_easy_setopt(ctx
->curl
, CURLOPT_ERRORBUFFER
, ctx
->err_buf
);
276 ctx
->user_agent
= make_user_agent();
278 if (curl_easy_setopt(ctx
->curl
, CURLOPT_USERAGENT
,
279 ctx
->user_agent
) != CURLE_OK
) {
280 set_err_details(ctx
, MH_OAUTH_CURL_INIT
, ctx
->err_buf
);
288 mh_oauth_free(mh_oauth_ctx
*ctx
)
291 free(ctx
->svc
.scope
);
292 free(ctx
->svc
.client_id
);
293 free(ctx
->svc
.client_secret
);
294 free(ctx
->svc
.auth_endpoint
);
295 free(ctx
->svc
.token_endpoint
);
296 free(ctx
->svc
.redirect_uri
);
298 free(ctx
->sasl_client_res
);
299 free(ctx
->err_formatted
);
300 free(ctx
->user_agent
);
302 if (ctx
->curl
!= NULL
) {
303 curl_easy_cleanup(ctx
->curl
);
309 mh_oauth_svc_display_name(const mh_oauth_ctx
*ctx
)
311 return ctx
->svc
.display_name
;
315 mh_oauth_log_to(FILE *log
, mh_oauth_ctx
*ctx
)
321 mh_oauth_get_err_code(const mh_oauth_ctx
*ctx
)
323 return ctx
->err_code
;
327 mh_oauth_get_err_string(mh_oauth_ctx
*ctx
)
331 free(ctx
->err_formatted
);
333 switch (ctx
->err_code
) {
334 case MH_OAUTH_BAD_PROFILE
:
335 base
= "incomplete OAuth2 service definition";
337 case MH_OAUTH_CURL_INIT
:
338 base
= "error initializing libcurl";
340 case MH_OAUTH_REQUEST_INIT
:
341 base
= "local error initializing HTTP request";
344 base
= "error making HTTP request to OAuth2 authorization endpoint";
346 case MH_OAUTH_RESPONSE_TOO_BIG
:
347 base
= "refusing to process response body larger than 8192 bytes";
349 case MH_OAUTH_RESPONSE_BAD
:
350 base
= "invalid response";
352 case MH_OAUTH_BAD_GRANT
:
353 base
= "bad grant (authorization code or refresh token)";
355 case MH_OAUTH_REQUEST_BAD
:
356 base
= "bad OAuth request; re-run with -snoop and send REDACTED output"
359 case MH_OAUTH_NO_REFRESH
:
360 base
= "no refresh token";
362 case MH_OAUTH_CRED_USER_NOT_FOUND
:
363 base
= "user not found in cred file";
365 case MH_OAUTH_CRED_FILE
:
366 base
= "error loading cred file";
369 base
= "unknown error";
371 if (ctx
->err_details
== NULL
) {
372 return ctx
->err_formatted
= mh_xstrdup(base
);
375 ctx
->err_formatted
= concat(base
, ": ", ctx
->err_details
, NULL
);
376 return ctx
->err_formatted
;
380 mh_oauth_get_authorize_url(mh_oauth_ctx
*ctx
)
382 /* [1] 4.1.1 Authorization Request */
383 if (!make_query_url(ctx
->buf
, sizeof ctx
->buf
, ctx
->curl
,
384 ctx
->svc
.auth_endpoint
,
385 "response_type", "code",
386 "client_id", ctx
->svc
.client_id
,
387 "redirect_uri", ctx
->svc
.redirect_uri
,
388 "scope", ctx
->svc
.scope
,
390 set_err(ctx
, MH_OAUTH_REQUEST_INIT
);
397 cred_from_response(mh_oauth_cred
*cred
, const char *content_type
,
398 const char *input
, size_t input_len
)
401 char *access_token
, *expires_in
, *refresh_token
;
402 const mh_oauth_ctx
*ctx
= cred
->ctx
;
404 if (!is_json(content_type
)) {
408 access_token
= expires_in
= refresh_token
= NULL
;
409 if (!get_json_strings(input
, input_len
, ctx
->log
,
410 "access_token", &access_token
,
411 "expires_in", &expires_in
,
412 "refresh_token", &refresh_token
,
417 if (access_token
== NULL
) {
418 /* Response is invalid, but if it has a refresh token, we can try. */
419 if (refresh_token
== NULL
) {
426 free(cred
->access_token
);
427 cred
->access_token
= access_token
;
430 cred
->expires_at
= 0;
431 if (expires_in
!= NULL
) {
434 e
= strtol(expires_in
, NULL
, 10);
437 cred
->expires_at
= time(NULL
) + e
;
439 } else if (ctx
->log
!= NULL
) {
440 fprintf(ctx
->log
, "* invalid expiration: %s\n", expires_in
);
444 /* [1] 6 Refreshing an Access Token says a new refresh token may be issued
445 * in refresh responses. */
446 if (refresh_token
!= NULL
) {
447 free(cred
->refresh_token
);
448 cred
->refresh_token
= refresh_token
;
449 refresh_token
= NULL
;
460 do_access_request(mh_oauth_cred
*cred
, const char *req_body
)
462 mh_oauth_ctx
*ctx
= cred
->ctx
;
463 struct curl_ctx curl_ctx
;
465 curl_ctx
.curl
= ctx
->curl
;
466 curl_ctx
.log
= ctx
->log
;
467 if (!post(&curl_ctx
, ctx
->svc
.token_endpoint
, req_body
)) {
468 if (curl_ctx
.too_big
) {
469 set_err(ctx
, MH_OAUTH_RESPONSE_TOO_BIG
);
471 set_err_details(ctx
, MH_OAUTH_POST
, ctx
->err_buf
);
476 if (curl_ctx
.res_code
!= 200) {
477 set_err_http(ctx
, &curl_ctx
);
481 if (!cred_from_response(cred
, curl_ctx
.content_type
, curl_ctx
.res_body
,
483 set_err(ctx
, MH_OAUTH_RESPONSE_BAD
);
491 mh_oauth_authorize(const char *code
, mh_oauth_ctx
*ctx
)
493 mh_oauth_cred
*result
;
495 if (!make_query_url(ctx
->buf
, sizeof ctx
->buf
, ctx
->curl
, NULL
,
497 "grant_type", "authorization_code",
498 "redirect_uri", ctx
->svc
.redirect_uri
,
499 "client_id", ctx
->svc
.client_id
,
500 "client_secret", ctx
->svc
.client_secret
,
502 set_err(ctx
, MH_OAUTH_REQUEST_INIT
);
508 result
->access_token
= result
->refresh_token
= NULL
;
510 if (!do_access_request(result
, ctx
->buf
)) {
519 mh_oauth_refresh(mh_oauth_cred
*cred
)
522 mh_oauth_ctx
*ctx
= cred
->ctx
;
524 if (cred
->refresh_token
== NULL
) {
525 set_err(ctx
, MH_OAUTH_NO_REFRESH
);
529 if (!make_query_url(ctx
->buf
, sizeof ctx
->buf
, ctx
->curl
, NULL
,
530 "grant_type", "refresh_token",
531 "refresh_token", cred
->refresh_token
,
532 "client_id", ctx
->svc
.client_id
,
533 "client_secret", ctx
->svc
.client_secret
,
535 set_err(ctx
, MH_OAUTH_REQUEST_INIT
);
539 result
= do_access_request(cred
, ctx
->buf
);
541 if (result
&& cred
->access_token
== NULL
) {
542 set_err_details(ctx
, MH_OAUTH_RESPONSE_BAD
, "no access token");
550 mh_oauth_access_token_valid(time_t t
, const mh_oauth_cred
*cred
)
552 return cred
->access_token
!= NULL
&& t
+ EXPIRY_FUDGE
< cred
->expires_at
;
556 mh_oauth_cred_free(mh_oauth_cred
*cred
)
558 free(cred
->refresh_token
);
559 free(cred
->access_token
);
563 /* for loading multi-user cred files */
565 mh_oauth_cred
*creds
;
567 /* number of allocated mh_oauth_cred structs above points to */
570 /* number that are actually filled in and used */
574 /* If user has an entry in user_creds, return pointer to it. Else allocate a
575 * new struct in user_creds and return pointer to that. */
576 static mh_oauth_cred
*
577 find_or_alloc_user_creds(struct user_creds user_creds
[], const char *user
)
579 mh_oauth_cred
*creds
= user_creds
->creds
;
581 for (i
= 0; i
< user_creds
->len
; i
++) {
582 if (strcmp(creds
[i
].user
, user
) == 0) {
586 if (user_creds
->alloc
== user_creds
->len
) {
587 user_creds
->alloc
*= 2;
588 user_creds
->creds
= mh_xrealloc(user_creds
->creds
, user_creds
->alloc
);
590 creds
= user_creds
->creds
+user_creds
->len
;
592 creds
->user
= getcpy(user
);
593 creds
->access_token
= creds
->refresh_token
= NULL
;
594 creds
->expires_at
= 0;
599 free_user_creds(struct user_creds
*user_creds
)
603 cred
= user_creds
->creds
;
604 for (i
= 0; i
< user_creds
->len
; i
++) {
606 free(cred
[i
].access_token
);
607 free(cred
[i
].refresh_token
);
609 free(user_creds
->creds
);
614 load_creds(struct user_creds
**result
, FILE *fp
, mh_oauth_ctx
*ctx
)
616 bool success
= false;
617 char name
[NAMESZ
], value_buf
[BUFSIZ
];
619 m_getfld_state_t getfld_ctx
;
621 struct user_creds
*user_creds
;
623 user_creds
->alloc
= 4;
625 user_creds
->creds
= mh_xmalloc(user_creds
->alloc
* sizeof *user_creds
->creds
);
627 getfld_ctx
= m_getfld_state_init(fp
);
629 int size
= sizeof value_buf
;
630 switch (state
= m_getfld2(&getfld_ctx
, name
, value_buf
, &size
)) {
633 char **save
, *expire
;
634 time_t *expires_at
= NULL
;
635 if (has_prefix(name
, "access-")) {
636 const char *user
= name
+ 7;
637 mh_oauth_cred
*creds
= find_or_alloc_user_creds(user_creds
,
639 save
= &creds
->access_token
;
640 } else if (has_prefix(name
, "refresh-")) {
641 const char *user
= name
+ 8;
642 mh_oauth_cred
*creds
= find_or_alloc_user_creds(user_creds
,
644 save
= &creds
->refresh_token
;
645 } else if (has_prefix(name
, "expire-")) {
646 const char *user
= name
+ 7;
647 mh_oauth_cred
*creds
= find_or_alloc_user_creds(user_creds
,
649 expires_at
= &creds
->expires_at
;
652 set_err_details(ctx
, MH_OAUTH_CRED_FILE
, "unexpected field");
657 *save
= trimcpy(value_buf
);
659 char *tmp
= getcpy(value_buf
);
660 while (state
== FLDPLUS
) {
661 size
= sizeof value_buf
;
662 state
= m_getfld2(&getfld_ctx
, name
, value_buf
, &size
);
663 tmp
= add(value_buf
, tmp
);
665 *save
= trimcpy(tmp
);
668 if (expires_at
!= NULL
) {
670 *expires_at
= strtol(expire
, NULL
, 10);
673 set_err_details(ctx
, MH_OAUTH_CRED_FILE
,
674 "invalid expiration time");
688 /* Not adding details for LENERR/FMTERR because m_getfld2 already
689 * wrote advise message to stderr. */
690 set_err(ctx
, MH_OAUTH_CRED_FILE
);
695 m_getfld_state_destroy(&getfld_ctx
);
698 *result
= user_creds
;
700 free_user_creds(user_creds
);
707 save_user(FILE *fp
, const char *user
, const char *access
, const char *refresh
,
710 if (access
!= NULL
) {
711 if (fprintf(fp
, "access-%s: %s\n", user
, access
) < 0) return false;
713 if (refresh
!= NULL
) {
714 if (fprintf(fp
, "refresh-%s: %s\n", user
, refresh
) < 0) return false;
716 if (expires_at
> 0) {
717 if (fprintf(fp
, "expire-%s: %ld\n", user
, (long)expires_at
) < 0) {
725 mh_oauth_cred_save(FILE *fp
, mh_oauth_cred
*cred
, const char *user
)
727 struct user_creds
*user_creds
;
731 /* Load existing creds if any. */
732 if (!load_creds(&user_creds
, fp
, cred
->ctx
)) {
736 if (fchmod(fd
, S_IRUSR
| S_IWUSR
) < 0) goto err
;
737 if (ftruncate(fd
, 0) < 0) goto err
;
738 if (fseek(fp
, 0, SEEK_SET
) < 0) goto err
;
740 /* Write all creds except for this user. */
741 for (i
= 0; i
< user_creds
->len
; i
++) {
742 mh_oauth_cred
*c
= &user_creds
->creds
[i
];
743 if (strcmp(c
->user
, user
) == 0) continue;
744 if (!save_user(fp
, c
->user
, c
->access_token
, c
->refresh_token
,
750 /* Write updated creds for this user. */
751 if (!save_user(fp
, user
, cred
->access_token
, cred
->refresh_token
,
756 free_user_creds(user_creds
);
761 free_user_creds(user_creds
);
762 set_err(cred
->ctx
, MH_OAUTH_CRED_FILE
);
767 mh_oauth_cred_load(FILE *fp
, mh_oauth_ctx
*ctx
, const char *user
)
769 mh_oauth_cred
*creds
, *result
= NULL
;
770 struct user_creds
*user_creds
;
773 if (!load_creds(&user_creds
, fp
, ctx
)) {
777 /* Search user_creds for this user. If we don't find it, return NULL.
778 * If we do, free fields of all structs except this one, moving this one to
779 * the first struct if necessary. When we return it, it just looks like one
780 * struct to the caller, and the whole array is freed later. */
781 creds
= user_creds
->creds
;
782 for (i
= 0; i
< user_creds
->len
; i
++) {
783 if (strcmp(creds
[i
].user
, user
) == 0) {
786 result
->access_token
= creds
[i
].access_token
;
787 result
->refresh_token
= creds
[i
].refresh_token
;
788 result
->expires_at
= creds
[i
].expires_at
;
791 free(creds
[i
].access_token
);
792 free(creds
[i
].refresh_token
);
797 /* No longer need user_creds. result just uses its creds member. */
800 if (result
== NULL
) {
801 set_err_details(ctx
, MH_OAUTH_CRED_USER_NOT_FOUND
, user
);
812 mh_oauth_sasl_client_response(size_t *res_len
,
813 const char *user
, const mh_oauth_cred
*cred
)
817 p
= &cred
->ctx
->sasl_client_res
;
819 *p
= concat("user=", user
, "\1auth=Bearer ", cred
->access_token
, "\1\1", NULL
);
820 *res_len
= strlen(*p
);
824 /*******************************************************************************
825 * building URLs and making HTTP requests with libcurl
829 * Build null-terminated URL in the array pointed to by s. If the URL doesn't
830 * fit within size (including the terminating null byte), return false without *
831 * building the entire URL. Some of URL may already have been written into the
832 * result array in that case.
835 make_query_url(char *s
, size_t size
, CURL
*curl
, const char *base_url
, ...)
843 if (base_url
== NULL
) {
847 len
= strlen(base_url
);
848 if (len
> size
- 1) /* Less one for NUL. */
854 va_start(ap
, base_url
);
855 for (name
= va_arg(ap
, char *); name
!= NULL
; name
= va_arg(ap
, char *)) {
856 char *name_esc
= curl_easy_escape(curl
, name
, 0);
857 char *val_esc
= curl_easy_escape(curl
, va_arg(ap
, char *), 0);
858 /* prefix + name_esc + '=' + val_esc + '\0' must fit within size */
864 if (new_len
+ 1 > size
) {
869 sprintf(s
+ len
, "%s%s=%s", prefix
, name_esc
, val_esc
);
884 debug_callback(CURL
*handle
, curl_infotype type
, char *data
,
885 size_t size
, void *userptr
)
891 case CURLINFO_HEADER_IN
:
892 case CURLINFO_DATA_IN
:
895 case CURLINFO_HEADER_OUT
:
896 case CURLINFO_DATA_OUT
:
902 fwrite(data
, 1, size
, fp
);
903 if (data
[size
- 1] != '\n') {
911 write_callback(const char *ptr
, size_t size
, size_t nmemb
, void *userdata
)
913 struct curl_ctx
*ctx
= userdata
;
921 new_len
= ctx
->res_len
+ size
;
922 if (new_len
> sizeof ctx
->res_body
) {
927 memcpy(ctx
->res_body
+ ctx
->res_len
, ptr
, size
);
928 ctx
->res_len
= new_len
;
934 post(struct curl_ctx
*ctx
, const char *url
, const char *req_body
)
936 CURL
*curl
= ctx
->curl
;
939 ctx
->too_big
= false;
942 if (ctx
->log
!= NULL
) {
943 curl_easy_setopt(curl
, CURLOPT_VERBOSE
, (long)1);
944 curl_easy_setopt(curl
, CURLOPT_DEBUGFUNCTION
, debug_callback
);
945 curl_easy_setopt(curl
, CURLOPT_DEBUGDATA
, ctx
->log
);
948 if (curl_easy_setopt(curl
, CURLOPT_URL
, url
) != CURLE_OK
) {
952 curl_easy_setopt(curl
, CURLOPT_POSTFIELDS
, req_body
);
953 curl_easy_setopt(curl
, CURLOPT_WRITEFUNCTION
, write_callback
);
954 curl_easy_setopt(curl
, CURLOPT_WRITEDATA
, ctx
);
956 if (has_prefix(url
, "http://127.0.0.1:")) {
957 /* Hack: on Cygwin, curl doesn't fail to connect with ECONNREFUSED.
958 Instead, it waits to timeout. So set a really short timeout, but
959 just on localhost (for convenience of the user, and the test
961 curl_easy_setopt(curl
, CURLOPT_CONNECTTIMEOUT
, 2L);
964 status
= curl_easy_perform(curl
);
965 /* first check for error from callback */
970 if (status
!= CURLE_OK
) {
974 return curl_easy_getinfo(curl
,
975 CURLINFO_RESPONSE_CODE
, &ctx
->res_code
) == CURLE_OK
&&
976 curl_easy_getinfo(curl
,
977 CURLINFO_CONTENT_TYPE
, &ctx
->content_type
) == CURLE_OK
;
980 /*******************************************************************************
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
990 * Parse JSON, store pointer to array of jsmntok_t in tokens.
992 * Returns whether parsing is successful.
994 * Even in that case, tokens has been allocated and must be freed.
997 parse_json(jsmntok_t
**tokens
, size_t *tokens_len
,
998 const char *input
, size_t input_len
, FILE *log
)
1003 *tokens_len
= JSMN_TOKENS
;
1004 *tokens
= mh_xmalloc(*tokens_len
* sizeof **tokens
);
1007 while ((r
= jsmn_parse(&p
, input
, input_len
,
1008 *tokens
, *tokens_len
)) == JSMN_ERROR_NOMEM
) {
1009 *tokens_len
= 2 * *tokens_len
;
1011 fprintf(log
, "* need more jsmntok_t! allocating %ld\n",
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
);
1023 * Search input and tokens for the value identified by null-terminated name.
1025 * If found, allocate a null-terminated copy of the value and store the address
1026 * in val. val is left untouched if not found.
1029 get_json_string(char **val
, const char *input
, const jsmntok_t
*tokens
,
1032 /* number of top-level tokens (not counting object/list children) */
1033 int token_count
= tokens
[0].size
* 2;
1034 /* number of tokens to skip when we encounter objects and lists */
1035 /* We only look for top-level strings. */
1036 int skip_tokens
= 0;
1037 /* whether the current token represents a field name */
1038 /* The next token will be the value. */
1042 for (i
= 1; i
<= token_count
; i
++) {
1045 if (tokens
[i
].type
== JSMN_ARRAY
|| tokens
[i
].type
== JSMN_OBJECT
) {
1046 /* We're not interested in any array or object children; skip. */
1047 int children
= tokens
[i
].size
;
1048 if (tokens
[i
].type
== JSMN_OBJECT
) {
1049 /* Object size counts key/value pairs, skip both. */
1052 /* Add children to token_count. */
1053 token_count
+= children
;
1054 if (skip_tokens
== 0) {
1055 /* This token not already skipped; skip it. */
1056 /* Would already be skipped if child of object or list. */
1059 /* Skip this token's children. */
1060 skip_tokens
+= children
;
1062 if (skip_tokens
> 0) {
1064 /* When we finish with the object or list, we'll have a key. */
1072 key
= input
+ tokens
[i
- 1].start
;
1073 key_len
= tokens
[i
- 1].end
- tokens
[i
- 1].start
;
1074 if (strncmp(key
, name
, key_len
) == 0) {
1075 int val_len
= tokens
[i
].end
- tokens
[i
].start
;
1076 *val
= mh_xmalloc(val_len
+ 1);
1077 memcpy(*val
, input
+ tokens
[i
].start
, val_len
);
1078 (*val
)[val_len
] = '\0';
1086 * Parse input as JSON, extracting specified string values.
1088 * Variadic arguments are pairs of null-terminated strings indicating the value
1089 * to extract from the JSON and addresses into which pointers to null-terminated
1090 * copies of the values are written. These must be followed by one NULL pointer
1091 * to indicate the end of pairs.
1093 * The extracted strings are copies which caller must free. If any name is not
1094 * found, the address to store the value is not touched.
1096 * Returns non-zero if parsing is successful.
1098 * When parsing failed, no strings have been copied.
1100 * log may be used for debug-logging if not NULL.
1103 get_json_strings(const char *input
, size_t input_len
, FILE *log
, ...)
1105 bool result
= false;
1111 if (!parse_json(&tokens
, &tokens_len
, input
, input_len
, log
)) {
1115 if (tokens
->type
!= JSMN_OBJECT
|| tokens
->size
== 0) {
1122 for (name
= va_arg(ap
, char *); name
!= NULL
; name
= va_arg(ap
, char *)) {
1123 get_json_string(va_arg(ap
, char **), input
, tokens
, name
);