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
This commit is contained in:
Stefan Eissing 2026-05-15 11:45:49 +02:00 committed by Daniel Stenberg
parent 5c1e017987
commit 4ae1d7cc26
No known key found for this signature in database
GPG key ID: 5CC908FDB71E12C2
4 changed files with 616 additions and 563 deletions

View file

@ -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 */

View file

@ -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)

153
lib/url.c
View file

@ -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;

View file

@ -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