From 4ae1d7cc2643e4773a136395f12bc02fc6867854 Mon Sep 17 00:00:00 2001 From: Stefan Eissing Date: Fri, 15 May 2026 11:45:49 +0200 Subject: [PATCH] netrc: scanner refactor Refactor the netrc scanner. Add test case for checking that the last matched machine with unmatched login does not return the password as success (unit1304). Closes #21624 --- lib/netrc.c | 895 +++++++++++++++++++++++------------------- lib/netrc.h | 14 +- lib/url.c | 153 ++++---- tests/unit/unit1304.c | 117 +++--- 4 files changed, 616 insertions(+), 563 deletions(-) diff --git a/lib/netrc.c b/lib/netrc.c index eb67f2505e..48aaa76816 100644 --- a/lib/netrc.c +++ b/lib/netrc.c @@ -36,38 +36,469 @@ #endif #include "netrc.h" +#include "urldata.h" #include "creds.h" +#include "curl_trc.h" #include "strcase.h" #include "curl_get_line.h" #include "curlx/fopen.h" #include "curlx/strparse.h" -/* Get user and password from .netrc when given a machine name */ -enum host_lookup_state { - NOTHING, - HOSTFOUND, /* the 'machine' keyword was found */ - HOSTVALID, /* this is "our" machine! */ - MACDEF -}; - -enum found_state { - NONE, - LOGIN, - PASSWORD -}; - -#define FOUND_LOGIN 1 -#define FOUND_PASSWORD 2 +/* .netrc is not really a standard. The GNU definition can be found here: + * https://www.gnu.org/software/inetutils/manual/\ + * html_node/The-_002enetrc-file.html + * This gives grammar like: + * + * LITERAL := \S+ | QUOTED + * QUOTED := "(\\[rnt\]|[^"])*" + * ANYTHING := . + * EMPTY_LINE := \r*\n\r*\n + * MACHINE := machine # case-insensitive + * LOGIN := login # case-insensitive + * PASSWD := password # case-insensitive + * ACCOUNT := account # case-insensitive + * MACDEF := macdef # case-insensitive + * DEFAULT := default # case-insensitive + * + * MACRO := MACDEF ANYTHING* EMPTY_LINE + * JUNK := LITERAL + * LKEY := ( LOGIN | PASSWD | ACCOUNT ) LITERAL + * MENTRY := MACHINE LITERAL LKEY* + * DENTRY := DEFAULT LKEY* + * NETRC := (MENTRY | DENTRY | MACRO | JUNK )* EOF + * + * Tokens are separated by whitespace or newlines. which have otherwise + * no special meaning, apart from the empty line ending a MACRO. + * + * Parsing is not strict, unmatched LITERALs are ignored + */ #define MAX_NETRC_LINE 16384 #define MAX_NETRC_FILE (128 * 1024) #define MAX_NETRC_TOKEN 4096 +#define NETRC_DEBUG 0 + /* convert a dynbuf call CURLcode error to a NETRCcode error */ -#define curl2netrc(result) \ - (((result) == CURLE_OUT_OF_MEMORY) ? \ - NETRC_OUT_OF_MEMORY : NETRC_SYNTAX_ERROR) +#define curl2netrc(r) \ + ((!(r)) ? NETRC_OK : (((r) == CURLE_OUT_OF_MEMORY) ? \ + NETRC_OUT_OF_MEMORY : NETRC_SYNTAX_ERROR)) + +typedef enum { + NETRC_TOK_EOF, + NETRC_TOK_LITERAL, + NETRC_TOK_MACHINE, + NETRC_TOK_DEFAULT, + NETRC_TOK_ACCOUNT, + NETRC_TOK_LOGIN, + NETRC_TOK_PASSWD, + NETRC_TOK_MACDEF, + NETRC_TOK_JUNK +} curl_netrc_token; + +struct netrc_lexer { + struct Curl_easy *data; + const char *content; + const char *pos; + struct dynbuf literal; + curl_netrc_token token; + bool pushed; +}; + +#if NETRC_DEBUG +static const char *netrc_tokenstr(curl_netrc_token token) +{ + switch(token) { + case NETRC_TOK_EOF: + return "[EOF]"; + case NETRC_TOK_LITERAL: + return "[LITERAL]"; + case NETRC_TOK_MACHINE: + return "[MACHINE]"; + case NETRC_TOK_DEFAULT: + return "[DEFAULT]"; + case NETRC_TOK_ACCOUNT: + return "[ACCOUNT]"; + case NETRC_TOK_LOGIN: + return "[LOGIN]"; + case NETRC_TOK_PASSWD: + return "[PASSWORD]"; + case NETRC_TOK_MACDEF: + return "[MACDEF]"; + case NETRC_TOK_JUNK: + return "[JUNK]"; + default: + return "[???]"; + } +} + +#endif + +static void netrc_lexer_init(struct netrc_lexer *lexer, + struct Curl_easy *data, + const char *content) +{ + curlx_dyn_init(&lexer->literal, MAX_NETRC_TOKEN); + lexer->data = data; + lexer->content = lexer->pos = content; +} + +static void netrc_lexer_cleanup(struct netrc_lexer *lexer) +{ + lexer->content = lexer->pos = NULL; + lexer->data = NULL; + curlx_dyn_free(&lexer->literal); +} + +static void netrc_skip_blanks(struct netrc_lexer *lexer) +{ + const char *s = lexer->pos; + while(*s) { + curlx_str_passblanks(&s); + while(*s == '\r') + ++s; + if(*s == '\n') { + ++s; + } + else + break; + } + lexer->pos = s; +} + +static void netrc_skip_to_empty_line(struct netrc_lexer *lexer) +{ + const char *s = lexer->pos; + while(*s) { + if(*s == '\r') + ++s; + else if(*s == '\n') { + ++s; + while(*s == '\r') + ++s; + if(*s == '\n') + goto out; + } + else + ++s; + } +out: + lexer->pos = s; +} + +/* + * Parse a quoted token starting after the opening '"'. Handles \n, \r, \t + * escape sequences. Advances *tok_endp past the closing '"'. + * + * Returns NETRC_OK or error. + */ +static NETRCcode netrc_lexer_quoted(struct netrc_lexer *lexer) +{ + NETRCcode rc = NETRC_SYNTAX_ERROR; + const char *s = lexer->pos; + bool escape = FALSE; + CURLcode result; + + DEBUGASSERT(*s == '\"'); + ++s; /* pass the leading quote */ + while(*s) { + char c = *s; + if(escape) { + escape = FALSE; + switch(c) { + case 'n': + c = '\n'; + break; + case 'r': + c = '\r'; + break; + case 't': + c = '\t'; + break; + } + } + else if(c == '\\') { + escape = TRUE; + ++s; + continue; + } + else if(c == '\"') { + ++s; /* pass the ending quote */ + rc = NETRC_OK; + goto out; + } + result = curlx_dyn_addn(&lexer->literal, &c, 1); + if(result) { + rc = curl2netrc(result); + goto out; + } + ++s; + } +out: + lexer->pos = s; + return rc; +} + +static void netrc_lexer_push(struct netrc_lexer *lexer) +{ + lexer->pushed = TRUE; +} + +static NETRCcode netrc_lexer_next(struct netrc_lexer *lexer, + bool want_literal) +{ + const char *s = lexer->pos, *start; + NETRCcode rc = NETRC_OK; + size_t slen; + CURLcode result; + + if(lexer->pushed) { + lexer->pushed = FALSE; + goto out; + } + + curlx_dyn_reset(&lexer->literal); + netrc_skip_blanks(lexer); + s = lexer->pos; + + switch(*s) { + case 0: + lexer->token = NETRC_TOK_EOF; + break; + case '\"': + rc = netrc_lexer_quoted(lexer); + lexer->token = NETRC_TOK_LITERAL; + s = lexer->pos; + break; + default: + /* unquoted token */ + start = s; + while(*s && !ISBLANK(*s) && !ISNEWLINE(*s)) + ++s; + slen = s - start; + if(!slen) { + rc = NETRC_SYNTAX_ERROR; + } + if(want_literal) { + lexer->token = NETRC_TOK_LITERAL; + result = curlx_dyn_addn(&lexer->literal, start, slen); + rc = curl2netrc(result); + } + else if((slen == 7) && curl_strnequal(start, "machine", slen)) { + lexer->token = NETRC_TOK_MACHINE; + } + else if((slen == 7) && curl_strnequal(start, "default", slen)) { + lexer->token = NETRC_TOK_DEFAULT; + } + else if((slen == 7) && curl_strnequal(start, "account", slen)) { + lexer->token = NETRC_TOK_ACCOUNT; + } + else if((slen == 5) && curl_strnequal(start, "login", slen)) { + lexer->token = NETRC_TOK_LOGIN; + } + else if((slen == 8) && curl_strnequal(start, "password", slen)) { + lexer->token = NETRC_TOK_PASSWD; + } + else if((slen == 6) && curl_strnequal(start, "macdef", slen)) { + lexer->token = NETRC_TOK_MACDEF; + } + else { + lexer->token = NETRC_TOK_JUNK; + } + break; + } + +out: +#if NETRC_DEBUG + CURL_TRC_M(lexer->data, "[NETRC] token %s '%s', rc=%d", + netrc_tokenstr(lexer->token), + curlx_dyn_ptr(&lexer->literal), rc); +#endif + lexer->pos = s; + return rc; +} + +struct netrc_scanner { + struct netrc_lexer lexer; + const char *hostname; /* non-NULL, machine to scan for */ + const char *user; /* maybe NULL, login to scan for */ + char *login; + char *passwd; + struct Curl_creds *creds; + bool matches_host; + bool found; +}; + +static void netrc_scan_reset(struct netrc_scanner *sc) +{ + curlx_safefree(sc->login); + curlx_safefree(sc->passwd); + sc->matches_host = FALSE; +} + +static void netrc_scan_init(struct netrc_scanner *sc, + struct Curl_easy *data, + const char *content, + const char *hostname, + const char *user) +{ + memset(sc, 0, sizeof(*sc)); + netrc_lexer_init(&sc->lexer, data, content); + sc->hostname = hostname; + sc->user = (user && user[0]) ? user : NULL; + netrc_scan_reset(sc); +} + +static void netrc_scan_cleanup(struct netrc_scanner *sc) +{ + netrc_scan_reset(sc); + sc->hostname = NULL; + sc->user = NULL; + Curl_creds_unlink(&sc->creds); + netrc_lexer_cleanup(&sc->lexer); +} + +static NETRCcode netrc_scan_literal(struct netrc_scanner *sc, + char **pdest) +{ + NETRCcode rc = netrc_lexer_next(&sc->lexer, TRUE); + if(!rc) { + if(sc->lexer.token == NETRC_TOK_LITERAL) { + if(pdest && sc->matches_host) { + curlx_free(*pdest); + *pdest = curlx_strdup(curlx_dyn_ptr(&sc->lexer.literal)); + if(!*pdest) + rc = NETRC_OUT_OF_MEMORY; + } + } + else + netrc_lexer_push(&sc->lexer); + } + return rc; +} + +static NETRCcode netrc_scan_end_entry(struct netrc_scanner *sc) +{ + NETRCcode rc = NETRC_OK; +#if NETRC_DEBUG + CURL_TRC_M(sc->lexer.data, + "[NETRC] entry matches_host=%d, login='%s', passwd='%s'", + sc->matches_host, sc->login, sc->passwd); +#endif + if(sc->matches_host) { + if(sc->login) { + if(sc->user) { + if(Curl_timestrcmp(sc->user, sc->login)) + goto out; + /* We look for a specific user, + * entry is only interesting with password */ + sc->found = !!sc->passwd; + } + else { + sc->found = TRUE; + } + } + else if(sc->passwd) { + /* found a passwd that applies to any user */ + sc->found = TRUE; + } + else { + /* entry has nothing interesting */ + } + if(sc->found) { +#if NETRC_DEBUG + CURL_TRC_M(sc->lexer.data, "[NETRC] entry match found"); +#endif + if(Curl_creds_create(sc->user ? sc->user : sc->login, sc->passwd, + NULL, NULL, NULL, CREDS_NETRC, &sc->creds)) + rc = NETRC_OUT_OF_MEMORY; + } + } +out: + netrc_scan_reset(sc); + return rc; +} + +static NETRCcode netrc_scan(struct Curl_easy *data, + const char *content, + const char *hostname, + const char *user, + struct Curl_creds **pcreds) +{ + struct netrc_scanner sc; + NETRCcode rc = NETRC_OK; + + Curl_creds_unlink(pcreds); + netrc_scan_init(&sc, data, content, hostname, user); + + while(!rc && !sc.found) { + rc = netrc_lexer_next(&sc.lexer, FALSE); + if(!rc) { + /* Does this token end any previous entry? */ + switch(sc.lexer.token) { + case NETRC_TOK_EOF: + case NETRC_TOK_MACHINE: + case NETRC_TOK_DEFAULT: + case NETRC_TOK_MACDEF: + rc = netrc_scan_end_entry(&sc); + if(rc || sc.found) + goto out; + break; + default: + break; + } + + switch(sc.lexer.token) { + case NETRC_TOK_EOF: + goto out; + case NETRC_TOK_MACHINE: + rc = netrc_lexer_next(&sc.lexer, TRUE); + if(!rc) { + if(sc.lexer.token == NETRC_TOK_LITERAL) { + sc.matches_host = curl_strequal( + sc.hostname, curlx_dyn_ptr(&sc.lexer.literal)); + } + else { + sc.matches_host = FALSE; + netrc_lexer_push(&sc.lexer); + } + } + break; + case NETRC_TOK_DEFAULT: + sc.matches_host = TRUE; + break; + case NETRC_TOK_ACCOUNT: + rc = netrc_scan_literal(&sc, NULL); /* ignore, not used */ + break; + case NETRC_TOK_LOGIN: + rc = netrc_scan_literal(&sc, &sc.login); + break; + case NETRC_TOK_PASSWD: + rc = netrc_scan_literal(&sc, &sc.passwd); + break; + case NETRC_TOK_MACDEF: + netrc_skip_to_empty_line(&sc.lexer); + break; + case NETRC_TOK_LITERAL: + case NETRC_TOK_JUNK: + default: + /* skip this */ + break; + } + } + } + +out: + if(!rc) { + if(sc.creds) + Curl_creds_link(pcreds, sc.creds); + else + rc = NETRC_NO_MATCH; + } + netrc_scan_cleanup(&sc); + return rc; +} static NETRCcode file2memory(const char *filename, struct dynbuf *filebuf) { @@ -107,393 +538,25 @@ static NETRCcode file2memory(const char *filename, struct dynbuf *filebuf) return ret; } -/* bundled parser state to keep function signatures compact */ -struct netrc_state { - struct Curl_creds *existing; - char *login; - char *password; - enum host_lookup_state state; - enum found_state keyword; - NETRCcode retcode; - unsigned char found; /* FOUND_LOGIN | FOUND_PASSWORD bits */ - bool our_login; - bool done; -}; - -/* - * Parse a quoted token starting after the opening '"'. Handles \n, \r, \t - * escape sequences. Advances *tok_endp past the closing '"'. - * - * Returns NETRC_OK or error. - */ -static NETRCcode netrc_quoted_token(const char **tok_endp, - struct dynbuf *token) +static NETRCcode netrc_scan_file(struct Curl_easy *data, + struct store_netrc *store, + const char *hostname, + const char *user, + const char *netrcfile, + struct Curl_creds **pcreds) { - bool escape = FALSE; - NETRCcode rc = NETRC_SYNTAX_ERROR; - const char *tok_end = *tok_endp; - tok_end++; /* pass the leading quote */ - while(*tok_end) { - CURLcode result; - char s = *tok_end; - if(escape) { - escape = FALSE; - switch(s) { - case 'n': - s = '\n'; - break; - case 'r': - s = '\r'; - break; - case 't': - s = '\t'; - break; - } - } - else if(s == '\\') { - escape = TRUE; - tok_end++; - continue; - } - else if(s == '\"') { - tok_end++; /* pass the ending quote */ - rc = NETRC_OK; - break; - } - result = curlx_dyn_addn(token, &s, 1); - if(result) { - *tok_endp = tok_end; - return curl2netrc(result); - } - tok_end++; - } - *tok_endp = tok_end; - return rc; -} - -/* - * Gets the next token from the netrc buffer at *tokp. Writes the token into - * the 'token' dynbuf. Advances *tok_endp past the consumed token in the input - * buffer. Updates *statep for MACDEF newline handling. Sets *lineend = TRUE - * when the line is exhausted. - * - * Returns NETRC_OK or an error code. - */ -static NETRCcode netrc_get_token(const char **tokp, - const char **tok_endp, - struct dynbuf *token, - enum host_lookup_state *statep, - bool *lineend) -{ - const char *tok = *tokp; - const char *tok_end; - - *lineend = FALSE; - curlx_dyn_reset(token); - curlx_str_passblanks(&tok); - - /* tok is first non-space letter */ - if(*statep == MACDEF) { - if((*tok == '\n') || (*tok == '\r')) - *statep = NOTHING; /* end of macro definition */ - *lineend = TRUE; - *tokp = tok; - return NETRC_OK; - } - - if(!*tok || (*tok == '\n')) { - /* end of line */ - *lineend = TRUE; - *tokp = tok; - return NETRC_OK; - } - - tok_end = tok; - if(*tok == '\"') { - /* quoted string */ - NETRCcode ret = netrc_quoted_token(&tok_end, token); - if(ret) - return ret; - } - else { - /* unquoted token */ - size_t len = 0; - CURLcode result; - while(*tok_end > ' ') { - tok_end++; - len++; - } - if(!len) - return NETRC_SYNTAX_ERROR; - result = curlx_dyn_addn(token, tok, len); - if(result) - return curl2netrc(result); - } - - *tok_endp = tok_end; - - if(curlx_dyn_len(token)) - *tokp = curlx_dyn_ptr(token); - else - /* set it to blank to avoid NULL */ - *tokp = ""; - - return NETRC_OK; -} - -/* - * Reset parser for a new machine entry. Frees password and optionally login - * if it was not user-specified. - */ -static void netrc_new_machine(struct netrc_state *ns) -{ - ns->keyword = NONE; - ns->found = 0; - ns->our_login = FALSE; - curlx_safefree(ns->password); - curlx_safefree(ns->login); -} - -/* - * Process a parsed token through the HOSTVALID state machine branch. This - * handles login/password values and keyword transitions for the matched host. - * - * Returns NETRC_OK or an error code. - */ -static NETRCcode netrc_hostvalid(struct netrc_state *ns, const char *tok) -{ - if(ns->keyword == LOGIN) { - if(Curl_creds_has_user(ns->existing)) - ns->our_login = !Curl_timestrcmp(ns->existing->user, tok); - else { - ns->our_login = TRUE; - curlx_free(ns->login); - ns->login = curlx_strdup(tok); - if(!ns->login) - return NETRC_OUT_OF_MEMORY; - } - ns->found |= FOUND_LOGIN; - ns->keyword = NONE; - } - else if(ns->keyword == PASSWORD) { - curlx_free(ns->password); - ns->password = curlx_strdup(tok); - if(!ns->password) - return NETRC_OUT_OF_MEMORY; - ns->found |= FOUND_PASSWORD; - ns->keyword = NONE; - } - else if(curl_strequal("login", tok)) - ns->keyword = LOGIN; - else if(curl_strequal("password", tok)) - ns->keyword = PASSWORD; - else if(curl_strequal("machine", tok)) { - /* a new machine here */ - bool specific_login = Curl_creds_has_user(ns->existing); - - if((ns->found & FOUND_PASSWORD) && - /* a password was provided for this host */ - (!specific_login || ns->our_login || - /* and found a login that is suitable - (either matched specific one or simply present) */ - (specific_login && !(ns->found & FOUND_LOGIN)))) { - /* or we look for a specific login, but no login was not specified */ - - ns->done = TRUE; - return NETRC_OK; - } - - ns->state = HOSTFOUND; - netrc_new_machine(ns); - } - else if(curl_strequal("default", tok)) { - ns->state = HOSTVALID; - ns->retcode = NETRC_OK; - netrc_new_machine(ns); - } - if((ns->found == (FOUND_PASSWORD | FOUND_LOGIN)) && ns->our_login) - ns->done = TRUE; - return NETRC_OK; -} - -/* - * Process one parsed token through the netrc state - * machine. Updates the parser state in *ns. - * Returns NETRC_OK or an error code. - */ -static NETRCcode netrc_handle_token(struct netrc_state *ns, - const char *tok, - const char *host) -{ - switch(ns->state) { - case NOTHING: - if(curl_strequal("macdef", tok)) - ns->state = MACDEF; - else if(curl_strequal("machine", tok)) { - ns->state = HOSTFOUND; - netrc_new_machine(ns); - } - else if(curl_strequal("default", tok)) { - ns->state = HOSTVALID; - ns->retcode = NETRC_OK; - } - break; - case MACDEF: - if(!*tok) - ns->state = NOTHING; - break; - case HOSTFOUND: - if(curl_strequal(host, tok)) { - ns->state = HOSTVALID; - ns->retcode = NETRC_OK; - } - else - ns->state = NOTHING; - break; - case HOSTVALID: - return netrc_hostvalid(ns, tok); - } - return NETRC_OK; -} - -/* - * Finalize the parse result: fill in defaults and free - * resources on error. - */ -static NETRCcode netrc_finalize(struct netrc_state *ns, - struct store_netrc *store, - struct Curl_creds **pcreds) -{ - NETRCcode retcode = ns->retcode; - if(!retcode) { - if(!ns->password && ns->our_login) { - /* success without a password, set a blank one */ - ns->password = curlx_strdup(""); - if(!ns->password) { - retcode = NETRC_OUT_OF_MEMORY; - goto out; - } - } - else if(!ns->login && !ns->password) { - /* a default with no credentials */ - retcode = NETRC_NO_MATCH; - goto out; - } - } - - if(!retcode) { - /* success - netrc_finalize() can return a password even when specific_login is set - but our_login is false (e.g., host matched but the requested login - never matched). See test 685. */ - const char *login = Curl_creds_has_user(ns->existing) ? - ns->existing->user : ns->login; - /* success without a password, set a blank one */ - const char *passwd = ns->password ? ns->password : ""; - - if(Curl_creds_create(login, passwd, NULL, NULL, NULL, CREDS_NETRC, - pcreds)) { - retcode = NETRC_OUT_OF_MEMORY; - goto out; - } - } - -out: - curlx_free(ns->login); - curlx_free(ns->password); - if(retcode) { - curlx_dyn_free(&store->filebuf); - store->loaded = FALSE; - } - return retcode; -} - -/* - * Returns zero on success. - */ -static NETRCcode parsenetrc(struct store_netrc *store, - const char *host, - struct Curl_creds *existing, - const char *netrcfile, - struct Curl_creds **pcreds) -{ - const char *netrcbuffer; - struct dynbuf token; struct dynbuf *filebuf = &store->filebuf; - struct netrc_state ns; - - DEBUGASSERT(!existing || !Curl_creds_has_passwd(existing)); - memset(&ns, 0, sizeof(ns)); - ns.retcode = NETRC_NO_MATCH; - ns.existing = existing; - - curlx_dyn_init(&token, MAX_NETRC_TOKEN); if(!store->loaded) { NETRCcode ret = file2memory(netrcfile, filebuf); - if(ret) + if(ret) { + CURL_TRC_M(data, "[NETRC] could not load '%s'", netrcfile); return ret; + } store->loaded = TRUE; } - netrcbuffer = curlx_dyn_ptr(filebuf); - - while(!ns.done) { - const char *tok = netrcbuffer; - while(tok && !ns.done) { - const char *tok_end; - bool lineend; - NETRCcode ret; - - ret = netrc_get_token(&tok, &tok_end, &token, &ns.state, &lineend); - if(ret) { - ns.retcode = ret; - goto out; - } - if(lineend) - break; - - ret = netrc_handle_token(&ns, tok, host); - if(ret) { - ns.retcode = ret; - goto out; - } - /* tok_end cannot point to a null byte here since lines are always - newline terminated */ - DEBUGASSERT(*tok_end); - tok = ++tok_end; - } - if(!ns.done) { - const char *nl = NULL; - if(tok) - nl = strchr(tok, '\n'); - if(!nl) - break; - /* point to next line */ - netrcbuffer = &nl[1]; - } - } /* while !done */ - -out: - curlx_dyn_free(&token); - return netrc_finalize(&ns, store, pcreds); -} - -const char *Curl_netrc_strerror(NETRCcode ret) -{ - switch(ret) { - default: - return ""; /* not a legit error */ - case NETRC_FILE_MISSING: - return "no such file"; - case NETRC_NO_MATCH: - return "no matching entry"; - case NETRC_OUT_OF_MEMORY: - return "out of memory"; - case NETRC_SYNTAX_ERROR: - return "syntax error"; - } - /* never reached */ + return netrc_scan(data, curlx_dyn_ptr(filebuf), hostname, user, pcreds); } /* @@ -502,14 +565,18 @@ const char *Curl_netrc_strerror(NETRCcode ret) * *loginp and *passwordp MUST be allocated if they are not NULL when passed * in. */ -NETRCcode Curl_parsenetrc(struct store_netrc *store, const char *host, - struct Curl_creds *existing, +NETRCcode Curl_netrc_scan(struct Curl_easy *data, + struct store_netrc *store, + const char *hostname, + const char *user, const char *netrcfile, struct Curl_creds **pcreds) { NETRCcode retcode = NETRC_OK; char *filealloc = NULL; + CURL_TRC_M(data, "[NETRC] scanning '%s' for host '%s' user '%s'", + netrcfile, hostname, user); Curl_creds_unlink(pcreds); if(!netrcfile) { char *home = NULL; @@ -559,7 +626,8 @@ NETRCcode Curl_parsenetrc(struct store_netrc *store, const char *host, goto out; } } - retcode = parsenetrc(store, host, existing, filealloc, pcreds); + retcode = netrc_scan_file( + data, store, hostname, user, filealloc, pcreds); curlx_free(filealloc); #ifdef _WIN32 if(retcode == NETRC_FILE_MISSING) { @@ -569,14 +637,17 @@ NETRCcode Curl_parsenetrc(struct store_netrc *store, const char *host, curlx_free(homea); return NETRC_OUT_OF_MEMORY; } - retcode = parsenetrc(store, host, existing, filealloc, pcreds); + retcode = netrc_scan_file( + data, store, hostname, user, filealloc, pcreds); curlx_free(filealloc); } #endif curlx_free(homea); } else - retcode = parsenetrc(store, host, existing, netrcfile, pcreds); + retcode = netrc_scan_file( + data, store, hostname, user, netrcfile, pcreds); + out: if(retcode) Curl_creds_unlink(pcreds); @@ -593,4 +664,22 @@ void Curl_netrc_cleanup(struct store_netrc *store) curlx_dyn_free(&store->filebuf); store->loaded = FALSE; } -#endif + +const char *Curl_netrc_strerror(NETRCcode ret) +{ + switch(ret) { + default: + return ""; /* not a legit error */ + case NETRC_FILE_MISSING: + return "no such file"; + case NETRC_NO_MATCH: + return "no matching entry"; + case NETRC_OUT_OF_MEMORY: + return "out of memory"; + case NETRC_SYNTAX_ERROR: + return "syntax error"; + } + /* never reached */ +} + +#endif /* !CURL_DISABLE_NETRC */ diff --git a/lib/netrc.h b/lib/netrc.h index 92dd4d47c9..6be9b83316 100644 --- a/lib/netrc.h +++ b/lib/netrc.h @@ -29,6 +29,7 @@ #include "curlx/dynbuf.h" +struct Curl_easy; struct Curl_creds; struct store_netrc { @@ -50,15 +51,14 @@ const char *Curl_netrc_strerror(NETRCcode ret); void Curl_netrc_init(struct store_netrc *store); void Curl_netrc_cleanup(struct store_netrc *store); -NETRCcode Curl_parsenetrc(struct store_netrc *store, const char *host, - struct Curl_creds *existing, +/* Scan a netrc file for credentials matching hostname + * and optional user. */ +NETRCcode Curl_netrc_scan(struct Curl_easy *data, + struct store_netrc *store, + const char *hostname, + const char *user, const char *netrcfile, struct Curl_creds **pcreds); -/* Assume: (*passwordp)[0]=0, host[0] != 0. - * If (*loginp)[0] = 0, search for login and password within a machine - * section in the netrc. - * If (*loginp)[0] != 0, search for password within machine and login. - */ #else /* disabled */ #define Curl_netrc_init(x) diff --git a/lib/url.c b/lib/url.c index ec4fb8b1b0..a569c3e4bc 100644 --- a/lib/url.c +++ b/lib/url.c @@ -2161,14 +2161,11 @@ static bool str_has_ctrl(const char *input) static CURLcode override_login(struct Curl_easy *data, struct connectdata *conn) { - CURLUcode uc; char **optionsp = &conn->options; #ifndef CURL_DISABLE_NETRC - struct Curl_creds *ncreds_in = NULL; struct Curl_creds *ncreds_out = NULL; #endif CURLcode result = CURLE_OK; - bool creds_changed = FALSE; if(data->set.str[STRING_OPTIONS]) { curlx_free(*optionsp); @@ -2180,107 +2177,97 @@ static CURLcode override_login(struct Curl_easy *data, } #ifndef CURL_DISABLE_NETRC - if(data->set.use_netrc) { - /* Determine how to react on already existing credentials */ - if(data->set.use_netrc == CURL_NETRC_REQUIRED) { - Curl_creds_unlink(&conn->creds); - } + if(data->set.use_netrc) { /* not CURL_NETRC_IGNORED */ + struct Curl_creds *ncreds_in = NULL; + bool scan_netrc = TRUE; + NETRCcode ret; + CURLUcode uc; if(data->state.creds) { switch(data->state.creds->source) { case CREDS_OPTION: - /* we never override credentials set via CURLOPT_* */ - goto out; - case CREDS_URL: + /* we never override credentials set via CURLOPT_*, leave. */ + scan_netrc = FALSE; + break; + case CREDS_URL: /* only apply when netrc is not required */ if(data->set.use_netrc == CURL_NETRC_REQUIRED) { - /* use the URL user to search netrc */ - result = Curl_creds_create( - data->state.creds->user, NULL, NULL, NULL, NULL, CREDS_URL, - &ncreds_in); - if(result) - goto out; + /* We ignore password from URL */ + ncreds_in = data->state.creds; + } + else if(!Curl_creds_has_user(data->state.creds) || + !Curl_creds_has_passwd(data->state.creds)) { + /* We use netrc to complete what is missing */ + ncreds_in = data->state.creds; } else - /* only search when something is still missing */ - Curl_creds_link(&ncreds_in, data->state.creds); + scan_netrc = FALSE; break; - default: - /* ignore credentials from other sources */ + default: /* ignore credentials from other sources */ break; } } - /* Only search in netrc when the creds are not already complete */ - if(!Curl_creds_has_passwd(ncreds_in)) { - NETRCcode ret; + if(!scan_netrc) + goto out; - CURL_TRC_M(data, "netrc: find credentials for %s, user %s", - conn->origin->hostname, - Curl_creds_has_user(ncreds_in) ? ncreds_in->user : "*"); - ret = Curl_parsenetrc(&data->state.netrc, - conn->origin->hostname, - ncreds_in, - data->set.str[STRING_NETRC_FILE], - &ncreds_out); - DEBUGASSERT(!ret || !ncreds_out); - if(ret == NETRC_OUT_OF_MEMORY) { - result = CURLE_OUT_OF_MEMORY; - goto out; - } - else if(ret && ((ret == NETRC_NO_MATCH) || - (data->set.use_netrc == CURL_NETRC_OPTIONAL))) { - infof(data, "Could not find host %s in the %s file; using defaults", - conn->origin->hostname, - (data->set.str[STRING_NETRC_FILE] ? - data->set.str[STRING_NETRC_FILE] : ".netrc")); - } - else if(ret) { - const char *m = Curl_netrc_strerror(ret); - failf(data, ".netrc error: %s", m); - result = CURLE_READ_ERROR; - goto out; - } - else if(ncreds_out) { - if(!(conn->scheme->flags & PROTOPT_USERPWDCTRL)) { - /* if the protocol cannot handle control codes in credentials, make - sure there are none */ - if(str_has_ctrl(ncreds_out->user) || - str_has_ctrl(ncreds_out->passwd)) { - failf(data, "control code detected in .netrc credentials"); - result = CURLE_READ_ERROR; - goto out; - } - } - CURL_TRC_M(data, "netrc: using credentials for %s as %s", - conn->origin->hostname, ncreds_out->user); - result = Curl_creds_merge(ncreds_out->user, ncreds_out->passwd, - data->state.creds, CREDS_NETRC, - &data->state.creds); - if(result) + ret = Curl_netrc_scan(data, &data->state.netrc, + conn->origin->hostname, + Curl_creds_user(ncreds_in), + data->set.str[STRING_NETRC_FILE], + &ncreds_out); + DEBUGASSERT(!ret || !ncreds_out); + if(ret == NETRC_OUT_OF_MEMORY) { + result = CURLE_OUT_OF_MEMORY; + goto out; + } + else if(ret && ((ret == NETRC_NO_MATCH) || + (data->set.use_netrc == CURL_NETRC_OPTIONAL))) { + infof(data, "Could not find host %s in the %s file; using defaults", + conn->origin->hostname, + (data->set.str[STRING_NETRC_FILE] ? + data->set.str[STRING_NETRC_FILE] : ".netrc")); + } + else if(ret) { + const char *m = Curl_netrc_strerror(ret); + failf(data, ".netrc error: %s", m); + result = CURLE_READ_ERROR; + goto out; + } + else if(ncreds_out) { + if(!(conn->scheme->flags & PROTOPT_USERPWDCTRL)) { + /* if the protocol cannot handle control codes in credentials, make + sure there are none */ + if(str_has_ctrl(ncreds_out->user) || + str_has_ctrl(ncreds_out->passwd)) { + failf(data, "control code detected in .netrc credentials"); + result = CURLE_READ_ERROR; goto out; - creds_changed = TRUE; + } } - else - DEBUGASSERT(0); + CURL_TRC_M(data, "netrc: using credentials for %s as %s", + conn->origin->hostname, ncreds_out->user); + result = Curl_creds_merge(ncreds_out->user, ncreds_out->passwd, + data->state.creds, CREDS_NETRC, + &data->state.creds); + if(result) + goto out; + /* for updated strings, we update them in the URL */ + uc = curl_url_set(data->state.uh, CURLUPART_USER, + Curl_creds_user(data->state.creds), CURLU_URLENCODE); + if(!uc) + uc = curl_url_set(data->state.uh, CURLUPART_PASSWORD, + Curl_creds_passwd(data->state.creds), + CURLU_URLENCODE); + if(uc) + result = Curl_uc_to_curlcode(uc); } + else + DEBUGASSERT(0); } - #endif - if(creds_changed) { - /* for updated strings, we update them in the URL */ - uc = curl_url_set(data->state.uh, CURLUPART_USER, - Curl_creds_user(data->state.creds), CURLU_URLENCODE); - if(!uc) - uc = curl_url_set(data->state.uh, CURLUPART_PASSWORD, - Curl_creds_passwd(data->state.creds), CURLU_URLENCODE); - if(uc) - result = Curl_uc_to_curlcode(uc); - } - out: #ifndef CURL_DISABLE_NETRC - Curl_creds_unlink(&ncreds_in); Curl_creds_unlink(&ncreds_out); #endif return result; diff --git a/tests/unit/unit1304.c b/tests/unit/unit1304.c index 099f39dd91..18bc9d4215 100644 --- a/tests/unit/unit1304.c +++ b/tests/unit/unit1304.c @@ -27,21 +27,23 @@ #include "netrc.h" #include "creds.h" -static void t1304_stop(struct Curl_creds **pc1, struct Curl_creds **pc2) +static CURLcode t1304_setup(struct Curl_easy **easy) { - Curl_creds_unlink(pc1); - Curl_creds_unlink(pc2); + CURLcode result = CURLE_OK; + + global_init(CURL_GLOBAL_ALL); + *easy = curl_easy_init(); + if(!*easy) { + curl_global_cleanup(); + return CURLE_OUT_OF_MEMORY; + } + return result; } -static bool t1304_set_creds(const char *user, const char *passwd, - struct Curl_creds **pcreds) +static void t1304_stop(struct Curl_easy *easy) { - Curl_creds_unlink(pcreds); - if(user || passwd) - return !Curl_creds_create(user, passwd, NULL, NULL, NULL, CREDS_NONE, - pcreds); - else - return TRUE; + curl_easy_cleanup(easy); + curl_global_cleanup(); } static bool t1304_no_user(struct Curl_creds *creds) @@ -56,130 +58,105 @@ static bool t1304_no_passwd(struct Curl_creds *creds) static CURLcode test_unit1304(const char *arg) { - struct Curl_creds *cr_out = NULL, *cr_in = NULL; - - UNITTEST_BEGIN_SIMPLE - + struct Curl_creds *cr_out = NULL; + struct Curl_easy *data; int result; struct store_netrc store; + UNITTEST_BEGIN(t1304_setup(&data)) + /* * Test a non existent host in our netrc file. */ Curl_netrc_init(&store); - result = Curl_parsenetrc(&store, "test.example.com", NULL, arg, &cr_out); + result = Curl_netrc_scan( + data, &store, "test.example.com", NULL, arg, &cr_out); fail_unless(result == 1, "expected no match"); - abort_unless(cr_out == NULL, "creds did not return NULL!"); + fail_unless(cr_out == NULL, "creds did not return NULL!"); Curl_netrc_cleanup(&store); /* * Test a non existent login in our netrc file. */ - fail_unless(t1304_set_creds("me", NULL, &cr_in), "err set creds"); Curl_netrc_init(&store); - result = Curl_parsenetrc(&store, "example.com", cr_in, arg, &cr_out); + result = Curl_netrc_scan(data, &store, "example.com", "me", arg, &cr_out); fail_unless(result == 1, "expected no match"); - abort_unless(t1304_no_passwd(cr_out), "password is not NULL!"); + fail_unless(t1304_no_passwd(cr_out), "password is not NULL!"); Curl_netrc_cleanup(&store); /* * Test a non existent login and host in our netrc file. */ - fail_unless(t1304_set_creds("me", NULL, &cr_in), "err set creds"); Curl_netrc_init(&store); - result = Curl_parsenetrc(&store, "test.example.com", cr_in, arg, &cr_out); + result = Curl_netrc_scan( + data, &store, "test.example.com", "me", arg, &cr_out); fail_unless(result == 1, "expected no match"); - abort_unless(t1304_no_passwd(cr_out), "password is not NULL!"); + fail_unless(t1304_no_passwd(cr_out), "password is not NULL!"); Curl_netrc_cleanup(&store); /* * Test a non existent login (substring of an existing one) in our * netrc file. */ - fail_unless(t1304_set_creds( - "admi", NULL, &cr_in), "err set creds"); /* spellchecker:disable-line */ Curl_netrc_init(&store); - result = Curl_parsenetrc(&store, "example.com", cr_in, arg, &cr_out); + result = Curl_netrc_scan( + data, &store, "example.com", "a", arg, &cr_out); fail_unless(result == 1, "expected no match"); - abort_unless(t1304_no_passwd(cr_out), "password is not NULL!"); + fail_unless(t1304_no_passwd(cr_out), "password is not NULL!"); Curl_netrc_cleanup(&store); /* * Test a non existent login (superstring of an existing one) * in our netrc file. */ - fail_unless(t1304_set_creds("adminn", NULL, &cr_in), "err set creds"); Curl_netrc_init(&store); - result = Curl_parsenetrc(&store, "example.com", cr_in, arg, &cr_out); + result = Curl_netrc_scan( + data, &store, "example.com", "administrator", arg, &cr_out); fail_unless(result == 1, "expected no match"); - abort_unless(t1304_no_passwd(cr_out), "password is not NULL!"); + fail_unless(t1304_no_passwd(cr_out), "password is not NULL!"); Curl_netrc_cleanup(&store); /* - * Test for the first existing host in our netrc file - * with login[0] = 0. + * Test for the first existing host in our netrc file with no user */ - Curl_creds_unlink(&cr_in); Curl_netrc_init(&store); - result = Curl_parsenetrc(&store, "example.com", cr_in, arg, &cr_out); + result = Curl_netrc_scan(data, &store, "example.com", NULL, arg, &cr_out); fail_unless(result == 0, "Host should have been found"); - abort_unless(!t1304_no_passwd(cr_out), "returned NULL!"); fail_unless(strncmp(Curl_creds_passwd(cr_out), "passwd", 6) == 0, "password should be 'passwd'"); - abort_unless(!t1304_no_user(cr_out), "returned NULL!"); + fail_unless(!t1304_no_user(cr_out), "returned NULL!"); fail_unless(strncmp(Curl_creds_user(cr_out), "admin", 5) == 0, "login should be 'admin'"); Curl_netrc_cleanup(&store); /* - * Test for the first existing host in our netrc file - * with login[0] != 0. + * Test for the second existing host in our netrc file with no user */ - Curl_creds_unlink(&cr_in); Curl_netrc_init(&store); - result = Curl_parsenetrc(&store, "example.com", cr_in, arg, &cr_out); + result = Curl_netrc_scan( + data, &store, "curl.example.com", NULL, arg, &cr_out); fail_unless(result == 0, "Host should have been found"); - abort_unless(!t1304_no_passwd(cr_out), "returned NULL!"); - fail_unless(strncmp(Curl_creds_passwd(cr_out), "passwd", 6) == 0, - "password should be 'passwd'"); - abort_unless(!t1304_no_user(cr_out), "returned NULL!"); - fail_unless(strncmp(Curl_creds_user(cr_out), "admin", 5) == 0, - "login should be 'admin'"); - Curl_netrc_cleanup(&store); - - /* - * Test for the second existing host in our netrc file - * with login[0] = 0. - */ - Curl_creds_unlink(&cr_in); - Curl_netrc_init(&store); - result = Curl_parsenetrc(&store, "curl.example.com", cr_in, arg, &cr_out); - fail_unless(result == 0, "Host should have been found"); - abort_unless(!t1304_no_passwd(cr_out), "returned NULL!"); fail_unless(strncmp(Curl_creds_passwd(cr_out), "none", 4) == 0, "password should be 'none'"); - abort_unless(!t1304_no_user(cr_out), "returned NULL!"); + fail_unless(!t1304_no_user(cr_out), "returned NULL!"); fail_unless(strncmp(Curl_creds_user(cr_out), "none", 4) == 0, "login should be 'none'"); Curl_netrc_cleanup(&store); /* - * Test for the second existing host in our netrc file - * with login[0] != 0. + * Test for the last host where we do not want to see the password + * if the login does not match. */ - Curl_creds_unlink(&cr_in); Curl_netrc_init(&store); - result = Curl_parsenetrc(&store, "curl.example.com", cr_in, arg, &cr_out); - fail_unless(result == 0, "Host should have been found"); - abort_unless(!t1304_no_passwd(cr_out), "returned NULL!"); - fail_unless(strncmp(Curl_creds_passwd(cr_out), "none", 4) == 0, - "password should be 'none'"); - abort_unless(!t1304_no_user(cr_out), "returned NULL!"); - fail_unless(strncmp(Curl_creds_user(cr_out), "none", 4) == 0, - "login should be 'none'"); + result = Curl_netrc_scan( + data, &store, "curl.example.com", "hilarious", arg, &cr_out); + fail_unless(result == 1, "expect no match"); + fail_unless(!Curl_creds_has_passwd(cr_out), "password must be NULL"); Curl_netrc_cleanup(&store); - UNITTEST_END(t1304_stop(&cr_in, &cr_out)) + Curl_creds_unlink(&cr_out); + + UNITTEST_END(t1304_stop(data)) } #else