netrc: cache the netrc file in memory

So that on redirects etc it does not reread the file but just parses it
again.

Reported-by: Pierre-Etienne Meunier
Fixes #15248
Closes #15259
This commit is contained in:
Daniel Stenberg 2024-10-10 18:08:07 +02:00
parent 962097b8dd
commit 3b43a05e00
No known key found for this signature in database
GPG Key ID: 5CC908FDB71E12C2
6 changed files with 298 additions and 194 deletions

View File

@ -757,6 +757,8 @@ static CURLcode multi_done(struct Curl_easy *data,
mdctx.premature = premature;
Curl_cpool_do_locked(data, data->conn, multi_done_locked, &mdctx);
/* flush the netrc cache */
Curl_netrc_cleanup(&data->state.netrc);
return result;
}

View File

@ -31,7 +31,6 @@
#include <curl/curl.h>
#include "netrc.h"
#include "strtok.h"
#include "strcase.h"
#include "curl_get_line.h"
@ -49,21 +48,56 @@ enum host_lookup_state {
MACDEF
};
enum found_state {
NONE,
LOGIN,
PASSWORD
};
#define NETRC_FILE_MISSING 1
#define NETRC_FAILED -1
#define NETRC_SUCCESS 0
#define MAX_NETRC_LINE 4096
#define MAX_NETRC_FILE (64*1024)
#define MAX_NETRC_TOKEN 128
static CURLcode file2memory(const char *filename, struct dynbuf *filebuf)
{
CURLcode result = CURLE_OK;
FILE *file = fopen(filename, FOPEN_READTEXT);
struct dynbuf linebuf;
Curl_dyn_init(&linebuf, MAX_NETRC_LINE);
if(file) {
while(Curl_get_line(&linebuf, file)) {
const char *line = Curl_dyn_ptr(&linebuf);
/* skip comments on load */
while(ISBLANK(*line))
line++;
if(*line == '#')
continue;
result = Curl_dyn_add(filebuf, line);
if(result)
goto done;
}
}
done:
Curl_dyn_free(&linebuf);
if(file)
fclose(file);
return result;
}
/*
* Returns zero on success.
*/
static int parsenetrc(const char *host,
static int parsenetrc(struct store_netrc *store,
const char *host,
char **loginp,
char **passwordp,
char *netrcfile)
const char *netrcfile)
{
FILE *file;
int retcode = NETRC_FILE_MISSING;
char *login = *loginp;
char *password = *passwordp;
@ -71,204 +105,212 @@ static int parsenetrc(const char *host,
bool login_alloc = FALSE;
bool password_alloc = FALSE;
enum host_lookup_state state = NOTHING;
enum found_state found = NONE;
bool our_login = TRUE; /* With specific_login, found *our* login name (or
login-less line) */
bool done = FALSE;
char *netrcbuffer;
struct dynbuf token;
struct dynbuf *filebuf = &store->filebuf;
Curl_dyn_init(&token, MAX_NETRC_TOKEN);
char state_login = 0; /* Found a login keyword */
char state_password = 0; /* Found a password keyword */
bool state_our_login = TRUE; /* With specific_login, found *our* login
name (or login-less line) */
if(!store->loaded) {
if(file2memory(netrcfile, filebuf))
return NETRC_FAILED;
store->loaded = TRUE;
}
DEBUGASSERT(netrcfile);
netrcbuffer = Curl_dyn_ptr(filebuf);
file = fopen(netrcfile, FOPEN_READTEXT);
if(file) {
bool done = FALSE;
struct dynbuf buf;
Curl_dyn_init(&buf, MAX_NETRC_LINE);
while(!done && Curl_get_line(&buf, file)) {
char *tok;
while(!done) {
char *tok = netrcbuffer;
while(tok) {
char *tok_end;
bool quoted;
char *netrcbuffer = Curl_dyn_ptr(&buf);
Curl_dyn_reset(&token);
while(ISBLANK(*tok))
tok++;
/* tok is first non-space letter */
if(state == MACDEF) {
if((netrcbuffer[0] == '\n') || (netrcbuffer[0] == '\r'))
state = NOTHING;
else
continue;
if((*tok == '\n') || (*tok == '\r'))
state = NOTHING; /* end of macro definition */
}
tok = netrcbuffer;
while(tok) {
while(ISBLANK(*tok))
tok++;
/* tok is first non-space letter */
if(!*tok || (*tok == '#'))
/* end of line or the rest is a comment */
break;
/* leading double-quote means quoted string */
quoted = (*tok == '\"');
if(!*tok || (*tok == '\n'))
/* end of line */
break;
tok_end = tok;
if(!quoted) {
while(!ISSPACE(*tok_end))
tok_end++;
*tok_end = 0;
/* leading double-quote means quoted string */
quoted = (*tok == '\"');
tok_end = tok;
if(!quoted) {
size_t len = 0;
while(!ISSPACE(*tok_end)) {
tok_end++;
len++;
}
else {
bool escape = FALSE;
bool endquote = FALSE;
char *store = tok;
tok_end++; /* pass the leading quote */
while(*tok_end) {
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 */
endquote = TRUE;
if(!len || Curl_dyn_addn(&token, tok, len)) {
retcode = NETRC_FAILED;
goto out;
}
}
else {
bool escape = FALSE;
bool endquote = FALSE;
tok_end++; /* pass the leading quote */
while(*tok_end) {
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;
}
*store++ = s;
tok_end++;
}
*store = 0;
if(escape || !endquote) {
/* bad syntax, get out */
else if(s == '\\') {
escape = TRUE;
tok_end++;
continue;
}
else if(s == '\"') {
tok_end++; /* pass the ending quote */
endquote = TRUE;
break;
}
if(Curl_dyn_addn(&token, &s, 1)) {
retcode = NETRC_FAILED;
goto out;
}
tok_end++;
}
if((login && *login) && (password && *password)) {
done = TRUE;
break;
if(escape || !endquote) {
/* bad syntax, get out */
retcode = NETRC_FAILED;
goto out;
}
switch(state) {
case NOTHING:
if(strcasecompare("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;
}
else if(strcasecompare("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;
}
else if(strcasecompare("default", tok)) {
state = HOSTVALID;
retcode = NETRC_SUCCESS; /* we did find our host */
}
break;
case MACDEF:
if(!strlen(tok)) {
state = NOTHING;
}
break;
case HOSTFOUND:
if(strcasecompare(host, tok)) {
/* and yes, this is our host! */
state = HOSTVALID;
retcode = NETRC_SUCCESS; /* we did find our host */
}
else
/* not our host */
state = NOTHING;
break;
case HOSTVALID:
/* we are now parsing sub-keywords concerning "our" host */
if(state_login) {
if(specific_login) {
state_our_login = !Curl_timestrcmp(login, tok);
}
else if(!login || Curl_timestrcmp(login, tok)) {
if(login_alloc) {
free(login);
login_alloc = FALSE;
}
login = strdup(tok);
if(!login) {
retcode = NETRC_FAILED; /* allocation failed */
goto out;
}
login_alloc = TRUE;
}
state_login = 0;
}
else if(state_password) {
if((state_our_login || !specific_login)
&& (!password || Curl_timestrcmp(password, tok))) {
if(password_alloc) {
free(password);
password_alloc = FALSE;
}
password = strdup(tok);
if(!password) {
retcode = NETRC_FAILED; /* allocation failed */
goto out;
}
password_alloc = TRUE;
}
state_password = 0;
}
else if(strcasecompare("login", tok))
state_login = 1;
else if(strcasecompare("password", tok))
state_password = 1;
else if(strcasecompare("machine", tok)) {
/* ok, there is machine here go => */
state = HOSTFOUND;
state_our_login = FALSE;
}
break;
} /* switch (state) */
tok = ++tok_end;
}
} /* while Curl_get_line() */
if((login && *login) && (password && *password)) {
done = TRUE;
break;
}
tok = Curl_dyn_ptr(&token);
switch(state) {
case NOTHING:
if(strcasecompare("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;
else if(strcasecompare("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;
else if(strcasecompare("default", tok)) {
state = HOSTVALID;
retcode = NETRC_SUCCESS; /* we did find our host */
}
break;
case MACDEF:
if(!*tok)
state = NOTHING;
break;
case HOSTFOUND:
if(strcasecompare(host, tok)) {
/* and yes, this is our host! */
state = HOSTVALID;
retcode = NETRC_SUCCESS; /* we did find our host */
}
else
/* not our host */
state = NOTHING;
break;
case HOSTVALID:
/* we are now parsing sub-keywords concerning "our" host */
if(found == LOGIN) {
if(specific_login) {
our_login = !Curl_timestrcmp(login, tok);
}
else if(!login || Curl_timestrcmp(login, tok)) {
if(login_alloc)
free(login);
login = strdup(tok);
if(!login) {
retcode = NETRC_FAILED; /* allocation failed */
goto out;
}
login_alloc = TRUE;
}
found = NONE;
}
else if(found == PASSWORD) {
if((our_login || !specific_login) &&
(!password || Curl_timestrcmp(password, tok))) {
if(password_alloc)
free(password);
password = strdup(tok);
if(!password) {
retcode = NETRC_FAILED; /* allocation failed */
goto out;
}
password_alloc = TRUE;
}
found = NONE;
}
else if(strcasecompare("login", tok))
found = LOGIN;
else if(strcasecompare("password", tok))
found = PASSWORD;
else if(strcasecompare("machine", tok)) {
/* ok, there is machine here go => */
state = HOSTFOUND;
found = NONE;
}
break;
} /* switch (state) */
tok = ++tok_end;
}
if(!done) {
char *nl = NULL;
if(tok)
nl = strchr(tok, '\n');
if(!nl)
break;
/* point to next line */
netrcbuffer = &nl[1];
}
} /* while !done */
out:
Curl_dyn_free(&buf);
if(!retcode) {
/* success */
if(login_alloc) {
if(*loginp)
free(*loginp);
*loginp = login;
}
if(password_alloc) {
if(*passwordp)
free(*passwordp);
*passwordp = password;
}
Curl_dyn_free(&token);
if(!retcode) {
/* success */
if(login_alloc) {
free(*loginp);
*loginp = login;
}
else {
if(login_alloc)
free(login);
if(password_alloc)
free(password);
if(password_alloc) {
free(*passwordp);
*passwordp = password;
}
fclose(file);
}
else {
Curl_dyn_free(filebuf);
if(login_alloc)
free(login);
if(password_alloc)
free(password);
}
return retcode;
@ -280,7 +322,8 @@ out:
* *loginp and *passwordp MUST be allocated if they are not NULL when passed
* in.
*/
int Curl_parsenetrc(const char *host, char **loginp, char **passwordp,
int Curl_parsenetrc(struct store_netrc *store, const char *host,
char **loginp, char **passwordp,
char *netrcfile)
{
int retcode = 1;
@ -329,7 +372,7 @@ int Curl_parsenetrc(const char *host, char **loginp, char **passwordp,
free(homea);
return -1;
}
retcode = parsenetrc(host, loginp, passwordp, filealloc);
retcode = parsenetrc(store, host, loginp, passwordp, filealloc);
free(filealloc);
#ifdef _WIN32
if(retcode == NETRC_FILE_MISSING) {
@ -339,15 +382,25 @@ int Curl_parsenetrc(const char *host, char **loginp, char **passwordp,
free(homea);
return -1;
}
retcode = parsenetrc(host, loginp, passwordp, filealloc);
retcode = parsenetrc(store, host, loginp, passwordp, filealloc);
free(filealloc);
}
#endif
free(homea);
}
else
retcode = parsenetrc(host, loginp, passwordp, netrcfile);
retcode = parsenetrc(store, host, loginp, passwordp, netrcfile);
return retcode;
}
void Curl_netrc_init(struct store_netrc *s)
{
Curl_dyn_init(&s->filebuf, MAX_NETRC_FILE);
s->loaded = FALSE;
}
void Curl_netrc_cleanup(struct store_netrc *s)
{
Curl_dyn_free(&s->filebuf);
s->loaded = FALSE;
}
#endif

View File

@ -26,9 +26,19 @@
#include "curl_setup.h"
#ifndef CURL_DISABLE_NETRC
#include "dynbuf.h"
struct store_netrc {
struct dynbuf filebuf;
char *filename;
BIT(loaded);
};
void Curl_netrc_init(struct store_netrc *s);
void Curl_netrc_cleanup(struct store_netrc *s);
/* returns -1 on failure, 0 if the host is found, 1 is the host is not found */
int Curl_parsenetrc(const char *host, char **loginp,
int Curl_parsenetrc(struct store_netrc *s, const char *host, char **loginp,
char **passwordp, char *filename);
/* Assume: (*passwordp)[0]=0, host[0] != 0.
* If (*loginp)[0] = 0, search for login and password within a machine
@ -38,6 +48,8 @@ int Curl_parsenetrc(const char *host, char **loginp,
#else
/* disabled */
#define Curl_parsenetrc(a,b,c,d,e,f) 1
#define Curl_netrc_init(x)
#define Curl_netrc_cleanup(x)
#endif
#endif /* HEADER_CURL_NETRC_H */

View File

@ -338,6 +338,7 @@ CURLcode Curl_close(struct Curl_easy **datap)
Curl_wildcard_dtor(&data->wildcard);
Curl_freeset(data);
Curl_headers_cleanup(data);
Curl_netrc_cleanup(&data->state.netrc);
free(data);
return CURLE_OK;
}
@ -545,6 +546,7 @@ CURLcode Curl_open(struct Curl_easy **curl)
#ifndef CURL_DISABLE_HTTP
Curl_llist_init(&data->state.httphdrs, NULL);
#endif
Curl_netrc_init(&data->state.netrc);
}
if(result) {
@ -2689,7 +2691,7 @@ static CURLcode override_login(struct Curl_easy *data,
url_provided = TRUE;
}
ret = Curl_parsenetrc(conn->host.name,
ret = Curl_parsenetrc(&data->state.netrc, conn->host.name,
userp, passwdp,
data->set.str[STRING_NETRC_FILE]);
if(ret > 0) {

View File

@ -163,6 +163,7 @@ typedef unsigned int curl_prot_t;
#include "dynbuf.h"
#include "dynhds.h"
#include "request.h"
#include "netrc.h"
/* return the count of bytes sent, or -1 on error */
typedef ssize_t (Curl_send)(struct Curl_easy *data, /* transfer */
@ -1313,6 +1314,10 @@ struct UrlState {
struct curl_trc_feat *feat; /* opt. trace feature transfer is part of */
#endif
#ifndef CURL_DISABLE_NETRC
struct store_netrc netrc;
#endif
/* Dynamically allocated strings, MUST be freed before this struct is
killed. */
struct dynamically_allocated_data {

View File

@ -49,17 +49,22 @@ static void unit_stop(void)
}
UNITTEST_START
{
int result;
struct store_netrc store;
/*
* Test a non existent host in our netrc file.
*/
result = Curl_parsenetrc("test.example.com", &s_login, &s_password, arg);
Curl_netrc_init(&store);
result = Curl_parsenetrc(&store,
"test.example.com", &s_login, &s_password, arg);
fail_unless(result == 1, "Host not found should return 1");
abort_unless(s_password != NULL, "returned NULL!");
fail_unless(s_password[0] == 0, "password should not have been changed");
abort_unless(s_login != NULL, "returned NULL!");
fail_unless(s_login[0] == 0, "login should not have been changed");
Curl_netrc_cleanup(&store);
/*
* Test a non existent login in our netrc file.
@ -67,13 +72,16 @@ UNITTEST_START
free(s_login);
s_login = strdup("me");
abort_unless(s_login != NULL, "returned NULL!");
result = Curl_parsenetrc("example.com", &s_login, &s_password, arg);
Curl_netrc_init(&store);
result = Curl_parsenetrc(&store,
"example.com", &s_login, &s_password, arg);
fail_unless(result == 0, "Host should have been found");
abort_unless(s_password != NULL, "returned NULL!");
fail_unless(s_password[0] == 0, "password should not have been changed");
abort_unless(s_login != NULL, "returned NULL!");
fail_unless(strncmp(s_login, "me", 2) == 0,
"login should not have been changed");
Curl_netrc_cleanup(&store);
/*
* Test a non existent login and host in our netrc file.
@ -81,13 +89,16 @@ UNITTEST_START
free(s_login);
s_login = strdup("me");
abort_unless(s_login != NULL, "returned NULL!");
result = Curl_parsenetrc("test.example.com", &s_login, &s_password, arg);
Curl_netrc_init(&store);
result = Curl_parsenetrc(&store,
"test.example.com", &s_login, &s_password, arg);
fail_unless(result == 1, "Host not found should return 1");
abort_unless(s_password != NULL, "returned NULL!");
fail_unless(s_password[0] == 0, "password should not have been changed");
abort_unless(s_login != NULL, "returned NULL!");
fail_unless(strncmp(s_login, "me", 2) == 0,
"login should not have been changed");
Curl_netrc_cleanup(&store);
/*
* Test a non existent login (substring of an existing one) in our
@ -96,13 +107,16 @@ UNITTEST_START
free(s_login);
s_login = strdup("admi");
abort_unless(s_login != NULL, "returned NULL!");
result = Curl_parsenetrc("example.com", &s_login, &s_password, arg);
Curl_netrc_init(&store);
result = Curl_parsenetrc(&store,
"example.com", &s_login, &s_password, arg);
fail_unless(result == 0, "Host should have been found");
abort_unless(s_password != NULL, "returned NULL!");
fail_unless(s_password[0] == 0, "password should not have been changed");
abort_unless(s_login != NULL, "returned NULL!");
fail_unless(strncmp(s_login, "admi", 4) == 0,
"login should not have been changed");
Curl_netrc_cleanup(&store);
/*
* Test a non existent login (superstring of an existing one)
@ -111,13 +125,16 @@ UNITTEST_START
free(s_login);
s_login = strdup("adminn");
abort_unless(s_login != NULL, "returned NULL!");
result = Curl_parsenetrc("example.com", &s_login, &s_password, arg);
Curl_netrc_init(&store);
result = Curl_parsenetrc(&store,
"example.com", &s_login, &s_password, arg);
fail_unless(result == 0, "Host should have been found");
abort_unless(s_password != NULL, "returned NULL!");
fail_unless(s_password[0] == 0, "password should not have been changed");
abort_unless(s_login != NULL, "returned NULL!");
fail_unless(strncmp(s_login, "adminn", 6) == 0,
"login should not have been changed");
Curl_netrc_cleanup(&store);
/*
* Test for the first existing host in our netrc file
@ -126,13 +143,16 @@ UNITTEST_START
free(s_login);
s_login = strdup("");
abort_unless(s_login != NULL, "returned NULL!");
result = Curl_parsenetrc("example.com", &s_login, &s_password, arg);
Curl_netrc_init(&store);
result = Curl_parsenetrc(&store,
"example.com", &s_login, &s_password, arg);
fail_unless(result == 0, "Host should have been found");
abort_unless(s_password != NULL, "returned NULL!");
fail_unless(strncmp(s_password, "passwd", 6) == 0,
"password should be 'passwd'");
abort_unless(s_login != NULL, "returned NULL!");
fail_unless(strncmp(s_login, "admin", 5) == 0, "login should be 'admin'");
Curl_netrc_cleanup(&store);
/*
* Test for the first existing host in our netrc file
@ -141,13 +161,16 @@ UNITTEST_START
free(s_password);
s_password = strdup("");
abort_unless(s_password != NULL, "returned NULL!");
result = Curl_parsenetrc("example.com", &s_login, &s_password, arg);
Curl_netrc_init(&store);
result = Curl_parsenetrc(&store,
"example.com", &s_login, &s_password, arg);
fail_unless(result == 0, "Host should have been found");
abort_unless(s_password != NULL, "returned NULL!");
fail_unless(strncmp(s_password, "passwd", 6) == 0,
"password should be 'passwd'");
abort_unless(s_login != NULL, "returned NULL!");
fail_unless(strncmp(s_login, "admin", 5) == 0, "login should be 'admin'");
Curl_netrc_cleanup(&store);
/*
* Test for the second existing host in our netrc file
@ -159,13 +182,16 @@ UNITTEST_START
free(s_login);
s_login = strdup("");
abort_unless(s_login != NULL, "returned NULL!");
result = Curl_parsenetrc("curl.example.com", &s_login, &s_password, arg);
Curl_netrc_init(&store);
result = Curl_parsenetrc(&store,
"curl.example.com", &s_login, &s_password, arg);
fail_unless(result == 0, "Host should have been found");
abort_unless(s_password != NULL, "returned NULL!");
fail_unless(strncmp(s_password, "none", 4) == 0,
"password should be 'none'");
abort_unless(s_login != NULL, "returned NULL!");
fail_unless(strncmp(s_login, "none", 4) == 0, "login should be 'none'");
Curl_netrc_cleanup(&store);
/*
* Test for the second existing host in our netrc file
@ -174,14 +200,18 @@ UNITTEST_START
free(s_password);
s_password = strdup("");
abort_unless(s_password != NULL, "returned NULL!");
result = Curl_parsenetrc("curl.example.com", &s_login, &s_password, arg);
Curl_netrc_init(&store);
result = Curl_parsenetrc(&store,
"curl.example.com", &s_login, &s_password, arg);
fail_unless(result == 0, "Host should have been found");
abort_unless(s_password != NULL, "returned NULL!");
fail_unless(strncmp(s_password, "none", 4) == 0,
"password should be 'none'");
abort_unless(s_login != NULL, "returned NULL!");
fail_unless(strncmp(s_login, "none", 4) == 0, "login should be 'none'");
Curl_netrc_cleanup(&store);
}
UNITTEST_STOP
#else