From 7ee4337cdc1a140c1f269923d9cc59a6d633a412 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Fri, 12 Jul 2024 11:36:44 +0400 Subject: [PATCH] Video tests --- submodules/BrowserUI/BUILD | 1 + .../BrowserUI/Sources/BrowserWebContent.swift | 135 +- submodules/BrowserUI/TonutilsProxy/BUILD | 18 + .../Sources/Extensions/URL.swift | 19 + .../Sources/Extensions/URLSession.swift | 16 + .../Sources/Extensions/WKURLSchemeTask.swift | 12 + .../Sources/Extensions/WKWebView.swift | 36 + .../Extensions/WKWebViewConfiguration.swift | 15 + .../TonutilsProxy/Sources/TonutilsProxy.swift | 27 + .../Sources/TonutilsTunnelError.swift | 22 + .../Sources/TonutilsTunnelProvider.swift | 75 + .../Sources/TonutilsURLSchemeHandler.swift | 138 + .../BrowserUI/TonutilsProxyBridge/BUILD | 22 + .../PublicHeaders/TonProxyBridge/TPB.h | 42 + .../PublicHeaders/TonProxyBridge/TPBTunnel.h | 27 + .../TonProxyBridge/TPBTunnelParameters.h | 19 + .../TonProxyBridge/TonProxyBridge.h | 14 + .../TonutilsProxyBridge/Sources/TPBTunnel.m | 105 + .../Sources/TPBTunnelParameters.m | 23 + .../Public/FFMpegBinding/FFMpegBinding.h | 1 + .../Public/FFMpegBinding/FFMpegLiveMuxer.h | 11 + .../FFMpegBinding/Sources/FFMpegLiveMuxer.m | 334 +++ .../GalleryUI/Sources/GalleryController.swift | 4 +- submodules/TelegramCallsUI/BUILD | 2 + submodules/TelegramCallsUI/CHTTPParser/BUILD | 25 + .../PublicHeaders/CHTTPParser/CHTTPParser.h | 443 +++ .../CHTTPParser/Sources/CHTTPParser.c | 2568 +++++++++++++++++ .../Resources/dash_player.html | 17 + .../MediaStreamVideoComponent.swift | 200 +- .../NWHTTPProtocol/HTTPContext.swift | 54 + .../NWHTTPProtocol/HTTPMessage.swift | 92 + .../NWHTTPProtocol/HTTPProtocol.swift | 369 +++ .../LocalServer/NWHTTPProtocol/README.md | 33 + .../LocalServer/NWHTTPServer/HTTPMethod.swift | 63 + .../LocalServer/NWHTTPServer/HTTPServer.swift | 372 +++ .../LocalServer/NWHTTPServer/HTTPStatus.swift | 34 + .../NWHTTPServer/IncomingMessage.swift | 309 ++ .../LocalServer/NWHTTPServer/README.md | 32 + .../NWHTTPServer/ServerResponse.swift | 396 +++ .../NWHTTPServer/StringEncodingError.swift | 12 + .../Sources/PresentationGroupCall.swift | 55 +- .../Sources/State/AccountViewTracker.swift | 1 + .../Messages/SearchMessages.swift | 53 + .../Messages/TelegramEngineMessages.swift | 4 + .../Sources/AdminUserActionsSheet.swift | 25 +- .../Sources/ChatMessageBubbleItemNode.swift | 20 +- .../Chat/ChatMessageForwardInfoNode/BUILD | 1 + .../Sources/ChatMessageForwardInfoNode.swift | 90 +- .../Sources/ReplyAccessoryPanelNode.swift | 9 +- .../Sources/PeerAllowedReactionsScreen.swift | 2 +- .../Sources/TextLoadingEffect.swift | 30 + .../ChatControllerNavigateToMessage.swift | 166 +- .../Sources/ChatControllerAdminBanUsers.swift | 26 +- .../TelegramUI/Sources/OpenChatMessage.swift | 19 + submodules/TelegramUI/Sources/OpenUrl.swift | 10 +- .../Sources/NativeVideoContent.swift | 4 + submodules/TelegramVoip/BUILD | 1 + .../WrappedMediaStreamingContext.swift | 458 +++ .../UrlHandling/Sources/UrlHandling.swift | 3 + .../Sources/FFMpeg/build-ffmpeg-bazel.sh | 8 +- 60 files changed, 7023 insertions(+), 99 deletions(-) create mode 100644 submodules/BrowserUI/TonutilsProxy/BUILD create mode 100644 submodules/BrowserUI/TonutilsProxy/Sources/Extensions/URL.swift create mode 100644 submodules/BrowserUI/TonutilsProxy/Sources/Extensions/URLSession.swift create mode 100644 submodules/BrowserUI/TonutilsProxy/Sources/Extensions/WKURLSchemeTask.swift create mode 100644 submodules/BrowserUI/TonutilsProxy/Sources/Extensions/WKWebView.swift create mode 100644 submodules/BrowserUI/TonutilsProxy/Sources/Extensions/WKWebViewConfiguration.swift create mode 100644 submodules/BrowserUI/TonutilsProxy/Sources/TonutilsProxy.swift create mode 100644 submodules/BrowserUI/TonutilsProxy/Sources/TonutilsTunnelError.swift create mode 100644 submodules/BrowserUI/TonutilsProxy/Sources/TonutilsTunnelProvider.swift create mode 100644 submodules/BrowserUI/TonutilsProxy/Sources/TonutilsURLSchemeHandler.swift create mode 100644 submodules/BrowserUI/TonutilsProxyBridge/BUILD create mode 100644 submodules/BrowserUI/TonutilsProxyBridge/PublicHeaders/TonProxyBridge/TPB.h create mode 100644 submodules/BrowserUI/TonutilsProxyBridge/PublicHeaders/TonProxyBridge/TPBTunnel.h create mode 100644 submodules/BrowserUI/TonutilsProxyBridge/PublicHeaders/TonProxyBridge/TPBTunnelParameters.h create mode 100644 submodules/BrowserUI/TonutilsProxyBridge/PublicHeaders/TonProxyBridge/TonProxyBridge.h create mode 100644 submodules/BrowserUI/TonutilsProxyBridge/Sources/TPBTunnel.m create mode 100644 submodules/BrowserUI/TonutilsProxyBridge/Sources/TPBTunnelParameters.m create mode 100644 submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegLiveMuxer.h create mode 100644 submodules/FFMpegBinding/Sources/FFMpegLiveMuxer.m create mode 100644 submodules/TelegramCallsUI/CHTTPParser/BUILD create mode 100644 submodules/TelegramCallsUI/CHTTPParser/PublicHeaders/CHTTPParser/CHTTPParser.h create mode 100644 submodules/TelegramCallsUI/CHTTPParser/Sources/CHTTPParser.c create mode 100644 submodules/TelegramCallsUI/Resources/dash_player.html create mode 100644 submodules/TelegramCallsUI/Sources/LocalServer/NWHTTPProtocol/HTTPContext.swift create mode 100644 submodules/TelegramCallsUI/Sources/LocalServer/NWHTTPProtocol/HTTPMessage.swift create mode 100644 submodules/TelegramCallsUI/Sources/LocalServer/NWHTTPProtocol/HTTPProtocol.swift create mode 100644 submodules/TelegramCallsUI/Sources/LocalServer/NWHTTPProtocol/README.md create mode 100644 submodules/TelegramCallsUI/Sources/LocalServer/NWHTTPServer/HTTPMethod.swift create mode 100644 submodules/TelegramCallsUI/Sources/LocalServer/NWHTTPServer/HTTPServer.swift create mode 100644 submodules/TelegramCallsUI/Sources/LocalServer/NWHTTPServer/HTTPStatus.swift create mode 100644 submodules/TelegramCallsUI/Sources/LocalServer/NWHTTPServer/IncomingMessage.swift create mode 100644 submodules/TelegramCallsUI/Sources/LocalServer/NWHTTPServer/README.md create mode 100644 submodules/TelegramCallsUI/Sources/LocalServer/NWHTTPServer/ServerResponse.swift create mode 100644 submodules/TelegramCallsUI/Sources/LocalServer/NWHTTPServer/StringEncodingError.swift diff --git a/submodules/BrowserUI/BUILD b/submodules/BrowserUI/BUILD index 9bc316a4a8..aaaf37a569 100644 --- a/submodules/BrowserUI/BUILD +++ b/submodules/BrowserUI/BUILD @@ -27,6 +27,7 @@ swift_library( "//submodules/Components/BundleIconComponent:BundleIconComponent", "//submodules/Components/BlurredBackgroundComponent:BlurredBackgroundComponent", "//submodules/TelegramUI/Components/MinimizedContainer", + "//submodules/BrowserUI/TonutilsProxy", ], visibility = [ "//visibility:public", diff --git a/submodules/BrowserUI/Sources/BrowserWebContent.swift b/submodules/BrowserUI/Sources/BrowserWebContent.swift index 9f227afc8b..54866b6735 100644 --- a/submodules/BrowserUI/Sources/BrowserWebContent.swift +++ b/submodules/BrowserUI/Sources/BrowserWebContent.swift @@ -9,6 +9,7 @@ import TelegramUIPreferences import AccountContext import WebKit import AppBundle +import TonutilsProxy private final class IpfsSchemeHandler: NSObject, WKURLSchemeHandler { private final class PendingTask { @@ -19,35 +20,81 @@ private final class IpfsSchemeHandler: NSObject, WKURLSchemeHandler { init(sourceTask: any WKURLSchemeTask) { self.sourceTask = sourceTask - var cleanUrl = sourceTask.request.url!.absoluteString - if let range = cleanUrl.range(of: "/ipfs/") { - cleanUrl = "ipfs://" + String(cleanUrl[range.upperBound...]) - } else if let range = cleanUrl.range(of: "/ipns/") { - cleanUrl = "ipns://" + String(cleanUrl[range.upperBound...]) - } - print("Load: \(cleanUrl)") - cleanUrl = cleanUrl.replacingOccurrences(of: "ipns://", with: "ipns/") - cleanUrl = cleanUrl.replacingOccurrences(of: "ipfs://", with: "ipfs/") - let mappedUrl = "https://cloudflare-ipfs.com/\(cleanUrl)" - let isCompleted = self.isCompleted - self.urlSessionTask = URLSession.shared.dataTask(with: URLRequest(url: URL(string: mappedUrl)!), completionHandler: { data, response, error in - if isCompleted.swap(true) { + if sourceTask.request.url?.scheme == "ton" { + var cleanUrl = sourceTask.request.url!.absoluteString + if cleanUrl.hasPrefix("ton://") { + cleanUrl = String(cleanUrl[cleanUrl.index(cleanUrl.startIndex, offsetBy: "ton://".count)...]) + } + + if cleanUrl == "foundation.ton" { + guard let path = getAppBundle().path(forResource: "testsite.data", ofType: nil) else { + return + } + guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else { + return + } + + let response = URLResponse(url: sourceTask.request.url!, mimeType: "text/html", expectedContentLength: data.count, textEncodingName: "UTF-8") + sourceTask.didReceive(response) + sourceTask.didReceive(data) + sourceTask.didFinish() + return } - if let error { - sourceTask.didFailWithError(error) - } else { - if let response { - sourceTask.didReceive(response) + let mappedUrl = "http://ton.x1337.dev:8080" + let isCompleted = self.isCompleted + var request = URLRequest(url: URL(string: mappedUrl)!) + request.httpBody = cleanUrl.data(using: .utf8) + self.urlSessionTask = URLSession.shared.dataTask(with: request, completionHandler: { data, response, error in + if isCompleted.swap(true) { + return } - if let data { - sourceTask.didReceive(data) + + if let error { + sourceTask.didFailWithError(error) + } else { + if let response { + sourceTask.didReceive(response) + } + if let data { + sourceTask.didReceive(data) + } + sourceTask.didFinish() } - sourceTask.didFinish() + }) + self.urlSessionTask?.resume() + } else { + var cleanUrl = sourceTask.request.url!.absoluteString + if let range = cleanUrl.range(of: "/ipfs/") { + cleanUrl = "ipfs://" + String(cleanUrl[range.upperBound...]) + } else if let range = cleanUrl.range(of: "/ipns/") { + cleanUrl = "ipns://" + String(cleanUrl[range.upperBound...]) } - }) - self.urlSessionTask?.resume() + + cleanUrl = cleanUrl.replacingOccurrences(of: "ipns://", with: "ipns/") + cleanUrl = cleanUrl.replacingOccurrences(of: "ipfs://", with: "ipfs/") + let mappedUrl = "https://cloudflare-ipfs.com/\(cleanUrl)" + let isCompleted = self.isCompleted + self.urlSessionTask = URLSession.shared.dataTask(with: URLRequest(url: URL(string: mappedUrl)!), completionHandler: { data, response, error in + if isCompleted.swap(true) { + return + } + + if let error { + sourceTask.didFailWithError(error) + } else { + if let response { + sourceTask.didReceive(response) + } + if let data { + sourceTask.didReceive(data) + } + sourceTask.didFinish() + } + }) + self.urlSessionTask?.resume() + } } func cancel() { @@ -80,6 +127,40 @@ private final class IpfsSchemeHandler: NSObject, WKURLSchemeHandler { } } +@available (iOS 13.0, *) +@MainActor +private final class SharedTonProxy { + static let shared = SharedTonProxy() + + private(set) var params: (host: String, port: UInt16)? + + init() { + let port = UInt16(1234) + Task.detached(operation: { @MainActor [weak self] in + do { + let parameters = try await TonutilsProxy.shared.start(port) + + guard let self else { + return + } + self.params = (parameters.host, parameters.port) + } catch { + print("\(error)") + } + }) + } + + deinit { + Task.detached(operation: { + do { + try await TonutilsProxy.shared.stop() + } catch { + print("\(error)") + } + }) + } +} + final class BrowserWebContent: UIView, BrowserContent, UIScrollViewDelegate { private let webView: WKWebView @@ -96,8 +177,14 @@ final class BrowserWebContent: UIView, BrowserContent, UIScrollViewDelegate { let configuration = WKWebViewConfiguration() if context.sharedContext.immediateExperimentalUISettings.browserExperiment { - configuration.setURLSchemeHandler(IpfsSchemeHandler(), forURLScheme: "ipns") + if #available (iOS 13.0, *) { + if let params = SharedTonProxy.shared.params { + configuration.setURLSchemeHandler(TonutilsURLSchemeHandler(address: params.host, port: params.port)) + } + } + /*configuration.setURLSchemeHandler(IpfsSchemeHandler(), forURLScheme: "ipns") configuration.setURLSchemeHandler(IpfsSchemeHandler(), forURLScheme: "ipfs") + configuration.setURLSchemeHandler(IpfsSchemeHandler(), forURLScheme: "ton")*/ } self.webView = WKWebView(frame: CGRect(), configuration: configuration) diff --git a/submodules/BrowserUI/TonutilsProxy/BUILD b/submodules/BrowserUI/TonutilsProxy/BUILD new file mode 100644 index 0000000000..b854317b06 --- /dev/null +++ b/submodules/BrowserUI/TonutilsProxy/BUILD @@ -0,0 +1,18 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "TonutilsProxy", + module_name = "TonutilsProxy", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/BrowserUI/TonutilsProxyBridge", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/BrowserUI/TonutilsProxy/Sources/Extensions/URL.swift b/submodules/BrowserUI/TonutilsProxy/Sources/Extensions/URL.swift new file mode 100644 index 0000000000..41a50f6aaf --- /dev/null +++ b/submodules/BrowserUI/TonutilsProxy/Sources/Extensions/URL.swift @@ -0,0 +1,19 @@ +// +// Created by Adam Stragner +// + +import Foundation + +internal extension URL { + var isTON: Bool { + if #available(iOS 13.0, *) { + return !TonutilsProxy.SupportedDomain + .allCases + .map({ ".\($0)" }) + .filter({ (host ?? "").hasSuffix($0) }) + .isEmpty + } else { + return false + } + } +} diff --git a/submodules/BrowserUI/TonutilsProxy/Sources/Extensions/URLSession.swift b/submodules/BrowserUI/TonutilsProxy/Sources/Extensions/URLSession.swift new file mode 100644 index 0000000000..2482c294f4 --- /dev/null +++ b/submodules/BrowserUI/TonutilsProxy/Sources/Extensions/URLSession.swift @@ -0,0 +1,16 @@ +// +// Created by Adam Stragner +// + +import Foundation + +internal extension URLSession { + static let `default` = URLSession(configuration: .default) + static var proxyable: URLSession? + + static func proxyable(with connectionProxyDictionary: [AnyHashable: Any]) -> URLSession { + let configuration = URLSessionConfiguration.default + configuration.connectionProxyDictionary = connectionProxyDictionary + return URLSession(configuration: configuration) + } +} diff --git a/submodules/BrowserUI/TonutilsProxy/Sources/Extensions/WKURLSchemeTask.swift b/submodules/BrowserUI/TonutilsProxy/Sources/Extensions/WKURLSchemeTask.swift new file mode 100644 index 0000000000..29a873ba99 --- /dev/null +++ b/submodules/BrowserUI/TonutilsProxy/Sources/Extensions/WKURLSchemeTask.swift @@ -0,0 +1,12 @@ +// +// Created by Adam Stragner +// + +import Foundation +import WebKit + +internal extension WKURLSchemeTask { + var identifier: AnyHashable { + AnyHashable(request) + } +} diff --git a/submodules/BrowserUI/TonutilsProxy/Sources/Extensions/WKWebView.swift b/submodules/BrowserUI/TonutilsProxy/Sources/Extensions/WKWebView.swift new file mode 100644 index 0000000000..5facbcd4da --- /dev/null +++ b/submodules/BrowserUI/TonutilsProxy/Sources/Extensions/WKWebView.swift @@ -0,0 +1,36 @@ +// +// Created by Adam Stragner +// + +import Foundation +import WebKit + +/// `WKWebView` does not allow the addition of a custom` WKURLSchemeHandler` for the `HTTP` and `HTTPS` schemes. +/// However, this workaround enables us to accomplish this task. +internal extension WKWebView { + static let _swizzle: Void = { + let origin = class_getClassMethod(WKWebView.self, #selector(WKWebView.handlesURLScheme(_:))) + let swizzled = class_getClassMethod( + WKWebView.self, + #selector(WKWebView.swizzled_handlesURLScheme(_:)) + ) + + guard let origin, let swizzled + else { + return + } + + method_exchangeImplementations(origin, swizzled) + return () + }() + + @objc(swizzled_handlesURLScheme:) + private static func swizzled_handlesURLScheme(_ urlScheme: String) -> Bool { + guard !TonutilsURLSchemeHandler.schemas.contains(urlScheme) + else { + return false + } + + return swizzled_handlesURLScheme(urlScheme) + } +} diff --git a/submodules/BrowserUI/TonutilsProxy/Sources/Extensions/WKWebViewConfiguration.swift b/submodules/BrowserUI/TonutilsProxy/Sources/Extensions/WKWebViewConfiguration.swift new file mode 100644 index 0000000000..b3e91dda04 --- /dev/null +++ b/submodules/BrowserUI/TonutilsProxy/Sources/Extensions/WKWebViewConfiguration.swift @@ -0,0 +1,15 @@ +// +// Created by Adam Stragner +// + +import Foundation +import WebKit + +public extension WKWebViewConfiguration { + func setURLSchemeHandler(_ urlSchemeHandler: TonutilsURLSchemeHandler) { + let _ = WKWebView._swizzle + TonutilsURLSchemeHandler.schemas.forEach({ + setURLSchemeHandler(urlSchemeHandler, forURLScheme: $0) + }) + } +} diff --git a/submodules/BrowserUI/TonutilsProxy/Sources/TonutilsProxy.swift b/submodules/BrowserUI/TonutilsProxy/Sources/TonutilsProxy.swift new file mode 100644 index 0000000000..b429c811f2 --- /dev/null +++ b/submodules/BrowserUI/TonutilsProxy/Sources/TonutilsProxy.swift @@ -0,0 +1,27 @@ +// +// Created by Adam Stragner +// + +import Foundation +import TonutilsProxyBridge + +// MARK: - TonutilsProxy + +@available(iOS 13.0, *) +public final class TonutilsProxy: TPBTunnel { + enum SupportedDomain: String, CaseIterable, RawRepresentable { + case ton + case adnl + case tme = "t.me" + case bag + } + + public static var shared: TonutilsProxy { + shared() + } + + @discardableResult + public func start(_ port: UInt16 = 9090) async throws -> TPBTunnelParameters { + try await start(withPort: port) + } +} diff --git a/submodules/BrowserUI/TonutilsProxy/Sources/TonutilsTunnelError.swift b/submodules/BrowserUI/TonutilsProxy/Sources/TonutilsTunnelError.swift new file mode 100644 index 0000000000..58afbb1dfd --- /dev/null +++ b/submodules/BrowserUI/TonutilsProxy/Sources/TonutilsTunnelError.swift @@ -0,0 +1,22 @@ +// +// Created by Adam Stragner +// + +import Foundation + +// MARK: - TonutilsTunnelError + +public enum TonutilsTunnelError { + case unableUpdateNetworkSettings(underlyingError: Error) +} + +// MARK: LocalizedError + +extension TonutilsTunnelError: LocalizedError { + public var errorDescription: String? { + switch self { + case let .unableUpdateNetworkSettings(underlyingError): + return "[TonutilsTunnelError]: Unable update network settings - \(underlyingError)" + } + } +} diff --git a/submodules/BrowserUI/TonutilsProxy/Sources/TonutilsTunnelProvider.swift b/submodules/BrowserUI/TonutilsProxy/Sources/TonutilsTunnelProvider.swift new file mode 100644 index 0000000000..665df80a5c --- /dev/null +++ b/submodules/BrowserUI/TonutilsProxy/Sources/TonutilsTunnelProvider.swift @@ -0,0 +1,75 @@ +// +// Created by Adam Stragner +// + +import NetworkExtension + +@available(iOS 13.0, *) +open class TonproxyTunnelProvider: NEPacketTunnelProvider { + // MARK: Open + + open var preferredPort: UInt16 { + 9090 + } + + open override func startTunnel(options: [String: NSObject]? = nil) async throws { + let tunnel = TonutilsProxy.shared + let parameters = try await tunnel.start(preferredPort) + + let settings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: parameters.host) + + settings.mtu = NSNumber(value: 1500) + settings.ipv4Settings = NEIPv4Settings( + addresses: ["127.0.0.1"], + subnetMasks: ["255.255.255.255"] + ) + + settings.proxySettings = { + let proxySettings = NEProxySettings() + proxySettings.httpEnabled = true + proxySettings.httpServer = NEProxyServer( + address: parameters.host, + port: Int(parameters.port) + ) + + proxySettings.httpsEnabled = false + proxySettings.excludeSimpleHostnames = false + proxySettings.matchDomains = TonutilsProxy.SupportedDomain.allCases.map({ + ".\($0.rawValue)" + }) + + proxySettings.autoProxyConfigurationEnabled = true + return proxySettings + }() + + do { + try await setTunnelNetworkSettings(settings) + } catch { + await _stop() + throw TonutilsTunnelError.unableUpdateNetworkSettings(underlyingError: error) + } + } + + open override func stopTunnel(with reason: NEProviderStopReason) async { + await _stop() + } + + open override func handleAppMessage(_ messageData: Data) async -> Data? { + if let string = String(data: messageData, encoding: .utf8) { + print("[TonproxyTunnelProvider]: Did handle a message - \(string)") + } + + return nil + } + + // MARK: Private + + private func _stop() async { + let tunnel = TonutilsProxy.shared + do { + try await tunnel.stop() + } catch { + print("\(error)") + } + } +} diff --git a/submodules/BrowserUI/TonutilsProxy/Sources/TonutilsURLSchemeHandler.swift b/submodules/BrowserUI/TonutilsProxy/Sources/TonutilsURLSchemeHandler.swift new file mode 100644 index 0000000000..34e5418e4d --- /dev/null +++ b/submodules/BrowserUI/TonutilsProxy/Sources/TonutilsURLSchemeHandler.swift @@ -0,0 +1,138 @@ +// +// Created by Adam Stragner +// + +import Foundation +import WebKit + +// MARK: - URLSchemeHandlerSessionType + +public enum URLSchemeHandlerSessionType { + case `default` + case proxy(connectionProxyDictionary: [AnyHashable: Any]) +} + +// MARK: - TonutilsURLSchemeHandlerDelegate + +public protocol TonutilsURLSchemeHandlerDelegate: AnyObject { + func tonutilsURLSchemeHandler( + _ schemeHandler: TonutilsURLSchemeHandler, + sessionFor type: URLSchemeHandlerSessionType + ) -> URLSession +} + +// MARK: - TonutilsURLSchemeHandler + +open class TonutilsURLSchemeHandler: NSObject, WKURLSchemeHandler { + // MARK: Lifecycle + + public init(address: String, port: UInt16) { + self.connectionProxyDictionary = [ + kCFNetworkProxiesHTTPEnable: true, + kCFNetworkProxiesHTTPProxy: address, + kCFNetworkProxiesHTTPPort: port, + ] + + super.init() + } + + // MARK: Open + + open func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) { + guard let url = urlSchemeTask.request.url + else { + return + } + + if url.isTON { + route(urlSchemeTask, with: .proxy(connectionProxyDictionary: connectionProxyDictionary)) + } else { + route(urlSchemeTask, with: .default) + } + } + + open func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) { + tasks.removeValue(forKey: urlSchemeTask.identifier) + } + + // MARK: Public + + public weak var delegate: TonutilsURLSchemeHandlerDelegate? + + // MARK: Internal + + internal static let schemas = ["http"] + + // MARK: Private + + private let connectionProxyDictionary: [AnyHashable: Any] + private var tasks: [AnyHashable: URLSessionDataTask] = [:] + + private func route( + _ schemeTask: WKURLSchemeTask, + with sesstionType: URLSchemeHandlerSessionType + ) { + let session = session(for: sesstionType) + let task = session.dataTask( + with: schemeTask.request, + completionHandler: { [weak schemeTask, weak self] data, response, error in + guard let self, let schemeTask + else { + schemeTask?.didFailWithError(URLError(.cancelled)) + return + } + + finish(schemeTask, data: data, response: response, error: error) + } + ) + + tasks[schemeTask.identifier] = task + task.resume() + } + + private func finish( + _ schemeTask: WKURLSchemeTask, + data: Data?, + response: URLResponse?, + error: Error? + ) { + tasks.removeValue(forKey: schemeTask.identifier) + if let error = error, error._code != NSURLErrorCancelled { + schemeTask.didFailWithError(error) + } else { + if let response = response { + schemeTask.didReceive(response) + } + if let data = data { + schemeTask.didReceive(data) + } + + schemeTask.didFinish() + } + } +} + +private extension TonutilsURLSchemeHandler { + func session(for type: URLSchemeHandlerSessionType) -> URLSession { + guard let session = delegate?.tonutilsURLSchemeHandler(self, sessionFor: type) + else { + return _session(for: type) + } + return session + } + + private func _session(for type: URLSchemeHandlerSessionType) -> URLSession { + switch type { + case .default: + return .default + case let .proxy(connectionProxyDictionary): + if let session = URLSession.proxyable { + return session + } else { + let session: URLSession = .proxyable(with: connectionProxyDictionary) + URLSession.proxyable = session + return session + } + } + } +} diff --git a/submodules/BrowserUI/TonutilsProxyBridge/BUILD b/submodules/BrowserUI/TonutilsProxyBridge/BUILD new file mode 100644 index 0000000000..24b92170e8 --- /dev/null +++ b/submodules/BrowserUI/TonutilsProxyBridge/BUILD @@ -0,0 +1,22 @@ + +objc_library( + name = "TonutilsProxyBridge", + enable_modules = True, + module_name = "TonutilsProxyBridge", + srcs = glob([ + "Sources/**/*.h", + "Sources/**/*.m", + ]), + hdrs = glob([ + "PublicHeaders/**/*.h", + ]), + includes = [ + "PublicHeaders", + ], + deps = [ + "//third-party/tonutilsproxy", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/BrowserUI/TonutilsProxyBridge/PublicHeaders/TonProxyBridge/TPB.h b/submodules/BrowserUI/TonutilsProxyBridge/PublicHeaders/TonProxyBridge/TPB.h new file mode 100644 index 0000000000..30f0572f7a --- /dev/null +++ b/submodules/BrowserUI/TonutilsProxyBridge/PublicHeaders/TonProxyBridge/TPB.h @@ -0,0 +1,42 @@ +// +// Created by Adam Stragner +// + +#ifndef TPB_h +#define TPB_h + +#import + +// _TPB_EXPORT +#if !defined(_TPB_EXPORT) +# if defined(__cplusplus) +# define _TPB_EXPORT extern "C" +# else +# define _TPB_EXPORT extern +# endif +#endif /* _TPB_EXPORT */ + +// _TPB_SWIFT_ERROR +#if !defined(_TPB_SWIFT_ERROR) +# if __OBJC__ && __has_attribute(swift_error) +# define _TPB_SWIFT_ERROR __attribute__((swift_error(nonnull_error))); +# else +# abort(); +# endif +#endif /* _TPB_SWIFT_ERROR */ + +// _TPB_EXTERN +#if !defined(_TPB_EXTERN) +# if defined(__cplusplus) +# define _TPB_EXTERN extern "C" __attribute__((visibility ("default"))) +# else +# define _TPB_EXTERN extern __attribute__((visibility ("default"))) +# endif +#endif /* _TPB_EXTERN */ + +#define TPB_EXPORT _TPB_EXPORT +#define TPB_EXTERN _TPB_EXTERN +#define TPB_SWIFT_ERROR _TPB_SWIFT_ERROR +#define TPB_STATIC_INLINE static inline + +#endif /* TPB_h */ diff --git a/submodules/BrowserUI/TonutilsProxyBridge/PublicHeaders/TonProxyBridge/TPBTunnel.h b/submodules/BrowserUI/TonutilsProxyBridge/PublicHeaders/TonProxyBridge/TPBTunnel.h new file mode 100644 index 0000000000..6f6e6be140 --- /dev/null +++ b/submodules/BrowserUI/TonutilsProxyBridge/PublicHeaders/TonProxyBridge/TPBTunnel.h @@ -0,0 +1,27 @@ +// +// Created by Adam Stragner +// + +#import + +@class TPBTunnelParameters; + +NS_ASSUME_NONNULL_BEGIN + +TPB_EXPORT NSErrorDomain const TPBErrorDomain; + +@interface TPBTunnel : NSObject + +@property (nonatomic, readonly, retain) TPBTunnelParameters * _Nullable parameters; + ++ (instancetype)sharedTunnel; +- (instancetype)init NS_UNAVAILABLE; + +- (void)startWithPort:(UInt16)port completionBlock:(void (^ _Nullable)(TPBTunnelParameters * _Nullable parameters, NSError * _Nullable error))completionBlock; +- (void)stopWithCompletionBlock:(void (^ _Nullable)(NSError * _Nullable error))completionBlock; + +- (BOOL)isRunning; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/BrowserUI/TonutilsProxyBridge/PublicHeaders/TonProxyBridge/TPBTunnelParameters.h b/submodules/BrowserUI/TonutilsProxyBridge/PublicHeaders/TonProxyBridge/TPBTunnelParameters.h new file mode 100644 index 0000000000..113a157734 --- /dev/null +++ b/submodules/BrowserUI/TonutilsProxyBridge/PublicHeaders/TonProxyBridge/TPBTunnelParameters.h @@ -0,0 +1,19 @@ +// +// Created by Adam Stragner +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface TPBTunnelParameters : NSObject + +@property (nonatomic, readonly, copy) NSString *host; +@property (nonatomic, readonly, assign) UInt16 port; + +- (instancetype)initWithHost:(NSString *)host port:(UInt16)port; +- (NSURL *)URL; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/BrowserUI/TonutilsProxyBridge/PublicHeaders/TonProxyBridge/TonProxyBridge.h b/submodules/BrowserUI/TonutilsProxyBridge/PublicHeaders/TonProxyBridge/TonProxyBridge.h new file mode 100644 index 0000000000..7ccc4c3956 --- /dev/null +++ b/submodules/BrowserUI/TonutilsProxyBridge/PublicHeaders/TonProxyBridge/TonProxyBridge.h @@ -0,0 +1,14 @@ +// +// Created by Adam Stragner +// + +#ifndef TonProxyBridge_h +#define TonProxyBridge_h + +#import + +#import +#import +#import + +#endif /* TPB_h */ diff --git a/submodules/BrowserUI/TonutilsProxyBridge/Sources/TPBTunnel.m b/submodules/BrowserUI/TonutilsProxyBridge/Sources/TPBTunnel.m new file mode 100644 index 0000000000..eee85303d3 --- /dev/null +++ b/submodules/BrowserUI/TonutilsProxyBridge/Sources/TPBTunnel.m @@ -0,0 +1,105 @@ +// +// Created by Adam Stragner +// + +#import +#import + +#import + +NSErrorDomain const TPBErrorDomain = @"TPBErrorDomain"; + +@interface TPBTunnel () + +@property (nonatomic, retain) TPBTunnelParameters * _Nullable _parameters; +@property (nonatomic, retain) dispatch_queue_t queue; + +@end + +@implementation TPBTunnel + +- (instancetype)init_ { + self = [super init]; + if (self) { + _queue = dispatch_queue_create("tonutils-proxy", DISPATCH_QUEUE_SERIAL); + } + return self; +} + ++ (instancetype)sharedTunnel { + static dispatch_once_t onceToken; + static TPBTunnel *sharedTunnel = nil; + dispatch_once(&onceToken, ^{ + sharedTunnel = [[TPBTunnel alloc] init_]; + }); + return sharedTunnel; +} + +- (void)startWithPort:(UInt16)port completionBlock:(void (^ _Nullable)(TPBTunnelParameters * _Nullable parameters, NSError * _Nullable error))completionBlock { + dispatch_async(_queue, ^{ + TPBTunnelParameters *parameters = self._parameters; + if (parameters != nil) { + if (completionBlock) { + completionBlock(parameters, nil); + } + return; + } + + char *result = StartProxy(port); + NSError *error = [self errorWithResult:result]; + + if (error == nil) { + parameters = [[TPBTunnelParameters alloc] initWithHost:@"127.0.0.1" port:port]; + completionBlock(parameters, nil); + } else { + completionBlock(nil, error); + } + + self._parameters = parameters; + }); +} + +- (void)stopWithCompletionBlock:(void (^ _Nullable)(NSError * _Nullable error))completionBlock { + dispatch_async(_queue, ^{ + if (self._parameters == nil) { + if (completionBlock) { + completionBlock(nil); + } + return; + } + + char *result = StopProxy(); + self._parameters = nil; + + if (completionBlock) { + completionBlock([self errorWithResult:result]); + } + }); +} + +- (NSError * _Nullable)errorWithResult:(char *)result { + NSString *string = [NSString stringWithUTF8String:result]; + if ([string isEqualToString:@"OK"]) { + return nil; + } else { + return [[NSError alloc] initWithDomain:TPBErrorDomain code:0 userInfo:@{ + NSLocalizedDescriptionKey : [string copy] + }]; + } +} + +#pragma Setters & Getters + +- (TPBTunnelParameters * _Nullable)parameters { + __block TPBTunnelParameters *parameters = nil; + dispatch_sync(_queue, ^{ + parameters = self._parameters; + }); + return parameters; +} + +- (BOOL)isRunning { + return [self parameters] != nil; +} + +@end diff --git a/submodules/BrowserUI/TonutilsProxyBridge/Sources/TPBTunnelParameters.m b/submodules/BrowserUI/TonutilsProxyBridge/Sources/TPBTunnelParameters.m new file mode 100644 index 0000000000..c75fab29f5 --- /dev/null +++ b/submodules/BrowserUI/TonutilsProxyBridge/Sources/TPBTunnelParameters.m @@ -0,0 +1,23 @@ +// +// Created by Adam Stragner +// + +#import + +@implementation TPBTunnelParameters + +- (instancetype)initWithHost:(NSString *)host port:(UInt16)port { + self = [super init]; + if (self) { + _host = [host copy]; + _port = port; + } + return self; +} + +- (NSURL *)URL { + NSString *path = [NSString stringWithFormat:@"%@:%d", self.host, self.port]; + return [[NSURL alloc] initWithString:path]; +} + +@end diff --git a/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegBinding.h b/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegBinding.h index 5ed187b5fb..21801d9355 100644 --- a/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegBinding.h +++ b/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegBinding.h @@ -10,4 +10,5 @@ #import #import #import +#import #import diff --git a/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegLiveMuxer.h b/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegLiveMuxer.h new file mode 100644 index 0000000000..ec480c1f13 --- /dev/null +++ b/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegLiveMuxer.h @@ -0,0 +1,11 @@ +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FFMpegLiveMuxer : NSObject + ++ (bool)remux:(NSString * _Nonnull)path to:(NSString * _Nonnull)outPath offsetSeconds:(double)offsetSeconds; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/FFMpegBinding/Sources/FFMpegLiveMuxer.m b/submodules/FFMpegBinding/Sources/FFMpegLiveMuxer.m new file mode 100644 index 0000000000..76caddeadf --- /dev/null +++ b/submodules/FFMpegBinding/Sources/FFMpegLiveMuxer.m @@ -0,0 +1,334 @@ +#import +#import + +#include "libavutil/timestamp.h" +#include "libavformat/avformat.h" +#include "libavcodec/avcodec.h" +#include "libswresample/swresample.h" + +#define MOV_TIMESCALE 1000 + +@implementation FFMpegLiveMuxer + ++ (bool)remux:(NSString * _Nonnull)path to:(NSString * _Nonnull)outPath offsetSeconds:(double)offsetSeconds { + AVFormatContext *input_format_context = NULL, *output_format_context = NULL; + AVPacket packet; + const char *in_filename, *out_filename; + int ret, i; + int stream_index = 0; + int *streams_list = NULL; + int number_of_streams = 0; + + struct SwrContext *swr_ctx = NULL; + + in_filename = [path UTF8String]; + out_filename = [outPath UTF8String]; + + if ((ret = avformat_open_input(&input_format_context, in_filename, av_find_input_format("mp4"), NULL)) < 0) { + fprintf(stderr, "Could not open input file '%s'\n", in_filename); + goto end; + } + if ((ret = avformat_find_stream_info(input_format_context, NULL)) < 0) { + fprintf(stderr, "Failed to retrieve input stream information\n"); + goto end; + } + + avformat_alloc_output_context2(&output_format_context, NULL, "mpegts", out_filename); + + if (!output_format_context) { + fprintf(stderr, "Could not create output context\n"); + ret = AVERROR_UNKNOWN; + goto end; + } + + const AVCodec *aac_codec = avcodec_find_encoder(AV_CODEC_ID_AAC); + if (!aac_codec) { + fprintf(stderr, "Could not find AAC encoder\n"); + ret = AVERROR_UNKNOWN; + goto end; + } + + AVCodecContext *aac_codec_context = avcodec_alloc_context3(aac_codec); + if (!aac_codec_context) { + fprintf(stderr, "Could not allocate AAC codec context\n"); + ret = AVERROR_UNKNOWN; + goto end; + } + + const AVCodec *opus_decoder = avcodec_find_decoder(AV_CODEC_ID_OPUS); + if (!opus_decoder) { + fprintf(stderr, "Could not find Opus decoder\n"); + ret = AVERROR_UNKNOWN; + goto end; + } + + AVCodecContext *opus_decoder_context = avcodec_alloc_context3(opus_decoder); + if (!opus_decoder_context) { + fprintf(stderr, "Could not allocate Opus decoder context\n"); + ret = AVERROR_UNKNOWN; + goto end; + } + + number_of_streams = input_format_context->nb_streams; + streams_list = av_malloc_array(number_of_streams, sizeof(*streams_list)); + + if (!streams_list) { + ret = AVERROR(ENOMEM); + goto end; + } + + for (i = 0; i < input_format_context->nb_streams; i++) { + AVStream *out_stream; + AVStream *in_stream = input_format_context->streams[i]; + AVCodecParameters *in_codecpar = in_stream->codecpar; + + if (in_codecpar->codec_type != AVMEDIA_TYPE_AUDIO && in_codecpar->codec_type != AVMEDIA_TYPE_VIDEO) { + streams_list[i] = -1; + continue; + } + + streams_list[i] = stream_index++; + + if (in_codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { + out_stream = avformat_new_stream(output_format_context, NULL); + if (!out_stream) { + fprintf(stderr, "Failed allocating output stream\n"); + ret = AVERROR_UNKNOWN; + goto end; + } + ret = avcodec_parameters_copy(out_stream->codecpar, in_codecpar); + if (ret < 0) { + fprintf(stderr, "Failed to copy codec parameters\n"); + goto end; + } + out_stream->time_base = in_stream->time_base; + out_stream->duration = in_stream->duration; + } else if (in_codecpar->codec_type == AVMEDIA_TYPE_AUDIO) { + out_stream = avformat_new_stream(output_format_context, aac_codec); + if (!out_stream) { + fprintf(stderr, "Failed allocating output stream\n"); + ret = AVERROR_UNKNOWN; + goto end; + } + + // Set the codec parameters for the AAC encoder + aac_codec_context->sample_rate = in_codecpar->sample_rate; + aac_codec_context->channel_layout = in_codecpar->channel_layout ? in_codecpar->channel_layout : AV_CH_LAYOUT_STEREO; + aac_codec_context->channels = av_get_channel_layout_nb_channels(aac_codec_context->channel_layout); + aac_codec_context->sample_fmt = aac_codec->sample_fmts ? aac_codec->sample_fmts[0] : AV_SAMPLE_FMT_FLTP; // Use the first supported sample format + aac_codec_context->bit_rate = 128000; // Set a default bitrate, you can adjust this as needed + //aac_codec_context->time_base = (AVRational){1, 90000}; + + ret = avcodec_open2(aac_codec_context, aac_codec, NULL); + if (ret < 0) { + fprintf(stderr, "Could not open AAC encoder\n"); + goto end; + } + + ret = avcodec_parameters_from_context(out_stream->codecpar, aac_codec_context); + if (ret < 0) { + fprintf(stderr, "Failed initializing audio output stream\n"); + goto end; + } + + out_stream->time_base = (AVRational){1, 90000}; + out_stream->duration = av_rescale_q(in_stream->duration, in_stream->time_base, out_stream->time_base); + + // Set up the Opus decoder context + ret = avcodec_parameters_to_context(opus_decoder_context, in_codecpar); + if (ret < 0) { + fprintf(stderr, "Could not copy codec parameters to decoder context\n"); + goto end; + } + if (opus_decoder_context->channel_layout == 0) { + opus_decoder_context->channel_layout = av_get_default_channel_layout(opus_decoder_context->channels); + } + ret = avcodec_open2(opus_decoder_context, opus_decoder, NULL); + if (ret < 0) { + fprintf(stderr, "Could not open Opus decoder\n"); + goto end; + } + + // Reset the channel layout if it was unset before opening the codec + if (opus_decoder_context->channel_layout == 0) { + opus_decoder_context->channel_layout = av_get_default_channel_layout(opus_decoder_context->channels); + } + } + } + + // Set up the resampling context + swr_ctx = swr_alloc_set_opts(NULL, + aac_codec_context->channel_layout, aac_codec_context->sample_fmt, aac_codec_context->sample_rate, + opus_decoder_context->channel_layout, opus_decoder_context->sample_fmt, opus_decoder_context->sample_rate, + 0, NULL); + if (!swr_ctx) { + fprintf(stderr, "Could not allocate resampler context\n"); + ret = AVERROR(ENOMEM); + goto end; + } + + if ((ret = swr_init(swr_ctx)) < 0) { + fprintf(stderr, "Failed to initialize the resampling context\n"); + goto end; + } + + if (!(output_format_context->oformat->flags & AVFMT_NOFILE)) { + ret = avio_open(&output_format_context->pb, out_filename, AVIO_FLAG_WRITE); + if (ret < 0) { + fprintf(stderr, "Could not open output file '%s'\n", out_filename); + goto end; + } + } + + AVDictionary* opts = NULL; + ret = avformat_write_header(output_format_context, &opts); + if (ret < 0) { + fprintf(stderr, "Error occurred when opening output file\n"); + goto end; + } + + while (1) { + AVStream *in_stream, *out_stream; + ret = av_read_frame(input_format_context, &packet); + if (ret < 0) + break; + + in_stream = input_format_context->streams[packet.stream_index]; + if (packet.stream_index >= number_of_streams || streams_list[packet.stream_index] < 0) { + av_packet_unref(&packet); + continue; + } + + packet.stream_index = streams_list[packet.stream_index]; + out_stream = output_format_context->streams[packet.stream_index]; + + if (in_stream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) { + ret = avcodec_send_packet(opus_decoder_context, &packet); + if (ret < 0) { + fprintf(stderr, "Error sending packet to decoder\n"); + av_packet_unref(&packet); + continue; + } + + AVFrame *frame = av_frame_alloc(); + ret = avcodec_receive_frame(opus_decoder_context, frame); + if (ret < 0 && ret != AVERROR(EAGAIN) && ret != AVERROR_EOF) { + fprintf(stderr, "Error receiving frame from decoder\n"); + av_frame_free(&frame); + av_packet_unref(&packet); + continue; + } + + if (ret >= 0) { + frame->pts = frame->best_effort_timestamp; + + AVFrame *resampled_frame = av_frame_alloc(); + resampled_frame->channel_layout = aac_codec_context->channel_layout; + resampled_frame->sample_rate = aac_codec_context->sample_rate; + resampled_frame->format = aac_codec_context->sample_fmt; + resampled_frame->nb_samples = aac_codec_context->frame_size; + + if ((ret = av_frame_get_buffer(resampled_frame, 0)) < 0) { + fprintf(stderr, "Could not allocate resampled frame buffer\n"); + av_frame_free(&resampled_frame); + av_frame_free(&frame); + av_packet_unref(&packet); + continue; + } + + memset(resampled_frame->data[0], 0, resampled_frame->nb_samples * 2 * 2); + //arc4random_buf(resampled_frame->data[0], resampled_frame->nb_samples * 2 * 2); + //memset(frame->data[0], 0, frame->nb_samples * 2 * 2); + + if ((ret = swr_convert(swr_ctx, + resampled_frame->data, resampled_frame->nb_samples, + (const uint8_t **)frame->data, frame->nb_samples)) < 0) { + fprintf(stderr, "Error while converting\n"); + av_frame_free(&resampled_frame); + av_frame_free(&frame); + av_packet_unref(&packet); + continue; + } + + resampled_frame->pts = av_rescale_q(frame->pts, opus_decoder_context->time_base, aac_codec_context->time_base); + + ret = avcodec_send_frame(aac_codec_context, resampled_frame); + if (ret < 0) { + fprintf(stderr, "Error sending frame to encoder\n"); + av_frame_free(&resampled_frame); + av_frame_free(&frame); + av_packet_unref(&packet); + continue; + } + + AVPacket out_packet; + av_init_packet(&out_packet); + out_packet.data = NULL; + out_packet.size = 0; + + ret = avcodec_receive_packet(aac_codec_context, &out_packet); + if (ret >= 0) { + out_packet.pts = av_rescale_q_rnd(packet.pts, in_stream->time_base, out_stream->time_base, AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX); + out_packet.dts = av_rescale_q_rnd(packet.dts, in_stream->time_base, out_stream->time_base, AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX); + out_packet.pts += (int64_t)(offsetSeconds * out_stream->time_base.den); + out_packet.dts += (int64_t)(offsetSeconds * out_stream->time_base.den); + out_packet.duration = av_rescale_q(out_packet.duration, aac_codec_context->time_base, out_stream->time_base); + out_packet.stream_index = packet.stream_index; + + ret = av_interleaved_write_frame(output_format_context, &out_packet); + if (ret < 0) { + fprintf(stderr, "Error muxing packet\n"); + av_packet_unref(&out_packet); + av_frame_free(&resampled_frame); + av_frame_free(&frame); + av_packet_unref(&packet); + break; + } + av_packet_unref(&out_packet); + } + av_frame_free(&resampled_frame); + av_frame_free(&frame); + } + } else { + packet.pts = av_rescale_q_rnd(packet.pts, in_stream->time_base, out_stream->time_base, AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX); + packet.dts = av_rescale_q_rnd(packet.dts, in_stream->time_base, out_stream->time_base, AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX); + packet.pts += (int64_t)(offsetSeconds * out_stream->time_base.den); + packet.dts += (int64_t)(offsetSeconds * out_stream->time_base.den); + packet.duration = av_rescale_q(packet.duration, in_stream->time_base, out_stream->time_base); + packet.pos = -1; + + ret = av_interleaved_write_frame(output_format_context, &packet); + if (ret < 0) { + fprintf(stderr, "Error muxing packet\n"); + av_packet_unref(&packet); + break; + } + } + + av_packet_unref(&packet); + } + + av_write_trailer(output_format_context); + +end: + avformat_close_input(&input_format_context); + if (output_format_context && !(output_format_context->oformat->flags & AVFMT_NOFILE)) { + avio_closep(&output_format_context->pb); + } + avformat_free_context(output_format_context); + avcodec_free_context(&aac_codec_context); + avcodec_free_context(&opus_decoder_context); + av_freep(&streams_list); + if (swr_ctx) { + swr_free(&swr_ctx); + } + if (ret < 0 && ret != AVERROR_EOF) { + fprintf(stderr, "Error occurred: %s\n", av_err2str(ret)); + return false; + } + + printf("Remuxed video into %s\n", outPath.UTF8String); + return true; +} + +@end diff --git a/submodules/GalleryUI/Sources/GalleryController.swift b/submodules/GalleryUI/Sources/GalleryController.swift index 4ce047d5ce..fc27efab03 100644 --- a/submodules/GalleryUI/Sources/GalleryController.swift +++ b/submodules/GalleryUI/Sources/GalleryController.swift @@ -244,7 +244,9 @@ public func galleryItemForEntry( if file.isAnimated { content = NativeVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), imageReference: mediaImage.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), loopVideo: true, enableSound: false, tempFilePath: tempFilePath, captureProtected: captureProtected, storeAfterDownload: generateStoreAfterDownload?(message, file)) } else { - if true || (file.mimeType == "video/mpeg4" || file.mimeType == "video/mov" || file.mimeType == "video/mp4") { + if !"".isEmpty && context.sharedContext.immediateExperimentalUISettings.liveStreamV2 { + content = PlatformVideoContent(id: .message(message.id, message.stableId, file.fileId), userLocation: .peer(message.id.peerId), content: .file(.message(message: MessageReference(message), media: file)), streamVideo: streamVideos, loopVideo: loopVideos) + } else if true || (file.mimeType == "video/mpeg4" || file.mimeType == "video/mov" || file.mimeType == "video/mp4") { content = NativeVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), imageReference: mediaImage.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), streamVideo: .conservative, loopVideo: loopVideos, tempFilePath: tempFilePath, captureProtected: captureProtected, storeAfterDownload: generateStoreAfterDownload?(message, file)) } else { content = PlatformVideoContent(id: .message(message.id, message.stableId, file.fileId), userLocation: .peer(message.id.peerId), content: .file(.message(message: MessageReference(message), media: file)), streamVideo: streamVideos, loopVideo: loopVideos) diff --git a/submodules/TelegramCallsUI/BUILD b/submodules/TelegramCallsUI/BUILD index f18dd62a2f..f08e7ed740 100644 --- a/submodules/TelegramCallsUI/BUILD +++ b/submodules/TelegramCallsUI/BUILD @@ -111,6 +111,8 @@ swift_library( "//submodules/ImageBlur", "//submodules/MetalEngine", "//submodules/TelegramUI/Components/Calls/VoiceChatActionButton", + "//submodules/FFMpegBinding", + "//submodules/TelegramCallsUI/CHTTPParser", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramCallsUI/CHTTPParser/BUILD b/submodules/TelegramCallsUI/CHTTPParser/BUILD new file mode 100644 index 0000000000..34d5bc481d --- /dev/null +++ b/submodules/TelegramCallsUI/CHTTPParser/BUILD @@ -0,0 +1,25 @@ + +objc_library( + name = "CHTTPParser", + enable_modules = True, + module_name = "CHTTPParser", + srcs = glob([ + "Sources/**/*.c", + "Sources/**/*.h", + ]), + hdrs = glob([ + "PublicHeaders/**/*.h", + ]), + includes = [ + "PublicHeaders", + ], + copts = [ + "-I{}/PublicHeaders/CHTTPParser".format(package_name()), + ], + sdk_frameworks = [ + "Foundation", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramCallsUI/CHTTPParser/PublicHeaders/CHTTPParser/CHTTPParser.h b/submodules/TelegramCallsUI/CHTTPParser/PublicHeaders/CHTTPParser/CHTTPParser.h new file mode 100644 index 0000000000..df8825260d --- /dev/null +++ b/submodules/TelegramCallsUI/CHTTPParser/PublicHeaders/CHTTPParser/CHTTPParser.h @@ -0,0 +1,443 @@ +/* Copyright Joyent, Inc. and other Node contributors. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ +#ifndef http_parser_h +#define http_parser_h +#ifdef __cplusplus +extern "C" { +#endif + +/* Also update SONAME in the Makefile whenever you change these. */ +#define HTTP_PARSER_VERSION_MAJOR 2 +#define HTTP_PARSER_VERSION_MINOR 9 +#define HTTP_PARSER_VERSION_PATCH 4 + +#include +#if defined(_WIN32) && !defined(__MINGW32__) && \ + (!defined(_MSC_VER) || _MSC_VER<1600) && !defined(__WINE__) +#include +typedef __int8 int8_t; +typedef unsigned __int8 uint8_t; +typedef __int16 int16_t; +typedef unsigned __int16 uint16_t; +typedef __int32 int32_t; +typedef unsigned __int32 uint32_t; +typedef __int64 int64_t; +typedef unsigned __int64 uint64_t; +#else +#include +#endif + +/* Compile with -DHTTP_PARSER_STRICT=0 to make less checks, but run + * faster + */ +#ifndef HTTP_PARSER_STRICT +# define HTTP_PARSER_STRICT 1 +#endif + +/* Maximium header size allowed. If the macro is not defined + * before including this header then the default is used. To + * change the maximum header size, define the macro in the build + * environment (e.g. -DHTTP_MAX_HEADER_SIZE=). To remove + * the effective limit on the size of the header, define the macro + * to a very large number (e.g. -DHTTP_MAX_HEADER_SIZE=0x7fffffff) + */ +#ifndef HTTP_MAX_HEADER_SIZE +# define HTTP_MAX_HEADER_SIZE (80*1024) +#endif + +typedef struct http_parser http_parser; +typedef struct http_parser_settings http_parser_settings; + + +/* Callbacks should return non-zero to indicate an error. The parser will + * then halt execution. + * + * The one exception is on_headers_complete. In a HTTP_RESPONSE parser + * returning '1' from on_headers_complete will tell the parser that it + * should not expect a body. This is used when receiving a response to a + * HEAD request which may contain 'Content-Length' or 'Transfer-Encoding: + * chunked' headers that indicate the presence of a body. + * + * Returning `2` from on_headers_complete will tell parser that it should not + * expect neither a body nor any futher responses on this connection. This is + * useful for handling responses to a CONNECT request which may not contain + * `Upgrade` or `Connection: upgrade` headers. + * + * http_data_cb does not return data chunks. It will be called arbitrarily + * many times for each string. E.G. you might get 10 callbacks for "on_url" + * each providing just a few characters more data. + */ +typedef int (*http_data_cb) (http_parser*, const char *at, size_t length); +typedef int (*http_cb) (http_parser*); + + +/* Status Codes */ +#define HTTP_STATUS_MAP(XX) \ + XX(100, CONTINUE, Continue) \ + XX(101, SWITCHING_PROTOCOLS, Switching Protocols) \ + XX(102, PROCESSING, Processing) \ + XX(200, OK, OK) \ + XX(201, CREATED, Created) \ + XX(202, ACCEPTED, Accepted) \ + XX(203, NON_AUTHORITATIVE_INFORMATION, Non-Authoritative Information) \ + XX(204, NO_CONTENT, No Content) \ + XX(205, RESET_CONTENT, Reset Content) \ + XX(206, PARTIAL_CONTENT, Partial Content) \ + XX(207, MULTI_STATUS, Multi-Status) \ + XX(208, ALREADY_REPORTED, Already Reported) \ + XX(226, IM_USED, IM Used) \ + XX(300, MULTIPLE_CHOICES, Multiple Choices) \ + XX(301, MOVED_PERMANENTLY, Moved Permanently) \ + XX(302, FOUND, Found) \ + XX(303, SEE_OTHER, See Other) \ + XX(304, NOT_MODIFIED, Not Modified) \ + XX(305, USE_PROXY, Use Proxy) \ + XX(307, TEMPORARY_REDIRECT, Temporary Redirect) \ + XX(308, PERMANENT_REDIRECT, Permanent Redirect) \ + XX(400, BAD_REQUEST, Bad Request) \ + XX(401, UNAUTHORIZED, Unauthorized) \ + XX(402, PAYMENT_REQUIRED, Payment Required) \ + XX(403, FORBIDDEN, Forbidden) \ + XX(404, NOT_FOUND, Not Found) \ + XX(405, METHOD_NOT_ALLOWED, Method Not Allowed) \ + XX(406, NOT_ACCEPTABLE, Not Acceptable) \ + XX(407, PROXY_AUTHENTICATION_REQUIRED, Proxy Authentication Required) \ + XX(408, REQUEST_TIMEOUT, Request Timeout) \ + XX(409, CONFLICT, Conflict) \ + XX(410, GONE, Gone) \ + XX(411, LENGTH_REQUIRED, Length Required) \ + XX(412, PRECONDITION_FAILED, Precondition Failed) \ + XX(413, PAYLOAD_TOO_LARGE, Payload Too Large) \ + XX(414, URI_TOO_LONG, URI Too Long) \ + XX(415, UNSUPPORTED_MEDIA_TYPE, Unsupported Media Type) \ + XX(416, RANGE_NOT_SATISFIABLE, Range Not Satisfiable) \ + XX(417, EXPECTATION_FAILED, Expectation Failed) \ + XX(421, MISDIRECTED_REQUEST, Misdirected Request) \ + XX(422, UNPROCESSABLE_ENTITY, Unprocessable Entity) \ + XX(423, LOCKED, Locked) \ + XX(424, FAILED_DEPENDENCY, Failed Dependency) \ + XX(426, UPGRADE_REQUIRED, Upgrade Required) \ + XX(428, PRECONDITION_REQUIRED, Precondition Required) \ + XX(429, TOO_MANY_REQUESTS, Too Many Requests) \ + XX(431, REQUEST_HEADER_FIELDS_TOO_LARGE, Request Header Fields Too Large) \ + XX(451, UNAVAILABLE_FOR_LEGAL_REASONS, Unavailable For Legal Reasons) \ + XX(500, INTERNAL_SERVER_ERROR, Internal Server Error) \ + XX(501, NOT_IMPLEMENTED, Not Implemented) \ + XX(502, BAD_GATEWAY, Bad Gateway) \ + XX(503, SERVICE_UNAVAILABLE, Service Unavailable) \ + XX(504, GATEWAY_TIMEOUT, Gateway Timeout) \ + XX(505, HTTP_VERSION_NOT_SUPPORTED, HTTP Version Not Supported) \ + XX(506, VARIANT_ALSO_NEGOTIATES, Variant Also Negotiates) \ + XX(507, INSUFFICIENT_STORAGE, Insufficient Storage) \ + XX(508, LOOP_DETECTED, Loop Detected) \ + XX(510, NOT_EXTENDED, Not Extended) \ + XX(511, NETWORK_AUTHENTICATION_REQUIRED, Network Authentication Required) \ + +enum http_status + { +#define XX(num, name, string) HTTP_STATUS_##name = num, + HTTP_STATUS_MAP(XX) +#undef XX + }; + + +/* Request Methods */ +#define HTTP_METHOD_MAP(XX) \ + XX(0, DELETE, DELETE) \ + XX(1, GET, GET) \ + XX(2, HEAD, HEAD) \ + XX(3, POST, POST) \ + XX(4, PUT, PUT) \ + /* pathological */ \ + XX(5, CONNECT, CONNECT) \ + XX(6, OPTIONS, OPTIONS) \ + XX(7, TRACE, TRACE) \ + /* WebDAV */ \ + XX(8, COPY, COPY) \ + XX(9, LOCK, LOCK) \ + XX(10, MKCOL, MKCOL) \ + XX(11, MOVE, MOVE) \ + XX(12, PROPFIND, PROPFIND) \ + XX(13, PROPPATCH, PROPPATCH) \ + XX(14, SEARCH, SEARCH) \ + XX(15, UNLOCK, UNLOCK) \ + XX(16, BIND, BIND) \ + XX(17, REBIND, REBIND) \ + XX(18, UNBIND, UNBIND) \ + XX(19, ACL, ACL) \ + /* subversion */ \ + XX(20, REPORT, REPORT) \ + XX(21, MKACTIVITY, MKACTIVITY) \ + XX(22, CHECKOUT, CHECKOUT) \ + XX(23, MERGE, MERGE) \ + /* upnp */ \ + XX(24, MSEARCH, M-SEARCH) \ + XX(25, NOTIFY, NOTIFY) \ + XX(26, SUBSCRIBE, SUBSCRIBE) \ + XX(27, UNSUBSCRIBE, UNSUBSCRIBE) \ + /* RFC-5789 */ \ + XX(28, PATCH, PATCH) \ + XX(29, PURGE, PURGE) \ + /* CalDAV */ \ + XX(30, MKCALENDAR, MKCALENDAR) \ + /* RFC-2068, section 19.6.1.2 */ \ + XX(31, LINK, LINK) \ + XX(32, UNLINK, UNLINK) \ + /* icecast */ \ + XX(33, SOURCE, SOURCE) \ + +enum http_method + { +#define XX(num, name, string) HTTP_##name = num, + HTTP_METHOD_MAP(XX) +#undef XX + }; + + +enum http_parser_type { HTTP_REQUEST, HTTP_RESPONSE, HTTP_BOTH }; + + +/* Flag values for http_parser.flags field */ +enum flags + { F_CHUNKED = 1 << 0 + , F_CONNECTION_KEEP_ALIVE = 1 << 1 + , F_CONNECTION_CLOSE = 1 << 2 + , F_CONNECTION_UPGRADE = 1 << 3 + , F_TRAILING = 1 << 4 + , F_UPGRADE = 1 << 5 + , F_SKIPBODY = 1 << 6 + , F_CONTENTLENGTH = 1 << 7 + , F_TRANSFER_ENCODING = 1 << 8 /* Never set in http_parser.flags */ + }; + + +/* Map for errno-related constants + * + * The provided argument should be a macro that takes 2 arguments. + */ +#define HTTP_ERRNO_MAP(XX) \ + /* No error */ \ + XX(OK, "success") \ + \ + /* Callback-related errors */ \ + XX(CB_message_begin, "the on_message_begin callback failed") \ + XX(CB_url, "the on_url callback failed") \ + XX(CB_header_field, "the on_header_field callback failed") \ + XX(CB_header_value, "the on_header_value callback failed") \ + XX(CB_headers_complete, "the on_headers_complete callback failed") \ + XX(CB_body, "the on_body callback failed") \ + XX(CB_message_complete, "the on_message_complete callback failed") \ + XX(CB_status, "the on_status callback failed") \ + XX(CB_chunk_header, "the on_chunk_header callback failed") \ + XX(CB_chunk_complete, "the on_chunk_complete callback failed") \ + \ + /* Parsing-related errors */ \ + XX(INVALID_EOF_STATE, "stream ended at an unexpected time") \ + XX(HEADER_OVERFLOW, \ + "too many header bytes seen; overflow detected") \ + XX(CLOSED_CONNECTION, \ + "data received after completed connection: close message") \ + XX(INVALID_VERSION, "invalid HTTP version") \ + XX(INVALID_STATUS, "invalid HTTP status code") \ + XX(INVALID_METHOD, "invalid HTTP method") \ + XX(INVALID_URL, "invalid URL") \ + XX(INVALID_HOST, "invalid host") \ + XX(INVALID_PORT, "invalid port") \ + XX(INVALID_PATH, "invalid path") \ + XX(INVALID_QUERY_STRING, "invalid query string") \ + XX(INVALID_FRAGMENT, "invalid fragment") \ + XX(LF_EXPECTED, "LF character expected") \ + XX(INVALID_HEADER_TOKEN, "invalid character in header") \ + XX(INVALID_CONTENT_LENGTH, \ + "invalid character in content-length header") \ + XX(UNEXPECTED_CONTENT_LENGTH, \ + "unexpected content-length header") \ + XX(INVALID_CHUNK_SIZE, \ + "invalid character in chunk size header") \ + XX(INVALID_CONSTANT, "invalid constant string") \ + XX(INVALID_INTERNAL_STATE, "encountered unexpected internal state")\ + XX(STRICT, "strict mode assertion failed") \ + XX(PAUSED, "parser is paused") \ + XX(UNKNOWN, "an unknown error occurred") \ + XX(INVALID_TRANSFER_ENCODING, \ + "request has invalid transfer-encoding") \ + + +/* Define HPE_* values for each errno value above */ +#define HTTP_ERRNO_GEN(n, s) HPE_##n, +enum http_errno { + HTTP_ERRNO_MAP(HTTP_ERRNO_GEN) +}; +#undef HTTP_ERRNO_GEN + + +/* Get an http_errno value from an http_parser */ +#define HTTP_PARSER_ERRNO(p) ((enum http_errno) (p)->http_errno) + + +struct http_parser { + /** PRIVATE **/ + unsigned int type : 2; /* enum http_parser_type */ + unsigned int flags : 8; /* F_* values from 'flags' enum; semi-public */ + unsigned int state : 7; /* enum state from http_parser.c */ + unsigned int header_state : 7; /* enum header_state from http_parser.c */ + unsigned int index : 5; /* index into current matcher */ + unsigned int extra_flags : 2; + unsigned int lenient_http_headers : 1; + + uint32_t nread; /* # bytes read in various scenarios */ + uint64_t content_length; /* # bytes in body (0 if no Content-Length header) */ + + /** READ-ONLY **/ + unsigned short http_major; + unsigned short http_minor; + unsigned int status_code : 16; /* responses only */ + unsigned int method : 8; /* requests only */ + unsigned int http_errno : 7; + + /* 1 = Upgrade header was present and the parser has exited because of that. + * 0 = No upgrade header present. + * Should be checked when http_parser_execute() returns in addition to + * error checking. + */ + unsigned int upgrade : 1; + + /** PUBLIC **/ + void *data; /* A pointer to get hook to the "connection" or "socket" object */ +}; + + +struct http_parser_settings { + http_cb on_message_begin; + http_data_cb on_url; + http_data_cb on_status; + http_data_cb on_header_field; + http_data_cb on_header_value; + http_cb on_headers_complete; + http_data_cb on_body; + http_cb on_message_complete; + /* When on_chunk_header is called, the current chunk length is stored + * in parser->content_length. + */ + http_cb on_chunk_header; + http_cb on_chunk_complete; +}; + + +enum http_parser_url_fields + { UF_SCHEMA = 0 + , UF_HOST = 1 + , UF_PORT = 2 + , UF_PATH = 3 + , UF_QUERY = 4 + , UF_FRAGMENT = 5 + , UF_USERINFO = 6 + , UF_MAX = 7 + }; + + +/* Result structure for http_parser_parse_url(). + * + * Callers should index into field_data[] with UF_* values iff field_set + * has the relevant (1 << UF_*) bit set. As a courtesy to clients (and + * because we probably have padding left over), we convert any port to + * a uint16_t. + */ +struct http_parser_url { + uint16_t field_set; /* Bitmask of (1 << UF_*) values */ + uint16_t port; /* Converted UF_PORT string */ + + struct { + uint16_t off; /* Offset into buffer in which field starts */ + uint16_t len; /* Length of run in buffer */ + } field_data[UF_MAX]; +}; + + +/* Returns the library version. Bits 16-23 contain the major version number, + * bits 8-15 the minor version number and bits 0-7 the patch level. + * Usage example: + * + * unsigned long version = http_parser_version(); + * unsigned major = (version >> 16) & 255; + * unsigned minor = (version >> 8) & 255; + * unsigned patch = version & 255; + * printf("http_parser v%u.%u.%u\n", major, minor, patch); + */ +unsigned long http_parser_version(void); + +void http_parser_init(http_parser *parser, enum http_parser_type type); + + +/* Initialize http_parser_settings members to 0 + */ +void http_parser_settings_init(http_parser_settings *settings); + + +/* Executes the parser. Returns number of parsed bytes. Sets + * `parser->http_errno` on error. */ +size_t http_parser_execute(http_parser *parser, + const http_parser_settings *settings, + const char *data, + size_t len); + + +/* If http_should_keep_alive() in the on_headers_complete or + * on_message_complete callback returns 0, then this should be + * the last message on the connection. + * If you are the server, respond with the "Connection: close" header. + * If you are the client, close the connection. + */ +int http_should_keep_alive(const http_parser *parser); + +/* Returns a string version of the HTTP method. */ +const char *http_method_str(enum http_method m); + +/* Returns a string version of the HTTP status code. */ +const char *http_status_str(enum http_status s); + +/* Return a string name of the given error */ +const char *http_errno_name(enum http_errno err); + +/* Return a string description of the given error */ +const char *http_errno_description(enum http_errno err); + +/* Initialize all http_parser_url members to 0 */ +void http_parser_url_init(struct http_parser_url *u); + +/* Parse a URL; return nonzero on failure */ +int http_parser_parse_url(const char *buf, size_t buflen, + int is_connect, + struct http_parser_url *u); + +/* Pause or un-pause the parser; a nonzero value pauses */ +void http_parser_pause(http_parser *parser, int paused); + +/* Checks if this is the final chunk of the body. */ +int http_body_is_final(const http_parser *parser); + +/* Change the maximum header size provided at compile time. */ +void http_parser_set_max_header_size(uint32_t size); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/submodules/TelegramCallsUI/CHTTPParser/Sources/CHTTPParser.c b/submodules/TelegramCallsUI/CHTTPParser/Sources/CHTTPParser.c new file mode 100644 index 0000000000..73971029ad --- /dev/null +++ b/submodules/TelegramCallsUI/CHTTPParser/Sources/CHTTPParser.c @@ -0,0 +1,2568 @@ +/* Copyright Joyent, Inc. and other Node contributors. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ +#include +#include +#include +#include +#include +#include + +static uint32_t max_header_size = HTTP_MAX_HEADER_SIZE; + +#ifndef ULLONG_MAX +# define ULLONG_MAX ((uint64_t) -1) /* 2^64-1 */ +#endif + +#ifndef MIN +# define MIN(a,b) ((a) < (b) ? (a) : (b)) +#endif + +#ifndef ARRAY_SIZE +# define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0])) +#endif + +#ifndef BIT_AT +# define BIT_AT(a, i) \ + (!!((unsigned int) (a)[(unsigned int) (i) >> 3] & \ + (1 << ((unsigned int) (i) & 7)))) +#endif + +#ifndef ELEM_AT +# define ELEM_AT(a, i, v) ((unsigned int) (i) < ARRAY_SIZE(a) ? (a)[(i)] : (v)) +#endif + +#define SET_ERRNO(e) \ +do { \ + parser->nread = nread; \ + parser->http_errno = (e); \ +} while(0) + +#define CURRENT_STATE() p_state +#define UPDATE_STATE(V) p_state = (enum state) (V); +#define RETURN(V) \ +do { \ + parser->nread = nread; \ + parser->state = CURRENT_STATE(); \ + return (V); \ +} while (0); +#define REEXECUTE() \ + goto reexecute; \ + + +#ifdef __GNUC__ +# define LIKELY(X) __builtin_expect(!!(X), 1) +# define UNLIKELY(X) __builtin_expect(!!(X), 0) +#else +# define LIKELY(X) (X) +# define UNLIKELY(X) (X) +#endif + + +/* Run the notify callback FOR, returning ER if it fails */ +#define CALLBACK_NOTIFY_(FOR, ER) \ +do { \ + assert(HTTP_PARSER_ERRNO(parser) == HPE_OK); \ + \ + if (LIKELY(settings->on_##FOR)) { \ + parser->state = CURRENT_STATE(); \ + if (UNLIKELY(0 != settings->on_##FOR(parser))) { \ + SET_ERRNO(HPE_CB_##FOR); \ + } \ + UPDATE_STATE(parser->state); \ + \ + /* We either errored above or got paused; get out */ \ + if (UNLIKELY(HTTP_PARSER_ERRNO(parser) != HPE_OK)) { \ + return (ER); \ + } \ + } \ +} while (0) + +/* Run the notify callback FOR and consume the current byte */ +#define CALLBACK_NOTIFY(FOR) CALLBACK_NOTIFY_(FOR, p - data + 1) + +/* Run the notify callback FOR and don't consume the current byte */ +#define CALLBACK_NOTIFY_NOADVANCE(FOR) CALLBACK_NOTIFY_(FOR, p - data) + +/* Run data callback FOR with LEN bytes, returning ER if it fails */ +#define CALLBACK_DATA_(FOR, LEN, ER) \ +do { \ + assert(HTTP_PARSER_ERRNO(parser) == HPE_OK); \ + \ + if (FOR##_mark) { \ + if (LIKELY(settings->on_##FOR)) { \ + parser->state = CURRENT_STATE(); \ + if (UNLIKELY(0 != \ + settings->on_##FOR(parser, FOR##_mark, (LEN)))) { \ + SET_ERRNO(HPE_CB_##FOR); \ + } \ + UPDATE_STATE(parser->state); \ + \ + /* We either errored above or got paused; get out */ \ + if (UNLIKELY(HTTP_PARSER_ERRNO(parser) != HPE_OK)) { \ + return (ER); \ + } \ + } \ + FOR##_mark = NULL; \ + } \ +} while (0) + +/* Run the data callback FOR and consume the current byte */ +#define CALLBACK_DATA(FOR) \ + CALLBACK_DATA_(FOR, p - FOR##_mark, p - data + 1) + +/* Run the data callback FOR and don't consume the current byte */ +#define CALLBACK_DATA_NOADVANCE(FOR) \ + CALLBACK_DATA_(FOR, p - FOR##_mark, p - data) + +/* Set the mark FOR; non-destructive if mark is already set */ +#define MARK(FOR) \ +do { \ + if (!FOR##_mark) { \ + FOR##_mark = p; \ + } \ +} while (0) + +/* Don't allow the total size of the HTTP headers (including the status + * line) to exceed max_header_size. This check is here to protect + * embedders against denial-of-service attacks where the attacker feeds + * us a never-ending header that the embedder keeps buffering. + * + * This check is arguably the responsibility of embedders but we're doing + * it on the embedder's behalf because most won't bother and this way we + * make the web a little safer. max_header_size is still far bigger + * than any reasonable request or response so this should never affect + * day-to-day operation. + */ +#define COUNT_HEADER_SIZE(V) \ +do { \ + nread += (uint32_t)(V); \ + if (UNLIKELY(nread > max_header_size)) { \ + SET_ERRNO(HPE_HEADER_OVERFLOW); \ + goto error; \ + } \ +} while (0) + + +#define PROXY_CONNECTION "proxy-connection" +#define CONNECTION "connection" +#define CONTENT_LENGTH "content-length" +#define TRANSFER_ENCODING "transfer-encoding" +#define UPGRADE "upgrade" +#define CHUNKED "chunked" +#define KEEP_ALIVE "keep-alive" +#define CLOSE "close" + + +static const char *method_strings[] = + { +#define XX(num, name, string) #string, + HTTP_METHOD_MAP(XX) +#undef XX + }; + + +/* Tokens as defined by rfc 2616. Also lowercases them. + * token = 1* + * separators = "(" | ")" | "<" | ">" | "@" + * | "," | ";" | ":" | "\" | <"> + * | "/" | "[" | "]" | "?" | "=" + * | "{" | "}" | SP | HT + */ +static const char tokens[256] = { +/* 0 nul 1 soh 2 stx 3 etx 4 eot 5 enq 6 ack 7 bel */ + 0, 0, 0, 0, 0, 0, 0, 0, +/* 8 bs 9 ht 10 nl 11 vt 12 np 13 cr 14 so 15 si */ + 0, 0, 0, 0, 0, 0, 0, 0, +/* 16 dle 17 dc1 18 dc2 19 dc3 20 dc4 21 nak 22 syn 23 etb */ + 0, 0, 0, 0, 0, 0, 0, 0, +/* 24 can 25 em 26 sub 27 esc 28 fs 29 gs 30 rs 31 us */ + 0, 0, 0, 0, 0, 0, 0, 0, +/* 32 sp 33 ! 34 " 35 # 36 $ 37 % 38 & 39 ' */ + ' ', '!', 0, '#', '$', '%', '&', '\'', +/* 40 ( 41 ) 42 * 43 + 44 , 45 - 46 . 47 / */ + 0, 0, '*', '+', 0, '-', '.', 0, +/* 48 0 49 1 50 2 51 3 52 4 53 5 54 6 55 7 */ + '0', '1', '2', '3', '4', '5', '6', '7', +/* 56 8 57 9 58 : 59 ; 60 < 61 = 62 > 63 ? */ + '8', '9', 0, 0, 0, 0, 0, 0, +/* 64 @ 65 A 66 B 67 C 68 D 69 E 70 F 71 G */ + 0, 'a', 'b', 'c', 'd', 'e', 'f', 'g', +/* 72 H 73 I 74 J 75 K 76 L 77 M 78 N 79 O */ + 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', +/* 80 P 81 Q 82 R 83 S 84 T 85 U 86 V 87 W */ + 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', +/* 88 X 89 Y 90 Z 91 [ 92 \ 93 ] 94 ^ 95 _ */ + 'x', 'y', 'z', 0, 0, 0, '^', '_', +/* 96 ` 97 a 98 b 99 c 100 d 101 e 102 f 103 g */ + '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', +/* 104 h 105 i 106 j 107 k 108 l 109 m 110 n 111 o */ + 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', +/* 112 p 113 q 114 r 115 s 116 t 117 u 118 v 119 w */ + 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', +/* 120 x 121 y 122 z 123 { 124 | 125 } 126 ~ 127 del */ + 'x', 'y', 'z', 0, '|', 0, '~', 0 }; + + +static const int8_t unhex[256] = + {-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 + ,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 + ,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 + , 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,-1,-1,-1,-1,-1,-1 + ,-1,10,11,12,13,14,15,-1,-1,-1,-1,-1,-1,-1,-1,-1 + ,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 + ,-1,10,11,12,13,14,15,-1,-1,-1,-1,-1,-1,-1,-1,-1 + ,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 + }; + + +#if HTTP_PARSER_STRICT +# define T(v) 0 +#else +# define T(v) v +#endif + + +static const uint8_t normal_url_char[32] = { +/* 0 nul 1 soh 2 stx 3 etx 4 eot 5 enq 6 ack 7 bel */ + 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0, +/* 8 bs 9 ht 10 nl 11 vt 12 np 13 cr 14 so 15 si */ + 0 | T(2) | 0 | 0 | T(16) | 0 | 0 | 0, +/* 16 dle 17 dc1 18 dc2 19 dc3 20 dc4 21 nak 22 syn 23 etb */ + 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0, +/* 24 can 25 em 26 sub 27 esc 28 fs 29 gs 30 rs 31 us */ + 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0, +/* 32 sp 33 ! 34 " 35 # 36 $ 37 % 38 & 39 ' */ + 0 | 2 | 4 | 0 | 16 | 32 | 64 | 128, +/* 40 ( 41 ) 42 * 43 + 44 , 45 - 46 . 47 / */ + 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, +/* 48 0 49 1 50 2 51 3 52 4 53 5 54 6 55 7 */ + 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, +/* 56 8 57 9 58 : 59 ; 60 < 61 = 62 > 63 ? */ + 1 | 2 | 4 | 8 | 16 | 32 | 64 | 0, +/* 64 @ 65 A 66 B 67 C 68 D 69 E 70 F 71 G */ + 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, +/* 72 H 73 I 74 J 75 K 76 L 77 M 78 N 79 O */ + 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, +/* 80 P 81 Q 82 R 83 S 84 T 85 U 86 V 87 W */ + 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, +/* 88 X 89 Y 90 Z 91 [ 92 \ 93 ] 94 ^ 95 _ */ + 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, +/* 96 ` 97 a 98 b 99 c 100 d 101 e 102 f 103 g */ + 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, +/* 104 h 105 i 106 j 107 k 108 l 109 m 110 n 111 o */ + 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, +/* 112 p 113 q 114 r 115 s 116 t 117 u 118 v 119 w */ + 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, +/* 120 x 121 y 122 z 123 { 124 | 125 } 126 ~ 127 del */ + 1 | 2 | 4 | 8 | 16 | 32 | 64 | 0, }; + +#undef T + +enum state + { s_dead = 1 /* important that this is > 0 */ + + , s_start_req_or_res + , s_res_or_resp_H + , s_start_res + , s_res_H + , s_res_HT + , s_res_HTT + , s_res_HTTP + , s_res_http_major + , s_res_http_dot + , s_res_http_minor + , s_res_http_end + , s_res_first_status_code + , s_res_status_code + , s_res_status_start + , s_res_status + , s_res_line_almost_done + + , s_start_req + + , s_req_method + , s_req_spaces_before_url + , s_req_schema + , s_req_schema_slash + , s_req_schema_slash_slash + , s_req_server_start + , s_req_server + , s_req_server_with_at + , s_req_path + , s_req_query_string_start + , s_req_query_string + , s_req_fragment_start + , s_req_fragment + , s_req_http_start + , s_req_http_H + , s_req_http_HT + , s_req_http_HTT + , s_req_http_HTTP + , s_req_http_I + , s_req_http_IC + , s_req_http_major + , s_req_http_dot + , s_req_http_minor + , s_req_http_end + , s_req_line_almost_done + + , s_header_field_start + , s_header_field + , s_header_value_discard_ws + , s_header_value_discard_ws_almost_done + , s_header_value_discard_lws + , s_header_value_start + , s_header_value + , s_header_value_lws + + , s_header_almost_done + + , s_chunk_size_start + , s_chunk_size + , s_chunk_parameters + , s_chunk_size_almost_done + + , s_headers_almost_done + , s_headers_done + + /* Important: 's_headers_done' must be the last 'header' state. All + * states beyond this must be 'body' states. It is used for overflow + * checking. See the PARSING_HEADER() macro. + */ + + , s_chunk_data + , s_chunk_data_almost_done + , s_chunk_data_done + + , s_body_identity + , s_body_identity_eof + + , s_message_done + }; + + +#define PARSING_HEADER(state) (state <= s_headers_done) + + +enum header_states + { h_general = 0 + , h_C + , h_CO + , h_CON + + , h_matching_connection + , h_matching_proxy_connection + , h_matching_content_length + , h_matching_transfer_encoding + , h_matching_upgrade + + , h_connection + , h_content_length + , h_content_length_num + , h_content_length_ws + , h_transfer_encoding + , h_upgrade + + , h_matching_transfer_encoding_token_start + , h_matching_transfer_encoding_chunked + , h_matching_transfer_encoding_token + + , h_matching_connection_token_start + , h_matching_connection_keep_alive + , h_matching_connection_close + , h_matching_connection_upgrade + , h_matching_connection_token + + , h_transfer_encoding_chunked + , h_connection_keep_alive + , h_connection_close + , h_connection_upgrade + }; + +enum http_host_state + { + s_http_host_dead = 1 + , s_http_userinfo_start + , s_http_userinfo + , s_http_host_start + , s_http_host_v6_start + , s_http_host + , s_http_host_v6 + , s_http_host_v6_end + , s_http_host_v6_zone_start + , s_http_host_v6_zone + , s_http_host_port_start + , s_http_host_port +}; + +/* Macros for character classes; depends on strict-mode */ +#define CR '\r' +#define LF '\n' +#define LOWER(c) (unsigned char)(c | 0x20) +#define IS_ALPHA(c) (LOWER(c) >= 'a' && LOWER(c) <= 'z') +#define IS_NUM(c) ((c) >= '0' && (c) <= '9') +#define IS_ALPHANUM(c) (IS_ALPHA(c) || IS_NUM(c)) +#define IS_HEX(c) (IS_NUM(c) || (LOWER(c) >= 'a' && LOWER(c) <= 'f')) +#define IS_MARK(c) ((c) == '-' || (c) == '_' || (c) == '.' || \ + (c) == '!' || (c) == '~' || (c) == '*' || (c) == '\'' || (c) == '(' || \ + (c) == ')') +#define IS_USERINFO_CHAR(c) (IS_ALPHANUM(c) || IS_MARK(c) || (c) == '%' || \ + (c) == ';' || (c) == ':' || (c) == '&' || (c) == '=' || (c) == '+' || \ + (c) == '$' || (c) == ',') + +#define STRICT_TOKEN(c) ((c == ' ') ? 0 : tokens[(unsigned char)c]) + +#if HTTP_PARSER_STRICT +#define TOKEN(c) STRICT_TOKEN(c) +#define IS_URL_CHAR(c) (BIT_AT(normal_url_char, (unsigned char)c)) +#define IS_HOST_CHAR(c) (IS_ALPHANUM(c) || (c) == '.' || (c) == '-') +#else +#define TOKEN(c) tokens[(unsigned char)c] +#define IS_URL_CHAR(c) \ + (BIT_AT(normal_url_char, (unsigned char)c) || ((c) & 0x80)) +#define IS_HOST_CHAR(c) \ + (IS_ALPHANUM(c) || (c) == '.' || (c) == '-' || (c) == '_') +#endif + +/** + * Verify that a char is a valid visible (printable) US-ASCII + * character or %x80-FF + **/ +#define IS_HEADER_CHAR(ch) \ + (ch == CR || ch == LF || ch == 9 || ((unsigned char)ch > 31 && ch != 127)) + +#define start_state (parser->type == HTTP_REQUEST ? s_start_req : s_start_res) + + +#if HTTP_PARSER_STRICT +# define STRICT_CHECK(cond) \ +do { \ + if (cond) { \ + SET_ERRNO(HPE_STRICT); \ + goto error; \ + } \ +} while (0) +# define NEW_MESSAGE() (http_should_keep_alive(parser) ? start_state : s_dead) +#else +# define STRICT_CHECK(cond) +# define NEW_MESSAGE() start_state +#endif + + +/* Map errno values to strings for human-readable output */ +#define HTTP_STRERROR_GEN(n, s) { "HPE_" #n, s }, +static struct { + const char *name; + const char *description; +} http_strerror_tab[] = { + HTTP_ERRNO_MAP(HTTP_STRERROR_GEN) +}; +#undef HTTP_STRERROR_GEN + +int http_message_needs_eof(const http_parser *parser); + +/* Our URL parser. + * + * This is designed to be shared by http_parser_execute() for URL validation, + * hence it has a state transition + byte-for-byte interface. In addition, it + * is meant to be embedded in http_parser_parse_url(), which does the dirty + * work of turning state transitions URL components for its API. + * + * This function should only be invoked with non-space characters. It is + * assumed that the caller cares about (and can detect) the transition between + * URL and non-URL states by looking for these. + */ +static enum state +parse_url_char(enum state s, const char ch) +{ + if (ch == ' ' || ch == '\r' || ch == '\n') { + return s_dead; + } + +#if HTTP_PARSER_STRICT + if (ch == '\t' || ch == '\f') { + return s_dead; + } +#endif + + switch (s) { + case s_req_spaces_before_url: + /* Proxied requests are followed by scheme of an absolute URI (alpha). + * All methods except CONNECT are followed by '/' or '*'. + */ + + if (ch == '/' || ch == '*') { + return s_req_path; + } + + if (IS_ALPHA(ch)) { + return s_req_schema; + } + + break; + + case s_req_schema: + if (IS_ALPHA(ch)) { + return s; + } + + if (ch == ':') { + return s_req_schema_slash; + } + + break; + + case s_req_schema_slash: + if (ch == '/') { + return s_req_schema_slash_slash; + } + + break; + + case s_req_schema_slash_slash: + if (ch == '/') { + return s_req_server_start; + } + + break; + + case s_req_server_with_at: + if (ch == '@') { + return s_dead; + } + + /* fall through */ + case s_req_server_start: + case s_req_server: + if (ch == '/') { + return s_req_path; + } + + if (ch == '?') { + return s_req_query_string_start; + } + + if (ch == '@') { + return s_req_server_with_at; + } + + if (IS_USERINFO_CHAR(ch) || ch == '[' || ch == ']') { + return s_req_server; + } + + break; + + case s_req_path: + if (IS_URL_CHAR(ch)) { + return s; + } + + switch (ch) { + case '?': + return s_req_query_string_start; + + case '#': + return s_req_fragment_start; + } + + break; + + case s_req_query_string_start: + case s_req_query_string: + if (IS_URL_CHAR(ch)) { + return s_req_query_string; + } + + switch (ch) { + case '?': + /* allow extra '?' in query string */ + return s_req_query_string; + + case '#': + return s_req_fragment_start; + } + + break; + + case s_req_fragment_start: + if (IS_URL_CHAR(ch)) { + return s_req_fragment; + } + + switch (ch) { + case '?': + return s_req_fragment; + + case '#': + return s; + } + + break; + + case s_req_fragment: + if (IS_URL_CHAR(ch)) { + return s; + } + + switch (ch) { + case '?': + case '#': + return s; + } + + break; + + default: + break; + } + + /* We should never fall out of the switch above unless there's an error */ + return s_dead; +} + +size_t http_parser_execute (http_parser *parser, + const http_parser_settings *settings, + const char *data, + size_t len) +{ + char c, ch; + int8_t unhex_val; + const char *p = data; + const char *header_field_mark = 0; + const char *header_value_mark = 0; + const char *url_mark = 0; + const char *body_mark = 0; + const char *status_mark = 0; + enum state p_state = (enum state) parser->state; + const unsigned int lenient = parser->lenient_http_headers; + uint32_t nread = parser->nread; + + /* We're in an error state. Don't bother doing anything. */ + if (HTTP_PARSER_ERRNO(parser) != HPE_OK) { + return 0; + } + + if (len == 0) { + switch (CURRENT_STATE()) { + case s_body_identity_eof: + /* Use of CALLBACK_NOTIFY() here would erroneously return 1 byte read if + * we got paused. + */ + CALLBACK_NOTIFY_NOADVANCE(message_complete); + return 0; + + case s_dead: + case s_start_req_or_res: + case s_start_res: + case s_start_req: + return 0; + + default: + SET_ERRNO(HPE_INVALID_EOF_STATE); + return 1; + } + } + + + if (CURRENT_STATE() == s_header_field) + header_field_mark = data; + if (CURRENT_STATE() == s_header_value) + header_value_mark = data; + switch (CURRENT_STATE()) { + case s_req_path: + case s_req_schema: + case s_req_schema_slash: + case s_req_schema_slash_slash: + case s_req_server_start: + case s_req_server: + case s_req_server_with_at: + case s_req_query_string_start: + case s_req_query_string: + case s_req_fragment_start: + case s_req_fragment: + url_mark = data; + break; + case s_res_status: + status_mark = data; + break; + default: + break; + } + + for (p=data; p != data + len; p++) { + ch = *p; + + if (PARSING_HEADER(CURRENT_STATE())) + COUNT_HEADER_SIZE(1); + +reexecute: + switch (CURRENT_STATE()) { + + case s_dead: + /* this state is used after a 'Connection: close' message + * the parser will error out if it reads another message + */ + if (LIKELY(ch == CR || ch == LF)) + break; + + SET_ERRNO(HPE_CLOSED_CONNECTION); + goto error; + + case s_start_req_or_res: + { + if (ch == CR || ch == LF) + break; + parser->flags = 0; + parser->extra_flags = 0; + parser->content_length = ULLONG_MAX; + + if (ch == 'H') { + UPDATE_STATE(s_res_or_resp_H); + + CALLBACK_NOTIFY(message_begin); + } else { + parser->type = HTTP_REQUEST; + UPDATE_STATE(s_start_req); + REEXECUTE(); + } + + break; + } + + case s_res_or_resp_H: + if (ch == 'T') { + parser->type = HTTP_RESPONSE; + UPDATE_STATE(s_res_HT); + } else { + if (UNLIKELY(ch != 'E')) { + SET_ERRNO(HPE_INVALID_CONSTANT); + goto error; + } + + parser->type = HTTP_REQUEST; + parser->method = HTTP_HEAD; + parser->index = 2; + UPDATE_STATE(s_req_method); + } + break; + + case s_start_res: + { + if (ch == CR || ch == LF) + break; + parser->flags = 0; + parser->extra_flags = 0; + parser->content_length = ULLONG_MAX; + + if (ch == 'H') { + UPDATE_STATE(s_res_H); + } else { + SET_ERRNO(HPE_INVALID_CONSTANT); + goto error; + } + + CALLBACK_NOTIFY(message_begin); + break; + } + + case s_res_H: + STRICT_CHECK(ch != 'T'); + UPDATE_STATE(s_res_HT); + break; + + case s_res_HT: + STRICT_CHECK(ch != 'T'); + UPDATE_STATE(s_res_HTT); + break; + + case s_res_HTT: + STRICT_CHECK(ch != 'P'); + UPDATE_STATE(s_res_HTTP); + break; + + case s_res_HTTP: + STRICT_CHECK(ch != '/'); + UPDATE_STATE(s_res_http_major); + break; + + case s_res_http_major: + if (UNLIKELY(!IS_NUM(ch))) { + SET_ERRNO(HPE_INVALID_VERSION); + goto error; + } + + parser->http_major = ch - '0'; + UPDATE_STATE(s_res_http_dot); + break; + + case s_res_http_dot: + { + if (UNLIKELY(ch != '.')) { + SET_ERRNO(HPE_INVALID_VERSION); + goto error; + } + + UPDATE_STATE(s_res_http_minor); + break; + } + + case s_res_http_minor: + if (UNLIKELY(!IS_NUM(ch))) { + SET_ERRNO(HPE_INVALID_VERSION); + goto error; + } + + parser->http_minor = ch - '0'; + UPDATE_STATE(s_res_http_end); + break; + + case s_res_http_end: + { + if (UNLIKELY(ch != ' ')) { + SET_ERRNO(HPE_INVALID_VERSION); + goto error; + } + + UPDATE_STATE(s_res_first_status_code); + break; + } + + case s_res_first_status_code: + { + if (!IS_NUM(ch)) { + if (ch == ' ') { + break; + } + + SET_ERRNO(HPE_INVALID_STATUS); + goto error; + } + parser->status_code = ch - '0'; + UPDATE_STATE(s_res_status_code); + break; + } + + case s_res_status_code: + { + if (!IS_NUM(ch)) { + switch (ch) { + case ' ': + UPDATE_STATE(s_res_status_start); + break; + case CR: + case LF: + UPDATE_STATE(s_res_status_start); + REEXECUTE(); + break; + default: + SET_ERRNO(HPE_INVALID_STATUS); + goto error; + } + break; + } + + parser->status_code *= 10; + parser->status_code += ch - '0'; + + if (UNLIKELY(parser->status_code > 999)) { + SET_ERRNO(HPE_INVALID_STATUS); + goto error; + } + + break; + } + + case s_res_status_start: + { + MARK(status); + UPDATE_STATE(s_res_status); + parser->index = 0; + + if (ch == CR || ch == LF) + REEXECUTE(); + + break; + } + + case s_res_status: + if (ch == CR) { + UPDATE_STATE(s_res_line_almost_done); + CALLBACK_DATA(status); + break; + } + + if (ch == LF) { + UPDATE_STATE(s_header_field_start); + CALLBACK_DATA(status); + break; + } + + break; + + case s_res_line_almost_done: + STRICT_CHECK(ch != LF); + UPDATE_STATE(s_header_field_start); + break; + + case s_start_req: + { + if (ch == CR || ch == LF) + break; + parser->flags = 0; + parser->extra_flags = 0; + parser->content_length = ULLONG_MAX; + + if (UNLIKELY(!IS_ALPHA(ch))) { + SET_ERRNO(HPE_INVALID_METHOD); + goto error; + } + + parser->method = (enum http_method) 0; + parser->index = 1; + switch (ch) { + case 'A': parser->method = HTTP_ACL; break; + case 'B': parser->method = HTTP_BIND; break; + case 'C': parser->method = HTTP_CONNECT; /* or COPY, CHECKOUT */ break; + case 'D': parser->method = HTTP_DELETE; break; + case 'G': parser->method = HTTP_GET; break; + case 'H': parser->method = HTTP_HEAD; break; + case 'L': parser->method = HTTP_LOCK; /* or LINK */ break; + case 'M': parser->method = HTTP_MKCOL; /* or MOVE, MKACTIVITY, MERGE, M-SEARCH, MKCALENDAR */ break; + case 'N': parser->method = HTTP_NOTIFY; break; + case 'O': parser->method = HTTP_OPTIONS; break; + case 'P': parser->method = HTTP_POST; + /* or PROPFIND|PROPPATCH|PUT|PATCH|PURGE */ + break; + case 'R': parser->method = HTTP_REPORT; /* or REBIND */ break; + case 'S': parser->method = HTTP_SUBSCRIBE; /* or SEARCH, SOURCE */ break; + case 'T': parser->method = HTTP_TRACE; break; + case 'U': parser->method = HTTP_UNLOCK; /* or UNSUBSCRIBE, UNBIND, UNLINK */ break; + default: + SET_ERRNO(HPE_INVALID_METHOD); + goto error; + } + UPDATE_STATE(s_req_method); + + CALLBACK_NOTIFY(message_begin); + + break; + } + + case s_req_method: + { + const char *matcher; + if (UNLIKELY(ch == '\0')) { + SET_ERRNO(HPE_INVALID_METHOD); + goto error; + } + + matcher = method_strings[parser->method]; + if (ch == ' ' && matcher[parser->index] == '\0') { + UPDATE_STATE(s_req_spaces_before_url); + } else if (ch == matcher[parser->index]) { + ; /* nada */ + } else if ((ch >= 'A' && ch <= 'Z') || ch == '-') { + + switch (parser->method << 16 | parser->index << 8 | ch) { +#define XX(meth, pos, ch, new_meth) \ + case (HTTP_##meth << 16 | pos << 8 | ch): \ + parser->method = HTTP_##new_meth; break; + + XX(POST, 1, 'U', PUT) + XX(POST, 1, 'A', PATCH) + XX(POST, 1, 'R', PROPFIND) + XX(PUT, 2, 'R', PURGE) + XX(CONNECT, 1, 'H', CHECKOUT) + XX(CONNECT, 2, 'P', COPY) + XX(MKCOL, 1, 'O', MOVE) + XX(MKCOL, 1, 'E', MERGE) + XX(MKCOL, 1, '-', MSEARCH) + XX(MKCOL, 2, 'A', MKACTIVITY) + XX(MKCOL, 3, 'A', MKCALENDAR) + XX(SUBSCRIBE, 1, 'E', SEARCH) + XX(SUBSCRIBE, 1, 'O', SOURCE) + XX(REPORT, 2, 'B', REBIND) + XX(PROPFIND, 4, 'P', PROPPATCH) + XX(LOCK, 1, 'I', LINK) + XX(UNLOCK, 2, 'S', UNSUBSCRIBE) + XX(UNLOCK, 2, 'B', UNBIND) + XX(UNLOCK, 3, 'I', UNLINK) +#undef XX + default: + SET_ERRNO(HPE_INVALID_METHOD); + goto error; + } + } else { + SET_ERRNO(HPE_INVALID_METHOD); + goto error; + } + + ++parser->index; + break; + } + + case s_req_spaces_before_url: + { + if (ch == ' ') break; + + MARK(url); + if (parser->method == HTTP_CONNECT) { + UPDATE_STATE(s_req_server_start); + } + + UPDATE_STATE(parse_url_char(CURRENT_STATE(), ch)); + if (UNLIKELY(CURRENT_STATE() == s_dead)) { + SET_ERRNO(HPE_INVALID_URL); + goto error; + } + + break; + } + + case s_req_schema: + case s_req_schema_slash: + case s_req_schema_slash_slash: + case s_req_server_start: + { + switch (ch) { + /* No whitespace allowed here */ + case ' ': + case CR: + case LF: + SET_ERRNO(HPE_INVALID_URL); + goto error; + default: + UPDATE_STATE(parse_url_char(CURRENT_STATE(), ch)); + if (UNLIKELY(CURRENT_STATE() == s_dead)) { + SET_ERRNO(HPE_INVALID_URL); + goto error; + } + } + + break; + } + + case s_req_server: + case s_req_server_with_at: + case s_req_path: + case s_req_query_string_start: + case s_req_query_string: + case s_req_fragment_start: + case s_req_fragment: + { + switch (ch) { + case ' ': + UPDATE_STATE(s_req_http_start); + CALLBACK_DATA(url); + break; + case CR: + case LF: + parser->http_major = 0; + parser->http_minor = 9; + UPDATE_STATE((ch == CR) ? + s_req_line_almost_done : + s_header_field_start); + CALLBACK_DATA(url); + break; + default: + UPDATE_STATE(parse_url_char(CURRENT_STATE(), ch)); + if (UNLIKELY(CURRENT_STATE() == s_dead)) { + SET_ERRNO(HPE_INVALID_URL); + goto error; + } + } + break; + } + + case s_req_http_start: + switch (ch) { + case ' ': + break; + case 'H': + UPDATE_STATE(s_req_http_H); + break; + case 'I': + if (parser->method == HTTP_SOURCE) { + UPDATE_STATE(s_req_http_I); + break; + } + /* fall through */ + default: + SET_ERRNO(HPE_INVALID_CONSTANT); + goto error; + } + break; + + case s_req_http_H: + STRICT_CHECK(ch != 'T'); + UPDATE_STATE(s_req_http_HT); + break; + + case s_req_http_HT: + STRICT_CHECK(ch != 'T'); + UPDATE_STATE(s_req_http_HTT); + break; + + case s_req_http_HTT: + STRICT_CHECK(ch != 'P'); + UPDATE_STATE(s_req_http_HTTP); + break; + + case s_req_http_I: + STRICT_CHECK(ch != 'C'); + UPDATE_STATE(s_req_http_IC); + break; + + case s_req_http_IC: + STRICT_CHECK(ch != 'E'); + UPDATE_STATE(s_req_http_HTTP); /* Treat "ICE" as "HTTP". */ + break; + + case s_req_http_HTTP: + STRICT_CHECK(ch != '/'); + UPDATE_STATE(s_req_http_major); + break; + + case s_req_http_major: + if (UNLIKELY(!IS_NUM(ch))) { + SET_ERRNO(HPE_INVALID_VERSION); + goto error; + } + + parser->http_major = ch - '0'; + UPDATE_STATE(s_req_http_dot); + break; + + case s_req_http_dot: + { + if (UNLIKELY(ch != '.')) { + SET_ERRNO(HPE_INVALID_VERSION); + goto error; + } + + UPDATE_STATE(s_req_http_minor); + break; + } + + case s_req_http_minor: + if (UNLIKELY(!IS_NUM(ch))) { + SET_ERRNO(HPE_INVALID_VERSION); + goto error; + } + + parser->http_minor = ch - '0'; + UPDATE_STATE(s_req_http_end); + break; + + case s_req_http_end: + { + if (ch == CR) { + UPDATE_STATE(s_req_line_almost_done); + break; + } + + if (ch == LF) { + UPDATE_STATE(s_header_field_start); + break; + } + + SET_ERRNO(HPE_INVALID_VERSION); + goto error; + break; + } + + /* end of request line */ + case s_req_line_almost_done: + { + if (UNLIKELY(ch != LF)) { + SET_ERRNO(HPE_LF_EXPECTED); + goto error; + } + + UPDATE_STATE(s_header_field_start); + break; + } + + case s_header_field_start: + { + if (ch == CR) { + UPDATE_STATE(s_headers_almost_done); + break; + } + + if (ch == LF) { + /* they might be just sending \n instead of \r\n so this would be + * the second \n to denote the end of headers*/ + UPDATE_STATE(s_headers_almost_done); + REEXECUTE(); + } + + c = TOKEN(ch); + + if (UNLIKELY(!c)) { + SET_ERRNO(HPE_INVALID_HEADER_TOKEN); + goto error; + } + + MARK(header_field); + + parser->index = 0; + UPDATE_STATE(s_header_field); + + switch (c) { + case 'c': + parser->header_state = h_C; + break; + + case 'p': + parser->header_state = h_matching_proxy_connection; + break; + + case 't': + parser->header_state = h_matching_transfer_encoding; + break; + + case 'u': + parser->header_state = h_matching_upgrade; + break; + + default: + parser->header_state = h_general; + break; + } + break; + } + + case s_header_field: + { + const char* start = p; + for (; p != data + len; p++) { + ch = *p; + c = TOKEN(ch); + + if (!c) + break; + + switch (parser->header_state) { + case h_general: { + size_t left = data + len - p; + const char* pe = p + MIN(left, max_header_size); + while (p+1 < pe && TOKEN(p[1])) { + p++; + } + break; + } + + case h_C: + parser->index++; + parser->header_state = (c == 'o' ? h_CO : h_general); + break; + + case h_CO: + parser->index++; + parser->header_state = (c == 'n' ? h_CON : h_general); + break; + + case h_CON: + parser->index++; + switch (c) { + case 'n': + parser->header_state = h_matching_connection; + break; + case 't': + parser->header_state = h_matching_content_length; + break; + default: + parser->header_state = h_general; + break; + } + break; + + /* connection */ + + case h_matching_connection: + parser->index++; + if (parser->index > sizeof(CONNECTION)-1 + || c != CONNECTION[parser->index]) { + parser->header_state = h_general; + } else if (parser->index == sizeof(CONNECTION)-2) { + parser->header_state = h_connection; + } + break; + + /* proxy-connection */ + + case h_matching_proxy_connection: + parser->index++; + if (parser->index > sizeof(PROXY_CONNECTION)-1 + || c != PROXY_CONNECTION[parser->index]) { + parser->header_state = h_general; + } else if (parser->index == sizeof(PROXY_CONNECTION)-2) { + parser->header_state = h_connection; + } + break; + + /* content-length */ + + case h_matching_content_length: + parser->index++; + if (parser->index > sizeof(CONTENT_LENGTH)-1 + || c != CONTENT_LENGTH[parser->index]) { + parser->header_state = h_general; + } else if (parser->index == sizeof(CONTENT_LENGTH)-2) { + parser->header_state = h_content_length; + } + break; + + /* transfer-encoding */ + + case h_matching_transfer_encoding: + parser->index++; + if (parser->index > sizeof(TRANSFER_ENCODING)-1 + || c != TRANSFER_ENCODING[parser->index]) { + parser->header_state = h_general; + } else if (parser->index == sizeof(TRANSFER_ENCODING)-2) { + parser->header_state = h_transfer_encoding; + parser->extra_flags |= F_TRANSFER_ENCODING >> 8; + } + break; + + /* upgrade */ + + case h_matching_upgrade: + parser->index++; + if (parser->index > sizeof(UPGRADE)-1 + || c != UPGRADE[parser->index]) { + parser->header_state = h_general; + } else if (parser->index == sizeof(UPGRADE)-2) { + parser->header_state = h_upgrade; + } + break; + + case h_connection: + case h_content_length: + case h_transfer_encoding: + case h_upgrade: + if (ch != ' ') parser->header_state = h_general; + break; + + default: + assert(0 && "Unknown header_state"); + break; + } + } + + if (p == data + len) { + --p; + COUNT_HEADER_SIZE(p - start); + break; + } + + COUNT_HEADER_SIZE(p - start); + + if (ch == ':') { + UPDATE_STATE(s_header_value_discard_ws); + CALLBACK_DATA(header_field); + break; + } + + SET_ERRNO(HPE_INVALID_HEADER_TOKEN); + goto error; + } + + case s_header_value_discard_ws: + if (ch == ' ' || ch == '\t') break; + + if (ch == CR) { + UPDATE_STATE(s_header_value_discard_ws_almost_done); + break; + } + + if (ch == LF) { + UPDATE_STATE(s_header_value_discard_lws); + break; + } + + /* fall through */ + + case s_header_value_start: + { + MARK(header_value); + + UPDATE_STATE(s_header_value); + parser->index = 0; + + c = LOWER(ch); + + switch (parser->header_state) { + case h_upgrade: + parser->flags |= F_UPGRADE; + parser->header_state = h_general; + break; + + case h_transfer_encoding: + /* looking for 'Transfer-Encoding: chunked' */ + if ('c' == c) { + parser->header_state = h_matching_transfer_encoding_chunked; + } else { + parser->header_state = h_matching_transfer_encoding_token; + } + break; + + /* Multi-value `Transfer-Encoding` header */ + case h_matching_transfer_encoding_token_start: + break; + + case h_content_length: + if (UNLIKELY(!IS_NUM(ch))) { + SET_ERRNO(HPE_INVALID_CONTENT_LENGTH); + goto error; + } + + if (parser->flags & F_CONTENTLENGTH) { + SET_ERRNO(HPE_UNEXPECTED_CONTENT_LENGTH); + goto error; + } + + parser->flags |= F_CONTENTLENGTH; + parser->content_length = ch - '0'; + parser->header_state = h_content_length_num; + break; + + /* when obsolete line folding is encountered for content length + * continue to the s_header_value state */ + case h_content_length_ws: + break; + + case h_connection: + /* looking for 'Connection: keep-alive' */ + if (c == 'k') { + parser->header_state = h_matching_connection_keep_alive; + /* looking for 'Connection: close' */ + } else if (c == 'c') { + parser->header_state = h_matching_connection_close; + } else if (c == 'u') { + parser->header_state = h_matching_connection_upgrade; + } else { + parser->header_state = h_matching_connection_token; + } + break; + + /* Multi-value `Connection` header */ + case h_matching_connection_token_start: + break; + + default: + parser->header_state = h_general; + break; + } + break; + } + + case s_header_value: + { + const char* start = p; + enum header_states h_state = (enum header_states) parser->header_state; + for (; p != data + len; p++) { + ch = *p; + if (ch == CR) { + UPDATE_STATE(s_header_almost_done); + parser->header_state = h_state; + CALLBACK_DATA(header_value); + break; + } + + if (ch == LF) { + UPDATE_STATE(s_header_almost_done); + COUNT_HEADER_SIZE(p - start); + parser->header_state = h_state; + CALLBACK_DATA_NOADVANCE(header_value); + REEXECUTE(); + } + + if (!lenient && !IS_HEADER_CHAR(ch)) { + SET_ERRNO(HPE_INVALID_HEADER_TOKEN); + goto error; + } + + c = LOWER(ch); + + switch (h_state) { + case h_general: + { + size_t left = data + len - p; + const char* pe = p + MIN(left, max_header_size); + + for (; p != pe; p++) { + ch = *p; + if (ch == CR || ch == LF) { + --p; + break; + } + if (!lenient && !IS_HEADER_CHAR(ch)) { + SET_ERRNO(HPE_INVALID_HEADER_TOKEN); + goto error; + } + } + if (p == data + len) + --p; + break; + } + + case h_connection: + case h_transfer_encoding: + assert(0 && "Shouldn't get here."); + break; + + case h_content_length: + if (ch == ' ') break; + h_state = h_content_length_num; + /* fall through */ + + case h_content_length_num: + { + uint64_t t; + + if (ch == ' ') { + h_state = h_content_length_ws; + break; + } + + if (UNLIKELY(!IS_NUM(ch))) { + SET_ERRNO(HPE_INVALID_CONTENT_LENGTH); + parser->header_state = h_state; + goto error; + } + + t = parser->content_length; + t *= 10; + t += ch - '0'; + + /* Overflow? Test against a conservative limit for simplicity. */ + if (UNLIKELY((ULLONG_MAX - 10) / 10 < parser->content_length)) { + SET_ERRNO(HPE_INVALID_CONTENT_LENGTH); + parser->header_state = h_state; + goto error; + } + + parser->content_length = t; + break; + } + + case h_content_length_ws: + if (ch == ' ') break; + SET_ERRNO(HPE_INVALID_CONTENT_LENGTH); + parser->header_state = h_state; + goto error; + + /* Transfer-Encoding: chunked */ + case h_matching_transfer_encoding_token_start: + /* looking for 'Transfer-Encoding: chunked' */ + if ('c' == c) { + h_state = h_matching_transfer_encoding_chunked; + } else if (STRICT_TOKEN(c)) { + /* TODO(indutny): similar code below does this, but why? + * At the very least it seems to be inconsistent given that + * h_matching_transfer_encoding_token does not check for + * `STRICT_TOKEN` + */ + h_state = h_matching_transfer_encoding_token; + } else if (c == ' ' || c == '\t') { + /* Skip lws */ + } else { + h_state = h_general; + } + break; + + case h_matching_transfer_encoding_chunked: + parser->index++; + if (parser->index > sizeof(CHUNKED)-1 + || c != CHUNKED[parser->index]) { + h_state = h_matching_transfer_encoding_token; + } else if (parser->index == sizeof(CHUNKED)-2) { + h_state = h_transfer_encoding_chunked; + } + break; + + case h_matching_transfer_encoding_token: + if (ch == ',') { + h_state = h_matching_transfer_encoding_token_start; + parser->index = 0; + } + break; + + case h_matching_connection_token_start: + /* looking for 'Connection: keep-alive' */ + if (c == 'k') { + h_state = h_matching_connection_keep_alive; + /* looking for 'Connection: close' */ + } else if (c == 'c') { + h_state = h_matching_connection_close; + } else if (c == 'u') { + h_state = h_matching_connection_upgrade; + } else if (STRICT_TOKEN(c)) { + h_state = h_matching_connection_token; + } else if (c == ' ' || c == '\t') { + /* Skip lws */ + } else { + h_state = h_general; + } + break; + + /* looking for 'Connection: keep-alive' */ + case h_matching_connection_keep_alive: + parser->index++; + if (parser->index > sizeof(KEEP_ALIVE)-1 + || c != KEEP_ALIVE[parser->index]) { + h_state = h_matching_connection_token; + } else if (parser->index == sizeof(KEEP_ALIVE)-2) { + h_state = h_connection_keep_alive; + } + break; + + /* looking for 'Connection: close' */ + case h_matching_connection_close: + parser->index++; + if (parser->index > sizeof(CLOSE)-1 || c != CLOSE[parser->index]) { + h_state = h_matching_connection_token; + } else if (parser->index == sizeof(CLOSE)-2) { + h_state = h_connection_close; + } + break; + + /* looking for 'Connection: upgrade' */ + case h_matching_connection_upgrade: + parser->index++; + if (parser->index > sizeof(UPGRADE) - 1 || + c != UPGRADE[parser->index]) { + h_state = h_matching_connection_token; + } else if (parser->index == sizeof(UPGRADE)-2) { + h_state = h_connection_upgrade; + } + break; + + case h_matching_connection_token: + if (ch == ',') { + h_state = h_matching_connection_token_start; + parser->index = 0; + } + break; + + case h_transfer_encoding_chunked: + if (ch != ' ') h_state = h_matching_transfer_encoding_token; + break; + + case h_connection_keep_alive: + case h_connection_close: + case h_connection_upgrade: + if (ch == ',') { + if (h_state == h_connection_keep_alive) { + parser->flags |= F_CONNECTION_KEEP_ALIVE; + } else if (h_state == h_connection_close) { + parser->flags |= F_CONNECTION_CLOSE; + } else if (h_state == h_connection_upgrade) { + parser->flags |= F_CONNECTION_UPGRADE; + } + h_state = h_matching_connection_token_start; + parser->index = 0; + } else if (ch != ' ') { + h_state = h_matching_connection_token; + } + break; + + default: + UPDATE_STATE(s_header_value); + h_state = h_general; + break; + } + } + parser->header_state = h_state; + + if (p == data + len) + --p; + + COUNT_HEADER_SIZE(p - start); + break; + } + + case s_header_almost_done: + { + if (UNLIKELY(ch != LF)) { + SET_ERRNO(HPE_LF_EXPECTED); + goto error; + } + + UPDATE_STATE(s_header_value_lws); + break; + } + + case s_header_value_lws: + { + if (ch == ' ' || ch == '\t') { + if (parser->header_state == h_content_length_num) { + /* treat obsolete line folding as space */ + parser->header_state = h_content_length_ws; + } + UPDATE_STATE(s_header_value_start); + REEXECUTE(); + } + + /* finished the header */ + switch (parser->header_state) { + case h_connection_keep_alive: + parser->flags |= F_CONNECTION_KEEP_ALIVE; + break; + case h_connection_close: + parser->flags |= F_CONNECTION_CLOSE; + break; + case h_transfer_encoding_chunked: + parser->flags |= F_CHUNKED; + break; + case h_connection_upgrade: + parser->flags |= F_CONNECTION_UPGRADE; + break; + default: + break; + } + + UPDATE_STATE(s_header_field_start); + REEXECUTE(); + } + + case s_header_value_discard_ws_almost_done: + { + STRICT_CHECK(ch != LF); + UPDATE_STATE(s_header_value_discard_lws); + break; + } + + case s_header_value_discard_lws: + { + if (ch == ' ' || ch == '\t') { + UPDATE_STATE(s_header_value_discard_ws); + break; + } else { + switch (parser->header_state) { + case h_connection_keep_alive: + parser->flags |= F_CONNECTION_KEEP_ALIVE; + break; + case h_connection_close: + parser->flags |= F_CONNECTION_CLOSE; + break; + case h_connection_upgrade: + parser->flags |= F_CONNECTION_UPGRADE; + break; + case h_transfer_encoding_chunked: + parser->flags |= F_CHUNKED; + break; + case h_content_length: + /* do not allow empty content length */ + SET_ERRNO(HPE_INVALID_CONTENT_LENGTH); + goto error; + break; + default: + break; + } + + /* header value was empty */ + MARK(header_value); + UPDATE_STATE(s_header_field_start); + CALLBACK_DATA_NOADVANCE(header_value); + REEXECUTE(); + } + } + + case s_headers_almost_done: + { + STRICT_CHECK(ch != LF); + + if (parser->flags & F_TRAILING) { + /* End of a chunked request */ + UPDATE_STATE(s_message_done); + CALLBACK_NOTIFY_NOADVANCE(chunk_complete); + REEXECUTE(); + } + + /* Cannot us transfer-encoding and a content-length header together + per the HTTP specification. (RFC 7230 Section 3.3.3) */ + if ((parser->extra_flags & (F_TRANSFER_ENCODING >> 8)) && + (parser->flags & F_CONTENTLENGTH)) { + /* Allow it for lenient parsing as long as `Transfer-Encoding` is + * not `chunked` + */ + if (!lenient || (parser->flags & F_CHUNKED)) { + SET_ERRNO(HPE_UNEXPECTED_CONTENT_LENGTH); + goto error; + } + } + + UPDATE_STATE(s_headers_done); + + /* Set this here so that on_headers_complete() callbacks can see it */ + if ((parser->flags & F_UPGRADE) && + (parser->flags & F_CONNECTION_UPGRADE)) { + /* For responses, "Upgrade: foo" and "Connection: upgrade" are + * mandatory only when it is a 101 Switching Protocols response, + * otherwise it is purely informational, to announce support. + */ + parser->upgrade = + (parser->type == HTTP_REQUEST || parser->status_code == 101); + } else { + parser->upgrade = (parser->method == HTTP_CONNECT); + } + + /* Here we call the headers_complete callback. This is somewhat + * different than other callbacks because if the user returns 1, we + * will interpret that as saying that this message has no body. This + * is needed for the annoying case of recieving a response to a HEAD + * request. + * + * We'd like to use CALLBACK_NOTIFY_NOADVANCE() here but we cannot, so + * we have to simulate it by handling a change in errno below. + */ + if (settings->on_headers_complete) { + switch (settings->on_headers_complete(parser)) { + case 0: + break; + + case 2: + parser->upgrade = 1; + + /* fall through */ + case 1: + parser->flags |= F_SKIPBODY; + break; + + default: + SET_ERRNO(HPE_CB_headers_complete); + RETURN(p - data); /* Error */ + } + } + + if (HTTP_PARSER_ERRNO(parser) != HPE_OK) { + RETURN(p - data); + } + + REEXECUTE(); + } + + case s_headers_done: + { + int hasBody; + STRICT_CHECK(ch != LF); + + parser->nread = 0; + nread = 0; + + hasBody = parser->flags & F_CHUNKED || + (parser->content_length > 0 && parser->content_length != ULLONG_MAX); + if (parser->upgrade && (parser->method == HTTP_CONNECT || + (parser->flags & F_SKIPBODY) || !hasBody)) { + /* Exit, the rest of the message is in a different protocol. */ + UPDATE_STATE(NEW_MESSAGE()); + CALLBACK_NOTIFY(message_complete); + RETURN((p - data) + 1); + } + + if (parser->flags & F_SKIPBODY) { + UPDATE_STATE(NEW_MESSAGE()); + CALLBACK_NOTIFY(message_complete); + } else if (parser->flags & F_CHUNKED) { + /* chunked encoding - ignore Content-Length header, + * prepare for a chunk */ + UPDATE_STATE(s_chunk_size_start); + } else if (parser->extra_flags & (F_TRANSFER_ENCODING >> 8)) { + if (parser->type == HTTP_REQUEST && !lenient) { + /* RFC 7230 3.3.3 */ + + /* If a Transfer-Encoding header field + * is present in a request and the chunked transfer coding is not + * the final encoding, the message body length cannot be determined + * reliably; the server MUST respond with the 400 (Bad Request) + * status code and then close the connection. + */ + SET_ERRNO(HPE_INVALID_TRANSFER_ENCODING); + RETURN(p - data); /* Error */ + } else { + /* RFC 7230 3.3.3 */ + + /* If a Transfer-Encoding header field is present in a response and + * the chunked transfer coding is not the final encoding, the + * message body length is determined by reading the connection until + * it is closed by the server. + */ + UPDATE_STATE(s_body_identity_eof); + } + } else { + if (parser->content_length == 0) { + /* Content-Length header given but zero: Content-Length: 0\r\n */ + UPDATE_STATE(NEW_MESSAGE()); + CALLBACK_NOTIFY(message_complete); + } else if (parser->content_length != ULLONG_MAX) { + /* Content-Length header given and non-zero */ + UPDATE_STATE(s_body_identity); + } else { + if (!http_message_needs_eof(parser)) { + /* Assume content-length 0 - read the next */ + UPDATE_STATE(NEW_MESSAGE()); + CALLBACK_NOTIFY(message_complete); + } else { + /* Read body until EOF */ + UPDATE_STATE(s_body_identity_eof); + } + } + } + + break; + } + + case s_body_identity: + { + uint64_t to_read = MIN(parser->content_length, + (uint64_t) ((data + len) - p)); + + assert(parser->content_length != 0 + && parser->content_length != ULLONG_MAX); + + /* The difference between advancing content_length and p is because + * the latter will automaticaly advance on the next loop iteration. + * Further, if content_length ends up at 0, we want to see the last + * byte again for our message complete callback. + */ + MARK(body); + parser->content_length -= to_read; + p += to_read - 1; + + if (parser->content_length == 0) { + UPDATE_STATE(s_message_done); + + /* Mimic CALLBACK_DATA_NOADVANCE() but with one extra byte. + * + * The alternative to doing this is to wait for the next byte to + * trigger the data callback, just as in every other case. The + * problem with this is that this makes it difficult for the test + * harness to distinguish between complete-on-EOF and + * complete-on-length. It's not clear that this distinction is + * important for applications, but let's keep it for now. + */ + CALLBACK_DATA_(body, p - body_mark + 1, p - data); + REEXECUTE(); + } + + break; + } + + /* read until EOF */ + case s_body_identity_eof: + MARK(body); + p = data + len - 1; + + break; + + case s_message_done: + UPDATE_STATE(NEW_MESSAGE()); + CALLBACK_NOTIFY(message_complete); + if (parser->upgrade) { + /* Exit, the rest of the message is in a different protocol. */ + RETURN((p - data) + 1); + } + break; + + case s_chunk_size_start: + { + assert(nread == 1); + assert(parser->flags & F_CHUNKED); + + unhex_val = unhex[(unsigned char)ch]; + if (UNLIKELY(unhex_val == -1)) { + SET_ERRNO(HPE_INVALID_CHUNK_SIZE); + goto error; + } + + parser->content_length = unhex_val; + UPDATE_STATE(s_chunk_size); + break; + } + + case s_chunk_size: + { + uint64_t t; + + assert(parser->flags & F_CHUNKED); + + if (ch == CR) { + UPDATE_STATE(s_chunk_size_almost_done); + break; + } + + unhex_val = unhex[(unsigned char)ch]; + + if (unhex_val == -1) { + if (ch == ';' || ch == ' ') { + UPDATE_STATE(s_chunk_parameters); + break; + } + + SET_ERRNO(HPE_INVALID_CHUNK_SIZE); + goto error; + } + + t = parser->content_length; + t *= 16; + t += unhex_val; + + /* Overflow? Test against a conservative limit for simplicity. */ + if (UNLIKELY((ULLONG_MAX - 16) / 16 < parser->content_length)) { + SET_ERRNO(HPE_INVALID_CONTENT_LENGTH); + goto error; + } + + parser->content_length = t; + break; + } + + case s_chunk_parameters: + { + assert(parser->flags & F_CHUNKED); + /* just ignore this shit. TODO check for overflow */ + if (ch == CR) { + UPDATE_STATE(s_chunk_size_almost_done); + break; + } + break; + } + + case s_chunk_size_almost_done: + { + assert(parser->flags & F_CHUNKED); + STRICT_CHECK(ch != LF); + + parser->nread = 0; + nread = 0; + + if (parser->content_length == 0) { + parser->flags |= F_TRAILING; + UPDATE_STATE(s_header_field_start); + } else { + UPDATE_STATE(s_chunk_data); + } + CALLBACK_NOTIFY(chunk_header); + break; + } + + case s_chunk_data: + { + uint64_t to_read = MIN(parser->content_length, + (uint64_t) ((data + len) - p)); + + assert(parser->flags & F_CHUNKED); + assert(parser->content_length != 0 + && parser->content_length != ULLONG_MAX); + + /* See the explanation in s_body_identity for why the content + * length and data pointers are managed this way. + */ + MARK(body); + parser->content_length -= to_read; + p += to_read - 1; + + if (parser->content_length == 0) { + UPDATE_STATE(s_chunk_data_almost_done); + } + + break; + } + + case s_chunk_data_almost_done: + assert(parser->flags & F_CHUNKED); + assert(parser->content_length == 0); + STRICT_CHECK(ch != CR); + UPDATE_STATE(s_chunk_data_done); + CALLBACK_DATA(body); + break; + + case s_chunk_data_done: + assert(parser->flags & F_CHUNKED); + STRICT_CHECK(ch != LF); + parser->nread = 0; + nread = 0; + UPDATE_STATE(s_chunk_size_start); + CALLBACK_NOTIFY(chunk_complete); + break; + + default: + assert(0 && "unhandled state"); + SET_ERRNO(HPE_INVALID_INTERNAL_STATE); + goto error; + } + } + + /* Run callbacks for any marks that we have leftover after we ran out of + * bytes. There should be at most one of these set, so it's OK to invoke + * them in series (unset marks will not result in callbacks). + * + * We use the NOADVANCE() variety of callbacks here because 'p' has already + * overflowed 'data' and this allows us to correct for the off-by-one that + * we'd otherwise have (since CALLBACK_DATA() is meant to be run with a 'p' + * value that's in-bounds). + */ + + assert(((header_field_mark ? 1 : 0) + + (header_value_mark ? 1 : 0) + + (url_mark ? 1 : 0) + + (body_mark ? 1 : 0) + + (status_mark ? 1 : 0)) <= 1); + + CALLBACK_DATA_NOADVANCE(header_field); + CALLBACK_DATA_NOADVANCE(header_value); + CALLBACK_DATA_NOADVANCE(url); + CALLBACK_DATA_NOADVANCE(body); + CALLBACK_DATA_NOADVANCE(status); + + RETURN(len); + +error: + if (HTTP_PARSER_ERRNO(parser) == HPE_OK) { + SET_ERRNO(HPE_UNKNOWN); + } + + RETURN(p - data); +} + + +/* Does the parser need to see an EOF to find the end of the message? */ +int +http_message_needs_eof (const http_parser *parser) +{ + if (parser->type == HTTP_REQUEST) { + return 0; + } + + /* See RFC 2616 section 4.4 */ + if (parser->status_code / 100 == 1 || /* 1xx e.g. Continue */ + parser->status_code == 204 || /* No Content */ + parser->status_code == 304 || /* Not Modified */ + parser->flags & F_SKIPBODY) { /* response to a HEAD request */ + return 0; + } + + /* RFC 7230 3.3.3, see `s_headers_almost_done` */ + if ((parser->extra_flags & (F_TRANSFER_ENCODING >> 8)) && + (parser->flags & F_CHUNKED) == 0) { + return 1; + } + + if ((parser->flags & F_CHUNKED) || parser->content_length != ULLONG_MAX) { + return 0; + } + + return 1; +} + + +int +http_should_keep_alive (const http_parser *parser) +{ + if (parser->http_major > 0 && parser->http_minor > 0) { + /* HTTP/1.1 */ + if (parser->flags & F_CONNECTION_CLOSE) { + return 0; + } + } else { + /* HTTP/1.0 or earlier */ + if (!(parser->flags & F_CONNECTION_KEEP_ALIVE)) { + return 0; + } + } + + return !http_message_needs_eof(parser); +} + + +const char * +http_method_str (enum http_method m) +{ + return ELEM_AT(method_strings, m, ""); +} + +const char * +http_status_str (enum http_status s) +{ + switch (s) { +#define XX(num, name, string) case HTTP_STATUS_##name: return #string; + HTTP_STATUS_MAP(XX) +#undef XX + default: return ""; + } +} + +void +http_parser_init (http_parser *parser, enum http_parser_type t) +{ + void *data = parser->data; /* preserve application data */ + memset(parser, 0, sizeof(*parser)); + parser->data = data; + parser->type = t; + parser->state = (t == HTTP_REQUEST ? s_start_req : (t == HTTP_RESPONSE ? s_start_res : s_start_req_or_res)); + parser->http_errno = HPE_OK; +} + +void +http_parser_settings_init(http_parser_settings *settings) +{ + memset(settings, 0, sizeof(*settings)); +} + +const char * +http_errno_name(enum http_errno err) { + assert(((size_t) err) < ARRAY_SIZE(http_strerror_tab)); + return http_strerror_tab[err].name; +} + +const char * +http_errno_description(enum http_errno err) { + assert(((size_t) err) < ARRAY_SIZE(http_strerror_tab)); + return http_strerror_tab[err].description; +} + +static enum http_host_state +http_parse_host_char(enum http_host_state s, const char ch) { + switch(s) { + case s_http_userinfo: + case s_http_userinfo_start: + if (ch == '@') { + return s_http_host_start; + } + + if (IS_USERINFO_CHAR(ch)) { + return s_http_userinfo; + } + break; + + case s_http_host_start: + if (ch == '[') { + return s_http_host_v6_start; + } + + if (IS_HOST_CHAR(ch)) { + return s_http_host; + } + + break; + + case s_http_host: + if (IS_HOST_CHAR(ch)) { + return s_http_host; + } + + /* fall through */ + case s_http_host_v6_end: + if (ch == ':') { + return s_http_host_port_start; + } + + break; + + case s_http_host_v6: + if (ch == ']') { + return s_http_host_v6_end; + } + + /* fall through */ + case s_http_host_v6_start: + if (IS_HEX(ch) || ch == ':' || ch == '.') { + return s_http_host_v6; + } + + if (s == s_http_host_v6 && ch == '%') { + return s_http_host_v6_zone_start; + } + break; + + case s_http_host_v6_zone: + if (ch == ']') { + return s_http_host_v6_end; + } + + /* fall through */ + case s_http_host_v6_zone_start: + /* RFC 6874 Zone ID consists of 1*( unreserved / pct-encoded) */ + if (IS_ALPHANUM(ch) || ch == '%' || ch == '.' || ch == '-' || ch == '_' || + ch == '~') { + return s_http_host_v6_zone; + } + break; + + case s_http_host_port: + case s_http_host_port_start: + if (IS_NUM(ch)) { + return s_http_host_port; + } + + break; + + default: + break; + } + return s_http_host_dead; +} + +static int +http_parse_host(const char * buf, struct http_parser_url *u, int found_at) { + enum http_host_state s; + + const char *p; + size_t buflen = u->field_data[UF_HOST].off + u->field_data[UF_HOST].len; + + assert(u->field_set & (1 << UF_HOST)); + + u->field_data[UF_HOST].len = 0; + + s = found_at ? s_http_userinfo_start : s_http_host_start; + + for (p = buf + u->field_data[UF_HOST].off; p < buf + buflen; p++) { + enum http_host_state new_s = http_parse_host_char(s, *p); + + if (new_s == s_http_host_dead) { + return 1; + } + + switch(new_s) { + case s_http_host: + if (s != s_http_host) { + u->field_data[UF_HOST].off = (uint16_t)(p - buf); + } + u->field_data[UF_HOST].len++; + break; + + case s_http_host_v6: + if (s != s_http_host_v6) { + u->field_data[UF_HOST].off = (uint16_t)(p - buf); + } + u->field_data[UF_HOST].len++; + break; + + case s_http_host_v6_zone_start: + case s_http_host_v6_zone: + u->field_data[UF_HOST].len++; + break; + + case s_http_host_port: + if (s != s_http_host_port) { + u->field_data[UF_PORT].off = (uint16_t)(p - buf); + u->field_data[UF_PORT].len = 0; + u->field_set |= (1 << UF_PORT); + } + u->field_data[UF_PORT].len++; + break; + + case s_http_userinfo: + if (s != s_http_userinfo) { + u->field_data[UF_USERINFO].off = (uint16_t)(p - buf); + u->field_data[UF_USERINFO].len = 0; + u->field_set |= (1 << UF_USERINFO); + } + u->field_data[UF_USERINFO].len++; + break; + + default: + break; + } + s = new_s; + } + + /* Make sure we don't end somewhere unexpected */ + switch (s) { + case s_http_host_start: + case s_http_host_v6_start: + case s_http_host_v6: + case s_http_host_v6_zone_start: + case s_http_host_v6_zone: + case s_http_host_port_start: + case s_http_userinfo: + case s_http_userinfo_start: + return 1; + default: + break; + } + + return 0; +} + +void +http_parser_url_init(struct http_parser_url *u) { + memset(u, 0, sizeof(*u)); +} + +int +http_parser_parse_url(const char *buf, size_t buflen, int is_connect, + struct http_parser_url *u) +{ + enum state s; + const char *p; + enum http_parser_url_fields uf, old_uf; + int found_at = 0; + + if (buflen == 0) { + return 1; + } + + u->port = u->field_set = 0; + s = is_connect ? s_req_server_start : s_req_spaces_before_url; + old_uf = UF_MAX; + + for (p = buf; p < buf + buflen; p++) { + s = parse_url_char(s, *p); + + /* Figure out the next field that we're operating on */ + switch (s) { + case s_dead: + return 1; + + /* Skip delimeters */ + case s_req_schema_slash: + case s_req_schema_slash_slash: + case s_req_server_start: + case s_req_query_string_start: + case s_req_fragment_start: + continue; + + case s_req_schema: + uf = UF_SCHEMA; + break; + + case s_req_server_with_at: + found_at = 1; + + /* fall through */ + case s_req_server: + uf = UF_HOST; + break; + + case s_req_path: + uf = UF_PATH; + break; + + case s_req_query_string: + uf = UF_QUERY; + break; + + case s_req_fragment: + uf = UF_FRAGMENT; + break; + + default: + assert(!"Unexpected state"); + return 1; + } + + /* Nothing's changed; soldier on */ + if (uf == old_uf) { + u->field_data[uf].len++; + continue; + } + + u->field_data[uf].off = (uint16_t)(p - buf); + u->field_data[uf].len = 1; + + u->field_set |= (1 << uf); + old_uf = uf; + } + + /* host must be present if there is a schema */ + /* parsing http:///toto will fail */ + if ((u->field_set & (1 << UF_SCHEMA)) && + (u->field_set & (1 << UF_HOST)) == 0) { + return 1; + } + + if (u->field_set & (1 << UF_HOST)) { + if (http_parse_host(buf, u, found_at) != 0) { + return 1; + } + } + + /* CONNECT requests can only contain "hostname:port" */ + if (is_connect && u->field_set != ((1 << UF_HOST)|(1 << UF_PORT))) { + return 1; + } + + if (u->field_set & (1 << UF_PORT)) { + uint16_t off; + uint16_t len; + const char* p; + const char* end; + unsigned long v; + + off = u->field_data[UF_PORT].off; + len = u->field_data[UF_PORT].len; + end = buf + off + len; + + /* NOTE: The characters are already validated and are in the [0-9] range */ + assert(off + len <= buflen && "Port number overflow"); + v = 0; + for (p = buf + off; p < end; p++) { + v *= 10; + v += *p - '0'; + + /* Ports have a max value of 2^16 */ + if (v > 0xffff) { + return 1; + } + } + + u->port = (uint16_t) v; + } + + return 0; +} + +void +http_parser_pause(http_parser *parser, int paused) { + /* Users should only be pausing/unpausing a parser that is not in an error + * state. In non-debug builds, there's not much that we can do about this + * other than ignore it. + */ + if (HTTP_PARSER_ERRNO(parser) == HPE_OK || + HTTP_PARSER_ERRNO(parser) == HPE_PAUSED) { + uint32_t nread = parser->nread; /* used by the SET_ERRNO macro */ + SET_ERRNO((paused) ? HPE_PAUSED : HPE_OK); + } else { + assert(0 && "Attempting to pause parser in error state"); + } +} + +int +http_body_is_final(const struct http_parser *parser) { + return parser->state == s_message_done; +} + +unsigned long +http_parser_version(void) { + return HTTP_PARSER_VERSION_MAJOR * 0x10000 | + HTTP_PARSER_VERSION_MINOR * 0x00100 | + HTTP_PARSER_VERSION_PATCH * 0x00001; +} + +void +http_parser_set_max_header_size(uint32_t size) { + max_header_size = size; +} diff --git a/submodules/TelegramCallsUI/Resources/dash_player.html b/submodules/TelegramCallsUI/Resources/dash_player.html new file mode 100644 index 0000000000..458e945218 --- /dev/null +++ b/submodules/TelegramCallsUI/Resources/dash_player.html @@ -0,0 +1,17 @@ + + + + Dash.js Player + + + +
+ +
+ + + \ No newline at end of file diff --git a/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift b/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift index 2debd4072b..c28292bac5 100644 --- a/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift +++ b/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift @@ -1,4 +1,5 @@ import Foundation +import WebKit import UIKit import ComponentFlow import AccountContext @@ -6,11 +7,12 @@ import AVKit import MultilineTextComponent import Display import ShimmerEffect - import TelegramCore import SwiftSignalKit import AvatarNode import Postbox +import FFMpegBinding +import TelegramVoip final class MediaStreamVideoComponent: Component { let call: PresentationGroupCallImpl @@ -157,6 +159,8 @@ final class MediaStreamVideoComponent: Component { private var lastPresentation: UIView? private var pipTrackDisplayLink: CADisplayLink? + private var livePlayerView: ProxyVideoView? + override init(frame: CGRect) { self.blurTintView = UIView() self.blurTintView.backgroundColor = UIColor(white: 0.0, alpha: 0.55) @@ -211,7 +215,7 @@ final class MediaStreamVideoComponent: Component { let needsFadeInAnimation = hadVideo if loadingBlurView.superview == nil { - addSubview(loadingBlurView) + //addSubview(loadingBlurView) if needsFadeInAnimation { let anim = CABasicAnimation(keyPath: "opacity") anim.duration = 0.5 @@ -542,6 +546,21 @@ final class MediaStreamVideoComponent: Component { videoFrameUpdateTransition.setFrame(layer: self.videoBlurGradientMask, frame: videoBlurView.bounds) videoFrameUpdateTransition.setFrame(layer: self.videoBlurSolidMask, frame: self.videoBlurGradientMask.bounds) } + + if self.livePlayerView == nil { + let livePlayerView = ProxyVideoView(context: component.call.accountContext, call: component.call) + self.livePlayerView = livePlayerView + livePlayerView.layer.masksToBounds = true + self.addSubview(livePlayerView) + livePlayerView.frame = newVideoFrame + livePlayerView.layer.cornerRadius = videoCornerRadius + livePlayerView.update(size: newVideoFrame.size) + } + if let livePlayerView = self.livePlayerView { + videoFrameUpdateTransition.setFrame(view: livePlayerView, frame: newVideoFrame, completion: nil) + videoFrameUpdateTransition.setCornerRadius(layer: livePlayerView.layer, cornerRadius: videoCornerRadius) + livePlayerView.update(size: newVideoFrame.size) + } } else { videoSize = CGSize(width: 16 / 9 * 100.0, height: 100.0).aspectFitted(.init(width: availableSize.width - videoInset * 2, height: availableSize.height)) } @@ -601,7 +620,7 @@ final class MediaStreamVideoComponent: Component { } } - if self.noSignalTimeout { + if self.noSignalTimeout, !"".isEmpty { var noSignalTransition = transition let noSignalView: ComponentHostView if let current = self.noSignalView { @@ -769,3 +788,178 @@ private final class CustomIntensityVisualEffectView: UIVisualEffectView { animator.stopAnimation(true) } } + +private final class ProxyVideoView: UIView { + private let call: PresentationGroupCallImpl + private let id: Int64 + private let player: AVPlayer + private let playerItem: AVPlayerItem + private let playerLayer: AVPlayerLayer + + private var contextDisposable: Disposable? + + private var failureObserverId: AnyObject? + private var errorObserverId: AnyObject? + + private var server: AnyObject? + + init(context: AccountContext, call: PresentationGroupCallImpl) { + self.call = call + + self.id = Int64.random(in: Int64.min ... Int64.max) + + /*if #available(iOS 13.0, *) { + do { + let server = try HTTPServer(port: NWEndpoint.Port(integerLiteral: 8012), tcpOptions: nil, queue: .main, handler: { request, response in + if request.url == "/master.m3u8" { + let _ = (call.externalMediaStream.get() + |> take(1) + |> mapToSignal { externalMediaStream in + return externalMediaStream.masterPlaylistData() + } + |> take(1) + |> deliverOnMainQueue).start(next: { masterPlaylistData in + response.send(masterPlaylistData.data(using: .utf8)!) + }) + } else if request.url == "/hls_level_0.m3u8" { + let _ = (call.externalMediaStream.get() + |> take(1) + |> mapToSignal { externalMediaStream in + return externalMediaStream.playlistData(quality: 0) + } + |> take(1) + |> deliverOnMainQueue).start(next: { playlistData in + response.send(playlistData.data(using: .utf8)!) + }) + } else if request.url == "/hls_level_1.m3u8" { + let _ = (call.externalMediaStream.get() + |> take(1) + |> mapToSignal { externalMediaStream in + return externalMediaStream.playlistData(quality: 1) + } + |> take(1) + |> deliverOnMainQueue).start(next: { playlistData in + response.send(playlistData.data(using: .utf8)!) + }) + } else if request.url.hasPrefix("/hls_stream0_") && request.url.hasSuffix(".ts") { + if let partIndex = Int(request.url[request.url.index(request.url.startIndex, offsetBy: "/hls_stream0_".count).. take(1) + |> mapToSignal { externalMediaStream in + return externalMediaStream.partData(index: partIndex, quality: 0) + } + |> take(1) + |> deliverOnMainQueue).start(next: { partData in + guard let partData else { + return + } + + let sourceTempFile = TempBox.shared.tempFile(fileName: "part.mp4") + let tempFile = TempBox.shared.tempFile(fileName: "part.ts") + defer { + TempBox.shared.dispose(sourceTempFile) + TempBox.shared.dispose(tempFile) + } + + let _ = try? partData.write(to: URL(fileURLWithPath: sourceTempFile.path)) + + let sourcePath = sourceTempFile.path + FFMpegLiveMuxer.remux(sourcePath, to: tempFile.path, offsetSeconds: Double(partIndex)) + if let data = try? Data(contentsOf: URL(fileURLWithPath: tempFile.path)) { + response.send(data) + } else { + let _ = try? response.send("Error") + } + }) + } else { + try response.send("Error") + } + } else if request.url.hasPrefix("/hls_stream1_") && request.url.hasSuffix(".ts") { + if let partIndex = Int(request.url[request.url.index(request.url.startIndex, offsetBy: "/hls_stream1_".count).. take(1) + |> mapToSignal { externalMediaStream in + return externalMediaStream.partData(index: partIndex, quality: 1) + } + |> take(1) + |> deliverOnMainQueue).start(next: { partData in + guard let partData else { + return + } + + let sourceTempFile = TempBox.shared.tempFile(fileName: "part.mp4") + let tempFile = TempBox.shared.tempFile(fileName: "part.ts") + defer { + TempBox.shared.dispose(sourceTempFile) + TempBox.shared.dispose(tempFile) + } + + let _ = try? partData.write(to: URL(fileURLWithPath: sourceTempFile.path)) + + let sourcePath = sourceTempFile.path + FFMpegLiveMuxer.remux(sourcePath, to: tempFile.path, offsetSeconds: Double(partIndex)) + if let data = try? Data(contentsOf: URL(fileURLWithPath: tempFile.path)) { + response.send(data) + } else { + let _ = try? response.send("Error") + } + }) + } else { + try response.send("Error") + } + } else { + try response.send("Error") + } + }) + self.server = server + server.resume() + } catch let e { + print("HTTPServer start error: \(e)") + } + }*/ + + let assetUrl = "http://127.0.0.1:\(SharedHLSServer.shared.port)/\(call.internalId)/master.m3u8" + Logger.shared.log("MediaStreamVideoComponent", "Initializing HLS asset at \(assetUrl)") + #if DEBUG + print("Initializing HLS asset at \(assetUrl)") + #endif + let asset = AVURLAsset(url: URL(string: assetUrl)!, options: [:]) + self.playerItem = AVPlayerItem(asset: asset) + self.player = AVPlayer(playerItem: self.playerItem) + self.playerLayer = AVPlayerLayer(player: self.player) + + super.init(frame: CGRect()) + + self.failureObserverId = NotificationCenter.default.addObserver(forName: AVPlayerItem.failedToPlayToEndTimeNotification, object: playerItem, queue: .main, using: { notification in + print("Player Error: \(notification.description)") + }) + self.errorObserverId = NotificationCenter.default.addObserver(forName: AVPlayerItem.newErrorLogEntryNotification, object: playerItem, queue: .main, using: { notification in + print("Player Error: \(notification.description)") + }) + + self.layer.addSublayer(self.playerLayer) + + //self.contextDisposable = ResourceAdaptor.shared.addContext(id: self.id, context: context, fileReference: fileReference) + + self.player.play() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.contextDisposable?.dispose() + if let failureObserverId = self.failureObserverId { + NotificationCenter.default.removeObserver(failureObserverId) + } + if let errorObserverId = self.errorObserverId { + NotificationCenter.default.removeObserver(errorObserverId) + } + } + + func update(size: CGSize) { + self.playerLayer.frame = CGRect(origin: CGPoint(), size: size) + } +} + diff --git a/submodules/TelegramCallsUI/Sources/LocalServer/NWHTTPProtocol/HTTPContext.swift b/submodules/TelegramCallsUI/Sources/LocalServer/NWHTTPProtocol/HTTPContext.swift new file mode 100644 index 0000000000..d860bee1d7 --- /dev/null +++ b/submodules/TelegramCallsUI/Sources/LocalServer/NWHTTPProtocol/HTTPContext.swift @@ -0,0 +1,54 @@ +// +// HTTPContext.swift +// NWHTTPProtocol +// +// Copyright © 2020 ZeeZide GmbH. All rights reserved. +// + +import class Network.NWProtocolFramer +import class Network.NWConnection + +public extension Optional where Wrapped == NWConnection.ContentContext { + + /** + * Extract a HTTPProtocol Message from the context. + * + * Usage: + * + * guard let message = context.httpMessage else { + * print("Connection closed ...") + * connection.cancel() + * return + * } + * + * if let method = message.method { + * print("REQUEST:", method) + * } + */ + @available(iOS 13.0, *) + var httpMessage : NWProtocolFramer.Message? { self?.httpMessage } +} + +public extension NWConnection.ContentContext { + + /** + * Extract a HTTPProtocol Message from the context. + * + * Usage: + * + * guard let message = context?.httpMessage else { + * print("Connection closed...") + * connection.cancel() + * return + * } + * + * if let method = message.method { + * + * } + */ + @available(iOS 13.0, *) + var httpMessage : NWProtocolFramer.Message? { + return protocolMetadata(definition: HTTPProtocol.definition) + as? NWProtocolFramer.Message + } +} diff --git a/submodules/TelegramCallsUI/Sources/LocalServer/NWHTTPProtocol/HTTPMessage.swift b/submodules/TelegramCallsUI/Sources/LocalServer/NWHTTPProtocol/HTTPMessage.swift new file mode 100644 index 0000000000..cdb1a8cbd7 --- /dev/null +++ b/submodules/TelegramCallsUI/Sources/LocalServer/NWHTTPProtocol/HTTPMessage.swift @@ -0,0 +1,92 @@ +// +// HTTPMessage.swift +// NWHTTPProtocol +// +// Copyright © 2020 ZeeZide GmbH. All rights reserved. +// + +import class Network.NWProtocolFramer + +/** + * Message extensions to access the HTTPProtocol metadata, which are: + * - method (e.g. "GET") + * - path (e.g. "/vaca") + * - status (e.g. 402) + * - headers (e.g. [ ( "Content-Type", "text/html" ), + * ( "ETag", "42" ) ]) + * - error + */ + +@available(iOS 13.0, *) +public extension NWProtocolFramer.Message { + + static var httpMessage : NWProtocolFramer.Message { + .init(definition: HTTPProtocol.definition) + } + + convenience init(method : HTTPProtocol.Method = "GET", + path : String, // really a URI + headers : HTTPProtocol.Headers = [], + shouldKeepAlive : Bool = false) + { + self.init(definition: HTTPProtocol.definition) + self.method = method + self.path = path + self.headers = headers + self.shouldKeepAlive = shouldKeepAlive + } + + convenience init(status : HTTPProtocol.Status, + headers : HTTPProtocol.Headers = []) + { + self.init(definition: HTTPProtocol.definition) + self.status = status + self.headers = headers + } + + var method : HTTPProtocol.Method? { + set { self["http.method"] = newValue } + get { self["http.method"] as? HTTPProtocol.Method } + } + + var path : String? { + set { self["http.path"] = newValue } + get { self["http.path"] as? String } + } + + var status : HTTPProtocol.Status? { + set { self["http.status"] = newValue } + get { self["http.status"] as? HTTPProtocol.Status } + } + + var headers : HTTPProtocol.Headers { + set { self["http.headers"] = newValue } + get { (self["http.headers"] as? HTTPProtocol.Headers) ?? [] } + } + + var isEndOfMessage : Bool { + set { self["http.eom"] = newValue } + get { (self["http.eom"] as? Bool) ?? false } + } + + var shouldKeepAlive : Bool { + set { self["http.keepalive"] = newValue } + get { (self["http.keepalive"] as? Bool) ?? false } + } +} + +@available(iOS 13.0, *) +public extension NWProtocolFramer.Message { + + internal convenience init(error: HTTPProtocol.Error) { + self.init(definition: HTTPProtocol.definition) + self["http.error"] = error + } + + /** + * Contains a `HTTPProtocol.Error` if the parser failed. + */ + var error : HTTPProtocol.Error? { + self["http.error"] as? HTTPProtocol.Error + } +} diff --git a/submodules/TelegramCallsUI/Sources/LocalServer/NWHTTPProtocol/HTTPProtocol.swift b/submodules/TelegramCallsUI/Sources/LocalServer/NWHTTPProtocol/HTTPProtocol.swift new file mode 100644 index 0000000000..5040d2855f --- /dev/null +++ b/submodules/TelegramCallsUI/Sources/LocalServer/NWHTTPProtocol/HTTPProtocol.swift @@ -0,0 +1,369 @@ +// +// HTTPProtocol.swift +// NWHTTPProtocol +// +// Copyright © 2020 ZeeZide GmbH. All rights reserved. +// + +import Foundation +import Network +import CHTTPParser + +/** + * A Network.framework HTTP protocol parser (aka a NWProtocolFramer). + * + * Just wrapping http_parser.c/h in the API boilerplate required for + * Network.framework protocols. + */ +@available(iOS 13.0, *) +public final class HTTPProtocol: NWProtocolFramerImplementation { + // Note: We don't really need a framer here because http_parser does all the + // framing already. + + public enum Error: Swift.Error { + case parserError(name: String, description: String) + case unknownMessageType + case missingURI + case writeFailed(Swift.Error) + } + + public typealias Method = String + public typealias Header = ( name: String, value: String ) + public typealias Headers = [ Header ] + public typealias Status = Int + + // Not required by protocol, but used for: + // - protocol registration in protocol stack + // - message object construction + // - message object retrieval from context in receive + public static let definition = + NWProtocolFramer.Definition(implementation: HTTPProtocol.self) + + public static let label = "HTTP" + + private var parser = UnsafeMutablePointer.allocate(capacity: 1) + private var settings = http_parser_settings() + + public init(framer: NWProtocolFramer.Instance) { + // TBD: maybe we could get the parser mode as an HTTP protocol option?! + http_parser_init(parser, HTTP_BOTH) + parser.pointee.data = unsafeBitCast(self, to: UnsafeMutableRawPointer.self) + + settings.on_message_begin = { $0?.framer.onBegin() ; return 0 } + settings.on_message_complete = { $0?.framer.messageFinished() ; return 0 } + settings.on_headers_complete = { $0?.framer.headerFinished() ; return 0 } + + settings.on_url = { parser, data, len in + parser?.framer.processDataForState(.url, data, len) ?? 1 + } + settings.on_header_field = { parser, data, len in + parser?.framer.processDataForState(.headerName, data, len) ?? 1 + } + settings.on_header_value = { parser, data, len in + parser?.framer.processDataForState(.headerValue, data, len) ?? 1 + } + settings.on_body = { parser, data, len in + parser?.framer.processDataForState(.body, data, len) ?? 1 + } + } + deinit { + parser.deallocate() + } + + + // MARK: - Lifecycle + + public func start (framer: NWProtocolFramer.Instance) + -> NWProtocolFramer.StartResult { return .ready } + public func stop (framer: NWProtocolFramer.Instance) -> Bool { return true } + public func wakeup (framer: NWProtocolFramer.Instance) {} + public func cleanup(framer: NWProtocolFramer.Instance) {} + + + // MARK: - Parsing + + private enum ParseState { + case idle, url, headerName, headerValue, body + } + + private var parseState = ParseState.idle + private var url : String? + private var lastName : String? + private var headers = [ ( name: String, value: String) ]() + private var buffer = Data(capacity: 500) + private var activeFramer : NWProtocolFramer.Instance? + + private func onBegin() { + clearState() + } + + private func clearState() { + self.url = nil + self.lastName = nil + self.headers.removeAll(keepingCapacity: true) + } + + private func emit(_ message: NWProtocolFramer.Message, + to framer: NWProtocolFramer.Instance) + { + let ok = framer.deliverInputNoCopy(length : 0, + message : message, + isComplete : true) + assert(ok) + } + private func emit(_ data : Data, + to framer : NWProtocolFramer.Instance) + { + let message = NWProtocolFramer.Message.httpMessage + message.isEndOfMessage = http_body_is_final (parser) != 0 + message.shouldKeepAlive = http_should_keep_alive(parser) != 0 + framer.deliverInput(data: data, message: message, isComplete: true) + } + private func emit(_ data : UnsafeBufferPointer, + to framer : NWProtocolFramer.Instance) + { + emit(Data(buffer: data), to: framer) + } + + private func headerFinished() { + _ = processDataForState(.body, nil, 0) // start body + + assert(activeFramer != nil) + guard let framer = activeFramer else { return } + + // We could also do HTTP major/minor too, not really important. + + if isRequest { + assert(!isResponse) + defer { clearState()} + + + let method = http_method(CUnsignedInt(parser.pointee.method)).stringValue + guard let url = url else { + return emit(.init(error: .missingURI), to: framer) + } + return emit(.init(method: method, path: url, headers: headers, + shouldKeepAlive: http_should_keep_alive(parser) != 0), + to: framer) + } + + if isResponse { + assert(!isResponse) + defer { clearState()} + + let status = Int(parser.pointee.status_code) + return emit(.init(status: status, headers: headers), to: framer) + } + + emit(.init(error: .unknownMessageType), to: framer) + } + + private func messageFinished() { + _ = processDataForState(.idle, nil, 0) // flush end body + + if let framer = activeFramer { + // send explicit EOM + let message = NWProtocolFramer.Message.httpMessage + message.isEndOfMessage = true + message.shouldKeepAlive = http_should_keep_alive(parser) != 0 + emit(message, to: framer) + } + } + + private var isRequest : Bool { return parser.pointee.type == 0 } + private var isResponse : Bool { return parser.pointee.type == 1 } + + private func addData(_ data: UnsafePointer?, _ length: Int) -> Int32 { + switch parseState { + + case .idle: + guard length == 0 else { + assertionFailure("receiving data in idle state!") + return 1 + } + return 0 + + case .url, .headerName, .headerValue: + guard length > 0 else { return 0 } + buffer.append(UnsafeBufferPointer(start: data, count: length)) + return 0 + + case .body: + guard let framer = activeFramer else { return 1 } + guard length > 0 else { return 1 } + assert(activeFramer != nil) + + // TBD: Can we avoid the copying? Maybe. Would need to track more + // things (i.e. whether state switches happened). + let bptr = UnsafeBufferPointer(start: data, count: length) + emit(bptr, to: framer) + return 0 + } + } + + private func flushOldState(_ oldState: ParseState) -> Int32 { + defer { buffer.removeAll() } + + var bufferAsString : String? { + return String(data: buffer, encoding: .utf8) // Hm, latin-1? ;-) + } + + switch oldState { // finish up OLD parse state + case .idle: + assert(buffer.isEmpty) + guard buffer.isEmpty else { return 1 } + + case .headerValue: + assert(lastName != nil) + guard let n = lastName, let s = bufferAsString else { return 1 } + headers.append( (name: n, value: s) ) + lastName = nil + + case .headerName: + assert(lastName == nil) + guard lastName == nil, let s = bufferAsString else { return 1 } + lastName = s + + case .url: + assert(url == nil) + guard url == nil, let s = bufferAsString else { return 1 } + url = s + + case .body: + if !buffer.isEmpty { + guard let framer = activeFramer else { return 1 } + emit(buffer, to: framer) + } + } + + return 0 + } + + private func processDataForState(_ state: ParseState, + _ ptr: UnsafePointer?, _ length: Int) + -> Int32 + { + if state == parseState { // more data for same field + return addData(ptr, length) + } + + let rc = flushOldState(parseState) + if rc != 0 { return rc } + assert(buffer.isEmpty) + + /* start thew new state */ + parseState = state + return addData(ptr, length) + } + + public func handleInput(framer: NWProtocolFramer.Instance) -> Int { + // Note: http_parser itself does all the framing already + assert(activeFramer == nil, "handleInput reentrancy") + activeFramer = framer + defer { activeFramer = nil } + + _ = framer.parseInput(minimumIncompleteLength: 1, + maximumLength: 1024 * 1024) + { + buffer, isComplete in + + let bytesConsumed = http_parser_execute( + parser, &settings, + buffer?.baseAddress?.assumingMemoryBound(to: Int8.self), + buffer?.count ?? 0 + ) + + let errno = http_errno(parser.pointee.http_errno) + if errno != HPE_OK { + let error = HTTPProtocol.Error + .parserError(name: errno.name, description: errno.description) + + emit(.init(error: error), to: framer) + if bytesConsumed < 1 { return buffer?.count ?? 1 } // Hm + } + + return bytesConsumed + } + return 0 + } + + + // MARK: - Output + + public func handleOutput(framer : NWProtocolFramer.Instance, + message : NWProtocolFramer.Message, + messageLength : Int, + isComplete : Bool) + { + // We can also support trailers and chunking, but would need to + // track more output state. + + assert(isComplete) + + func headerData(with firstLine: String) -> Data { + var header = firstLine + message.headers.forEach { ( name, value ) in + header += name + ": " + value + "\r\n" + } + header += "\r\n" + guard let data = header.data(using: .utf8) else { + assertionFailure("could not encode header data?") + return Data() + } + return data + } + + if let status = message.status { + assert(message.method == nil, "message has status AND method?!") + let reason = http_status(UInt32(status)).reason + let data = headerData( + with: "HTTP/1.1 \(status) \(reason)\r\n") + assert(!data.isEmpty) + framer.writeOutput(data: data) + } + else if let method = message.method { + assert(message.status == nil, "message has status AND method?!") + assert(message.path == nil, "method but no path?") + let uri = message.path ?? "/" + let data = headerData(with: "\(method) \(uri) HTTP/1.1\r\n") + assert(!data.isEmpty) + framer.writeOutput(data: data) + } + + // body data (the user can also include some in the original message) + if messageLength > 0 { + do { + assert(messageLength > 0) + try framer.writeOutputNoCopy(length: messageLength) + } + catch { + assertionFailure("write error: \(error)") + let error = HTTPProtocol.Error.writeFailed(error) + emit(.init(error: error), to: framer) + } + } + } +} + + +// MARK: - Helper Extensions + +fileprivate extension http_method { + var stringValue : String { return String(cString: http_method_str(self)) } +} +fileprivate extension http_status { + var reason : String { return String(cString: http_status_str(self)) } +} +fileprivate extension http_errno { + var name : String { return String(cString: http_errno_name(self)) } + var description : String { + return String(cString: http_errno_description(self)) + } +} + +@available(iOS 13.0, *) +fileprivate extension UnsafeMutablePointer where Pointee == http_parser { + var framer : HTTPProtocol { + return unsafeBitCast(pointee.data, to: HTTPProtocol.self) + } +} diff --git a/submodules/TelegramCallsUI/Sources/LocalServer/NWHTTPProtocol/README.md b/submodules/TelegramCallsUI/Sources/LocalServer/NWHTTPProtocol/README.md new file mode 100644 index 0000000000..53c2dade31 --- /dev/null +++ b/submodules/TelegramCallsUI/Sources/LocalServer/NWHTTPProtocol/README.md @@ -0,0 +1,33 @@ +# NWHTTPProtocol + +![Swift5](https://img.shields.io/badge/swift-5-blue.svg?style=flat) +![macOS](https://img.shields.io/badge/os-macOS-green.svg?style=flat) +![iOS](https://img.shields.io/badge/os-iOS-green.svg?style=flat) + +An HTTP protocol parser (aka +[NWProtocolFramer](https://developer.apple.com/documentation/network/nwprotocolframer)) +for the Apple +[Network](https://developer.apple.com/documentation/network).framework. + +Network.framework requires iOS 13+ / macOS 10.15+. + +This is intentionally kept very simple and basic. E.g. it does not define +Swift types for HTTP requests and the like, but puts the status/request/URI in plain +`NWProtocolFramer.Message` metadata fields. + +The protocol is just wrapping the (embedded) +[http_parser.c/h](https://github.com/nodejs/http-parser/) +developed as part of the Node.js project. + +### Who + +**NWHTTPProtocol** is brought to you by +the +[Always Right Institute](http://www.alwaysrightinstitute.com) +and +[ZeeZide](http://zeezide.de). +We like +[feedback](https://twitter.com/ar_institute), +GitHub stars, +cool [contract work](http://zeezide.com/en/services/services.html), +presumably any form of praise you can think of. diff --git a/submodules/TelegramCallsUI/Sources/LocalServer/NWHTTPServer/HTTPMethod.swift b/submodules/TelegramCallsUI/Sources/LocalServer/NWHTTPServer/HTTPMethod.swift new file mode 100644 index 0000000000..f3a6165607 --- /dev/null +++ b/submodules/TelegramCallsUI/Sources/LocalServer/NWHTTPServer/HTTPMethod.swift @@ -0,0 +1,63 @@ +// +// HTTPMethod.swift +// NWHTTPServer +// +// Copyright © 2020 ZeeZide GmbH. All rights reserved. +// + +public struct HTTPMethod: RawRepresentable, Hashable { + + public let rawValue : String + + @inlinable + public init(rawValue string: String) { rawValue = string } +} + +extension HTTPMethod: CustomStringConvertible { + @inlinable + public var description: String { return rawValue } +} + +@inlinable +public func ==(lhs: HTTPMethod, rhs: String) -> Bool { + return lhs.rawValue == rhs +} +@inlinable +public func ==(lhs: String, rhs: HTTPMethod) -> Bool { + return lhs == rhs.rawValue +} + +public extension HTTPMethod { + static let GET : HTTPMethod = "GET" + static let POST : HTTPMethod = "POST" + static let MKCALENDAR : HTTPMethod = "MKCALENDAR" + static let DELETE : HTTPMethod = "DELETE" + static let HEAD : HTTPMethod = "HEAD" + static let PUT : HTTPMethod = "PUT" + static let CONNECT : HTTPMethod = "CONNECT" + static let OPTIONS : HTTPMethod = "OPTIONS" + static let TRACE : HTTPMethod = "TRACE" + static let COPY : HTTPMethod = "COPY" + static let LOCK : HTTPMethod = "LOCK" + static let MKCOL : HTTPMethod = "MKCOL" + static let MOVE : HTTPMethod = "MOVE" + static let PROPFIND : HTTPMethod = "PROPFIND" + static let PROPPATCH : HTTPMethod = "PROPPATCH" + static let SEARCH : HTTPMethod = "SEARCH" + static let UNLOCK : HTTPMethod = "UNLOCK" + static let REPORT : HTTPMethod = "REPORT" + static let MKACTIVITY : HTTPMethod = "MKACTIVITY" + static let CHECKOUT : HTTPMethod = "CHECKOUT" + static let MERGE : HTTPMethod = "MERGE" + static let MSEARCH : HTTPMethod = "MSEARCH" + static let NOTIFY : HTTPMethod = "NOTIFY" + static let SUBSCRIBE : HTTPMethod = "SUBSCRIBE" + static let UNSUBSCRIBE : HTTPMethod = "UNSUBSCRIBE" + static let PATCH : HTTPMethod = "PATCH" + static let PURGE : HTTPMethod = "PURGE" +} + +extension HTTPMethod: ExpressibleByStringLiteral { + @inlinable + public init(stringLiteral string: String) { self.init(rawValue: string) } +} diff --git a/submodules/TelegramCallsUI/Sources/LocalServer/NWHTTPServer/HTTPServer.swift b/submodules/TelegramCallsUI/Sources/LocalServer/NWHTTPServer/HTTPServer.swift new file mode 100644 index 0000000000..54b4feea96 --- /dev/null +++ b/submodules/TelegramCallsUI/Sources/LocalServer/NWHTTPServer/HTTPServer.swift @@ -0,0 +1,372 @@ +// +// HTTPServer.swift +// NWHTTPServer +// +// Copyright © 2020-2022 ZeeZide GmbH. All rights reserved. +// + +import class Dispatch.DispatchQueue +import func Dispatch.dispatchMain +import class Network.NWListener +import class Network.NWConnection +import class Network.NWParameters +import class Network.NWProtocolFramer +import class Network.NWProtocolTCP +import enum Network.NWEndpoint + +/** + * A very simple Network.framework based HTTP server. + * + * Example: + * + * let server = HTTPServer { request, response in + * print("Received:", request) + * try response.send("Hello!\n") + * } + * server.run() + * + */ +@available(iOS 13.0, *) +public final class HTTPServer { + + let queue : DispatchQueue + let handler : ( IncomingMessage, ServerResponse ) throws -> Void + let listener : NWListener + + private var clients = [ ObjectIdentifier : ConnectionState ]() + private var _listenCB : (( HTTPServer ) -> Void)? + private var _errorCB : (( HTTPServer, Swift.Error ) -> Void)? + + /** + * Initialize an HTTP server. + * + * Example: + * + * let server = HTTPServer { request, response in + * print("Received:", request) + * try response.send("Hello!\n") + * } + * + * - Parameters: + * - port: The TCP port the server should listen on (default: 8000) + * - tcpOptions: Optional, alternative Network framework TCP options + * - queue: The DispatchQueue the server operates on. This should be + * a _serial_ queue. Defaults to `.main` + * - handler: Closure to invoke when an HTTP request was received. + */ + public init(port : NWEndpoint.Port = 8000, + tcpOptions : NWProtocolTCP.Options? = nil, + queue : DispatchQueue = .main, + handler : @escaping + ( IncomingMessage, ServerResponse ) throws -> Void) + throws + { + self.queue = queue + self.handler = handler + + let params : NWParameters = { + let params = NWParameters(tls: nil, tcp: tcpOptions ?? { + let tcpOptions = NWProtocolTCP.Options() + tcpOptions.enableKeepalive = true + tcpOptions.keepaliveIdle = 2 + return tcpOptions + }()) + + let httpProtocol = + NWProtocolFramer.Options(definition: HTTPProtocol.definition) + + params.defaultProtocolStack + .applicationProtocols + .insert(httpProtocol, at: 0) + + return params + }() + + listener = try NWListener(using: params, on: port) + } + + /** + * Starts the server, never returns + * + * Example: + * + * server.run() + * + * For more fine grained flow control, use `resume` and `suspend`. + */ + public func run() -> Never { + resume() + dispatchMain() + } + + /** + * Starts the server in the background. + * + * Note: This does not keep a commandline tool (or thread) running, use `run` + * to start a server "forever". + */ + public func resume() { + listener.newConnectionHandler = handleNewConnection + listener.stateUpdateHandler = handleListenerStateChange + listener.start(queue: queue) + } + + /** + * Stops the server and cancels all running connections. + */ + public func suspend() { + listener.cancel() + listener.newConnectionHandler = nil + listener.stateUpdateHandler = nil + + // TBD: we could also offer a more graceful option + for state in clients.values { + state.cancel() + unregister(state.connection) + } + assert(clients.isEmpty) + clients = [:] + } + + + // MARK: - Events + + /** + * Register a callback to be called when the server starts accepting requests. + * + * - Parameter execute: A closure to run when the server actually started + * listening (i.e. can be reached by clients). + */ + public func onListen(execute: @escaping ( HTTPServer ) -> Void) { + _listenCB = execute + } + + /** + * Register an error handler which will be invoked when errors happen on + * either the listener or the connection. + * + * - Parameter execute: A closure to run when the server encounters an error. + */ + public func onError(execute: @escaping ( HTTPServer, Swift.Error ) -> Void) { + _errorCB = execute + } + + + // MARK: - Connection Handling + + private enum ConnectionState { + case idle (NWConnection) + case running(NWConnection, IncomingMessage, ServerResponse) + + var connection : NWConnection { + switch self { + case .idle (let c) : return c + case .running(let c, _, _) : return c + } + } + func cancel() { + switch self { + case .idle(let c): + c.cancel() + case .running(let c, let m, let res): + if !m.readableEnded { m.push(nil) } + if !res.writableEnded { res.end() } + c.cancel() + } + } + var incomingMessage : IncomingMessage? { + switch self { + case .idle : return nil + case .running(_, let m, _) : return m + } + } + } + + private func handleListenerStateChange(_ state: NWListener.State) { + switch state { + case .setup, .waiting : break + case .ready : _listenCB?(self) + case .cancelled : suspend() + + case .failed(let error): + suspend() + _errorCB?(self, error) + + @unknown default: break + } + } + + private func handleStateChange(_ state: NWConnection.State, + on connection: NWConnection) + { + switch state { + case .setup, .preparing, .waiting: break + case .ready : readNextMessage(from: connection) + case .cancelled : unregister(connection) + + case .failed(let error): + unregister(connection) + _errorCB?(self, error) + + @unknown default: break + } + } + + private func unregister(_ connection: NWConnection) { + if let state = clients.removeValue(forKey: ObjectIdentifier(connection)) { + if case .running(_, let req, let res) = state { + if !req.readableEnded { req.push(nil) } + if !res.writableEnded { res.end() } + } + } + connection.stateUpdateHandler = nil + } + + private func handleNewConnection(_ connection: NWConnection) { + let oid = ObjectIdentifier(connection) + assert(clients[oid] == nil) + clients[oid] = .idle(connection) + + connection.stateUpdateHandler = { + self.handleStateChange($0, on: connection) + } + connection.start(queue: queue) + } + + private func readNextMessage(from connection: NWConnection) { + let oid = ObjectIdentifier(connection) + + connection.receiveMessage { data, context, isComplete, error in + assert(isComplete || error != nil) // right? + + guard let state = self.clients[oid] else { // already cancelled. + // can happen when the server is suspended (that cancels all the + // connections and drops all states synchronously). + return + } + + func emitError(_ error: Swift.Error) { + if case .running(_, let req, _) = state, req.emitError(error) { + // error handled as part of the request + } + else if let cb = self._errorCB { + cb(self, error) + } + } + func endBothEnds() { + if case .running(_, let req, let res) = state { + if !req.readableEnded { req.push(nil) } + if !res.writableEnded { res.end() } + } + } + + if let error = error { + emitError(error) + connection.cancel() // no recovery possible? + return endBothEnds() + } + + guard let message = context.httpMessage else { + switch state { + case .idle: // hit if the client closes a keep-alive connection + connection.cancel() + return endBothEnds() + case .running(_, let req, _): + if !req.readableEnded { req.push(nil) } + // Do not close `response`(/connection), we could still be writing! + } + return + } + + if let error = message.error { + emitError(error) + connection.cancel() // no recovery possible? + return endBothEnds() + } + + /* HTTP Request HEAD */ + if let method = message.method, let path = message.path { + if case .running(_, let req, _) = state { + if !req.readableEnded { req.push(nil) } + } + + let req = IncomingMessage(method: .init(rawValue: method), path: path, + headers: message.headers) + let res = ServerResponse(keepAlive: false) + res.connection = connection + + res.onEnd { res in + if let state = self.clients[oid], + case .running(let connection, _, _) = state, + connection === res.connection + { + self.clients[oid] = .idle(connection) + } + } + + self.clients[oid] = ConnectionState.running(connection, req, res) + + do { + try self.handler(req, res) + return self.readNextMessage(from: connection) + } + catch { + emitError(error) + if !res.writableEnded && !res.didWriteHead { + res.keepAlive = false + res.writeHead(status: 500, headers: [ "Connection": "close" ]) + res.end() + } + return connection.cancel() + } + } + + /* HTTP response HEAD */ + if let _ = message.status { + // attempt to send a response to the server :-) + switch state { + case .idle : break + case .running : endBothEnds() + } + return connection.cancel() + } + + + func endRequest() { + if case .running(_, let req, _) = state { + if !req.readableEnded { req.push(nil) } + } + } + + if let data = data { + assert(message.status == nil && message.method == nil) + switch state { + case .idle: + assertionFailure("receiving body data, w/o a HEAD?") + return connection.cancel() + + case .running(_, let req, let res): + res.keepAlive = message.shouldKeepAlive + if data.isEmpty { + assert(message.isEndOfMessage) + endRequest() + } + else { + req.push(data) + if message.isEndOfMessage { endRequest() } + } + return self.readNextMessage(from: connection) + } + } + + if message.isEndOfMessage { + endRequest() + return self.readNextMessage(from: connection) + } + + assertionFailure("got neither data nor metadata? \(message)") + return connection.cancel() + } + } +} + diff --git a/submodules/TelegramCallsUI/Sources/LocalServer/NWHTTPServer/HTTPStatus.swift b/submodules/TelegramCallsUI/Sources/LocalServer/NWHTTPServer/HTTPStatus.swift new file mode 100644 index 0000000000..3030de37da --- /dev/null +++ b/submodules/TelegramCallsUI/Sources/LocalServer/NWHTTPServer/HTTPStatus.swift @@ -0,0 +1,34 @@ +// +// HTTPStatus.swift +// NWHTTPServer +// +// Copyright © 2020 ZeeZide GmbH. All rights reserved. +// + + +public struct HTTPStatus: RawRepresentable, Hashable { + public let rawValue : Int + public init(rawValue status: Int) { self.rawValue = status } +} + +extension HTTPStatus { + public init(_ status: Int) { self.init(rawValue: status) } +} + +extension HTTPStatus: ExpressibleByIntegerLiteral { + public init(integerLiteral status: Int) { self.init(rawValue: status) } +} + +public extension HTTPStatus { + + static let ok : HTTPStatus = 200 + static let created : HTTPStatus = 201 + static let noContent : HTTPStatus = 204 + + static let badRequest : HTTPStatus = 400 + static let paymentRequired : HTTPStatus = 402 + static let forbidden : HTTPStatus = 403 + static let notFound : HTTPStatus = 404 + + static let serverError : HTTPStatus = 500 +} diff --git a/submodules/TelegramCallsUI/Sources/LocalServer/NWHTTPServer/IncomingMessage.swift b/submodules/TelegramCallsUI/Sources/LocalServer/NWHTTPServer/IncomingMessage.swift new file mode 100644 index 0000000000..5835f3ab54 --- /dev/null +++ b/submodules/TelegramCallsUI/Sources/LocalServer/NWHTTPServer/IncomingMessage.swift @@ -0,0 +1,309 @@ +// +// IncomingMessage.swift +// NWHTTPServer +// +// Copyright © 2020 ZeeZide GmbH. All rights reserved. +// + +import struct Foundation.Data +import class Foundation.JSONDecoder +import class Foundation.JSONSerialization + +/** + * Represents an incoming HTTP message. + * + * This can be both, a Request or a Response - it is a Response when it got + * created by a client and it is a Request if it is coming from the Server. + * + * The content of the message can be either streamed to the client, + * or if no data callback is set, it will get buffered within the + * object. + * In both cases one needs to wait for `end`! + * + * Content Buffering: + * + * let server = try HTTPServer { request, response in + * print("Request:", request) + * request.onEnd { + * print("received content:", request.content) + * response.send("OK, got it!\n") + * } + * } + * + * Content Streaming: + * + * let server = try HTTPServer { request, response in + * print("Request:", request) + * request.onData { data in + * print("received data chunk:", data) + * } + * request.onEnd { + * response.send("OK, got it!\n") + * } + * } + * + */ +@available(iOS 13.0, *) +open class IncomingMessage: CustomStringConvertible { + // Most is marked `open` in case the consumer wants to patch stuff in a + // subclass. + + public enum IncomingType { + case request (method: HTTPMethod, path: String) + case response(status: HTTPStatus) + } + + open var messageType : IncomingType + open var headers : HTTPProtocol.Headers + open var bufferedData = Data() + open var readableEnded = false + + open var _errorCB : (( Swift.Error ) -> Void)? + open var _dataCB : (( Data ) -> Void)? + open var _endCB : (() -> Void)? + + public init(method: HTTPMethod, path: String, + headers: HTTPProtocol.Headers = []) + { + self.messageType = .request(method: method, path: path) + self.headers = headers + } + public init(status: HTTPStatus, headers: HTTPProtocol.Headers = []) { + self.messageType = .response(status: status) + self.headers = headers + } + + + // MARK: - Callbacks + + /** + * Register a callback to be executed when content is received. When a + * callback is set, the buffer won't get filled. + * + * If data was accumulated already, it will be flushed to that closure. + * + * Example: + * + * request.onData { data in + * print("received data chunk:", data) + * } + * + */ + open func onData(execute: @escaping ( Data ) -> Void) { + _dataCB = execute + flush() // TBD: async? + } + + /** + * Register a callback to be executed when the request has been retrieved + * completely, i.e. all body data has been read. + */ + open func onEnd(execute: @escaping () -> Void) { + guard !readableEnded else { return execute() } // TBD: async? + _endCB = execute + } + + /** + * Register a callback to be executed when request specific errors arrive. + * If none is set, errors will be sent to the HTTPserver error handler. + */ + open func onError(execute: @escaping ( Swift.Error ) -> Void) { + _errorCB = execute + } + + internal func emitError(_ error: Swift.Error) -> Bool { + guard let cb = _errorCB else { return false } + cb(error) + return true + } + + + // MARK: - Receiving Body Data + + private func flush() { + guard let dataCB = _dataCB else { return } + guard !bufferedData.isEmpty else { return } + let data = bufferedData; bufferedData = Data() + dataCB(data) + } + private func invalidate() { + _dataCB = nil + _endCB = nil + _errorCB = nil + } + + /** + * Push new body data into the request. Push `nil` for end-of-message. + */ + open func push(_ data: Data?) { + assert(!readableEnded) + guard !readableEnded else { return } + + guard let data = data else { // EOF + readableEnded = true + flush() + _endCB?() + return invalidate() + } + + if let dataCB = _dataCB { + flush() + dataCB(data) + } + else { + bufferedData.append(data) + } + } + + + // MARK: - Content Accessors (when used w/o a callback) + + /** + * Returns all body data buffered so far. Note that this will be empty at the + * time the HTTPServer handler is invoked! + * To wait for all content to arrive in the buffer, use the `onEnd` callback. + * + * Example: + * + * request.onEnd { + * print("received content:", request.content) + * } + * + * No data will be buffered if the user has installed an `onData` handler. + */ + open var content : Data { + return bufferedData + } + + /** + * Returns all body data buffered so far. Note that this will be empty at the + * time the HTTPServer handler is invoked! + * To wait for all content to arrive in the buffer, use the `onEnd` callback. + * + * This variant tries to return the content as an UTF-8 string. If a + * conversion to UTF-8 fails, an error will be emitted and nil will be + * returned. + * + * Example: + * + * request.onEnd { + * print("received content:", request.contentAsString ?? "-") + * } + * + * No data will be buffered if the user has installed an `onData` handler. + */ + open var contentAsString : String? { + // TODO: scan for charset in headers :-) + guard !content.isEmpty else { return "" } + guard let s = String(data: content, encoding: .utf8) else { + _errorCB?(StringEncodingError(encoding: .utf8)) + return nil + } + return s + } + + /** + * Returns all body data buffered so far. Note that this will be empty at the + * time the HTTPServer handler is invoked! + * To wait for all content to arrive in the buffer, use the `onEnd` callback. + * + * This variant tries to parse the content as JSON into a Decodable type + * provided. + * + * Example: + * + * struct Entry: Codable { + * let date : Date + * let title : String + * let body : String + * } + * + * request.onEnd { + * guard let entry = try? request.decodeJSON(as: Entry.self) else { + * response.writeHead(status: badRequest) + * response.end() + * return + * } + * print("received entry:", entry) + * response.send("got entry!") + * } + * + * No data will be buffered if the user has installed an `onData` handler. + */ + open func decodeJSON(as type: T.Type) throws -> T { + return try JSONDecoder().decode(type, from: content) + } + + /** + * Returns all body data buffered so far. Note that this will be empty at the + * time the HTTPServer handler is invoked! + * To wait for all content to arrive in the buffer, use the `onEnd` callback. + * + * This variant tries to parse the content as JSON into property list values. + * + * Example: + * + * request.onEnd { + * guard let entry = try? request.decodeJSON() + * as? [ String : String] else + * { + * response.writeHead(status: badRequest) + * response.end() + * return + * } + * print("received entry:", entry) + * response.send("got entry!") + * } + * + * No data will be buffered if the user has installed an `onData` handler. + */ + open func decodeJSON(options : JSONSerialization.ReadingOptions = []) throws + -> Any + { + return try JSONSerialization.jsonObject(with: content, options: options) + } + + + // MARK: - HTTP Requests + + @inlinable + public var method : HTTPMethod { + guard case .request(let method, _) = messageType else { return "" } + return method + } + + @inlinable + public var url : String { + guard case .request(_, let path) = messageType else { return "" } + return path + } + + // MARK: - HTTP Responses + + @inlinable + public var statusCode : Int { + guard case .response(let status) = messageType else { return 0 } + return status.rawValue + } + + + // MARK: - Description + + open var description: String { + var ms = "<" + + switch messageType { + case .request(let method, let path): + ms += method.rawValue + ": " + path + case .response(let status): + ms += "\(status)" + } + + headers.forEach { ( name, value ) in + ms += " " + name + "=" + value + } + + ms += ">" + return ms + } +} diff --git a/submodules/TelegramCallsUI/Sources/LocalServer/NWHTTPServer/README.md b/submodules/TelegramCallsUI/Sources/LocalServer/NWHTTPServer/README.md new file mode 100644 index 0000000000..b52d738c00 --- /dev/null +++ b/submodules/TelegramCallsUI/Sources/LocalServer/NWHTTPServer/README.md @@ -0,0 +1,32 @@ +# NWHTTPServer + +![Swift5](https://img.shields.io/badge/swift-5-blue.svg?style=flat) +![macOS](https://img.shields.io/badge/os-macOS-green.svg?style=flat) +![iOS](https://img.shields.io/badge/os-iOS-green.svg?style=flat) + +A very simple HTTP server +for the Apple +[Network](https://developer.apple.com/documentation/network).framework. +Based on the `NWHTTPProtocol`. + +Example: +```swift +let server = HTTPServer { request, response in + print("Received:", request) + try response.send("Hello!\n") +} +server.run() +``` + +### Who + +**NWHTTPProtocol** is brought to you by +the +[Always Right Institute](http://www.alwaysrightinstitute.com) +and +[ZeeZide](http://zeezide.de). +We like +[feedback](https://twitter.com/ar_institute), +GitHub stars, +cool [contract work](http://zeezide.com/en/services/services.html), +presumably any form of praise you can think of. diff --git a/submodules/TelegramCallsUI/Sources/LocalServer/NWHTTPServer/ServerResponse.swift b/submodules/TelegramCallsUI/Sources/LocalServer/NWHTTPServer/ServerResponse.swift new file mode 100644 index 0000000000..f2a8ff5684 --- /dev/null +++ b/submodules/TelegramCallsUI/Sources/LocalServer/NWHTTPServer/ServerResponse.swift @@ -0,0 +1,396 @@ +// +// ServerResponse.swift +// NWHTTPServer +// +// Copyright © 2020 ZeeZide GmbH. All rights reserved. +// + +import struct Foundation.Data +import class Foundation.JSONEncoder +import class Foundation.JSONSerialization +import class Network.NWConnection +import class Network.NWProtocolFramer +import enum Network.NWError + +/** + * Represents an HTTP response sent by the server. + * + * `ServerResponse` is not usually created by user code, it is provided to + * the HTTPServer handler closure when a request is initiated. + * + * Example: + * + * let server = HTTPServer { request, response in + * try response.send("Hello!\n") + * } + * server.run() + * + * The `send` functions are convenience which setup things like Content-Length + * and Content-Type headers. + * + * When using the lower level `write` function, make sure to properly `end` + * the request! + * + * Example: + * + * let server = HTTPServer { request, response in + * response.write("Hello" .data(using: .utf8)!) + * response.write(" World".data(using: .utf8)!) + * response.keepAlive = false // because we set no content-length + * response.end() // important! + * } + * server.run() + * + */ + +@available(iOS 13.0, *) +open class ServerResponse { + // Most is marked `open` in case the consumer wants to patch stuff in a + // subclass. + + open var connection : NWConnection? + open var keepAlive : Bool + open var status : HTTPStatus + open var headers : [ String : String ] + + open var didWriteHead = false + open var content : Data? + open var writableEnded = false + + open var _endHandlers = [ ( ServerResponse ) -> Void ]() + + /** + * Setup a new `ServerResponse`. + * + * `ServerResponse` is not usually created by user code, it is provided to + * the HTTPServer handler closure when a request is initiated. + */ + public init(status : HTTPStatus = 200, + headers : [ String : String ] = [:], + keepAlive : Bool = false) + { + self.status = status + self.headers = headers + self.keepAlive = keepAlive + if !keepAlive && self.headers["Connection"] == nil { + self.headers["Connection"] = "close" + } + } + + + // MARK: - Event Handlers + + // Note: We could add onError, but we can't really do much in this case? + // It currently just cancels the connection which should teardown + // everything. + + /** + * Register closures to be executed when the response has been fully written + * to the client. I.e. if `end` has been called and successfully executed. + */ + open func onEnd(execute: @escaping ( ServerResponse ) -> Void) { + guard !writableEnded else { + return execute(self) // TBD: async this? + } + _endHandlers.append(execute) + } + + + // MARK: - Writing Response Data + + /** + * Write the HTTP response head to the connection. + * + * Status and headers can be provided as arguments, and are combined w/ + * the values set in the response object. + * + * Consider using the higher level `send` functions instead. + */ + open func writeHead(status : HTTPStatus? = nil, + headers : [ String : String ] = [:]) + { + assert(!didWriteHead) + assert(!writableEnded) + + guard let connection = connection else { + if let status = status { self.status = status } + for ( name, value ) in headers { + self.headers[name] = value + } + return + } + + didWriteHead = true + + var headerArray = HTTPProtocol.Headers() + headerArray.reserveCapacity(headers.count + self.headers.count) + for ( name, value ) in self.headers { + guard headers[name] == nil else { continue } + headerArray.append( ( name: name, value: value ) ) + } + for ( name, value ) in headers { + headerArray.append( ( name: name, value: value ) ) + } + + let message = NWProtocolFramer.Message(status : status?.rawValue + ?? self.status.rawValue, + headers : headerArray) + let context = NWConnection + .ContentContext(identifier: "HTTPResponseHead", metadata: [ message ]) + + connection.send(content: nil, + contentContext: context, isComplete: true, + completion: .contentProcessed({ error in + if let error = error { self.handleWriteError(error) } + })) + + flushIfPossible() + } + + /** + * Write HTTP body data to the connection. + * + * If the response head has not been written yet, it will be now. + * + * Important: Once finished writing to the response, call `end` to mark + * the response as complete. + * + * Consider using the higher level `send` functions instead. + */ + @discardableResult + open func write(_ data: Data) -> Bool { + assert(!writableEnded) + guard let connection = connection else { + if content?.append(data) == nil { content = data } + let hwm = 1000 + return (content?.count ?? 0) < hwm + } + + if !didWriteHead { writeHead() } + flushIfPossible() + guard !data.isEmpty else { return true } + + let message = NWProtocolFramer.Message.httpMessage + let context = NWConnection + .ContentContext(identifier: "HTTPResponseBody", metadata: [ message ]) + + connection.send(content: data, + contentContext: context, isComplete: true, + completion: .contentProcessed({ error in + if let error = error { self.handleWriteError(error) } + })) + return true + } + + /** + * Finish the response. + * + * If the response head has not been written yet, it will be now, the same + * for any potentially buffered data. + * + * Consider using the higher level `send` functions instead. + */ + open func end() { + assert(!writableEnded) + + writableEnded = true + let callbacks = _endHandlers; _endHandlers = [] + + guard let connection = connection else { + return callbacks.forEach { $0(self) } + } + + if !didWriteHead { writeHead() } + flushIfPossible() + + let message = NWProtocolFramer.Message.httpMessage + message.isEndOfMessage = true + let context = NWConnection + .ContentContext(identifier: "HTTPResponseEnd", metadata: [ message ]) + + let keepAlive = self.keepAlive + connection.send(content: nil, contentContext: context, isComplete: true, + completion: .contentProcessed({ error in + callbacks.forEach { $0(self) } + if !keepAlive { + connection.cancel() + } + // TODO: still need to teardown the state! + self.handleWriteResult(error) + self.connection = nil + })) + } + + /** + * If body content has been buffered due to a connection not being available, + * this will try to flush the buffer. + */ + public func flushIfPossible() { + /* Flush buffered content */ + guard let connection = connection else { return } + defer { self.content = nil } + guard let content = content, !content.isEmpty else { return } + connection.send(content: content, + completion: .contentProcessed(handleWriteResult(_:))) + } + + + // MARK: - Error Handling + + private func handleWriteResult(_ error: NWError?) { + if let error = error { + handleWriteError(error) + } + } + + /** + * Invoked by writing functions if a write failed. + * + * This will cancel the connection and call all `onEnd` callbacks. + */ + open func handleWriteError(_ error: NWError) { + keepAlive = false + if !writableEnded { // like `end`, but no flush, EOM etc + writableEnded = true + let callbacks = _endHandlers; _endHandlers = [] + callbacks.forEach { $0(self) } + } + connection?.cancel() + connection = nil + } + + + // MARK: - Header Access + // This is crazy wrong and expects consistent casing :-) + + /** + * Extract the `Content-Type` header, if available. + */ + var contentType: String? { + set { + if let v = newValue { headers["Content-Type"] = v } + else { headers.removeValue(forKey: "Content-Type") } + } + get { return headers["Content-Type"] } + } + + + // MARK: - Write Convenience + + /** + * Send the `Data` object to the client. Can only be called once. + * + * This sets the `Content-Length` header to the length of the Data. + * If no `Content-Type` header is set, it will add that as + * "application/octet-stream". + * + * Example: + * + * response.send("hello".data(using: .utf8)!) + * + */ + open func send(_ data: Data) { + if !didWriteHead { + if headers["Content-Length"] == nil { + headers["Content-Length"] = "\(data.count)" + } + if contentType == nil { + contentType = "application/octet-stream" + } + } + write(data) + end() + } + + /** + * Send the `String` object as UTF-8 to the client. Can only be called once. + * + * This sets the `Content-Length` header to the length of the UTF-8 + * representation of the String. + * If no `Content-Type` header is set, it will add that as + * "text/plain; charset=UTF-8". + * + * If the UTF-8 conversion fails, a `StringEncodingError` is thrown. + * + * Example: + * + * try response.send("hello") + * + */ + open func send(_ string: String) throws { + if contentType == nil { contentType = "text/plain; charset=UTF-8" } + + if string.isEmpty { return send(Data()) } + + // TODO: scan headers for requested charset + guard let data = string.data(using: .utf8) else { + throw StringEncodingError(encoding: .utf8) + } + + send(data) + } + + /** + * Send the `String` object as UTF-8 to the client. Can only be called once. + * + * This sets the `Content-Length` header to the length of the UTF-8 + * representation of the String. + * If no `Content-Type` header is set, it will add that as + * "text/plain; charset=UTF-8". + * + * If the UTF-8 conversion fails, a `StringEncodingError` is thrown. + * + * Example: + * + * try response.send("hello") + * + */ + open func send(_ string: S) throws { + try send(String(string)) + } + + /** + * Send the Encodable object as JSON to the client. Can only be called once. + * + * This sets the `Content-Length` header to the length of the encoded data. + * If no `Content-Type` header is set, it will add that as + * "application/json; charset=UTF-8". + * + * Example: + * + * struct Entry: Codable { + * let date : Date + * let title : String + * let body : String + * } + * try response.send(Entry(date: Date(), title: "Hello", body: "World") + * + */ + open func sendJSON(_ value: T) throws { + if contentType == nil { contentType = "application/json; charset=UTF-8" } + let data = try JSONEncoder().encode(value) + send(data) + } + + /** + * Send the property list object as JSON to the client. Can only be called + * once. + * + * This sets the `Content-Length` header to the length of the encoded data. + * If no `Content-Type` header is set, it will add that as + * "application/json; charset=UTF-8". + * + * Example: + * + * try response.send([ "title": "Hello", "body": "World" ]) + * + */ + open func sendJSON(_ value : Any, + options : JSONSerialization.WritingOptions = []) throws + { + if contentType == nil { contentType = "application/json; charset=UTF-8" } + let data = + try JSONSerialization.data(withJSONObject: value, options: options) + send(data) + } +} diff --git a/submodules/TelegramCallsUI/Sources/LocalServer/NWHTTPServer/StringEncodingError.swift b/submodules/TelegramCallsUI/Sources/LocalServer/NWHTTPServer/StringEncodingError.swift new file mode 100644 index 0000000000..4265b29484 --- /dev/null +++ b/submodules/TelegramCallsUI/Sources/LocalServer/NWHTTPServer/StringEncodingError.swift @@ -0,0 +1,12 @@ +// +// StringEncodingError.swift +// NWHTTPServer +// +// Copyright © 2020 ZeeZide GmbH. All rights reserved. +// + +import Foundation + +public struct StringEncodingError : Swift.Error { + public let encoding : String.Encoding +} diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index 284f939f91..811d2553ef 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -276,6 +276,7 @@ private extension PresentationGroupCallState { private enum CurrentImpl { case call(OngoingGroupCallContext) case mediaStream(WrappedMediaStreamingContext) + case externalMediaStream(ExternalMediaStreamingContext) } private extension CurrentImpl { @@ -283,7 +284,7 @@ private extension CurrentImpl { switch self { case let .call(callContext): return callContext.joinPayload - case .mediaStream: + case .mediaStream, .externalMediaStream: let ssrcId = UInt32.random(in: 0 ..< UInt32(Int32.max - 1)) let dict: [String: Any] = [ "fingerprints": [] as [Any], @@ -303,7 +304,7 @@ private extension CurrentImpl { switch self { case let .call(callContext): return callContext.networkState - case .mediaStream: + case .mediaStream, .externalMediaStream: return .single(OngoingGroupCallContext.NetworkState(isConnected: true, isTransitioningFromBroadcastToRtc: false)) } } @@ -312,7 +313,7 @@ private extension CurrentImpl { switch self { case let .call(callContext): return callContext.audioLevels - case .mediaStream: + case .mediaStream, .externalMediaStream: return .single([]) } } @@ -321,7 +322,7 @@ private extension CurrentImpl { switch self { case let .call(callContext): return callContext.isMuted - case .mediaStream: + case .mediaStream, .externalMediaStream: return .single(true) } } @@ -330,7 +331,7 @@ private extension CurrentImpl { switch self { case let .call(callContext): return callContext.isNoiseSuppressionEnabled - case .mediaStream: + case .mediaStream, .externalMediaStream: return .single(false) } } @@ -339,7 +340,7 @@ private extension CurrentImpl { switch self { case let .call(callContext): callContext.stop() - case .mediaStream: + case .mediaStream, .externalMediaStream: break } } @@ -348,7 +349,7 @@ private extension CurrentImpl { switch self { case let .call(callContext): callContext.setIsMuted(isMuted) - case .mediaStream: + case .mediaStream, .externalMediaStream: break } } @@ -357,7 +358,7 @@ private extension CurrentImpl { switch self { case let .call(callContext): callContext.setIsNoiseSuppressionEnabled(isNoiseSuppressionEnabled) - case .mediaStream: + case .mediaStream, .externalMediaStream: break } } @@ -366,7 +367,7 @@ private extension CurrentImpl { switch self { case let .call(callContext): callContext.requestVideo(capturer) - case .mediaStream: + case .mediaStream, .externalMediaStream: break } } @@ -375,7 +376,7 @@ private extension CurrentImpl { switch self { case let .call(callContext): callContext.disableVideo() - case .mediaStream: + case .mediaStream, .externalMediaStream: break } } @@ -384,7 +385,7 @@ private extension CurrentImpl { switch self { case let .call(callContext): callContext.setVolume(ssrc: ssrc, volume: volume) - case .mediaStream: + case .mediaStream, .externalMediaStream: break } } @@ -393,7 +394,7 @@ private extension CurrentImpl { switch self { case let .call(callContext): callContext.setRequestedVideoChannels(channels) - case .mediaStream: + case .mediaStream, .externalMediaStream: break } } @@ -402,17 +403,19 @@ private extension CurrentImpl { switch self { case let .call(callContext): callContext.makeIncomingVideoView(endpointId: endpointId, requestClone: requestClone, completion: completion) - case .mediaStream: + case .mediaStream, .externalMediaStream: break } } - func video(endpointId: String) -> Signal { + func video(endpointId: String) -> Signal? { switch self { case let .call(callContext): return callContext.video(endpointId: endpointId) case let .mediaStream(mediaStreamContext): return mediaStreamContext.video() + case .externalMediaStream: + return .never() } } @@ -420,7 +423,7 @@ private extension CurrentImpl { switch self { case let .call(callContext): callContext.addExternalAudioData(data: data) - case .mediaStream: + case .mediaStream, .externalMediaStream: break } } @@ -429,7 +432,7 @@ private extension CurrentImpl { switch self { case let .call(callContext): callContext.getStats(completion: completion) - case .mediaStream: + case .mediaStream, .externalMediaStream: break } } @@ -438,7 +441,7 @@ private extension CurrentImpl { switch self { case let .call(callContext): callContext.setTone(tone: tone) - case .mediaStream: + case .mediaStream, .externalMediaStream: break } } @@ -647,6 +650,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { private var genericCallContext: CurrentImpl? private var currentConnectionMode: OngoingGroupCallContext.ConnectionMode = .none private var didInitializeConnectionMode: Bool = false + + let externalMediaStream = Promise() private var screencastCallContext: OngoingGroupCallContext? private var screencastBufferServerContext: IpcGroupCallBufferAppContext? @@ -1638,7 +1643,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { genericCallContext = current } else { if self.isStream, self.accountContext.sharedContext.immediateExperimentalUISettings.liveStreamV2 { - genericCallContext = .mediaStream(WrappedMediaStreamingContext(rejoinNeeded: { [weak self] in + let externalMediaStream = ExternalMediaStreamingContext(id: self.internalId, rejoinNeeded: { [weak self] in Queue.mainQueue().async { guard let strongSelf = self else { return @@ -1650,7 +1655,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { strongSelf.requestCall(movingFromBroadcastToRtc: false) } } - })) + }) + genericCallContext = .externalMediaStream(externalMediaStream) + self.externalMediaStream.set(.single(externalMediaStream)) } else { var outgoingAudioBitrateKbit: Int32? let appConfiguration = self.accountContext.currentAppConfiguration.with({ $0 }) @@ -1797,6 +1804,14 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { strongSelf.currentConnectionMode = .broadcast mediaStreamContext.setAudioStreamData(audioStreamData: OngoingGroupCallContext.AudioStreamData(engine: strongSelf.accountContext.engine, callId: callInfo.id, accessHash: callInfo.accessHash, isExternalStream: callInfo.isStream)) } + case let .externalMediaStream(externalMediaStream): + switch joinCallResult.connectionMode { + case .rtc: + strongSelf.currentConnectionMode = .rtc + case .broadcast: + strongSelf.currentConnectionMode = .broadcast + externalMediaStream.setAudioStreamData(audioStreamData: OngoingGroupCallContext.AudioStreamData(engine: strongSelf.accountContext.engine, callId: callInfo.id, accessHash: callInfo.accessHash, isExternalStream: callInfo.isStream)) + } } } @@ -3199,7 +3214,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { switch genericCallContext { case let .call(callContext): callContext.setConnectionMode(.none, keepBroadcastConnectedIfWasEnabled: movingFromBroadcastToRtc, isUnifiedBroadcast: false) - case .mediaStream: + case .mediaStream, .externalMediaStream: assertionFailure() break } diff --git a/submodules/TelegramCore/Sources/State/AccountViewTracker.swift b/submodules/TelegramCore/Sources/State/AccountViewTracker.swift index 432ed9bf48..2c21f90075 100644 --- a/submodules/TelegramCore/Sources/State/AccountViewTracker.swift +++ b/submodules/TelegramCore/Sources/State/AccountViewTracker.swift @@ -1896,6 +1896,7 @@ public final class AccountViewTracker { addHole = true } pollingCompleted = context.isUpdated.get() + //pollingCompleted = .single(true) } else { addHole = true pollingCompleted = .single(true) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift index 84aedac1f2..baac6eb27b 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift @@ -236,6 +236,59 @@ private func mergedResult(_ state: SearchMessagesState) -> SearchMessagesResult return SearchMessagesResult(messages: messages, readStates: readStates, threadInfo: threadInfo, totalCount: state.main.totalCount + (state.additional?.totalCount ?? 0), completed: state.main.completed && (state.additional?.completed ?? true)) } +func _internal_getSearchMessageCount(account: Account, location: SearchMessagesLocation, query: String) -> Signal { + guard case let .peer(peerId, fromId, _, _, threadId, _, _) = location else { + return .single(nil) + } + return account.postbox.transaction { transaction -> (Api.InputPeer?, Api.InputPeer?) in + var chatPeer = transaction.getPeer(peerId).flatMap(apiInputPeer) + var fromPeer: Api.InputPeer? + if let fromId { + if let value = transaction.getPeer(fromId).flatMap(apiInputPeer) { + fromPeer = value + } else { + chatPeer = nil + } + } + + return (chatPeer, fromPeer) + } + |> mapToSignal { inputPeer, fromPeer -> Signal in + guard let inputPeer else { + return .single(nil) + } + + var flags: Int32 = 0 + + if let _ = fromPeer { + flags |= (1 << 0) + } + + var topMsgId: Int32? + if let threadId = threadId { + flags |= (1 << 1) + topMsgId = Int32(clamping: threadId) + } + + return account.network.request(Api.functions.messages.search(flags: flags, peer: inputPeer, q: query, fromId: fromPeer, savedPeerId: nil, savedReaction: nil, topMsgId: topMsgId, filter: .inputMessagesFilterEmpty, minDate: 0, maxDate: 0, offsetId: 0, addOffset: 0, limit: 1, maxId: 0, minId: 0, hash: 0)) + |> map { result -> Int? in + switch result { + case let .channelMessages(_, _, count, _, _, _, _, _): + return Int(count) + case let .messages(messages, _, _): + return messages.count + case let .messagesNotModified(count): + return Int(count) + case let .messagesSlice(_, count, _, _, _, _, _): + return Int(count) + } + } + |> `catch` { _ -> Signal in + return .single(nil) + } + } +} + func _internal_searchMessages(account: Account, location: SearchMessagesLocation, query: String, state: SearchMessagesState?, centerId: MessageId?, limit: Int32 = 100) -> Signal<(SearchMessagesResult, SearchMessagesState), NoError> { if case let .peer(peerId, fromId, tags, reactions, threadId, minDate, maxDate) = location, fromId == nil, tags == nil, peerId == account.peerId, let reactions, let reaction = reactions.first, (minDate == nil || minDate == 0), (maxDate == nil || maxDate == 0) { return account.postbox.transaction { transaction -> (SearchMessagesResult, SearchMessagesState) in diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index b8ddc75bde..187ddcc9e7 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -76,6 +76,10 @@ public extension TelegramEngine { return _internal_searchMessages(account: self.account, location: location, query: query, state: state, centerId: centerId, limit: limit) } + public func getSearchMessageCount(location: SearchMessagesLocation, query: String) -> Signal { + return _internal_getSearchMessageCount(account: self.account, location: location, query: query) + } + public func searchHashtagPosts(hashtag: String, state: SearchMessagesState?, limit: Int32 = 100) -> Signal<(SearchMessagesResult, SearchMessagesState), NoError> { return _internal_searchHashtagPosts(account: self.account, hashtag: hashtag, state: state, limit: limit) } diff --git a/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift b/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift index b85e764df7..439be5c6bf 100644 --- a/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift +++ b/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift @@ -194,6 +194,7 @@ private final class AdminUserActionsSheetComponent: Component { let chatPeer: EnginePeer let peers: [RenderedChannelParticipant] let messageCount: Int + let deleteAllMessageCount: Int? let completion: (AdminUserActionsSheet.Result) -> Void init( @@ -201,12 +202,14 @@ private final class AdminUserActionsSheetComponent: Component { chatPeer: EnginePeer, peers: [RenderedChannelParticipant], messageCount: Int, + deleteAllMessageCount: Int?, completion: @escaping (AdminUserActionsSheet.Result) -> Void ) { self.context = context self.chatPeer = chatPeer self.peers = peers self.messageCount = messageCount + self.deleteAllMessageCount = deleteAllMessageCount self.completion = completion } @@ -223,6 +226,9 @@ private final class AdminUserActionsSheetComponent: Component { if lhs.messageCount != rhs.messageCount { return false } + if lhs.deleteAllMessageCount != rhs.deleteAllMessageCount { + return false + } return true } @@ -642,7 +648,7 @@ private final class AdminUserActionsSheetComponent: Component { let sectionId: AnyHashable let selectedPeers: Set let isExpanded: Bool - let title: String + var title: String switch section { case .report: @@ -870,7 +876,14 @@ private final class AdminUserActionsSheetComponent: Component { ))) } - let titleString: String = environment.strings.Chat_AdminActionSheet_DeleteTitle(Int32(component.messageCount)) + var titleString: String = environment.strings.Chat_AdminActionSheet_DeleteTitle(Int32(component.messageCount)) + + if let deleteAllMessageCount = component.deleteAllMessageCount { + if self.optionDeleteAllSelectedPeers == Set(component.peers.map(\.peer.id)) { + titleString = environment.strings.Chat_AdminActionSheet_DeleteTitle(Int32(deleteAllMessageCount)) + } + } + let titleSize = self.title.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( @@ -884,7 +897,9 @@ private final class AdminUserActionsSheetComponent: Component { if titleView.superview == nil { self.navigationBarContainer.addSubview(titleView) } - transition.setFrame(view: titleView, frame: titleFrame) + //transition.setPosition(view: titleView, position: titleFrame.center) + titleView.center = titleFrame.center + titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) } let navigationBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: 54.0)) @@ -1424,10 +1439,10 @@ public class AdminUserActionsSheet: ViewControllerComponentContainer { private var isDismissed: Bool = false - public init(context: AccountContext, chatPeer: EnginePeer, peers: [RenderedChannelParticipant], messageCount: Int, completion: @escaping (Result) -> Void) { + public init(context: AccountContext, chatPeer: EnginePeer, peers: [RenderedChannelParticipant], messageCount: Int, deleteAllMessageCount: Int?, completion: @escaping (Result) -> Void) { self.context = context - super.init(context: context, component: AdminUserActionsSheetComponent(context: context, chatPeer: chatPeer, peers: peers, messageCount: messageCount, completion: completion), navigationBarAppearance: .none) + super.init(context: context, component: AdminUserActionsSheetComponent(context: context, chatPeer: chatPeer, peers: peers, messageCount: messageCount, deleteAllMessageCount: deleteAllMessageCount, completion: completion), navigationBarAppearance: .none) self.statusBar.statusBarStyle = .Ignore self.navigationPresentation = .flatModal diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index a261f2b647..ae17904f04 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -4747,25 +4747,31 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } if let forwardInfoNode = self.forwardInfoNode, forwardInfoNode.frame.contains(location) { if let item = self.item, let forwardInfo = item.message.forwardInfo { - let performAction: () -> Void = { + let performAction: () -> Void = { [weak forwardInfoNode] in if let sourceMessageId = forwardInfo.sourceMessageId { if let channel = forwardInfo.author as? TelegramChannel, channel.addressName == nil { if case let .broadcast(info) = channel.info, info.flags.contains(.hasDiscussionGroup) { } else if case .member = channel.participationStatus { } else if !item.message.id.peerId.isReplies { - item.controllerInteraction.displayMessageTooltip(item.message.id, item.presentationData.strings.Conversation_PrivateChannelTooltip, false, forwardInfoNode, nil) + if let forwardInfoNode { + item.controllerInteraction.displayMessageTooltip(item.message.id, item.presentationData.strings.Conversation_PrivateChannelTooltip, false, forwardInfoNode, nil) + } return } } - item.controllerInteraction.navigateToMessage(item.message.id, sourceMessageId, NavigateToMessageParams(timestamp: nil, quote: nil)) + if let forwardInfoNode { + item.controllerInteraction.navigateToMessage(item.message.id, sourceMessageId, NavigateToMessageParams(timestamp: nil, quote: nil, progress: forwardInfoNode.makeActivate()?())) + } } else if let peer = forwardInfo.source ?? forwardInfo.author { item.controllerInteraction.openPeer(EnginePeer(peer), peer is TelegramUser ? .info(nil) : .chat(textInputState: nil, subject: nil, peekData: nil), nil, .default) } else if let _ = forwardInfo.authorSignature { - var subRect: CGRect? - if let textNode = forwardInfoNode.nameNode { - subRect = textNode.frame + if let forwardInfoNode { + var subRect: CGRect? + if let textNode = forwardInfoNode.nameNode { + subRect = textNode.frame + } + item.controllerInteraction.displayMessageTooltip(item.message.id, item.presentationData.strings.Conversation_ForwardAuthorHiddenTooltip, false, forwardInfoNode, subRect) } - item.controllerInteraction.displayMessageTooltip(item.message.id, item.presentationData.strings.Conversation_ForwardAuthorHiddenTooltip, false, forwardInfoNode, subRect) } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode/BUILD index dd8301c5f5..9eb97d563e 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode/BUILD @@ -25,6 +25,7 @@ swift_library( "//submodules/TelegramUI/Components/TextNodeWithEntities", "//submodules/TelegramUI/Components/AnimationCache", "//submodules/TelegramUI/Components/MultiAnimationRenderer", + "//submodules/TelegramUI/Components/TextLoadingEffect", "//submodules/AvatarNode", ], visibility = [ diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode/Sources/ChatMessageForwardInfoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode/Sources/ChatMessageForwardInfoNode.swift index 8d79dea5e7..33121fb337 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode/Sources/ChatMessageForwardInfoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode/Sources/ChatMessageForwardInfoNode.swift @@ -8,6 +8,8 @@ import TelegramPresentationData import LocalizedPeerData import AccountContext import AvatarNode +import TextLoadingEffect +import SwiftSignalKit public enum ChatMessageForwardInfoType: Equatable { case bubble(incoming: Bool) @@ -85,6 +87,10 @@ public class ChatMessageForwardInfoNode: ASDisplayNode { private var highlightColor: UIColor? private var linkHighlightingNode: LinkHighlightingNode? + private var hasLinkProgress: Bool = false + private var linkProgressView: TextLoadingEffectView? + private var linkProgressDisposable: Disposable? + private var previousPeer: Peer? public var openPsa: ((String, ASDisplayNode) -> Void)? @@ -93,6 +99,10 @@ public class ChatMessageForwardInfoNode: ASDisplayNode { super.init() } + deinit { + self.linkProgressDisposable?.dispose() + } + public func hasAction(at point: CGPoint) -> Bool { if let infoNode = self.infoNode, infoNode.frame.contains(point) { return true @@ -172,7 +182,6 @@ public class ChatMessageForwardInfoNode: ASDisplayNode { if isHighlighted, !initialRects.isEmpty, let highlightColor = self.highlightColor { let rects = initialRects - let linkHighlightingNode: LinkHighlightingNode if let current = self.linkHighlightingNode { linkHighlightingNode = current @@ -191,6 +200,85 @@ public class ChatMessageForwardInfoNode: ASDisplayNode { } } + public func makeActivate() -> (() -> Promise?)? { + return { [weak self] in + guard let self else { + return nil + } + + let promise = Promise() + self.linkProgressDisposable?.dispose() + + if self.hasLinkProgress { + self.hasLinkProgress = false + self.updateLinkProgressState() + } + + self.linkProgressDisposable = (promise.get() |> deliverOnMainQueue).startStrict(next: { [weak self] value in + guard let self else { + return + } + if self.hasLinkProgress != value { + self.hasLinkProgress = value + self.updateLinkProgressState() + } + }) + + return promise + } + } + + private func updateLinkProgressState() { + guard let highlightColor = self.highlightColor else { + return + } + + if self.hasLinkProgress, let titleNode = self.titleNode, let nameNode = self.nameNode { + var initialRects: [CGRect] = [] + let addRects: (TextNode, CGPoint, CGFloat) -> Void = { textNode, offset, additionalWidth in + guard let cachedLayout = textNode.cachedLayout else { + return + } + for rect in cachedLayout.linesRects() { + var rect = rect + rect.size.width += rect.origin.x + additionalWidth + rect.origin.x = 0.0 + initialRects.append(rect.offsetBy(dx: offset.x, dy: offset.y)) + } + } + + let offsetY: CGFloat = -12.0 + if let titleNode = self.titleNode { + addRects(titleNode, CGPoint(x: titleNode.frame.minX, y: offsetY + titleNode.frame.minY), 0.0) + + if let nameNode = self.nameNode { + addRects(nameNode, CGPoint(x: titleNode.frame.minX, y: offsetY + nameNode.frame.minY), nameNode.frame.minX - titleNode.frame.minX) + } + } + + let linkProgressView: TextLoadingEffectView + if let current = self.linkProgressView { + linkProgressView = current + } else { + linkProgressView = TextLoadingEffectView(frame: CGRect()) + self.linkProgressView = linkProgressView + self.view.addSubview(linkProgressView) + } + linkProgressView.frame = titleNode.frame + + let progressColor: UIColor = highlightColor + + linkProgressView.update(color: progressColor, size: CGRectUnion(titleNode.frame, nameNode.frame).size, rects: initialRects) + } else { + if let linkProgressView = self.linkProgressView { + self.linkProgressView = nil + linkProgressView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak linkProgressView] _ in + linkProgressView?.removeFromSuperview() + }) + } + } + } + public static func asyncLayout(_ maybeNode: ChatMessageForwardInfoNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ strings: PresentationStrings, _ type: ChatMessageForwardInfoType, _ peer: Peer?, _ authorName: String?, _ psaType: String?, _ storyData: StoryData?, _ constrainedSize: CGSize) -> (CGSize, (CGFloat) -> ChatMessageForwardInfoNode) { let titleNodeLayout = TextNode.asyncLayout(maybeNode?.titleNode) let nameNodeLayout = TextNode.asyncLayout(maybeNode?.nameNode) diff --git a/submodules/TelegramUI/Components/Chat/ReplyAccessoryPanelNode/Sources/ReplyAccessoryPanelNode.swift b/submodules/TelegramUI/Components/Chat/ReplyAccessoryPanelNode/Sources/ReplyAccessoryPanelNode.swift index 5e54ec3215..d1210c4e14 100644 --- a/submodules/TelegramUI/Components/Chat/ReplyAccessoryPanelNode/Sources/ReplyAccessoryPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ReplyAccessoryPanelNode/Sources/ReplyAccessoryPanelNode.swift @@ -370,6 +370,7 @@ public final class ReplyAccessoryPanelNode: AccessoryPanelNode { super.didLoad() self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + self.view.addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: #selector(self.longPressGesture(_:)))) } override public func animateIn() { @@ -491,9 +492,9 @@ public final class ReplyAccessoryPanelNode: AccessoryPanelNode { } } - /*@objc func tapGesture(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state { - self.interfaceInteraction?.navigateToMessage(self.messageId, false, true, .generic) + @objc func longPressGesture(_ recognizer: UILongPressGestureRecognizer) { + if case .began = recognizer.state { + self.interfaceInteraction?.navigateToMessage(self.messageId, false, true, ChatLoadingMessageSubject.generic) } - }*/ + } } diff --git a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift index ba062d1d2a..28d1151a16 100644 --- a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift +++ b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift @@ -868,7 +868,7 @@ final class PeerAllowedReactionsScreenComponent: Component { } contentHeight += reactionCountSectionSize.height - if "".isEmpty { + if !"".isEmpty { contentHeight += 32.0 let paidReactionsSection: ComponentView diff --git a/submodules/TelegramUI/Components/TextLoadingEffect/Sources/TextLoadingEffect.swift b/submodules/TelegramUI/Components/TextLoadingEffect/Sources/TextLoadingEffect.swift index 953953dafa..9b5bacdb47 100644 --- a/submodules/TelegramUI/Components/TextLoadingEffect/Sources/TextLoadingEffect.swift +++ b/submodules/TelegramUI/Components/TextLoadingEffect/Sources/TextLoadingEffect.swift @@ -171,4 +171,34 @@ public final class TextLoadingEffectView: UIView { self.updateAnimations(size: maskFrame.size) } } + + public func update(color: UIColor, size: CGSize, rects: [CGRect]) { + let rectsSet: [CGRect] = rects + + let maskFrame = CGRect(origin: CGPoint(), size: size).insetBy(dx: -4.0, dy: -4.0) + + self.maskContentsView.backgroundColor = color.withAlphaComponent(0.1) + self.maskBorderContentsView.backgroundColor = color.withAlphaComponent(0.12) + + self.backgroundView.tintColor = color + self.borderBackgroundView.tintColor = color + + self.maskContentsView.frame = maskFrame + self.maskBorderContentsView.frame = maskFrame + + self.maskHighlightNode.updateRects(rectsSet) + self.maskHighlightNode.frame = CGRect(origin: CGPoint(x: -maskFrame.minX, y: -maskFrame.minY), size: CGSize()) + + self.maskBorderHighlightNode.updateRects(rectsSet) + self.maskBorderHighlightNode.frame = CGRect(origin: CGPoint(x: -maskFrame.minX, y: -maskFrame.minY), size: CGSize()) + + if self.size != maskFrame.size { + self.size = maskFrame.size + + self.backgroundView.frame = CGRect(origin: CGPoint(x: -self.gradientWidth, y: 0.0), size: CGSize(width: self.gradientWidth, height: maskFrame.height)) + self.borderBackgroundView.frame = CGRect(origin: CGPoint(x: -self.gradientWidth, y: 0.0), size: CGSize(width: self.gradientWidth, height: maskFrame.height)) + + self.updateAnimations(size: maskFrame.size) + } + } } diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerNavigateToMessage.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerNavigateToMessage.swift index 41fe42586f..f2095cd1ce 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerNavigateToMessage.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerNavigateToMessage.swift @@ -29,7 +29,7 @@ extension ChatControllerImpl { guard let self else { return } - self.navigateToMessage(from: fromId, to: .id(id, params), forceInCurrentChat: fromId.peerId == id.peerId && !params.forceNew, forceNew: params.forceNew) + self.navigateToMessage(from: fromId, to: .id(id, params), forceInCurrentChat: fromId.peerId == id.peerId && !params.forceNew, forceNew: params.forceNew, progress: params.progress) } let _ = (self.context.engine.data.get( @@ -77,6 +77,7 @@ extension ChatControllerImpl { animated: Bool = true, completion: (() -> Void)? = nil, customPresentProgress: ((ViewController, Any?) -> Void)? = nil, + progress: Promise? = nil, statusSubject: ChatLoadingMessageSubject = .generic ) { if !self.isNodeLoaded { @@ -160,31 +161,156 @@ extension ChatControllerImpl { guard let self, let peer = peer else { return } - if let navigationController = self.effectiveNavigationController { - var chatLocation: NavigateToChatControllerParams.Location = .peer(peer) - var displayMessageNotFoundToast = false - if case let .channel(channel) = peer, channel.flags.contains(.isForum) { - if let message = message, let threadId = message.threadId { - chatLocation = .replyThread(ChatReplyThreadMessage(peerId: peer.id, threadId: threadId, channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false)) + + var quote: ChatControllerSubject.MessageHighlight.Quote? + if case let .id(_, params) = messageLocation { + quote = params.quote.flatMap { quote in ChatControllerSubject.MessageHighlight.Quote(string: quote.string, offset: quote.offset) } + } + var progressValue: Promise? + if let value = progress { + progressValue = value + } else if case let .id(_, params) = messageLocation { + progressValue = params.progress + } + self.loadingMessage.set(.single(statusSubject) |> delay(0.1, queue: .mainQueue())) + + var chatLocation: NavigateToChatControllerParams.Location = .peer(peer) + var preloadChatLocation: ChatLocation = .peer(id: peer.id) + var displayMessageNotFoundToast = false + if case let .channel(channel) = peer, channel.flags.contains(.isForum) { + if let message = message, let threadId = message.threadId { + let replyThreadMessage = ChatReplyThreadMessage(peerId: peer.id, threadId: threadId, channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false) + chatLocation = .replyThread(replyThreadMessage) + preloadChatLocation = .replyThread(message: replyThreadMessage) + } else { + displayMessageNotFoundToast = true + } + } + + let searchLocation: ChatHistoryInitialSearchLocation + switch messageLocation { + case let .id(id, _): + if case let .replyThread(message) = chatLocation, id == message.effectiveMessageId { + searchLocation = .index(.absoluteLowerBound()) + } else { + searchLocation = .id(id) + } + case let .index(index): + searchLocation = .index(index) + case .upperBound: + searchLocation = .index(MessageIndex.upperBound(peerId: chatLocation.peerId)) + } + var historyView: Signal + + let subject: ChatControllerSubject = .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: quote), timecode: nil) + + historyView = preloadedChatHistoryViewForLocation(ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: searchLocation, quote: nil), count: 50, highlight: true), id: 0), context: self.context, chatLocation: preloadChatLocation, subject: subject, chatLocationContextHolder: Atomic(value: nil), fixedCombinedReadStates: nil, tag: nil, additionalData: []) + + var signal: Signal<(MessageIndex?, Bool), NoError> + signal = historyView + |> mapToSignal { historyView -> Signal<(MessageIndex?, Bool), NoError> in + switch historyView { + case .Loading: + return .single((nil, true)) + case let .HistoryView(view, _, _, _, _, _, _): + for entry in view.entries { + if entry.message.id == messageLocation.messageId { + return .single((entry.message.index, false)) + } + } + if case let .index(index) = searchLocation { + return .single((index, false)) + } + return .single((nil, false)) + } + } + |> take(until: { index in + return SignalTakeAction(passthrough: true, complete: !index.1) + }) + + /*#if DEBUG + signal = .single((nil, true)) |> then(signal |> delay(2.0, queue: .mainQueue())) + #endif*/ + + var cancelImpl: (() -> Void)? + let presentationData = self.presentationData + let displayTime = CACurrentMediaTime() + let progressSignal = Signal { [weak self] subscriber in + if let progressValue { + progressValue.set(.single(true)) + return ActionDisposable { + Queue.mainQueue().async() { + progressValue.set(.single(false)) + } + } + } else if case .generic = statusSubject { + let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { + if CACurrentMediaTime() - displayTime > 1.5 { + cancelImpl?() + } + })) + if let customPresentProgress = customPresentProgress { + customPresentProgress(controller, nil) } else { - displayMessageNotFoundToast = true + self?.present(controller, in: .window(.root)) } + return ActionDisposable { [weak controller] in + Queue.mainQueue().async() { + controller?.dismiss() + } + } + } else { + return EmptyDisposable + } + } + |> runOn(Queue.mainQueue()) + |> delay(progressValue == nil ? 0.05 : 0.0, queue: Queue.mainQueue()) + let progressDisposable = MetaDisposable() + var progressStarted = false + self.messageIndexDisposable.set((signal + |> afterDisposed { + Queue.mainQueue().async { + progressDisposable.dispose() + } + } + |> deliverOnMainQueue).startStrict(next: { [weak self] index in + guard let self else { + return } - var quote: ChatControllerSubject.MessageHighlight.Quote? - if case let .id(_, params) = messageLocation { - quote = params.quote.flatMap { quote in ChatControllerSubject.MessageHighlight.Quote(string: quote.string, offset: quote.offset) } + if let index = index.0 { + let _ = index + //strongSelf.chatDisplayNode.historyNode.scrollToMessage(from: scrollFromIndex, to: index, animated: animated, quote: quote, scrollPosition: scrollPosition) + } else if index.1 { + if !progressStarted { + progressStarted = true + progressDisposable.set(progressSignal.start()) + } + return } - let context = self.context - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: chatLocation, subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: quote), timecode: nil), keepStack: .always, chatListCompletion: { chatListController in - if displayMessageNotFoundToast { - let presentationData = context.sharedContext.currentPresentationData.with({ $0 }) - chatListController.present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.Conversation_MessageDoesntExist, timeout: nil, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in - return true - }), in: .current) - } - })) + if let navigationController = self.effectiveNavigationController { + let context = self.context + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: chatLocation, subject: subject, keepStack: .always, chatListCompletion: { chatListController in + if displayMessageNotFoundToast { + let presentationData = context.sharedContext.currentPresentationData.with({ $0 }) + chatListController.present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.Conversation_MessageDoesntExist, timeout: nil, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in + return true + }), in: .current) + } + })) + } + }, completed: { [weak self] in + if let strongSelf = self { + strongSelf.loadingMessage.set(.single(nil)) + } + completion?() + })) + cancelImpl = { [weak self] in + if let strongSelf = self { + strongSelf.loadingMessage.set(.single(nil)) + strongSelf.messageIndexDisposable.set(nil) + } } completion?() diff --git a/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift b/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift index e9aa686373..568e230870 100644 --- a/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift +++ b/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift @@ -125,6 +125,14 @@ extension ChatControllerImpl { return } + var deleteAllMessageCount: Signal = .single(nil) + if authors.count == 1 { + deleteAllMessageCount = self.context.engine.messages.searchMessages(location: .peer(peerId: peerId, fromId: authors[0].id, tags: nil, reactions: nil, threadId: self.chatLocation.threadId, minDate: nil, maxDate: nil), query: "", state: nil) + |> map { result, _ -> Int? in + return Int(result.totalCount) + } + } + var signal = combineLatest(authors.map { author in self.context.engine.peers.fetchChannelParticipant(peerId: peerId, participantId: author.id) |> map { result -> (Peer, ChannelParticipant?) in @@ -161,8 +169,8 @@ extension ChatControllerImpl { disposables.set(nil) } - disposables.set((signal - |> deliverOnMainQueue).startStrict(next: { [weak self] authorsAndParticipants in + disposables.set((combineLatest(signal, deleteAllMessageCount) + |> deliverOnMainQueue).startStrict(next: { [weak self] authorsAndParticipants, deleteAllMessageCount in guard let self else { return } @@ -212,6 +220,7 @@ extension ChatControllerImpl { chatPeer: chatPeer, peers: renderedParticipants, messageCount: messageIds.count, + deleteAllMessageCount: deleteAllMessageCount, completion: { [weak self] result in guard let self else { return @@ -259,8 +268,16 @@ extension ChatControllerImpl { disposables.set(nil) } - disposables.set((signal - |> deliverOnMainQueue).startStrict(next: { [weak self] maybeParticipant in + var deleteAllMessageCount: Signal = .single(nil) + do { + deleteAllMessageCount = self.context.engine.messages.getSearchMessageCount(location: .peer(peerId: peerId, fromId: author.id, tags: nil, reactions: nil, threadId: self.chatLocation.threadId, minDate: nil, maxDate: nil), query: "") + |> map { result -> Int? in + return result + } + } + + disposables.set((combineLatest(signal, deleteAllMessageCount) + |> deliverOnMainQueue).startStrict(next: { [weak self] maybeParticipant, deleteAllMessageCount in guard let self else { return } @@ -310,6 +327,7 @@ extension ChatControllerImpl { peer: authorPeer._asPeer() )], messageCount: messageIds.count, + deleteAllMessageCount: deleteAllMessageCount, completion: { [weak self] result in guard let self else { return diff --git a/submodules/TelegramUI/Sources/OpenChatMessage.swift b/submodules/TelegramUI/Sources/OpenChatMessage.swift index 30f7ee7b64..229cb441fb 100644 --- a/submodules/TelegramUI/Sources/OpenChatMessage.swift +++ b/submodules/TelegramUI/Sources/OpenChatMessage.swift @@ -24,6 +24,7 @@ import WebsiteType import GalleryData import StoryContainerScreen import WallpaperGalleryScreen +import FFMpegBinding func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool { var story: TelegramMediaStory? @@ -127,6 +128,24 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool { return true } + #if DEBUG + for media in params.message.media { + if let file = media as? TelegramMediaFile, let fileName = file.fileName, (fileName.hasSuffix(".ts") || fileName.hasSuffix(".mp4s")) { + if let path = params.context.account.postbox.mediaBox.completedResourcePath(id: file.resource.id, pathExtension: nil) { + let outDir = NSTemporaryDirectory() + "test_remuxed" + let _ = try? FileManager.default.removeItem(atPath: outDir) + let _ = try? FileManager.default.createDirectory(atPath: outDir, withIntermediateDirectories: true) + for i in 0 ... 10 { + FFMpegLiveMuxer.remux(path, to: outDir + "/hls_stream0_0000\(i).ts", offsetSeconds: Double(i * 10) - 1.400000) + } + print("Remuxed into: \(outDir)") + + return true + } + } + } + #endif + if let mediaData = chatMessageGalleryControllerData(context: params.context, chatLocation: params.chatLocation, chatFilterTag: params.chatFilterTag, chatLocationContextHolder: params.chatLocationContextHolder, message: params.message, mediaIndex: params.mediaIndex, navigationController: params.navigationController, standalone: params.standalone, reverseMessageGalleryOrder: params.reverseMessageGalleryOrder, mode: params.mode, source: params.gallerySource, synchronousLoad: false, actionInteraction: params.actionInteraction) { switch mediaData { case let .url(url): diff --git a/submodules/TelegramUI/Sources/OpenUrl.swift b/submodules/TelegramUI/Sources/OpenUrl.swift index dc3144464b..e6a021bbfc 100644 --- a/submodules/TelegramUI/Sources/OpenUrl.swift +++ b/submodules/TelegramUI/Sources/OpenUrl.swift @@ -257,10 +257,10 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur if let value = URL(string: "ipfs:/" + parsedUrl.path) { parsedUrl = value } - } - } else if let scheme = parsedUrl.scheme, scheme == "https", parsedUrl.host == "t.me", parsedUrl.path.hasPrefix("/ipfs/") { - if let value = URL(string: "ipfs://" + String(parsedUrl.path[parsedUrl.path.index(parsedUrl.path.startIndex, offsetBy: "/ipfs/".count)...])) { - parsedUrl = value + } else if parsedUrl.host == "ton" { + if let value = URL(string: "ton:/" + parsedUrl.path) { + parsedUrl = value + } } } } @@ -1009,7 +1009,7 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur isInternetUrl = true } if context.sharedContext.immediateExperimentalUISettings.browserExperiment { - if parsedUrl.scheme == "ipfs" || parsedUrl.scheme == "ipns" { + if parsedUrl.scheme == "ipfs" || parsedUrl.scheme == "ipns" || parsedUrl.scheme == "ton" { isInternetUrl = true } } diff --git a/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift index 905d87206b..3df84b6729 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift @@ -251,6 +251,10 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent } if displayImage { + if captureProtected { + setLayerDisableScreenshots(self.imageNode.layer, captureProtected) + } + self.imageNode.setSignal(internalMediaGridMessageVideo(postbox: postbox, userLocation: userLocation, videoReference: fileReference, imageReference: imageReference, onlyFullSize: onlyFullSizeThumbnail, useLargeThumbnail: useLargeThumbnail, autoFetchFullSizeThumbnail: autoFetchFullSizeThumbnail || fileReference.media.isInstantVideo) |> map { [weak self] getSize, getData in Queue.mainQueue().async { if let strongSelf = self, strongSelf.dimensions == nil { diff --git a/submodules/TelegramVoip/BUILD b/submodules/TelegramVoip/BUILD index ad9406ad93..62595d2fb6 100644 --- a/submodules/TelegramVoip/BUILD +++ b/submodules/TelegramVoip/BUILD @@ -17,6 +17,7 @@ swift_library( "//submodules/TelegramUIPreferences:TelegramUIPreferences", "//submodules/TgVoip:TgVoip", "//submodules/TgVoipWebrtc:TgVoipWebrtc", + "//submodules/FFMpegBinding", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramVoip/Sources/WrappedMediaStreamingContext.swift b/submodules/TelegramVoip/Sources/WrappedMediaStreamingContext.swift index 3ae1b27e2e..0b8e717fa6 100644 --- a/submodules/TelegramVoip/Sources/WrappedMediaStreamingContext.swift +++ b/submodules/TelegramVoip/Sources/WrappedMediaStreamingContext.swift @@ -2,6 +2,9 @@ import Foundation import SwiftSignalKit import TgVoipWebrtc import TelegramCore +import Network +import Postbox +import FFMpegBinding public final class WrappedMediaStreamingContext { private final class Impl { @@ -132,3 +135,458 @@ public final class WrappedMediaStreamingContext { } } } + +public final class ExternalMediaStreamingContext { + private final class Impl { + let queue: Queue + + private var broadcastPartsSource: BroadcastPartSource? + + private let resetPlaylistDisposable = MetaDisposable() + private let updatePlaylistDisposable = MetaDisposable() + + let masterPlaylistData = Promise() + let playlistData = Promise() + let mediumPlaylistData = Promise() + + init(queue: Queue, rejoinNeeded: @escaping () -> Void) { + self.queue = queue + } + + deinit { + self.updatePlaylistDisposable.dispose() + } + + func setAudioStreamData(audioStreamData: OngoingGroupCallContext.AudioStreamData?) { + if let audioStreamData { + let broadcastPartsSource = NetworkBroadcastPartSource(queue: self.queue, engine: audioStreamData.engine, callId: audioStreamData.callId, accessHash: audioStreamData.accessHash, isExternalStream: audioStreamData.isExternalStream) + self.broadcastPartsSource = broadcastPartsSource + + self.updatePlaylistDisposable.set(nil) + + let queue = self.queue + self.resetPlaylistDisposable.set(broadcastPartsSource.requestTime(completion: { [weak self] timestamp in + queue.async { + guard let self else { + return + } + + let segmentDuration: Int64 = 1000 + + var adjustedTimestamp: Int64 = 0 + if timestamp > 0 { + adjustedTimestamp = timestamp / segmentDuration * segmentDuration - 4 * segmentDuration + } + + if adjustedTimestamp > 0 { + var masterPlaylistData = "#EXTM3U\n" + + "#EXT-X-VERSION:6\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=3300000,RESOLUTION=1280x720,CODECS=\"avc1.64001f,mp4a.40.2\"\n" + + "hls_level_0.m3u8\n" + + masterPlaylistData += "#EXT-X-STREAM-INF:BANDWIDTH=1000000,RESOLUTION=640x360,CODECS=\"avc1.64001f,mp4a.40.2\"\n" + + "hls_level_1.m3u8\n" + + self.masterPlaylistData.set(.single(masterPlaylistData)) + + self.beginUpdatingPlaylist(initialHeadTimestamp: adjustedTimestamp) + } + } + })) + } + } + + private func beginUpdatingPlaylist(initialHeadTimestamp: Int64) { + let segmentDuration: Int64 = 1000 + + var timestamp = initialHeadTimestamp + self.updatePlaylist(headTimestamp: timestamp, quality: 0) + self.updatePlaylist(headTimestamp: timestamp, quality: 1) + + self.updatePlaylistDisposable.set(( + Signal.single(Void()) + |> delay(1.0, queue: self.queue) + |> restart + |> deliverOn(self.queue) + ).start(next: { [weak self] _ in + guard let self else { + return + } + + timestamp += segmentDuration + self.updatePlaylist(headTimestamp: timestamp, quality: 0) + self.updatePlaylist(headTimestamp: timestamp, quality: 1) + })) + } + + private func updatePlaylist(headTimestamp: Int64, quality: Int) { + let segmentDuration: Int64 = 1000 + let headIndex = headTimestamp / segmentDuration + let minIndex = headIndex - 20 + + var playlistData = "#EXTM3U\n" + + "#EXT-X-VERSION:6\n" + + "#EXT-X-TARGETDURATION:1\n" + + "#EXT-X-MEDIA-SEQUENCE:\(minIndex)\n" + + "#EXT-X-INDEPENDENT-SEGMENTS\n" + + for index in minIndex ... headIndex { + playlistData.append("#EXTINF:1.000000,\n") + playlistData.append("hls_stream\(quality)_\(index).ts\n") + } + + print("Player: updating playlist \(quality) \(minIndex) ... \(headIndex)") + + if quality == 0 { + self.playlistData.set(.single(playlistData)) + } else { + self.mediumPlaylistData.set(.single(playlistData)) + } + } + + func partData(index: Int, quality: Int) -> Signal { + let segmentDuration: Int64 = 1000 + let timestamp = Int64(index) * segmentDuration + + print("Player: request part(q: \(quality)) \(index) -> \(timestamp)") + + guard let broadcastPartsSource = self.broadcastPartsSource else { + return .single(nil) + } + + return Signal { subscriber in + return broadcastPartsSource.requestPart( + timestampMilliseconds: timestamp, + durationMilliseconds: segmentDuration, + subject: .video(channelId: 1, quality: quality == 0 ? .full : .medium), + completion: { part in + var data = part.oggData + if data.count > 32 { + data = data.subdata(in: 32 ..< data.count) + } + subscriber.putNext(data) + }, + rejoinNeeded: { + //TODO + } + ) + } + } + } + + private let queue = Queue() + let id: CallSessionInternalId + private let impl: QueueLocalObject + private var hlsServerDisposable: Disposable? + + public init(id: CallSessionInternalId, rejoinNeeded: @escaping () -> Void) { + self.id = id + let queue = self.queue + self.impl = QueueLocalObject(queue: queue, generate: { + return Impl(queue: queue, rejoinNeeded: rejoinNeeded) + }) + + self.hlsServerDisposable = SharedHLSServer.shared.registerPlayer(streamingContext: self) + } + + deinit { + self.hlsServerDisposable?.dispose() + } + + public func setAudioStreamData(audioStreamData: OngoingGroupCallContext.AudioStreamData?) { + self.impl.with { impl in + impl.setAudioStreamData(audioStreamData: audioStreamData) + } + } + + public func masterPlaylistData() -> Signal { + return self.impl.signalWith { impl, subscriber in + impl.masterPlaylistData.get().start(next: subscriber.putNext) + } + } + + public func playlistData(quality: Int) -> Signal { + return self.impl.signalWith { impl, subscriber in + if quality == 0 { + impl.playlistData.get().start(next: subscriber.putNext) + } else { + impl.mediumPlaylistData.get().start(next: subscriber.putNext) + } + } + } + + public func partData(index: Int, quality: Int) -> Signal { + return self.impl.signalWith { impl, subscriber in + impl.partData(index: index, quality: quality).start(next: subscriber.putNext) + } + } +} + +public final class SharedHLSServer { + public static let shared: SharedHLSServer = { + return SharedHLSServer() + }() + + private enum ResponseError { + case badRequest + case notFound + case internalServerError + + var httpString: String { + switch self { + case .badRequest: + return "400 Bad Request" + case .notFound: + return "404 Not Found" + case .internalServerError: + return "500 Internal Server Error" + } + } + } + + private final class ContextReference { + weak var streamingContext: ExternalMediaStreamingContext? + + init(streamingContext: ExternalMediaStreamingContext) { + self.streamingContext = streamingContext + } + } + + private final class Impl { + private let queue: Queue + + private let port: NWEndpoint.Port + private var listener: NWListener? + + private var contextReferences = Bag() + + init(queue: Queue, port: UInt16) { + self.queue = queue + self.port = NWEndpoint.Port(rawValue: port)! + self.start() + } + + func start() { + let listener: NWListener + do { + listener = try NWListener(using: .tcp, on: self.port) + } catch { + Logger.shared.log("SharedHLSServer", "Failed to create listener: \(error)") + return + } + self.listener = listener + + listener.newConnectionHandler = { [weak self] connection in + guard let self else { + return + } + self.handleConnection(connection: connection) + } + + listener.stateUpdateHandler = { [weak self] state in + guard let self else { + return + } + switch state { + case .ready: + Logger.shared.log("SharedHLSServer", "Server is ready on port \(self.port)") + case let .failed(error): + Logger.shared.log("SharedHLSServer", "Server failed with error: \(error)") + self.listener?.cancel() + default: + break + } + } + + listener.start(queue: self.queue.queue) + } + + private func handleConnection(connection: NWConnection) { + connection.start(queue: self.queue.queue) + connection.receive(minimumIncompleteLength: 1, maximumLength: 1024, completion: { [weak self] data, _, isComplete, error in + guard let self else { + return + } + if let data, !data.isEmpty { + self.handleRequest(data: data, connection: connection) + } else if isComplete { + connection.cancel() + } else if let error = error { + Logger.shared.log("SharedHLSServer", "Error on connection: \(error)") + connection.cancel() + } + }) + } + + private func handleRequest(data: Data, connection: NWConnection) { + guard let requestString = String(data: data, encoding: .utf8) else { + connection.cancel() + return + } + + if !requestString.hasPrefix("GET /") { + self.sendErrorAndClose(connection: connection) + return + } + guard let firstCrLf = requestString.range(of: "\r\n") else { + self.sendErrorAndClose(connection: connection) + return + } + let firstLine = String(requestString[requestString.index(requestString.startIndex, offsetBy: "GET /".count) ..< firstCrLf.lowerBound]) + if !(firstLine.hasSuffix(" HTTP/1.0") || firstLine.hasSuffix(" HTTP/1.1")) { + self.sendErrorAndClose(connection: connection) + return + } + + let requestPath = String(firstLine[firstLine.startIndex ..< firstLine.index(firstLine.endIndex, offsetBy: -" HTTP/1.1".count)]) + + guard let firstSlash = requestPath.range(of: "/") else { + self.sendErrorAndClose(connection: connection, error: .notFound) + return + } + guard let streamId = UUID(uuidString: String(requestPath[requestPath.startIndex ..< firstSlash.lowerBound])) else { + self.sendErrorAndClose(connection: connection) + return + } + guard let streamingContext = self.contextReferences.copyItems().first(where: { $0.streamingContext?.id == streamId })?.streamingContext else { + self.sendErrorAndClose(connection: connection) + return + } + + let filePath = String(requestPath[firstSlash.upperBound...]) + if filePath == "master.m3u8" { + let _ = (streamingContext.masterPlaylistData() + |> deliverOn(self.queue) + |> take(1)).start(next: { [weak self] result in + guard let self else { + return + } + + self.sendResponseAndClose(connection: connection, data: result.data(using: .utf8)!) + }) + } else if filePath.hasPrefix("hls_level_") && filePath.hasSuffix(".m3u8") { + guard let levelIndex = Int(String(filePath[filePath.index(filePath.startIndex, offsetBy: "hls_level_".count) ..< filePath.index(filePath.endIndex, offsetBy: -".m3u8".count)])) else { + self.sendErrorAndClose(connection: connection) + return + } + + let _ = (streamingContext.playlistData(quality: levelIndex) + |> deliverOn(self.queue) + |> take(1)).start(next: { [weak self] result in + guard let self else { + return + } + + self.sendResponseAndClose(connection: connection, data: result.data(using: .utf8)!) + }) + } else if filePath.hasPrefix("hls_stream") && filePath.hasSuffix(".ts") { + let fileId = String(filePath[filePath.index(filePath.startIndex, offsetBy: "hls_stream".count) ..< filePath.index(filePath.endIndex, offsetBy: -".ts".count)]) + guard let underscoreRange = fileId.range(of: "_") else { + self.sendErrorAndClose(connection: connection) + return + } + guard let levelIndex = Int(String(fileId[fileId.startIndex ..< underscoreRange.lowerBound])) else { + self.sendErrorAndClose(connection: connection) + return + } + guard let partIndex = Int(String(fileId[underscoreRange.upperBound...])) else { + self.sendErrorAndClose(connection: connection) + return + } + let _ = (streamingContext.partData(index: partIndex, quality: levelIndex) + |> deliverOn(self.queue) + |> take(1)).start(next: { [weak self] result in + guard let self else { + return + } + + if let result { + let sourceTempFile = TempBox.shared.tempFile(fileName: "part.mp4") + let tempFile = TempBox.shared.tempFile(fileName: "part.ts") + defer { + TempBox.shared.dispose(sourceTempFile) + TempBox.shared.dispose(tempFile) + } + + guard let _ = try? result.write(to: URL(fileURLWithPath: sourceTempFile.path)) else { + self.sendErrorAndClose(connection: connection, error: .internalServerError) + return + } + + let sourcePath = sourceTempFile.path + FFMpegLiveMuxer.remux(sourcePath, to: tempFile.path, offsetSeconds: Double(partIndex)) + + if let data = try? Data(contentsOf: URL(fileURLWithPath: tempFile.path)) { + self.sendResponseAndClose(connection: connection, data: data) + } else { + self.sendErrorAndClose(connection: connection, error: .internalServerError) + } + } else { + self.sendErrorAndClose(connection: connection, error: .notFound) + } + }) + } else { + self.sendErrorAndClose(connection: connection, error: .notFound) + } + } + + private func sendErrorAndClose(connection: NWConnection, error: ResponseError = .badRequest) { + let errorResponse = "HTTP/1.1 \(error.httpString)\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n" + connection.send(content: errorResponse.data(using: .utf8), completion: .contentProcessed { error in + if let error { + Logger.shared.log("SharedHLSServer", "Failed to send response: \(error)") + } + connection.cancel() + }) + } + + private func sendResponseAndClose(connection: NWConnection, data: Data) { + let responseHeaders = "HTTP/1.1 200 OK\r\nContent-Type: application/octet-stream\r\nConnection: close\r\n\r\n" + var responseData = Data() + responseData.append(responseHeaders.data(using: .utf8)!) + responseData.append(data) + connection.send(content: responseData, completion: .contentProcessed { error in + if let error { + Logger.shared.log("SharedHLSServer", "Failed to send response: \(error)") + } + connection.cancel() + }) + } + + func registerPlayer(streamingContext: ExternalMediaStreamingContext) -> Disposable { + let queue = self.queue + let index = self.contextReferences.add(ContextReference(streamingContext: streamingContext)) + + return ActionDisposable { [weak self] in + queue.async { + guard let self else { + return + } + self.contextReferences.remove(index) + } + } + } + } + + private static let queue = Queue(name: "SharedHLSServer") + public let port: UInt16 = 8016 + private let impl: QueueLocalObject + + private init() { + let queue = SharedHLSServer.queue + let port = self.port + self.impl = QueueLocalObject(queue: queue, generate: { + return Impl(queue: queue, port: port) + }) + } + + fileprivate func registerPlayer(streamingContext: ExternalMediaStreamingContext) -> Disposable { + let disposable = MetaDisposable() + + self.impl.with { impl in + disposable.set(impl.registerPlayer(streamingContext: streamingContext)) + } + + return disposable + } +} diff --git a/submodules/UrlHandling/Sources/UrlHandling.swift b/submodules/UrlHandling/Sources/UrlHandling.swift index 5b9c30e7d5..b704748036 100644 --- a/submodules/UrlHandling/Sources/UrlHandling.swift +++ b/submodules/UrlHandling/Sources/UrlHandling.swift @@ -134,6 +134,9 @@ public func parseInternalUrl(sharedContext: SharedAccountContext, query: String) if query.hasPrefix("ipfs/") { return .externalUrl(url: "ipfs://" + String(query[query.index(query.startIndex, offsetBy: "ipfs/".count)...])) } + if query.hasPrefix("ton/") { + return .externalUrl(url: "ton://" + String(query[query.index(query.startIndex, offsetBy: "ton/".count)...])) + } } if pathComponents[0].hasPrefix("+") || pathComponents[0].hasPrefix("%20") { diff --git a/submodules/ffmpeg/Sources/FFMpeg/build-ffmpeg-bazel.sh b/submodules/ffmpeg/Sources/FFMpeg/build-ffmpeg-bazel.sh index 32239864f5..be58ac5e4f 100755 --- a/submodules/ffmpeg/Sources/FFMpeg/build-ffmpeg-bazel.sh +++ b/submodules/ffmpeg/Sources/FFMpeg/build-ffmpeg-bazel.sh @@ -47,13 +47,13 @@ CONFIGURE_FLAGS="--enable-cross-compile --disable-programs \ --enable-libopus \ --enable-libvpx \ --enable-audiotoolbox \ - --enable-bsf=aac_adtstoasc,vp9_superframe \ + --enable-bsf=aac_adtstoasc,vp9_superframe,h264_mp4toannexb \ --enable-decoder=h264,libvpx_vp9,hevc,libopus,mp3,aac,flac,alac_at,pcm_s16le,pcm_s24le,gsm_ms_at \ - --enable-encoder=libvpx_vp9 \ - --enable-demuxer=aac,mov,m4v,mp3,ogg,libopus,flac,wav,aiff,matroska \ + --enable-encoder=libvpx_vp9,aac_at \ + --enable-demuxer=aac,mov,m4v,mp3,ogg,libopus,flac,wav,aiff,matroska,mpegts \ --enable-parser=aac,h264,mp3,libopus \ --enable-protocol=file \ - --enable-muxer=mp4,matroska \ + --enable-muxer=mp4,matroska,mpegts \ "