Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a URLSession implementation that provides transparent proxy support. #834

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
267 changes: 267 additions & 0 deletions Sources/Engine/UrlSessionEngine.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
//
// UrlSessionEngine.swift
// Starscream
//
// Created by Gary Hughes on 22/9/20.
// Copyright © 2020. All rights reserved.
//
import Foundation

// This engine implementation provides transparent proxy support for macOS 10.11 onwards which is not possible with the WSEngine implementation.
@available(macOS 10.11, *)
public class UrlSessionEngine : NSObject, Engine, FramerEventClient, FrameCollectorDelegate, HTTPHandlerDelegate
{
var urlSession: URLSession? = nil
var streamTask: URLSessionStreamTask? = nil

weak var delegate: EngineDelegate?

let compressionHandler: CompressionHandler?
let frameHandler = FrameCollector()
let headerChecker: HeaderValidator = FoundationSecurity()
let framer: Framer = WSFramer()
let httpHandler: HTTPHandler = FoundationHTTPHandler()

var didUpgrade = false
var secKeyValue = ""
var request: URLRequest!

public var respondToPingWithPong: Bool = true

// It is useful to ignore errors caused by invalid or self signed certificates etc particularly
// in development environments.
public var acceptAnyCredentials: Bool = false


public init(compressionHandler: CompressionHandler? = nil)
{
self.compressionHandler = compressionHandler
super.init()
framer.register(delegate: self)
httpHandler.register(delegate: self)
framer.updateCompression(supports: compressionHandler != nil)
frameHandler.delegate = self
}

public func register(delegate: EngineDelegate)
{
self.delegate = delegate
}

public func start(request: URLRequest)
{
self.request = request

guard let host = request.url?.host, let port = request.url?.port else {
return
}

urlSession = URLSession(configuration: URLSessionConfiguration.ephemeral, delegate: self, delegateQueue: OperationQueue.main)

guard let session = urlSession else {
return
}

streamTask = session.streamTask(withHostName: host, port: port)

guard let task = streamTask else {
return
}

if request.url?.scheme == "https" {
task.startSecureConnection()
}

task.resume()
doRead()

secKeyValue = HTTPWSHeader.generateWebSocketKey()
let wsReq = HTTPWSHeader.createUpgrade(request: request, supportsCompression: framer.supportsCompression(), secKeyValue: secKeyValue)
let data = httpHandler.convert(request: wsReq)
write(data: data, opcode: .binaryFrame) {}
}

public func stop(closeCode: UInt16 = CloseCode.normal.rawValue)
{
streamTask?.cancel()
}

public func forceStop()
{
streamTask?.cancel()
}

private func doRead()
{
guard let task = streamTask else {
return
}

task.readData(ofMinLength: 2, maxLength: Int.max, timeout: 0) { [weak self] data, atEOF, error in

guard let welf = self else {
return
}

if let error = error {
welf.stop()
welf.broadcast(event: .error(error))
return
}

if atEOF {
welf.stop()
welf.broadcast(event: .disconnected("read failed with eof", 0))
return
}

if let data = data {
if welf.didUpgrade {
welf.framer.add(data: data)
} else {
let offset = welf.httpHandler.parse(data: data)
if offset > 0 {
let extraData = data.subdata(in: offset..<data.endIndex)
welf.framer.add(data: extraData)
}
}
}

welf.doRead()
}
}

public func write(data: Data, opcode: FrameOpCode, completion: (() -> ())?)
{
var isCompressed = false
var sendData = data
if let compressedData = compressionHandler?.compress(data: data) {
sendData = compressedData
isCompressed = true
}

guard let task = streamTask else {
return
}

switch opcode {
case .pong:
fallthrough
case .binaryFrame:
if self.didUpgrade {
sendData = framer.createWriteFrame(opcode: opcode, payload: data, isCompressed: isCompressed)
}
task.write(sendData, timeout: 0) { error in
if let error = error {
self.stop()
self.broadcast(event: .disconnected(error.localizedDescription, 0))
return
}
if let completion = completion {
completion()
}
}
case .textFrame:
let text = String(data: data, encoding: .utf8)!
write(string: text, completion: completion)
default:
break
}
}

public func write(string: String, completion: (() -> ())?)
{
let data = string.data(using: .utf8)!
write(data: data, opcode: .textFrame, completion: completion)
}

private func broadcast(event: WebSocketEvent)
{
delegate?.didReceive(event: event)
}

private func handleError(_ error: Error?) {
if let wsError = error as? WSError {
stop(closeCode: wsError.code)
} else {
stop()
}

delegate?.didReceive(event: .error(error))
}

// MARK: - HTTPHandlerDelegate

public func didReceiveHTTP(event: HTTPEvent) {
switch event {
case .success(let headers):
if let error = headerChecker.validate(headers: headers, key: secKeyValue) {
handleError(error)
return
}
didUpgrade = true
compressionHandler?.load(headers: headers)
if let url = request.url {
HTTPCookie.cookies(withResponseHeaderFields: headers, for: url).forEach {
HTTPCookieStorage.shared.setCookie($0)
}
}

broadcast(event: .connected(headers))
case .failure(let error):
handleError(error)
}
}

// MARK: - FramerEventClient

public func frameProcessed(event: FrameEvent) {
switch event {
case .frame(let frame):
frameHandler.add(frame: frame)
case .error(let error):
handleError(error)
}
}

// MARK: - FrameCollectorDelegate

public func decompress(data: Data, isFinal: Bool) -> Data? {
return compressionHandler?.decompress(data: data, isFinal: isFinal)
}

public func didForm(event: FrameCollector.Event) {
switch event {
case .text(let string):
broadcast(event: .text(string))
case .binary(let data):
broadcast(event: .binary(data))
case .pong(let data):
broadcast(event: .pong(data))
case .ping(let data):
broadcast(event: .ping(data))
if respondToPingWithPong {
write(data: data ?? Data(), opcode: .pong, completion: nil)
}
case .closed(let reason, let code):
broadcast(event: .disconnected(reason, code))
stop(closeCode: code)
case .error(let error):
handleError(error)
}
}
}

@available(macOS 10.11, *)
extension UrlSessionEngine : URLSessionDelegate
{
public func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
{
if acceptAnyCredentials {
completionHandler(.useCredential, URLCredential(trust: challenge.protectionSpace.serverTrust!))
}
else {
completionHandler(.performDefaultHandling, challenge.proposedCredential)
}
}
}
4 changes: 4 additions & 0 deletions Starscream.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
8A906E3D2208BD9B0015057D /* Compression.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A906E3C2208BD9B0015057D /* Compression.swift */; };
8A906E3F2208C7E80015057D /* WSCompression.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A906E3E2208C7E80015057D /* WSCompression.swift */; };
8ABD4470224C036A00FB8370 /* Data+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8ABD446F224C036A00FB8370 /* Data+Extensions.swift */; };
9A36C27F251ACBBF00D69392 /* UrlSessionEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A36C27E251ACBBF00D69392 /* UrlSessionEngine.swift */; };
BBB5ABE8215E2217005B48B6 /* WebSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBB5ABE4215E2217005B48B6 /* WebSocket.swift */; };
/* End PBXBuildFile section */

Expand Down Expand Up @@ -77,6 +78,7 @@
8A906E3C2208BD9B0015057D /* Compression.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Compression.swift; path = Compression/Compression.swift; sourceTree = "<group>"; };
8A906E3E2208C7E80015057D /* WSCompression.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = WSCompression.swift; path = Compression/WSCompression.swift; sourceTree = "<group>"; };
8ABD446F224C036A00FB8370 /* Data+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Extensions.swift"; sourceTree = "<group>"; };
9A36C27E251ACBBF00D69392 /* UrlSessionEngine.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UrlSessionEngine.swift; sourceTree = "<group>"; };
BBB5ABE4215E2217005B48B6 /* WebSocket.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; name = WebSocket.swift; path = Starscream/WebSocket.swift; sourceTree = "<group>"; tabWidth = 4; };
D88EAF811ED4DFD3004FE2C3 /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; };
D88EAF831ED4E7D8004FE2C3 /* CompressionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompressionTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -105,6 +107,7 @@
5C7CB5A522B59A82006AF81B /* Engine */ = {
isa = PBXGroup;
children = (
9A36C27E251ACBBF00D69392 /* UrlSessionEngine.swift */,
5C7CB5A822B5A14C006AF81B /* Engine.swift */,
5C7CB5A622B59ABA006AF81B /* NativeEngine.swift */,
5C7CB5AA22B5ABF0006AF81B /* WSEngine.swift */,
Expand Down Expand Up @@ -359,6 +362,7 @@
buildActionMask = 2147483647;
files = (
8A7A719321FA42BA0061166D /* HTTPHandler.swift in Sources */,
9A36C27F251ACBBF00D69392 /* UrlSessionEngine.swift in Sources */,
8A7A719121FA3DCD0061166D /* FrameCollector.swift in Sources */,
8A1681A7223D8664000C08D8 /* Security.swift in Sources */,
8A7A718A21F8E23C0061166D /* TCPTransport.swift in Sources */,
Expand Down