Hi Steffen,

On 11/27/20 05:59 AM, Steffen Nurpmeso wrote:
> Nick Gasson wrote in
>  <87im9srza8....@bertha.nickg.me.uk>:
>  |Hi,
>  |
>  |I often need to go through a SOCKS proxy to access certain sites. The
>  |diff below adds SOCKS5 support to ftp(1) for HTTP transfers, similar to
>  |curl(1). Enabled when http_proxy is set to a socks5:// URL.
>  |
>  |Also fixes two existing memory leaks: proxyurl (set to NULL on line 646
>  |before freeing) and sslpath (never freed).
>  |
>  |Tested with ssh -D and a few other SOCKS5 proxies. Also verified the
>  |existing HTTP proxy feature still works with squid(8).
>
> By the way, the $SOCKS5_PROXY environment variable becomes used
> for automatic selection of SOCKS5.  (Some things on FreeBSD,
> lynx(1), and, hm, the MUA i maintain, s-nail; maybe more.)
>

(Sorry for the late reply.)

Yes I see FreeBSD fetch added SOCKS5_PROXY recently. I've updated the
diff below to support that too. Anyone interested?

--
Thanks,
Nick


Index: usr.bin/ftp/fetch.c
===================================================================
RCS file: /cvs/src/usr.bin/ftp/fetch.c,v
retrieving revision 1.199
diff -u -p -u -r1.199 fetch.c
--- usr.bin/ftp/fetch.c 1 Jan 2021 17:39:54 -0000       1.199
+++ usr.bin/ftp/fetch.c 2 Jan 2021 12:02:39 -0000
@@ -88,13 +88,18 @@ static int  proxy_connect(int, char *, ch
 static int     stdio_tls_write_wrapper(void *, const char *, int);
 static int     stdio_tls_read_wrapper(void *, char *, int);
 #endif /* !NOSSL */
+static int     read_fully(int, void *, size_t);
+static int     write_fully(int, const void *, size_t);
+static int     socks5_connect(int, const char *, const char *);
 
 #define        FTP_URL         "ftp://";        /* ftp URL prefix */
 #define        HTTP_URL        "http://";       /* http URL prefix */
 #define        HTTPS_URL       "https://";      /* https URL prefix */
+#define        SOCKS5_URL      "socks5://"     /* socks5 URL prefix */
 #define        FILE_URL        "file:"         /* file URL prefix */
 #define FTP_PROXY      "ftp_proxy"     /* env var with ftp proxy location */
 #define HTTP_PROXY     "http_proxy"    /* env var with http proxy location */
+#define SOCKS5_PROXY   "SOCKS5_PROXY"  /* env var with socks5 proxy location */
 
 #define EMPTYSTRING(x) ((x) == NULL || (*(x) == '\0'))
 
@@ -345,6 +350,7 @@ url_get(const char *origline, const char
        int save_errno;
        const size_t buflen = 128 * 1024;
        int chunked = 0;
+       enum proxy_scheme proxy = PROXY_NONE;
 
        direction = "received";
 
@@ -455,11 +461,16 @@ noslash:
                proxyurl = strdup(proxyenv);
                if (proxyurl == NULL)
                        errx(1, "Can't allocate memory for proxy URL.");
-               if (strncasecmp(proxyurl, HTTP_URL, sizeof(HTTP_URL) - 1) == 0)
+               if (strncasecmp(proxyurl, HTTP_URL, sizeof(HTTP_URL) - 1) == 0) 
{
                        host = proxyurl + sizeof(HTTP_URL) - 1;
-               else if (strncasecmp(proxyurl, FTP_URL, sizeof(FTP_URL) - 1) == 
0)
+                       proxy = PROXY_HTTP;
+               } else if (strncasecmp(proxyurl, FTP_URL, sizeof(FTP_URL) - 1) 
== 0) {
                        host = proxyurl + sizeof(FTP_URL) - 1;
-               else {
+                       proxy = PROXY_HTTP;  /* Treat ftp:// as a HTTP proxy */
+               } else if (strncasecmp(proxyurl, SOCKS5_URL, sizeof(SOCKS5_URL) 
- 1) == 0) {
+                       host = proxyurl + sizeof(SOCKS5_URL) - 1;
+                       proxy = PROXY_SOCKS5;
+               } else {
                        warnx("Malformed proxy URL: %s", proxyenv);
                        goto cleanup_url_get;
                }
@@ -467,11 +478,14 @@ noslash:
                        warnx("Malformed proxy URL: %s", proxyenv);
                        goto cleanup_url_get;
                }
+       }
+
+       if (proxy == PROXY_HTTP) {
                if (*--path == '\0')
                        *path = '/';            /* add / back to real path */
                path = strchr(host, '/');       /* remove trailing / on host */
                if (!EMPTYSTRING(path))
-                       *path++ = '\0';         /* i guess this ++ is useless */
+                       *path = '\0';
 
                path = strchr(host, '@');       /* look for credentials in 
proxy */
                if (!EMPTYSTRING(path)) {
@@ -623,9 +637,25 @@ noslash:
                        port = NULL;
 
 #ifndef NOSSL
-               if (proxyenv && sslhost)
+               if (proxy == PROXY_HTTP && sslhost)
                        proxy_connect(fd, sslhost, proxy_credentials);
 #endif /* !NOSSL */
+
+               if (proxy == PROXY_SOCKS5) {
+                       portnum = strrchr(proxyhost, ':');
+                       if (portnum != NULL)
+                               *portnum++ = '\0';
+                       else
+                               portnum = ishttpsurl ? httpsport : httpport;
+
+                       if (socks5_connect(fd, proxyhost, portnum) != 0) {
+                               close(fd);
+                               errno = EINVAL;
+                               fd = -1;
+                               cause = "socks5";
+                       }
+               }
+
                break;
        }
        freeaddrinfo(res0);
@@ -641,9 +671,10 @@ noslash:
 #ifndef NOSSL
        if (ishttpsurl) {
                ssize_t ret;
-               if (proxyenv && sslpath) {
+               if (proxy == PROXY_HTTP && sslpath) {
+                       /* HTTP proxy CONNECT handled above. */
                        ishttpsurl = 0;
-                       proxyurl = NULL;
+                       proxy = PROXY_NONE;
                        path = sslpath;
                }
                if (sslhost == NULL) {
@@ -707,7 +738,7 @@ noslash:
 #endif /* !NOSSL */
 
        epath = url_encode(path);
-       if (proxyurl) {
+       if (proxy == PROXY_HTTP) {
                if (verbose) {
                        fprintf(ttyout, "Requesting %s (via %s)\n",
                            origline, proxyurl);
@@ -1112,6 +1143,7 @@ cleanup_url_get:
 #endif /* !SMALL */
 #ifndef NOSSL
        free(sslhost);
+       free(sslpath);
 #endif /* !NOSSL */
        ftp_close(&fin, &tls, &fd);
        if (out >= 0 && out != fileno(stdout))
@@ -1225,7 +1257,8 @@ auto_fetch(int argc, char *argv[], char 
        char *xargv[5];
        char *cp, *url, *host, *dir, *file, *portnum;
        char *username, *pass, *pathstart;
-       char *ftpproxy, *httpproxy;
+       char *ftpproxy, *httpproxy, *socks5env;
+       char *socks5proxy = NULL;
        int rval, xargc, lastfile;
        volatile int argpos;
        int dirhasglob, filehasglob, oautologin;
@@ -1245,6 +1278,20 @@ auto_fetch(int argc, char *argv[], char 
                ftpproxy = NULL;
        if ((httpproxy = getenv(HTTP_PROXY)) != NULL && *httpproxy == '\0')
                httpproxy = NULL;
+       if ((socks5env = getenv(SOCKS5_PROXY)) != NULL && *socks5env == '\0')
+               socks5env = NULL;
+
+       /*
+        * If the SOCKS5_PROXY environment variable is set then this is
+        * used as the proxy for all http:// and ftp:// requests.
+        */
+       if (socks5env != NULL) {
+               if (asprintf(&socks5proxy, "socks5://%s%s", socks5env,
+                            strchr(socks5env, ':') ? "" : ":1080") == -1)
+                       errx(1, "Cannot allocate memory for proxy URL");
+               httpproxy = socks5proxy;
+               ftpproxy = socks5proxy;
+       }
 
        /*
         * Loop through as long as there's files to fetch.
@@ -1523,6 +1570,7 @@ bad_ftp_url:
        }
        if (connected && rval != -1)
                disconnect(0, NULL);
+       free(socks5proxy);
        return (rval);
 }
 
@@ -1682,6 +1730,161 @@ sockerror(struct tls *tls)
        }
 #endif
        return strerror(save_errno);
+}
+
+static int
+read_fully(int fd, void *buf, size_t len)
+{
+       ssize_t  count;
+       char    *p = buf;
+
+       while ((count = read(fd, p, len)) > 0) {
+               len -= count;
+               p += count;
+       }
+
+       return len == 0;
+}
+
+static int
+write_fully(int fd, const void *buf, size_t len)
+{
+       ssize_t          count;
+       const char      *p = buf;
+
+       while ((count = write(fd, p, len)) > 0) {
+               len -= count;
+               p += count;
+       }
+
+       return len == 0;
+}
+
+/*
+ * Perform SOCKS5 handshake on a connected socket. Returns zero on
+ * success after which all subsequent data on the socket will be
+ * tunnelled to the target host. Does not support authentication. See
+ * RFC1928 for details.
+ */
+static int
+socks5_connect(int socket, const char *host, const char *port)
+{
+       uint8_t          packetbuf[NI_MAXHOST + 7];
+       size_t           bindlen   = 0;
+       size_t           domainlen = 0;
+       struct servent  *servent   = NULL;
+       uint16_t         portnum   = 0;
+       const char      *errstr    = NULL;
+
+       domainlen = strlen(host);
+       if (domainlen >= NI_MAXHOST) {
+               warnx("Host name too long");
+               return -1;
+       }
+
+       servent = getservbyname(port, "tcp");
+       if (servent != NULL)
+               portnum = servent->s_port;
+       else {
+               portnum = htons(strtonum(port, 1, UINT16_MAX, &errstr));
+               if (errstr != NULL) {
+                       warnx("Parsing port number: %s: %s", port, errstr);
+                       return -1;
+               }
+       }
+
+       /*
+        * 1. Send client hello with no authentication methods.
+        */
+
+       packetbuf[0] = 5;               /* version */
+       packetbuf[1] = 1;               /* number of auth methods */
+       packetbuf[2] = SOCKS_AUTH_NONE; /* auth method */
+
+       if (!write_fully(socket, packetbuf, 3)) {
+               warn("Error starting SOCKS5 handshake");
+               return -1;
+       }
+
+       /*
+        * 2. Wait for server hello. Check expected version and
+        * authentication method.
+        */
+
+       if (!read_fully(socket, packetbuf, 2)) {
+               warn("Error reading SOCKS5 handshake reply");
+               return -1;
+       }
+
+       if (packetbuf[0] /* version */ != 5) {
+               warnx("Bad version %d in reply from SOCKS5 server",
+                   packetbuf[0]);
+               return -1;
+       } else if (packetbuf[1] /* auth method */ != SOCKS_AUTH_NONE) {
+               warnx("SOCKS5 server chose unsupported authentication method");
+               return -1;
+       }
+
+       /*
+        * 3. Send the connect request. The target host name is passed
+        * as a string and the DNS lookup will happen on the proxy
+        * server side.
+        */
+
+       packetbuf[0] = 5;                       /* version */
+       packetbuf[1] = SOCKS_CMD_CONNECT;       /* command */
+       packetbuf[2] = 0;                       /* reserved */
+       packetbuf[3] = SOCKS_ATYPE_DOMAIN;      /* address type */
+       packetbuf[4] = domainlen;               /* name length */
+       memcpy(packetbuf + 5, host, domainlen);
+       packetbuf[domainlen + 5] = portnum & 0xff;
+       packetbuf[domainlen + 6] = portnum >> 8;
+
+       if (!write_fully(socket, packetbuf, domainlen + 7)) {
+               warn("Error sending SOCKS5 connect request");
+               return -1;
+       }
+
+       /*
+        * 4. Wait for the server reply and check the response code.
+        * Read and discard the bind address and port number that
+        * follows the reply header: this is usually a dummy value like
+        * 0.0.0.0 and not reliable.
+        */
+
+       if (!read_fully(socket, packetbuf, 4)) {
+               warn("Error reading SOCKS5 connect reply");
+               return -1;
+       }
+
+       if (packetbuf[0] /* version */ != 5) {
+               warnx("Bad version %d in reply from SOCKS5 server",
+                   packetbuf[0]);
+               return -1;
+       }
+
+       if (packetbuf[1] /* response code */ != SOCKS_REP_SUCCEEDED) {
+               warnx("SOCKS5 request failed with status %d", packetbuf[1]);
+               return -1;
+       }
+
+       switch (packetbuf[3] /* address type */) {
+       case SOCKS_ATYPE_IPV4: bindlen = 4; break;
+       case SOCKS_ATYPE_IPV6: bindlen = 16; break;
+       default:
+               warnx("Unexpected SOCKS5 address type %d", packetbuf[3]);
+               return -1;
+       }
+
+       if (!read_fully(socket, packetbuf + 4, bindlen + 2)) {
+               warn("Error reading SOCKS5 bind address");
+               return -1;
+       }
+
+       if (verbose)
+               fprintf(ttyout, "Connected to %s via SOCKS5 proxy\n", host);
+
+       return 0;
 }
 
 #ifndef NOSSL
Index: usr.bin/ftp/ftp.1
===================================================================
RCS file: /cvs/src/usr.bin/ftp/ftp.1,v
retrieving revision 1.121
diff -u -p -u -r1.121 ftp.1
--- usr.bin/ftp/ftp.1   6 Sep 2020 09:15:04 -0000       1.121
+++ usr.bin/ftp/ftp.1   2 Jan 2021 12:02:40 -0000
@@ -1371,7 +1371,7 @@ with a password of
 An HTTP URL, retrieved using the HTTP protocol.
 If
 .Ev http_proxy
-is defined, it is used as a URL to an HTTP proxy server.
+is defined, it is used as a URL to an HTTP or SOCKS5 proxy server.
 If a
 .Ar user
 and
@@ -1394,8 +1394,8 @@ using Basic authentication.
 An HTTPS URL, retrieved using the HTTPS protocol.
 If
 .Ev http_proxy
-is defined, this HTTPS proxy server will be used to fetch the
-file using the CONNECT method.
+is defined, it is used as a URL to an HTTP or SOCKS5 proxy server.
+If using an HTTP proxy server, it must support the CONNECT method.
 If a
 .Ar user
 and
@@ -1753,7 +1753,17 @@ For default shell.
 URL of FTP proxy to use when making FTP URL requests
 (if not defined, use the standard FTP protocol).
 .It Ev http_proxy
-URL of HTTP proxy to use when making HTTP or HTTPS URL requests.
+URL of HTTP (http://) or SOCKS5 (socks5://) proxy to use when making
+HTTP or HTTPS URL requests.
+.It Ev SOCKS5_PROXY
+Hostname or IP address of a SOCKS5 proxy to use when making HTTP or HTTPS
+URL requests.  An optional port can be specified separated with a colon.
+If no port number is provided the default 1080 is used.  This overrides
+the value of
+.Ev http_proxy
+and
+.Ev ftp_proxy
+if set.
 .It Ev http_cookies
 Path of a Netscape-like cookiejar file to use when making
 HTTP or HTTPS URL requests.
Index: usr.bin/ftp/ftp_var.h
===================================================================
RCS file: /cvs/src/usr.bin/ftp/ftp_var.h,v
retrieving revision 1.45
diff -u -p -u -r1.45 ftp_var.h
--- usr.bin/ftp/ftp_var.h       1 Sep 2020 12:33:48 -0000       1.45
+++ usr.bin/ftp/ftp_var.h       2 Jan 2021 12:02:40 -0000
@@ -229,3 +229,33 @@ extern struct cmd cmdtab[];
 extern struct tls_config *tls_config;
 extern int tls_session_fd;
 #endif /* !NOSSL */
+
+enum proxy_scheme {
+       PROXY_NONE, PROXY_HTTP, PROXY_SOCKS5
+};
+
+/*
+ * SOCKS protocol constants.
+ */
+
+#define SOCKS_CMD_CONNECT      1
+#define SOCKS_CMD_BIND         2
+#define SOCKS_CMD_UDP_ASSOC    3
+
+#define SOCKS_AUTH_NONE                0
+#define SOCKS_AUTH_GSSAPI      1
+#define SOCKS_AUTH_PASSWORD    2
+
+#define SOCKS_ATYPE_IPV4       1
+#define SOCKS_ATYPE_DOMAIN     3
+#define SOCKS_ATYPE_IPV6       4
+
+#define SOCKS_REP_SUCCEEDED                    0
+#define SOCKS_REP_GENERAL_FAILURE              1
+#define SOCKS_REP_CONNECTION_NOT_ALLOWED       2
+#define SOCKS_REP_NETWORK_UNREACHABLE          3
+#define SOCKS_REP_HOST_UNREACHABLE             4
+#define SOCKS_REP_CONNECTION_REFUSED           5
+#define SOCKS_REP_TTL_EXPIRED                  6
+#define SOCKS_REP_COMMAND_NOT_SUPPORTED                7
+#define SOCKS_REP_ADDRESS_NOT_SUPPORTED                8

Reply via email to