From d92b5f4a9d69d508e4f5228e881ceb9ef2a30e1f Mon Sep 17 00:00:00 2001 From: "B. Petersen" Date: Sat, 16 Feb 2019 01:17:24 +0100 Subject: [PATCH] add function to regenerate oauth2 access token --- deltachat-core.cbp | 6 + libs/jsmn/LICENSE | 20 +++ libs/jsmn/README.md | 168 ++++++++++++++++++++++++ libs/jsmn/jsmn.c | 314 ++++++++++++++++++++++++++++++++++++++++++++ libs/jsmn/jsmn.h | 76 +++++++++++ src/dc_context.c | 2 + src/dc_context.h | 2 + src/dc_imap.c | 13 +- src/dc_oauth2.c | 145 ++++++++++++++++++++ src/dc_oauth2.h | 16 +++ src/dc_smtp.c | 16 +-- src/meson.build | 1 + 12 files changed, 766 insertions(+), 13 deletions(-) create mode 100644 libs/jsmn/LICENSE create mode 100644 libs/jsmn/README.md create mode 100644 libs/jsmn/jsmn.c create mode 100644 libs/jsmn/jsmn.h create mode 100644 src/dc_oauth2.c create mode 100644 src/dc_oauth2.h diff --git a/deltachat-core.cbp b/deltachat-core.cbp index 875d3385..bec85594 100644 --- a/deltachat-core.cbp +++ b/deltachat-core.cbp @@ -65,6 +65,9 @@ + + @@ -497,6 +500,9 @@ + + diff --git a/libs/jsmn/LICENSE b/libs/jsmn/LICENSE new file mode 100644 index 00000000..c84fb2e9 --- /dev/null +++ b/libs/jsmn/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2010 Serge A. Zaitsev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/libs/jsmn/README.md b/libs/jsmn/README.md new file mode 100644 index 00000000..45436b36 --- /dev/null +++ b/libs/jsmn/README.md @@ -0,0 +1,168 @@ +JSMN +==== + +[![Build Status](https://travis-ci.org/zserge/jsmn.svg?branch=master)](https://travis-ci.org/zserge/jsmn) + +jsmn (pronounced like 'jasmine') is a minimalistic JSON parser in C. It can be +easily integrated into resource-limited or embedded projects. + +You can find more information about JSON format at [json.org][1] + +Library sources are available at https://github.com/zserge/jsmn + +The web page with some information about jsmn can be found at +[http://zserge.com/jsmn.html][2] + +Philosophy +---------- + +Most JSON parsers offer you a bunch of functions to load JSON data, parse it +and extract any value by its name. jsmn proves that checking the correctness of +every JSON packet or allocating temporary objects to store parsed JSON fields +often is an overkill. + +JSON format itself is extremely simple, so why should we complicate it? + +jsmn is designed to be **robust** (it should work fine even with erroneous +data), **fast** (it should parse data on the fly), **portable** (no superfluous +dependencies or non-standard C extensions). And of course, **simplicity** is a +key feature - simple code style, simple algorithm, simple integration into +other projects. + +Features +-------- + +* compatible with C89 +* no dependencies (even libc!) +* highly portable (tested on x86/amd64, ARM, AVR) +* about 200 lines of code +* extremely small code footprint +* API contains only 2 functions +* no dynamic memory allocation +* incremental single-pass parsing +* library code is covered with unit-tests + +Design +------ + +The rudimentary jsmn object is a **token**. Let's consider a JSON string: + + '{ "name" : "Jack", "age" : 27 }' + +It holds the following tokens: + +* Object: `{ "name" : "Jack", "age" : 27}` (the whole object) +* Strings: `"name"`, `"Jack"`, `"age"` (keys and some values) +* Number: `27` + +In jsmn, tokens do not hold any data, but point to token boundaries in JSON +string instead. In the example above jsmn will create tokens like: Object +[0..31], String [3..7], String [12..16], String [20..23], Number [27..29]. + +Every jsmn token has a type, which indicates the type of corresponding JSON +token. jsmn supports the following token types: + +* Object - a container of key-value pairs, e.g.: + `{ "foo":"bar", "x":0.3 }` +* Array - a sequence of values, e.g.: + `[ 1, 2, 3 ]` +* String - a quoted sequence of chars, e.g.: `"foo"` +* Primitive - a number, a boolean (`true`, `false`) or `null` + +Besides start/end positions, jsmn tokens for complex types (like arrays +or objects) also contain a number of child items, so you can easily follow +object hierarchy. + +This approach provides enough information for parsing any JSON data and makes +it possible to use zero-copy techniques. + +Install +------- + +To clone the repository you should have Git installed. Just run: + + $ git clone https://github.com/zserge/jsmn + +Repository layout is simple: jsmn.c and jsmn.h are library files, tests are in +the jsmn\_test.c, you will also find README, LICENSE and Makefile files inside. + +To build the library, run `make`. It is also recommended to run `make test`. +Let me know, if some tests fail. + +If build was successful, you should get a `libjsmn.a` library. +The header file you should include is called `"jsmn.h"`. + +API +--- + +Token types are described by `jsmntype_t`: + + typedef enum { + JSMN_UNDEFINED = 0, + JSMN_OBJECT = 1, + JSMN_ARRAY = 2, + JSMN_STRING = 3, + JSMN_PRIMITIVE = 4 + } jsmntype_t; + +**Note:** Unlike JSON data types, primitive tokens are not divided into +numbers, booleans and null, because one can easily tell the type using the +first character: + +* 't', 'f' - boolean +* 'n' - null +* '-', '0'..'9' - number + +Token is an object of `jsmntok_t` type: + + typedef struct { + jsmntype_t type; // Token type + int start; // Token start position + int end; // Token end position + int size; // Number of child (nested) tokens + } jsmntok_t; + +**Note:** string tokens point to the first character after +the opening quote and the previous symbol before final quote. This was made +to simplify string extraction from JSON data. + +All job is done by `jsmn_parser` object. You can initialize a new parser using: + + jsmn_parser parser; + jsmntok_t tokens[10]; + + jsmn_init(&parser); + + // js - pointer to JSON string + // tokens - an array of tokens available + // 10 - number of tokens available + jsmn_parse(&parser, js, strlen(js), tokens, 10); + +This will create a parser, and then it tries to parse up to 10 JSON tokens from +the `js` string. + +A non-negative return value of `jsmn_parse` is the number of tokens actually +used by the parser. +Passing NULL instead of the tokens array would not store parsing results, but +instead the function will return the value of tokens needed to parse the given +string. This can be useful if you don't know yet how many tokens to allocate. + +If something goes wrong, you will get an error. Error will be one of these: + +* `JSMN_ERROR_INVAL` - bad token, JSON string is corrupted +* `JSMN_ERROR_NOMEM` - not enough tokens, JSON string is too large +* `JSMN_ERROR_PART` - JSON string is too short, expecting more JSON data + +If you get `JSMN_ERROR_NOMEM`, you can re-allocate more tokens and call +`jsmn_parse` once more. If you read json data from the stream, you can +periodically call `jsmn_parse` and check if return value is `JSMN_ERROR_PART`. +You will get this error until you reach the end of JSON data. + +Other info +---------- + +This software is distributed under [MIT license](http://www.opensource.org/licenses/mit-license.php), + so feel free to integrate it in your commercial products. + +[1]: http://www.json.org/ +[2]: http://zserge.com/jsmn.html diff --git a/libs/jsmn/jsmn.c b/libs/jsmn/jsmn.c new file mode 100644 index 00000000..853c3f17 --- /dev/null +++ b/libs/jsmn/jsmn.c @@ -0,0 +1,314 @@ +#include "jsmn.h" + +/** + * Allocates a fresh unused token from the token pool. + */ +static jsmntok_t *jsmn_alloc_token(jsmn_parser *parser, + jsmntok_t *tokens, size_t num_tokens) { + jsmntok_t *tok; + if (parser->toknext >= num_tokens) { + return NULL; + } + tok = &tokens[parser->toknext++]; + tok->start = tok->end = -1; + tok->size = 0; +#ifdef JSMN_PARENT_LINKS + tok->parent = -1; +#endif + return tok; +} + +/** + * Fills token type and boundaries. + */ +static void jsmn_fill_token(jsmntok_t *token, jsmntype_t type, + int start, int end) { + token->type = type; + token->start = start; + token->end = end; + token->size = 0; +} + +/** + * Fills next available token with JSON primitive. + */ +static int jsmn_parse_primitive(jsmn_parser *parser, const char *js, + size_t len, jsmntok_t *tokens, size_t num_tokens) { + jsmntok_t *token; + int start; + + start = parser->pos; + + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { + switch (js[parser->pos]) { +#ifndef JSMN_STRICT + /* In strict mode primitive must be followed by "," or "}" or "]" */ + case ':': +#endif + case '\t' : case '\r' : case '\n' : case ' ' : + case ',' : case ']' : case '}' : + goto found; + } + if (js[parser->pos] < 32 || js[parser->pos] >= 127) { + parser->pos = start; + return JSMN_ERROR_INVAL; + } + } +#ifdef JSMN_STRICT + /* In strict mode primitive must be followed by a comma/object/array */ + parser->pos = start; + return JSMN_ERROR_PART; +#endif + +found: + if (tokens == NULL) { + parser->pos--; + return 0; + } + token = jsmn_alloc_token(parser, tokens, num_tokens); + if (token == NULL) { + parser->pos = start; + return JSMN_ERROR_NOMEM; + } + jsmn_fill_token(token, JSMN_PRIMITIVE, start, parser->pos); +#ifdef JSMN_PARENT_LINKS + token->parent = parser->toksuper; +#endif + parser->pos--; + return 0; +} + +/** + * Fills next token with JSON string. + */ +static int jsmn_parse_string(jsmn_parser *parser, const char *js, + size_t len, jsmntok_t *tokens, size_t num_tokens) { + jsmntok_t *token; + + int start = parser->pos; + + parser->pos++; + + /* Skip starting quote */ + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { + char c = js[parser->pos]; + + /* Quote: end of string */ + if (c == '\"') { + if (tokens == NULL) { + return 0; + } + token = jsmn_alloc_token(parser, tokens, num_tokens); + if (token == NULL) { + parser->pos = start; + return JSMN_ERROR_NOMEM; + } + jsmn_fill_token(token, JSMN_STRING, start+1, parser->pos); +#ifdef JSMN_PARENT_LINKS + token->parent = parser->toksuper; +#endif + return 0; + } + + /* Backslash: Quoted symbol expected */ + if (c == '\\' && parser->pos + 1 < len) { + int i; + parser->pos++; + switch (js[parser->pos]) { + /* Allowed escaped symbols */ + case '\"': case '/' : case '\\' : case 'b' : + case 'f' : case 'r' : case 'n' : case 't' : + break; + /* Allows escaped symbol \uXXXX */ + case 'u': + parser->pos++; + for(i = 0; i < 4 && parser->pos < len && js[parser->pos] != '\0'; i++) { + /* If it isn't a hex character we have an error */ + if(!((js[parser->pos] >= 48 && js[parser->pos] <= 57) || /* 0-9 */ + (js[parser->pos] >= 65 && js[parser->pos] <= 70) || /* A-F */ + (js[parser->pos] >= 97 && js[parser->pos] <= 102))) { /* a-f */ + parser->pos = start; + return JSMN_ERROR_INVAL; + } + parser->pos++; + } + parser->pos--; + break; + /* Unexpected symbol */ + default: + parser->pos = start; + return JSMN_ERROR_INVAL; + } + } + } + parser->pos = start; + return JSMN_ERROR_PART; +} + +/** + * Parse JSON string and fill tokens. + */ +int jsmn_parse(jsmn_parser *parser, const char *js, size_t len, + jsmntok_t *tokens, unsigned int num_tokens) { + int r; + int i; + jsmntok_t *token; + int count = parser->toknext; + + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { + char c; + jsmntype_t type; + + c = js[parser->pos]; + switch (c) { + case '{': case '[': + count++; + if (tokens == NULL) { + break; + } + token = jsmn_alloc_token(parser, tokens, num_tokens); + if (token == NULL) + return JSMN_ERROR_NOMEM; + if (parser->toksuper != -1) { + tokens[parser->toksuper].size++; +#ifdef JSMN_PARENT_LINKS + token->parent = parser->toksuper; +#endif + } + token->type = (c == '{' ? JSMN_OBJECT : JSMN_ARRAY); + token->start = parser->pos; + parser->toksuper = parser->toknext - 1; + break; + case '}': case ']': + if (tokens == NULL) + break; + type = (c == '}' ? JSMN_OBJECT : JSMN_ARRAY); +#ifdef JSMN_PARENT_LINKS + if (parser->toknext < 1) { + return JSMN_ERROR_INVAL; + } + token = &tokens[parser->toknext - 1]; + for (;;) { + if (token->start != -1 && token->end == -1) { + if (token->type != type) { + return JSMN_ERROR_INVAL; + } + token->end = parser->pos + 1; + parser->toksuper = token->parent; + break; + } + if (token->parent == -1) { + if(token->type != type || parser->toksuper == -1) { + return JSMN_ERROR_INVAL; + } + break; + } + token = &tokens[token->parent]; + } +#else + for (i = parser->toknext - 1; i >= 0; i--) { + token = &tokens[i]; + if (token->start != -1 && token->end == -1) { + if (token->type != type) { + return JSMN_ERROR_INVAL; + } + parser->toksuper = -1; + token->end = parser->pos + 1; + break; + } + } + /* Error if unmatched closing bracket */ + if (i == -1) return JSMN_ERROR_INVAL; + for (; i >= 0; i--) { + token = &tokens[i]; + if (token->start != -1 && token->end == -1) { + parser->toksuper = i; + break; + } + } +#endif + break; + case '\"': + r = jsmn_parse_string(parser, js, len, tokens, num_tokens); + if (r < 0) return r; + count++; + if (parser->toksuper != -1 && tokens != NULL) + tokens[parser->toksuper].size++; + break; + case '\t' : case '\r' : case '\n' : case ' ': + break; + case ':': + parser->toksuper = parser->toknext - 1; + break; + case ',': + if (tokens != NULL && parser->toksuper != -1 && + tokens[parser->toksuper].type != JSMN_ARRAY && + tokens[parser->toksuper].type != JSMN_OBJECT) { +#ifdef JSMN_PARENT_LINKS + parser->toksuper = tokens[parser->toksuper].parent; +#else + for (i = parser->toknext - 1; i >= 0; i--) { + if (tokens[i].type == JSMN_ARRAY || tokens[i].type == JSMN_OBJECT) { + if (tokens[i].start != -1 && tokens[i].end == -1) { + parser->toksuper = i; + break; + } + } + } +#endif + } + break; +#ifdef JSMN_STRICT + /* In strict mode primitives are: numbers and booleans */ + case '-': case '0': case '1' : case '2': case '3' : case '4': + case '5': case '6': case '7' : case '8': case '9': + case 't': case 'f': case 'n' : + /* And they must not be keys of the object */ + if (tokens != NULL && parser->toksuper != -1) { + jsmntok_t *t = &tokens[parser->toksuper]; + if (t->type == JSMN_OBJECT || + (t->type == JSMN_STRING && t->size != 0)) { + return JSMN_ERROR_INVAL; + } + } +#else + /* In non-strict mode every unquoted value is a primitive */ + default: +#endif + r = jsmn_parse_primitive(parser, js, len, tokens, num_tokens); + if (r < 0) return r; + count++; + if (parser->toksuper != -1 && tokens != NULL) + tokens[parser->toksuper].size++; + break; + +#ifdef JSMN_STRICT + /* Unexpected char in strict mode */ + default: + return JSMN_ERROR_INVAL; +#endif + } + } + + if (tokens != NULL) { + for (i = parser->toknext - 1; i >= 0; i--) { + /* Unmatched opened object or array */ + if (tokens[i].start != -1 && tokens[i].end == -1) { + return JSMN_ERROR_PART; + } + } + } + + return count; +} + +/** + * Creates a new parser based over a given buffer with an array of tokens + * available. + */ +void jsmn_init(jsmn_parser *parser) { + parser->pos = 0; + parser->toknext = 0; + parser->toksuper = -1; +} + diff --git a/libs/jsmn/jsmn.h b/libs/jsmn/jsmn.h new file mode 100644 index 00000000..5a5200ee --- /dev/null +++ b/libs/jsmn/jsmn.h @@ -0,0 +1,76 @@ +#ifndef __JSMN_H_ +#define __JSMN_H_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * JSON type identifier. Basic types are: + * o Object + * o Array + * o String + * o Other primitive: number, boolean (true/false) or null + */ +typedef enum { + JSMN_UNDEFINED = 0, + JSMN_OBJECT = 1, + JSMN_ARRAY = 2, + JSMN_STRING = 3, + JSMN_PRIMITIVE = 4 +} jsmntype_t; + +enum jsmnerr { + /* Not enough tokens were provided */ + JSMN_ERROR_NOMEM = -1, + /* Invalid character inside JSON string */ + JSMN_ERROR_INVAL = -2, + /* The string is not a full JSON packet, more bytes expected */ + JSMN_ERROR_PART = -3 +}; + +/** + * JSON token description. + * type type (object, array, string etc.) + * start start position in JSON data string + * end end position in JSON data string + */ +typedef struct { + jsmntype_t type; + int start; + int end; + int size; +#ifdef JSMN_PARENT_LINKS + int parent; +#endif +} jsmntok_t; + +/** + * JSON parser. Contains an array of token blocks available. Also stores + * the string being parsed now and current position in that string + */ +typedef struct { + unsigned int pos; /* offset in the JSON string */ + unsigned int toknext; /* next token to allocate */ + int toksuper; /* superior token node, e.g parent object or array */ +} jsmn_parser; + +/** + * Create JSON parser over an array of tokens + */ +void jsmn_init(jsmn_parser *parser); + +/** + * Run JSON parser. It parses a JSON data string into and array of tokens, each describing + * a single JSON object. + */ +int jsmn_parse(jsmn_parser *parser, const char *js, size_t len, + jsmntok_t *tokens, unsigned int num_tokens); + +#ifdef __cplusplus +} +#endif + +#endif /* __JSMN_H_ */ diff --git a/src/dc_context.c b/src/dc_context.c index 7a334197..ae573d79 100644 --- a/src/dc_context.c +++ b/src/dc_context.c @@ -187,6 +187,7 @@ dc_context_t* dc_context_new(dc_callback_t cb, void* userdata, const char* os_na dc_jobthread_init(&context->mvbox_thread, context, "MVBOX", "configured_mvbox_folder"); pthread_mutex_init(&context->smtpidle_condmutex, NULL); pthread_cond_init(&context->smtpidle_cond, NULL); + pthread_mutex_init(&context->oauth2_critical, NULL); context->magic = DC_CONTEXT_MAGIC; context->userdata = userdata; @@ -258,6 +259,7 @@ void dc_context_unref(dc_context_t* context) dc_jobthread_exit(&context->mvbox_thread); pthread_cond_destroy(&context->smtpidle_cond); pthread_mutex_destroy(&context->smtpidle_condmutex); + pthread_mutex_destroy(&context->oauth2_critical); free(context->os_name); context->magic = 0; diff --git a/src/dc_context.h b/src/dc_context.h index 0158262f..41019e96 100644 --- a/src/dc_context.h +++ b/src/dc_context.h @@ -64,6 +64,8 @@ struct _dc_context int perform_smtp_jobs_needed; int probe_smtp_network; /**< if this flag is set, the smtp-job timeouts are bypassed and messages are sent until they fail */ + pthread_mutex_t oauth2_critical; + dc_callback_t cb; /**< Internal */ char* os_name; /**< Internal, may be NULL */ diff --git a/src/dc_imap.c b/src/dc_imap.c index 9793bf93..81ced01d 100644 --- a/src/dc_imap.c +++ b/src/dc_imap.c @@ -7,6 +7,7 @@ #include "dc_imap.h" #include "dc_job.h" #include "dc_loginparam.h" +#include "dc_oauth2.h" static int setup_handle_if_needed (dc_imap_t*); @@ -774,12 +775,14 @@ static int setup_handle_if_needed(dc_imap_t* imap) { // for DC_LP_AUTH_OAUTH2, user_pw is assumed to be the oauth_token dc_log_info(imap->context, 0, "IMAP-OAuth2 connect..."); - if (imap->imap_pw==NULL || imap->imap_pw[0]==0) { - dc_log_event_seq(imap->context, DC_EVENT_ERROR_NETWORK, &imap->log_connect_errors, - "IMAP-OAuth2 token missing for %s@%s:%i.", imap->imap_user, imap->imap_server, (int)imap->imap_port); - goto cleanup; + char* access_token = dc_oauth2_get_access_token(imap->context, imap->imap_pw, 0); + r = mailimap_oauth2_authenticate(imap->etpan, imap->imap_user, access_token); + if (dc_imap_is_error(imap, r)) { + free(access_token); + access_token = dc_oauth2_get_access_token(imap->context, imap->imap_pw, DC_REGENERATE); + r = mailimap_oauth2_authenticate(imap->etpan, imap->imap_user, access_token); } - r = mailimap_oauth2_authenticate(imap->etpan, imap->imap_user, imap->imap_pw); + free(access_token); } else { diff --git a/src/dc_oauth2.c b/src/dc_oauth2.c new file mode 100644 index 00000000..c7078ce3 --- /dev/null +++ b/src/dc_oauth2.c @@ -0,0 +1,145 @@ +#include "dc_context.h" +#include "dc_oauth2.h" +#include "../libs/jsmn/jsmn.h" + + +static int jsoneq(const char *json, jsmntok_t *tok, const char *s) { + // from the jsmn parser example + if (tok->type == JSMN_STRING && (int) strlen(s) == tok->end - tok->start && + strncmp(json + tok->start, s, tok->end - tok->start) == 0) { + return 0; + } + return -1; +} + + +static char* jsondup(const char *json, jsmntok_t *tok) { + if (tok->type == JSMN_STRING) { + return strndup(json+tok->start, tok->end - tok->start); + } + return strdup(""); +} + + +char* dc_oauth2_get_access_token(dc_context_t* context, const char* code, int flags) +{ + char* access_token = NULL; + char* refresh_token = NULL; + char* auth_url = NULL; + char* expires_in_str = NULL; + char* error = NULL; + char* error_description = NULL; + char* json = NULL; + jsmn_parser parser; + jsmntok_t tok[128]; // we do not expect nor read more tokens + int tok_cnt = 0; + int locked = 0; + + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC + || code==NULL || code[0]==0) { + dc_log_warning(context, 0, "Internal OAuth2 error"); + goto cleanup; + } + + pthread_mutex_lock(&context->oauth2_critical); + locked = 1; + + // read generated token + if (!(flags&DC_REGENERATE)) { + access_token = dc_sqlite3_get_config(context->sql, "oauth2_access_token", NULL); + if (access_token!=NULL) { + goto cleanup; // success + } + } + + // generate new token: build & call auth url + #define CLIENT_ID "959970109878-t6pl4k9fmsdvfnobae862urapdmhfvbe.apps.googleusercontent.com" + #define CLIENT_SECRET "g2f_Gc1YUJ-fWjnTkdsuk4Xo" + #define REDIRECT_URI "urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob" + + refresh_token = dc_sqlite3_get_config(context->sql, "oauth2_refresh_token", NULL); + if (refresh_token==NULL) + { + auth_url = dc_mprintf("https://accounts.google.com/o/oauth2/token" + "?client_id=%s" + "&client_secret=%s" + "&grant_type=authorization_code" + "&code=%s" + "&redirect_uri=%s", + CLIENT_ID, CLIENT_SECRET, code, REDIRECT_URI); + } + else + { + auth_url = dc_mprintf("https://accounts.google.com/o/oauth2/token" + "?client_id=%s" + "&client_secret=%s" + "&grant_type=refresh_token" + "&refresh_token=%s" + "&redirect_uri=%s", + CLIENT_ID, CLIENT_SECRET, refresh_token, REDIRECT_URI); + } + + json = (char*)context->cb(context, DC_EVENT_HTTP_POST, (uintptr_t)auth_url, 0); + if (json==NULL) { + dc_log_warning(context, 0, "Error calling OAuth2 url"); + goto cleanup; + } + + // generate new token: parse returned json + jsmn_init(&parser); + tok_cnt = jsmn_parse(&parser, json, strlen(json), tok, sizeof(tok)/sizeof(tok[0])); + if (tok_cnt<2 || tok[0].type!=JSMN_OBJECT) { + dc_log_warning(context, 0, "Failed to parse OAuth2 json"); + goto cleanup; + } + + for (int i = 1; i < tok_cnt; i++) { + if (jsoneq(json, &tok[i], "access_token")==0) { + access_token = jsondup(json, &tok[i+1]); + } + else if (jsoneq(json, &tok[i], "refresh_token")==0) { + refresh_token = jsondup(json, &tok[i+1]); + } + else if (jsoneq(json, &tok[i], "expires_in")==0) { + expires_in_str = jsondup(json, &tok[i+1]); + } + else if (jsoneq(json, &tok[i], "error")==0) { + error = jsondup(json, &tok[i+1]); + } + else if (jsoneq(json, &tok[i], "error_description")==0) { + error_description = jsondup(json, &tok[i+1]); + } + } + + if (error || error_description) { + dc_log_warning(context, 0, "OAuth error: %s: %s", + error? error : "unknown", + error_description? error_description : "no details"); + dc_log_warning(context, 0, "FULL COMMAND WAS: %s", auth_url); + // continue, errors do not imply everything went wrong + } + + if (access_token==NULL || access_token[0]==0) { + dc_log_warning(context, 0, "Failed to find OAuth2 access token"); + goto cleanup; + } + + dc_sqlite3_set_config(context->sql, "oauth2_access_token", access_token); + + // update refresh_token if given, + // typically this is on the first round with `grant_type=authorization_code` + // but we update it later, too. + if (refresh_token && refresh_token[0]) { + dc_sqlite3_set_config(context->sql, "oauth2_refresh_token", refresh_token); + } + +cleanup: + if (locked) { pthread_mutex_unlock(&context->oauth2_critical); } + free(refresh_token); + free(auth_url); + free(json); + free(expires_in_str); + free(error); + free(error_description); + return access_token? access_token : dc_strdup(NULL); +} diff --git a/src/dc_oauth2.h b/src/dc_oauth2.h new file mode 100644 index 00000000..3a328616 --- /dev/null +++ b/src/dc_oauth2.h @@ -0,0 +1,16 @@ +#ifndef __DC_OAUTH2_H__ +#define __DC_OAUTH2_H__ +#ifdef __cplusplus +extern "C" { +#endif + + +#define DC_REGENERATE 0x01 + +char* dc_oauth2_get_access_token(dc_context_t*, const char* code, int flags); + + +#ifdef __cplusplus +} /* /extern "C" */ +#endif +#endif /* __DC_OAUTH2_H__ */ diff --git a/src/dc_smtp.c b/src/dc_smtp.c index e14aac65..e1090d1d 100644 --- a/src/dc_smtp.c +++ b/src/dc_smtp.c @@ -3,6 +3,7 @@ #include "dc_context.h" #include "dc_smtp.h" #include "dc_job.h" +#include "dc_oauth2.h" #ifndef DEBUG_SMTP @@ -180,15 +181,14 @@ int dc_smtp_connect(dc_smtp_t* smtp, const dc_loginparam_t* lp) if (lp->server_flags&DC_LP_AUTH_OAUTH2) { dc_log_info(smtp->context, 0, "SMTP-OAuth2 connect..."); - if (lp->send_pw==NULL || lp->send_pw[0]==0) { - dc_log_event_seq(smtp->context, DC_EVENT_ERROR_NETWORK, &smtp->log_connect_errors, - "SMTP-OAuth2 token missing for %s@%s:%i.", lp->send_user, lp->send_server, (int)lp->send_port); - goto cleanup; - } - - if ((r = mailsmtp_oauth2_authenticate(smtp->etpan, lp->send_user, lp->send_pw)) != MAILSMTP_NO_ERROR) { - r = mailsmtp_oauth2_outlook_authenticate(smtp->etpan, lp->send_user, lp->send_pw); + char* access_token = dc_oauth2_get_access_token(smtp->context, lp->send_pw, 0); + // if we want to support outlook, there is a mailsmtp_oauth2_outlook_authenticate() ... + if ((r = mailsmtp_oauth2_authenticate(smtp->etpan, lp->send_user, access_token)) != MAILSMTP_NO_ERROR) { + free(access_token); + access_token = dc_oauth2_get_access_token(smtp->context, lp->send_pw, DC_REGENERATE); + r = mailsmtp_oauth2_authenticate(smtp->etpan, lp->send_user, access_token); } + free(access_token); } else if((r=mailsmtp_auth(smtp->etpan, lp->send_user, lp->send_pw))!=MAILSMTP_NO_ERROR) { diff --git a/src/meson.build b/src/meson.build index 1dd86ca5..c378980d 100644 --- a/src/meson.build +++ b/src/meson.build @@ -28,6 +28,7 @@ lib_src = [ 'dc_mimeparser.c', 'dc_msg.c', 'dc_openssl.c', + 'dc_oauth2.c', 'dc_param.c', 'dc_pgp.c', 'dc_saxparser.c',