/* * TCP protocol * Copyright (c) 2002 Fabrice Bellard * * This file is part of FFmpeg. * * FFmpeg is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * FFmpeg is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with FFmpeg; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "libavformat/avformat.h" #include "libavutil/avassert.h" #include "libavutil/parseutils.h" #include "libavutil/opt.h" #include "libavutil/time.h" #include "libavutil/random_seed.h" #include "libavformat/internal.h" #include "libavformat/network.h" #include "libavformat/os_support.h" #include "libavformat/url.h" #include "aml_getaddrinfo.h" #include "get_config.h" #if HAVE_POLL_H #include #endif #if HAVE_PTHREADS #include #endif #include "fftcp.h" #include "dns_cache.h" #include "getaddrinfo_nonblock.h" #include "addrinfo_reorder.h" typedef struct TCPContext { const AVClass *class; int fd; int listen; int open_timeout; int rw_timeout; int listen_timeout; int recv_buffer_size; int send_buffer_size; int tcp_nodelay; int addrinfo_one_by_one; int addrinfo_timeout; int dns_cache_timeout; DNS_seq_mode dns_seq_mode; char *ip_char; } TCPContext; #define OFFSET(x) offsetof(TCPContext, x) #define D AV_OPT_FLAG_DECODING_PARAM #define E AV_OPT_FLAG_ENCODING_PARAM static const AVOption tcp_options[] = { { "listen", "Listen for incoming connections", OFFSET(listen), AV_OPT_TYPE_INT, { .i64 = 0 }, 0, 2, .flags = D|E }, { "tcp_rw_timeout", "set timeout (in microseconds) of socket I/O operations", OFFSET(rw_timeout), AV_OPT_TYPE_INT, { .i64 = 100000 }, -1, INT_MAX, .flags = D|E }, { "tcp_connect_timeout", "set connect timeout (in microseconds) of socket", OFFSET(open_timeout), AV_OPT_TYPE_INT, { .i64 = 1000000 }, -1, INT_MAX, .flags = D|E }, { "listen_timeout", "Connection awaiting timeout (in milliseconds)", OFFSET(listen_timeout), AV_OPT_TYPE_INT, { .i64 = 5000 }, -1, INT_MAX, .flags = D|E }, { "send_buffer_size", "Socket send buffer size (in bytes)", OFFSET(send_buffer_size), AV_OPT_TYPE_INT, { .i64 = INT_MAX }, -1, INT_MAX, .flags = D|E }, { "recv_buffer_size", "Socket receive buffer size (in bytes)", OFFSET(recv_buffer_size), AV_OPT_TYPE_INT, { .i64 = -1 }, -1, INT_MAX, .flags = D|E }, { "tcp_nodelay", "Use TCP_NODELAY to disable nagle's algorithm", OFFSET(tcp_nodelay), AV_OPT_TYPE_BOOL, { .i64 = 0 }, 0, 1, .flags = D|E }, { "addrinfo_one_by_one", "parse addrinfo one by one in getaddrinfo()", OFFSET(addrinfo_one_by_one), AV_OPT_TYPE_INT, { .i64 = 0 }, 0, INT_MAX, .flags = D|E }, { "dns_seq_mode", "the seq to get IP from DNS list", OFFSET(dns_seq_mode), AV_OPT_TYPE_INT, { .i64 = 0 }, 0, INT_MAX, .flags = D|E }, { "addrinfo_timeout", "set timeout (in microseconds) for getaddrinfo()", OFFSET(addrinfo_timeout), AV_OPT_TYPE_INT, { .i64 = 5000000 }, -1, INT_MAX, .flags = D|E }, { "dns_cache_timeout", "dns cache TTL (in second)", OFFSET(dns_cache_timeout), AV_OPT_TYPE_INT, { .i64 = 50000 }, -1, INT_MAX, .flags = D|E }, { "tcp_ip_char", "set HTTP proxy to tunnel through", OFFSET(ip_char), AV_OPT_TYPE_STRING, { .str = NULL }, 0, 0, D | E }, { NULL } }; static const AVClass tcp_class = { .class_name = "tcp", .item_name = av_default_item_name, .option = tcp_options, .version = LIBAVUTIL_VERSION_INT, }; /* return non zero if error */ static int tcp_open(URLContext *h, const char *uri, int flags) { struct addrinfo hints = { 0 }, *ai = NULL, *cur_ai = NULL; int port, fd = -1; TCPContext *s = h->priv_data; const char *p; char buf[256]; int ret = -1; char hostname[1024],proto[1024],path[1024]; char portstr[10]; char hostname_bak[1024] = {0}; DnsPrivateEntry *dns_entry = NULL; int priority = check_ip_priority(); if (s->open_timeout < 0) { 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(NULL, AV_LOG_ERROR,"Port missing in uri"); 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; else { if (priority == DNS_INTE_V4_only) hints.ai_family = AF_INET; if (priority == DNS_INTE_V6_only) hints.ai_family = AF_INET6; } if (s->dns_cache_timeout > 0) { //enable dns cache memcpy(hostname_bak, hostname, 1024); dns_entry = get_dns_cache_reference(hostname); if (dns_entry) { av_log(NULL, AV_LOG_VERBOSE,"Hit DNS cache"); cur_ai = dns_entry->res; } else { #ifdef HAVE_PTHREADS ret = tcp_getaddrinfo_nonblock(hostname, portstr, &hints, &ai, s->addrinfo_timeout, &h->interrupt_callback, s->addrinfo_one_by_one); #else if (s->addrinfo_timeout > 0) av_log(NULL, AV_LOG_WARNING,"Ignore addrinfo_timeout without pthreads support."); if (!hostname[0]) ret = aml_getaddrinfo(NULL, portstr, &hints, &ai); else ret = aml_getaddrinfo(hostname, portstr, &hints, &ai); #endif if (ret) { av_log(NULL, AV_LOG_ERROR,"Failed to resolve hostname %s: %s:%d", hostname, gai_strerror(ret), ret); return AVERROR(EIO); } ret = addrinfo_reorder(&ai, priority, s->dns_seq_mode); if (ret) { if (ai) { freeaddrinfo(ai); } av_log(NULL, AV_LOG_ERROR,"sortaddrinfo err, hostname:%s", hostname); return AVERROR(EIO); } ret = add_dns_cache_entry(hostname, ai, s->dns_cache_timeout, priority, s->dns_seq_mode); if (ret) { if (ai) { freeaddrinfo(ai); } av_log(NULL, AV_LOG_ERROR,"add_dns_cache_entry err, hostname:%s: %s", hostname, gai_strerror(ret)); return AVERROR(EIO); } ai = NULL; } } else { //disable dns cache if (!hostname[0]) ret = getaddrinfo(NULL, portstr, &hints, &ai); else ret = getaddrinfo(hostname, portstr, &hints, &ai); if (ret) { av_log(NULL, AV_LOG_ERROR,"Failed to resolve hostname %s: %s", hostname, gai_strerror(ret)); return AVERROR(EIO); } cur_ai = ai; } restart: if (ai == NULL && dns_entry == NULL) { dns_entry = get_dns_cache_reference(hostname); if (dns_entry) { av_log(NULL, AV_LOG_VERBOSE,"Hit DNS cache"); cur_ai = dns_entry->res; } else { av_log(NULL, AV_LOG_ERROR,"get_dns_cache_reference err, hostname:%s: %s", hostname, gai_strerror(ret)); return AVERROR(EAGAIN); } } { char cur_serv[96]; char cur_ip[1024]; getnameinfo(cur_ai->ai_addr, cur_ai->ai_addrlen, cur_ip, sizeof(cur_ip), cur_serv, sizeof(cur_serv), NI_NUMERICHOST); av_log(NULL, AV_LOG_DEBUG,"now to connect hostname = %s, ip = %s, port:%s", hostname_bak , cur_ip, portstr); } #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; sockaddr_v6->sin6_port = htons(port); } #endif if (cur_ai->ai_family == AF_INET) { struct sockaddr_in * sockaddr_v4 = (struct sockaddr_in *)cur_ai->ai_addr; sockaddr_v4->sin_port = htons(port); } fd = ff_socket(cur_ai->ai_family, cur_ai->ai_socktype, cur_ai->ai_protocol); if (fd < 0) { ret = ff_neterrno(); av_log(NULL, AV_LOG_WARNING,"ff_neterrno:%d, goto fail", ret); goto fail; } /* Set the socket's send or receive buffer sizes, if specified. If unspecified or setting fails, system default is used. */ if (s->recv_buffer_size > 0) { setsockopt (fd, SOL_SOCKET, SO_RCVBUF, &s->recv_buffer_size, sizeof (s->recv_buffer_size)); int optval = 0; int optlen = sizeof(optval); getsockopt (fd, SOL_SOCKET, SO_RCVBUF, (char*)&optval, &optlen); av_log(NULL, AV_LOG_VERBOSE,"this tcp SO_RCVBUF:%d", optval); } if (s->send_buffer_size > 0) { setsockopt (fd, SOL_SOCKET, SO_SNDBUF, &s->send_buffer_size, sizeof (s->send_buffer_size)); } if (s->tcp_nodelay > 0) { setsockopt (fd, IPPROTO_TCP, TCP_NODELAY, &s->tcp_nodelay, sizeof (s->tcp_nodelay)); } if (s->listen == 2) { // multi-client if ((ret = ff_listen(fd, cur_ai->ai_addr, cur_ai->ai_addrlen)) < 0) { av_log(NULL, AV_LOG_WARNING,"ff_listen:%d, goto fail1", ret); 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) { av_log(NULL, AV_LOG_WARNING,"ff_listen_bind:%d, goto fail1", ret); goto fail1; } // Socket descriptor already closed here. Safe to overwrite to client one. fd = ret; } else { if ((ret = ff_listen_connect(fd, cur_ai->ai_addr, cur_ai->ai_addrlen, s->open_timeout / 1000, h, !!cur_ai->ai_next)) < 0) { if (ret == AVERROR_EXIT) { av_log(NULL, AV_LOG_WARNING,"ff_listen_connect:%d, goto fail1", ret); goto fail1; } else { av_log(NULL, AV_LOG_WARNING,"ff_listen_connect:%d, goto fail", ret); goto fail; } } } h->is_streamed = 1; s->fd = fd; { char cur_serv[96]; char cur_ip[1024]; getnameinfo(cur_ai->ai_addr, cur_ai->ai_addrlen, cur_ip, sizeof(cur_ip), cur_serv, sizeof(cur_serv), NI_NUMERICHOST); av_log(NULL, AV_LOG_VERBOSE,"connect successful hostname = %s, ip = %s", hostname_bak , cur_ip); //add_IP_cache(fd, cur_ip); s->ip_char = av_mallocz(strlen(cur_ip)+1); strcpy(s->ip_char, cur_ip); } if (dns_entry) { release_DnsPriEntry(&dns_entry); } if (ai) { freeaddrinfo(ai); } return 0; fail: if (fd >= 0) { closesocket(fd); fd = -1; } if (ai) { if (cur_ai->ai_next) { /* Retry with the next sockaddr */ cur_ai = cur_ai->ai_next; av_freep(&s->ip_char); //remove_IP_cache(s->fd); ret = 0; av_log(NULL, AV_LOG_WARNING,"fail, goto restart"); goto restart; } else { av_log(NULL, AV_LOG_WARNING,"fail, goto fail1"); goto fail1; } } else { if (dns_entry->res == NULL) { av_log(NULL, AV_LOG_WARNING,"fail, dns_entry->res == NULL, goto fail1"); goto fail1; } char cur_serv[96]; char cur_ip[1024]; getnameinfo(cur_ai->ai_addr, cur_ai->ai_addrlen, cur_ip, sizeof(cur_ip), cur_serv, sizeof(cur_serv), NI_NUMERICHOST); int ret_set_dns = set_dns_IP_failed(hostname_bak, cur_ip); if (ret_set_dns < 0) { if (ret_set_dns == -2) { av_log(NULL, AV_LOG_WARNING,"tcp open net down, hostname:%s, IP:%s", hostname_bak, cur_ip); goto fail2; } else { av_log(NULL, AV_LOG_WARNING,"DNS Cache fail! return, hostname:%s, IP:%s, ret:%d", hostname_bak, cur_ip, ret_set_dns); goto fail1; } } av_log(NULL, AV_LOG_WARNING,"DNS Cache fail! restart now, hostname:%s, IP:%s", hostname_bak, cur_ip); release_DnsPriEntry(&dns_entry); goto restart; } av_log(NULL, AV_LOG_WARNING,"goto fail1"); goto fail1; fail2: if (dns_entry) { av_log(NULL, AV_LOG_ERROR,"Hit dns cache but net down"); release_DnsPriEntry(&dns_entry); dns_entry = NULL; } fail1: av_freep(&s->ip_char); //remove_IP_cache(s->fd); if (fd >= 0) { closesocket(fd); fd = -1; } if (dns_entry) { av_log(NULL, AV_LOG_ERROR,"Hit dns cache but connect fail hostname = %s", hostname); release_DnsPriEntry(&dns_entry); if (ret != AVERROR_EXIT) remove_dns_cache_entry(hostname_bak); } if (ai) { freeaddrinfo(ai); } return ret; } static int tcp_accept(URLContext *s, URLContext **c) { TCPContext *sc = s->priv_data; TCPContext *cc; int ret; av_assert0(sc->listen); if ((ret = ffurl_alloc(c, s->filename, s->flags, &s->interrupt_callback)) < 0) return ret; cc = (*c)->priv_data; ret = ff_accept(sc->fd, sc->listen_timeout, s); if (ret < 0) return ret; cc->fd = ret; return 0; } static int tcp_read(URLContext *h, uint8_t *buf, int size) { TCPContext *s = h->priv_data; int ret; if (!(h->flags & AVIO_FLAG_NONBLOCK)) { ret = ff_network_wait_fd_timeout(s->fd, 0, h->rw_timeout, &h->interrupt_callback); if (ret) return ret; } ret = recv(s->fd, buf, size, 0); if (ret == 0) return AVERROR_EOF; return ret < 0 ? ff_neterrno() : ret; } static int tcp_write(URLContext *h, const uint8_t *buf, int size) { TCPContext *s = h->priv_data; int ret; if (!(h->flags & AVIO_FLAG_NONBLOCK)) { ret = ff_network_wait_fd_timeout(s->fd, 1, h->rw_timeout, &h->interrupt_callback); if (ret) return ret; } ret = send(s->fd, buf, size, MSG_NOSIGNAL); return ret < 0 ? ff_neterrno() : ret; } static int tcp_shutdown(URLContext *h, int flags) { TCPContext *s = h->priv_data; int how; if (flags & AVIO_FLAG_WRITE && flags & AVIO_FLAG_READ) { how = SHUT_RDWR; } else if (flags & AVIO_FLAG_WRITE) { how = SHUT_WR; } else { how = SHUT_RD; } return shutdown(s->fd, how); } static int tcp_close(URLContext *h) { TCPContext *s = h->priv_data; av_freep(&s->ip_char); //remove_IP_cache(s->fd); closesocket(s->fd); return 0; } static int tcp_get_file_handle(URLContext *h) { TCPContext *s = h->priv_data; return s->fd; } static int tcp_get_window_size(URLContext *h) { TCPContext *s = h->priv_data; int avail; socklen_t avail_len = sizeof(avail); #if HAVE_WINSOCK2_H /* SO_RCVBUF with winsock only reports the actual TCP window size when auto-tuning has been disabled via setting SO_RCVBUF */ if (s->recv_buffer_size < 0) { return AVERROR(ENOSYS); } #endif if (getsockopt(s->fd, SOL_SOCKET, SO_RCVBUF, &avail, &avail_len)) { return ff_neterrno(); } return avail; } /*aml_tcp_protocol*/ const URLProtocol ff_tcp_protocol = { .name = "tcp", .url_open = tcp_open, .url_accept = tcp_accept, .url_read = tcp_read, .url_write = tcp_write, .url_close = tcp_close, .url_get_file_handle = tcp_get_file_handle, .url_get_short_seek = tcp_get_window_size, .url_shutdown = tcp_shutdown, .priv_data_size = sizeof(TCPContext), .flags = URL_PROTOCOL_FLAG_NETWORK, .priv_data_class = &tcp_class, };