diff mbox series

[FFmpeg-devel,2/2] add socks5 support for tcp clients

Message ID HK0PR02MB360172F9182A3895A56BAB5FC59E0@HK0PR02MB3601.apcprd02.prod.outlook.com
State New
Headers show
Series None | expand

Commit Message

ZHAOYI YI June 13, 2020, 3:17 p.m. UTC
From: zhaoyi <levizhao@live.cn>

---
 libavformat/tcp.c | 204 ++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 204 insertions(+)

Comments

Michael Niedermayer June 14, 2020, 5:07 p.m. UTC | #1
On Sat, Jun 13, 2020 at 11:17:41PM +0800, levizhao@live.cn wrote:
> From: zhaoyi <levizhao@live.cn>
> 
> ---
>  libavformat/tcp.c | 204 ++++++++++++++++++++++++++++++++++++++++++++++
>  1 file changed, 204 insertions(+)

seems to break build on mingw64
AR	libavdevice/libavdevice.a
AR	libavfilter/libavfilter.a
CC	libavformat/tcp.o
src/libavformat/tcp.c: In function ‘tcp_open2’:
src/libavformat/tcp.c:338:9: error: unknown type name ‘in_addr_t’; did you mean ‘in_addr6’?
         in_addr_t addr;
         ^~~~~~~~~
         in_addr6
src/ffbuild/common.mak:59: recipe for target 'libavformat/tcp.o' failed
make: *** [libavformat/tcp.o] Error 1

[..]
Moritz Barsnick June 23, 2020, 3:24 p.m. UTC | #2
On Sat, Jun 13, 2020 at 23:17:41 +0800, levizhao@live.cn wrote:
> From: zhaoyi <levizhao@live.cn>

Is this the name you'd like to appear in the git log, asuming the patch
gets accepted? It cannot be changed afterwards. (I'm saying: Most
people use their "real name" here.)

In your commit message, please also mention "Fixes #5776", as this is
an open ffmpeg ticket:
http://trac.ffmpeg.org/ticket/5776

>  libavformat/tcp.c | 204 ++++++++++++++++++++++++++++++++++++++++++++++
>  1 file changed, 204 insertions(+)

When adding options, please also kindly update the documentation, in
this case doc/protocols.texi. (We might also want to bump the micro
version of libavformat.)

And make sure to point out there that it's currently only for SOCKS5.

And this may be worth a Changelog entry.

> +    if(use_proxy) {
> +        av_url_split(proto_proxy, sizeof(proto_proxy), NULL, 0, hostname_proxy, sizeof(hostname_proxy),
> +            &port, path_proxy, sizeof(path_proxy), proxy_path);
> +        port = (port > 0 && port < 65536) ? port : 1080;

An out-of-range port should be checked here, or before. You have it ten
lines down.

> +    p = strchr(uri, '?');
> +    if (p) {
> +        if (av_find_info_tag(buf, sizeof(buf), "listen", p)) {
> +            char *endptr = NULL;
> +            s->listen = strtol(buf, &endptr, 10);
> +            /* assume if no digits were found it is a request to enable it */
> +            if (buf == endptr)
> +                s->listen = 1;
> +        }
> +        if (av_find_info_tag(buf, sizeof(buf), "timeout", p)) {
> +            s->rw_timeout = strtol(buf, NULL, 10);
> +        }
> +        if (av_find_info_tag(buf, sizeof(buf), "listen_timeout", p)) {
> +            s->listen_timeout = strtol(buf, NULL, 10);
> +        }
> +    }

Are you duplicating some functionality found elsewhere? I believe the
URI '?' syntax is supported in many places.

Let's not try to duplicate code too much. (I didn't have the time to
double-check which other protocols already use similar code.)


Please also make sure to stick to the ffmpeg code style (e.g.
whitespace between arguments, whitespace in "if (", indentation of
function arguments, and so on).

I can't judge on the rest, hoping others will pitch in.

I'm very willing to test this feature (I need it ;-)), but won't be
able to do so before tomorrow.

Cheers,
Moritz
Carl Eugen Hoyos June 23, 2020, 4:48 p.m. UTC | #3
Am Di., 23. Juni 2020 um 17:24 Uhr schrieb Moritz Barsnick <barsnick@gmx.net>:
>
> On Sat, Jun 13, 2020 at 23:17:41 +0800, levizhao@live.cn wrote:
> > From: zhaoyi <levizhao@live.cn>
>
> Is this the name you'd like to appear in the git log, asuming the patch
> gets accepted?

It is your (the author's) choice what you want to appear as your name.

Carl Eugen
Moritz Barsnick June 24, 2020, 10:52 a.m. UTC | #4
Now that I have tested this:

On Sat, Jun 13, 2020 at 23:17:41 +0800, levizhao@live.cn wrote:
>  #endif /* !HAVE_WINSOCK2_H */
> +    char *socks_proxy;
>  } TCPContext;
[...]
>      { "listen",          "Listen for incoming connections",  OFFSET(listen),         AV_OPT_TYPE_INT, { .i64 = 0 },     0,       2,       .flags = D|E },
> +    { "socks_proxy",     "set socks proxy for connection", OFFSET(socks_proxy),   AV_OPT_TYPE_STRING, { .str = NULL }, 0,     0,       .flags = D },
>      { "timeout",     "set timeout (in microseconds) of socket I/O operations", OFFSET(rw_timeout),     AV_OPT_TYPE_INT, { .i64 = -1 },         -1, INT_MAX, .flags = D|E },

You're not using this variable anywhere. So the introduced command line
option is dead.

> +    proxy_path = getenv("socks_proxy");
> +    use_proxy = proxy_path && av_strstart(proxy_path, "socks5://", NULL);
> +    if(use_proxy) {

And here's the reason:
You use the environment variable only.

Furthermore, I'm not sure this is the proper convention for specifying
a SOCKS proxy in the environment. I found various other suggestions
around the net, but not this one. Please don't invent one, instead make
the command line option functional.

>      .url_open            = tcp_open,
> +    .url_open2            = tcp_open2,

If the url_open2 function pointer exists, url_open will never be used.
So you should be writing a *replacement* function for tcp_open(), not
an amendment.

Actually, since your tcp_open2() doesn't even use the "options"
argument, it would be a url_open callback. url_open2: "This callback is
to be used by protocols which open further nested protocols." I think
this does not apply to the "tcp" protocol.

Therefore, you should merge the socks functionality into the existing
tcp_open().

I would also appreciate a log message (at level verbose or debug),
indicating that a SOCKS "redirection" is taking place. Otherwise,
there's a seemingly random mismatch between the URI's hostname and the
logged IP/port.


All this said, when using the environment variable, I managed to get
ffmpeg to connect to an http:// and an https:// input URL (HLS) through
a SOCKS5 proxy. So at least it works. Thanks for your effort so far.

Regards,
Moritz
diff mbox series

Patch

diff --git a/libavformat/tcp.c b/libavformat/tcp.c
index 2198e0f00e..ca6ba2867e 100644
--- a/libavformat/tcp.c
+++ b/libavformat/tcp.c
@@ -45,6 +45,7 @@  typedef struct TCPContext {
 #if !HAVE_WINSOCK2_H
     int tcp_mss;
 #endif /* !HAVE_WINSOCK2_H */
+    char *socks_proxy;
 } TCPContext;
 
 #define OFFSET(x) offsetof(TCPContext, x)
@@ -52,6 +53,7 @@  typedef struct TCPContext {
 #define E AV_OPT_FLAG_ENCODING_PARAM
 static const AVOption options[] = {
     { "listen",          "Listen for incoming connections",  OFFSET(listen),         AV_OPT_TYPE_INT, { .i64 = 0 },     0,       2,       .flags = D|E },
+    { "socks_proxy",     "set socks proxy for connection", OFFSET(socks_proxy),   AV_OPT_TYPE_STRING, { .str = NULL }, 0,     0,       .flags = D },
     { "timeout",     "set timeout (in microseconds) of socket I/O operations", OFFSET(rw_timeout),     AV_OPT_TYPE_INT, { .i64 = -1 },         -1, INT_MAX, .flags = D|E },
     { "listen_timeout",  "Connection awaiting timeout (in milliseconds)",      OFFSET(listen_timeout), AV_OPT_TYPE_INT, { .i64 = -1 },         -1, INT_MAX, .flags = D|E },
     { "send_buffer_size", "Socket send buffer size (in bytes)",                OFFSET(send_buffer_size), AV_OPT_TYPE_INT, { .i64 = -1 },         -1, INT_MAX, .flags = D|E },
@@ -213,6 +215,207 @@  static int tcp_open(URLContext *h, const char *uri, int flags)
     return ret;
 }
 
+/* use options for socks5 proxy from input */
+static int tcp_open2(URLContext *h, const char *uri, int flags, AVDictionary **options) {
+    struct addrinfo hints = { 0 }, *ai, *cur_ai;
+    int port, fd = -1;
+    TCPContext *s = h->priv_data;
+    const char *p;
+    char buf[256];
+    int ret;
+    char hostname[1024],proto[1024],path[1024];
+    char portstr[10];
+    /* current just processing the socks5 non authentication */
+    const char *proxy_path;
+    char hostname_proxy[1024] = { 0 },portstr_proxy[10] = { 0 },proto_proxy[1024] = { 0 },path_proxy[1024] = { 0 };
+    int use_proxy = 0;
+    proxy_path = getenv("socks_proxy");
+    use_proxy = proxy_path && av_strstart(proxy_path, "socks5://", NULL);
+    if(use_proxy) {
+        av_url_split(proto_proxy, sizeof(proto_proxy), NULL, 0, hostname_proxy, sizeof(hostname_proxy),
+            &port, path_proxy, sizeof(path_proxy), proxy_path);
+        port = (port > 0 && port < 65536) ? port : 1080;
+        snprintf(portstr_proxy, sizeof(portstr_proxy), "%d", port);
+    }
+
+    s->open_timeout = 5000000;
+    av_url_split(proto, sizeof(proto), NULL, 0, hostname, sizeof(hostname),
+        &port, path, sizeof(path), uri);
+    if (strcmp(proto, "tcp"))
+        return AVERROR(EINVAL);
+    if (port <= 0 || port >= 65536) {
+        av_log(h, AV_LOG_ERROR, "Port missing in uri\n");
+        return AVERROR(EINVAL);
+    }
+    p = strchr(uri, '?');
+    if (p) {
+        if (av_find_info_tag(buf, sizeof(buf), "listen", p)) {
+            char *endptr = NULL;
+            s->listen = strtol(buf, &endptr, 10);
+            /* assume if no digits were found it is a request to enable it */
+            if (buf == endptr)
+                s->listen = 1;
+        }
+        if (av_find_info_tag(buf, sizeof(buf), "timeout", p)) {
+            s->rw_timeout = strtol(buf, NULL, 10);
+        }
+        if (av_find_info_tag(buf, sizeof(buf), "listen_timeout", p)) {
+            s->listen_timeout = strtol(buf, NULL, 10);
+        }
+    }
+    if (s->rw_timeout >= 0) {
+        s->open_timeout =
+        h->rw_timeout   = s->rw_timeout;
+    }
+    hints.ai_family = AF_UNSPEC;
+    hints.ai_socktype = SOCK_STREAM;
+    snprintf(portstr, sizeof(portstr), "%d", port);
+    if (s->listen)
+        hints.ai_flags |= AI_PASSIVE;
+    if (!hostname[0])
+        ret = getaddrinfo(NULL, portstr, &hints, &ai);
+    else if (use_proxy)
+        ret = getaddrinfo(hostname_proxy, portstr_proxy, &hints, &ai); 
+    else
+        ret = getaddrinfo(hostname, portstr, &hints, &ai);
+    if (ret) {
+        av_log(h, AV_LOG_ERROR,
+               "Failed to resolve hostname %s: %s\n",
+               hostname, gai_strerror(ret));
+        return AVERROR(EIO);
+    }
+
+    cur_ai = ai;
+
+#if HAVE_STRUCT_SOCKADDR_IN6
+    // workaround for IOS9 getaddrinfo in IPv6 only network use hardcode IPv4 address can not resolve port number.
+    if (cur_ai->ai_family == AF_INET6){
+        struct sockaddr_in6 * sockaddr_v6 = (struct sockaddr_in6 *)cur_ai->ai_addr;
+        if (!sockaddr_v6->sin6_port){
+            sockaddr_v6->sin6_port = htons(port);
+        }
+    }
+#endif
+
+    if (s->listen > 0) {
+        while (cur_ai && fd < 0) {
+            fd = ff_socket(cur_ai->ai_family,
+                           cur_ai->ai_socktype,
+                           cur_ai->ai_protocol);
+            if (fd < 0) {
+                ret = ff_neterrno();
+                cur_ai = cur_ai->ai_next;
+            }
+        }
+        if (fd < 0)
+            goto fail1;
+        customize_fd(s, fd);
+    }
+
+    if (s->listen == 2) {
+        // multi-client
+        if ((ret = ff_listen(fd, cur_ai->ai_addr, cur_ai->ai_addrlen)) < 0)
+            goto fail1;
+    } else if (s->listen == 1) {
+        // single client
+        if ((ret = ff_listen_bind(fd, cur_ai->ai_addr, cur_ai->ai_addrlen,
+                                  s->listen_timeout, h)) < 0)
+            goto fail1;
+        // Socket descriptor already closed here. Safe to overwrite to client one.
+        fd = ret;
+    } else {
+        ret = ff_connect_parallel(ai, s->open_timeout / 1000, 3, h, &fd, customize_fd, s);
+        if (ret < 0)
+            goto fail1;
+    }
+
+    h->is_streamed = 1;
+    s->fd = fd;
+
+
+    if(use_proxy) {
+        // make socks5 proxy request
+        in_addr_t addr;
+        int next_pos;
+        unsigned char req[1024];
+        req[0] = 5;
+        req[1] = 1;
+        req[2] = 0;
+        
+        // send handshake
+        if (!(h->flags & AVIO_FLAG_NONBLOCK)) {
+            ret = ff_network_wait_fd_timeout(s->fd, 1, h->rw_timeout, &h->interrupt_callback);
+            if (ret)
+                goto fail1;
+        }
+        ret = send(s->fd, req, 3, MSG_NOSIGNAL);
+        if (ret != 3)
+            goto fail1;
+
+        // receive handshake response
+        if (!(h->flags & AVIO_FLAG_NONBLOCK)) {
+            ret = ff_network_wait_fd_timeout(s->fd, 0, h->rw_timeout, &h->interrupt_callback);
+            if (ret)
+                goto fail1;
+        }
+        ret = recv(s->fd, req, 2, 0);
+        if (ret != 2) {
+            ret = AVERROR_INVALIDDATA;
+            goto fail1;
+        }
+
+        // send connect request
+        req[0] = 5;
+        req[1] = 1;
+        req[2] = 0;
+        addr = inet_addr(hostname);
+        req[3] = INADDR_NONE == addr ? 3 : 1;
+        next_pos = 0;
+        if(req[3] == 3) {
+            req[4] = strlen(hostname);
+            memcpy(req + 5, hostname, strlen(hostname));
+            next_pos = 5 + strlen(hostname);
+        } else {
+            memcpy(req + 4, (unsigned char*)&addr, sizeof(addr));
+            next_pos = 4 + sizeof(addr);
+        }
+        *(unsigned short *)(req + next_pos) = htons(port);
+        // req[next_pos] = (htons(port) & 0x00FF);
+        // req[next_pos + 1] = (htons(port) >> 8);
+
+        if (!(h->flags & AVIO_FLAG_NONBLOCK)) {
+            ret = ff_network_wait_fd_timeout(s->fd, 1, h->rw_timeout, &h->interrupt_callback);
+            if (ret)
+                goto fail1;
+        }
+        ret = send(s->fd, req, next_pos + 2, MSG_NOSIGNAL);
+        if (ret != next_pos + 2)
+            goto fail1;
+
+        // recv connect response
+        if (!(h->flags & AVIO_FLAG_NONBLOCK)) {
+            ret = ff_network_wait_fd_timeout(s->fd, 0, h->rw_timeout, &h->interrupt_callback);
+            if (ret)
+                goto fail1;
+        }
+        ret = recv(s->fd, req, 10, 0);
+        if (ret != 10) {
+            av_log(s, AV_LOG_ERROR, "socks5 connect failed bytes %d\n", ret);
+            ret = AVERROR_INVALIDDATA;
+            goto fail1;
+        }
+    }
+
+    freeaddrinfo(ai);
+    return 0;
+
+ fail1:
+    if (fd >= 0)
+        closesocket(fd);
+    freeaddrinfo(ai);
+    return ret;
+}
+
 static int tcp_accept(URLContext *s, URLContext **c)
 {
     TCPContext *sc = s->priv_data;
@@ -313,6 +516,7 @@  static int tcp_get_window_size(URLContext *h)
 const URLProtocol ff_tcp_protocol = {
     .name                = "tcp",
     .url_open            = tcp_open,
+    .url_open2            = tcp_open2,
     .url_accept          = tcp_accept,
     .url_read            = tcp_read,
     .url_write           = tcp_write,