]> git.ipfire.org Git - thirdparty/curl.git/commitdiff
netrc: refactor into smaller sub functions
authorDaniel Stenberg <daniel@haxx.se>
Mon, 9 Mar 2026 07:38:14 +0000 (08:38 +0100)
committerDaniel Stenberg <daniel@haxx.se>
Tue, 17 Mar 2026 15:51:38 +0000 (16:51 +0100)
Fixes #20950 - test 685 is extended for this
Closes #20932

docs/cmdline-opts/netrc.md
docs/libcurl/opts/CURLOPT_NETRC.md
lib/netrc.c
tests/data/test685

index 72b5ff935ba974b5fefe82e2ad8a0f3b02536246..03a7ed6e5ded0cd6821109b8dd550922a4d9cb69 100644 (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.
index 65d87cf88158d0561ce05f92ea757fc223dd32f1..1e3dbb1dde361d840a8ccbd0e8356902e351d345 100644 (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.
 
index d88609e35827b2e02b0a7094daf9c84b9865ad76..9326315116264a9db390a357ede01e32962c0e23 100644 (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,28 +106,313 @@ 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;
+};
+
+/*
+ * 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)
+{
+  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 */
+  }
+
+  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;
+  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)) {
+    /* 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)) {
+    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,
+                                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 {
+    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, /* might point to a username */
+                            char **loginp,
                             char **passwordp,
                             const char *netrcfile)
 {
-  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;
+  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);
 
@@ -139,195 +425,32 @@ static NETRCcode parsenetrc(struct store_netrc *store,
 
   netrcbuffer = curlx_dyn_ptr(filebuf);
 
-  while(!done) {
+  while(!ns.done) {
     const char *tok = netrcbuffer;
-    while(tok && !done) {
+    while(tok && !ns.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 */
-      }
+      bool lineend;
+      NETRCcode ret;
 
-      if(!*tok || (*tok == '\n'))
-        /* end of line */
+      ret = netrc_get_token(&tok, &tok_end, &token, &ns.state, &lineend);
+      if(ret) {
+        ns.retcode = ret;
+        goto out;
+      }
+      if(lineend)
         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;
-        }
+      ret = netrc_handle_token(&ns, tok, host);
+      if(ret) {
+        ns.retcode = ret;
+        goto out;
       }
-      else {
-        bool escape = FALSE;
-        bool endquote = FALSE;
-        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 */
-            endquote = TRUE;
-            break;
-          }
-          result = curlx_dyn_addn(&token, &s, 1);
-          if(result) {
-            retcode = curl2netrc(result);
-            goto out;
-          }
-          tok_end++;
-        }
-        if(escape || !endquote) {
-          /* bad syntax, get out */
-          retcode = NETRC_SYNTAX_ERROR;
-          goto out;
-        }
-      }
-
-      if(curlx_dyn_len(&token))
-        tok = 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 = "";
-
-      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;
-        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);
-        }
-        else if(curl_strequal("default", tok)) {
-          state = HOSTVALID;
-          retcode = NETRC_OK; /* we did find our host */
-        }
-        break;
-      case MACDEF:
-        if(!*tok)
-          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 */
-        }
-        else
-          /* not our host */
-          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);
-          else {
-            our_login = TRUE;
-            curlx_free(login);
-            login = curlx_strdup(tok);
-            if(!login) {
-              retcode = NETRC_OUT_OF_MEMORY; /* allocation failed */
-              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 */
-            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)
index a069ff80a190f61326a657c52d3ff6ce5a471d06..5346cd17ef227ab9be38ace29ff4a6890d056618 100644 (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>