X-Git-Url: https://diplodocus.org/git/nmh/blobdiff_plain/aaf70a50e13231ba06df17d79147ac0d7fed6d91..fb6ea8dca0129dbb93ecb5fe1147a7b03138bbf8:/sbr/oauth.c diff --git a/sbr/oauth.c b/sbr/oauth.c old mode 100644 new mode 100755 index b795eb81..0cc95e4e --- a/sbr/oauth.c +++ b/sbr/oauth.c @@ -26,13 +26,13 @@ #define JSON_TYPE "application/json" -/* We pretend access tokens expire 30 seconds earlier than they actually do to +/* We pretend access tokens expire 60 seconds earlier than they actually do to * allow for separate processes to use and refresh access tokens. The process * that uses the access token (post) has an error if the token is expired; the * process that refreshes the access token (send) must have already refreshed if * the expiration is close. * - * 30s is arbitrary, and hopefully is enough to allow for clock skew. + * 60s is arbitrary, and hopefully is enough to allow for clock skew. * Currently only Gmail supports XOAUTH2, and seems to always use a token * life-time of 3600s, but that is not guaranteed. It is possible for Gmail to * issue an access token with a life-time so short that even after send @@ -44,7 +44,7 @@ * (not counting header and not null-terminated) */ #define RESPONSE_BODY_MAX 8192 -/* Maxium size for URLs and URI-encoded query strings, null-terminated. +/* Maximum size for URLs and URI-encoded query strings, null-terminated. * * Actual maximum we need is based on the size of tokens (limited by * RESPONSE_BODY_MAX), code user copies from a web page (arbitrarily large), and @@ -54,46 +54,6 @@ */ #define URL_MAX 8192 -struct service_info { - /* Name of service, so we can search static SERVICES (below) and for - * determining default credential file name. */ - char *name; - - /* Human-readable name of the service; in mh_oauth_ctx::svc this is not - * another buffer to free, but a pointer to either static SERVICE data - * (below) or to the name field. */ - char *display_name; - - /* [1] 2.2 Client Identifier, 2.3.1 Client Password */ - char *client_id; - /* [1] 2.3.1 Client Password */ - char *client_secret; - /* [1] 3.1 Authorization Endpoint */ - char *auth_endpoint; - /* [1] 3.1.2 Redirection Endpoint */ - char *redirect_uri; - /* [1] 3.2 Token Endpoint */ - char *token_endpoint; - /* [1] 3.3 Access Token Scope */ - char *scope; -}; - -static const struct service_info SERVICES[] = { - /* https://developers.google.com/accounts/docs/OAuth2InstalledApp */ - { - /* name */ "gmail", - /* display_name */ "Gmail", - - /* client_id */ "91584523849-8lv9kgp1rvp8ahta6fa4b125tn2polcg.apps.googleusercontent.com", - /* client_secret */ "Ua8sX34xyv7hVrKM-U70dKI6", - - /* auth_endpoint */ "https://accounts.google.com/o/oauth2/auth", - /* redirect_uri */ "urn:ietf:wg:oauth:2.0:oob", - /* token_endpoint */ "https://accounts.google.com/o/oauth2/token", - /* scope */ "https://mail.google.com/" - } -}; - struct mh_oauth_cred { mh_oauth_ctx *ctx; @@ -108,10 +68,13 @@ struct mh_oauth_cred { /* Ignoring token_type ([1] 7.1) because * https://developers.google.com/accounts/docs/OAuth2InstalledApp says * "Currently, this field always has the value Bearer". */ + + /* only filled while loading cred files, otherwise NULL */ + char *user; }; struct mh_oauth_ctx { - struct service_info svc; + struct mh_oauth_service_info svc; CURL *curl; FILE *log; @@ -164,23 +127,22 @@ static boolean get_json_strings(const char *, size_t, FILE *, ...); static boolean make_query_url(char *, size_t, CURL *, const char *, ...); static boolean post(struct curl_ctx *, const char *, const char *); -char * -mh_oauth_do_xoauth(const char *user, const char *svc, FILE *log) +int +mh_oauth_do_xoauth(const char *user, const char *svc, unsigned char **oauth_res, + size_t *oauth_res_len, FILE *log) { mh_oauth_ctx *ctx; mh_oauth_cred *cred; char *fn; int failed_to_lock = 0; FILE *fp; - size_t client_res_len; char *client_res; - char *client_res_b64; if (!mh_oauth_new (&ctx, svc)) adios(NULL, mh_oauth_get_err_string(ctx)); if (log != NULL) mh_oauth_log_to(stderr, ctx); - fn = getcpy(mh_oauth_cred_fn(ctx)); + fn = getcpy(mh_oauth_cred_fn(svc)); fp = lkfopendata(fn, "r+", &failed_to_lock); if (fp == NULL) { if (errno == ENOENT) { @@ -192,7 +154,7 @@ mh_oauth_do_xoauth(const char *user, const char *svc, FILE *log) adios(fn, "failed to lock"); } - if ((cred = mh_oauth_cred_load(fp, ctx)) == NULL) { + if ((cred = mh_oauth_cred_load(fp, ctx, user)) == NULL) { adios(NULL, mh_oauth_get_err_string(ctx)); } @@ -209,7 +171,7 @@ mh_oauth_do_xoauth(const char *user, const char *svc, FILE *log) } fseek(fp, 0, SEEK_SET); - if (!mh_oauth_cred_save(fp, cred)) { + if (!mh_oauth_cred_save(fp, cred, user)) { adios(NULL, mh_oauth_get_err_string(ctx)); } } @@ -220,18 +182,14 @@ mh_oauth_do_xoauth(const char *user, const char *svc, FILE *log) free(fn); /* XXX writeBase64raw modifies the source buffer! make a copy */ - client_res = getcpy(mh_oauth_sasl_client_response(&client_res_len, user, + client_res = getcpy(mh_oauth_sasl_client_response(oauth_res_len, user, cred)); mh_oauth_cred_free(cred); mh_oauth_free(ctx); - client_res_b64 = mh_xmalloc(((((client_res_len) + 2) / 3 ) * 4) + 1); - if (writeBase64raw((unsigned char *)client_res, client_res_len, - (unsigned char *)client_res_b64) != OK) { - adios(NULL, "base64 encoding of XOAUTH2 client response failed"); - } - free(client_res); - return client_res_b64; + *oauth_res = (unsigned char *) client_res; + + return OK; } static boolean @@ -262,7 +220,7 @@ set_err_http(mh_oauth_ctx *ctx, const struct curl_ctx *curl_ctx) /* 5.2. Error Response says error response should use status code 400 and * application/json body. If Content-Type matches, try to parse the body * regardless of the status code. */ - if (curl_ctx->res_body != NULL + if (curl_ctx->res_len > 0 && is_json(curl_ctx->content_type) && get_json_strings(curl_ctx->res_body, curl_ctx->res_len, ctx->log, "error", &error, (void *)NULL) @@ -280,78 +238,6 @@ set_err_http(mh_oauth_ctx *ctx, const struct curl_ctx *curl_ctx) free(error); } -/* Copy service info so we don't have to free it only sometimes. */ -static void -copy_svc(struct service_info *to, const struct service_info *from) -{ - to->display_name = from->display_name; -#define copy(_field_) to->_field_ = getcpy(from->_field_) - copy(name); - copy(scope); - copy(client_id); - copy(client_secret); - copy(auth_endpoint); - copy(token_endpoint); - copy(redirect_uri); -#undef copy -} - -/* Return profile component node name for a service parameter. */ -static char * -node_name_for_svc(const char *base_name, const char *svc) -{ - char *result = mh_xmalloc(sizeof "oauth-" - 1 - + strlen(svc) - + 1 /* '-' */ - + strlen(base_name) - + 1 /* '\0' */); - sprintf(result, "oauth-%s-%s", svc, base_name); - /* TODO: s/_/-/g ? */ - return result; -} - -/* Update one service_info field if overridden in profile. */ -static void -update_svc_field(char **field, const char *base_name, const char *svc) -{ - char *name = node_name_for_svc(base_name, svc); - const char *value = context_find(name); - if (value != NULL) { - free(*field); - *field = getcpy(value); - } - free(name); -} - -/* Update all service_info fields that are overridden in profile. */ -static boolean -update_svc(struct service_info *svc, const char *svc_name, mh_oauth_ctx *ctx) -{ -#define update(name) \ - update_svc_field(&svc->name, #name, svc_name); \ - if (svc->name == NULL) { \ - set_err_details(ctx, MH_OAUTH_BAD_PROFILE, #name " is missing"); \ - return FALSE; \ - } - update(scope); - update(client_id); - update(client_secret); - update(auth_endpoint); - update(token_endpoint); - update(redirect_uri); -#undef update - - if (svc->name == NULL) { - svc->name = getcpy(svc_name); - } - - if (svc->display_name == NULL) { - svc->display_name = svc->name; - } - - return TRUE; -} - static char * make_user_agent() { @@ -370,26 +256,15 @@ boolean mh_oauth_new(mh_oauth_ctx **result, const char *svc_name) { mh_oauth_ctx *ctx = *result = mh_xmalloc(sizeof *ctx); - size_t i; ctx->curl = NULL; ctx->log = NULL; ctx->cred_fn = ctx->sasl_client_res = ctx->err_formatted = NULL; - ctx->svc.name = ctx->svc.display_name = NULL; - ctx->svc.scope = ctx->svc.client_id = NULL; - ctx->svc.client_secret = ctx->svc.auth_endpoint = NULL; - ctx->svc.token_endpoint = ctx->svc.redirect_uri = NULL; - - for (i = 0; i < sizeof SERVICES / sizeof SERVICES[0]; i++) { - if (strcmp(SERVICES[i].name, svc_name) == 0) { - copy_svc(&ctx->svc, &SERVICES[i]); - break; - } - } - - if (!update_svc(&ctx->svc, svc_name, ctx)) { + if (!mh_oauth_get_service_info(svc_name, &ctx->svc, ctx->err_buf, + sizeof(ctx->err_buf))) { + set_err_details(ctx, MH_OAUTH_BAD_PROFILE, ctx->err_buf); return FALSE; } @@ -487,6 +362,9 @@ mh_oauth_get_err_string(mh_oauth_ctx *ctx) case MH_OAUTH_NO_REFRESH: base = "no refresh token"; break; + case MH_OAUTH_CRED_USER_NOT_FOUND: + base = "user not found in cred file"; + break; case MH_OAUTH_CRED_FILE: base = "error loading cred file"; break; @@ -686,79 +564,92 @@ mh_oauth_cred_free(mh_oauth_cred *cred) free(cred); } -const char * -mh_oauth_cred_fn(mh_oauth_ctx *ctx) -{ - char *result, *result_if_allocated; - const char *svc = ctx->svc.name; +/* for loading multi-user cred files */ +struct user_creds { + mh_oauth_cred *creds; - char *component = node_name_for_svc("credential-file", svc); - result = context_find(component); - free(component); + /* number of allocated mh_oauth_cred structs above points to */ + size_t alloc; - if (result == NULL) { - result = mh_xmalloc(sizeof "oauth-" - 1 - + strlen(svc) - + 1 /* '\0' */); - sprintf(result, "oauth-%s", svc); - result_if_allocated = result; - } else { - result_if_allocated = NULL; - } + /* number that are actually filled in and used */ + size_t len; +}; - if (result[0] != '/') { - const char *tmp = m_maildir(result); - free(result_if_allocated); - result = getcpy(tmp); +/* If user has an entry in user_creds, return pointer to it. Else allocate a + * new struct in user_creds and return pointer to that. */ +static mh_oauth_cred * +find_or_alloc_user_creds(struct user_creds user_creds[], const char *user) +{ + mh_oauth_cred *creds = user_creds->creds; + size_t i; + for (i = 0; i < user_creds->len; i++) { + if (strcmp(creds[i].user, user) == 0) { + return &creds[i]; + } } - - free(ctx->cred_fn); - return ctx->cred_fn = result; + if (user_creds->alloc == user_creds->len) { + user_creds->alloc *= 2; + user_creds->creds = mh_xrealloc(user_creds->creds, user_creds->alloc); + } + creds = user_creds->creds+user_creds->len; + user_creds->len++; + creds->user = getcpy(user); + creds->access_token = creds->refresh_token = NULL; + creds->expires_at = 0; + return creds; } -boolean -mh_oauth_cred_save(FILE *fp, mh_oauth_cred *cred) +static void +free_user_creds(struct user_creds *user_creds) { - int fd = fileno(fp); - if (fchmod(fd, S_IRUSR | S_IWUSR) < 0) goto err; - if (ftruncate(fd, 0) < 0) goto err; - if (cred->access_token != NULL) { - if (fprintf(fp, "access: %s\n", cred->access_token) < 0) goto err; - } - if (cred->refresh_token != NULL) { - if (fprintf(fp, "refresh: %s\n", cred->refresh_token) < 0) goto err; - } - if (cred->expires_at > 0) { - if (fprintf(fp, "expire: %ld\n", (long)cred->expires_at) < 0) goto err; + mh_oauth_cred *cred; + size_t i; + cred = user_creds->creds; + for (i = 0; i < user_creds->len; i++) { + free(cred[i].user); + free(cred[i].access_token); + free(cred[i].refresh_token); } - return TRUE; - - err: - set_err(cred->ctx, MH_OAUTH_CRED_FILE); - return FALSE; + free(user_creds->creds); + free(user_creds); } static boolean -parse_cred(char **access, char **refresh, char **expire, FILE *fp, - mh_oauth_ctx *ctx) +load_creds(struct user_creds **result, FILE *fp, mh_oauth_ctx *ctx) { - boolean result = FALSE; + boolean success = FALSE; char name[NAMESZ], value_buf[BUFSIZ]; int state; m_getfld_state_t getfld_ctx = 0; + struct user_creds *user_creds = mh_xmalloc(sizeof *user_creds); + user_creds->alloc = 4; + user_creds->len = 0; + user_creds->creds = mh_xmalloc(user_creds->alloc * sizeof *user_creds->creds); + for (;;) { int size = sizeof value_buf; switch (state = m_getfld(&getfld_ctx, name, value_buf, &size, fp)) { case FLD: case FLDPLUS: { - char **save; - if (strcmp(name, "access") == 0) { - save = access; - } else if (strcmp(name, "refresh") == 0) { - save = refresh; - } else if (strcmp(name, "expire") == 0) { - save = expire; + char **save, *expire; + time_t *expires_at = NULL; + if (strncmp(name, "access-", 7) == 0) { + const char *user = name + 7; + mh_oauth_cred *creds = find_or_alloc_user_creds(user_creds, + user); + save = &creds->access_token; + } else if (strncmp(name, "refresh-", 8) == 0) { + const char *user = name + 8; + mh_oauth_cred *creds = find_or_alloc_user_creds(user_creds, + user); + save = &creds->refresh_token; + } else if (strncmp(name, "expire-", 7) == 0) { + const char *user = name + 7; + mh_oauth_cred *creds = find_or_alloc_user_creds(user_creds, + user); + expires_at = &creds->expires_at; + save = &expire; } else { set_err_details(ctx, MH_OAUTH_CRED_FILE, "unexpected field"); break; @@ -776,12 +667,23 @@ parse_cred(char **access, char **refresh, char **expire, FILE *fp, *save = trimcpy(tmp); free(tmp); } + if (expires_at != NULL) { + errno = 0; + *expires_at = strtol(expire, NULL, 10); + free(expire); + if (errno != 0) { + set_err_details(ctx, MH_OAUTH_CRED_FILE, + "invalid expiration time"); + break; + } + expires_at = NULL; + } continue; } case BODY: case FILEEOF: - result = TRUE; + success = TRUE; break; default: @@ -793,41 +695,117 @@ parse_cred(char **access, char **refresh, char **expire, FILE *fp, break; } m_getfld_state_destroy(&getfld_ctx); - return result; + + if (success) { + *result = user_creds; + } else { + free_user_creds(user_creds); + } + + return success; +} + +static boolean +save_user(FILE *fp, const char *user, const char *access, const char *refresh, + long expires_at) +{ + if (access != NULL) { + if (fprintf(fp, "access-%s: %s\n", user, access) < 0) return FALSE; + } + if (refresh != NULL) { + if (fprintf(fp, "refresh-%s: %s\n", user, refresh) < 0) return FALSE; + } + if (expires_at > 0) { + if (fprintf(fp, "expire-%s: %ld\n", user, (long)expires_at) < 0) { + return FALSE; + } + } + return TRUE; +} + +boolean +mh_oauth_cred_save(FILE *fp, mh_oauth_cred *cred, const char *user) +{ + struct user_creds *user_creds; + int fd = fileno(fp); + size_t i; + + /* Load existing creds if any. */ + if (!load_creds(&user_creds, fp, cred->ctx)) { + return FALSE; + } + + if (fchmod(fd, S_IRUSR | S_IWUSR) < 0) goto err; + if (ftruncate(fd, 0) < 0) goto err; + if (fseek(fp, 0, SEEK_SET) < 0) goto err; + + /* Write all creds except for this user. */ + for (i = 0; i < user_creds->len; i++) { + mh_oauth_cred *c = &user_creds->creds[i]; + if (strcmp(c->user, user) == 0) continue; + if (!save_user(fp, c->user, c->access_token, c->refresh_token, + c->expires_at)) { + goto err; + } + } + + /* Write updated creds for this user. */ + if (!save_user(fp, user, cred->access_token, cred->refresh_token, + cred->expires_at)) { + goto err; + } + + free_user_creds(user_creds); + + return TRUE; + + err: + free_user_creds(user_creds); + set_err(cred->ctx, MH_OAUTH_CRED_FILE); + return FALSE; } mh_oauth_cred * -mh_oauth_cred_load(FILE *fp, mh_oauth_ctx *ctx) +mh_oauth_cred_load(FILE *fp, mh_oauth_ctx *ctx, const char *user) { - mh_oauth_cred *result; - time_t expires_at = 0; - char *access, *refresh, *expire; - - access = refresh = expire = NULL; - if (!parse_cred(&access, &refresh, &expire, fp, ctx)) { - free(access); - free(refresh); - free(expire); + mh_oauth_cred *creds, *result = NULL; + struct user_creds *user_creds; + size_t i; + + if (!load_creds(&user_creds, fp, ctx)) { return NULL; } - if (expire != NULL) { - errno = 0; - expires_at = strtol(expire, NULL, 10); - free(expire); - if (errno != 0) { - set_err_details(ctx, MH_OAUTH_CRED_FILE, "invalid expiration time"); - free(access); - free(refresh); - return NULL; + /* Search user_creds for this user. If we don't find it, return NULL. + * If we do, free fields of all structs except this one, moving this one to + * the first struct if necessary. When we return it, it just looks like one + * struct to the caller, and the whole array is freed later. */ + creds = user_creds->creds; + for (i = 0; i < user_creds->len; i++) { + if (strcmp(creds[i].user, user) == 0) { + result = creds; + if (i > 0) { + result->access_token = creds[i].access_token; + result->refresh_token = creds[i].refresh_token; + result->expires_at = creds[i].expires_at; + } + } else { + free(creds[i].access_token); + free(creds[i].refresh_token); } + free(creds[i].user); + } + + /* No longer need user_creds. result just uses its creds member. */ + free(user_creds); + + if (result == NULL) { + set_err_details(ctx, MH_OAUTH_CRED_USER_NOT_FOUND, user); + return NULL; } - result = mh_xmalloc(sizeof *result); result->ctx = ctx; - result->access_token = access; - result->refresh_token = refresh; - result->expires_at = expires_at; + result->user = NULL; return result; } @@ -978,6 +956,14 @@ post(struct curl_ctx *ctx, const char *url, const char *req_body) curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, ctx); + if (strncmp(url, "http://127.0.0.1:", 17) == 0) { + /* Hack: on Cygwin, curl doesn't fail to connect with ECONNREFUSED. + Instead, it waits to timeout. So set a really short timeout, but + just on localhost (for convenience of the user, and the test + suite). */ + curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 2L); + } + status = curl_easy_perform(curl); /* first check for error from callback */ if (ctx->too_big) { @@ -1036,7 +1022,7 @@ parse_json(jsmntok_t **tokens, size_t *tokens_len, of the response body. */ *tokens = mh_xrealloc(*tokens, *tokens_len * sizeof **tokens); } - if (r == 0) { + if (r <= 0) { return FALSE; } @@ -1152,5 +1138,4 @@ get_json_strings(const char *input, size_t input_len, FILE *log, ...) free(tokens); return result; } - #endif