├── .gitignore ├── Package.swift ├── Sources ├── Streams.swift ├── Frames.swift └── Http2.swift └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | import PackageDescription 2 | 3 | let package = Package( 4 | name: "SwiftHttp2", 5 | dependencies: [ 6 | .Package(url: "https://github.com/nathanborror/swift-hpack.git", Version(0, 1, 2)), 7 | ] 8 | ) 9 | -------------------------------------------------------------------------------- /Sources/Streams.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftHttp2/Sources/Streams.swift - HTTP/2 Library 3 | // 4 | // This source file is part of the SwiftHttp2 open source project 5 | // https://github.com/nathanborror/swift-http2 6 | // Created by Nathan Borror on 10/1/16. 7 | // 8 | 9 | import Foundation 10 | 11 | enum StreamState { 12 | case none 13 | case idle 14 | case reservedLocal 15 | case reservedRemote 16 | case open 17 | case halfClosedRemote 18 | case halfClosedLocal 19 | case closed 20 | } 21 | 22 | public typealias StreamID = Int 23 | 24 | public struct StreamCache { 25 | 26 | var streams = [Int: StreamState]() 27 | var counter = 1 28 | 29 | public mutating func next() -> StreamID { 30 | streams[counter] = .none 31 | let s = counter 32 | counter += 2 33 | return s 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Swift Http2 2 | 3 | A very simple [HTTP/2][1] library for Swift. 4 | 5 | --- 6 | 7 | :warning: There is active work going on here that will result in API changes. :warning: 8 | 9 | --- 10 | 11 | ## Usage 12 | 13 | ```swift 14 | class MyDelegate: NSObject, Http2SessionDelegate { 15 | 16 | public func sessionConnected(session: http2Session) {} 17 | public func session(session: http2Session, hasFrame frame: Frame) {} 18 | } 19 | 20 | let session = Http2Session(url: URL(string: "http://localhost")!) 21 | let sessionDelegate = MyDelegate() 22 | session.delegate = sessionDelegate 23 | 24 | // Connect to host 25 | session.connect() 26 | 27 | // Get a new stream ID 28 | let stream = session.streams.next() 29 | 30 | 31 | // Set some headers 32 | let headers = [ 33 | (":method", "POST"), 34 | (":scheme", "http"), 35 | (":path", "/"), 36 | ("content-type", "application/json"), 37 | ("te", "trailers"), 38 | ] 39 | 40 | // Create a header frame and write it to the session 41 | let frame = Frame(headers: headers, stream: stream, flags: .endHeaders) 42 | try? session.write(frame: frame) 43 | 44 | // Create a data frame and send some bytes 45 | let bytes = [UInt8]() 46 | let data = Frame(data: bytes, stream: stream, flags: .endStream) 47 | try? session.write(frame: data) 48 | 49 | // Disconnect from host 50 | session.disconnect() 51 | ``` 52 | 53 | [1]:https://tools.ietf.org/html/rfc7540 54 | -------------------------------------------------------------------------------- /Sources/Frames.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftHttp2/Sources/Frames.swift - HTTP/2 Library 3 | // 4 | // This source file is part of the SwiftHttp2 open source project 5 | // https://github.com/nathanborror/swift-http2 6 | // Created by Nathan Borror on 10/1/16. 7 | // 8 | 9 | import Foundation 10 | import SwiftHpack 11 | 12 | public enum FrameType: UInt8 { 13 | case data = 0x0 14 | case headers = 0x1 15 | case priority = 0x2 16 | case rstStream = 0x3 17 | case settings = 0x4 18 | case pushPromise = 0x5 19 | case ping = 0x6 20 | case goaway = 0x7 21 | case windowUpdate = 0x8 22 | case continuation = 0x9 23 | } 24 | 25 | public typealias FrameFlag = UInt8 26 | extension FrameFlag { 27 | public static let endStream: FrameFlag = 0x1 28 | public static let settingsAck: FrameFlag = 0x1 29 | public static let pingAck: FrameFlag = 0x1 30 | public static let endHeaders: FrameFlag = 0x4 31 | public static let streamClosed: FrameFlag = 0x5 32 | public static let padded: FrameFlag = 0x8 33 | public static let priority: FrameFlag = 0x20 34 | } 35 | 36 | public struct Frame { 37 | public let type: FrameType 38 | public let stream: StreamID 39 | public let flags: FrameFlag 40 | public let length: Int 41 | public var payload: [UInt8]? 42 | 43 | public init(type: FrameType, stream: StreamID = 0, flags: FrameFlag = 0, length: Int = 0, payload: [UInt8]? = nil) { 44 | self.type = type 45 | self.stream = stream 46 | self.flags = flags 47 | self.length = length 48 | self.payload = payload 49 | } 50 | 51 | public init(headers: [(String, String)], stream: StreamID = 0, flags: FrameFlag = 0) { 52 | self.type = .headers 53 | self.stream = stream 54 | self.flags = flags 55 | 56 | let encoder = Encoder() 57 | let payload = Bytes() 58 | do { 59 | for (name, value) in headers { 60 | try encoder.encodeHeader(out: payload, name: name, value: value) 61 | } 62 | } catch { 63 | print(error) 64 | } 65 | 66 | self.payload = payload.data 67 | self.length = self.payload?.count ?? 0 68 | } 69 | 70 | public init(data: [UInt8], stream: StreamID = 0, flags: FrameFlag = 0) { 71 | self.type = .data 72 | self.stream = stream 73 | self.flags = flags 74 | self.payload = data 75 | self.length = data.count 76 | } 77 | 78 | public init?(bytes: [UInt8]) { 79 | guard bytes.count >= 9 else { return nil } 80 | let length = (UInt32(bytes[0]) << 16) + (UInt32(bytes[1]) << 8) + UInt32(bytes[2]) 81 | self.length = Int(length) 82 | guard let type = FrameType(rawValue: bytes[3]) else { 83 | return nil 84 | } 85 | self.type = type 86 | self.flags = bytes[4] 87 | var stream = UInt32(bytes[5]) 88 | stream <<= 8 89 | stream += UInt32(bytes[6]) 90 | stream <<= 8 91 | stream += UInt32(bytes[7]) 92 | stream <<= 8 93 | stream += UInt32(bytes[8]) 94 | stream &= ~0x80000000 95 | self.stream = Int(stream) 96 | self.payload = nil 97 | } 98 | 99 | public func bytes() -> [UInt8] { 100 | var data = [UInt8]() 101 | 102 | let l = htonl(UInt32(self.length)) >> 8 103 | data.append(UInt8(l & 0xFF)) 104 | data.append(UInt8((l >> 8) & 0xFF)) 105 | data.append(UInt8((l >> 16) & 0xFF)) 106 | 107 | data.append(self.type.rawValue) 108 | data.append(self.flags) 109 | 110 | let s = htonl(UInt32(self.stream)) 111 | data.append(UInt8(s & 0xFF)) 112 | data.append(UInt8((s >> 8) & 0xFF)) 113 | data.append(UInt8((s >> 16) & 0xFF)) 114 | data.append(UInt8((s >> 24) & 0xFF)) 115 | return data 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Sources/Http2.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftHttp2/Sources/Http2.swift - HTTP/2 Library 3 | // 4 | // This source file is part of the SwiftHttp2 open source project 5 | // https://github.com/nathanborror/swift-http2 6 | // Created by Nathan Borror on 10/1/16. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol Http2SessionDelegate: class { 12 | 13 | func sessionConnected(session: Http2Session) 14 | func session(session: Http2Session, hasFrame frame: Frame) 15 | } 16 | 17 | public enum Http2SessionError: Error { 18 | 19 | case missingInput 20 | case missingOutput 21 | case connectionTimeout 22 | } 23 | 24 | public enum Http2Error: Error { 25 | case none 26 | case protocol_ 27 | case internal_ 28 | case flowControl 29 | case settingsTimeout 30 | case streamClosed 31 | case frameSize 32 | case refusedStream 33 | case cancel 34 | case compression 35 | case connect 36 | case enhanceYourCalm 37 | case inadequateSecurity 38 | case http1_1Required 39 | 40 | init?(hex: UInt8) { 41 | switch hex { 42 | case 0x1: self = .protocol_ 43 | case 0x2: self = .internal_ 44 | case 0x3: self = .flowControl 45 | case 0x4: self = .settingsTimeout 46 | case 0x5: self = .streamClosed 47 | case 0x6: self = .frameSize 48 | case 0x7: self = .refusedStream 49 | case 0x8: self = .cancel 50 | case 0x9: self = .compression 51 | case 0xa: self = .connect 52 | case 0xb: self = .enhanceYourCalm 53 | case 0xc: self = .inadequateSecurity 54 | case 0xd: self = .http1_1Required 55 | default: return nil 56 | } 57 | } 58 | } 59 | 60 | public enum Http2SessionState { 61 | case disconnected 62 | case connecting 63 | case connected 64 | case ready 65 | } 66 | 67 | public class Http2Session: NSObject { 68 | 69 | let url: URL 70 | 71 | public var delegate: Http2SessionDelegate? 72 | 73 | var inputStream: InputStream? 74 | var outputStream: OutputStream? 75 | 76 | var state: Http2SessionState = .disconnected { 77 | didSet { handleStateChange(previous: oldValue) } 78 | } 79 | 80 | var isCertValidated = false 81 | 82 | private var inputQueue: [UInt8] 83 | private let writeQueue: OperationQueue 84 | private var fragBuffer: Data? 85 | 86 | private static let sharedQueue = DispatchQueue(label: "org.swift.http2.session", attributes: []) 87 | 88 | public var streams: StreamCache 89 | 90 | public init(url: URL) { 91 | self.url = url 92 | self.writeQueue = OperationQueue() 93 | self.writeQueue.maxConcurrentOperationCount = 1 94 | self.inputQueue = [] 95 | self.streams = StreamCache() 96 | } 97 | 98 | func handleStateChange(previous: Http2SessionState) { 99 | if state == .connected { 100 | writeHandshake() 101 | } 102 | } 103 | 104 | public func connect() { 105 | guard state == .disconnected else { return } 106 | state = .connecting 107 | makeConnection() 108 | } 109 | 110 | func makeConnection() { 111 | guard let req = makeRequest() else { 112 | print("HTTP/2 connection attempt failed") 113 | return 114 | } 115 | makeStreams(with: req) 116 | } 117 | 118 | func makeRequest() -> Data? { 119 | let req = CFHTTPMessageCreateEmpty(kCFAllocatorDefault, true).takeRetainedValue() 120 | 121 | CFHTTPMessageAppendBytes(req, prism, prism.count) 122 | 123 | // TODO: Figure out how to get rid of the above without having 124 | // CFHTTPMessageCopySerializedMessage returning nil. 125 | 126 | guard let cfData = CFHTTPMessageCopySerializedMessage(req) else { 127 | print("CFHTTPMessageCopySerializedMessage returned nil") 128 | return nil 129 | } 130 | return cfData.takeRetainedValue() as Data 131 | } 132 | 133 | func makeStreams(with request: Data) { 134 | var readStream: Unmanaged? 135 | var writeStream: Unmanaged? 136 | 137 | guard let host = url.host, 138 | let port = url.port else { fatalError() } 139 | 140 | CFStreamCreatePairWithSocketToHost(nil, host as CFString, UInt32(port), &readStream, &writeStream) 141 | 142 | inputStream = readStream!.takeRetainedValue() 143 | outputStream = writeStream!.takeRetainedValue() 144 | 145 | guard let input = inputStream, 146 | let output = outputStream else { fatalError() } 147 | 148 | input.delegate = self 149 | output.delegate = self 150 | 151 | if let scheme = url.scheme, scheme == "https" { 152 | input.setProperty(StreamSocketSecurityLevel.negotiatedSSL.rawValue, 153 | forKey: Stream.PropertyKey.socketSecurityLevelKey) 154 | output.setProperty(StreamSocketSecurityLevel.negotiatedSSL.rawValue, 155 | forKey: Stream.PropertyKey.socketSecurityLevelKey) 156 | 157 | let settings: [NSObject: NSObject] = [ 158 | kCFStreamSSLValidatesCertificateChain: NSNumber(booleanLiteral: false), 159 | ] 160 | 161 | input.setProperty(settings, forKey: kCFStreamPropertySSLSettings as Stream.PropertyKey) 162 | output.setProperty(settings, forKey: kCFStreamPropertySSLSettings as Stream.PropertyKey) 163 | } 164 | 165 | CFReadStreamSetDispatchQueue(input, Http2Session.sharedQueue) 166 | CFWriteStreamSetDispatchQueue(output, Http2Session.sharedQueue) 167 | 168 | input.open() 169 | output.open() 170 | } 171 | 172 | func writeHandshake() { 173 | var bytes = [UInt8]() 174 | bytes += prism 175 | bytes += settings 176 | 177 | guard let output = outputStream else { fatalError() } 178 | output.write(bytes, maxLength: bytes.count) 179 | 180 | state = .ready 181 | delegate?.sessionConnected(session: self) 182 | } 183 | 184 | lazy var prism: [UInt8] = { 185 | return [UInt8]("PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n".utf8) 186 | }() 187 | 188 | lazy var settings: [UInt8] = { 189 | return Frame(type: .settings).bytes() 190 | }() 191 | 192 | func processInput() throws { 193 | guard let input = inputStream else { return } 194 | 195 | var buffer = [UInt8](repeating: 0, count: 4096) 196 | let read = input.read(&buffer, maxLength: buffer.count) 197 | guard read > 0 else { print("processInput: failed -1"); return } 198 | inputQueue += Array(buffer[0..= 9 else { 205 | throw Http2Error.frameSize 206 | } 207 | while !inputQueue.isEmpty { 208 | let buffer = Array(inputQueue[0..<9]) 209 | inputQueue = Array(inputQueue[buffer.count.. 0 { 214 | let count = Int(frame.length) 215 | frame.payload = Array(inputQueue[0..