netrc: refactor into smaller sub functions

Fixes #20950 - test 685 is extended for this
Closes #20932
This commit is contained in:
Daniel Stenberg 2026-03-09 08:38:14 +01:00
parent a46816b4cb
commit a327a5bded
No known key found for this signature in database
GPG key ID: 5CC908FDB71E12C2
4 changed files with 334 additions and 221 deletions

View file

@ -35,10 +35,18 @@ On Windows two filenames in the home directory are checked: *.netrc* and
*_netrc*, preferring the former. Older versions on Windows checked for
*_netrc* only.
A quick and simple example of how to setup a *.netrc* to allow curl to FTP to
the machine host.example.com with username 'myself' and password 'secret'
A quick and simple example of how to setup a *.netrc* to allow curl to access
the machine host.example.com with username `myself` and password `secret`
could look similar to:
machine host.example.com
login myself
password secret
curl also supports the `default` keyword. This is the same as machine name
except that default matches any name. There can be only one `default` token,
and it must be after all machine tokens.
When providing a username in the URL and a *.netrc* file, curl looks for the
password for that specific user for the given host if such an entry appears in
the file before a "generic" `machine` entry without `login` specified.

View file

@ -50,6 +50,11 @@ and similar things are not supported).
The netrc file provides credentials for a hostname independent of which
protocol and port number that are used.
When providing a username in the URL and a *.netrc* file, libcurl looks for
and uses the password for that specific user for the given host if such an
entry appears in the file before a "generic" `machine` entry without `login`
specified.
libcurl does not verify that the file has the correct properties set (as the
standard Unix ftp client does). It should only be readable by user.

View file

@ -83,6 +83,7 @@ static NETRCcode file2memory(const char *filename, struct dynbuf *filebuf)
ret = NETRC_OK;
do {
const char *line;
/* Curl_get_line always returns lines ending with a newline */
result = Curl_get_line(&linebuf, file, &eof);
if(!result) {
line = curlx_dyn_ptr(&linebuf);
@ -105,81 +106,31 @@ static NETRCcode file2memory(const char *filename, struct dynbuf *filebuf)
return ret;
}
/* bundled parser state to keep function signatures compact */
struct netrc_state {
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;
bool specific_login;
};
/*
* Returns zero on success.
* 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 parsenetrc(struct store_netrc *store,
const char *host,
char **loginp, /* might point to a username */
char **passwordp,
const char *netrcfile)
static NETRCcode netrc_quoted_token(const char **tok_endp,
struct dynbuf *token)
{
NETRCcode retcode = NETRC_NO_MATCH;
char *login = *loginp;
char *password = NULL;
bool specific_login = !!login; /* points to something */
enum host_lookup_state state = NOTHING;
enum found_state keyword = NONE;
unsigned char found = 0; /* login + password found bits, as they can come in
any order */
bool our_login = FALSE; /* found our login name */
bool done = FALSE;
const char *netrcbuffer;
struct dynbuf token;
struct dynbuf *filebuf = &store->filebuf;
DEBUGASSERT(!*passwordp);
curlx_dyn_init(&token, MAX_NETRC_TOKEN);
if(!store->loaded) {
NETRCcode ret = file2memory(netrcfile, filebuf);
if(ret)
return ret;
store->loaded = TRUE;
}
netrcbuffer = curlx_dyn_ptr(filebuf);
while(!done) {
const char *tok = netrcbuffer;
while(tok && !done) {
const char *tok_end;
bool quoted;
curlx_dyn_reset(&token);
curlx_str_passblanks(&tok);
/* tok is first non-space letter */
if(state == MACDEF) {
if((*tok == '\n') || (*tok == '\r'))
state = NOTHING; /* end of macro definition */
}
if(!*tok || (*tok == '\n'))
/* end of line */
break;
/* leading double-quote means quoted string */
quoted = (*tok == '\"');
tok_end = tok;
if(!quoted) {
size_t len = 0;
CURLcode result;
while(*tok_end > ' ') {
tok_end++;
len++;
}
if(!len) {
retcode = NETRC_SYNTAX_ERROR;
goto out;
}
result = curlx_dyn_addn(&token, tok, len);
if(result) {
retcode = curl2netrc(result);
goto out;
}
}
else {
bool escape = FALSE;
bool endquote = FALSE;
NETRCcode rc = NETRC_SYNTAX_ERROR;
const char *tok_end = *tok_endp;
tok_end++; /* pass the leading quote */
while(*tok_end) {
CURLcode result;
@ -205,129 +156,301 @@ static NETRCcode parsenetrc(struct store_netrc *store,
}
else if(s == '\"') {
tok_end++; /* pass the ending quote */
endquote = TRUE;
rc = NETRC_OK;
break;
}
result = curlx_dyn_addn(&token, &s, 1);
result = curlx_dyn_addn(token, &s, 1);
if(result) {
retcode = curl2netrc(result);
goto out;
*tok_endp = tok_end;
return curl2netrc(result);
}
tok_end++;
}
if(escape || !endquote) {
/* bad syntax, get out */
retcode = NETRC_SYNTAX_ERROR;
goto out;
}
*tok_endp = tok_end;
return rc;
}
if(curlx_dyn_len(&token))
tok = curlx_dyn_ptr(&token);
/*
* 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 */
}
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
/* since tok might actually be NULL for no content, set it to blank
to avoid having to deal with it being NULL */
tok = "";
/* set it to blank to avoid NULL */
*tokp = "";
switch(state) {
case NOTHING:
if(curl_strequal("macdef", tok))
/* Define a macro. A macro is defined with the specified name; its
contents begin with the next .netrc line and continue until a
null line (consecutive new-line characters) is encountered. */
state = MACDEF;
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;
Curl_safefree(ns->password);
if(!ns->specific_login)
Curl_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(ns->specific_login)
ns->our_login = !Curl_timestrcmp(ns->login, 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)) {
/* the next tok is the machine name, this is in itself the delimiter
that starts the stuff entered for this machine, after this we
need to search for 'login' and 'password'. */
state = HOSTFOUND;
keyword = NONE;
found = 0;
our_login = FALSE;
Curl_safefree(password);
if(!specific_login)
Curl_safefree(login);
/* a new machine here */
if(ns->found & FOUND_PASSWORD &&
/* a password was provided for this host */
((!ns->specific_login || ns->our_login) ||
/* either there was no specific login to search for, or this
is the specific one we wanted */
(ns->specific_login && !(ns->found & FOUND_LOGIN)))) {
/* or we look for a specific login, but that was not specified */
ns->done = TRUE;
return NETRC_OK;
}
ns->state = HOSTFOUND;
netrc_new_machine(ns);
}
else if(curl_strequal("default", tok)) {
state = HOSTVALID;
retcode = NETRC_OK; /* we did find our host */
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)
state = NOTHING;
ns->state = NOTHING;
break;
case HOSTFOUND:
if(curl_strequal(host, tok)) {
/* and yes, this is our host! */
state = HOSTVALID;
retcode = NETRC_OK; /* we did find our host */
ns->state = HOSTVALID;
ns->retcode = NETRC_OK;
}
else
/* not our host */
state = NOTHING;
ns->state = NOTHING;
break;
case HOSTVALID:
/* we are now parsing sub-keywords concerning "our" host */
if(keyword == LOGIN) {
if(specific_login)
our_login = !Curl_timestrcmp(login, tok);
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,
char **loginp,
char **passwordp,
struct store_netrc *store)
{
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;
}
else if(!ns->login && !ns->password)
/* a default with no credentials */
retcode = NETRC_NO_MATCH;
}
if(!retcode) {
/* success */
if(!ns->specific_login)
*loginp = ns->login;
/* 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. */
*passwordp = ns->password;
}
else {
our_login = TRUE;
curlx_free(login);
login = curlx_strdup(tok);
if(!login) {
retcode = NETRC_OUT_OF_MEMORY; /* allocation failed */
curlx_dyn_free(&store->filebuf);
store->loaded = FALSE;
if(!ns->specific_login)
curlx_free(ns->login);
curlx_free(ns->password);
}
return retcode;
}
/*
* Returns zero on success.
*/
static NETRCcode parsenetrc(struct store_netrc *store,
const char *host,
char **loginp,
char **passwordp,
const char *netrcfile)
{
const char *netrcbuffer;
struct dynbuf token;
struct dynbuf *filebuf = &store->filebuf;
struct netrc_state ns;
memset(&ns, 0, sizeof(ns));
ns.retcode = NETRC_NO_MATCH;
ns.login = *loginp;
ns.specific_login = !!ns.login;
DEBUGASSERT(!*passwordp);
curlx_dyn_init(&token, MAX_NETRC_TOKEN);
if(!store->loaded) {
NETRCcode ret = file2memory(netrcfile, filebuf);
if(ret)
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;
}
}
found |= FOUND_LOGIN;
keyword = NONE;
}
else if(keyword == PASSWORD) {
curlx_free(password);
password = curlx_strdup(tok);
if(!password) {
retcode = NETRC_OUT_OF_MEMORY; /* allocation failed */
if(lineend)
break;
ret = netrc_handle_token(&ns, tok, host);
if(ret) {
ns.retcode = ret;
goto out;
}
if(!specific_login || our_login)
found |= FOUND_PASSWORD;
keyword = NONE;
}
else if(curl_strequal("login", tok))
keyword = LOGIN;
else if(curl_strequal("password", tok))
keyword = PASSWORD;
else if(curl_strequal("machine", tok)) {
/* a new machine here */
if(found & FOUND_PASSWORD) {
done = TRUE;
break;
}
state = HOSTFOUND;
keyword = NONE;
found = 0;
Curl_safefree(password);
if(!specific_login)
Curl_safefree(login);
}
else if(curl_strequal("default", tok)) {
state = HOSTVALID;
retcode = NETRC_OK; /* we did find our host */
Curl_safefree(password);
if(!specific_login)
Curl_safefree(login);
}
if((found == (FOUND_PASSWORD | FOUND_LOGIN)) && our_login) {
done = TRUE;
break;
}
break;
} /* switch (state) */
/* tok_end cannot point to a null byte here since lines are always
newline terminated */
DEBUGASSERT(*tok_end);
tok = ++tok_end;
}
if(!done) {
if(!ns.done) {
const char *nl = NULL;
if(tok)
nl = strchr(tok, '\n');
@ -340,32 +463,7 @@ static NETRCcode parsenetrc(struct store_netrc *store,
out:
curlx_dyn_free(&token);
if(!retcode) {
if(!password && our_login) {
/* success without a password, set a blank one */
password = curlx_strdup("");
if(!password)
retcode = NETRC_OUT_OF_MEMORY; /* out of memory */
}
else if(!login && !password)
/* a default with no credentials */
retcode = NETRC_NO_MATCH;
}
if(!retcode) {
/* success */
if(!specific_login)
*loginp = login;
*passwordp = password;
}
else {
curlx_dyn_free(filebuf);
store->loaded = FALSE;
if(!specific_login)
curlx_free(login);
curlx_free(password);
}
return retcode;
return netrc_finalize(&ns, loginp, passwordp, store);
}
const char *Curl_netrc_strerror(NETRCcode ret)

View file

@ -26,13 +26,15 @@ Connection: close
http
</server>
<name>
netrc with no login - provided user
netrc with no login - but provided user in URL
</name>
<command>
--netrc-optional --netrc-file %LOGDIR/netrc%TESTNUMBER http://user@%HOSTIP:%HTTPPORT/
</command>
<file name="%LOGDIR/netrc%TESTNUMBER" >
machine %HOSTIP password firstthis login unknown
machine %HOSTIP password 5up3r53cr37
machine %HOSTIP password d1ff3r3nt login anotherone
</file>
</client>