[FFmpeg-devel,6/6] lavf/tls_apple: add support for the new Network framework

Submitted by Rodger Combs on June 11, 2019, 2:16 p.m.

Details

Message ID 20190611141623.59440-6-rodger.combs@gmail.com
State New
Headers show

Commit Message

Rodger Combs June 11, 2019, 2:16 p.m.
Network.framework was added in macOS 10.14/iOS 12, replacing Secure Transport.
Its TLS functionality is (currently) implemented on top of BoringSSL.
Secure Transport is deprecated as of macOS 10.15/iOS 13. It'll likely remain
available for the forseeable future, but it's considered "legacy" and won't
receive new features; most significantly, TLS 1.3. Network.framework also adds
additional functionality (like multipath TCP) that ffmpeg may eventually want
to support.

The new code is behind compile-time checks for header availability, as well
as runtime checks for OS version. The framework is linked weakly, so ffmpeg
built with Network.framework support will continue to work normally on OS
versions prior to macOS 10.14/iOS 12, with the Secure Transport code path
being taken.

Currently, it's not possible to build tls_apple without support for Secure
Transport. I'm not sure if that's a useful case currently, though I suppose
if its headers are ever removed then we'll need to add some compile-time
conditionals.
---
 configure               |  11 +-
 libavformat/Makefile    |   1 +
 libavformat/tls_apple.c | 401 +++++++++++++++++++++++++++++++++++++++-
 3 files changed, 404 insertions(+), 9 deletions(-)

Patch hide | download patch | download mbox

diff --git a/configure b/configure
index 32fc26356c..81be23a54f 100755
--- a/configure
+++ b/configure
@@ -301,6 +301,8 @@  External library support:
   --enable-mbedtls         enable mbedTLS, needed for https support
                            if openssl, gnutls or libtls is not used [no]
   --enable-mediacodec      enable Android MediaCodec support [no]
+  --disable-nwf            disable Apple Network.framework, needed for TLS support
+                           on macOS if openssl and gnutls are not used [autodetect]
   --enable-libmysofa       enable libmysofa, needed for sofalizer filter [no]
   --enable-openal          enable OpenAL 1.1 capture support [no]
   --enable-opencl          enable OpenCL processing [no]
@@ -1693,6 +1695,7 @@  EXTERNAL_AUTODETECT_LIBRARY_LIST="
     libxcb_shape
     libxcb_xfixes
     lzma
+    nwf
     schannel
     sdl2
     securetransport
@@ -3357,6 +3360,7 @@  https_protocol_suggest="zlib"
 icecast_protocol_select="http_protocol"
 mmsh_protocol_select="http_protocol"
 mmst_protocol_select="network"
+nwf_conflict="openssl gnutls libtls mbedtls"
 rtmp_protocol_conflict="librtmp_protocol"
 rtmp_protocol_select="tcp_protocol"
 rtmp_protocol_suggest="zlib"
@@ -3378,7 +3382,7 @@  sctp_protocol_select="network"
 securetransport_conflict="openssl gnutls libtls mbedtls"
 srtp_protocol_select="rtp_protocol srtp"
 tcp_protocol_select="network"
-tls_protocol_deps_any="gnutls openssl schannel securetransport libtls mbedtls"
+tls_protocol_deps_any="gnutls nwf openssl schannel securetransport libtls mbedtls"
 tls_protocol_select="tcp_protocol"
 udp_protocol_select="network"
 udplite_protocol_select="network"
@@ -6353,8 +6357,11 @@  if enabled decklink; then
     esac
 fi
 
+enabled nwf &&
+    check_lib nwf "Network/Network.h" "nw_connection_create" "-Wl,-framework,CoreFoundation -Wl,-framework,Security -Wl,-weak_framework,Network" ||
+        disable nwf
+
 enabled securetransport &&
-    check_func SecIdentityCreate "-Wl,-framework,CoreFoundation -Wl,-framework,Security" &&
     check_lib securetransport "Security/SecureTransport.h Security/Security.h" "SSLCreateContext" "-Wl,-framework,CoreFoundation -Wl,-framework,Security" ||
         disable securetransport
 
diff --git a/libavformat/Makefile b/libavformat/Makefile
index 358d4abf49..fdf981bedc 100644
--- a/libavformat/Makefile
+++ b/libavformat/Makefile
@@ -613,6 +613,7 @@  OBJS-$(CONFIG_TCP_PROTOCOL)              += tcp.o
 TLS-OBJS-$(CONFIG_GNUTLS)                += tls_gnutls.o
 TLS-OBJS-$(CONFIG_LIBTLS)                += tls_libtls.o
 TLS-OBJS-$(CONFIG_MBEDTLS)               += tls_mbedtls.o
+TLS-OBJS-$(CONFIG_NWF)                   += tls_apple.o
 TLS-OBJS-$(CONFIG_OPENSSL)               += tls_openssl.o
 TLS-OBJS-$(CONFIG_SECURETRANSPORT)       += tls_apple.o
 TLS-OBJS-$(CONFIG_SCHANNEL)              += tls_schannel.o
diff --git a/libavformat/tls_apple.c b/libavformat/tls_apple.c
index 23042eb8ee..dd202eb9c0 100644
--- a/libavformat/tls_apple.c
+++ b/libavformat/tls_apple.c
@@ -19,7 +19,7 @@ 
  */
 
 #include <errno.h>
-
+#include <pthread.h>
 
 #include "avformat.h"
 #include "avio_internal.h"
@@ -37,18 +37,88 @@ 
 #include <Security/SecureTransport.h>
 #include <CoreFoundation/CoreFoundation.h>
 
+#if CONFIG_NWF
+#include <Network/Network.h>
+#endif
+
 // We use a private API call here; it's good enough for WebKit.
 SecIdentityRef __attribute__((weak)) SecIdentityCreate(CFAllocatorRef allocator, SecCertificateRef certificate, SecKeyRef privateKey);
 #define ioErr -36
 
+#define NWF_CHECK __builtin_available(macOS 10.14, iOS 12.0, watchOS 5.0, tvOS 12.0, *)
+
 typedef struct TLSContext {
     const AVClass *class;
     TLSShared tls_shared;
     SSLContextRef ssl_context;
     CFArrayRef ca_array;
     int lastErr;
+#if CONFIG_NWF
+    nw_connection_t nw_conn;
+    dispatch_semaphore_t semaphore;
+    dispatch_semaphore_t state_semaphore;
+    pthread_mutex_t state_lock;
+    int at_eof;
+    nw_listener_state_t nw_listen_state;
+    nw_connection_state_t nw_state;
+    nw_error_t nw_state_error;
+    int tcp_nodelay;
+#endif
 } TLSContext;
 
+#if CONFIG_NWF
+static int nwf_init = 0;
+static dispatch_queue_t nwf_queue = NULL;
+
+static int print_nwf_error(void *log_ctx, nw_error_t error, int successRet, const char *func)
+{
+    if (!error)
+        return successRet;
+
+    switch (nw_error_get_error_domain(error)) {
+    case nw_error_domain_posix:
+        return AVERROR(nw_error_get_error_code(error));
+    default:
+        av_log(log_ctx, AV_LOG_ERROR, "[%s] Error domain %u, code %i\n", func,
+               nw_error_get_error_domain(error),
+               nw_error_get_error_code(error));
+        return AVERROR(EIO);
+    }
+}
+
+#define PRINT_NWF_ERROR(err, successRet) print_nwf_error(h, err, successRet, __FUNCTION__)
+
+static int ff_nwf_init(void)
+{
+    if (NWF_CHECK) {
+        int ret = 0;
+        ff_lock_avformat();
+        if (!nwf_init)
+            nwf_queue = dispatch_queue_create("org.ffmpeg.nwf", DISPATCH_QUEUE_SERIAL);
+        if (nwf_queue)
+            nwf_init++;
+        else
+            ret = AVERROR(ENOMEM);
+        ff_unlock_avformat();
+
+        return ret;
+    }
+
+    return AVERROR(EINVAL);
+}
+
+static void ff_nwf_deinit(void)
+{
+    if (NWF_CHECK) {
+        ff_lock_avformat();
+        nwf_init--;
+        if (!nwf_init)
+            dispatch_release(nwf_queue);
+        ff_unlock_avformat();
+    }
+}
+#endif
+
 static int print_tls_error(URLContext *h, int ret)
 {
     TLSContext *c = h->priv_data;
@@ -272,6 +342,35 @@  static OSStatus tls_write_cb(SSLConnectionRef connection, const void *data, size
 static int tls_close(URLContext *h)
 {
     TLSContext *c = h->priv_data;
+
+#if CONFIG_NWF
+    if (NWF_CHECK) {
+        if (c->nw_conn) {
+            nw_connection_cancel(c->nw_conn);
+            nw_release(c->nw_conn);
+        }
+
+        pthread_mutex_lock(&c->state_lock);
+        while (c->nw_state != nw_connection_state_cancelled &&
+               c->nw_state != nw_connection_state_failed &&
+               c->nw_state != nw_connection_state_invalid) {
+            pthread_mutex_unlock(&c->state_lock);
+            dispatch_semaphore_wait(c->state_semaphore, DISPATCH_TIME_FOREVER);
+            pthread_mutex_lock(&c->state_lock);
+        }
+        pthread_mutex_unlock(&c->state_lock);
+
+        if (c->semaphore)
+            dispatch_release(c->semaphore);
+        if (c->state_semaphore)
+            dispatch_release(c->state_semaphore);
+
+        pthread_mutex_destroy(&c->state_lock);
+
+        ff_nwf_deinit();
+    }
+#endif
+
     if (c->ssl_context) {
         SSLClose(c->ssl_context);
         CFRelease(c->ssl_context);
@@ -296,7 +395,235 @@  static int tls_open(URLContext *h, const char *uri, int flags, AVDictionary **op
 {
     TLSContext *c = h->priv_data;
     TLSShared *s = &c->tls_shared;
-    int ret;
+    int ret = 0;
+
+    if (s->ca_file) {
+        if ((ret = load_ca(h)) < 0)
+            goto fail;
+    }
+
+#if CONFIG_NWF
+    if (NWF_CHECK) {
+        int port;
+        char portStr[8];
+        SecIdentityRef identity = NULL;
+        CFArrayRef certArray = NULL;
+        nw_endpoint_t endpoint = NULL;
+        nw_parameters_t parameters = NULL;
+        bool finished = false;
+
+        if ((ret = ff_nwf_init()) < 0)
+            return ret;
+
+        if ((ret = AVERROR(pthread_mutex_init(&c->state_lock, NULL))) < 0)
+            goto nwf_fail;
+
+        if ((ret = ff_tls_process_underlying(s, h, uri, &port)) < 0)
+            goto nwf_fail;
+
+        snprintf(portStr, sizeof(portStr), "%i", port);
+
+        if (!(c->semaphore = dispatch_semaphore_create(0))) {
+            ret = AVERROR(ENOMEM);
+            goto nwf_fail;
+        }
+
+        if (!(c->state_semaphore = dispatch_semaphore_create(0))) {
+            ret = AVERROR(ENOMEM);
+            goto nwf_fail;
+        }
+
+        if (!(endpoint = nw_endpoint_create_host(s->underlying_host, portStr))) {
+            ret = AVERROR(ENOMEM);
+            goto nwf_fail;
+        }
+
+        if (s->cert_file && (ret = load_identity(h, &identity, &certArray)) < 0)
+            goto nwf_fail;
+
+        if (!(parameters = nw_parameters_create_secure_tcp(^(nw_protocol_options_t tls_options) {
+            sec_protocol_options_t options = nw_tls_copy_sec_protocol_options(tls_options);
+            sec_protocol_options_set_tls_server_name(options, s->host);
+            sec_protocol_options_set_peer_authentication_required(options, s->verify);
+            if (s->ca_file || !s->verify)
+                sec_protocol_options_set_verify_block(options, ^(sec_protocol_metadata_t metadata, sec_trust_t trust_ref, sec_protocol_verify_complete_t complete) {
+                    bool succ = false;
+                    SecTrustRef peerTrust = NULL;
+                    SecTrustResultType trustResult;
+
+                    // set_peer_authentication_required is buggy; seems to no-op
+                    if (!s->verify)
+                        complete(true);
+
+                    if (!(peerTrust = sec_trust_copy_ref(trust_ref)))
+                        goto verify_fail;
+
+                    if (SecTrustSetAnchorCertificates(peerTrust, c->ca_array) != noErr)
+                        goto verify_fail;
+
+                    if (SecTrustEvaluate(peerTrust, &trustResult) != noErr)
+                        goto verify_fail;
+
+                    succ = (trustResult == kSecTrustResultProceed ||
+                            trustResult == kSecTrustResultUnspecified);
+
+verify_fail:
+                    if (peerTrust)
+                        CFRelease(peerTrust);
+                    complete(succ);
+                }, nwf_queue);
+            if (identity) {
+                sec_identity_t sec_id = sec_identity_create_with_certificates(identity, certArray);
+                if (sec_id) {
+                    sec_protocol_options_set_local_identity(options, sec_id);
+                    nw_release(sec_id);
+                }
+            }
+        }, ^(nw_protocol_options_t tcp_options) {
+            nw_tcp_options_set_no_delay(tcp_options, c->tcp_nodelay);
+        }))) {
+            ret = AVERROR(ENOMEM);
+            goto nwf_fail;
+        }
+
+        if (s->listen) {
+            nw_listener_t listener;
+
+            nw_parameters_set_local_endpoint(parameters, endpoint);
+
+            listener = nw_listener_create(parameters);
+            if (!listener) {
+                ret = AVERROR(ENOMEM);
+                goto nwf_fail;
+            }
+
+            nw_listener_set_queue(listener, nwf_queue);
+
+            nw_listener_set_new_connection_handler(listener, ^(nw_connection_t connection) {
+                pthread_mutex_lock(&c->state_lock);
+                if (!c->nw_conn) {
+                    nw_retain(connection);
+                    c->nw_conn = connection;
+                    dispatch_semaphore_signal(c->state_semaphore);
+                }
+                pthread_mutex_unlock(&c->state_lock);
+            });
+            nw_listener_set_state_changed_handler(listener, ^(nw_listener_state_t state, nw_error_t error) {
+                pthread_mutex_lock(&c->state_lock);
+                c->nw_listen_state = state;
+                c->nw_state_error = error;
+                dispatch_semaphore_signal(c->state_semaphore);
+                pthread_mutex_unlock(&c->state_lock);
+            });
+
+            nw_listener_start(listener);
+
+            while (!finished) {
+                dispatch_semaphore_wait(c->state_semaphore, DISPATCH_TIME_FOREVER);
+                pthread_mutex_lock(&c->state_lock);
+                switch (c->nw_listen_state) {
+                case nw_listener_state_invalid:
+                    ret = AVERROR_UNKNOWN;
+                    finished = true;
+                    break;
+                case nw_listener_state_waiting:
+                    break;
+                case nw_listener_state_ready:
+                    ret = 0;
+                    if (c->nw_conn)
+                        finished = true;
+                    break;
+                case nw_listener_state_failed:
+                default:
+                    ret = PRINT_NWF_ERROR(c->nw_state_error, AVERROR_UNKNOWN);
+                    finished = true;
+                    break;
+                case nw_listener_state_cancelled:
+                    ret = AVERROR(EIO);
+                    finished = true;
+                    break;
+                }
+                pthread_mutex_unlock(&c->state_lock);
+            }
+
+            finished = false;
+
+            nw_listener_cancel(listener);
+            nw_release(listener);
+
+            pthread_mutex_lock(&c->state_lock);
+            while (c->nw_listen_state != nw_listener_state_cancelled &&
+                   c->nw_listen_state != nw_listener_state_failed &&
+                   c->nw_listen_state != nw_listener_state_invalid) {
+                pthread_mutex_unlock(&c->state_lock);
+                dispatch_semaphore_wait(c->state_semaphore, DISPATCH_TIME_FOREVER);
+                pthread_mutex_lock(&c->state_lock);
+            }
+            pthread_mutex_unlock(&c->state_lock);
+
+            if (!c->nw_conn)
+                goto nwf_fail;
+        } else {
+            if (!(c->nw_conn = nw_connection_create(endpoint, parameters))) {
+                ret = AVERROR(ENOMEM);
+                goto nwf_fail;
+            }
+        }
+
+        nw_connection_set_state_changed_handler(c->nw_conn, ^(nw_connection_state_t state, nw_error_t error) {
+            pthread_mutex_lock(&c->state_lock);
+            c->nw_state = state;
+            c->nw_state_error = error;
+            dispatch_semaphore_signal(c->state_semaphore);
+            pthread_mutex_unlock(&c->state_lock);
+        });
+
+        nw_connection_set_queue(c->nw_conn, nwf_queue);
+
+        nw_connection_start(c->nw_conn);
+
+        while (!finished) {
+            dispatch_semaphore_wait(c->state_semaphore, DISPATCH_TIME_FOREVER);
+            pthread_mutex_lock(&c->state_lock);
+            switch (c->nw_state) {
+            case nw_connection_state_invalid:
+                ret = AVERROR_UNKNOWN;
+                finished = true;
+                break;
+            case nw_connection_state_waiting:
+            case nw_connection_state_preparing:
+                break;
+            case nw_connection_state_ready:
+                ret = 0;
+                finished = true;
+                break;
+            case nw_connection_state_failed:
+            default:
+                ret = PRINT_NWF_ERROR(c->nw_state_error, AVERROR_UNKNOWN);
+                finished = true;
+                break;
+            case nw_connection_state_cancelled:
+                ret = AVERROR(EIO);
+                finished = true;
+                break;
+            }
+            pthread_mutex_unlock(&c->state_lock);
+        }
+
+nwf_fail:
+        if (endpoint)
+            nw_release(endpoint);
+        if (parameters)
+            nw_release(parameters);
+        if (certArray)
+            CFRelease(certArray);
+        if (identity)
+            CFRelease(identity);
+        if (ret < 0)
+            tls_close(h);
+        return ret;
+    }
+#endif
 
     if ((ret = ff_tls_open_underlying(s, h, uri, options)) < 0)
         goto fail;
@@ -307,10 +634,6 @@  static int tls_open(URLContext *h, const char *uri, int flags, AVDictionary **op
         ret = AVERROR(ENOMEM);
         goto fail;
     }
-    if (s->ca_file) {
-        if ((ret = load_ca(h)) < 0)
-            goto fail;
-    }
     if (s->ca_file || !s->verify)
         CHECK_ERROR(SSLSetSessionOption, c->ssl_context, kSSLSessionOptionBreakOnServerAuth, true);
     if (s->cert_file)
@@ -393,6 +716,41 @@  static int tls_read(URLContext *h, uint8_t *buf, int size)
     TLSContext *c = h->priv_data;
     size_t available = 0, processed = 0;
     int ret;
+
+#if CONFIG_NWF
+    if (NWF_CHECK) {
+        __block nw_error_t error;
+        __block int gotSize = 0;
+
+        if (c->at_eof)
+            return AVERROR_EOF;
+
+        nw_connection_receive(c->nw_conn, 1, size, ^(dispatch_data_t content, nw_content_context_t context, bool is_complete, nw_error_t inError) {
+            if (is_complete)
+                c->at_eof = 1;
+
+            if (content) {
+                gotSize = dispatch_data_get_size(content);
+
+                dispatch_data_apply(content, ^(dispatch_data_t region, size_t offset, const void *buffer, size_t inSize) {
+                    memcpy(buf + offset, buffer, inSize);
+                    return (bool)true;
+                });
+            }
+
+            error = inError;
+            dispatch_semaphore_signal(c->semaphore);
+        });
+
+        dispatch_semaphore_wait(c->semaphore, DISPATCH_TIME_FOREVER);
+
+        if (c->at_eof && !gotSize && !error)
+            return AVERROR_EOF;
+
+        return PRINT_NWF_ERROR(error, gotSize);
+    }
+#endif
+
     SSLGetBufferedReadSize(c->ssl_context, &available);
     if (available)
         size = FFMIN(available, size);
@@ -409,7 +767,25 @@  static int tls_write(URLContext *h, const uint8_t *buf, int size)
 {
     TLSContext *c = h->priv_data;
     size_t processed = 0;
-    int ret = SSLWrite(c->ssl_context, buf, size, &processed);
+    int ret;
+
+#if CONFIG_NWF
+    if (NWF_CHECK) {
+        __block nw_error_t error;
+        dispatch_data_t content = dispatch_data_create(buf, size, nwf_queue, DISPATCH_DATA_DESTRUCTOR_DEFAULT);
+        nw_connection_send(c->nw_conn, content, NW_CONNECTION_DEFAULT_MESSAGE_CONTEXT, true, ^(nw_error_t inError) {
+            error = inError;
+            dispatch_semaphore_signal(c->semaphore);
+        });
+        dispatch_release(content);
+
+        dispatch_semaphore_wait(c->semaphore, DISPATCH_TIME_FOREVER);
+
+        return PRINT_NWF_ERROR(error, size);
+    }
+#endif
+
+    ret = SSLWrite(c->ssl_context, buf, size, &processed);
     ret = map_ssl_error(ret, processed);
     if (ret > 0)
         return ret;
@@ -421,11 +797,22 @@  static int tls_write(URLContext *h, const uint8_t *buf, int size)
 static int tls_get_file_handle(URLContext *h)
 {
     TLSContext *c = h->priv_data;
+
+#if CONFIG_NWF
+    if (NWF_CHECK) // Network.framework doesn't expose file handles
+        return -1;
+#endif
+
     return ffurl_get_file_handle(c->tls_shared.tcp);
 }
 
+#define OFFSET(v) offsetof(TLSContext, v)
+
 static const AVOption options[] = {
     TLS_COMMON_OPTIONS(TLSContext, tls_shared),
+#if CONFIG_NWF
+    { "tcp_nodelay", "Use TCP_NODELAY to disable nagle's algorithm", OFFSET(tcp_nodelay), AV_OPT_TYPE_BOOL, { .i64 = 0 }, 0, 1, .flags = TLS_OPTFL },
+#endif
     { NULL }
 };