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.
21 #include <curl/curl.h>
22 #include <thirdparty/jsmn/jsmn.h>
27 #define JSON_TYPE "application/json"
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.
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
41 #define EXPIRY_FUDGE 60
43 /* maximum size for HTTP response bodies
44 * (not counting header and not null-terminated) */
45 #define RESPONSE_BODY_MAX 8192
47 /* Maxium size for URLs and URI-encoded query strings, null-terminated.
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?
58 /* Name of service, so we can search static SERVICES (below) and for
59 * determining default credential file name. */
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. */
67 /* [1] 2.2 Client Identifier, 2.3.1 Client Password */
69 /* [1] 2.3.1 Client Password */
71 /* [1] 3.1 Authorization Endpoint */
73 /* [1] 3.1.2 Redirection Endpoint */
75 /* [1] 3.2 Token Endpoint */
77 /* [1] 3.3 Access Token Scope */
81 static const struct service_info SERVICES
[] = {
82 /* https://developers.google.com/accounts/docs/OAuth2InstalledApp */
85 /* display_name */ "Gmail",
87 /* client_id */ "91584523849-8lv9kgp1rvp8ahta6fa4b125tn2polcg.apps.googleusercontent.com",
88 /* client_secret */ "Ua8sX34xyv7hVrKM-U70dKI6",
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/"
97 struct mh_oauth_cred
{
100 /* opaque access token ([1] 1.4) in null-terminated string */
102 /* opaque refresh token ([1] 1.5) in null-terminated string */
105 /* time at which the access token expires, or 0 if unknown */
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". */
113 struct mh_oauth_ctx
{
114 struct service_info svc
;
121 char *sasl_client_res
;
124 mh_oauth_err_code err_code
;
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
;
130 /* Pointer to buffer mh_oauth_err_get_string allocates. */
133 /* Ask libcurl to store errors here. */
134 char err_buf
[CURL_ERROR_SIZE
];
141 /* NULL or a file handle to have curl log diagnostics to */
146 /* Whether the response was too big; if so, the rest of the output fields
150 /* HTTP response code */
153 /* NULL or null-terminated value of Content-Type response header field */
154 const char *content_type
;
156 /* number of bytes in the response body */
159 /* response body; NOT null-terminated */
160 char res_body
[RESPONSE_BODY_MAX
];
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 *);
168 mh_oauth_do_xoauth(const char *user
, const char *svc
, FILE *log
)
173 int failed_to_lock
= 0;
175 size_t client_res_len
;
177 char *client_res_b64
;
179 if (!mh_oauth_new (&ctx
, svc
)) adios(NULL
, mh_oauth_get_err_string(ctx
));
181 if (log
!= NULL
) mh_oauth_log_to(stderr
, ctx
);
183 fn
= getcpy(mh_oauth_cred_fn(ctx
));
184 fp
= lkfopendata(fn
, "r+", &failed_to_lock
);
186 if (errno
== ENOENT
) {
187 adios(NULL
, "no credentials -- run mhlogin -saslmech xoauth2 -authservice %s", svc
);
189 adios(fn
, "failed to open");
191 if (failed_to_lock
) {
192 adios(fn
, "failed to lock");
195 if ((cred
= mh_oauth_cred_load(fp
, ctx
)) == NULL
) {
196 adios(NULL
, mh_oauth_get_err_string(ctx
));
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
);
204 if (mh_oauth_get_err_code(ctx
) == MH_OAUTH_BAD_GRANT
) {
205 adios(NULL
, "credentials rejected -- run mhlogin -saslmech xoauth2 -authservice %s", svc
);
207 advise(NULL
, "error refreshing OAuth2 token");
208 adios(NULL
, mh_oauth_get_err_string(ctx
));
211 fseek(fp
, 0, SEEK_SET
);
212 if (!mh_oauth_cred_save(fp
, cred
)) {
213 adios(NULL
, mh_oauth_get_err_string(ctx
));
217 if (lkfclosedata(fp
, fn
) < 0) {
218 adios(fn
, "failed to close");
222 /* XXX writeBase64raw modifies the source buffer! make a copy */
223 client_res
= getcpy(mh_oauth_sasl_client_response(&client_res_len
, user
,
225 mh_oauth_cred_free(cred
);
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");
234 return client_res_b64
;
238 is_json(const char *content_type
)
240 return content_type
!= NULL
241 && strncasecmp(content_type
, JSON_TYPE
, sizeof JSON_TYPE
- 1) == 0;
245 set_err_details(mh_oauth_ctx
*ctx
, mh_oauth_err_code code
, const char *details
)
247 ctx
->err_code
= code
;
248 ctx
->err_details
= details
;
252 set_err(mh_oauth_ctx
*ctx
, mh_oauth_err_code code
)
254 set_err_details(ctx
, code
, NULL
);
258 set_err_http(mh_oauth_ctx
*ctx
, const struct curl_ctx
*curl_ctx
)
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
)
270 if (strcmp(error
, "invalid_grant") == 0) {
271 code
= MH_OAUTH_BAD_GRANT
;
273 /* All other errors indicate a bug, not anything the user did. */
274 code
= MH_OAUTH_REQUEST_BAD
;
277 code
= MH_OAUTH_RESPONSE_BAD
;
283 /* Copy service info so we don't have to free it only sometimes. */
285 copy_svc(struct service_info
*to
, const struct service_info
*from
)
287 to
->display_name
= from
->display_name
;
288 #define copy(_field_) to->_field_ = getcpy(from->_field_)
294 copy(token_endpoint
);
299 /* Return profile component node name for a service parameter. */
301 node_name_for_svc(const char *base_name
, const char *svc
)
303 char *result
= mh_xmalloc(sizeof "oauth-" - 1
308 sprintf(result
, "oauth-%s-%s", svc
, base_name
);
309 /* TODO: s/_/-/g ? */
313 /* Update one service_info field if overridden in profile. */
315 update_svc_field(char **field
, const char *base_name
, const char *svc
)
317 char *name
= node_name_for_svc(base_name
, svc
);
318 const char *value
= context_find(name
);
321 *field
= getcpy(value
);
326 /* Update all service_info fields that are overridden in profile. */
328 update_svc(struct service_info
*svc
, const char *svc_name
, mh_oauth_ctx
*ctx
)
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"); \
338 update(client_secret
);
339 update(auth_endpoint
);
340 update(token_endpoint
);
341 update(redirect_uri
);
344 if (svc
->name
== NULL
) {
345 svc
->name
= getcpy(svc_name
);
348 if (svc
->display_name
== NULL
) {
349 svc
->display_name
= svc
->name
;
358 const char *curl
= curl_version_info(CURLVERSION_NOW
)->version
;
359 char *s
= mh_xmalloc(strlen(user_agent
)
365 sprintf(s
, "%s libcurl/%s", user_agent
, curl
);
370 mh_oauth_new(mh_oauth_ctx
**result
, const char *svc_name
)
372 mh_oauth_ctx
*ctx
= *result
= mh_xmalloc(sizeof *ctx
);
378 ctx
->cred_fn
= ctx
->sasl_client_res
= ctx
->err_formatted
= NULL
;
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
;
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
]);
392 if (!update_svc(&ctx
->svc
, svc_name
, ctx
)) {
396 ctx
->curl
= curl_easy_init();
397 if (ctx
->curl
== NULL
) {
398 set_err(ctx
, MH_OAUTH_CURL_INIT
);
401 curl_easy_setopt(ctx
->curl
, CURLOPT_ERRORBUFFER
, ctx
->err_buf
);
403 ctx
->user_agent
= make_user_agent();
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
);
415 mh_oauth_free(mh_oauth_ctx
*ctx
)
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
);
425 free(ctx
->sasl_client_res
);
426 free(ctx
->err_formatted
);
427 free(ctx
->user_agent
);
429 if (ctx
->curl
!= NULL
) {
430 curl_easy_cleanup(ctx
->curl
);
436 mh_oauth_svc_display_name(const mh_oauth_ctx
*ctx
)
438 return ctx
->svc
.display_name
;
442 mh_oauth_log_to(FILE *log
, mh_oauth_ctx
*ctx
)
448 mh_oauth_get_err_code(const mh_oauth_ctx
*ctx
)
450 return ctx
->err_code
;
454 mh_oauth_get_err_string(mh_oauth_ctx
*ctx
)
459 free(ctx
->err_formatted
);
461 switch (ctx
->err_code
) {
462 case MH_OAUTH_BAD_PROFILE
:
463 base
= "incomplete OAuth2 service definition";
465 case MH_OAUTH_CURL_INIT
:
466 base
= "error initializing libcurl";
468 case MH_OAUTH_REQUEST_INIT
:
469 base
= "local error initializing HTTP request";
472 base
= "error making HTTP request to OAuth2 authorization endpoint";
474 case MH_OAUTH_RESPONSE_TOO_BIG
:
475 base
= "refusing to process response body larger than 8192 bytes";
477 case MH_OAUTH_RESPONSE_BAD
:
478 base
= "invalid response";
480 case MH_OAUTH_BAD_GRANT
:
481 base
= "bad grant (authorization code or refresh token)";
483 case MH_OAUTH_REQUEST_BAD
:
484 base
= "bad OAuth request; re-run with -snoop and send REDACTED output"
487 case MH_OAUTH_NO_REFRESH
:
488 base
= "no refresh token";
490 case MH_OAUTH_CRED_FILE
:
491 base
= "error loading cred file";
494 base
= "unknown error";
496 if (ctx
->err_details
== NULL
) {
497 return ctx
->err_formatted
= getcpy(base
);
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
;
506 mh_oauth_get_authorize_url(mh_oauth_ctx
*ctx
)
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
,
516 set_err(ctx
, MH_OAUTH_REQUEST_INIT
);
523 cred_from_response(mh_oauth_cred
*cred
, const char *content_type
,
524 const char *input
, size_t input_len
)
526 boolean result
= FALSE
;
527 char *access_token
, *expires_in
, *refresh_token
;
528 const mh_oauth_ctx
*ctx
= cred
->ctx
;
530 if (!is_json(content_type
)) {
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
,
543 if (access_token
== NULL
) {
544 /* Response is invalid, but if it has a refresh token, we can try. */
545 if (refresh_token
== NULL
) {
552 free(cred
->access_token
);
553 cred
->access_token
= access_token
;
556 cred
->expires_at
= 0;
557 if (expires_in
!= NULL
) {
560 e
= strtol(expires_in
, NULL
, 10);
563 cred
->expires_at
= time(NULL
) + e
;
565 } else if (ctx
->log
!= NULL
) {
566 fprintf(ctx
->log
, "* invalid expiration: %s\n", expires_in
);
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
;
586 do_access_request(mh_oauth_cred
*cred
, const char *req_body
)
588 mh_oauth_ctx
*ctx
= cred
->ctx
;
589 struct curl_ctx curl_ctx
;
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
);
597 set_err_details(ctx
, MH_OAUTH_POST
, ctx
->err_buf
);
602 if (curl_ctx
.res_code
!= 200) {
603 set_err_http(ctx
, &curl_ctx
);
607 if (!cred_from_response(cred
, curl_ctx
.content_type
, curl_ctx
.res_body
,
609 set_err(ctx
, MH_OAUTH_RESPONSE_BAD
);
617 mh_oauth_authorize(const char *code
, mh_oauth_ctx
*ctx
)
619 mh_oauth_cred
*result
;
621 if (!make_query_url(ctx
->buf
, sizeof ctx
->buf
, ctx
->curl
, NULL
,
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
,
628 set_err(ctx
, MH_OAUTH_REQUEST_INIT
);
632 result
= mh_xmalloc(sizeof *result
);
634 result
->access_token
= result
->refresh_token
= NULL
;
636 if (!do_access_request(result
, ctx
->buf
)) {
645 mh_oauth_refresh(mh_oauth_cred
*cred
)
648 mh_oauth_ctx
*ctx
= cred
->ctx
;
650 if (cred
->refresh_token
== NULL
) {
651 set_err(ctx
, MH_OAUTH_NO_REFRESH
);
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
,
661 set_err(ctx
, MH_OAUTH_REQUEST_INIT
);
665 result
= do_access_request(cred
, ctx
->buf
);
667 if (result
&& cred
->access_token
== NULL
) {
668 set_err_details(ctx
, MH_OAUTH_RESPONSE_BAD
, "no access token");
676 mh_oauth_access_token_valid(time_t t
, const mh_oauth_cred
*cred
)
678 return cred
->access_token
!= NULL
&& t
+ EXPIRY_FUDGE
< cred
->expires_at
;
682 mh_oauth_cred_free(mh_oauth_cred
*cred
)
684 free(cred
->refresh_token
);
685 free(cred
->access_token
);
690 mh_oauth_cred_fn(mh_oauth_ctx
*ctx
)
692 char *result
, *result_if_allocated
;
693 const char *svc
= ctx
->svc
.name
;
695 char *component
= node_name_for_svc("credential-file", svc
);
696 result
= context_find(component
);
699 if (result
== NULL
) {
700 result
= mh_xmalloc(sizeof "oauth-" - 1
703 sprintf(result
, "oauth-%s", svc
);
704 result_if_allocated
= result
;
706 result_if_allocated
= NULL
;
709 if (result
[0] != '/') {
710 const char *tmp
= m_maildir(result
);
711 free(result_if_allocated
);
712 result
= getcpy(tmp
);
716 return ctx
->cred_fn
= result
;
720 mh_oauth_cred_save(FILE *fp
, mh_oauth_cred
*cred
)
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
;
728 if (cred
->refresh_token
!= NULL
) {
729 if (fprintf(fp
, "refresh: %s\n", cred
->refresh_token
) < 0) goto err
;
731 if (cred
->expires_at
> 0) {
732 if (fprintf(fp
, "expire: %ld\n", (long)cred
->expires_at
) < 0) goto err
;
737 set_err(cred
->ctx
, MH_OAUTH_CRED_FILE
);
742 parse_cred(char **access
, char **refresh
, char **expire
, FILE *fp
,
745 boolean result
= FALSE
;
746 char name
[NAMESZ
], value_buf
[BUFSIZ
];
748 m_getfld_state_t getfld_ctx
= 0;
751 int size
= sizeof value_buf
;
752 switch (state
= m_getfld(&getfld_ctx
, name
, value_buf
, &size
, fp
)) {
756 if (strcmp(name
, "access") == 0) {
758 } else if (strcmp(name
, "refresh") == 0) {
760 } else if (strcmp(name
, "expire") == 0) {
763 set_err_details(ctx
, MH_OAUTH_CRED_FILE
, "unexpected field");
768 *save
= trimcpy(value_buf
);
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
);
776 *save
= trimcpy(tmp
);
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
);
795 m_getfld_state_destroy(&getfld_ctx
);
800 mh_oauth_cred_load(FILE *fp
, mh_oauth_ctx
*ctx
)
802 mh_oauth_cred
*result
;
803 time_t expires_at
= 0;
804 char *access
, *refresh
, *expire
;
806 access
= refresh
= expire
= NULL
;
807 if (!parse_cred(&access
, &refresh
, &expire
, fp
, ctx
)) {
814 if (expire
!= NULL
) {
816 expires_at
= strtol(expire
, NULL
, 10);
819 set_err_details(ctx
, MH_OAUTH_CRED_FILE
, "invalid expiration time");
826 result
= mh_xmalloc(sizeof *result
);
828 result
->access_token
= access
;
829 result
->refresh_token
= refresh
;
830 result
->expires_at
= expires_at
;
836 mh_oauth_sasl_client_response(size_t *res_len
,
837 const char *user
, const mh_oauth_cred
*cred
)
839 size_t len
= sizeof "user=" - 1
841 + sizeof "\1auth=Bearer " - 1
842 + strlen(cred
->access_token
)
844 free(cred
->ctx
->sasl_client_res
);
845 cred
->ctx
->sasl_client_res
= mh_xmalloc(len
+ 1);
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
;
852 /*******************************************************************************
853 * building URLs and making HTTP requests with libcurl
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.
863 make_query_url(char *s
, size_t size
, CURL
*curl
, const char *base_url
, ...)
865 boolean result
= FALSE
;
871 if (base_url
== NULL
) {
875 len
= sprintf(s
, "%s", base_url
);
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 */
889 if (new_len
+ 1 > size
) {
894 sprintf(s
+ len
, "%s%s=%s", prefix
, name_esc
, val_esc
);
909 debug_callback(const CURL
*handle
, curl_infotype type
, const char *data
,
910 size_t size
, void *userptr
)
916 case CURLINFO_HEADER_IN
:
917 case CURLINFO_DATA_IN
:
920 case CURLINFO_HEADER_OUT
:
921 case CURLINFO_DATA_OUT
:
927 fwrite(data
, 1, size
, fp
);
928 if (data
[size
- 1] != '\n') {
936 write_callback(const char *ptr
, size_t size
, size_t nmemb
, void *userdata
)
938 struct curl_ctx
*ctx
= userdata
;
946 new_len
= ctx
->res_len
+ size
;
947 if (new_len
> sizeof ctx
->res_body
) {
952 memcpy(ctx
->res_body
+ ctx
->res_len
, ptr
, size
);
953 ctx
->res_len
= new_len
;
959 post(struct curl_ctx
*ctx
, const char *url
, const char *req_body
)
961 CURL
*curl
= ctx
->curl
;
964 ctx
->too_big
= FALSE
;
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
);
973 if ((status
= curl_easy_setopt(curl
, CURLOPT_URL
, url
)) != CURLE_OK
) {
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
);
981 status
= curl_easy_perform(curl
);
982 /* first check for error from callback */
987 if (status
!= CURLE_OK
) {
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
) {
1001 /*******************************************************************************
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
1011 * Parse JSON, store pointer to array of jsmntok_t in tokens.
1013 * Returns whether parsing is successful.
1015 * Even in that case, tokens has been allocated and must be freed.
1018 parse_json(jsmntok_t
**tokens
, size_t *tokens_len
,
1019 const char *input
, size_t input_len
, FILE *log
)
1024 *tokens_len
= JSMN_TOKENS
;
1025 *tokens
= mh_xmalloc(*tokens_len
* sizeof **tokens
);
1028 while ((r
= jsmn_parse(&p
, input
, input_len
,
1029 *tokens
, *tokens_len
)) == JSMN_ERROR_NOMEM
) {
1030 *tokens_len
= 2 * *tokens_len
;
1032 fprintf(log
, "* need more jsmntok_t! allocating %ld\n",
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
);
1047 * Search input and tokens for the value identified by null-terminated name.
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.
1053 get_json_string(char **val
, const char *input
, const jsmntok_t
*tokens
,
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
;
1066 for (i
= 1; i
<= token_count
; i
++) {
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. */
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. */
1083 /* Skip this token's children. */
1084 skip_tokens
+= children
;
1086 if (skip_tokens
> 0) {
1088 /* When we finish with the object or list, we'll have a key. */
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';
1110 * Parse input as JSON, extracting specified string values.
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.
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.
1120 * Returns non-zero if parsing is successful.
1122 * When parsing failed, no strings have been copied.
1124 * log may be used for debug-logging if not NULL.
1127 get_json_strings(const char *input
, size_t input_len
, FILE *log
, ...)
1129 boolean result
= FALSE
;
1135 if (!parse_json(&tokens
, &tokens_len
, input
, input_len
, log
)) {
1139 if (tokens
->type
!= JSMN_OBJECT
|| tokens
->size
== 0) {
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
);