From 6c120b9e3730f997af56fbbe19229915b6380b2d Mon Sep 17 00:00:00 2001 From: Eduardo Chappa Date: Fri, 12 Jun 2020 02:21:59 -0600 Subject: * Initial implementation of XOAUTH2 authentication support for Outlook. Based on documentation suggested by Andrew C Aitchison. --- alpine/alpine.c | 1 + alpine/imap.c | 259 ++++++++++++++++++++++++++++++--- alpine/imap.h | 3 + alpine/keymenu.c | 14 ++ alpine/keymenu.h | 1 + alpine/mailview.c | 16 ++- alpine/mailview.h | 5 + alpine/xoauth2.h | 4 +- alpine/xoauth2conf.c | 6 - imap/src/c-client/auth_bea.c | 181 +----------------------- imap/src/c-client/auth_oa2.c | 181 ------------------------ imap/src/c-client/http.h | 10 +- imap/src/c-client/mail.c | 17 +++ imap/src/c-client/mail.h | 29 +++- imap/src/c-client/oauth2_aux.c | 314 +++++++++++++++++++++++++++++++++++++++++ imap/src/c-client/oauth2_aux.h | 25 ++++ imap/src/osdep/unix/Makefile | 6 +- pith/pine.hlp | 12 +- 18 files changed, 681 insertions(+), 403 deletions(-) create mode 100644 imap/src/c-client/oauth2_aux.c create mode 100644 imap/src/c-client/oauth2_aux.h diff --git a/alpine/alpine.c b/alpine/alpine.c index 8155f54a..d72c60cf 100644 --- a/alpine/alpine.c +++ b/alpine/alpine.c @@ -344,6 +344,7 @@ main(int argc, char **argv) mail_parameters(NULL, SET_FREEBODYSPAREP, (void *) free_body_sparep); mail_parameters(NULL, SET_OA2CLIENTGETACCESSCODE, (void *) oauth2_get_access_code); mail_parameters(NULL, SET_OA2CLIENTINFO, (void *) oauth2_get_client_info); + mail_parameters(NULL, SET_OA2DEVICEINFO, (void *) oauth2_set_device_info); init_pinerc(pine_state, &init_pinerc_debugging); diff --git a/alpine/imap.c b/alpine/imap.c index f86587e6..54a58c68 100644 --- a/alpine/imap.c +++ b/alpine/imap.c @@ -153,45 +153,48 @@ OAUTH2_S alpine_oauth2_list[] = {"grant_type", "refresh_token"}, {"response_type", "code"}, {"state", NULL}, - {"prompt", NULL} + {"prompt", NULL}, + {"device_code", NULL} }, {{"GET", "https://accounts.google.com/o/oauth2/auth", {OA2_Id, OA2_Scope, OA2_Redirect, OA2_Response, OA2_End, OA2_End, OA2_End}}, {"POST", "https://accounts.google.com/o/oauth2/token", {OA2_Id, OA2_Secret, OA2_Redirect, OA2_GrantTypeforAccessToken, OA2_Code, OA2_End, OA2_End}}, {"POST", "https://accounts.google.com/o/oauth2/token", - {OA2_Id, OA2_Secret, OA2_RefreshToken, OA2_GrantTypefromRefreshToken, OA2_End, OA2_End, OA2_End}} + {OA2_Id, OA2_Secret, OA2_RefreshToken, OA2_GrantTypefromRefreshToken, OA2_End, OA2_End, OA2_End}}, + {NULL, NULL, {OA2_End, OA2_End, OA2_End, OA2_End, OA2_End, OA2_End, OA2_End}} }, - NULL, 0 + {NULL, NULL, NULL, 0, 0, NULL}, /* devicecode info */ + NULL, 0, 0 }, -#if 0 {"Outlook", - {"outlook.office365.com", "smtp.gmail.com", NULL, NULL}, -// {{"client_id", "2d681b88-9675-4ff0-b033-4de97dcb7a04"}, -// {"client_secret", "FHLY770;@%fmrzxbnEKG44!"}, + {"outlook.office365.com", "smtp.office365.com", NULL, NULL}, {{"client_id", NULL}, {"client_secret", NULL}, {"code", NULL}, {"refresh_token", NULL}, - {"scope", "openid offline_access profile https://outlook.office.com/mail.readwrite https://outlook.office.com/mail.readwrite.shared https://outlook.office.com/mail.send https://outlook.office.com/mail.send.shared https://outlook.office.com/calendars.readwrite https://outlook.office.com/calendars.readwrite.shared https://outlook.office.com/contacts.readwrite https://outlook.office.com/contacts.readwrite.shared https://outlook.office.com/tasks.readwrite https://outlook.office.com/tasks.readwrite.shared https://outlook.office.com/mailboxsettings.readwrite https://outlook.office.com/people.read https://outlook.office.com/user.readbasic.all"}, - {"redirect_uri", "https://login.microsoftonline.com/common/oauth2/nativeclient"}, - {"grant_type", "authorization_code"}, + {"scope", "offline_access https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/SMTP.Send"}, + {"grant_type", "urn:ietf:params:oauth:grant-type:device_code"}, + {"scope", "https://graph.microsoft.com/mail.read"}, {"grant_type", "refresh_token"}, {"response_type", "code"}, {"state", NULL}, - {"prompt", "login"} + {"prompt", "login"}, + {"device_code", NULL} }, {{"GET", "https://login.microsoftonline.com/common/oauth2/authorize", {OA2_Id, OA2_Scope, OA2_Redirect, OA2_Response, OA2_State, OA2_Prompt, OA2_End}}, - {"POST", "https://login.microsoftonline.com/common/oauth2/token", - {OA2_Id, OA2_Secret, OA2_Redirect, OA2_GrantTypeforAccessToken, OA2_Code, OA2_Scope, OA2_End}}, - {"POST", "https://login.microsoftonline.com/common/oauth2/token", - {OA2_Id, OA2_Secret, OA2_RefreshToken, OA2_GrantTypefromRefreshToken, OA2_End, OA2_End, OA2_End}} + {"POST", "https://login.microsoftonline.com/common/oauth2/v2.0/token", + {OA2_Id, OA2_Redirect, OA2_DeviceCode, OA2_End, OA2_End, OA2_End}}, + {"POST", "https://login.microsoftonline.com/common/oauth2/v2.0/token", + {OA2_Id, OA2_RefreshToken, OA2_Scope, OA2_GrantTypefromRefreshToken, OA2_End, OA2_End, OA2_End}}, + {"POST", "https://login.microsoftonline.com/common/oauth2/v2.0/devicecode", + {OA2_Id, OA2_Scope, OA2_End, OA2_End, OA2_End, OA2_End, OA2_End}} }, - NULL, 0 + {NULL, NULL, NULL, 0, 0, NULL}, + NULL, 0, 0 }, -#endif - { NULL, NULL, NULL, NULL, NULL, 0}, + { NULL, NULL, NULL, NULL, NULL, NULL, 0, 0}, }; typedef struct auth_code_s { @@ -199,6 +202,219 @@ typedef struct auth_code_s { int answer; } AUTH_CODE_S; +UCS +oauth2device_decode_reply(void *datap, void *replyp) +{ + OAUTH2_DEVICEPROC_S *av = (OAUTH2_DEVICEPROC_S *)datap; + int reply = *(int *) replyp; + + return reply < 0 ? av->code_failure : (reply == 0 ? av->code_success : av->code_wait); +} + +int +oauth2_elapsed_done(void *aux_valuep) +{ + OAUTH2_S *oauth2 = aux_valuep ? ((OAUTH2_DEVICEPROC_S *) aux_valuep)->xoauth2 : NULL; + static time_t savedt = 0, now; + int rv = 0; + + if(aux_valuep == NULL) savedt = 0; /* reset last time we approved */ + else{ + now = time(0); + if(oauth2->devicecode.interval + now >= savedt) + savedt = now; + else + rv = -1; + } + return rv; +} + +void +oauth2_set_device_info(OAUTH2_S *oa2, char *method) +{ + char tmp[MAILTMPLEN]; + char *code; + char *name = oa2->name; + int aux_rv_value; + OAUTH2_DEVICECODE_S *deviceinfo = &oa2->devicecode; + OAUTH2_DEVICEPROC_S aux_value; + + if(ps_global->ttyo){ + SCROLL_S sargs; + STORE_S *in_store, *out_store; + gf_io_t pc, gc; + HANDLE_S *handles = NULL; + AUTH_CODE_S user_input; + + if(!(in_store = so_get(CharStar, NULL, EDIT_ACCESS)) || + !(out_store = so_get(CharStar, NULL, EDIT_ACCESS))) + goto try_wantto; + + aux_value.xoauth2 = oa2; + aux_value.code_success = 'e'; + aux_value.code_failure = 'e'; + aux_value.code_wait = NO_OP_COMMAND; + + so_puts(in_store, "

"); + sprintf(tmp, _("

Authorizing Alpine Access to %s Email Services
"), name); + so_puts(in_store, tmp); + sprintf(tmp, _("

Alpine is attempting to log you into your %s account, using the %s method."), name, method), + so_puts(in_store, tmp); + + if(deviceinfo->verification_uri && deviceinfo->user_code){ + sprintf(tmp, + _("

To sign in, use a web browser to open the page %s and enter the code \"%s\" without the quotes."), + deviceinfo->verification_uri, deviceinfo->verification_uri, deviceinfo->user_code); + so_puts(in_store, tmp); + } + else{ + so_puts(in_store, "

"); + so_puts(in_store, deviceinfo->message); + } + so_puts(in_store, _("

Alpine will try to use your URL Viewers setting to find a browser to open this URL.")); + sprintf(tmp, _(" When you open this link, you will be sent to %s's servers to complete this process."), name); + so_puts(in_store, tmp); + so_puts(in_store, _(" Alternatively, you can copy and paste the previous link and open it with the browser of your choice.")); + + so_puts(in_store, _("

After you open the previous link, please enter the code above, and then you will be asked to authenticate and later ")); + so_puts(in_store, _("to grant access to Alpine to your data. ")); + so_puts(in_store, _("

Once you have authorized Alpine, you will be asked if you want to preserve the Refresh Token and Access Code. If you do ")); + so_puts(in_store, _("not wish to repeat this process again, answer \'Y\'es. If you did this process quickly, your connection to the server will still be ")); + so_puts(in_store, _("alive at the end of this process, and your connection will proceed from there.")); + so_puts(in_store, _("

If you do not wish to proceed, cancel at any time by pressing 'E' to exit")); + so_puts(in_store, _("

")); + + so_seek(in_store, 0L, 0); + init_handles(&handles); + gf_filter_init(); + gf_link_filter(gf_html2plain, + gf_html2plain_opt(NULL, + ps_global->ttyo->screen_cols, non_messageview_margin(), + &handles, NULL, GFHP_LOCAL_HANDLES)); + gf_set_so_readc(&gc, in_store); + gf_set_so_writec(&pc, out_store); + gf_pipe(gc, pc); + gf_clear_so_writec(out_store); + gf_clear_so_readc(in_store); + + memset(&sargs, 0, sizeof(SCROLL_S)); + sargs.text.handles = handles; + sargs.text.text = so_text(out_store); + sargs.text.src = CharStar; + sargs.text.desc = _("help text"); + sargs.bar.title = _("SETTING UP XOAUTH2 AUTHORIZATION"); + sargs.proc.tool = oauth2_auth_answer; + sargs.proc.data.p = (void *)&user_input; + sargs.keys.menu = &oauth2_device_auth_keymenu; + /* don't want to re-enter c-client */ + sargs.quell_newmail = 1; + setbitmap(sargs.keys.bitmap); + sargs.help.text = h_oauth2_start; + sargs.help.title = _("HELP FOR SETTING UP XOAUTH2"); + sargs.aux_function = oauth2deviceinfo_get_accesscode; + sargs.aux_value = (void *) &aux_value; + sargs.aux_condition = oauth2_elapsed_done; + sargs.decode_aux_rv_value = oauth2device_decode_reply; + sargs.aux_rv_value = (void *) &aux_rv_value; + + do { + scrolltool(&sargs); + ps_global->mangled_screen = 1; + ps_global->painted_body_on_startup = 0; + ps_global->painted_footer_on_startup = 0; + } while (user_input.answer != 'e'); + + so_give(&in_store); + so_give(&out_store); + free_handles(&handles); + oauth2_elapsed_done(NULL); + } + else{ + int flags, rc, q_line; + /* TRANSLATORS: user needs to input an access code from the server */ + char prompt[MAILTMPLEN], token[MAILTMPLEN]; + /* + * If screen hasn't been initialized yet, use want_to. + */ +try_wantto: + + tmp_20k_buf[0] = '\0'; + snprintf(tmp_20k_buf+strlen(tmp_20k_buf), SIZEOF_20KBUF-strlen(tmp_20k_buf), + _("Authorizing Alpine Access to %s Email Services\n\n"), name); + tmp_20k_buf[SIZEOF_20KBUF-1] = '\0'; + + snprintf(tmp_20k_buf+strlen(tmp_20k_buf), SIZEOF_20KBUF-strlen(tmp_20k_buf), + _("Alpine is attempting to log you into your %s account, using the %s method. "), name, method), + tmp_20k_buf[SIZEOF_20KBUF-1] = '\0'; + + if(deviceinfo->verification_uri && deviceinfo->user_code){ + snprintf(tmp_20k_buf+strlen(tmp_20k_buf), SIZEOF_20KBUF-strlen(tmp_20k_buf), + _("To sign in, user a web browser to open the page %s and enter the code \"%s\" without the quotes.\n\n"), + deviceinfo->verification_uri, deviceinfo->user_code); + tmp_20k_buf[SIZEOF_20KBUF-1] = '\0'; + } + else{ + snprintf(tmp_20k_buf+strlen(tmp_20k_buf), SIZEOF_20KBUF-strlen(tmp_20k_buf), + "%s\n\n", deviceinfo->message); + tmp_20k_buf[SIZEOF_20KBUF-1] = '\0'; + } + + snprintf(tmp_20k_buf+strlen(tmp_20k_buf), SIZEOF_20KBUF-strlen(tmp_20k_buf), + _("Copy and paste the previous URL into a web browser that supports javascript, to take you to %s's servers to complete this process.\n\n"), name); + tmp_20k_buf[SIZEOF_20KBUF-1] = '\0'; + + snprintf(tmp_20k_buf+strlen(tmp_20k_buf), SIZEOF_20KBUF-strlen(tmp_20k_buf), + "%s", _("After you open the previous link, please enter the code above, and then you will be asked to authenticate and later ")); + tmp_20k_buf[SIZEOF_20KBUF-1] = '\0'; + + snprintf(tmp_20k_buf+strlen(tmp_20k_buf), SIZEOF_20KBUF-strlen(tmp_20k_buf), + "%s", _("to grant access to Alpine to your data.\n\n")); + tmp_20k_buf[SIZEOF_20KBUF-1] = '\0'; + + snprintf(tmp_20k_buf+strlen(tmp_20k_buf), SIZEOF_20KBUF-strlen(tmp_20k_buf), + "%s", _(" Once you have authorized Alpine, you will be asked if you want to preserve the Refresh Token and Access Code. ")); + tmp_20k_buf[SIZEOF_20KBUF-1] = '\0'; + + snprintf(tmp_20k_buf+strlen(tmp_20k_buf), SIZEOF_20KBUF-strlen(tmp_20k_buf), + "%s", _("If you do not wish to repeat this process again, answer \'Y\'es. If you did this process quickly, your connection ")); + tmp_20k_buf[SIZEOF_20KBUF-1] = '\0'; + + snprintf(tmp_20k_buf+strlen(tmp_20k_buf), SIZEOF_20KBUF-strlen(tmp_20k_buf), + "%s", _("to the server will still be alive at the end of this process, and your connection will proceed from there.\n\n")); + tmp_20k_buf[SIZEOF_20KBUF-1] = '\0'; + + display_init_err(tmp_20k_buf, 0); + memset((void *)tmp, 0, sizeof(tmp)); + strncpy(tmp, _("Alpine would like to get authorization to access your email. Proceed "), sizeof(tmp)); + tmp[sizeof(tmp)-1] = '\0'; + + if(want_to(tmp, 'n', 'x', NO_HELP, WT_NORM) == 'y'){ + int rv; + UCS ch; + q_line = -(ps_global->ttyo ? ps_global->ttyo->footer_rows : 3); + flags = OE_APPEND_CURRENT; + + snprintf(tmp_20k_buf+strlen(tmp_20k_buf), SIZEOF_20KBUF-strlen(tmp_20k_buf), + "%s", _("After you are done going through the process described above, press \'y\' to continue, or \'n\' to cancel\n")); + tmp_20k_buf[SIZEOF_20KBUF-1] = '\0'; + + aux_value.xoauth2 = oa2; + aux_value.code_success = 'y'; + aux_value.code_failure = 'n'; + aux_value.code_wait = 'w'; + + strncpy(tmp, _("Continue waiting"), sizeof(tmp)); + tmp[sizeof(tmp)-1] = '\0'; + do { + if(oauth2_elapsed_done((void *) &aux_value) == 0) + oauth2deviceinfo_get_accesscode((void *) &aux_value, (void *) &rv); + ch = oauth2device_decode_reply((void *) &aux_value, (void *) &rv); + } while (ch == 'w' || want_to(tmp, 'n', 'x', NO_HELP, WT_NORM) == 'y'); + oauth2_elapsed_done(NULL); + } + } +} + char * oauth2_get_access_code(unsigned char *url, char *method, OAUTH2_S *oauth2, int *tryanother) { @@ -238,7 +454,7 @@ oauth2_get_access_code(unsigned char *url, char *method, OAUTH2_S *oauth2, int * so_puts(in_store, _("

If you completed these steps successfully, you are ready to move to the second part, where you will authorize Gmail to give access to Alpine to access your email.")); } - so_puts(in_store, _("

In order to authrorize Alpine to access your email, Alpine needs to open the following URL:")); + so_puts(in_store, _("

In order to authorize Alpine to access your email, Alpine needs to open the following URL:")); so_puts(in_store,"

"); sprintf(tmp_20k_buf, _("%s"), url, url); so_puts(in_store, tmp_20k_buf); @@ -362,9 +578,8 @@ try_wantto: display_init_err(tmp_20k_buf, 0); memset((void *)tmp, 0, sizeof(tmp)); - strncpy(tmp, _("Alpine would like to get authorization to access your email: "), sizeof(tmp)); + strncpy(tmp, _("Alpine would like to get authorization to access your email. Proceed "), sizeof(tmp)); tmp[sizeof(tmp)-1] = '\0'; - strncat(tmp, _(": Proceed "), sizeof(tmp)-strlen(tmp)-1); if(want_to(tmp, 'n', 'x', NO_HELP, WT_NORM) == 'y'){ q_line = -(ps_global->ttyo ? ps_global->ttyo->footer_rows : 3); @@ -623,6 +838,7 @@ mm_login_oauth2(NETMBX *mb, char *user, char *method, if(NewAccessToken && (NewExpirationTime == 0L || !*NewAccessToken)) fs_give((void **) &NewAccessToken); } + else login->first_time++; /* Default to saving what we already had saved */ @@ -685,6 +901,7 @@ mm_login_oauth2(NETMBX *mb, char *user, char *method, oa2list->param[OA2_RefreshToken].value = SaveRefreshToken; oa2list->access_token = SaveAccessToken; oa2list->expiration = SaveExpirationTime; + oa2list->first_time = login->first_time; *login = *oa2list; /* load login pointer */ } diff --git a/alpine/imap.h b/alpine/imap.h index c4195fba..e595e196 100644 --- a/alpine/imap.h +++ b/alpine/imap.h @@ -32,6 +32,9 @@ int url_local_certdetails(char *); void pine_sslfailure(char *, char *, unsigned long); void mm_expunged_current(long unsigned int); char *oauth2_get_access_code(unsigned char *, char *, OAUTH2_S *, int *); +void oauth2_set_device_info(OAUTH2_S *, char *); +int oauth2_elapsed_done(void *); +UCS oauth2device_decode_reply(void *, void *); #ifdef LOCAL_PASSWD_CACHE int get_passfile_passwd(char *, char **, char *, STRLIST_S *, int); diff --git a/alpine/keymenu.c b/alpine/keymenu.c index 65ba6f88..519622a6 100644 --- a/alpine/keymenu.c +++ b/alpine/keymenu.c @@ -577,6 +577,20 @@ struct key oauth2_alpine_auth_keys[] = {"S", N_("Save"), {MC_SAVETEXT,1,{'s'}}, KS_SAVE}}; INST_KEY_MENU(oauth2_auth_keymenu, oauth2_alpine_auth_keys); +struct key oauth2_device_alpine_auth_keys[] = + {HELP_MENU, + NULL_MENU, + NULL_MENU, + {"V","[" N_("View URL") "]",{MC_VIEW_HANDLE,3,{'v',ctrl('M'),ctrl('J')}},KS_NONE}, + {"E",N_("Exit"),{MC_NO,1,{'e'}},KS_NONE}, + NULL_MENU, + PREVPAGE_MENU, + NEXTPAGE_MENU, + PRYNTTXT_MENU, + WHEREIS_MENU, + FWDEMAIL_MENU, + {"S", N_("Save"), {MC_SAVETEXT,1,{'s'}}, KS_SAVE}}; +INST_KEY_MENU(oauth2_device_auth_keymenu, oauth2_alpine_auth_keys); struct key forge_keys[] = {HELP_MENU, diff --git a/alpine/keymenu.h b/alpine/keymenu.h index e29ec201..87d1151d 100644 --- a/alpine/keymenu.h +++ b/alpine/keymenu.h @@ -585,6 +585,7 @@ extern struct key_menu cancel_keymenu, ans_certfail_keymenu, ans_certquery_keymenu, oauth2_auth_keymenu, + oauth2_device_auth_keymenu, forge_keymenu, listmgr_keymenu, index_keymenu, diff --git a/alpine/mailview.c b/alpine/mailview.c index a6531ea3..d1ace2f2 100644 --- a/alpine/mailview.c +++ b/alpine/mailview.c @@ -2382,7 +2382,7 @@ scrolltool(SCROLL_S *sparms) UCS ch; int result, done, cmd, found_on, found_on_index, first_view, force, scroll_lines, km_size, - cursor_row, cursor_col, km_popped, + cursor_row, cursor_col, km_popped, rv, nrows, ncols; char *utf8str; long jn; @@ -2712,11 +2712,21 @@ scrolltool(SCROLL_S *sparms) if(ps_global->prev_screen == mail_view_screen) mswin_setviewinwindcallback(view_in_new_window); #endif - if(cmd == MC_RESIZE + rv = -1; + if(sparms->aux_function + && sparms->aux_condition + && ((rv = (sparms->aux_condition)(sparms->aux_value)) == 0)) + (sparms->aux_function)(sparms->aux_value, sparms->aux_rv_value); + + if(rv == 0){ + ch = (sparms->decode_aux_rv_value)(sparms->aux_value, sparms->aux_rv_value); + if (ch == NO_OP_COMMAND) ch = read_command(&utf8str); + } + else if(cmd == MC_RESIZE || ps_global->ttyo == NULL || (ps_global->ttyo->screen_cols == ncols && ps_global->ttyo->screen_rows == nrows)) - ch = (sparms->quell_newmail || read_command_prep()) ? read_command(&utf8str) : NO_OP_COMMAND; + ch = (sparms->quell_newmail || read_command_prep()) ? read_command(&utf8str) : NO_OP_COMMAND; #ifdef MOUSE #ifndef WIN32 if(sparms->text.handles) diff --git a/alpine/mailview.h b/alpine/mailview.h index 9df7aeb6..35d4ac4b 100644 --- a/alpine/mailview.h +++ b/alpine/mailview.h @@ -63,6 +63,11 @@ typedef struct scrolltool_s { HelpType text; /* help text */ char *title; /* title for help screen */ } help; + UCS (*decode_aux_rv_value)(void *, void *); /* transforms a reply to a command */ + int (*aux_condition)(void *); /* has the condition been met to execute aux_function, 0 = success */ + void (*aux_function)(void *, void *); /* auxiliary function to help us decide what to do */ + void *aux_value; /* argument of aux_function */ + void *aux_rv_value; /* the return value of aux_function in (void *) */ struct { int (*click)(struct scrolltool_s *); int (*clickclick)(struct scrolltool_s *); diff --git a/alpine/xoauth2.h b/alpine/xoauth2.h index c9c69974..8a3fc3f7 100644 --- a/alpine/xoauth2.h +++ b/alpine/xoauth2.h @@ -26,7 +26,7 @@ typedef struct xoauth_default_s { #define GMAIL_SECRET "vwnqVJQrJZpR6JilCfAN5nY7" #define OUTLOOK_NAME "Outlook" -#define OUTLOOK_ID "c8df0dbf-4750-4bb9-98e9-562b10caa26a" -#define OUTLOOK_SECRET "ijrmPVDYP4yxbNL3442;!!_" +#define OUTLOOK_ID "f21dcaf2-8020-469b-8135-343bfc35d046" +#define OUTLOOK_SECRET "lIE42T4kZ2ZrN-2-AVNYSZ~8i_Co2WG4m." #endif /* ALPINE_XOAUTH2_INCLUDED */ diff --git a/alpine/xoauth2conf.c b/alpine/xoauth2conf.c index 97900d25..4a69eb9c 100644 --- a/alpine/xoauth2conf.c +++ b/alpine/xoauth2conf.c @@ -26,9 +26,7 @@ extern OAUTH2_S alpine_oauth2_list[]; XOAUTH2_INFO_S xoauth_default[] = { { GMAIL_NAME, GMAIL_ID, GMAIL_SECRET}, -#if 0 { OUTLOOK_NAME, OUTLOOK_ID, OUTLOOK_SECRET}, -#endif { NULL, NULL, NULL} }; @@ -202,9 +200,7 @@ void alpine_xoauth2_configuration(struct pine *ps, int edit_exceptions) { struct variable gmail_client_id_var, gmail_client_secret_var; -#if 0 struct variable outlook_client_id_var, outlook_client_secret_var; -#endif struct variable *varlist[2*NXSERVERS + 1]; char tmp[MAXPATH+1], *pval, **lval; char *id, *secret; @@ -245,10 +241,8 @@ alpine_xoauth2_configuration(struct pine *ps, int edit_exceptions) varlist[0] = &gmail_client_id_var; varlist[1] = &gmail_client_secret_var; -#if 0 varlist[2] = &outlook_client_id_var; varlist[3] = &outlook_client_secret_var; -#endif varlist[2*NXSERVERS] = NULL; for(i = 0; i < 2*NXSERVERS; i++) diff --git a/imap/src/c-client/auth_bea.c b/imap/src/c-client/auth_bea.c index 6079fb7b..bf5c9c1e 100644 --- a/imap/src/c-client/auth_bea.c +++ b/imap/src/c-client/auth_bea.c @@ -11,12 +11,11 @@ * ======================================================================== */ +#include "oauth2_aux.h" + long auth_oauthbearer_client (authchallenge_t challenger,authrespond_t responder, char *base, char *service,NETMBX *mb,void *stream, unsigned long port, unsigned long *trial,char *user); -#ifndef HTTP_OAUTH2_INCLUDED -void mm_login_oauth2_c_client_method (NETMBX *, char *, char *, OAUTH2_S *, unsigned long, int *); -#endif /* HTTP_OAUTH2_INCLUDED */ AUTHENTICATOR auth_bea = { AU_HIDE | AU_SINGLE, /* hidden, single trip */ @@ -34,38 +33,6 @@ AUTHENTICATOR auth_bea = { #define BEARER_HOST "host=" #define BEARER_PORT "port=" -#ifndef OAUTH2_GENERATE_STATE -#define OAUTH2_GENERATE_STATE -char *oauth2_generate_state(void); - -/* we generate something like a guid, but not care about - * anything, but that it is really random. - */ -char *oauth2_generate_state(void) -{ - char rv[37]; - int i; - - rv[0] = '\0'; - for(i = 0; i < 4; i++) - sprintf(rv + strlen(rv), "%x", (unsigned int) (random() % 256)); - sprintf(rv + strlen(rv), "%c", '-'); - for(i = 0; i < 2; i++) - sprintf(rv + strlen(rv), "%x", (unsigned int) (random() % 256)); - sprintf(rv + strlen(rv), "%c", '-'); - for(i = 0; i < 2; i++) - sprintf(rv + strlen(rv), "%x", (unsigned int) (random() % 256)); - sprintf(rv + strlen(rv), "%c", '-'); - for(i = 0; i < 2; i++) - sprintf(rv + strlen(rv), "%x", (unsigned int) (random() % 256)); - sprintf(rv + strlen(rv), "%c", '-'); - for(i = 0; i < 6; i++) - sprintf(rv + strlen(rv), "%x", (unsigned int) (random() % 256)); - rv[36] = '\0'; - return cpystr(rv); -} -#endif /* OAUTH2_GENERATE_STATE */ - /* Client authenticator * Accepts: challenger function * responder function @@ -196,147 +163,3 @@ long auth_oauthbearer_client (authchallenge_t challenger,authrespond_t responder *trial = 65535; /* don't retry if bad protocol */ return ret; } - -#ifndef HTTP_OAUTH2_INCLUDED -#define HTTP_OAUTH2_INCLUDED -/* - * The code above is enough to implement OAUTHBEARER, all one needs is the username - * and access token and give it to the function above. However, normal users cannot - * be expected to get the access token, so we ask the client to help with getting - * the access token, refresh token and expire values, so the code below is written - * to help with that. - */ - -#include "http.h" -#include "json.h" - -void -mm_login_oauth2_c_client_method (NETMBX *mb, char *user, char *method, - OAUTH2_S *oauth2, unsigned long trial, int *tryanother) -{ - int i; - HTTP_PARAM_S params[OAUTH2_PARAM_NUMBER]; - OAUTH2_SERVER_METHOD_S RefreshMethod; - unsigned char *s = NULL; - JSON_S *json = NULL; - int status = 0; - - if(oauth2->param[OA2_Id].value == NULL || oauth2->param[OA2_Secret].value == NULL){ - oauth2clientinfo_t ogci = - (oauth2clientinfo_t) mail_parameters (NIL, GET_OA2CLIENTINFO, NIL); - - if(ogci) (*ogci)(oauth2->name, &oauth2->param[OA2_Id].value, - &oauth2->param[OA2_Secret].value); - } - - if(oauth2->param[OA2_Id].value == NULL || oauth2->param[OA2_Secret].value == NULL) - return; - - /* first check if we have a refresh token, and in that case use it */ - if(oauth2->param[OA2_RefreshToken].value){ - - RefreshMethod = oauth2->server_mthd[OA2_GetAccessTokenFromRefreshToken]; - for(i = 0; RefreshMethod.params[i] != OA2_End; i++){ - OA2_type j = RefreshMethod.params[i]; - params[i].name = oauth2->param[j].name; - params[i].value = oauth2->param[j].value; - } - params[i].name = params[i].value = NULL; - - if(strcmp(RefreshMethod.name, "POST") == 0) - s = http_post_param(RefreshMethod.urlserver, params, &status); - else if(strcmp(RefreshMethod.name, "POST2") == 0) - s = http_post_param2(RefreshMethod.urlserver, params, &status); - - if(status != 200 && s) - fs_give((void **) &s); /* at this moment ignore the reply text */ - - if(s){ - unsigned char *u = s; - json = json_parse(&u); - fs_give((void **) &s); - } - - if(json != NULL){ - JSON_X *jx; - - jx = json_body_value(json, "access_token"); - if(jx && jx->jtype == JString) - oauth2->access_token = cpystr((char *) jx->value); - - jx = json_body_value(json, "expires_in"); - if(jx && jx->jtype == JString) - oauth2->expiration = time(0) + atol((char *) jx->value); - - json_free(&json); - } - return; - } - /* - * else, we do not have a refresh token, nor an access token. - * We need to start the process to get an access code. We use this - * to get an access token and refresh token. - */ - { - RefreshMethod = oauth2->server_mthd[OA2_GetAccessCode]; - for(i = 0; RefreshMethod.params[i] != OA2_End; i++){ - OA2_type j = RefreshMethod.params[i]; - params[i].name = oauth2->param[j].name; - params[i].value = oauth2->param[j].value; - } - params[i].name = params[i].value = NULL; - - if(strcmp(RefreshMethod.name, "GET") == 0){ - unsigned char *url = http_get_param_url(RefreshMethod.urlserver, params); - oauth2getaccesscode_t ogac = - (oauth2getaccesscode_t) mail_parameters (NIL, GET_OA2CLIENTGETACCESSCODE, NIL); - - if(ogac) - oauth2->param[OA2_Code].value = (*ogac)(url, method, oauth2, tryanother); - } - - if(oauth2->param[OA2_Code].value){ - RefreshMethod = oauth2->server_mthd[OA2_GetAccessTokenFromAccessCode]; - for(i = 0; RefreshMethod.params[i] != OA2_End; i++){ - OA2_type j = RefreshMethod.params[i]; - params[i].name = oauth2->param[j].name; - params[i].value = oauth2->param[j].value; - } - params[i].name = params[i].value = NULL; - - if(strcmp(RefreshMethod.name, "POST") == 0) - s = http_post_param(RefreshMethod.urlserver, params, &status); - else if(strcmp(RefreshMethod.name, "POST2") == 0) - s = http_post_param2(RefreshMethod.urlserver, params, &status); - - if(status != 200 && s) - fs_give((void **) &s); /* at this moment ignore the error */ - - if(s){ - unsigned char *u = s; - json = json_parse(&u); - fs_give((void **) &s); - } - - if(json != NULL){ - JSON_X *jx; - - jx = json_body_value(json, "refresh_token"); - if(jx && jx->jtype == JString) - oauth2->param[OA2_RefreshToken].value = cpystr((char *) jx->value); - - jx = json_body_value(json, "access_token"); - if(jx && jx->jtype == JString) - oauth2->access_token = cpystr((char *) jx->value); - - jx = json_body_value(json, "expires_in"); - if(jx && jx->jtype == JString) - oauth2->expiration = time(0) + atol((char *) jx->value); - - json_free(&json); - } - } - return; - } -} -#endif /* HTTP_OAUTH2_INCLUDED */ diff --git a/imap/src/c-client/auth_oa2.c b/imap/src/c-client/auth_oa2.c index 6de4395e..69987559 100644 --- a/imap/src/c-client/auth_oa2.c +++ b/imap/src/c-client/auth_oa2.c @@ -15,10 +15,6 @@ long auth_oauth2_client (authchallenge_t challenger,authrespond_t responder, cha char *service,NETMBX *mb,void *stream, unsigned long port, unsigned long *trial,char *user); -#ifndef HTTP_OAUTH2_INCLUDED -void mm_login_oauth2_c_client_method (NETMBX *, char *, char *, OAUTH2_S *, unsigned long, int *); -#endif /* HTTP_OAUTH2_INCLUDED */ - AUTHENTICATOR auth_oa2 = { AU_HIDE | AU_SINGLE, /* hidden */ OA2NAME, /* authenticator name */ @@ -31,39 +27,6 @@ AUTHENTICATOR auth_oa2 = { #define OAUTH2_USER "user=" #define OAUTH2_BEARER "auth=Bearer " -#ifndef OAUTH2_GENERATE_STATE -#define OAUTH2_GENERATE_STATE -char *oauth2_generate_state(void); -/* we generate something like a guid, but not care about - * anything, but that it is really random. - */ -char *oauth2_generate_state(void) -{ - char rv[37]; - int i; - - rv[0] = '\0'; - for(i = 0; i < 4; i++) - sprintf(rv + strlen(rv), "%x", (unsigned int) (random() % 256)); - sprintf(rv + strlen(rv), "%c", '-'); - for(i = 0; i < 2; i++) - sprintf(rv + strlen(rv), "%x", (unsigned int) (random() % 256)); - sprintf(rv + strlen(rv), "%c", '-'); - for(i = 0; i < 2; i++) - sprintf(rv + strlen(rv), "%x", (unsigned int) (random() % 256)); - sprintf(rv + strlen(rv), "%c", '-'); - for(i = 0; i < 2; i++) - sprintf(rv + strlen(rv), "%x", (unsigned int) (random() % 256)); - sprintf(rv + strlen(rv), "%c", '-'); - for(i = 0; i < 6; i++) - sprintf(rv + strlen(rv), "%x", (unsigned int) (random() % 256)); - rv[36] = '\0'; - return cpystr(rv); -} -#endif /* OAUTH2_GENERATE_STATE */ - - - /* Client authenticator * Accepts: challenger function * responder function @@ -213,147 +176,3 @@ long auth_oauth2_client (authchallenge_t challenger,authrespond_t responder, cha *trial = 65535; /* don't retry if bad protocol */ return ret; } - -#ifndef HTTP_OAUTH2_INCLUDED -#define HTTP_OAUTH2_INCLUDED -/* - * The code above is enough to implement XOAUTH2, all one needs is the username - * and access token and give it to the function above. However, normal users cannot - * be expected to get the access token, so we ask the client to help with getting - * the access token, refresh token and expire values, so the code below is written - * to help with that. - */ - -#include "http.h" -#include "json.h" - -void -mm_login_oauth2_c_client_method (NETMBX *mb, char *user, char *method, - OAUTH2_S *oauth2, unsigned long trial, int *tryanother) -{ - int i; - HTTP_PARAM_S params[OAUTH2_PARAM_NUMBER]; - OAUTH2_SERVER_METHOD_S RefreshMethod; - char *s = NULL; - JSON_S *json = NULL; - int status = 0; - - if(oauth2->param[OA2_Id].value == NULL || oauth2->param[OA2_Secret].value == NULL){ - oauth2clientinfo_t ogci = - (oauth2clientinfo_t) mail_parameters (NIL, GET_OA2CLIENTINFO, NIL); - - if(ogci) (*ogci)(oauth2->name, &oauth2->param[OA2_Id].value, - &oauth2->param[OA2_Secret].value); - } - - if(oauth2->param[OA2_Id].value == NULL || oauth2->param[OA2_Secret].value == NULL) - return; - - /* first check if we have a refresh token, and in that case use it */ - if(oauth2->param[OA2_RefreshToken].value){ - - RefreshMethod = oauth2->server_mthd[OA2_GetAccessTokenFromRefreshToken]; - for(i = 0; RefreshMethod.params[i] != OA2_End; i++){ - OA2_type j = RefreshMethod.params[i]; - params[i].name = oauth2->param[j].name; - params[i].value = oauth2->param[j].value; - } - params[i].name = params[i].value = NULL; - - if(strcmp(RefreshMethod.name, "POST") == 0) - s = http_post_param(RefreshMethod.urlserver, params, &status); - else if(strcmp(RefreshMethod.name, "POST2") == 0) - s = http_post_param2(RefreshMethod.urlserver, params, &status); - - if(status != 200 && s) - fs_give((void **) &s); /* at this moment, ignore the error */ - - if(s){ - unsigned char *u = s; - json = json_parse(&u); - fs_give((void **) &s); - } - - if(json != NULL){ - JSON_X *jx; - - jx = json_body_value(json, "access_token"); - if(jx && jx->jtype == JString) - oauth2->access_token = cpystr((char *) jx->value); - - jx = json_body_value(json, "expires_in"); - if(jx && jx->jtype == JString) - oauth2->expiration = time(0) + atol((char *) jx->value); - - json_free(&json); - } - return; - } - /* - * else, we do not have a refresh token, nor an access token. - * We need to start the process to get an access code. We use this - * to get an access token and refresh token. - */ - { - RefreshMethod = oauth2->server_mthd[OA2_GetAccessCode]; - for(i = 0; RefreshMethod.params[i] != OA2_End; i++){ - OA2_type j = RefreshMethod.params[i]; - params[i].name = oauth2->param[j].name; - params[i].value = oauth2->param[j].value; - } - params[i].name = params[i].value = NULL; - - if(strcmp(RefreshMethod.name, "GET") == 0){ - char *url = http_get_param_url(RefreshMethod.urlserver, params); - oauth2getaccesscode_t ogac = - (oauth2getaccesscode_t) mail_parameters (NIL, GET_OA2CLIENTGETACCESSCODE, NIL); - - if(ogac) - oauth2->param[OA2_Code].value = (*ogac)(url, method, oauth2, tryanother); - } - - if(oauth2->param[OA2_Code].value){ - RefreshMethod = oauth2->server_mthd[OA2_GetAccessTokenFromAccessCode]; - for(i = 0; RefreshMethod.params[i] != OA2_End; i++){ - OA2_type j = RefreshMethod.params[i]; - params[i].name = oauth2->param[j].name; - params[i].value = oauth2->param[j].value; - } - params[i].name = params[i].value = NULL; - - if(strcmp(RefreshMethod.name, "POST") == 0) - s = http_post_param(RefreshMethod.urlserver, params, &status); - else if(strcmp(RefreshMethod.name, "POST2") == 0) - s = http_post_param2(RefreshMethod.urlserver, paramsm &status); - - if(status != 200 && s) - fs_give((void **) &s); /* at this moment, ignore the error */ - - if(s){ - unsigned char *u = s; - json = json_parse(&u); - fs_give((void **) &s); - } - - if(json != NULL){ - JSON_X *jx; - - jx = json_body_value(json, "refresh_token"); - if(jx && jx->jtype == JString) - oauth2->param[OA2_RefreshToken].value = cpystr((char *) jx->value); - - jx = json_body_value(json, "access_token"); - if(jx && jx->jtype == JString) - oauth2->access_token = cpystr((char *) jx->value); - - jx = json_body_value(json, "expires_in"); - if(jx && jx->jtype == JString) - oauth2->expiration = time(0) + atol((char *) jx->value); - - json_free(&json); - } - } - return; - } -} -#endif /* HTTP_OAUTH2_INCLUDED */ diff --git a/imap/src/c-client/http.h b/imap/src/c-client/http.h index 84607707..621e7c48 100644 --- a/imap/src/c-client/http.h +++ b/imap/src/c-client/http.h @@ -71,11 +71,13 @@ typedef struct http_header_data_s { #define HTTP_MIME_URLENCODED "application/x-www-form-urlencoded" #define HTTP_1_1_VERSION "HTTP/1.1" +#define HTTP_OK 200 +#define HTTP_BAD 400 -#define GET_HTTPPORT (long) 456 -#define SET_HTTPPORT (long) 457 -#define GET_SSLHTTPPORT (long) 458 -#define SET_SSLHTTPPORT (long) 459 +#define GET_HTTPPORT (long) 490 +#define SET_HTTPPORT (long) 491 +#define GET_SSLHTTPPORT (long) 492 +#define SET_SSLHTTPPORT (long) 493 typedef struct http_status_s { char *version; diff --git a/imap/src/c-client/mail.c b/imap/src/c-client/mail.c index baf636a1..a9688d4b 100644 --- a/imap/src/c-client/mail.c +++ b/imap/src/c-client/mail.c @@ -93,6 +93,8 @@ static appenduid_t mailappenduid = NIL; static oauth2getaccesscode_t oauth2getaccesscode = NIL; static oauth2clientinfo_t oauth2clientinfo = NIL; + +static oauth2deviceinfo_t oauth2deviceinfo = NIL; /* free elt extra stuff callback */ static freeeltsparep_t mailfreeeltsparep = NIL; /* free envelope extra stuff callback */ @@ -680,6 +682,11 @@ void *mail_parameters (MAILSTREAM *stream,long function,void *value) case GET_OA2CLIENTINFO: ret = (void *) oauth2clientinfo; break; + case SET_OA2DEVICEINFO: + oauth2deviceinfo = (oauth2deviceinfo_t) value; + case GET_OA2DEVICEINFO: + ret = (void *) oauth2deviceinfo; + break; default: if ((r = smtp_parameters (function,value)) != NULL) ret = r; if ((r = env_parameters (function,value)) != NULL) ret = r; @@ -6444,3 +6451,13 @@ void free_c_client_module_globals(void) env_end(); tcp_end(); } + +/* OAUTH2 support code goes here. This is necessary because + * 1. it helps to coordinate two different methods, such as XOAUTH2 and + * OAUTHBEARER, which use the same code, so it can all go in one place + * + * 2. It helps with coordinating with the client when the server requires + * the deviceinfo method. + */ + +#include "oauth2_aux.c" diff --git a/imap/src/c-client/mail.h b/imap/src/c-client/mail.h index 39c7e4f8..174fb9e3 100644 --- a/imap/src/c-client/mail.h +++ b/imap/src/c-client/mail.h @@ -158,6 +158,8 @@ #define SET_OA2CLIENTGETACCESSCODE (long) 170 #define GET_OA2CLIENTINFO (long) 171 #define SET_OA2CLIENTINFO (long) 172 +#define GET_OA2DEVICEINFO (long) 173 +#define SET_OA2DEVICEINFO (long) 174 /* 2xx: environment */ #define GET_USERNAME (long) 201 @@ -298,6 +300,7 @@ #define SET_IDLETIMEOUT (long) 453 #define GET_FETCHLOOKAHEADLIMIT (long) 454 #define SET_FETCHLOOKAHEADLIMIT (long) 455 +/* HTTP SUPPORT DEFINES THEIR OWN SET_ AND GET_ CONSTANTS (490..493). See http.h */ /* 5xx: local file drivers */ #define GET_MBXPROTECTION (long) 500 @@ -1927,11 +1930,12 @@ int PFLUSH (void); typedef enum {OA2_Id = 0, OA2_Secret, OA2_Code, OA2_RefreshToken, OA2_Scope, OA2_Redirect, OA2_GrantTypeforAccessToken, OA2_GrantTypefromRefreshToken, - OA2_Response, OA2_State, OA2_Prompt, OA2_End} OA2_type; + OA2_Response, OA2_State, OA2_Prompt, OA2_DeviceCode, OA2_End} OA2_type; typedef enum {OA2_GetAccessCode = 0, OA2_GetAccessTokenFromAccessCode, OA2_GetAccessTokenFromRefreshToken, + OA2_GetDeviceCode, OA2_GetEnd} OA2_function; typedef struct OA2_param_s { @@ -1945,15 +1949,38 @@ typedef struct OA2_serverparam_s { OA2_type params[OAUTH2_PARAM_NUMBER]; } OAUTH2_SERVER_METHOD_S; +typedef struct device_code_s { + unsigned char *device_code; + unsigned char *user_code; + char *verification_uri; + int expires_in; + int interval; + unsigned char *message; +} OAUTH2_DEVICECODE_S; + typedef struct oauth2_s { unsigned char *name; /* provider name */ char *host[OAUTH2_TOT_EQUIV]; /* servers for which this data applies */ OAUTH2_PARAM_S param[OA2_End]; /* parameters name and values for this server */ /* servers, methods and parameters to retrieve access code and tokens */ OAUTH2_SERVER_METHOD_S server_mthd[OA2_GetEnd]; + OAUTH2_DEVICECODE_S devicecode; char *access_token; unsigned long expiration; + unsigned int first_time:1; /* this is the first time we get credentials for this account */ } OAUTH2_S; +typedef struct deviceproc_s { + OAUTH2_S *xoauth2; /* the full OAUTH2_S structure we need to update */ + char code_success; /* code to say we succeeded */ + char code_failure; /* code to say we failed */ + char code_wait; /* code to say keep waiting */ +} OAUTH2_DEVICEPROC_S; + +/* Supporting external functions for XOAUTH2 and OAUTHBEARER */ typedef char *(*oauth2getaccesscode_t) (unsigned char *, char *, OAUTH2_S *, int *); typedef void (*oauth2clientinfo_t)(unsigned char *name, char **id, char **secret); +typedef void (*oauth2deviceinfo_t)(OAUTH2_S *, char *method); +void mm_login_oauth2_c_client_method (NETMBX *, char *, char *, OAUTH2_S *, unsigned long, int *); +char *oauth2_generate_state(void); +void oauth2deviceinfo_get_accesscode(void *, void *); diff --git a/imap/src/c-client/oauth2_aux.c b/imap/src/c-client/oauth2_aux.c new file mode 100644 index 00000000..9ff14174 --- /dev/null +++ b/imap/src/c-client/oauth2_aux.c @@ -0,0 +1,314 @@ +/* + * ======================================================================== + * Copyright 2013-2020 Eduardo Chappa + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * ======================================================================== + */ + + +/* OAUTH2 support code goes here. This is necessary because + * 1. it helps to coordinate two different methods, such as XOAUTH2 and + * OAUTHBEARER, which use the same code, so it can all go in one place + * + * 2. It helps with coordinating with the client when the server requires + * the deviceinfo method. + */ + +#include "http.h" +#include "json.h" +#include "oauth2_aux.h" + +/* we generate something like a guid, but not care about + * anything, but that it is really random. + */ +char *oauth2_generate_state(void) +{ + char rv[37]; + int i; + + rv[0] = '\0'; + for(i = 0; i < 4; i++) + sprintf(rv + strlen(rv), "%x", (unsigned int) (random() % 256)); + sprintf(rv + strlen(rv), "%c", '-'); + for(i = 0; i < 2; i++) + sprintf(rv + strlen(rv), "%x", (unsigned int) (random() % 256)); + sprintf(rv + strlen(rv), "%c", '-'); + for(i = 0; i < 2; i++) + sprintf(rv + strlen(rv), "%x", (unsigned int) (random() % 256)); + sprintf(rv + strlen(rv), "%c", '-'); + for(i = 0; i < 2; i++) + sprintf(rv + strlen(rv), "%x", (unsigned int) (random() % 256)); + sprintf(rv + strlen(rv), "%c", '-'); + for(i = 0; i < 6; i++) + sprintf(rv + strlen(rv), "%x", (unsigned int) (random() % 256)); + rv[36] = '\0'; + return cpystr(rv); +} + + +JSON_S *oauth2_json_reply(OAUTH2_SERVER_METHOD_S, OAUTH2_S *, int *); + +#define LOAD_HTTP_PARAMS(X, Y) { \ + int i; \ + for(i = 0; (X).params[i] != OA2_End; i++){ \ + OA2_type j = (X).params[i]; \ + (Y)[i].name = oauth2->param[j].name; \ + (Y)[i].value = oauth2->param[j].value; \ + } \ + (Y)[i].name = (Y)[i].value = NULL; \ +} + +JSON_S *oauth2_json_reply(OAUTH2_SERVER_METHOD_S RefreshMethod, OAUTH2_S *oauth2, int *status) +{ + JSON_S *json = NULL; + HTTP_PARAM_S params[OAUTH2_PARAM_NUMBER]; + unsigned char *s; + + LOAD_HTTP_PARAMS(RefreshMethod, params); + *status = 0; + if(strcmp(RefreshMethod.name, "POST") == 0 + && ((s = http_post_param(RefreshMethod.urlserver, params, status)) != NULL)){ + unsigned char *u = s; + json = json_parse(&u); + fs_give((void **) &s); + } + return json; +} + + +void +mm_login_oauth2_c_client_method (NETMBX *mb, char *user, char *method, + OAUTH2_S *oauth2, unsigned long trial, int *tryanother) +{ + int i, status; + char *s = NULL; + JSON_S *json = NULL; + + if(oauth2->param[OA2_Id].value == NULL || oauth2->param[OA2_Secret].value == NULL){ + oauth2clientinfo_t ogci = + (oauth2clientinfo_t) mail_parameters (NIL, GET_OA2CLIENTINFO, NIL); + + if(ogci) (*ogci)(oauth2->name, &oauth2->param[OA2_Id].value, + &oauth2->param[OA2_Secret].value); + } + + if (oauth2->param[OA2_Id].value == NULL || oauth2->param[OA2_Secret].value == NULL) + return; + + /* Do we have a method to execute? */ + if (oauth2->first_time && oauth2->server_mthd[OA2_GetDeviceCode].name){ + oauth2deviceinfo_t ogdi; + + json = oauth2_json_reply(oauth2->server_mthd[OA2_GetDeviceCode], oauth2, &status); + + if(json != NULL){ + JSON_X *jx; + + jx = json_body_value(json, "device_code"); + if(jx && jx->jtype == JString) + oauth2->devicecode.device_code = cpystr((char *) jx->value); + + jx = json_body_value(json, "user_code"); + if(jx && jx->jtype == JString) + oauth2->devicecode.user_code = cpystr((char *) jx->value); + + jx = json_body_value(json, "verification_uri"); + if(jx && jx->jtype == JString) + oauth2->devicecode.verification_uri = cpystr((char *) jx->value); + + if((jx = json_body_value(json, "expires_in")) != NULL) + switch(jx->jtype){ + case JString: oauth2->devicecode.expires_in = atoi((char *) jx->value); + break; + case JLong : oauth2->devicecode.expires_in = *(long *) jx->value; + break; + } + + if((jx = json_body_value(json, "interval")) != NULL) + switch(jx->jtype){ + case JString: oauth2->devicecode.interval = atoi((char *) jx->value); + break; + case JLong : oauth2->devicecode.interval = *(long *) jx->value; + break; + } + + jx = json_body_value(json, "message"); + if(jx && jx->jtype == JString) + oauth2->devicecode.message = cpystr((char *) jx->value); + + json_free(&json); + + if(oauth2->devicecode.verification_uri && oauth2->devicecode.user_code){ + ogdi = (oauth2deviceinfo_t) mail_parameters (NIL, GET_OA2DEVICEINFO, NIL); + if(ogdi) (*ogdi)(oauth2, method); + } + } + return; + } + + /* else check if we have a refresh token, and in that case use it */ + + if(oauth2->param[OA2_RefreshToken].value){ + + json = oauth2_json_reply(oauth2->server_mthd[OA2_GetAccessTokenFromRefreshToken], oauth2, &status); + + if(json != NULL){ + JSON_X *jx; + + jx = json_body_value(json, "access_token"); + if(jx && jx->jtype == JString) + oauth2->access_token = cpystr((char *) jx->value); + + if((jx = json_body_value(json, "expires_in")) != NULL) + switch(jx->jtype){ + case JString: oauth2->expiration = time(0) + atol((char *) jx->value); + break; + case JLong : oauth2->expiration = time(0) + *(long *) jx->value; + break; + } + + json_free(&json); + } + return; + } + /* + * else, we do not have a refresh token, nor an access token. + * We need to start the process to get an access code. We use this + * to get an access token and refresh token. + */ + { OAUTH2_SERVER_METHOD_S RefreshMethod = oauth2->server_mthd[OA2_GetAccessCode]; + HTTP_PARAM_S params[OAUTH2_PARAM_NUMBER]; + + LOAD_HTTP_PARAMS(RefreshMethod, params); + + if(strcmp(RefreshMethod.name, "GET") == 0){ + char *url = http_get_param_url(RefreshMethod.urlserver, params); + oauth2getaccesscode_t ogac = + (oauth2getaccesscode_t) mail_parameters (NIL, GET_OA2CLIENTGETACCESSCODE, NIL); + + if(ogac) + oauth2->param[OA2_Code].value = (*ogac)(url, method, oauth2, tryanother); + } + + if(oauth2->param[OA2_Code].value){ + json = oauth2_json_reply(oauth2->server_mthd[OA2_GetAccessTokenFromAccessCode], oauth2, &status); + + if(json != NULL){ + JSON_X *jx; + + switch(status){ + case HTTP_OK : jx = json_body_value(json, "refresh_token"); + if(jx && jx->jtype == JString) + oauth2->param[OA2_RefreshToken].value = cpystr((char *) jx->value); + + jx = json_body_value(json, "access_token"); + if(jx && jx->jtype == JString) + oauth2->access_token = cpystr((char *) jx->value); + + if((jx = json_body_value(json, "expires_in")) != NULL) + switch(jx->jtype){ + case JString: oauth2->expiration = time(0) + atol((char *) jx->value); + break; + case JLong : oauth2->expiration = time(0) + *(long *) jx->value; + break; + } + + jx = json_body_value(json, "expires_in"); + if(jx && jx->jtype == JString) + oauth2->expiration = time(0) + atol((char *) jx->value); + + break; + + case HTTP_BAD : break; + + default : { char tmp[100]; + sprintf(tmp, "Oauth Client Received Code %d", status); + fatal (tmp); + } + } + + json_free(&json); + } + } + return; + } + + /* Else, does this server use the /devicecode method? */ +} + +void oauth2deviceinfo_get_accesscode(void *inp, void *outp) +{ + OAUTH2_DEVICEPROC_S *oad = (OAUTH2_DEVICEPROC_S *) inp; + OAUTH2_S *oauth2 = oad->xoauth2; + OAUTH2_DEVICECODE_S *dcode = &oauth2->devicecode; + int done = 0, status, rv; + HTTP_PARAM_S params[OAUTH2_PARAM_NUMBER]; + JSON_S *json; + + if(dcode->device_code && oauth2->param[OA2_DeviceCode].value == NULL) + oauth2->param[OA2_DeviceCode].value = cpystr(dcode->device_code); + + rv = OA2_CODE_WAIT; /* wait by default */ + json = oauth2_json_reply(oauth2->server_mthd[OA2_GetAccessTokenFromAccessCode], oauth2, &status); + + if(json != NULL){ + JSON_X *jx; + char *error; + + switch(status){ + case HTTP_BAD : jx = json_body_value(json, "error"); + if(jx && jx->jtype == JString) + error = cpystr((char *) jx->value); + else + break; + + if(compare_cstring(error, "authorization_pending") == 0) + rv = OA2_CODE_WAIT; + else if(compare_cstring(error, "authorization_declined") == 0) + rv = OA2_CODE_FAIL; + else if(compare_cstring(error, "bad_verification_code") == 0) + rv = OA2_CODE_FAIL; + else if(compare_cstring(error, "expired_token") == 0) + rv = OA2_CODE_FAIL; + else /* keep waiting? */ + rv = OA2_CODE_WAIT; + + break; + + case HTTP_OK : jx = json_body_value(json, "refresh_token"); + if(jx && jx->jtype == JString) + oauth2->param[OA2_RefreshToken].value = cpystr((char *) jx->value); + + jx = json_body_value(json, "access_token"); + if(jx && jx->jtype == JString) + oauth2->access_token = cpystr((char *) jx->value); + + if((jx = json_body_value(json, "expires_in")) != NULL) + switch(jx->jtype){ + case JString: oauth2->expiration = time(0) + atol((char *) jx->value); + break; + case JLong : oauth2->expiration = time(0) + *(long *) jx->value; + break; + } + + rv = OA2_CODE_SUCCESS; + + break; + + default : { char tmp[100]; + sprintf(tmp, "Oauth device Received Code %d", status); + fatal (tmp); + } + } + + json_free(&json); + } + + *(int *)outp = rv; +} diff --git a/imap/src/c-client/oauth2_aux.h b/imap/src/c-client/oauth2_aux.h new file mode 100644 index 00000000..b076da3c --- /dev/null +++ b/imap/src/c-client/oauth2_aux.h @@ -0,0 +1,25 @@ +/* + * ======================================================================== + * Copyright 2013-2020 Eduardo Chappa + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * ======================================================================== + */ +#ifndef C_CLIENT_OAUTH2_AUX_INCLUDED +#define C_CLIENT_OAUTH2_AUX_INCLUDED + +#define OA2_CODE_WAIT 1 +#define OA2_CODE_FAIL -1 +#define OA2_CODE_SUCCESS 0 + +void mm_login_oauth2_c_client_method (NETMBX *, char *, char *, OAUTH2_S *, unsigned long, int *); +char *oauth2_generate_state(void); +void oauth2deviceinfo_get_accesscode(void *, void *); + +#endif /* C_CLIENT_OAUTH2_AUX_INCLUDED */ + diff --git a/imap/src/osdep/unix/Makefile b/imap/src/osdep/unix/Makefile index cdcbf315..5c9c6051 100644 --- a/imap/src/osdep/unix/Makefile +++ b/imap/src/osdep/unix/Makefile @@ -897,7 +897,7 @@ dummy.o: mail.h misc.h osdep.h dummy.h fdstring.o: mail.h misc.h osdep.h fdstring.h flstring.o: mail.h misc.h osdep.h flstring.h imap4r1.o: mail.h misc.h osdep.h imap4r1.h rfc822.h -mail.o: mail.h misc.h osdep.h rfc822.h linkage.h json.h +mail.o: mail.h misc.h osdep.h rfc822.h linkage.h http.h json.h oauth2_aux.h oauth2_aux.c mbx.o: mail.h misc.h osdep.h dummy.h mh.o: mail.h misc.h osdep.h dummy.h mix.o: mail.h misc.h osdep.h dummy.h @@ -925,14 +925,14 @@ http.o: mail.h misc.h osdep.h utf8.h http.h json.h # OS-dependent osdep.o:mail.h misc.h env.h fs.h ftl.h nl.h tcp.h \ - osdep.h env_unix.h tcp_unix.h \ + osdep.h env_unix.h tcp_unix.h oauth2_aux.h \ osdep.c env_unix.c fs_unix.c ftl_unix.c nl_unix.c tcp_unix.c ip_unix.c\ auths.c crexcl.c flockcyg.c flocklnx.c flocksim.c fsync.c \ gethstid.c getspnam.c \ gr_wait.c gr_wait4.c gr_waitp.c \ kerb_mit.c \ auth_ext.c auth_gss.c auth_log.c auth_md5.c auth_ntl.c \ - auth_bea.c auth_oa2.c auth_pla.c \ + auth_bea.c auth_oa2.c auth_pla.c oauth2_aux.c \ pmatch.c scandir.c setpgrp.c strerror.c truncate.c write.c \ memmove.c memmove2.c memset.c \ tz_bsd.c tz_nul.c tz_sv4.c \ diff --git a/pith/pine.hlp b/pith/pine.hlp index c6ec53a4..9b3e55d3 100644 --- a/pith/pine.hlp +++ b/pith/pine.hlp @@ -140,7 +140,7 @@ with help text for the config screen and the composer that didn't have any reasonable place to be called from. Dummy change to get revision in pine.hlp ============= h_revision ================= -Alpine Commit 442 2020-06-11 01:39:17 +Alpine Commit 443 2020-06-12 02:21:54 ============= h_news ================= @@ -173,9 +173,15 @@ Apache web server.

New in Alpine ()

- +problems you find with this release. + +

Additions include: +

Version addresses bugs found in previous releases and -- cgit v1.2.3-70-g09d2