diff --git a/Cargo.toml b/Cargo.toml index 80e29415b..7f1dffd1b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,11 +23,14 @@ native-tls = "0.2.3" lettre = "0.9.0" imap = "1.0.1" mmime = { git = "https://github.com/dignifiedquire/mmime" } -base64 = "0.10.1" -charset = "0.1.2" +base64 = "0.10" +charset = "0.1" +percent-encoding = "1.0" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" [dev-dependencies] -tempfile = "3.0.7" +tempfile = "3.0" [workspace] members = [ diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index 3e9ac76cb..29d27c2b3 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -123,9 +123,14 @@ pub unsafe extern "C" fn dc_get_oauth2_url( redirect: *mut libc::c_char, ) -> *mut libc::c_char { assert!(!context.is_null()); - let context = &*context; - dc_oauth2::dc_get_oauth2_url(context, addr, redirect) + let context = &*context; + let addr = dc_tools::to_string(addr); + let redirect = dc_tools::to_string(redirect); + match oauth2::dc_get_oauth2_url(context, addr, redirect) { + Some(res) => libc::strdup(dc_tools::to_cstring(res).as_ptr()), + None => std::ptr::null_mut(), + } } #[no_mangle] diff --git a/examples/repl/cmdline.rs b/examples/repl/cmdline.rs index 547eb8755..04baad93e 100644 --- a/examples/repl/cmdline.rs +++ b/examples/repl/cmdline.rs @@ -16,7 +16,6 @@ use deltachat::dc_imap::*; use deltachat::dc_imex::*; use deltachat::dc_job::*; use deltachat::dc_jobthread::*; -use deltachat::dc_jsmn::*; use deltachat::dc_key::*; use deltachat::dc_keyhistory::*; use deltachat::dc_keyring::*; @@ -28,7 +27,6 @@ use deltachat::dc_mimefactory::*; use deltachat::dc_mimeparser::*; use deltachat::dc_move::*; use deltachat::dc_msg::*; -use deltachat::dc_oauth2::*; use deltachat::dc_param::*; use deltachat::dc_pgp::*; use deltachat::dc_qr::*; @@ -43,6 +41,7 @@ use deltachat::dc_strbuilder::*; use deltachat::dc_strencode::*; use deltachat::dc_token::*; use deltachat::dc_tools::*; +use deltachat::oauth2::*; use deltachat::peerstate::*; use deltachat::types::*; use deltachat::x::*; diff --git a/examples/repl/main.rs b/examples/repl/main.rs index e36972e58..6f8e347b1 100644 --- a/examples/repl/main.rs +++ b/examples/repl/main.rs @@ -35,7 +35,6 @@ use deltachat::dc_imap::*; use deltachat::dc_imex::*; use deltachat::dc_job::*; use deltachat::dc_jobthread::*; -use deltachat::dc_jsmn::*; use deltachat::dc_key::*; use deltachat::dc_keyhistory::*; use deltachat::dc_keyring::*; @@ -47,7 +46,6 @@ use deltachat::dc_mimefactory::*; use deltachat::dc_mimeparser::*; use deltachat::dc_move::*; use deltachat::dc_msg::*; -use deltachat::dc_oauth2::*; use deltachat::dc_param::*; use deltachat::dc_pgp::*; use deltachat::dc_qr::*; @@ -62,6 +60,7 @@ use deltachat::dc_strbuilder::*; use deltachat::dc_strencode::*; use deltachat::dc_token::*; use deltachat::dc_tools::*; +use deltachat::oauth2::*; use deltachat::peerstate::*; use deltachat::types::*; use deltachat::x::*; @@ -427,22 +426,19 @@ unsafe fn main_0(argc: libc::c_int, argv: *mut *mut libc::c_char) -> libc::c_int if addr.is_null() || *addr.offset(0isize) as libc::c_int == 0i32 { printf(b"oauth2: set addr first.\n\x00" as *const u8 as *const libc::c_char); } else { - let oauth2_url: *mut libc::c_char = dc_get_oauth2_url( + let oauth2_url = dc_get_oauth2_url( &ctx.read().unwrap(), - addr, - b"chat.delta:/com.b44t.messenger\x00" as *const u8 as *const libc::c_char, + to_str(addr), + "chat.delta:/com.b44t.messenger", ); - if oauth2_url.is_null() { + if oauth2_url.is_none() { printf( b"OAuth2 not available for %s.\n\x00" as *const u8 as *const libc::c_char, addr, ); } else { - printf(b"Open the following url, set mail_pw to the generated token and server_flags to 2:\n%s\n\x00" - as *const u8 as *const libc::c_char, - oauth2_url); + println!("Open the following url, set mail_pw to the generated token and server_flags to 2:\n{}", oauth2_url.unwrap()); } - free(oauth2_url as *mut libc::c_void); } free(addr as *mut libc::c_void); } else if strcmp(cmd, b"clear\x00" as *const u8 as *const libc::c_char) == 0i32 { diff --git a/src/dc_configure.rs b/src/dc_configure.rs index a3d0b93bf..e08e1fb39 100644 --- a/src/dc_configure.rs +++ b/src/dc_configure.rs @@ -5,11 +5,11 @@ use crate::dc_imap::*; use crate::dc_job::*; use crate::dc_log::*; use crate::dc_loginparam::*; -use crate::dc_oauth2::*; use crate::dc_saxparser::*; use crate::dc_sqlite3::*; use crate::dc_strencode::*; use crate::dc_tools::*; +use crate::oauth2::*; use crate::types::*; use crate::x::*; @@ -205,11 +205,14 @@ pub unsafe fn dc_job_do_DC_JOB_CONFIGURE_IMAP(context: &dc_context_t, _job: *mut }) as uintptr_t, 0i32 as uintptr_t, ); - let oauth2_addr: *mut libc::c_char = - dc_get_oauth2_addr(context, (*param).addr, (*param).mail_pw); - if !oauth2_addr.is_null() { + let oauth2_addr = dc_get_oauth2_addr( + context, + to_str((*param).addr), + to_str((*param).mail_pw), + ); + if oauth2_addr.is_some() { free((*param).addr as *mut libc::c_void); - (*param).addr = oauth2_addr; + (*param).addr = strdup(to_cstring(oauth2_addr.unwrap()).as_ptr()); dc_sqlite3_set_config( context, &context.sql.clone().read().unwrap(), diff --git a/src/dc_contact.rs b/src/dc_contact.rs index 55bf4cee7..c26eeacd8 100644 --- a/src/dc_contact.rs +++ b/src/dc_contact.rs @@ -839,7 +839,7 @@ pub unsafe fn dc_get_contact_encrinfo( .map(|k| k.formatted_fingerprint_c()) .unwrap_or(std::ptr::null_mut()); if peerstate.addr.is_some() - && to_str((*loginparam).addr) < peerstate.addr.as_ref().unwrap() + && to_str((*loginparam).addr) < peerstate.addr.as_ref().unwrap().as_str() { cat_fingerprint( &mut ret, diff --git a/src/dc_imap.rs b/src/dc_imap.rs index fd429de2c..dfb664cba 100644 --- a/src/dc_imap.rs +++ b/src/dc_imap.rs @@ -1,4 +1,4 @@ -use std::ffi::{CStr, CString}; +use std::ffi::CString; use std::net; use std::sync::{Arc, Condvar, Mutex, RwLock}; use std::time::{Duration, SystemTime}; @@ -7,9 +7,9 @@ use crate::constants::*; use crate::dc_context::dc_context_t; use crate::dc_log::*; use crate::dc_loginparam::*; -use crate::dc_oauth2::dc_get_oauth2_access_token; use crate::dc_sqlite3::*; use crate::dc_tools::{to_str, to_string}; +use crate::oauth2::dc_get_oauth2_access_token; use crate::types::*; pub const DC_IMAP_SEEN: usize = 0x0001; @@ -413,22 +413,17 @@ impl Imap { if (server_flags & DC_LP_AUTH_OAUTH2) != 0 { let addr: &str = config.addr.as_ref(); - let access_token = unsafe { - CStr::from_ptr(dc_get_oauth2_access_token( - context, - CString::new(addr).unwrap().as_ptr(), - CString::new(imap_pw).unwrap().as_ptr(), - DC_REGENERATE as libc::c_int, - )) - .to_str() - .unwrap() - }; - - let auth = OAuth2 { - user: imap_user.into(), - access_token: access_token.into(), - }; - client.authenticate("XOAUTH2", &auth) + if let Some(token) = + dc_get_oauth2_access_token(context, addr, imap_pw, DC_REGENERATE as usize) + { + let auth = OAuth2 { + user: imap_user.into(), + access_token: token, + }; + client.authenticate("XOAUTH2", &auth) + } else { + return 0; + } } else { client.login(imap_user, imap_pw) } diff --git a/src/dc_jsmn.rs b/src/dc_jsmn.rs deleted file mode 100644 index 1921ccbeb..000000000 --- a/src/dc_jsmn.rs +++ /dev/null @@ -1,405 +0,0 @@ -use crate::types::*; - -/* -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. -*/ -/* * - * JSON type identifier. Basic types are: - * o Object - * o Array - * o String - * o Other primitive: number, boolean (true/false) or null - */ -pub type jsmntype_t = libc::c_uint; -pub const JSMN_PRIMITIVE: jsmntype_t = 4; -pub const JSMN_STRING: jsmntype_t = 3; -pub const JSMN_ARRAY: jsmntype_t = 2; -pub const JSMN_OBJECT: jsmntype_t = 1; -pub const JSMN_UNDEFINED: jsmntype_t = 0; -pub type jsmnerr = libc::c_int; -/* The string is not a full JSON packet, more bytes expected */ -pub const JSMN_ERROR_PART: jsmnerr = -3; -/* Invalid character inside JSON string */ -pub const JSMN_ERROR_INVAL: jsmnerr = -2; -/* Not enough tokens were provided */ -pub const JSMN_ERROR_NOMEM: jsmnerr = -1; -/* * - * JSON token description. - * type type (object, array, string etc.) - * start start position in JSON data string - * end end position in JSON data string - */ -#[derive(Copy, Clone)] -#[repr(C)] -pub struct jsmntok_t { - pub type_0: jsmntype_t, - pub start: libc::c_int, - pub end: libc::c_int, - pub size: libc::c_int, -} -/* * - * JSON parser. Contains an array of token blocks available. Also stores - * the string being parsed now and current position in that string - */ -#[derive(Copy, Clone)] -#[repr(C)] -pub struct jsmn_parser { - pub pos: libc::c_uint, - pub toknext: libc::c_uint, - pub toksuper: libc::c_int, -} - -/* * - * Create JSON parser over an array of tokens - */ -pub unsafe fn jsmn_init(mut parser: *mut jsmn_parser) { - (*parser).pos = 0i32 as libc::c_uint; - (*parser).toknext = 0i32 as libc::c_uint; - (*parser).toksuper = -1i32; -} - -/* * - * Run JSON parser. It parses a JSON data string into and array of tokens, each describing - * a single JSON object. - */ -pub unsafe fn jsmn_parse( - mut parser: *mut jsmn_parser, - js: *const libc::c_char, - len: size_t, - tokens: *mut jsmntok_t, - num_tokens: libc::c_uint, -) -> libc::c_int { - let mut r: libc::c_int; - let mut i: libc::c_int; - let mut token: *mut jsmntok_t; - let mut count: libc::c_int = (*parser).toknext as libc::c_int; - while (*parser).pos < len as libc::c_uint - && *js.offset((*parser).pos as isize) as libc::c_int != '\u{0}' as i32 - { - let c: libc::c_char; - let type_0: jsmntype_t; - c = *js.offset((*parser).pos as isize); - match c as libc::c_int { - 123 | 91 => { - count += 1; - if !tokens.is_null() { - token = jsmn_alloc_token(parser, tokens, num_tokens as size_t); - if token.is_null() { - return JSMN_ERROR_NOMEM as libc::c_int; - } - if (*parser).toksuper != -1i32 { - let ref mut fresh0 = (*tokens.offset((*parser).toksuper as isize)).size; - *fresh0 += 1 - } - (*token).type_0 = (if c as libc::c_int == '{' as i32 { - JSMN_OBJECT as libc::c_int - } else { - JSMN_ARRAY as libc::c_int - }) as jsmntype_t; - (*token).start = (*parser).pos as libc::c_int; - (*parser).toksuper = - (*parser).toknext.wrapping_sub(1i32 as libc::c_uint) as libc::c_int - } - } - 125 | 93 => { - if !tokens.is_null() { - type_0 = (if c as libc::c_int == '}' as i32 { - JSMN_OBJECT as libc::c_int - } else { - JSMN_ARRAY as libc::c_int - }) as jsmntype_t; - i = (*parser).toknext.wrapping_sub(1i32 as libc::c_uint) as libc::c_int; - while i >= 0i32 { - token = &mut *tokens.offset(i as isize) as *mut jsmntok_t; - if (*token).start != -1i32 && (*token).end == -1i32 { - if (*token).type_0 as libc::c_uint != type_0 as libc::c_uint { - return JSMN_ERROR_INVAL as libc::c_int; - } - (*parser).toksuper = -1i32; - (*token).end = - (*parser).pos.wrapping_add(1i32 as libc::c_uint) as libc::c_int; - break; - } else { - i -= 1 - } - } - if i == -1i32 { - return JSMN_ERROR_INVAL as libc::c_int; - } - while i >= 0i32 { - token = &mut *tokens.offset(i as isize) as *mut jsmntok_t; - if (*token).start != -1i32 && (*token).end == -1i32 { - (*parser).toksuper = i; - break; - } else { - i -= 1 - } - } - } - } - 34 => { - r = jsmn_parse_string(parser, js, len, tokens, num_tokens as size_t); - if r < 0i32 { - return r; - } - count += 1; - if (*parser).toksuper != -1i32 && !tokens.is_null() { - let ref mut fresh1 = (*tokens.offset((*parser).toksuper as isize)).size; - *fresh1 += 1 - } - } - 9 | 13 | 10 | 32 => {} - 58 => { - (*parser).toksuper = - (*parser).toknext.wrapping_sub(1i32 as libc::c_uint) as libc::c_int - } - 44 => { - if !tokens.is_null() - && (*parser).toksuper != -1i32 - && (*tokens.offset((*parser).toksuper as isize)).type_0 as libc::c_uint - != JSMN_ARRAY as libc::c_int as libc::c_uint - && (*tokens.offset((*parser).toksuper as isize)).type_0 as libc::c_uint - != JSMN_OBJECT as libc::c_int as libc::c_uint - { - i = (*parser).toknext.wrapping_sub(1i32 as libc::c_uint) as libc::c_int; - while i >= 0i32 { - if (*tokens.offset(i as isize)).type_0 as libc::c_uint - == JSMN_ARRAY as libc::c_int as libc::c_uint - || (*tokens.offset(i as isize)).type_0 as libc::c_uint - == JSMN_OBJECT as libc::c_int as libc::c_uint - { - if (*tokens.offset(i as isize)).start != -1i32 - && (*tokens.offset(i as isize)).end == -1i32 - { - (*parser).toksuper = i; - break; - } - } - i -= 1 - } - } - } - _ => { - r = jsmn_parse_primitive(parser, js, len, tokens, num_tokens as size_t); - if r < 0i32 { - return r; - } - count += 1; - if (*parser).toksuper != -1i32 && !tokens.is_null() { - let ref mut fresh2 = (*tokens.offset((*parser).toksuper as isize)).size; - *fresh2 += 1 - } - } - } - (*parser).pos = (*parser).pos.wrapping_add(1) - } - if !tokens.is_null() { - i = (*parser).toknext.wrapping_sub(1i32 as libc::c_uint) as libc::c_int; - while i >= 0i32 { - if (*tokens.offset(i as isize)).start != -1i32 - && (*tokens.offset(i as isize)).end == -1i32 - { - return JSMN_ERROR_PART as libc::c_int; - } - i -= 1 - } - } - - count -} - -/* * - * Fills next available token with JSON primitive. - */ -unsafe fn jsmn_parse_primitive( - mut parser: *mut jsmn_parser, - js: *const libc::c_char, - len: size_t, - tokens: *mut jsmntok_t, - num_tokens: size_t, -) -> libc::c_int { - let token: *mut jsmntok_t; - let start: libc::c_int; - start = (*parser).pos as libc::c_int; - while (*parser).pos < len as libc::c_uint - && *js.offset((*parser).pos as isize) as libc::c_int != '\u{0}' as i32 - { - match *js.offset((*parser).pos as isize) as libc::c_int { - 58 | 9 | 13 | 10 | 32 | 44 | 93 | 125 => { - break; - } - _ => {} - } - if (*js.offset((*parser).pos as isize) as libc::c_int) < 32i32 - || *js.offset((*parser).pos as isize) as libc::c_int >= 127i32 - { - (*parser).pos = start as libc::c_uint; - return JSMN_ERROR_INVAL as libc::c_int; - } - (*parser).pos = (*parser).pos.wrapping_add(1) - } - if tokens.is_null() { - (*parser).pos = (*parser).pos.wrapping_sub(1); - return 0i32; - } - token = jsmn_alloc_token(parser, tokens, num_tokens); - if token.is_null() { - (*parser).pos = start as libc::c_uint; - return JSMN_ERROR_NOMEM as libc::c_int; - } - jsmn_fill_token(token, JSMN_PRIMITIVE, start, (*parser).pos as libc::c_int); - (*parser).pos = (*parser).pos.wrapping_sub(1); - - 0 -} - -/* * - * Fills token type and boundaries. - */ -unsafe fn jsmn_fill_token( - mut token: *mut jsmntok_t, - type_0: jsmntype_t, - start: libc::c_int, - end: libc::c_int, -) { - (*token).type_0 = type_0; - (*token).start = start; - (*token).end = end; - (*token).size = 0i32; -} - -/* -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. -*/ - -/* * - * Allocates a fresh unused token from the token pool. - */ -unsafe fn jsmn_alloc_token( - mut parser: *mut jsmn_parser, - tokens: *mut jsmntok_t, - num_tokens: size_t, -) -> *mut jsmntok_t { - let mut tok: *mut jsmntok_t; - if (*parser).toknext as size_t >= num_tokens { - return 0 as *mut jsmntok_t; - } - let fresh3 = (*parser).toknext; - (*parser).toknext = (*parser).toknext.wrapping_add(1); - tok = &mut *tokens.offset(fresh3 as isize) as *mut jsmntok_t; - (*tok).end = -1i32; - (*tok).start = (*tok).end; - (*tok).size = 0i32; - - tok -} - -/* * - * Fills next token with JSON string. - */ -unsafe fn jsmn_parse_string( - mut parser: *mut jsmn_parser, - js: *const libc::c_char, - len: size_t, - tokens: *mut jsmntok_t, - num_tokens: size_t, -) -> libc::c_int { - let token: *mut jsmntok_t; - let start: libc::c_int = (*parser).pos as libc::c_int; - (*parser).pos = (*parser).pos.wrapping_add(1); - while ((*parser).pos as size_t) < len - && *js.offset((*parser).pos as isize) as libc::c_int != '\u{0}' as i32 - { - let c: libc::c_char = *js.offset((*parser).pos as isize); - if c as libc::c_int == '\"' as i32 { - if tokens.is_null() { - return 0i32; - } - token = jsmn_alloc_token(parser, tokens, num_tokens); - if token.is_null() { - (*parser).pos = start as libc::c_uint; - return JSMN_ERROR_NOMEM as libc::c_int; - } - jsmn_fill_token( - token, - JSMN_STRING, - start + 1i32, - (*parser).pos as libc::c_int, - ); - return 0i32; - } - if c as libc::c_int == '\\' as i32 && ((*parser).pos.wrapping_add(1) as size_t) < len { - let mut i: libc::c_int; - (*parser).pos = (*parser).pos.wrapping_add(1); - match *js.offset((*parser).pos as isize) as libc::c_int { - 34 | 47 | 92 | 98 | 102 | 114 | 110 | 116 => {} - 117 => { - (*parser).pos = (*parser).pos.wrapping_add(1); - i = 0i32; - while i < 4i32 - && ((*parser).pos as size_t) < len - && *js.offset((*parser).pos as isize) as libc::c_int != '\u{0}' as i32 - { - if !(*js.offset((*parser).pos as isize) as libc::c_int >= 48i32 - && *js.offset((*parser).pos as isize) as libc::c_int <= 57i32 - || *js.offset((*parser).pos as isize) as libc::c_int >= 65i32 - && *js.offset((*parser).pos as isize) as libc::c_int <= 70i32 - || *js.offset((*parser).pos as isize) as libc::c_int >= 97i32 - && *js.offset((*parser).pos as isize) as libc::c_int <= 102i32) - { - (*parser).pos = start as libc::c_uint; - return JSMN_ERROR_INVAL as libc::c_int; - } - (*parser).pos = (*parser).pos.wrapping_add(1); - i += 1 - } - (*parser).pos = (*parser).pos.wrapping_sub(1) - } - _ => { - (*parser).pos = start as libc::c_uint; - return JSMN_ERROR_INVAL as libc::c_int; - } - } - } - (*parser).pos = (*parser).pos.wrapping_add(1) - } - (*parser).pos = start as libc::c_uint; - - JSMN_ERROR_PART as libc::c_int -} diff --git a/src/dc_oauth2.rs b/src/dc_oauth2.rs deleted file mode 100644 index 930ac2f90..000000000 --- a/src/dc_oauth2.rs +++ /dev/null @@ -1,621 +0,0 @@ -use crate::constants::Event; -use crate::dc_contact::*; -use crate::dc_context::dc_context_t; -use crate::dc_jsmn::*; -use crate::dc_log::*; -use crate::dc_sqlite3::*; -use crate::dc_strencode::*; -use crate::dc_tools::*; -use crate::types::*; -use crate::x::*; - -#[derive(Copy, Clone)] -#[repr(C)] -pub struct oauth2_t { - pub client_id: *mut libc::c_char, - pub get_code: *mut libc::c_char, - pub init_token: *mut libc::c_char, - pub refresh_token: *mut libc::c_char, - pub get_userinfo: *mut libc::c_char, -} - -pub unsafe fn dc_get_oauth2_url( - context: &dc_context_t, - addr: *const libc::c_char, - redirect_uri: *const libc::c_char, -) -> *mut libc::c_char { - let mut oauth2: *mut oauth2_t = 0 as *mut oauth2_t; - let mut oauth2_url: *mut libc::c_char = 0 as *mut libc::c_char; - if !(redirect_uri.is_null() || *redirect_uri.offset(0isize) as libc::c_int == 0i32) { - oauth2 = get_info(addr); - if !oauth2.is_null() { - dc_sqlite3_set_config( - context, - &context.sql.clone().read().unwrap(), - b"oauth2_pending_redirect_uri\x00" as *const u8 as *const libc::c_char, - redirect_uri, - ); - oauth2_url = dc_strdup((*oauth2).get_code); - replace_in_uri( - &mut oauth2_url, - b"$CLIENT_ID\x00" as *const u8 as *const libc::c_char, - (*oauth2).client_id, - ); - replace_in_uri( - &mut oauth2_url, - b"$REDIRECT_URI\x00" as *const u8 as *const libc::c_char, - redirect_uri, - ); - } - } - free(oauth2 as *mut libc::c_void); - - oauth2_url -} - -unsafe fn replace_in_uri( - uri: *mut *mut libc::c_char, - key: *const libc::c_char, - value: *const libc::c_char, -) { - if !uri.is_null() && !key.is_null() && !value.is_null() { - let value_urlencoded: *mut libc::c_char = dc_urlencode(value); - dc_str_replace(uri, key, value_urlencoded); - free(value_urlencoded as *mut libc::c_void); - }; -} - -unsafe fn get_info(addr: *const libc::c_char) -> *mut oauth2_t { - let mut oauth2: *mut oauth2_t = 0 as *mut oauth2_t; - let addr_normalized: *mut libc::c_char; - let mut domain: *const libc::c_char; - addr_normalized = dc_addr_normalize(addr); - domain = strchr(addr_normalized, '@' as i32); - if !(domain.is_null() || *domain.offset(0isize) as libc::c_int == 0i32) { - domain = domain.offset(1isize); - if strcasecmp(domain, b"gmail.com\x00" as *const u8 as *const libc::c_char) == 0i32 - || strcasecmp( - domain, - b"googlemail.com\x00" as *const u8 as *const libc::c_char, - ) == 0i32 - { - oauth2 = calloc(1, ::std::mem::size_of::()) as *mut oauth2_t; - (*oauth2).client_id = - b"959970109878-4mvtgf6feshskf7695nfln6002mom908.apps.googleusercontent.com\x00" - as *const u8 as *const libc::c_char as *mut libc::c_char; - (*oauth2).get_code = - b"https://accounts.google.com/o/oauth2/auth?client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URI&response_type=code&scope=https%3A%2F%2Fmail.google.com%2F%20email&access_type=offline\x00" - as *const u8 as *const libc::c_char as *mut libc::c_char; - (*oauth2).init_token = - b"https://accounts.google.com/o/oauth2/token?client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URI&code=$CODE&grant_type=authorization_code\x00" - as *const u8 as *const libc::c_char as *mut libc::c_char; - (*oauth2).refresh_token = - b"https://accounts.google.com/o/oauth2/token?client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URI&refresh_token=$REFRESH_TOKEN&grant_type=refresh_token\x00" - as *const u8 as *const libc::c_char as *mut libc::c_char; - (*oauth2).get_userinfo = - b"https://www.googleapis.com/oauth2/v1/userinfo?alt=json&access_token=$ACCESS_TOKEN\x00" - as *const u8 as *const libc::c_char as *mut libc::c_char - } else if strcasecmp( - domain, - b"yandex.com\x00" as *const u8 as *const libc::c_char, - ) == 0i32 - || strcasecmp(domain, b"yandex.ru\x00" as *const u8 as *const libc::c_char) == 0i32 - || strcasecmp(domain, b"yandex.ua\x00" as *const u8 as *const libc::c_char) == 0i32 - { - oauth2 = calloc(1, ::std::mem::size_of::()) as *mut oauth2_t; - (*oauth2).client_id = b"c4d0b6735fc8420a816d7e1303469341\x00" as *const u8 - as *const libc::c_char as *mut libc::c_char; - (*oauth2).get_code = - b"https://oauth.yandex.com/authorize?client_id=$CLIENT_ID&response_type=code&scope=mail%3Aimap_full%20mail%3Asmtp&force_confirm=true\x00" - as *const u8 as *const libc::c_char as *mut libc::c_char; - (*oauth2).init_token = - b"https://oauth.yandex.com/token?grant_type=authorization_code&code=$CODE&client_id=$CLIENT_ID&client_secret=58b8c6e94cf44fbe952da8511955dacf\x00" - as *const u8 as *const libc::c_char as *mut libc::c_char; - (*oauth2).refresh_token = - b"https://oauth.yandex.com/token?grant_type=refresh_token&refresh_token=$REFRESH_TOKEN&client_id=$CLIENT_ID&client_secret=58b8c6e94cf44fbe952da8511955dacf\x00" - as *const u8 as *const libc::c_char as *mut libc::c_char - } - } - free(addr_normalized as *mut libc::c_void); - - oauth2 -} - -// the following function may block due http-requests; -// must not be called from the main thread or by the ui! -pub unsafe fn dc_get_oauth2_access_token( - context: &dc_context_t, - addr: *const libc::c_char, - code: *const libc::c_char, - flags: libc::c_int, -) -> *mut libc::c_char { - let current_block: u64; - let mut oauth2: *mut oauth2_t = 0 as *mut oauth2_t; - let mut access_token: *mut libc::c_char = 0 as *mut libc::c_char; - let mut refresh_token: *mut libc::c_char = 0 as *mut libc::c_char; - let mut refresh_token_for: *mut libc::c_char = 0 as *mut libc::c_char; - let mut redirect_uri: *mut libc::c_char = 0 as *mut libc::c_char; - let mut update_redirect_uri_on_success: libc::c_int = 0i32; - let mut token_url: *mut libc::c_char = 0 as *mut libc::c_char; - let mut expires_in: time_t = 0i32 as time_t; - let mut error: *mut libc::c_char = 0 as *mut libc::c_char; - let mut error_description: *mut libc::c_char = 0 as *mut libc::c_char; - let mut json: *mut libc::c_char = 0 as *mut libc::c_char; - let mut parser: jsmn_parser = jsmn_parser { - pos: 0, - toknext: 0, - toksuper: 0, - }; - // we do not expect nor read more tokens - let mut tok: [jsmntok_t; 128] = [jsmntok_t { - type_0: JSMN_UNDEFINED, - start: 0, - end: 0, - size: 0, - }; 128]; - let tok_cnt: libc::c_int; - if code.is_null() || *code.offset(0isize) as libc::c_int == 0i32 { - dc_log_warning( - context, - 0i32, - b"Internal OAuth2 error\x00" as *const u8 as *const libc::c_char, - ); - } else { - oauth2 = get_info(addr); - if oauth2.is_null() { - dc_log_warning( - context, - 0i32, - b"Internal OAuth2 error: 2\x00" as *const u8 as *const libc::c_char, - ); - } else { - let lock = context.oauth2_critical.clone(); - - let l = lock.lock().unwrap(); - // read generated token - if 0 == flags & 0x1i32 && !is_expired(context) { - access_token = dc_sqlite3_get_config( - context, - &context.sql.clone().read().unwrap(), - b"oauth2_access_token\x00" as *const u8 as *const libc::c_char, - 0 as *const libc::c_char, - ); - if !access_token.is_null() { - // success - current_block = 16914036240511706173; - } else { - current_block = 2838571290723028321; - } - } else { - current_block = 2838571290723028321; - } - match current_block { - 16914036240511706173 => {} - _ => { - refresh_token = dc_sqlite3_get_config( - context, - &context.sql.clone().read().unwrap(), - b"oauth2_refresh_token\x00" as *const u8 as *const libc::c_char, - 0 as *const libc::c_char, - ); - refresh_token_for = dc_sqlite3_get_config( - context, - &context.sql.clone().read().unwrap(), - b"oauth2_refresh_token_for\x00" as *const u8 as *const libc::c_char, - b"unset\x00" as *const u8 as *const libc::c_char, - ); - if refresh_token.is_null() || strcmp(refresh_token_for, code) != 0i32 { - dc_log_info( - context, - 0i32, - b"Generate OAuth2 refresh_token and access_token...\x00" as *const u8 - as *const libc::c_char, - ); - redirect_uri = dc_sqlite3_get_config( - context, - &context.sql.clone().read().unwrap(), - b"oauth2_pending_redirect_uri\x00" as *const u8 as *const libc::c_char, - b"unset\x00" as *const u8 as *const libc::c_char, - ); - update_redirect_uri_on_success = 1i32; - token_url = dc_strdup((*oauth2).init_token) - } else { - dc_log_info( - context, - 0i32, - b"Regenerate OAuth2 access_token by refresh_token...\x00" as *const u8 - as *const libc::c_char, - ); - redirect_uri = dc_sqlite3_get_config( - context, - &context.sql.clone().read().unwrap(), - b"oauth2_redirect_uri\x00" as *const u8 as *const libc::c_char, - b"unset\x00" as *const u8 as *const libc::c_char, - ); - token_url = dc_strdup((*oauth2).refresh_token) - } - replace_in_uri( - &mut token_url, - b"$CLIENT_ID\x00" as *const u8 as *const libc::c_char, - (*oauth2).client_id, - ); - replace_in_uri( - &mut token_url, - b"$REDIRECT_URI\x00" as *const u8 as *const libc::c_char, - redirect_uri, - ); - replace_in_uri( - &mut token_url, - b"$CODE\x00" as *const u8 as *const libc::c_char, - code, - ); - replace_in_uri( - &mut token_url, - b"$REFRESH_TOKEN\x00" as *const u8 as *const libc::c_char, - refresh_token, - ); - json = (context.cb)( - context, - Event::HTTP_POST, - token_url as uintptr_t, - 0i32 as uintptr_t, - ) as *mut libc::c_char; - if json.is_null() { - dc_log_warning( - context, - 0i32, - b"Error calling OAuth2 at %s\x00" as *const u8 as *const libc::c_char, - token_url, - ); - } else { - jsmn_init(&mut parser); - tok_cnt = jsmn_parse( - &mut parser, - json, - strlen(json), - tok.as_mut_ptr(), - (::std::mem::size_of::<[jsmntok_t; 128]>() as libc::c_ulong) - .wrapping_div(::std::mem::size_of::() as libc::c_ulong) - as libc::c_uint, - ); - if tok_cnt < 2i32 - || tok[0usize].type_0 as libc::c_uint - != JSMN_OBJECT as libc::c_int as libc::c_uint - { - dc_log_warning( - context, - 0i32, - b"Failed to parse OAuth2 json from %s\x00" as *const u8 - as *const libc::c_char, - token_url, - ); - } else { - let mut i: libc::c_int = 1i32; - while i < tok_cnt { - if access_token.is_null() - && jsoneq( - json, - &mut *tok.as_mut_ptr().offset(i as isize), - b"access_token\x00" as *const u8 as *const libc::c_char, - ) == 0i32 - { - access_token = jsondup( - json, - &mut *tok.as_mut_ptr().offset((i + 1i32) as isize), - ) - } else if refresh_token.is_null() - && jsoneq( - json, - &mut *tok.as_mut_ptr().offset(i as isize), - b"refresh_token\x00" as *const u8 as *const libc::c_char, - ) == 0i32 - { - refresh_token = jsondup( - json, - &mut *tok.as_mut_ptr().offset((i + 1i32) as isize), - ) - } else if jsoneq( - json, - &mut *tok.as_mut_ptr().offset(i as isize), - b"expires_in\x00" as *const u8 as *const libc::c_char, - ) == 0i32 - { - let expires_in_str: *mut libc::c_char = jsondup( - json, - &mut *tok.as_mut_ptr().offset((i + 1i32) as isize), - ); - if !expires_in_str.is_null() { - let val: time_t = atol(expires_in_str); - if val > 20 && val < (60 * 60 * 24 * 365 * 5) { - expires_in = val - } - free(expires_in_str as *mut libc::c_void); - } - } else if error.is_null() - && jsoneq( - json, - &mut *tok.as_mut_ptr().offset(i as isize), - b"error\x00" as *const u8 as *const libc::c_char, - ) == 0i32 - { - error = jsondup( - json, - &mut *tok.as_mut_ptr().offset((i + 1i32) as isize), - ) - } else if error_description.is_null() - && jsoneq( - json, - &mut *tok.as_mut_ptr().offset(i as isize), - b"error_description\x00" as *const u8 - as *const libc::c_char, - ) == 0i32 - { - error_description = jsondup( - json, - &mut *tok.as_mut_ptr().offset((i + 1i32) as isize), - ) - } - i += 1 - } - if !error.is_null() || !error_description.is_null() { - dc_log_warning( - context, - 0i32, - b"OAuth error: %s: %s\x00" as *const u8 as *const libc::c_char, - if !error.is_null() { - error - } else { - b"unknown\x00" as *const u8 as *const libc::c_char - }, - if !error_description.is_null() { - error_description - } else { - b"no details\x00" as *const u8 as *const libc::c_char - }, - ); - } - if !refresh_token.is_null() - && 0 != *refresh_token.offset(0isize) as libc::c_int - { - dc_sqlite3_set_config( - context, - &context.sql.clone().read().unwrap(), - b"oauth2_refresh_token\x00" as *const u8 as *const libc::c_char, - refresh_token, - ); - dc_sqlite3_set_config( - context, - &context.sql.clone().read().unwrap(), - b"oauth2_refresh_token_for\x00" as *const u8 - as *const libc::c_char, - code, - ); - } - // after that, save the access token. - // if it's unset, we may get it in the next round as we have the refresh_token now. - if access_token.is_null() - || *access_token.offset(0isize) as libc::c_int == 0i32 - { - dc_log_warning( - context, - 0i32, - b"Failed to find OAuth2 access token\x00" as *const u8 - as *const libc::c_char, - ); - } else { - dc_sqlite3_set_config( - context, - &context.sql.clone().read().unwrap(), - b"oauth2_access_token\x00" as *const u8 as *const libc::c_char, - access_token, - ); - dc_sqlite3_set_config_int64( - context, - &context.sql.clone().read().unwrap(), - b"oauth2_timestamp_expires\x00" as *const u8 - as *const libc::c_char, - (if 0 != expires_in { - time(0 as *mut time_t) + expires_in - 5 - } else { - 0 - }) as int64_t, - ); - if 0 != update_redirect_uri_on_success { - dc_sqlite3_set_config( - context, - &context.sql.clone().read().unwrap(), - b"oauth2_redirect_uri\x00" as *const u8 - as *const libc::c_char, - redirect_uri, - ); - } - } - } - } - } - } - drop(l); - } - } - free(refresh_token as *mut libc::c_void); - free(refresh_token_for as *mut libc::c_void); - free(redirect_uri as *mut libc::c_void); - free(token_url as *mut libc::c_void); - free(json as *mut libc::c_void); - free(error as *mut libc::c_void); - free(error_description as *mut libc::c_void); - free(oauth2 as *mut libc::c_void); - return if !access_token.is_null() { - access_token - } else { - dc_strdup(0 as *const libc::c_char) - }; -} - -unsafe fn jsondup(json: *const libc::c_char, tok: *mut jsmntok_t) -> *mut libc::c_char { - if (*tok).type_0 as libc::c_uint == JSMN_STRING as libc::c_int as libc::c_uint - || (*tok).type_0 as libc::c_uint == JSMN_PRIMITIVE as libc::c_int as libc::c_uint - { - return strndup( - json.offset((*tok).start as isize), - ((*tok).end - (*tok).start) as libc::c_ulong, - ); - } - - strdup(b"\x00" as *const u8 as *const libc::c_char) -} - -unsafe fn jsoneq( - json: *const libc::c_char, - tok: *mut jsmntok_t, - s: *const libc::c_char, -) -> libc::c_int { - if (*tok).type_0 as libc::c_uint == JSMN_STRING as libc::c_int as libc::c_uint - && strlen(s) as libc::c_int == (*tok).end - (*tok).start - && strncmp( - json.offset((*tok).start as isize), - s, - ((*tok).end - (*tok).start) as usize, - ) == 0i32 - { - return 0i32; - } - - -1 -} - -unsafe fn is_expired(context: &dc_context_t) -> bool { - let expire_timestamp: time_t = dc_sqlite3_get_config_int64( - context, - &context.sql.clone().read().unwrap(), - b"oauth2_timestamp_expires\x00" as *const u8 as *const libc::c_char, - 0i32 as int64_t, - ) as time_t; - if expire_timestamp <= 0 { - return false; - } - if expire_timestamp > time(0 as *mut time_t) { - return false; - } - - true -} - -pub unsafe fn dc_get_oauth2_addr( - context: &dc_context_t, - addr: *const libc::c_char, - code: *const libc::c_char, -) -> *mut libc::c_char { - let mut access_token: *mut libc::c_char = 0 as *mut libc::c_char; - let mut addr_out: *mut libc::c_char = 0 as *mut libc::c_char; - let oauth2: *mut oauth2_t; - if !({ - oauth2 = get_info(addr); - oauth2.is_null() - } || (*oauth2).get_userinfo.is_null()) - { - access_token = dc_get_oauth2_access_token(context, addr, code, 0i32); - addr_out = get_oauth2_addr(context, oauth2, access_token); - if addr_out.is_null() { - free(access_token as *mut libc::c_void); - access_token = dc_get_oauth2_access_token(context, addr, code, 0x1i32); - addr_out = get_oauth2_addr(context, oauth2, access_token) - } - } - free(access_token as *mut libc::c_void); - free(oauth2 as *mut libc::c_void); - - addr_out -} - -unsafe fn get_oauth2_addr( - context: &dc_context_t, - oauth2: *const oauth2_t, - access_token: *const libc::c_char, -) -> *mut libc::c_char { - let mut addr_out: *mut libc::c_char = 0 as *mut libc::c_char; - let mut userinfo_url: *mut libc::c_char = 0 as *mut libc::c_char; - let mut json: *mut libc::c_char = 0 as *mut libc::c_char; - let mut parser: jsmn_parser = jsmn_parser { - pos: 0, - toknext: 0, - toksuper: 0, - }; - // we do not expect nor read more tokens - let mut tok: [jsmntok_t; 128] = [jsmntok_t { - type_0: JSMN_UNDEFINED, - start: 0, - end: 0, - size: 0, - }; 128]; - let tok_cnt: libc::c_int; - if !(access_token.is_null() - || *access_token.offset(0isize) as libc::c_int == 0i32 - || oauth2.is_null()) - { - userinfo_url = dc_strdup((*oauth2).get_userinfo); - replace_in_uri( - &mut userinfo_url, - b"$ACCESS_TOKEN\x00" as *const u8 as *const libc::c_char, - access_token, - ); - json = (context.cb)( - context, - Event::HTTP_GET, - userinfo_url as uintptr_t, - 0i32 as uintptr_t, - ) as *mut libc::c_char; - if json.is_null() { - dc_log_warning( - context, - 0i32, - b"Error getting userinfo.\x00" as *const u8 as *const libc::c_char, - ); - } else { - jsmn_init(&mut parser); - tok_cnt = jsmn_parse( - &mut parser, - json, - strlen(json), - tok.as_mut_ptr(), - (::std::mem::size_of::<[jsmntok_t; 128]>() as libc::c_ulong) - .wrapping_div(::std::mem::size_of::() as libc::c_ulong) - as libc::c_uint, - ); - if tok_cnt < 2i32 - || tok[0usize].type_0 as libc::c_uint != JSMN_OBJECT as libc::c_int as libc::c_uint - { - dc_log_warning( - context, - 0i32, - b"Failed to parse userinfo.\x00" as *const u8 as *const libc::c_char, - ); - } else { - let mut i: libc::c_int = 1i32; - while i < tok_cnt { - if addr_out.is_null() - && jsoneq( - json, - &mut *tok.as_mut_ptr().offset(i as isize), - b"email\x00" as *const u8 as *const libc::c_char, - ) == 0i32 - { - addr_out = jsondup(json, &mut *tok.as_mut_ptr().offset((i + 1i32) as isize)) - } - i += 1 - } - if addr_out.is_null() { - dc_log_warning( - context, - 0i32, - b"E-mail missing in userinfo.\x00" as *const u8 as *const libc::c_char, - ); - } - } - } - } - free(userinfo_url as *mut libc::c_void); - free(json as *mut libc::c_void); - - addr_out -} diff --git a/src/dc_smtp.rs b/src/dc_smtp.rs index 3f38332fc..5bcf70713 100644 --- a/src/dc_smtp.rs +++ b/src/dc_smtp.rs @@ -8,7 +8,8 @@ use crate::constants::*; use crate::dc_context::dc_context_t; use crate::dc_log::*; use crate::dc_loginparam::*; -use crate::dc_oauth2::*; +use crate::dc_tools::*; +use crate::oauth2::*; use crate::x::*; pub struct Smtp { @@ -106,18 +107,15 @@ impl Smtp { let creds = if 0 != lp.server_flags & (DC_LP_AUTH_OAUTH2 as i32) { // oauth2 - - let access_token = - unsafe { dc_get_oauth2_access_token(context, lp.addr, lp.send_pw, 0i32) }; - if access_token.is_null() { + let addr = to_str(lp.addr); + let send_pw = to_str(lp.send_pw); + let access_token = dc_get_oauth2_access_token(context, addr, send_pw, 0); + if access_token.is_none() { return 0; } + let user = to_str(lp.send_user); - let user = unsafe { CStr::from_ptr(lp.send_user).to_str().unwrap().to_string() }; - let token = unsafe { CStr::from_ptr(access_token).to_str().unwrap().to_string() }; - unsafe { free(access_token as *mut libc::c_void) }; - - lettre::smtp::authentication::Credentials::new(user, token) + lettre::smtp::authentication::Credentials::new(user.into(), access_token.unwrap()) } else { // plain let user = unsafe { CStr::from_ptr(lp.send_user).to_str().unwrap().to_string() }; diff --git a/src/dc_tools.rs b/src/dc_tools.rs index d19220b39..7c9bb1d9f 100644 --- a/src/dc_tools.rs +++ b/src/dc_tools.rs @@ -1645,6 +1645,22 @@ pub unsafe fn dc_make_rel_and_copy( success } +pub fn to_cstring>(s: S) -> std::ffi::CString { + std::ffi::CString::new(s.as_ref()).unwrap() +} + +pub fn to_string(s: *const libc::c_char) -> String { + if s.is_null() { + return "".into(); + } + unsafe { std::ffi::CStr::from_ptr(s).to_str().unwrap().to_string() } +} + +pub fn to_str<'a>(s: *const libc::c_char) -> &'a str { + assert!(!s.is_null(), "cannot be used on null pointers"); + unsafe { std::ffi::CStr::from_ptr(s).to_str().unwrap() } +} + #[cfg(test)] mod tests { use super::*; @@ -1952,19 +1968,3 @@ mod tests { } } } - -pub fn to_cstring>(s: S) -> std::ffi::CString { - std::ffi::CString::new(s.as_ref()).unwrap() -} - -pub fn to_string(s: *const libc::c_char) -> String { - if s.is_null() { - return "".into(); - } - unsafe { std::ffi::CStr::from_ptr(s).to_str().unwrap().to_string() } -} - -pub fn to_str<'a>(s: *const libc::c_char) -> &'a str { - assert!(!s.is_null(), "cannot be used on null pointers"); - unsafe { std::ffi::CStr::from_ptr(s).to_str().unwrap() } -} diff --git a/src/lib.rs b/src/lib.rs index c3e25dad8..ce3c93251 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,6 +22,7 @@ extern crate smallvec; #[macro_use] pub mod dc_log; +pub mod oauth2; pub mod peerstate; pub mod types; pub mod x; @@ -42,7 +43,6 @@ pub mod dc_imap; pub mod dc_imex; pub mod dc_job; pub mod dc_jobthread; -pub mod dc_jsmn; pub mod dc_key; pub mod dc_keyhistory; pub mod dc_keyring; @@ -53,7 +53,6 @@ pub mod dc_mimefactory; pub mod dc_mimeparser; pub mod dc_move; pub mod dc_msg; -pub mod dc_oauth2; pub mod dc_param; pub mod dc_pgp; pub mod dc_qr; diff --git a/src/oauth2.rs b/src/oauth2.rs new file mode 100644 index 000000000..9b2a0bf67 --- /dev/null +++ b/src/oauth2.rs @@ -0,0 +1,393 @@ +use std::collections::HashMap; +use std::ffi::CString; + +use percent_encoding::{utf8_percent_encode, DEFAULT_ENCODE_SET}; +use serde::Deserialize; + +use crate::dc_context::dc_context_t; +use crate::dc_log::*; +use crate::dc_sqlite3::*; +use crate::dc_tools::*; +use crate::types::*; +use crate::x::*; + +const OAUTH2_GMAIL: Oauth2 = Oauth2 { + client_id: "959970109878-4mvtgf6feshskf7695nfln6002mom908.apps.googleusercontent.com", + get_code: "https://accounts.google.com/o/oauth2/auth?client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URI&response_type=code&scope=https%3A%2F%2Fmail.google.com%2F%20email&access_type=offline", + init_token: "https://accounts.google.com/o/oauth2/token?client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URI&code=$CODE&grant_type=authorization_code", + refresh_token: "https://accounts.google.com/o/oauth2/token?client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URI&refresh_token=$REFRESH_TOKEN&grant_type=refresh_token", + get_userinfo: Some("https://www.googleapis.com/oauth2/v1/userinfo?alt=json&access_token=$ACCESS_TOKEN"), +}; + +const OAUTH2_YANDEX: Oauth2 = Oauth2 { + client_id: "c4d0b6735fc8420a816d7e1303469341", + get_code: "https://oauth.yandex.com/authorize?client_id=$CLIENT_ID&response_type=code&scope=mail%3Aimap_full%20mail%3Asmtp&force_confirm=true", + init_token: "https://oauth.yandex.com/token?grant_type=authorization_code&code=$CODE&client_id=$CLIENT_ID&client_secret=58b8c6e94cf44fbe952da8511955dacf", + refresh_token: "https://oauth.yandex.com/token?grant_type=refresh_token&refresh_token=$REFRESH_TOKEN&client_id=$CLIENT_ID&client_secret=58b8c6e94cf44fbe952da8511955dacf", + get_userinfo: None, +}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Oauth2 { + client_id: &'static str, + get_code: &'static str, + init_token: &'static str, + refresh_token: &'static str, + get_userinfo: Option<&'static str>, +} + +#[derive(Debug, Deserialize)] +struct Response { + // Should always be there according to: https://www.oauth.com/oauth2-servers/access-tokens/access-token-response/ + // but previous code handled its abscense. + access_token: Option, + token_type: String, + expires_in: Option, + refresh_token: Option, + scope: Option, +} + +pub fn dc_get_oauth2_url( + context: &dc_context_t, + addr: impl AsRef, + redirect_uri: impl AsRef, +) -> Option { + if let Some(oauth2) = Oauth2::from_address(addr) { + set_config( + context, + "oauth2_pending_redirect_uri", + redirect_uri.as_ref(), + ); + let oauth2_url = replace_in_uri(&oauth2.get_code, "$CLIENT_ID", &oauth2.client_id); + let oauth2_url = replace_in_uri(&oauth2_url, "$REDIRECT_URI", redirect_uri.as_ref()); + + Some(oauth2_url) + } else { + None + } +} + +// The following function may block due http-requests; +// must not be called from the main thread or by the ui! +pub fn dc_get_oauth2_access_token( + context: &dc_context_t, + addr: impl AsRef, + code: impl AsRef, + flags: usize, +) -> Option { + if let Some(oauth2) = Oauth2::from_address(addr) { + let lock = context.oauth2_critical.clone(); + let _l = lock.lock().unwrap(); + + // read generated token + if 0 == flags & 0x1 && !is_expired(context) { + let access_token = get_config(context, "oauth2_access_token"); + if access_token.is_some() { + // success + return access_token; + } + } + + let refresh_token = get_config(context, "oauth2_refresh_token"); + let refresh_token_for = + get_config(context, "oauth2_refresh_token_for").unwrap_or_else(|| "unset".into()); + + let (redirect_uri, token_url, update_redirect_uri_on_success) = + if refresh_token.is_none() || refresh_token_for != code.as_ref() { + info!( + context, + 0, "Generate OAuth2 refresh_token and access_token...", + ); + ( + get_config(context, "oauth2_pending_redirect_uri") + .unwrap_or_else(|| "unset".into()), + oauth2.init_token, + true, + ) + } else { + info!( + context, + 0, "Regenerate OAuth2 access_token by refresh_token...", + ); + ( + get_config(context, "oauth2_redirect_uri").unwrap_or_else(|| "unset".into()), + oauth2.refresh_token, + false, + ) + }; + let mut token_url = replace_in_uri(&token_url, "$CLIENT_ID", oauth2.client_id); + token_url = replace_in_uri(&token_url, "$REDIRECT_URI", &redirect_uri); + token_url = replace_in_uri(&token_url, "$CODE", code.as_ref()); + if let Some(ref token) = refresh_token { + token_url = replace_in_uri(&token_url, "$REFRESH_TOKEN", token); + } + + let response = reqwest::Client::new().post(&token_url).send(); + if response.is_err() { + warn!( + context, + 0, + format!("Error calling OAuth2 at {}: {:?}", token_url, response) + ); + return None; + } + let mut response = response.unwrap(); + if !response.status().is_success() { + warn!( + context, + 0, + format!( + "Error calling OAuth2 at {}: {:?}", + token_url, + response.status() + ) + ); + return None; + } + + let parsed: reqwest::Result = response.json(); + if parsed.is_err() { + warn!( + context, + 0, + format!( + "Failed to parse OAuth2 JSON response from {}: error: {:?}", + token_url, parsed + ) + ); + return None; + } + println!("response: {:?}", &parsed); + let response = parsed.unwrap(); + if let Some(ref token) = response.refresh_token { + set_config(context, "oauth2_refresh_token", token); + set_config(context, "oauth2_refresh_token_for", code.as_ref()); + } + + // after that, save the access token. + // if it's unset, we may get it in the next round as we have the refresh_token now. + if let Some(ref token) = response.access_token { + set_config(context, "oauth2_access_token", token); + let expires_in = response + .expires_in + // refresh a bet before + .map(|t| unsafe { time(0 as *mut time_t) as u64 } + t - 5) + .unwrap_or_else(|| 0); + set_config_int64(context, "oauth2_timestamp_expires", expires_in as i64); + + if update_redirect_uri_on_success { + set_config(context, "oauth2_redirect_uri", redirect_uri.as_ref()); + } + } else { + warn!(context, 0, "Failed to find OAuth2 access token"); + } + + response.access_token + } else { + warn!(context, 0, "Internal OAuth2 error: 2"); + + None + } +} + +pub fn dc_get_oauth2_addr( + context: &dc_context_t, + addr: impl AsRef, + code: impl AsRef, +) -> Option { + let oauth2 = Oauth2::from_address(addr.as_ref()); + if oauth2.is_none() { + return None; + } + let oauth2 = oauth2.unwrap(); + if oauth2.get_userinfo.is_none() { + return None; + } + + if let Some(access_token) = dc_get_oauth2_access_token(context, addr.as_ref(), code.as_ref(), 0) + { + let addr_out = oauth2.get_addr(context, access_token); + if addr_out.is_none() { + // regenerate + if let Some(access_token) = dc_get_oauth2_access_token(context, addr, code, 0x1) { + oauth2.get_addr(context, access_token) + } else { + None + } + } else { + addr_out + } + } else { + None + } +} + +impl Oauth2 { + fn from_address(addr: impl AsRef) -> Option { + let addr_normalized = normalize_addr(addr.as_ref()); + if let Some(domain) = addr_normalized + .find('@') + .map(|index| addr_normalized.split_at(index + 1).1) + { + match domain { + "gmail.com" | "googlemail.com" => Some(OAUTH2_GMAIL), + "yandex.com" | "yandex.ru" | "yandex.ua" => Some(OAUTH2_YANDEX), + _ => None, + } + } else { + None + } + } + + fn get_addr(&self, context: &dc_context_t, access_token: impl AsRef) -> Option { + let userinfo_url = self.get_userinfo.unwrap_or_else(|| ""); + let userinfo_url = replace_in_uri(&userinfo_url, "$ACCESS_TOKEN", access_token); + + let response = reqwest::Client::new().post(&userinfo_url).send(); + if response.is_err() { + warn!( + context, + 0, + format!("Error getting userinfo: {:?}", response) + ); + return None; + } + let mut response = response.unwrap(); + if !response.status().is_success() { + warn!( + context, + 0, + format!("Error getting userinfo: {:?}", response.status()) + ); + return None; + } + + let parsed: reqwest::Result> = response.json(); + if parsed.is_err() { + warn!( + context, + 0, + format!("Failed to parse userinfo JSON response: {:?}", parsed) + ); + return None; + } + if let Ok(response) = parsed { + let addr = response.get("email"); + if addr.is_none() { + warn!(context, 0, "E-mail missing in userinfo."); + } + + addr.map(|addr| addr.to_string()) + } else { + warn!(context, 0, "Failed to parse userinfo."); + None + } + } +} + +fn get_config(context: &dc_context_t, key: &str) -> Option { + let key_c = CString::new(key).unwrap(); + let res = unsafe { + dc_sqlite3_get_config( + context, + &context.sql.clone().read().unwrap(), + key_c.as_ptr(), + std::ptr::null(), + ) + }; + if res.is_null() { + return None; + } + + Some(to_string(res)) +} + +fn set_config(context: &dc_context_t, key: &str, value: &str) { + let key_c = CString::new(key).unwrap(); + let value_c = CString::new(value).unwrap(); + unsafe { + dc_sqlite3_set_config( + context, + &context.sql.clone().read().unwrap(), + key_c.as_ptr(), + value_c.as_ptr(), + ) + }; +} + +fn set_config_int64(context: &dc_context_t, key: &str, value: i64) { + let key_c = CString::new(key).unwrap(); + unsafe { + dc_sqlite3_set_config_int64( + context, + &context.sql.clone().read().unwrap(), + key_c.as_ptr(), + value, + ) + }; +} + +fn is_expired(context: &dc_context_t) -> bool { + let expire_timestamp = unsafe { + dc_sqlite3_get_config_int64( + context, + &context.sql.clone().read().unwrap(), + b"oauth2_timestamp_expires\x00" as *const u8 as *const libc::c_char, + 0i32 as int64_t, + ) + } as time_t; + + if expire_timestamp <= 0 { + return false; + } + if expire_timestamp > unsafe { time(0 as *mut time_t) } { + return false; + } + + true +} + +fn replace_in_uri(uri: impl AsRef, key: impl AsRef, value: impl AsRef) -> String { + let value_urlencoded = utf8_percent_encode(value.as_ref(), DEFAULT_ENCODE_SET).to_string(); + uri.as_ref().replace(key.as_ref(), &value_urlencoded) +} + +fn normalize_addr(addr: &str) -> &str { + let normalized = addr.trim(); + normalized.trim_start_matches("mailto:") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_normalize_addr() { + assert_eq!(normalize_addr(" hello@mail.de "), "hello@mail.de"); + assert_eq!(normalize_addr("mailto:hello@mail.de "), "hello@mail.de"); + } + + #[test] + fn test_replace_in_uri() { + assert_eq!( + replace_in_uri("helloworld", "world", "a-b c"), + "helloa-b%20c" + ); + } + + #[test] + fn test_oauth_from_address() { + assert_eq!(Oauth2::from_address("hello@gmail.com"), Some(OAUTH2_GMAIL)); + assert_eq!( + Oauth2::from_address("hello@googlemail.com"), + Some(OAUTH2_GMAIL) + ); + assert_eq!( + Oauth2::from_address("hello@yandex.com"), + Some(OAUTH2_YANDEX) + ); + assert_eq!(Oauth2::from_address("hello@yandex.ru"), Some(OAUTH2_YANDEX)); + + assert_eq!(Oauth2::from_address("hello@web.de"), None); + } +} diff --git a/tests/stress.rs b/tests/stress.rs index c9261559c..e13f7797f 100644 --- a/tests/stress.rs +++ b/tests/stress.rs @@ -24,6 +24,7 @@ use deltachat::dc_securejoin::*; use deltachat::dc_strbuilder::*; use deltachat::dc_strencode::*; use deltachat::dc_tools::*; +use deltachat::oauth2::*; use deltachat::types::*; use deltachat::x::*; use libc; @@ -2519,6 +2520,36 @@ fn test_dc_mimeparser_with_context() { } } +#[test] +fn test_dc_get_oauth2_url() { + let ctx = unsafe { create_test_context() }; + let addr = "dignifiedquire@gmail.com"; + let redirect_uri = "chat.delta:/com.b44t.messenger"; + let res = dc_get_oauth2_url(&ctx.ctx, addr, redirect_uri); + + assert_eq!(res, Some("https://accounts.google.com/o/oauth2/auth?client_id=959970109878-4mvtgf6feshskf7695nfln6002mom908.apps.googleusercontent.com&redirect_uri=chat.delta:/com.b44t.messenger&response_type=code&scope=https%3A%2F%2Fmail.google.com%2F%20email&access_type=offline".into())); +} + +#[test] +fn test_dc_get_oauth2_addr() { + let ctx = unsafe { create_test_context() }; + let addr = "dignifiedquire@gmail.com"; + let code = "fail"; + let res = dc_get_oauth2_addr(&ctx.ctx, addr, code); + // this should fail as it is an invalid password + assert_eq!(res, None); +} + +#[test] +fn test_dc_get_oauth2_token() { + let ctx = unsafe { create_test_context() }; + let addr = "dignifiedquire@gmail.com"; + let code = "fail"; + let res = dc_get_oauth2_access_token(&ctx.ctx, addr, code, 0); + // this should fail as it is an invalid password + assert_eq!(res, None); +} + #[test] fn test_stress_tests() { unsafe {