├── .gitignore ├── LICENSE.md ├── Package.swift ├── README.md └── Sources ├── Documentation.docc └── Documentation.md ├── Intermodular ├── Extensions │ └── NetworkExtension │ │ ├── NEVPNConnection++.swift │ │ ├── NEVPNManager++.swift │ │ ├── NEVPNStatus++.swift │ │ └── VPNManager.swift └── Protocol Implementations │ └── NetworkExtension │ ├── NetworkExtension+CustomStringConvertible.swift │ └── NetworkExtesion+Codable.swift ├── Intramodular ├── Encoding & Decoding │ ├── ASN1 │ │ ├── ASN1Decoder.swift │ │ ├── ASN1DistinguishedNames.swift │ │ ├── ASN1Identifier.swift │ │ └── ASN1Object.swift │ ├── OID.swift │ ├── PKCS7 │ │ ├── PKCS7.swift │ │ ├── PKCS7_AppleReceipt.swift │ │ └── PKCS7_Signature.swift │ └── X509 │ │ ├── X509Certificate.swift │ │ ├── X509Extension.swift │ │ └── X509PublicKey.swift ├── HTTP │ ├── HTTPAuthorizationType.swift │ ├── HTTPCache.swift │ ├── HTTPCacheControlType.swift │ ├── HTTPConnectionType.swift │ ├── HTTPEndpoint.swift │ ├── HTTPHeaderField.Link.swift │ ├── HTTPHeaderField.swift │ ├── HTTPInterface.swift │ ├── HTTPMediaType.swift │ ├── HTTPMethod.swift │ ├── HTTPProtocol.swift │ ├── HTTPRepository.swift │ ├── HTTPSession.Task.swift │ ├── HTTPSession.swift │ ├── HTTPUserAgent.swift │ ├── Request │ │ ├── HTTPRequest.Body.swift │ │ ├── HTTPRequest.Error.swift │ │ ├── HTTPRequest.swift │ │ ├── HTTPRequestBuilders.swift │ │ ├── HTTPRequestPopulator.swift │ │ └── Multipart │ │ │ ├── HTTPRequest.Multipart.Content.Boundary.swift │ │ │ ├── HTTPRequest.Multipart.Content.Entity.swift │ │ │ ├── HTTPRequest.Multipart.Content.Subtype.swift │ │ │ ├── HTTPRequest.Multipart.Content.swift │ │ │ ├── HTTPRequest.Multipart.HeaderField.swift │ │ │ ├── HTTPRequest.Multipart.Part.swift │ │ │ └── HTTPRequest.Multipart.swift │ └── Response │ │ ├── HTTPResponse.swift │ │ ├── HTTPResponseDecodable.swift │ │ └── HTTPResponseStatusCode.swift ├── ICMP │ ├── ICMP.swift │ ├── Ping.swift │ └── Pinger.swift ├── IPv4.swift ├── InternetProtocol.swift ├── Server Sent Events │ ├── ServerSentEvents.EventSource.SessionDelegate.swift │ ├── ServerSentEvents.EventSource.swift │ ├── ServerSentEvents.EventSourceError.swift │ ├── ServerSentEvents.ServerMessage.swift │ ├── ServerSentEvents._ServerMessageParser.swift │ └── ServerSentEvents.swift ├── TLV │ └── TLVMessageProtocol.swift └── Utilities │ └── _AsyncWebSocket.swift └── module.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .swiftpm/ 3 | .swiftpm/* 4 | /*.xcodeproj 5 | /.build 6 | /Packages 7 | xcuserdata/ 8 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ``` 2 | Copyright © 2023 Vatsal Manot 3 | 4 | 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: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | 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. 9 | ``` -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.10 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "NetworkKit", 7 | platforms: [ 8 | .iOS(.v14), 9 | .macOS(.v12), 10 | .tvOS(.v14), 11 | .watchOS(.v9) 12 | ], 13 | products: [ 14 | .library( 15 | name: "NetworkKit", 16 | targets: [ 17 | "NetworkKit" 18 | ] 19 | ) 20 | ], 21 | dependencies: [ 22 | .package(url: "https://github.com/vmanot/CorePersistence.git", branch: "main"), 23 | .package(url: "https://github.com/vmanot/Merge.git", branch: "master"), 24 | .package(url: "https://github.com/vmanot/Swallow.git", branch: "master"), 25 | .package(url: "https://github.com/vmanot/SwiftAPI.git", branch: "master"), 26 | ], 27 | targets: [ 28 | .target( 29 | name: "NetworkKit", 30 | dependencies: [ 31 | "CorePersistence", 32 | "Merge", 33 | "Swallow", 34 | "SwiftAPI", 35 | ], 36 | path: "Sources", 37 | swiftSettings: [] 38 | ), 39 | ] 40 | ) 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NetworkKit 2 | 3 | NetworkKit is a networking library written in Swift. It offers the following: 4 | 5 | - Idiomatic Swift types for representing HTTP requests and related data types. 6 | - A type-safe [Retrofit](https://square.github.io/retrofit/) inspired DSL for declaring HTTP interfaces. 7 | - Utilities to manipulate standard certificate formats used in secure telecommunication (ASN.1, PKCS 7, X509) 8 | - Common implementations for custom framing protocols. 9 | - Extensions for the `NetworkExtension` framework. 10 | 11 | ## Declarative HTTP Interfaces 12 | 13 | NetworkKit allows for powerful declarative composition of various kinds of HTTP interfaces. 14 | 15 | A NetworkKit HTTP interface is fundamentally composed of the following: 16 | 17 | - A base URL. 18 | - A list of endpoints. 19 | - A generic `Endpoint` class (inheriting from `BaseHTTPEndpoint`) responsible for configuring a generic HTTP request containing shared parameters (such as API keys etc.). 20 | 21 | Specific API endpoints by initializing the generic `Endpoint` class declared in the interface, and annotating them with NetworkKit provided decorators (i.e. property wrappers). The following decorators are supported: 22 | 23 | - `@Host(...)` 24 | - `@Path(...)` 25 | - `@AbsolutePath(...)` 26 | - `@DELETE` 27 | - `@GET` 28 | - `@PATCH` 29 | - `@POST` 30 | - `@PUT` 31 | - `@Query(...)` 32 | - `@Header(...)` 33 | - `@Body(...)` 34 | 35 | These decorators can be composed together to declare the configuration for an API endpoint. 36 | 37 | ### Declaring a REST interface for GIPHY with NetworkKit 38 | 39 | Here is a sample NetworkKit interface for the GIPHY API: 40 | 41 | ```swift 42 | public struct GIPHY_API: RESTAPISpecification { 43 | public var apiKey: String 44 | public var host = URL(string: "https://api.giphy.com")! 45 | 46 | public init(apiKey: String) { 47 | self.apiKey = apiKey 48 | } 49 | 50 | public var baseURL: URL { 51 | host.appendingPathComponent("/v1") 52 | } 53 | 54 | public var id: some Hashable { 55 | apiKey 56 | } 57 | 58 | @Path("gifs/search") 59 | @GET 60 | @Query({ context in 61 | return [ 62 | "api_key": context.root.apiKey, 63 | "q": context.input.q, 64 | "limit": context.input.limit?.description 65 | ] 66 | }) 67 | var search = Endpoint () 68 | } 69 | 70 | extension GIPHY_API { 71 | public final class Endpoint: BaseHTTPEndpoint { 72 | override public func buildRequestBase( 73 | from input: Input, 74 | context: BuildRequestContext 75 | ) throws -> Request { 76 | let request = try super.buildRequestBase(from: input, context: context) 77 | .header(.accept(.json)) 78 | .header(.contentType(.json)) 79 | 80 | return request 81 | } 82 | 83 | override public func decodeOutputBase( 84 | from response: Request.Response, 85 | context: DecodeOutputContext 86 | ) throws -> Output { 87 | try response.validate() 88 | 89 | return try response.decode(Output.self, using: JSONDecoder(keyDecodingStrategy: .convertFromSnakeCase)) 90 | } 91 | } 92 | } 93 | 94 | extension GIPHY_API { 95 | public enum RequestBodies { 96 | public struct Search: Codable, Hashable { 97 | public var q: String 98 | public var limit: Int32? 99 | public var offset: Int? 100 | public var lang = "en" 101 | } 102 | } 103 | 104 | public enum ResponseBodies { 105 | public struct Search: Codable, Hashable { 106 | public struct Pagination: Codable, Hashable { 107 | public let offset: Int32 108 | public let totalCount: Int32 109 | public let count: Int32 110 | } 111 | 112 | public struct Meta: Codable, Hashable { 113 | public let msg: String 114 | public let status: HTTPResponseStatusCode 115 | public let responseId: String 116 | } 117 | 118 | public let data: [GIPHY_API.Schema.GIFObject] 119 | public let pagination: Pagination 120 | public let meta: Meta 121 | } 122 | } 123 | } 124 | 125 | extension GIPHY_API { 126 | public enum Schema { 127 | public struct GIFObject: Codable, Hashable { 128 | public struct Images: Codable, Hashable { 129 | public struct Downsized: Codable, Hashable { 130 | public let url: URL 131 | public let width: String 132 | public let height: String 133 | public let size: String 134 | } 135 | 136 | public let downsized: Downsized 137 | } 138 | 139 | public let type: String 140 | public let id: String 141 | public let slug: String 142 | public let url: URL 143 | public let bitlyUrl: URL? 144 | public let embedUrl: URL? 145 | public let username: String? 146 | public let source: String 147 | public let rating: String? 148 | // ... 149 | public let images: Images 150 | public let title: String 151 | } 152 | } 153 | } 154 | ``` 155 | 156 | -------------------------------------------------------------------------------- /Sources/Documentation.docc/Documentation.md: -------------------------------------------------------------------------------- 1 | # ``NetworkKit`` 2 | 3 | Modern APIs and DSLs for writing client-side networking code. 4 | 5 | ## Overview 6 | 7 | TBD. 8 | -------------------------------------------------------------------------------- /Sources/Intermodular/Extensions/NetworkExtension/NEVPNConnection++.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | #if os(iOS) || os(macOS) || targetEnvironment(macCatalyst) 6 | 7 | import Merge 8 | import NetworkExtension 9 | import Swift 10 | 11 | extension NEVPNConnection { 12 | private enum _Error: Error { 13 | case badStatus(NEVPNStatus) 14 | case unknownStatus(NEVPNStatus) 15 | } 16 | 17 | public func start() -> AnySingleOutputPublisher { 18 | guard status != .connected else { 19 | return .just(()) 20 | } 21 | 22 | let publisher = NotificationCenter.default.publisher(for: .NEVPNStatusDidChange, object: self).flatMap { _ -> AnyPublisher in 23 | switch self.status { 24 | case .invalid: 25 | return .failure(.badStatus(self.status)) 26 | case .disconnected: 27 | return .empty() 28 | case .connecting: 29 | return .empty() 30 | case .connected: 31 | return .just(()) 32 | case .reasserting: 33 | return .empty() 34 | case .disconnecting: 35 | return .empty() 36 | @unknown default: 37 | return .failure(.unknownStatus(self.status)) 38 | } 39 | } 40 | .prefix(1) 41 | .eraseError() 42 | ._unsafe_eraseToAnySingleOutputPublisher() 43 | 44 | do { 45 | try startVPNTunnel() 46 | } catch { 47 | return .failure(error) 48 | } 49 | 50 | if status == .connected { 51 | return .just(()) 52 | } else if status == .invalid { 53 | return .failure(_Error.badStatus(status)) 54 | } 55 | 56 | return publisher 57 | } 58 | 59 | public func stop() -> AnySingleOutputPublisher { 60 | guard status != .disconnected else { 61 | return .just(()) 62 | } 63 | 64 | stopVPNTunnel() 65 | 66 | if status == .disconnected { 67 | return .just(()) 68 | } else if status == .invalid { 69 | return .failure(_Error.badStatus(status)) 70 | } 71 | 72 | return NotificationCenter.default.publisher(for: .NEVPNStatusDidChange, object: self).flatMap { _ -> AnyPublisher in 73 | switch self.status { 74 | case .invalid: 75 | return .failure(.badStatus(self.status)) 76 | case .disconnected: 77 | return .just(()) 78 | case .connecting: 79 | return .empty() 80 | case .connected: 81 | return .empty() 82 | case .reasserting: 83 | return .empty() 84 | case .disconnecting: 85 | return .empty() 86 | @unknown default: 87 | return .failure(.unknownStatus(self.status)) 88 | } 89 | } 90 | .prefix(1) 91 | .eraseError() 92 | ._unsafe_eraseToAnySingleOutputPublisher() 93 | } 94 | } 95 | 96 | #endif 97 | -------------------------------------------------------------------------------- /Sources/Intermodular/Extensions/NetworkExtension/NEVPNManager++.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | #if os(iOS) || os(macOS) || targetEnvironment(macCatalyst) 6 | 7 | import Merge 8 | import NetworkExtension 9 | import Swift 10 | 11 | extension NEVPNManager { 12 | // Start the process of disconnecting the VPN. 13 | public func disconnectIfNecessary( 14 | timeout timeoutInterval: RunLoop.SchedulerTimeType.Stride? = nil 15 | ) -> AnySingleOutputPublisher { 16 | enum DisconnectTimeoutError: Error { 17 | case unknown 18 | } 19 | 20 | if let timeoutInterval = timeoutInterval { 21 | return connection.stop().timeout( 22 | timeoutInterval, 23 | scheduler: RunLoop.main, 24 | options: nil, 25 | customError: { 26 | DisconnectTimeoutError.unknown 27 | } 28 | ) 29 | ._unsafe_eraseToAnySingleOutputPublisher() 30 | } 31 | 32 | return connection.stop() 33 | } 34 | 35 | /// Load the VPN configuration from the Network Extension preferences. 36 | public final func loadFromPreferences() -> Future { 37 | Future { attemptToFulfill in 38 | self.loadFromPreferences { error in 39 | if let error = error { 40 | attemptToFulfill(.failure(error)) 41 | } else { 42 | attemptToFulfill(.success(())) 43 | } 44 | } 45 | } 46 | } 47 | 48 | /// Remove the VPN configuration from the Network Extension preferences. 49 | public final func removeFromPreferences() -> Future { 50 | Future { attemptToFulfill in 51 | self.removeFromPreferences { error in 52 | if let error = error { 53 | attemptToFulfill(.failure(error)) 54 | } else { 55 | attemptToFulfill(.success(())) 56 | } 57 | } 58 | } 59 | } 60 | 61 | public final func saveToPreferences() -> Future { 62 | Future { attemptToFulfill in 63 | self.saveToPreferences { error in 64 | if let error = error { 65 | attemptToFulfill(.failure(error)) 66 | } else { 67 | attemptToFulfill(.success(())) 68 | } 69 | } 70 | } 71 | } 72 | } 73 | 74 | #endif 75 | -------------------------------------------------------------------------------- /Sources/Intermodular/Extensions/NetworkExtension/NEVPNStatus++.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | #if os(iOS) || os(macOS) || targetEnvironment(macCatalyst) 6 | 7 | import NetworkExtension 8 | import Swift 9 | 10 | extension NEVPNStatus { 11 | /// Indicates whether the status is of a transitionary state (for e.g. connecting, or disconnecting). 12 | public var isTransient: Bool { 13 | switch self { 14 | case .invalid: 15 | return false 16 | case .disconnected: 17 | return false 18 | case .connecting: 19 | return true 20 | case .connected: 21 | return false 22 | case .reasserting: 23 | return true 24 | case .disconnecting: 25 | return true 26 | @unknown default: 27 | return false 28 | } 29 | } 30 | } 31 | 32 | #endif 33 | -------------------------------------------------------------------------------- /Sources/Intermodular/Extensions/NetworkExtension/VPNManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | #if canImport(NetworkExtension) 6 | 7 | import NetworkExtension 8 | 9 | @available(macOS 10.11, tvOS 17.0, *) 10 | @available(watchOS, unavailable) 11 | open class VPNManager: ObservableObject { 12 | private let base: NEVPNManager 13 | 14 | public init(base: NEVPNManager = .shared()) { 15 | self.base = base 16 | 17 | NotificationCenter.default.addObserver( 18 | self, 19 | selector: #selector(Self.receiveNotification(_:)), 20 | name: NSNotification.Name.NEVPNStatusDidChange, 21 | object: nil 22 | ) 23 | 24 | NotificationCenter.default.addObserver( 25 | self, 26 | selector: #selector(Self.receiveNotification(_:)), 27 | name: NSNotification.Name.NEVPNConfigurationChange, 28 | object: nil 29 | ) 30 | } 31 | 32 | @objc private func receiveNotification(_: NSNotification?) { 33 | objectWillChange.send() 34 | } 35 | } 36 | 37 | @available(macOS 10.11, tvOS 17.0, *) 38 | @available(watchOS, unavailable) 39 | extension VPNManager { 40 | public var protocolConfiguration: NEVPNProtocol? { 41 | base.protocolConfiguration 42 | } 43 | 44 | public var status: NEVPNStatus { 45 | base.connection.status 46 | } 47 | 48 | public func startVPNTunnel() throws { 49 | try base.connection.startVPNTunnel() 50 | } 51 | 52 | public func stopVPNTunnel() { 53 | base.connection.stopVPNTunnel() 54 | } 55 | } 56 | 57 | #endif 58 | -------------------------------------------------------------------------------- /Sources/Intermodular/Protocol Implementations/NetworkExtension/NetworkExtension+CustomStringConvertible.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | #if os(iOS) || os(macOS) || targetEnvironment(macCatalyst) 6 | 7 | import NetworkExtension 8 | import Swift 9 | 10 | extension NEVPNStatus: Swift.CustomStringConvertible { 11 | public var description: String { 12 | switch self { 13 | case .invalid: 14 | return "Invalid" 15 | case .disconnected: 16 | return "Disconnected" 17 | case .connecting: 18 | return "Connecting" 19 | case .connected: 20 | return "Connected" 21 | case .reasserting: 22 | return "Reasserting" 23 | case .disconnecting: 24 | return "Disconnecting" 25 | @unknown default: 26 | return "Unknown" 27 | } 28 | } 29 | } 30 | 31 | #endif 32 | -------------------------------------------------------------------------------- /Sources/Intermodular/Protocol Implementations/NetworkExtension/NetworkExtesion+Codable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | #if os(iOS) || os(macOS) || targetEnvironment(macCatalyst) 6 | 7 | import NetworkExtension 8 | import Swift 9 | 10 | extension NEVPNStatus: Codable { 11 | 12 | } 13 | 14 | #endif 15 | -------------------------------------------------------------------------------- /Sources/Intramodular/Encoding & Decoding/ASN1/ASN1Decoder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Foundation 6 | import Swift 7 | 8 | public class ASN1DERDecoder { 9 | public static func decode(data: Data) throws -> [ASN1Object] { 10 | var iterator = data.makeIterator() 11 | 12 | return try parse(iterator: &iterator) 13 | } 14 | 15 | private static func parse(iterator: inout Data.Iterator) throws -> [ASN1Object] { 16 | var result: [ASN1Object] = [] 17 | 18 | while let nextValue = iterator.next() { 19 | 20 | let asn1obj = ASN1Object() 21 | asn1obj.identifier = ASN1Identifier(rawValue: nextValue) 22 | 23 | if asn1obj.identifier!.isConstructed() { 24 | 25 | let contentData = try loadSubContent(iterator: &iterator) 26 | 27 | if contentData.isEmpty { 28 | asn1obj.sub = try parse(iterator: &iterator) 29 | } else { 30 | var subIterator = contentData.makeIterator() 31 | asn1obj.sub = try parse(iterator: &subIterator) 32 | } 33 | 34 | asn1obj.value = nil 35 | 36 | asn1obj.rawValue = Data(contentData) 37 | 38 | for item in asn1obj.sub! { 39 | item.parent = asn1obj 40 | } 41 | } else { 42 | 43 | if asn1obj.identifier!.typeClass() == .universal { 44 | 45 | var contentData = try loadSubContent(iterator: &iterator) 46 | 47 | asn1obj.rawValue = Data(contentData) 48 | 49 | // decode the content data with come more convenient format 50 | 51 | switch asn1obj.identifier!.tagNumber() { 52 | 53 | case .endOfContent: 54 | return result 55 | 56 | case .boolean: 57 | if let value = contentData.first { 58 | asn1obj.value = value > 0 ? true : false 59 | 60 | } 61 | 62 | case .integer: 63 | while contentData.first == 0 { 64 | contentData.remove(at: 0) // remove not significant digit 65 | } 66 | asn1obj.value = contentData 67 | 68 | case .null: 69 | asn1obj.value = nil 70 | 71 | case .objectIdentifier: 72 | asn1obj.value = decodeOid(contentData: &contentData) 73 | 74 | case .utf8String, 75 | .printableString, 76 | .numericString, 77 | .generalString, 78 | .universalString, 79 | .characterString, 80 | .t61String: 81 | 82 | asn1obj.value = String(data: contentData, encoding: .utf8) 83 | 84 | case .bmpString: 85 | asn1obj.value = String(data: contentData, encoding: .unicode) 86 | 87 | case .visibleString, 88 | .ia5String: 89 | 90 | asn1obj.value = String(data: contentData, encoding: .ascii) 91 | 92 | case .utcTime: 93 | asn1obj.value = dateFormatter(contentData: &contentData, 94 | formats: ["yyMMddHHmmssZ", "yyMMddHHmmZ"]) 95 | 96 | case .generalizedTime: 97 | asn1obj.value = dateFormatter(contentData: &contentData, 98 | formats: ["yyyyMMddHHmmssZ"]) 99 | 100 | case .bitString: 101 | if contentData.count > 0 { 102 | _ = contentData.remove(at: 0) // unused bits 103 | } 104 | asn1obj.value = contentData 105 | 106 | case .octetString: 107 | do { 108 | var subIterator = contentData.makeIterator() 109 | asn1obj.sub = try parse(iterator: &subIterator) 110 | } catch { 111 | if let str = String(data: contentData, encoding: .utf8) { 112 | asn1obj.value = str 113 | } else { 114 | asn1obj.value = contentData 115 | } 116 | } 117 | 118 | default: 119 | print("unsupported tag: \(asn1obj.identifier!.tagNumber())") 120 | asn1obj.value = contentData 121 | } 122 | } else { 123 | // custom/private tag 124 | 125 | let contentData = try loadSubContent(iterator: &iterator) 126 | 127 | if let str = String(data: contentData, encoding: .utf8) { 128 | asn1obj.value = str 129 | } else { 130 | asn1obj.value = contentData 131 | } 132 | } 133 | } 134 | result.append(asn1obj) 135 | } 136 | return result 137 | } 138 | 139 | // Decode the number of bytes of the content 140 | private static func getContentLength(iterator: inout Data.Iterator) -> UInt64 { 141 | let first = iterator.next() 142 | 143 | guard first != nil else { 144 | return 0 145 | } 146 | 147 | if (first! & 0x80) != 0 { // long 148 | let octetsToRead = first! - 0x80 149 | var data = Data() 150 | for _ in 0.. Data { 164 | 165 | let len = getContentLength(iterator: &iterator) 166 | 167 | guard len < Int.max else { 168 | return Data() 169 | } 170 | 171 | var byteArray: [UInt8] = [] 172 | 173 | for _ in 0.. String { 185 | if contentData.isEmpty { 186 | return "" 187 | } 188 | 189 | var oid: String = "" 190 | 191 | let first = Int(contentData.remove(at: 0)) 192 | oid.append("\(first / 40).\(first % 40)") 193 | 194 | var t = 0 195 | while contentData.count > 0 { 196 | let n = Int(contentData.remove(at: 0)) 197 | t = (t << 7) | (n & 0x7F) 198 | if (n & 0x80) == 0 { 199 | oid.append(".\(t)") 200 | t = 0 201 | } 202 | } 203 | return oid 204 | } 205 | 206 | private static func dateFormatter(contentData: inout Data, formats: [String]) -> Date? { 207 | guard let str = String(data: contentData, encoding: .utf8) else { return nil } 208 | for format in formats { 209 | let fmt = DateFormatter() 210 | fmt.locale = Locale(identifier: "en_US_POSIX") 211 | fmt.dateFormat = format 212 | if let dt = fmt.date(from: str) { 213 | return dt 214 | } 215 | } 216 | return nil 217 | } 218 | } 219 | 220 | enum ASN1Error: Error { 221 | case parseError 222 | case outOfBuffer 223 | } 224 | 225 | extension Data { 226 | func toIntValue() -> UInt64? { 227 | if self.count > 8 { // check if suitable for UInt64 228 | return nil 229 | } 230 | 231 | var value: UInt64 = 0 232 | for (index, byte) in self.enumerated() { 233 | value += UInt64(byte) << UInt64(8*(count-index-1)) 234 | } 235 | return value 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /Sources/Intramodular/Encoding & Decoding/ASN1/ASN1DistinguishedNames.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Foundation 6 | import Swift 7 | 8 | public struct ASN1DistinguishedNames { 9 | public let oid: String 10 | public let representation: String 11 | 12 | init(oid: String, representation: String) { 13 | self.oid = oid 14 | self.representation = representation 15 | } 16 | 17 | public static let commonName = Self(oid: "2.5.4.3", representation: "CN") 18 | public static let dnQualifier = Self(oid: "2.5.4.46", representation: "DNQ") 19 | public static let serialNumber = Self(oid: "2.5.4.5", representation: "SERIALNUMBER") 20 | public static let givenName = Self(oid: "2.5.4.42", representation: "GIVENNAME") 21 | public static let surname = Self(oid: "2.5.4.4", representation: "SURNAME") 22 | public static let organizationalUnitName = Self(oid: "2.5.4.11", representation: "OU") 23 | public static let organizationName = Self(oid: "2.5.4.10", representation: "O") 24 | public static let streetAddress = Self(oid: "2.5.4.9", representation: "STREET") 25 | public static let localityName = Self(oid: "2.5.4.7", representation: "L") 26 | public static let stateOrProvinceName = Self(oid: "2.5.4.8", representation: "ST") 27 | public static let countryName = Self(oid: "2.5.4.6", representation: "C") 28 | public static let email = Self(oid: "1.2.840.113549.1.9.1", representation: "E") 29 | } 30 | -------------------------------------------------------------------------------- /Sources/Intramodular/Encoding & Decoding/ASN1/ASN1Identifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Foundation 6 | import Swift 7 | 8 | public class ASN1Identifier: CustomStringConvertible { 9 | public enum Class: UInt8 { 10 | case universal = 0x00 11 | case application = 0x40 12 | case contextSpecific = 0x80 13 | case `private` = 0xC0 14 | } 15 | 16 | public enum TagNumber: UInt8 { 17 | case endOfContent = 0x00 18 | case boolean = 0x01 19 | case integer = 0x02 20 | case bitString = 0x03 21 | case octetString = 0x04 22 | case null = 0x05 23 | case objectIdentifier = 0x06 24 | case objectDescriptor = 0x07 25 | case external = 0x08 26 | case read = 0x09 27 | case enumerated = 0x0A 28 | case embeddedPdv = 0x0B 29 | case utf8String = 0x0C 30 | case relativeOid = 0x0D 31 | case sequence = 0x10 32 | case set = 0x11 33 | case numericString = 0x12 34 | case printableString = 0x13 35 | case t61String = 0x14 36 | case videotexString = 0x15 37 | case ia5String = 0x16 38 | case utcTime = 0x17 39 | case generalizedTime = 0x18 40 | case graphicString = 0x19 41 | case visibleString = 0x1A 42 | case generalString = 0x1B 43 | case universalString = 0x1C 44 | case characterString = 0x1D 45 | case bmpString = 0x1E 46 | } 47 | 48 | var rawValue: UInt8 49 | 50 | init(rawValue: UInt8) { 51 | self.rawValue = rawValue 52 | } 53 | 54 | public func typeClass() -> Class { 55 | for tc in [Class.application, Class.contextSpecific, Class.private] where (rawValue & tc.rawValue) == tc.rawValue { 56 | return tc 57 | } 58 | return .universal 59 | } 60 | 61 | public func isPrimitive() -> Bool { 62 | return (rawValue & 0x20) == 0 63 | } 64 | public func isConstructed() -> Bool { 65 | return (rawValue & 0x20) != 0 66 | } 67 | 68 | public func tagNumber() -> TagNumber { 69 | return TagNumber(rawValue: rawValue & 0x1F) ?? .endOfContent 70 | } 71 | 72 | public var description: String { 73 | if typeClass() == .universal { 74 | return String(describing: tagNumber()) 75 | } else { 76 | return "\(typeClass())(\(tagNumber().rawValue))" 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Sources/Intramodular/Encoding & Decoding/ASN1/ASN1Object.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Foundation 6 | import Swift 7 | 8 | public class ASN1Object: CustomStringConvertible { 9 | 10 | /// This property contains the DER encoded object 11 | public var rawValue: Data? 12 | 13 | /// This property contains the decoded Swift object whenever is possible 14 | public var value: Any? 15 | 16 | public var identifier: ASN1Identifier? 17 | 18 | var sub: [ASN1Object]? 19 | 20 | weak var parent: ASN1Object? 21 | 22 | public func sub(_ index: Int) -> ASN1Object? { 23 | if let sub = self.sub, index >= 0, index < sub.count { 24 | return sub[index] 25 | } 26 | return nil 27 | } 28 | 29 | public func subCount() -> Int { 30 | return sub?.count ?? 0 31 | } 32 | 33 | public func findOid(_ oid: OID) -> ASN1Object? { 34 | return findOid(oid.rawValue) 35 | } 36 | 37 | public func findOid(_ oid: String) -> ASN1Object? { 38 | for child in sub ?? [] { 39 | if child.identifier?.tagNumber() == .objectIdentifier { 40 | if child.value as? String == oid { 41 | return child 42 | } 43 | } else { 44 | if let result = child.findOid(oid) { 45 | return result 46 | } 47 | } 48 | } 49 | return nil 50 | } 51 | 52 | public var description: String { 53 | return printAsn1() 54 | } 55 | 56 | fileprivate func printAsn1(insets: String = "") -> String { 57 | var output = insets 58 | output.append(identifier?.description.uppercased() ?? "") 59 | output.append(value != nil ? ": \(value!)": "") 60 | if identifier?.typeClass() == .universal, identifier?.tagNumber() == .objectIdentifier { 61 | if let descr = ASN1Object.oidDecodeMap[value as? String ?? ""] { 62 | output.append(" (\(descr))") 63 | } 64 | } 65 | output.append(sub != nil && sub!.count > 0 ? " {": "") 66 | output.append("\n") 67 | for item in sub ?? [] { 68 | output.append(item.printAsn1(insets: insets + " ")) 69 | } 70 | output.append(sub != nil && sub!.count > 0 ? insets + "}\n": "") 71 | return output 72 | } 73 | 74 | static let oidDecodeMap: [String: String] = [ 75 | "0.4.0.1862.1.1": "etsiQcsCompliance", 76 | "0.4.0.1862.1.3": "etsiQcsRetentionPeriod", 77 | "0.4.0.1862.1.4": "etsiQcsQcSSCD", 78 | "1.2.840.10040.4.1": "dsa", 79 | "1.2.840.10045.2.1": "ecPublicKey", 80 | "1.2.840.10045.3.1.7": "prime256v1", 81 | "1.2.840.10045.4.3.2": "ecdsaWithSHA256", 82 | "1.2.840.10045.4.3.4": "ecdsaWithSHA512", 83 | "1.2.840.113549.1.1.1": "rsaEncryption", 84 | "1.2.840.113549.1.1.4": "md5WithRSAEncryption", 85 | "1.2.840.113549.1.1.5": "sha1WithRSAEncryption", 86 | "1.2.840.113549.1.1.11": "sha256WithRSAEncryption", 87 | "1.2.840.113549.1.7.1": "data", 88 | "1.2.840.113549.1.7.2": "signedData", 89 | "1.2.840.113549.1.9.1": "emailAddress", 90 | "1.2.840.113549.1.9.16.2.47": "signingCertificateV2", 91 | "1.2.840.113549.1.9.3": "contentType", 92 | "1.2.840.113549.1.9.4": "messageDigest", 93 | "1.2.840.113549.1.9.5": "signingTime", 94 | "1.3.6.1.4.1.11129.2.4.2": "certificateExtension", 95 | "1.3.6.1.4.1.311.60.2.1.2": "jurisdictionOfIncorporationSP", 96 | "1.3.6.1.4.1.311.60.2.1.3": "jurisdictionOfIncorporationC", 97 | "1.3.6.1.5.5.7.1.1": "authorityInfoAccess", 98 | "1.3.6.1.5.5.7.1.3": "qcStatements", 99 | "1.3.6.1.5.5.7.2.1": "cps", 100 | "1.3.6.1.5.5.7.2.2": "unotice", 101 | "1.3.6.1.5.5.7.3.1": "serverAuth", 102 | "1.3.6.1.5.5.7.3.2": "clientAuth", 103 | "1.3.6.1.5.5.7.48.1": "ocsp", 104 | "1.3.6.1.5.5.7.48.2": "caIssuers", 105 | "1.3.6.1.5.5.7.9.1": "dateOfBirth", 106 | "2.16.840.1.101.3.4.2.1": "sha-256", 107 | "2.16.840.1.113733.1.7.23.6": "VeriSign EV policy", 108 | "2.23.140.1.1": "extendedValidation", 109 | "2.23.140.1.2.2": "extendedValidation", 110 | "2.5.29.14": "subjectKeyIdentifier", 111 | "2.5.29.15": "keyUsage", 112 | "2.5.29.17": "subjectAltName", 113 | "2.5.29.18": "issuerAltName", 114 | "2.5.29.19": "basicConstraints", 115 | "2.5.29.31": "cRLDistributionPoints", 116 | "2.5.29.32": "certificatePolicies", 117 | "2.5.29.35": "authorityKeyIdentifier", 118 | "2.5.29.37": "extKeyUsage", 119 | "2.5.29.9": "subjectDirectoryAttributes", 120 | "2.5.4.10": "organizationName", 121 | "2.5.4.11": "organizationalUnitName", 122 | "2.5.4.15": "businessCategory", 123 | "2.5.4.17": "postalCode", 124 | "2.5.4.3": "commonName", 125 | "2.5.4.4": "surname", 126 | "2.5.4.42": "givenName", 127 | "2.5.4.46": "dnQualifier", 128 | "2.5.4.5": "serialNumber", 129 | "2.5.4.6": "countryName", 130 | "2.5.4.7": "localityName", 131 | "2.5.4.8": "stateOrProvinceName", 132 | "2.5.4.9": "streetAddress" 133 | ] 134 | } 135 | -------------------------------------------------------------------------------- /Sources/Intramodular/Encoding & Decoding/OID.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Foundation 6 | import Swift 7 | 8 | public enum OID: String { 9 | case etsiQcsCompliance = "0.4.0.1862.1.1" 10 | case etsiQcsRetentionPeriod = "0.4.0.1862.1.3" 11 | case etsiQcsQcSSCD = "0.4.0.1862.1.4" 12 | case dsa = "1.2.840.10040.4.1" 13 | case ecPublicKey = "1.2.840.10045.2.1" 14 | case prime256v1 = "1.2.840.10045.3.1.7" 15 | case ecdsaWithSHA256 = "1.2.840.10045.4.3.2" 16 | case ecdsaWithSHA512 = "1.2.840.10045.4.3.4" 17 | case rsaEncryption = "1.2.840.113549.1.1.1" 18 | case sha256WithRSAEncryption = "1.2.840.113549.1.1.11" 19 | case md5WithRSAEncryption = "1.2.840.113549.1.1.4" 20 | case sha1WithRSAEncryption = "1.2.840.113549.1.1.5" 21 | 22 | case sha1 = "1.3.14.3.2.26" 23 | case pkcsSha256 = "1.3.6.1.4.1.22554.1.2.1" 24 | case sha2Family = "1.3.6.1.4.1.22554.1.2" 25 | case sha3_244 = "2.16.840.1.101.3.4.2.7" 26 | case sha3_256 = "2.16.840.1.101.3.4.2.8" 27 | case sha3_384 = "2.16.840.1.101.3.4.2.9" 28 | case md5 = "0.2.262.1.10.1.3.2" 29 | 30 | case pkcs7data = "1.2.840.113549.1.7.1" 31 | case pkcs7signedData = "1.2.840.113549.1.7.2" 32 | case pkcs7envelopedData = "1.2.840.113549.1.7.3" 33 | case emailAddress = "1.2.840.113549.1.9.1" 34 | case signingCertificateV2 = "1.2.840.113549.1.9.16.2.47" 35 | case contentType = "1.2.840.113549.1.9.3" 36 | case messageDigest = "1.2.840.113549.1.9.4" 37 | case signingTime = "1.2.840.113549.1.9.5" 38 | case certificateExtension = "1.3.6.1.4.1.11129.2.4.2" 39 | case jurisdictionOfIncorporationSP = "1.3.6.1.4.1.311.60.2.1.2" 40 | case jurisdictionOfIncorporationC = "1.3.6.1.4.1.311.60.2.1.3" 41 | case authorityInfoAccess = "1.3.6.1.5.5.7.1.1" 42 | case qcStatements = "1.3.6.1.5.5.7.1.3" 43 | case cps = "1.3.6.1.5.5.7.2.1" 44 | case unotice = "1.3.6.1.5.5.7.2.2" 45 | case serverAuth = "1.3.6.1.5.5.7.3.1" 46 | case clientAuth = "1.3.6.1.5.5.7.3.2" 47 | case ocsp = "1.3.6.1.5.5.7.48.1" 48 | case caIssuers = "1.3.6.1.5.5.7.48.2" 49 | case dateOfBirth = "1.3.6.1.5.5.7.9.1" 50 | case sha256 = "2.16.840.1.101.3.4.2.1" 51 | case VeriSignEVpolicy = "2.16.840.1.113733.1.7.23.6" 52 | case extendedValidation = "2.23.140.1.1" 53 | case organizationValidated = "2.23.140.1.2.2" 54 | case subjectKeyIdentifier = "2.5.29.14" 55 | case keyUsage = "2.5.29.15" 56 | case subjectAltName = "2.5.29.17" 57 | case issuerAltName = "2.5.29.18" 58 | case basicConstraints = "2.5.29.19" 59 | case cRLDistributionPoints = "2.5.29.31" 60 | case certificatePolicies = "2.5.29.32" 61 | case authorityKeyIdentifier = "2.5.29.35" 62 | case extKeyUsage = "2.5.29.37" 63 | case subjectDirectoryAttributes = "2.5.29.9" 64 | case organizationName = "2.5.4.10" 65 | case organizationalUnitName = "2.5.4.11" 66 | case businessCategory = "2.5.4.15" 67 | case postalCode = "2.5.4.17" 68 | case commonName = "2.5.4.3" 69 | case surname = "2.5.4.4" 70 | case givenName = "2.5.4.42" 71 | case dnQualifier = "2.5.4.46" 72 | case serialNumber = "2.5.4.5" 73 | case countryName = "2.5.4.6" 74 | case localityName = "2.5.4.7" 75 | case stateOrProvinceName = "2.5.4.8" 76 | case streetAddress = "2.5.4.9" 77 | } 78 | -------------------------------------------------------------------------------- /Sources/Intramodular/Encoding & Decoding/PKCS7/PKCS7.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Foundation 6 | import Swift 7 | 8 | public class PKCS7 { 9 | let derData: Data 10 | let asn1: [ASN1Object] 11 | let mainBlock: ASN1Object 12 | 13 | public init(data: Data) throws { 14 | derData = data 15 | asn1 = try ASN1DERDecoder.decode(data: derData) 16 | 17 | guard let firstBlock = asn1.first, 18 | let mainBlock = firstBlock.sub(1)?.sub(0) else { 19 | throw PKCS7Error.parseError 20 | } 21 | 22 | self.mainBlock = mainBlock 23 | 24 | guard firstBlock.sub(0)?.value as? String == OID.pkcs7signedData.rawValue else { 25 | throw PKCS7Error.notSupported 26 | } 27 | } 28 | 29 | public var digestAlgorithm: String? { 30 | if let block = mainBlock.sub(1) { 31 | return firstLeafValue(block: block) as? String 32 | } 33 | return nil 34 | } 35 | 36 | public var digestAlgorithmName: String? { 37 | return ASN1Object.oidDecodeMap[digestAlgorithm ?? ""] ?? digestAlgorithm 38 | } 39 | 40 | public var certificate: X509Certificate? { 41 | return mainBlock.sub(3)?.sub?.first.map { try? X509Certificate(asn1: $0) } ?? nil 42 | } 43 | 44 | public var certificates: [X509Certificate] { 45 | return mainBlock.sub(3)?.sub?.compactMap { try? X509Certificate(asn1: $0) } ?? [] 46 | } 47 | 48 | public var data: Data? { 49 | if let block = mainBlock.findOid(.pkcs7data) { 50 | if let dataBlock = block.parent?.sub?.last { 51 | var out = Data() 52 | if let value = dataBlock.value as? Data { 53 | out.append(value) 54 | } else if dataBlock.value is String, let rawValue = dataBlock.rawValue { 55 | out.append(rawValue) 56 | } else { 57 | for sub in dataBlock.sub ?? [] { 58 | if let value = sub.value as? Data { 59 | out.append(value) 60 | } else if sub.value is String, let rawValue = sub.rawValue { 61 | out.append(rawValue) 62 | } else { 63 | for sub2 in sub.sub ?? [] { 64 | if let value = sub2.rawValue { 65 | out.append(value) 66 | } 67 | } 68 | } 69 | } 70 | } 71 | return out.count > 0 ? out : nil 72 | } 73 | } 74 | return nil 75 | } 76 | } 77 | 78 | enum PKCS7Error: Error { 79 | case notSupported 80 | case parseError 81 | } 82 | -------------------------------------------------------------------------------- /Sources/Intramodular/Encoding & Decoding/PKCS7/PKCS7_AppleReceipt.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Foundation 6 | import Swift 7 | 8 | /* 9 | This extension allow to parse the content of an Apple receipt from the AppStore. 10 | 11 | Reference documentation 12 | https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html 13 | */ 14 | extension PKCS7 { 15 | 16 | public struct ReceiptInfo { 17 | 18 | /// CFBundleIdentifier in Info.plist 19 | public fileprivate(set) var bundleIdentifier: String? 20 | 21 | /// CFBundleVersion (in iOS) or CFBundleShortVersionString (in macOS) in Info.plist 22 | public fileprivate(set) var bundleVersion: String? 23 | 24 | /// CFBundleVersion (in iOS) or CFBundleShortVersionString (in macOS) in Info.plist 25 | public fileprivate(set) var originalApplicationVersion: String? 26 | 27 | /// Opaque value used, with other data, to compute the SHA-1 hash during validation. 28 | public fileprivate(set) var opaqueValue: Data? 29 | 30 | /// SHA-1 hash, used to validate the receipt. 31 | public fileprivate(set) var sha1: Data? 32 | 33 | public fileprivate(set) var receiptCreationDate: Date? 34 | public fileprivate(set) var receiptCreationDateString: String? 35 | public fileprivate(set) var receiptExpirationDate: Date? 36 | public fileprivate(set) var receiptExpirationDateString: String? 37 | public fileprivate(set) var inAppPurchases: [InAppPurchaseInfo]? 38 | } 39 | 40 | public struct InAppPurchaseInfo { 41 | public fileprivate(set) var quantity: UInt64? 42 | public fileprivate(set) var productId: String? 43 | public fileprivate(set) var transactionId: String? 44 | public fileprivate(set) var originalTransactionId: String? 45 | public fileprivate(set) var purchaseDate: Date? 46 | public fileprivate(set) var originalPurchaseDate: Date? 47 | public fileprivate(set) var expiresDate: Date? 48 | public fileprivate(set) var isInIntroOfferPeriod: UInt64? 49 | public fileprivate(set) var cancellationDate: Date? 50 | public fileprivate(set) var webOrderLineItemId: UInt64? 51 | } 52 | 53 | func parseDate(_ dateString: String) -> Date? { 54 | let rfc3339DateFormatter = DateFormatter() 55 | rfc3339DateFormatter.locale = Locale(identifier: "en_US_POSIX") 56 | rfc3339DateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" 57 | rfc3339DateFormatter.timeZone = TimeZone(secondsFromGMT: 0) 58 | return rfc3339DateFormatter.date(from: dateString) 59 | } 60 | 61 | public func receipt() -> ReceiptInfo? { 62 | guard let block = mainBlock.findOid(.pkcs7data) else { return nil } 63 | guard let receiptBlock = block.parent?.sub?.last?.sub(0)?.sub(0) else { return nil } 64 | var receiptInfo = ReceiptInfo() 65 | 66 | for item in receiptBlock.sub ?? [] { 67 | let fieldType = (item.sub(0)?.value as? Data)?.toIntValue() ?? 0 68 | let fieldValueString = item.sub(2)?.sub?.first?.value as? String 69 | switch fieldType { 70 | case 2: 71 | receiptInfo.bundleIdentifier = fieldValueString 72 | 73 | case 3: 74 | receiptInfo.bundleVersion = fieldValueString 75 | 76 | case 4: 77 | receiptInfo.opaqueValue = item.sub(2)?.rawValue 78 | 79 | case 5: 80 | receiptInfo.sha1 = item.sub(2)?.rawValue 81 | 82 | case 19: 83 | receiptInfo.originalApplicationVersion = fieldValueString 84 | 85 | case 12: 86 | guard let fieldValueString = fieldValueString else { continue } 87 | receiptInfo.receiptCreationDateString = fieldValueString 88 | receiptInfo.receiptCreationDate = parseDate(fieldValueString) 89 | 90 | case 21: 91 | guard let fieldValueString = fieldValueString else { continue } 92 | receiptInfo.receiptExpirationDateString = fieldValueString 93 | receiptInfo.receiptExpirationDate = parseDate(fieldValueString) 94 | 95 | case 17: 96 | let subItems = item.sub(2)?.sub?.first?.sub ?? [] 97 | if receiptInfo.inAppPurchases == nil { 98 | receiptInfo.inAppPurchases = [] 99 | } 100 | receiptInfo.inAppPurchases?.append(inAppPurchase(subItems)) 101 | 102 | default: 103 | break 104 | } 105 | } 106 | return receiptInfo 107 | } 108 | 109 | private func inAppPurchase(_ subItems: [ASN1Object]) -> InAppPurchaseInfo { 110 | var inAppPurchaseInfo = InAppPurchaseInfo() 111 | subItems.forEach { subItem in 112 | let fieldType = (subItem.sub(0)?.value as? Data)?.toIntValue() ?? 0 113 | let fieldValue = subItem.sub(2)?.sub?.first?.value 114 | switch fieldType { 115 | case 1701: 116 | inAppPurchaseInfo.quantity = (fieldValue as? Data)?.toIntValue() 117 | case 1702: 118 | inAppPurchaseInfo.productId = fieldValue as? String 119 | case 1703: 120 | inAppPurchaseInfo.transactionId = fieldValue as? String 121 | case 1705: 122 | inAppPurchaseInfo.originalTransactionId = fieldValue as? String 123 | case 1704: 124 | if let fieldValueString = fieldValue as? String { 125 | inAppPurchaseInfo.purchaseDate = parseDate(fieldValueString) 126 | } 127 | case 1706: 128 | if let fieldValueString = fieldValue as? String { 129 | inAppPurchaseInfo.originalPurchaseDate = parseDate(fieldValueString) 130 | } 131 | case 1708: 132 | if let fieldValueString = fieldValue as? String { 133 | inAppPurchaseInfo.expiresDate = parseDate(fieldValueString) 134 | } 135 | case 1719: 136 | inAppPurchaseInfo.isInIntroOfferPeriod = (fieldValue as? Data)?.toIntValue() 137 | case 1712: 138 | if let fieldValueString = fieldValue as? String { 139 | inAppPurchaseInfo.cancellationDate = parseDate(fieldValueString) 140 | } 141 | case 1711: 142 | inAppPurchaseInfo.webOrderLineItemId = (fieldValue as? Data)?.toIntValue() 143 | default: 144 | break 145 | } 146 | } 147 | return inAppPurchaseInfo 148 | } 149 | 150 | } 151 | -------------------------------------------------------------------------------- /Sources/Intramodular/Encoding & Decoding/PKCS7/PKCS7_Signature.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Foundation 6 | import Swift 7 | 8 | extension PKCS7 { 9 | public var signatures: [SignatureInfo]? { 10 | //Signer infos sequence. https://tools.ietf.org/html/rfc5652#section-5.3 11 | 12 | guard let signerInfos = mainBlock.sub(4) else {return nil} 13 | 14 | let numberOfSignatures = signerInfos.subCount() 15 | 16 | var signatures = [SignatureInfo]() 17 | 18 | for i in 0.. 0, 37 | let block1 = asn1.first?.sub(0) else { 38 | throw ASN1Error.parseError 39 | } 40 | 41 | self.block1 = block1 42 | } 43 | 44 | public convenience init(pem: Data) throws { 45 | guard let derData = X509Certificate.decodeToDER(pem: pem) else { 46 | throw ASN1Error.parseError 47 | } 48 | 49 | try self.init(der: derData) 50 | } 51 | 52 | init(asn1: ASN1Object) throws { 53 | guard let block1 = asn1.sub(0) else { throw ASN1Error.parseError } 54 | 55 | self.asn1 = [asn1] 56 | self.block1 = block1 57 | } 58 | 59 | public var description: String { 60 | return asn1.reduce("") { $0 + "\($1.description)\n" } 61 | } 62 | 63 | /// Checks that the given date is within the certificate's validity period. 64 | public func checkValidity(_ date: Date = Date()) -> Bool { 65 | if let notBefore = notBefore, let notAfter = notAfter { 66 | return date > notBefore && date < notAfter 67 | } 68 | return false 69 | } 70 | 71 | /// Gets the version (version number) value from the certificate. 72 | public var version: Int? { 73 | if let v = firstLeafValue(block: block1) as? Data, let index = v.toIntValue() { 74 | return Int(index) + 1 75 | } 76 | return nil 77 | } 78 | 79 | /// Gets the serialNumber value from the certificate. 80 | public var serialNumber: Data? { 81 | return block1[X509BlockPosition.serialNumber]?.value as? Data 82 | } 83 | 84 | /// Returns the issuer (issuer distinguished name) value from the certificate as a String. 85 | public var issuerDistinguishedName: String? { 86 | if let issuerBlock = block1[X509BlockPosition.issuer] { 87 | return blockDistinguishedName(block: issuerBlock) 88 | } 89 | return nil 90 | } 91 | 92 | public var issuerOIDs: [String] { 93 | var result: [String] = [] 94 | if let subjectBlock = block1[X509BlockPosition.issuer] { 95 | for sub in subjectBlock.sub ?? [] { 96 | if let value = firstLeafValue(block: sub) as? String { 97 | result.append(value) 98 | } 99 | } 100 | } 101 | return result 102 | } 103 | 104 | public func issuer(oid: String) -> String? { 105 | if let subjectBlock = block1[X509BlockPosition.issuer] { 106 | if let oidBlock = subjectBlock.findOid(oid) { 107 | return oidBlock.parent?.sub?.last?.value as? String 108 | } 109 | } 110 | return nil 111 | } 112 | 113 | public func issuer(dn: ASN1DistinguishedNames) -> String? { 114 | return issuer(oid: dn.oid) 115 | } 116 | 117 | /// Returns the subject (subject distinguished name) value from the certificate as a String. 118 | public var subjectDistinguishedName: String? { 119 | if let subjectBlock = block1[X509BlockPosition.subject] { 120 | return blockDistinguishedName(block: subjectBlock) 121 | } 122 | return nil 123 | } 124 | 125 | public var subjectOIDs: [String] { 126 | var result: [String] = [] 127 | if let subjectBlock = block1[X509BlockPosition.subject] { 128 | for sub in subjectBlock.sub ?? [] { 129 | if let value = firstLeafValue(block: sub) as? String { 130 | result.append(value) 131 | } 132 | } 133 | } 134 | return result 135 | } 136 | 137 | public func subject(oid: String) -> String? { 138 | if let subjectBlock = block1[X509BlockPosition.subject] { 139 | if let oidBlock = subjectBlock.findOid(oid) { 140 | return oidBlock.parent?.sub?.last?.value as? String 141 | } 142 | } 143 | return nil 144 | } 145 | 146 | public func subject(dn: ASN1DistinguishedNames) -> String? { 147 | return subject(oid: dn.oid) 148 | } 149 | 150 | /// Gets the notBefore date from the validity period of the certificate. 151 | public var notBefore: Date? { 152 | return block1[X509BlockPosition.dateValidity]?.sub(0)?.value as? Date 153 | } 154 | 155 | /// Gets the notAfter date from the validity period of the certificate. 156 | public var notAfter: Date? { 157 | return block1[X509BlockPosition.dateValidity]?.sub(1)?.value as? Date 158 | } 159 | 160 | /// Gets the signature value (the raw signature bits) from the certificate. 161 | public var signature: Data? { 162 | return asn1[0].sub(2)?.value as? Data 163 | } 164 | 165 | /// Gets the signature algorithm name for the certificate signature algorithm. 166 | public var sigAlgName: String? { 167 | return ASN1Object.oidDecodeMap[sigAlgOID ?? ""] 168 | } 169 | 170 | /// Gets the signature algorithm OID string from the certificate. 171 | public var sigAlgOID: String? { 172 | return block1.sub(2)?.sub(0)?.value as? String 173 | } 174 | 175 | /// Gets the DER-encoded signature algorithm parameters from this certificate's signature algorithm. 176 | public var sigAlgParams: Data? { 177 | return nil 178 | } 179 | 180 | /** 181 | Gets a boolean array representing bits of the KeyUsage extension, (OID = 2.5.29.15). 182 | ``` 183 | KeyUsage ::= BIT STRING { 184 | digitalSignature (0), 185 | nonRepudiation (1), 186 | keyEncipherment (2), 187 | dataEncipherment (3), 188 | keyAgreement (4), 189 | keyCertSign (5), 190 | cRLSign (6), 191 | encipherOnly (7), 192 | decipherOnly (8) 193 | } 194 | ``` 195 | */ 196 | public var keyUsage: [Bool] { 197 | var result: [Bool] = [] 198 | if let oidBlock = block1.findOid(OID.keyUsage) { 199 | let data = oidBlock.parent?.sub?.last?.sub(0)?.value as? Data 200 | let bits: UInt8 = data?.first ?? 0 201 | for index in 0...7 { 202 | let value = bits & UInt8(1 << index) != 0 203 | result.insert(value, at: 0) 204 | } 205 | } 206 | return result 207 | } 208 | 209 | /// Gets a list of Strings representing the OBJECT IDENTIFIERs of the ExtKeyUsageSyntax field of 210 | /// the extended key usage extension, (OID = 2.5.29.37). 211 | public var extendedKeyUsage: [String] { 212 | return extensionObject(oid: OID.extKeyUsage)?.valueAsStrings ?? [] 213 | } 214 | 215 | /// Gets a collection of subject alternative names from the SubjectAltName extension, (OID = 2.5.29.17). 216 | public var subjectAlternativeNames: [String] { 217 | return extensionObject(oid: OID.subjectAltName)?.valueAsStrings ?? [] 218 | } 219 | 220 | /// Gets a collection of issuer alternative names from the IssuerAltName extension, (OID = 2.5.29.18). 221 | public var issuerAlternativeNames: [String] { 222 | return extensionObject(oid: OID.issuerAltName)?.valueAsStrings ?? [] 223 | } 224 | 225 | /// Gets the informations of the public key from this certificate. 226 | public var publicKey: X509PublicKey? { 227 | return block1[X509BlockPosition.publicKey].map(X509PublicKey.init) 228 | } 229 | 230 | /// Get a list of critical extension OID codes 231 | public var criticalExtensionOIDs: [String] { 232 | guard let extensionBlocks = extensionBlocks else { return [] } 233 | return extensionBlocks 234 | .map { X509Extension(block: $0) } 235 | .filter { $0.isCritical } 236 | .compactMap { $0.oid } 237 | } 238 | 239 | /// Get a list of non critical extension OID codes 240 | public var nonCriticalExtensionOIDs: [String] { 241 | guard let extensionBlocks = extensionBlocks else { return [] } 242 | return extensionBlocks 243 | .map { X509Extension(block: $0) } 244 | .filter { !$0.isCritical } 245 | .compactMap { $0.oid } 246 | } 247 | 248 | private var extensionBlocks: [ASN1Object]? { 249 | return block1[X509BlockPosition.extensions]?.sub(0)?.sub 250 | } 251 | 252 | /// Gets the extension information of the given OID enum. 253 | public func extensionObject(oid: OID) -> X509Extension? { 254 | return extensionObject(oid: oid.rawValue) 255 | } 256 | 257 | /// Gets the extension information of the given OID code. 258 | public func extensionObject(oid: String) -> X509Extension? { 259 | return block1[X509BlockPosition.extensions]? 260 | .findOid(oid)? 261 | .parent 262 | .map(X509Extension.init) 263 | } 264 | 265 | // Format subject/issuer information in RFC1779 266 | private func blockDistinguishedName(block: ASN1Object) -> String { 267 | var result = "" 268 | let oidNames: [ASN1DistinguishedNames] = [ 269 | .commonName, 270 | .dnQualifier, 271 | .serialNumber, 272 | .givenName, 273 | .surname, 274 | .organizationalUnitName, 275 | .organizationName, 276 | .streetAddress, 277 | .localityName, 278 | .stateOrProvinceName, 279 | .countryName, 280 | .email 281 | ] 282 | for oidName in oidNames { 283 | if let oidBlock = block.findOid(oidName.oid) { 284 | if !result.isEmpty { 285 | result.append(", ") 286 | } 287 | result.append(oidName.representation) 288 | result.append("=") 289 | if let value = oidBlock.parent?.sub?.last?.value as? String { 290 | let specialChar = ",+=\n<>#;\\" 291 | let quote = value.contains(where: { specialChar.contains($0) }) ? "\"" : "" 292 | result.append(quote) 293 | result.append(value) 294 | result.append(quote) 295 | } 296 | } 297 | } 298 | return result 299 | } 300 | 301 | // read possibile PEM encoding 302 | private static func decodeToDER(pem pemData: Data) -> Data? { 303 | if 304 | let pem = String(data: pemData, encoding: .ascii), 305 | pem.contains(beginPemBlock) { 306 | 307 | let lines = pem.components(separatedBy: .newlines) 308 | var base64buffer = "" 309 | var certLine = false 310 | for line in lines { 311 | if line == endPemBlock { 312 | certLine = false 313 | } 314 | if certLine { 315 | base64buffer.append(line) 316 | } 317 | if line == beginPemBlock { 318 | certLine = true 319 | } 320 | } 321 | if let derDataDecoded = Data(base64Encoded: base64buffer) { 322 | return derDataDecoded 323 | } 324 | } 325 | 326 | return nil 327 | } 328 | } 329 | 330 | func firstLeafValue(block: ASN1Object) -> Any? { 331 | if let sub = block.sub?.first { 332 | return firstLeafValue(block: sub) 333 | } 334 | return block.value 335 | } 336 | 337 | extension ASN1Object { 338 | subscript(index: X509Certificate.X509BlockPosition) -> ASN1Object? { 339 | guard let sub = sub, 340 | sub.indices.contains(index.rawValue) else { return nil } 341 | return sub[index.rawValue] 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /Sources/Intramodular/Encoding & Decoding/X509/X509Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Foundation 6 | import Swift 7 | 8 | public class X509Extension { 9 | 10 | let block: ASN1Object 11 | 12 | init(block: ASN1Object) { 13 | self.block = block 14 | } 15 | 16 | public var oid: String? { 17 | return block.sub(0)?.value as? String 18 | } 19 | 20 | public var name: String? { 21 | return ASN1Object.oidDecodeMap[oid ?? ""] 22 | } 23 | 24 | public var isCritical: Bool { 25 | if block.sub?.count ?? 0 > 2 { 26 | return block.sub(1)?.value as? Bool ?? false 27 | } 28 | return false 29 | } 30 | 31 | public var value: Any? { 32 | if let valueBlock = block.sub?.last { 33 | return firstLeafValue(block: valueBlock) 34 | } 35 | return nil 36 | } 37 | 38 | var valueAsBlock: ASN1Object? { 39 | return block.sub?.last 40 | } 41 | 42 | var valueAsStrings: [String] { 43 | var result: [String] = [] 44 | for item in block.sub?.last?.sub?.last?.sub ?? [] { 45 | if let name = item.value as? String { 46 | result.append(name) 47 | } 48 | } 49 | return result 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/Intramodular/Encoding & Decoding/X509/X509PublicKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Foundation 6 | import Swift 7 | 8 | public class X509PublicKey { 9 | 10 | var pkBlock: ASN1Object! 11 | 12 | init(pkBlock: ASN1Object) { 13 | self.pkBlock = pkBlock 14 | } 15 | 16 | public var algOid: String? { 17 | return pkBlock.sub(0)?.sub(0)?.value as? String 18 | } 19 | 20 | public var algName: String? { 21 | return ASN1Object.oidDecodeMap[algOid ?? ""] 22 | } 23 | 24 | public var algParams: String? { 25 | return pkBlock.sub(0)?.sub(1)?.value as? String 26 | } 27 | 28 | public var key: Data? { 29 | guard 30 | let algOid = algOid, 31 | let oid = OID(rawValue: algOid), 32 | let keyData = pkBlock.sub(1)?.value as? Data else { 33 | return nil 34 | } 35 | 36 | switch oid { 37 | case .ecPublicKey: 38 | return keyData 39 | 40 | case .rsaEncryption: 41 | guard let publicKeyAsn1Objects = (try? ASN1DERDecoder.decode(data: keyData)) else { 42 | return nil 43 | } 44 | guard let publicKeyModulus = publicKeyAsn1Objects.first?.sub(0)?.value as? Data else { 45 | return nil 46 | } 47 | return publicKeyModulus 48 | 49 | default: 50 | return nil 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/Intramodular/HTTP/HTTPAuthorizationType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Swift 6 | 7 | public enum HTTPAuthorizationType: Hashable, Sendable { 8 | case basic 9 | case bearer 10 | case digest 11 | case hoba 12 | case mutual 13 | case aws 14 | case custom(String) 15 | 16 | public var rawValue: String { 17 | switch self { 18 | case .basic: 19 | return "Basic" 20 | case .bearer: 21 | return "Bearer" 22 | case .digest: 23 | return "Digest" 24 | case .hoba: 25 | return "HOBA" 26 | case .mutual: 27 | return "Mutual" 28 | case .aws: 29 | return "AWS4-HMAC-SHA256" 30 | case .custom(let value): 31 | return value 32 | } 33 | } 34 | 35 | public init(rawValue: String) { 36 | switch rawValue { 37 | case Self.basic.rawValue: 38 | self = .basic 39 | case Self.bearer.rawValue: 40 | self = .bearer 41 | case Self.digest.rawValue: 42 | self = .digest 43 | case Self.hoba.rawValue: 44 | self = .hoba 45 | case Self.mutual.rawValue: 46 | self = .mutual 47 | case Self.aws.rawValue: 48 | self = .aws 49 | default: 50 | self = .custom(rawValue) 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/Intramodular/HTTP/HTTPCache.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import FoundationX 6 | import Swallow 7 | 8 | public struct HTTPCache: KeyedCache, Initiable { 9 | public typealias Key = HTTPRequest 10 | public typealias Value = HTTPResponse 11 | 12 | public let base = URLCache() 13 | 14 | public init() { 15 | 16 | } 17 | 18 | public func cache(_ value: Value, forKey key: Key) async throws { 19 | try base.storeCachedResponse(.init(value), for: .init(key)) 20 | } 21 | 22 | public func retrieveInMemoryValue(forKey key: Key) throws -> Value? { 23 | 24 | // try base.cachedResponse(for: try URLRequest(key)).map(HTTPResponse.init) 25 | return nil // FIXME: The cache doesn't seem to ever expire. 26 | } 27 | 28 | public func removeCachedValue(forKey key: Key) async throws { 29 | try base.removeCachedResponse(for: .init(key)) 30 | } 31 | 32 | public func removeAllCachedValues() async throws { 33 | base.removeAllCachedResponses() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/Intramodular/HTTP/HTTPCacheControlType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Swift 6 | 7 | public enum HTTPCacheControlType: Hashable, RawRepresentable, Sendable { 8 | case noCache 9 | case noStore 10 | case noTransform 11 | case onlyIfCached 12 | case maxAge(seconds: Int) 13 | case maxStale(seconds: Int?) 14 | case minFresh(seconds: Int) 15 | 16 | public var rawValue: String { 17 | switch self { 18 | case .noCache: 19 | return "no-cache" 20 | case .noStore: 21 | return "no-store" 22 | case .noTransform: 23 | return "no-transform" 24 | case .onlyIfCached: 25 | return "only-if-cached" 26 | case .maxAge(let seconds): 27 | return "max-age=\(seconds)" 28 | case .maxStale(let seconds): 29 | if let seconds = seconds { 30 | return "max-stale=\(seconds)" 31 | } else { 32 | return "max-stale" 33 | } 34 | case .minFresh(let seconds): 35 | return "min-fresh=\(seconds)" 36 | } 37 | } 38 | 39 | public init?(rawValue: String) { 40 | let components = rawValue.lowercased().split(separator: "=") 41 | let directive = components[0].trimmingCharacters(in: .whitespaces) 42 | 43 | switch directive { 44 | case "no-cache": 45 | self = .noCache 46 | case "no-store": 47 | self = .noStore 48 | case "no-transform": 49 | self = .noTransform 50 | case "only-if-cached": 51 | self = .onlyIfCached 52 | case "max-age": 53 | guard components.count == 2, let seconds = Int(components[1]) else { return nil } 54 | self = .maxAge(seconds: seconds) 55 | case "max-stale": 56 | if components.count == 1 { 57 | self = .maxStale(seconds: nil) 58 | } else if components.count == 2, let seconds = Int(components[1]) { 59 | self = .maxStale(seconds: seconds) 60 | } else { 61 | return nil 62 | } 63 | case "min-fresh": 64 | guard components.count == 2, let seconds = Int(components[1]) else { return nil } 65 | self = .minFresh(seconds: seconds) 66 | default: 67 | return nil 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Sources/Intramodular/HTTP/HTTPConnectionType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Swift 6 | 7 | public enum HTTPConnectionType: String, Hashable, Sendable { 8 | case close = "close" 9 | case keepAlive = "keep-alive" 10 | } 11 | -------------------------------------------------------------------------------- /Sources/Intramodular/HTTP/HTTPEndpoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Foundation 6 | import Merge 7 | import Swallow 8 | import SwiftAPI 9 | 10 | public protocol HTTPEndpoint: Endpoint where Root: HTTPAPISpecification { 11 | 12 | } 13 | 14 | // MARK: - Conformances 15 | 16 | @propertyWrapper 17 | open class BaseHTTPEndpoint: 18 | ModifiableEndpointBase, HTTPEndpoint { 19 | open override var wrappedValue: ModifiableEndpointBase { 20 | self 21 | } 22 | 23 | override open func buildRequestBase( 24 | from input: Input, 25 | context: BuildRequestContext 26 | ) throws -> HTTPRequest { 27 | if let input = input as? HTTPRequestPopulator { 28 | return try input.populate(HTTPRequest(url: context.root.baseURL)) 29 | } else { 30 | return HTTPRequest(url: context.root.baseURL) 31 | } 32 | } 33 | 34 | override open func decodeOutputBase( 35 | from response: HTTPResponse, 36 | context: DecodeOutputContext 37 | ) throws -> Output { 38 | if let outputType = Output.self as? HTTPResponseDecodable.Type { 39 | return try outputType.init(from: response) as! Output 40 | } 41 | 42 | try response.validate() 43 | 44 | return try super.decodeOutput(from: response, context: context) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/Intramodular/HTTP/HTTPHeaderField.Link.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Foundation 6 | import Swift 7 | 8 | extension HTTPHeaderField { 9 | /// A structure representing a RFC 5988 link. 10 | public struct Link: Equatable, Hashable { 11 | public let uri: String 12 | public let parameters: [String: String] 13 | 14 | public var url: URL? { 15 | URL(string: uri) 16 | } 17 | 18 | public init(uri: String, parameters: [String: String]? = nil) { 19 | self.uri = uri 20 | self.parameters = parameters ?? [:] 21 | } 22 | 23 | public var relationType: String? { 24 | parameters["rel"] 25 | } 26 | 27 | public var reverseRelationType: String? { 28 | parameters["rev"] 29 | } 30 | 31 | public var type: String? { 32 | parameters["type"] 33 | } 34 | 35 | public init(header: String) throws { 36 | let components = header.components(separatedBy: "; ") 37 | 38 | let parameters = components.dropFirst() 39 | .map { 40 | $0.splitInHalf(separator: "=") 41 | } 42 | .map { parameter in 43 | [parameter.0: parameter.1.trim(prefix: "\"", suffix: "\"") as String] 44 | } 45 | 46 | 47 | self.uri = try components.first.unwrap().trim(prefix: "<", suffix: ">") 48 | self.parameters = parameters.reduce([:], { $0.merging($1, uniquingKeysWith: { lhs, _ in lhs }) }) 49 | } 50 | } 51 | } 52 | 53 | extension HTTPHeaderField.Link { 54 | public var html: String { 55 | let components = parameters.map { key, value in 56 | "\(key)=\"\(value)\"" 57 | } + ["href=\"\(uri)\""] 58 | 59 | let elements = components.joined(separator: " ") 60 | 61 | return "" 62 | } 63 | 64 | /// Encode the link into a header 65 | public var header: String { 66 | let components = ["<\(uri)>"] + parameters.map { key, value in 67 | "\(key)=\"\(value)\"" 68 | } 69 | 70 | return components.joined(separator: "; ") 71 | } 72 | } 73 | 74 | // MARK: - API 75 | 76 | extension HTTPResponse { 77 | public var links: [HTTPHeaderField.Link] { 78 | do { 79 | guard let linkHeader = self.headerFields[.custom("Link")] else { 80 | return [] 81 | } 82 | 83 | return try linkHeader 84 | .components(separatedBy: ",") 85 | .map(HTTPHeaderField.Link.init(header:)) 86 | .map { link in 87 | var uri = link.uri 88 | 89 | if let baseURL = self.cocoaURLResponse.url, let relativeURI = URL(string: uri, relativeTo: baseURL)?.absoluteString { 90 | uri = relativeURI 91 | } 92 | 93 | return .init(uri: uri, parameters: link.parameters) 94 | } 95 | } catch { 96 | return [] 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Sources/Intramodular/HTTP/HTTPHeaderField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Foundation 6 | import Swift 7 | 8 | public enum HTTPHeaderField: Hashable, Sendable { 9 | case accept(HTTPMediaType) 10 | case authorization(HTTPAuthorizationType, String) 11 | case cacheControl(HTTPCacheControlType) 12 | case connection(HTTPConnectionType) 13 | case contentDisposition(String) 14 | case contentLength(octets: Int) 15 | case contentType(HTTPMediaType) 16 | case cookie(String) 17 | case host(host: String, port: String) 18 | case location(URL) 19 | case origin(String) 20 | case referer(String) 21 | case userAgent(HTTPUserAgent) 22 | 23 | case custom(key: String, value: String) 24 | 25 | public static func custom(key: String, value: APIKey?) -> Self? { 26 | value.map({ Self.custom(key: key, value: $0.value) }) 27 | } 28 | 29 | public init(key: String, value: String) { 30 | switch key { 31 | case Self.Key.accept.rawValue: 32 | self = .accept(.init(rawValue: value)) 33 | case Self.Key.authorization.rawValue: 34 | self = .authorization(.init(rawValue: key), value) 35 | case Self.Key.cacheControl.rawValue: 36 | if let value = HTTPCacheControlType(rawValue: value) { 37 | self = .cacheControl(value) 38 | } else { 39 | fallthrough 40 | } 41 | case Self.Key.connection.rawValue: 42 | if let connection = HTTPConnectionType(rawValue: value) { 43 | self = .connection(connection) 44 | } else { 45 | fallthrough 46 | } 47 | case Self.Key.contentDisposition.rawValue: 48 | fallthrough // TODO 49 | case Self.Key.contentLength.rawValue: 50 | fallthrough // TODO 51 | case Self.Key.cookie.rawValue: 52 | self = .cookie(value) 53 | case Self.Key.contentType.rawValue: 54 | self = .contentType(.init(rawValue: value)) 55 | case Self.Key.host.rawValue: 56 | self = .custom(key: HTTPHeaderField.Key.host.rawValue, value: value) // FIXME 57 | case Self.Key.location.rawValue: 58 | self = .location(URL(string: value)!) 59 | case Self.Key.origin.rawValue: 60 | self = .origin(value) 61 | case Self.Key.referer.rawValue: 62 | self = .referer(value) 63 | case Self.Key.userAgent.rawValue: 64 | self = .userAgent(HTTPUserAgent(rawValue: value)) 65 | 66 | default: 67 | self = .custom(key: key, value: value) 68 | } 69 | } 70 | 71 | public init(key: AnyHashable, value: Any) { 72 | if let key = key.base as? String, let value = value as? String { 73 | self.init(key: key, value: value) 74 | } else { 75 | assertionFailure() 76 | 77 | self = .custom(key: String(describing: key), value: String(describing: value)) 78 | } 79 | } 80 | 81 | public static func custom(key: String, value: String?) -> Self? { 82 | guard let value else { 83 | return nil 84 | } 85 | 86 | return .custom(key: key, value: value) 87 | } 88 | 89 | } 90 | 91 | extension HTTPHeaderField { 92 | public enum Key: Hashable { 93 | case accept 94 | case authorization 95 | case cacheControl 96 | case connection 97 | case contentDisposition 98 | case contentLength 99 | case contentType 100 | case cookie 101 | case host 102 | case location 103 | case origin 104 | case referer 105 | case userAgent 106 | 107 | case custom(String) 108 | 109 | public var rawValue: String { 110 | switch self { 111 | case .accept: 112 | return "Accept" 113 | case .authorization: 114 | return "Authorization" 115 | case .cacheControl: 116 | return "Cache-Control" 117 | case .connection: 118 | return "Connection" 119 | case .contentDisposition: 120 | return "Content-Disposition" 121 | case .contentLength: 122 | return "Content-Length" 123 | case .contentType: 124 | return "Content-Type" 125 | case .cookie: 126 | return "Cookie" 127 | case .host: 128 | return "Host" 129 | case .location: 130 | return "Location" 131 | case .origin: 132 | return "Origin" 133 | case .referer: 134 | return "Referer" 135 | case .userAgent: 136 | return "UserAgent" 137 | 138 | case let .custom(value): 139 | return value 140 | } 141 | } 142 | 143 | public func hash(into hasher: inout Hasher) { 144 | hasher.combine(rawValue) 145 | } 146 | 147 | public static func == (lhs: Self, rhs: Self) -> Bool { 148 | lhs.rawValue == rhs.rawValue 149 | } 150 | } 151 | } 152 | 153 | extension HTTPHeaderField { 154 | public var key: HTTPHeaderField.Key { 155 | switch self { 156 | case .accept: 157 | return .accept 158 | case .authorization: 159 | return .authorization 160 | case .cacheControl: 161 | return .cacheControl 162 | case .connection: 163 | return .connection 164 | case .contentDisposition: 165 | return .contentDisposition 166 | case .contentLength: 167 | return .contentLength 168 | case .contentType: 169 | return .contentType 170 | case .cookie: 171 | return .cookie 172 | case .host: 173 | return .host 174 | case .location: 175 | return .location 176 | case .origin: 177 | return .origin 178 | case .referer: 179 | return .referer 180 | case .userAgent: 181 | return .userAgent 182 | case let .custom(key, _): 183 | return .custom(key) 184 | } 185 | } 186 | 187 | public var value: String { 188 | switch self { 189 | case .accept(let mediaType): 190 | return mediaType.rawValue 191 | case .authorization(let type, let credentials): 192 | return "\(type.rawValue) \(credentials)" 193 | case .cacheControl(let policy): 194 | return policy.rawValue 195 | case .connection(let connectionType): 196 | return connectionType.rawValue 197 | case .contentDisposition(let value): 198 | return value 199 | case .contentLength(let length): 200 | return String(length) 201 | case .contentType(let contentType): 202 | return contentType.rawValue 203 | case .cookie(let value): 204 | return value 205 | case .host(let host, let port): 206 | return host + port 207 | case .location(let value): 208 | return value.absoluteString 209 | case .origin(let origin): 210 | return origin 211 | case .referer(let referer): 212 | return referer 213 | case .userAgent(let userAgent): 214 | return userAgent.rawValue 215 | case let .custom(_, value): 216 | return value 217 | } 218 | } 219 | } 220 | 221 | // MARK: - Conformances 222 | 223 | extension HTTPHeaderField: Codable { 224 | private struct _CodableRepresentation: Codable { 225 | let key: String 226 | let value: String 227 | } 228 | 229 | public init(from decoder: Decoder) throws { 230 | let keyValuePair = try decoder.singleValueContainer().decode(_CodableRepresentation.self) 231 | 232 | self.init(key: keyValuePair.key, value: keyValuePair.value) 233 | } 234 | 235 | public func encode(to encoder: Encoder) throws { 236 | var container = encoder.singleValueContainer() 237 | 238 | try container.encode(_CodableRepresentation(key: key.rawValue, value: value)) 239 | } 240 | } 241 | 242 | extension HTTPHeaderField: CustomDebugStringConvertible { 243 | public var debugDescription: String { 244 | "\(key.rawValue): \(value)" 245 | } 246 | } 247 | 248 | // MARK: - Helpers 249 | 250 | extension Sequence where Element == HTTPHeaderField { 251 | public subscript(_ key: HTTPHeaderField.Key) -> String? { 252 | first(where: { $0.key == key })?.value 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /Sources/Intramodular/HTTP/HTTPInterface.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Foundation 6 | import Merge 7 | import Swallow 8 | import SwiftAPI 9 | 10 | public protocol HTTPAPISpecification: APISpecification where Request == HTTPRequest { 11 | var host: URL { get } 12 | var baseURL: URL { get } 13 | } 14 | 15 | public protocol RESTAPISpecification: HTTPAPISpecification, RESTfulInterface { 16 | 17 | } 18 | 19 | public protocol _RESTAPIConfiguration: Codable, Hashable, Sendable { 20 | 21 | } 22 | 23 | @available(*, deprecated, renamed: "HTTPAPISpecification") 24 | public typealias HTTPInterface = HTTPAPISpecification 25 | @available(*, deprecated, renamed: "RESTAPISpecification") 26 | public typealias RESTfulHTTPInterface = RESTAPISpecification 27 | 28 | // MARK: - Implementation 29 | 30 | extension HTTPAPISpecification { 31 | public var baseURL: URL { 32 | host 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/Intramodular/HTTP/HTTPMediaType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Swift 6 | import UniformTypeIdentifiers 7 | 8 | public enum HTTPMediaType: Codable, Hashable, RawRepresentable, Sendable { 9 | // Text types 10 | case plainText 11 | case html 12 | case css 13 | case javascript 14 | case csv 15 | case markdown 16 | 17 | // Image types 18 | case jpeg 19 | case png 20 | case gif 21 | case svg 22 | case webp 23 | 24 | // Application types 25 | case json 26 | case xml 27 | case pdf 28 | case zip 29 | case form 30 | case m4a 31 | case mp4 32 | case mpeg 33 | case eventStream 34 | case octetStream 35 | case webm 36 | case wav 37 | 38 | case anything 39 | case custom(String) 40 | 41 | public var rawValue: String { 42 | switch self { 43 | // Text types 44 | case .plainText: 45 | return "text/plain" 46 | case .html: 47 | return "text/html" 48 | case .css: 49 | return "text/css" 50 | case .javascript: 51 | return "text/javascript" 52 | case .csv: 53 | return "text/csv" 54 | case .markdown: 55 | return "text/markdown" 56 | 57 | // Image types 58 | case .jpeg: 59 | return "image/jpeg" 60 | case .png: 61 | return "image/png" 62 | case .gif: 63 | return "image/gif" 64 | case .svg: 65 | return "image/svg+xml" 66 | case .webp: 67 | return "image/webp" 68 | 69 | // Application types 70 | case .json: 71 | return "application/json" 72 | case .xml: 73 | return "application/xml" 74 | case .pdf: 75 | return "application/pdf" 76 | case .zip: 77 | return "application/zip" 78 | case .form: 79 | return "application/x-www-form-urlencoded" 80 | case .m4a: 81 | return "audio/m4a" 82 | case .mp4: 83 | return "audio/mp4" 84 | case .mpeg: 85 | return "audio/mpeg" 86 | case .eventStream: 87 | return "text/event-stream" 88 | case .octetStream: 89 | return "application/octet-stream" 90 | case .webm: 91 | return "audio/webm" 92 | case .wav: 93 | return "audio/wav" 94 | 95 | case .anything: 96 | return "*/*" 97 | case .custom(let value): 98 | return value 99 | } 100 | } 101 | 102 | public init(rawValue: String) { 103 | switch rawValue { 104 | case Self.plainText.rawValue: 105 | self = .plainText 106 | case Self.html.rawValue: 107 | self = .html 108 | case Self.css.rawValue: 109 | self = .css 110 | case Self.javascript.rawValue: 111 | self = .javascript 112 | case Self.csv.rawValue: 113 | self = .csv 114 | case Self.markdown.rawValue: 115 | self = .markdown 116 | case Self.jpeg.rawValue: 117 | self = .jpeg 118 | case Self.png.rawValue: 119 | self = .png 120 | case Self.gif.rawValue: 121 | self = .gif 122 | case Self.svg.rawValue: 123 | self = .svg 124 | case Self.webp.rawValue: 125 | self = .webp 126 | case Self.json.rawValue: 127 | self = .json 128 | case Self.xml.rawValue: 129 | self = .xml 130 | case Self.pdf.rawValue: 131 | self = .pdf 132 | case Self.zip.rawValue: 133 | self = .zip 134 | case Self.form.rawValue: 135 | self = .form 136 | case Self.mpeg.rawValue: 137 | self = .mpeg 138 | case Self.eventStream.rawValue: 139 | self = .eventStream 140 | case Self.octetStream.rawValue: 141 | self = .octetStream 142 | case Self.anything.rawValue: 143 | self = .anything 144 | default: 145 | self = .custom(rawValue) 146 | } 147 | } 148 | 149 | public init?(_swiftType type: Any.Type) { 150 | switch type { 151 | case Data.self: 152 | self = .octetStream 153 | case String.self: 154 | self = .plainText 155 | default: 156 | return nil 157 | } 158 | } 159 | } 160 | 161 | extension HTTPMediaType { 162 | public init?(fileURL: URL) { 163 | guard let mimeType: String = fileURL._actuallyStandardizedFileURL._detectPreferredMIMEType() else { 164 | runtimeIssue("Failed to determine preferred MIME type for file: \(fileURL)") 165 | 166 | return nil 167 | } 168 | 169 | self = Self(rawValue: mimeType) 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /Sources/Intramodular/HTTP/HTTPMethod.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Swift 6 | 7 | /// An HTTP method. 8 | public enum HTTPMethod: String, Codable, CustomStringConvertible, Hashable, Sendable { 9 | case connect = "CONNECT" 10 | case delete = "DELETE" 11 | case get = "GET" 12 | case head = "HEAD" 13 | case options = "OPTIONS" 14 | case patch = "PATCH" 15 | case post = "POST" 16 | case put = "PUT" 17 | case trace = "TRACE" 18 | 19 | public var description: String { 20 | rawValue 21 | } 22 | 23 | public var prefersQueryParameters: Bool { 24 | switch self { 25 | case .get, .head, .delete: 26 | return true 27 | default: 28 | return false 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/Intramodular/HTTP/HTTPProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Swift 6 | 7 | public enum HTTPProtocol: String, Codable, Hashable, Sendable { 8 | case http = "http" 9 | case https = "https" 10 | } 11 | -------------------------------------------------------------------------------- /Sources/Intramodular/HTTP/HTTPRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Merge 6 | import ObjectiveC 7 | import Swallow 8 | import SwiftAPI 9 | 10 | @available(*, deprecated, renamed: "HTTPClient") 11 | public typealias HTTPRepository = HTTPClient 12 | 13 | public protocol HTTPClient: Client where Session.Request == HTTPRequest { 14 | associatedtype Session = HTTPSession 15 | associatedtype SessionCache = HTTPCache 16 | } 17 | 18 | // MARK: - Implementation 19 | 20 | private var _HTTPClient_session_objcAssociationKey: UInt8 = 0 21 | private var _HTTPClient_sessionCache_objcAssociationKey: UInt8 = 0 22 | private var _HTTPClient_logger_objcAssociationKey: UInt8 = 0 23 | 24 | extension HTTPClient where Session == HTTPSession { 25 | public var session: HTTPSession { 26 | if let result = objc_getAssociatedObject(self, &_HTTPClient_session_objcAssociationKey) as? HTTPSession { 27 | return result 28 | } else { 29 | let result = HTTPSession() 30 | 31 | objc_setAssociatedObject(self, &_HTTPClient_session_objcAssociationKey, result, .OBJC_ASSOCIATION_RETAIN) 32 | 33 | return result 34 | } 35 | } 36 | } 37 | 38 | extension HTTPClient where SessionCache == HTTPCache { 39 | public var sessionCache: SessionCache { 40 | if let result = objc_getAssociatedObject(self, &_HTTPClient_sessionCache_objcAssociationKey) as? SessionCache { 41 | return result 42 | } else { 43 | let result = SessionCache() 44 | 45 | objc_setAssociatedObject(self, &_HTTPClient_sessionCache_objcAssociationKey, result, .OBJC_ASSOCIATION_RETAIN) 46 | 47 | return result 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/Intramodular/HTTP/HTTPSession.Task.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Foundation 6 | import Merge 7 | import Swallow 8 | import SwiftAPI 9 | 10 | extension HTTPSession { 11 | public final class Task: NSObject, Merge.ObservableTask, URLSessionTaskDelegate { 12 | public typealias Success = Data 13 | public typealias Error = Swift.Error 14 | public typealias Status = ObservableTaskStatus 15 | 16 | private var base: URLSessionTask? 17 | 18 | public let request: HTTPRequest 19 | public let session: HTTPSession 20 | 21 | private let _statusSubject = CurrentValueSubject(.idle) 22 | 23 | public init(request: HTTPRequest, session: HTTPSession) { 24 | self.request = request 25 | self.session = session 26 | } 27 | } 28 | } 29 | 30 | extension HTTPSession.Task { 31 | public var status: Status { 32 | _statusSubject.value 33 | } 34 | 35 | public var objectWillChange: AnyPublisher, Never> { 36 | _statusSubject.eraseToAnyPublisher() 37 | } 38 | 39 | public var objectDidChange: AnyPublisher, Never> { 40 | _statusSubject.eraseToAnyPublisher() // FIXME: !!! 41 | } 42 | 43 | public func start() { 44 | do { 45 | let dataTask = try session.bridgeToObjectiveC().dataTask(with: .init(request)) 46 | 47 | dataTask.resume() 48 | 49 | self.base = dataTask 50 | 51 | _statusSubject.send(.active) 52 | } catch { 53 | _statusSubject.send(.error(error)) 54 | } 55 | } 56 | 57 | public func cancel() { 58 | base?.cancel() 59 | 60 | _statusSubject.send(.canceled) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Sources/Intramodular/HTTP/HTTPSession.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import CorePersistence 6 | import Foundation 7 | import Merge 8 | import Swallow 9 | import SwiftAPI 10 | 11 | public final class HTTPSession: Identifiable, Initiable, RequestSession, @unchecked Sendable { 12 | private let lock = OSUnfairLock() 13 | 14 | public static let shared = HTTPSession(base: URLSession.shared) 15 | 16 | public static var localhost: HTTPSession { 17 | let result = HTTPSession(base: URLSession.shared) 18 | 19 | result._unsafeFlags.insert(.localhost) 20 | 21 | return result 22 | } 23 | 24 | public let cancellables = Cancellables() 25 | public let id: UUID 26 | public var _unsafeFlags: Set<_UnsafeFlag> = [] 27 | 28 | private var base: URLSession 29 | 30 | public var configuration: URLSessionConfiguration { 31 | base.configuration 32 | } 33 | 34 | public init(base: URLSession) { 35 | self.id = UUID() 36 | self.base = base 37 | } 38 | 39 | public convenience init(host: URL) { 40 | self.init(base: .shared) 41 | 42 | self._unsafeFlags.insert(.host(host)) 43 | } 44 | 45 | public func disableTimeouts() { 46 | lock.withCriticalScope { 47 | let sessionConfiguration = URLSessionConfiguration.default 48 | 49 | sessionConfiguration.timeoutIntervalForRequest = TimeInterval(INT_MAX) 50 | sessionConfiguration.timeoutIntervalForResource = TimeInterval(INT_MAX) 51 | 52 | self.base = URLSession(configuration: configuration) 53 | } 54 | } 55 | 56 | public convenience init() { 57 | self.init(base: .init(configuration: .default)) 58 | } 59 | 60 | public func task( 61 | with request: HTTPRequest 62 | ) -> AnyTask { 63 | if let host = _unsafeFlags.first(byUnwrapping: /_UnsafeFlag.host) { 64 | if !request.host.absoluteString.hasPrefix(host.absoluteString) { 65 | return .failure(.system(Never.Reason.illegal)) 66 | } 67 | } 68 | 69 | return lock.withCriticalScope { 70 | do { 71 | if request.method == .get { 72 | assert(request.body == nil) 73 | } 74 | 75 | return try base 76 | .dataTaskPublisher(for: request) 77 | .map { [weak self] output -> HTTPRequest.Response in 78 | let response = HTTPRequest.Response( 79 | request: request, 80 | data: output.data, 81 | cocoaURLResponse: output.response as! HTTPURLResponse 82 | ) 83 | 84 | if let `self` = self { 85 | if self._unsafeFlags.contains(.dumpRequestBodies) { 86 | #try(.optimistic) { 87 | let json = try JSON(data: output.data) 88 | 89 | print(json.prettyPrintedDescription) 90 | } 91 | } 92 | } 93 | 94 | return response 95 | } 96 | .mapError { 97 | HTTPRequest.Error.system(AnyError(erasing: $0)) 98 | } 99 | .convertToTask() 100 | } catch { 101 | return .failure(HTTPRequest.Error.system(AnyError(erasing: error))) 102 | } 103 | } 104 | } 105 | } 106 | 107 | extension HTTPSession { 108 | public func data( 109 | for request: URLRequest 110 | ) async throws -> HTTPResponse { 111 | let (data, response) = try await base.data(for: request) 112 | 113 | let result = try HTTPResponse( 114 | request: nil, 115 | data: data, 116 | cocoaURLResponse: cast(response, to: HTTPURLResponse.self) 117 | ) 118 | 119 | return result 120 | } 121 | 122 | public func data( 123 | for request: HTTPRequest 124 | ) async throws -> HTTPResponse { 125 | let (data, response) = try await base.data(for: request) 126 | 127 | let result = try HTTPResponse( 128 | request: request, 129 | data: data, 130 | cocoaURLResponse: cast(response, to: HTTPURLResponse.self) 131 | ) 132 | 133 | return result 134 | } 135 | } 136 | 137 | // MARK: - Conformances 138 | 139 | extension HTTPSession: ObjectiveCBridgeable { 140 | public typealias _ObjectiveCType = URLSession 141 | 142 | public static func bridgeFromObjectiveC(_ source: ObjectiveCType) throws -> Self { 143 | .init(base: source) 144 | } 145 | 146 | public func bridgeToObjectiveC() throws -> ObjectiveCType { 147 | base 148 | } 149 | } 150 | 151 | // MARK: - Auxiliary 152 | 153 | extension HTTPSession { 154 | public enum _UnsafeFlag: Codable, Hashable, Sendable { 155 | case localhost 156 | case dumpRequestBodies 157 | case host(URL) 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /Sources/Intramodular/HTTP/HTTPUserAgent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Swift 6 | 7 | public enum HTTPUserAgent: Codable, Hashable, RawRepresentable, Sendable { 8 | case bot 9 | case chrome 10 | case chromeAndroid 11 | case chromeiOS 12 | case firefoxMac 13 | case firefoxWindows 14 | case internetExplorer 15 | case opera 16 | case safari 17 | 18 | case custom(String) 19 | 20 | public var rawValue: String { 21 | switch self { 22 | case .bot: 23 | return "Googlebot/2.1 (+http://www.google.com/bot.html)" 24 | case .chrome: 25 | return "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36" 26 | case .chromeAndroid: 27 | return "Mozilla/5.0 (Linux; ; ) AppleWebKit/ (KHTML, like Gecko) Chrome/ Mobile Safari/" 28 | case .chromeiOS: 29 | return "Mozilla/5.0 (iPhone; CPU iPhone OS 10_3 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) CriOS/56.0.2924.75 Mobile/14E5239e Safari/602.1" 30 | case .firefoxMac: 31 | return "Mozilla/5.0 (Macintosh; Intel Mac OS X x.y; rv:42.0) Gecko/20100101 Firefox/42.0" 32 | case .firefoxWindows: 33 | return "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0" 34 | case .internetExplorer: 35 | return "Mozilla/5.0 (compatible; MSIE 9.0; Windows Phone OS 7.5; Trident/5.0; IEMobile/9.0)" 36 | case .opera: 37 | return "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36 OPR/38.0.2220.41" 38 | case .safari: 39 | return "Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1" 40 | 41 | case .custom(let value): 42 | return value 43 | } 44 | } 45 | 46 | public init(rawValue: String) { 47 | switch rawValue { 48 | case Self.bot.rawValue: 49 | self = .bot 50 | case Self.chrome.rawValue: 51 | self = .chrome 52 | case Self.chromeAndroid.rawValue: 53 | self = .chromeAndroid 54 | case Self.chromeiOS.rawValue: 55 | self = .chromeiOS 56 | case Self.firefoxMac.rawValue: 57 | self = .firefoxMac 58 | case Self.firefoxWindows.rawValue: 59 | self = .firefoxWindows 60 | case Self.internetExplorer.rawValue: 61 | self = .internetExplorer 62 | case Self.opera.rawValue: 63 | self = .opera 64 | case Self.safari.rawValue: 65 | self = .safari 66 | 67 | default: 68 | self = .custom(rawValue) 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Sources/Intramodular/HTTP/Request/HTTPRequest.Body.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import CorePersistence 6 | import Foundation 7 | import Swallow 8 | 9 | extension HTTPRequest { 10 | /// Represents the body of an HTTP request. 11 | public struct Body: Codable, Hashable, Sendable { 12 | /// The types of content that an `HTTPRequest.Body` can contain. 13 | public enum Content: Hashable, @unchecked Sendable { 14 | /// Raw binary data. 15 | case data(Data) 16 | 17 | /// A stream of input data. 18 | case inputStream(InputStream) 19 | 20 | /// Retrieves the `Data` value if the content is `.data`. 21 | public var dataValue: Data? { 22 | if case let .data(data) = self { 23 | return data 24 | } 25 | 26 | return nil 27 | } 28 | } 29 | 30 | public let header: [HTTPHeaderField] 31 | public let content: Content 32 | 33 | public var data: Data? { 34 | content.dataValue 35 | } 36 | } 37 | } 38 | 39 | // MARK: - Initializers 40 | 41 | extension HTTPRequest.Body { 42 | /// Create a request body with the given data. 43 | public static func data(_ data: Data) -> Self { 44 | Self(header: [], content: .data(data)) 45 | } 46 | 47 | /// Create a request body the given data and headers. 48 | public static func data(_ data: Data, headers: [HTTPHeaderField]) -> Self { 49 | Self(header: headers, content: .data(data)) 50 | } 51 | } 52 | 53 | // MARK: - Conformances 54 | 55 | extension HTTPRequest.Body.Content: Codable { 56 | public init(from decoder: Decoder) throws { 57 | self = .data(try decoder.singleValueContainer().decode(Data.self)) 58 | } 59 | 60 | public func encode(to encoder: Encoder) throws { 61 | switch self { 62 | case .data(let data): 63 | try data.encode(to: encoder) 64 | case .inputStream(let stream): 65 | throw EncodingError.invalidValue(stream, EncodingError.Context(codingPath: [], debugDescription: "Cannot encode an InputStream")) 66 | } 67 | } 68 | } 69 | 70 | extension HTTPRequest.Body: CustomDebugStringConvertible { 71 | public var debugDescription: String { 72 | do { 73 | guard header.isEmpty else { 74 | return Metatype(Self.self).name 75 | } 76 | 77 | switch content { 78 | case .data(let data): 79 | return try data.toString() 80 | case .inputStream: 81 | return "Input Stream" 82 | } 83 | } catch { 84 | return Metatype(Self.self).name 85 | } 86 | } 87 | } 88 | 89 | // MARK: - Supplementary 90 | 91 | extension HTTPRequest { 92 | /// Creates a JSON-based query. 93 | /// 94 | /// - Parameters: 95 | /// - value: An `Encodable` object to encode into a query. 96 | /// - dateEncodingStrategy: Optional date encoding strategy. 97 | /// - dataEncodingStrategy: Optional data encoding strategy. 98 | /// - keyEncodingStrategy: Optional key encoding strategy. 99 | /// - nonConformingFloatEncodingStrategy: Optional non-conforming float encoding strategy. 100 | /// - Throws: An error if encoding fails. 101 | /// - Returns: An `HTTPRequest` instance with the JSON query set. 102 | public func jsonQuery( 103 | _ value: T, 104 | dateEncodingStrategy: JSONEncoder.DateEncodingStrategy? = nil, 105 | dataEncodingStrategy: JSONEncoder.DataEncodingStrategy? = nil, 106 | keyEncodingStrategy: JSONEncoder.KeyEncodingStrategy? = nil, 107 | nonConformingFloatEncodingStrategy: JSONEncoder.NonConformingFloatEncodingStrategy? = nil 108 | ) throws -> Self { 109 | TODO.whole(.fix) 110 | 111 | let _encoder = JSONEncoder() 112 | 113 | dateEncodingStrategy.map(into: &_encoder.dateEncodingStrategy) 114 | dataEncodingStrategy.map(into: &_encoder.dataEncodingStrategy) 115 | keyEncodingStrategy.map(into: &_encoder.keyEncodingStrategy) 116 | nonConformingFloatEncodingStrategy.map(into: &_encoder.nonConformingFloatEncodingStrategy) 117 | 118 | let encoder = _ModularTopLevelEncoder(from: _encoder) 119 | 120 | let queryItems = try JSONDecoder() 121 | .decode([String: AnyCodable].self, from: try encoder.encode(value)) 122 | .map { 123 | URLQueryItem( 124 | name: $0.key, 125 | value: try encoder.encode($0.value).toString() 126 | ) 127 | } 128 | 129 | return query(queryItems) 130 | } 131 | 132 | /// Sets a JSON-encoded body. 133 | /// 134 | /// - Parameters: 135 | /// - value: An `Encodable` object to encode into the body. 136 | /// - dateEncodingStrategy: Optional date encoding strategy. 137 | /// - dataEncodingStrategy: Optional data encoding strategy. 138 | /// - keyEncodingStrategy: Optional key encoding strategy. 139 | /// - nonConformingFloatEncodingStrategy: Optional non-conforming float encoding strategy. 140 | /// - Throws: An error if encoding fails. 141 | /// - Returns: An `HTTPRequest` instance with the JSON body set. 142 | public func jsonBody( 143 | _ value: T, 144 | dateEncodingStrategy: JSONEncoder.DateEncodingStrategy? = nil, 145 | dataEncodingStrategy: JSONEncoder.DataEncodingStrategy? = nil, 146 | keyEncodingStrategy: JSONEncoder.KeyEncodingStrategy? = nil, 147 | nonConformingFloatEncodingStrategy: JSONEncoder.NonConformingFloatEncodingStrategy? = nil 148 | ) throws -> Self { 149 | return try _jsonBody( 150 | value, 151 | dateEncodingStrategy: dateEncodingStrategy, 152 | dataEncodingStrategy: dataEncodingStrategy, 153 | keyEncodingStrategy: keyEncodingStrategy, 154 | nonConformingFloatEncodingStrategy: nonConformingFloatEncodingStrategy 155 | ) 156 | } 157 | 158 | /// Sets a JSON-encoded body using a dictionary. 159 | /// 160 | /// - Parameter value: A `[String: Any?]` dictionary to encode into the body. 161 | /// - Throws: An error if serialization fails. 162 | /// - Returns: An `HTTPRequest` instance with the JSON body set. 163 | public func jsonBody( 164 | _ value: [String: Any?] 165 | ) throws -> Self { 166 | body( 167 | try JSONSerialization.data( 168 | withJSONObject: value.compactMapValues({ $0 }), 169 | options: [.fragmentsAllowed, .sortedKeys] 170 | ) 171 | ) 172 | } 173 | 174 | /// Sets a JSON-encoded body using generic type T. 175 | /// 176 | /// - Parameters: 177 | /// - value: An object to encode into the body. 178 | /// - dateEncodingStrategy: Optional date encoding strategy. 179 | /// - dataEncodingStrategy: Optional data encoding strategy. 180 | /// - keyEncodingStrategy: Optional key encoding strategy. 181 | /// - nonConformingFloatEncodingStrategy: Optional non-conforming float encoding strategy. 182 | /// - Throws: An error if encoding fails. 183 | /// - Returns: An `HTTPRequest` instance with the JSON body set. 184 | public func jsonBody( 185 | _ value: T, 186 | dateEncodingStrategy: JSONEncoder.DateEncodingStrategy? = nil, 187 | dataEncodingStrategy: JSONEncoder.DataEncodingStrategy? = nil, 188 | keyEncodingStrategy: JSONEncoder.KeyEncodingStrategy? = nil, 189 | nonConformingFloatEncodingStrategy: JSONEncoder.NonConformingFloatEncodingStrategy? = nil 190 | ) throws -> Self { 191 | if value is Void { 192 | return self // FIXME? 193 | } else if let value = value as? Encodable { 194 | return try _opaque_jsonBody( 195 | value, 196 | dateEncodingStrategy: dateEncodingStrategy, 197 | dataEncodingStrategy: dataEncodingStrategy, 198 | keyEncodingStrategy: keyEncodingStrategy, 199 | nonConformingFloatEncodingStrategy: nonConformingFloatEncodingStrategy 200 | ) 201 | } else if _isValueNil(value) { 202 | return self 203 | } else { 204 | assertionFailure("Failed to encode value of type: \(type(of: value)), \(value)") 205 | 206 | return self 207 | } 208 | } 209 | 210 | public func jsonBody( 211 | _ value: (any Encodable)?, 212 | dateEncodingStrategy: JSONEncoder.DateEncodingStrategy? = nil, 213 | dataEncodingStrategy: JSONEncoder.DataEncodingStrategy? = nil, 214 | keyEncodingStrategy: JSONEncoder.KeyEncodingStrategy? = nil, 215 | nonConformingFloatEncodingStrategy: JSONEncoder.NonConformingFloatEncodingStrategy? = nil 216 | ) throws -> Self { 217 | if let value { 218 | return try _opaque_jsonBody( 219 | value, 220 | dateEncodingStrategy: dateEncodingStrategy, 221 | dataEncodingStrategy: dataEncodingStrategy, 222 | keyEncodingStrategy: keyEncodingStrategy, 223 | nonConformingFloatEncodingStrategy: nonConformingFloatEncodingStrategy 224 | ) 225 | } else { 226 | return self 227 | } 228 | } 229 | 230 | private func _jsonBody( 231 | _ value: T, 232 | dateEncodingStrategy: JSONEncoder.DateEncodingStrategy? = nil, 233 | dataEncodingStrategy: JSONEncoder.DataEncodingStrategy? = nil, 234 | keyEncodingStrategy: JSONEncoder.KeyEncodingStrategy? = nil, 235 | nonConformingFloatEncodingStrategy: JSONEncoder.NonConformingFloatEncodingStrategy? = nil 236 | ) throws -> Self { 237 | let _encoder = JSONEncoder() 238 | 239 | dateEncodingStrategy.map(into: &_encoder.dateEncodingStrategy) 240 | dataEncodingStrategy.map(into: &_encoder.dataEncodingStrategy) 241 | keyEncodingStrategy.map(into: &_encoder.keyEncodingStrategy) 242 | nonConformingFloatEncodingStrategy.map(into: &_encoder.nonConformingFloatEncodingStrategy) 243 | 244 | let encoder = _ModularTopLevelEncoder(from: _encoder) 245 | 246 | return body(try encoder.encode(value)).header(.contentType(.json)) 247 | } 248 | 249 | private func _opaque_jsonBody( 250 | _ value: (any Encodable)?, 251 | dateEncodingStrategy: JSONEncoder.DateEncodingStrategy? = nil, 252 | dataEncodingStrategy: JSONEncoder.DataEncodingStrategy? = nil, 253 | keyEncodingStrategy: JSONEncoder.KeyEncodingStrategy? = nil, 254 | nonConformingFloatEncodingStrategy: JSONEncoder.NonConformingFloatEncodingStrategy? = nil 255 | ) throws -> Self { 256 | func _makeJSONBody(_ x: T) throws -> Self { 257 | try self._jsonBody( 258 | x, 259 | dateEncodingStrategy: dateEncodingStrategy, 260 | dataEncodingStrategy: dataEncodingStrategy, 261 | keyEncodingStrategy: keyEncodingStrategy, 262 | nonConformingFloatEncodingStrategy: nonConformingFloatEncodingStrategy 263 | ) 264 | } 265 | 266 | if let value { 267 | return try _openExistential(value, do: _makeJSONBody) 268 | } else { 269 | return self 270 | } 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /Sources/Intramodular/HTTP/Request/HTTPRequest.Error.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Diagnostics 6 | import Foundation 7 | import Swallow 8 | 9 | extension HTTPRequest { 10 | public enum Error: _ErrorX { 11 | case badRequest(request: HTTPRequest?, response: HTTPResponse) 12 | case system(AnyError) 13 | 14 | public var traits: ErrorTraits { 15 | let base: ErrorTraits = [.domain(.networking)] 16 | 17 | switch self { 18 | case .badRequest: 19 | return base // FIXME! 20 | case .system(let error): 21 | return base + error.traits 22 | } 23 | } 24 | } 25 | } 26 | 27 | extension HTTPRequest.Error { 28 | public var response: HTTPResponse { 29 | get throws { 30 | guard case .badRequest(_, let response) = self else { 31 | throw Never.Reason.unexpected 32 | } 33 | 34 | return response 35 | } 36 | } 37 | 38 | public var statusCode: HTTPResponseStatusCode { 39 | get throws { 40 | try response.statusCode 41 | } 42 | } 43 | } 44 | 45 | // MARK: - Initializers 46 | 47 | extension HTTPRequest.Error { 48 | @_disfavoredOverload 49 | public static func system(_ error: any Swift.Error) -> Self { 50 | .system(AnyError(erasing: error)) 51 | } 52 | 53 | public init?(_catchAll error: AnyError) throws { 54 | self = .system(error) 55 | } 56 | } 57 | 58 | // MARK: - Conformances 59 | 60 | extension HTTPRequest.Error: CustomDebugStringConvertible { 61 | public var debugDescription: String { 62 | switch self { 63 | case .badRequest(let request, let response): 64 | if let request { 65 | return "\(response.statusCode), bad request: \(request)" 66 | } else { 67 | return "Bad request: \(response.statusCode)" 68 | } 69 | case .system(let error): 70 | return String(describing: error) 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Sources/Intramodular/HTTP/Request/HTTPRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Foundation 7 | import Swallow 8 | import SwiftAPI 9 | 10 | /// An encapsulation of a HTTP request. 11 | public struct HTTPRequest: Codable, Request, Sendable { 12 | public typealias Method = HTTPMethod 13 | public typealias Query = [URLQueryItem] 14 | public typealias Header = [HTTPHeaderField] 15 | public typealias Response = HTTPResponse 16 | 17 | public private(set) var host: URL 18 | public private(set) var path: String? 19 | public private(set) var `protocol`: HTTPProtocol = .https 20 | public private(set) var method: HTTPMethod? 21 | public private(set) var query: Query = [] 22 | public private(set) var header: Header = [] 23 | public private(set) var body: Body? 24 | public private(set) var httpShouldHandleCookies: Bool = true 25 | 26 | public var url: URL { 27 | guard let path = path else { 28 | return host 29 | } 30 | 31 | return host.appendingPathComponent(path) 32 | } 33 | 34 | public init(url: URL) { 35 | self.host = url 36 | } 37 | 38 | public init!(url string: String) { 39 | guard let url = URL(string: string) else { 40 | return nil 41 | } 42 | 43 | self.host = url 44 | } 45 | } 46 | 47 | // MARK: - API 48 | 49 | extension HTTPRequest { 50 | public func host( 51 | _ host: URL 52 | ) -> Self { 53 | then({ $0.host = host }) 54 | } 55 | 56 | public func path( 57 | _ path: String 58 | ) -> Self { 59 | then({ $0.path = path }) 60 | } 61 | 62 | public func absolutePath( 63 | _ path: String 64 | ) -> Self { 65 | then { 66 | $0.host = URL(string: path)! 67 | $0.path = nil 68 | } 69 | } 70 | 71 | public func absolutePath( 72 | _ url: URL 73 | ) -> Self { 74 | then { 75 | $0.host = url 76 | $0.path = nil 77 | } 78 | } 79 | 80 | public func `protocol`( 81 | _ protocol: HTTPProtocol 82 | ) -> Self { 83 | then({ $0.protocol = `protocol` }) 84 | } 85 | 86 | public func method( 87 | _ method: HTTPMethod 88 | ) -> Self { 89 | then({ $0.method = method }) 90 | } 91 | 92 | public func query( 93 | _ items: [URLQueryItem] 94 | ) -> Self { 95 | then({ $0.query.append(contentsOf: items) }) 96 | } 97 | 98 | public func query( 99 | _ query: [String: String] 100 | ) -> Self { 101 | then({ $0.query.append(contentsOf: query.map({ URLQueryItem(name: $0.key, value: $0.value) })) }) 102 | } 103 | 104 | public func query( 105 | _ query: [String: String?] 106 | ) -> Self { 107 | self.query(query.compactMapValues({ $0 })) 108 | } 109 | 110 | public func query( 111 | _ queryString: String 112 | ) -> Self { 113 | query( 114 | queryString.components(separatedBy: "&").map { pair -> URLQueryItem in 115 | let value = pair 116 | .components(separatedBy:"=")[1] 117 | .replacingOccurrences(of: "+", with: " ") 118 | .removingPercentEncoding ?? "" 119 | 120 | return URLQueryItem(name: pair.components(separatedBy: "=")[0], value: value) 121 | } 122 | ) 123 | } 124 | 125 | public func header( 126 | _ header: Header 127 | ) -> Self { 128 | then({ $0.header.append(contentsOf: header) }) 129 | } 130 | 131 | public func headers( 132 | _ headers: [String: String] 133 | ) -> Self { 134 | then({ $0.header.append(contentsOf: headers.map(HTTPHeaderField.init(key:value:))) }) 135 | } 136 | 137 | public func header( 138 | _ key: String, 139 | _ value: String? 140 | ) -> Self { 141 | if let value { 142 | header(HTTPHeaderField(key: key, value: value)) 143 | } else { 144 | self 145 | } 146 | } 147 | 148 | public func header( 149 | _ key: String, 150 | _ value: APIKey? 151 | ) -> Self { 152 | if let value { 153 | header(HTTPHeaderField(key: key, value: value)) 154 | } else { 155 | self 156 | } 157 | } 158 | 159 | public func header( 160 | _ field: HTTPHeaderField? 161 | ) -> Self { 162 | guard let field else { 163 | return self 164 | } 165 | 166 | return then { 167 | if !$0.header.contains(field) { 168 | $0.header.append(field) 169 | } 170 | 171 | if field.key == .cookie { 172 | $0.httpShouldHandleCookies = true 173 | } 174 | } 175 | } 176 | 177 | public func deleteHeader(_ header: HTTPHeaderField.Key) -> Self { 178 | then({ $0.header = $0.header.filter({ $0.key != header }) }) 179 | } 180 | 181 | public func body(_ body: HTTPRequest.Body?) -> Self { 182 | then { 183 | $0.body = body 184 | 185 | if body != nil && $0.method == nil { 186 | $0.method = .post 187 | } 188 | } 189 | } 190 | 191 | public func body(_ data: Data) -> Self { 192 | body(HTTPRequest.Body(header: [], content: .data(data))) 193 | } 194 | 195 | public func httpShouldHandleCookies(_ httpShouldHandleCookies: Bool) -> Self { 196 | then({ $0.httpShouldHandleCookies = httpShouldHandleCookies }) 197 | } 198 | 199 | public func cookies( 200 | _ cookies: [String: String]? 201 | ) -> Self { 202 | header(.cookie((cookies ?? [:]).map({ "\($0.key)=\($0.value)" }).joined(separator: ";"))) 203 | } 204 | 205 | public func cookies( 206 | _ cookies: [String: String?]? 207 | ) -> Self { 208 | self.cookies(cookies?.compactMapValues({ $0 })) 209 | } 210 | } 211 | 212 | // MARK: - Supplementary 213 | 214 | extension HTTPRequest { 215 | public func dataTask( 216 | session: HTTPSession = .shared 217 | ) async throws -> Task<(data: Data, response: URLResponse), Swift.Error> { 218 | Task { 219 | try await URLSession.shared 220 | .dataTaskPublisher(for: self) 221 | .eraseToAnySingleOutputPublisher() 222 | .output() 223 | } 224 | } 225 | } 226 | 227 | // MARK: - Auxiliary 228 | 229 | extension URLRequest { 230 | public init(_ request: HTTPRequest) throws { 231 | guard var components = URLComponents(url: request.url, resolvingAgainstBaseURL: true) else { 232 | fatalError() 233 | } 234 | 235 | if !request.query.isEmpty { 236 | if components.queryItems == nil { 237 | components.queryItems = [] 238 | } 239 | 240 | components.queryItems?.append(contentsOf: request.query) 241 | } 242 | 243 | self.init(url: components.url!) 244 | 245 | httpMethod = request.method?.rawValue 246 | httpShouldHandleCookies = request.httpShouldHandleCookies 247 | 248 | request.header.forEach { component in 249 | addValue(component.value, forHTTPHeaderField: component.key.rawValue) 250 | } 251 | 252 | request.body?.header.forEach { component in 253 | addValue(component.value, forHTTPHeaderField: component.key.rawValue) 254 | } 255 | 256 | if let body = request.body?.content { 257 | switch body { 258 | case .data(let data): 259 | httpBody = data 260 | case .inputStream(let stream): 261 | httpBodyStream = stream 262 | } 263 | } 264 | } 265 | } 266 | 267 | extension URLSession { 268 | public func dataTaskPublisher( 269 | for request: HTTPRequest 270 | ) throws -> DataTaskPublisher { 271 | try dataTaskPublisher(for: URLRequest(request)) 272 | } 273 | 274 | public func data( 275 | for request: HTTPRequest 276 | ) async throws -> (Data, URLResponse) { 277 | try await data(for: URLRequest(request)) 278 | } 279 | } 280 | 281 | // MARK: - Helpers 282 | 283 | extension HTTPRequest { 284 | private func then(_ f: ((inout Self) throws -> Void)) rethrows -> Self { 285 | var result = self 286 | try f(&result) 287 | return result 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /Sources/Intramodular/HTTP/Request/HTTPRequestPopulator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Swift 6 | 7 | public protocol HTTPRequestPopulator { 8 | func populate(_: HTTPRequest) throws -> HTTPRequest 9 | } 10 | -------------------------------------------------------------------------------- /Sources/Intramodular/HTTP/Request/Multipart/HTTPRequest.Multipart.Content.Boundary.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Foundation 6 | import Swift 7 | 8 | extension HTTPRequest.Multipart.Content { 9 | public struct Boundary { 10 | public let stringValue: String 11 | 12 | public var delimiter: String { 13 | "--" + stringValue 14 | } 15 | 16 | public var distinguishedDelimiter: String { 17 | delimiter + "--" 18 | } 19 | 20 | public var delimiterData: Data { 21 | delimiter.data(using: .utf8)! 22 | } 23 | 24 | public var distinguishedDelimiterData: Data { 25 | distinguishedDelimiter.data(using: .utf8)! 26 | } 27 | 28 | public init() { 29 | stringValue = (UUID().uuidString + UUID().uuidString) 30 | .replacingOccurrences(of: "-", with: "") 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/Intramodular/HTTP/Request/Multipart/HTTPRequest.Multipart.Content.Entity.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Foundation 6 | import Swift 7 | 8 | protocol HTTPRequestMultipartContentEntity: CustomStringConvertible { 9 | var headers: [HTTPRequest.Multipart.HeaderField] { get set } 10 | var body: Data { get } 11 | } 12 | 13 | // MARK: - Extensions 14 | 15 | extension HTTPRequestMultipartContentEntity { 16 | mutating func setAttribute( 17 | _ attribute: HTTPRequest.Multipart.HeaderField.Attribute?, 18 | named attributeName: String, 19 | for key: HTTPHeaderField.Key 20 | ) { 21 | let attributeName = HTTPRequest.Multipart.HeaderField.Attribute.Name(rawValue: attributeName) 22 | 23 | if let attribute = attribute { 24 | headers[key]?.attributes[attributeName] = attribute 25 | } else { 26 | headers[key]?.attributes.removeValue(forKey: attributeName) 27 | } 28 | } 29 | 30 | /// Sets a value for a header field. If a value was previously set for the given header, that value is replaced with the given value. 31 | mutating func setValue( 32 | _ value: String?, 33 | for key: HTTPHeaderField.Key 34 | ) { 35 | if let value = value { 36 | if headers[key] != nil { 37 | headers[key]?.value = value 38 | } else { 39 | headers.append(HTTPRequest.Multipart.HeaderField(name: key, value: value)) 40 | } 41 | } else { 42 | headers.remove(key) 43 | } 44 | } 45 | } 46 | 47 | // MARK: - Deprecated 48 | 49 | extension HTTPRequestMultipartContentEntity { 50 | /// Sets an attribute for a header field, like the "name" attribute for the Content-Disposition header. 51 | /// If the specified header is not defined for this entity, the attribute is ignored. 52 | /// If a value was previously set for the given attribute, that value is replaced with the given value. 53 | mutating func setAttribute( 54 | attribute: String, 55 | value: String?, 56 | for key: HTTPHeaderField.Key 57 | ) { 58 | if let value = value { 59 | headers[key]?.attributes[.init(rawValue: attribute)] = .init(value: value) 60 | } else { 61 | headers[key]?.attributes.removeValue(forKey: .init(rawValue: attribute)) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Sources/Intramodular/HTTP/Request/Multipart/HTTPRequest.Multipart.Content.Subtype.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Swift 6 | 7 | extension HTTPRequest.Multipart.Content { 8 | public enum Subtype: String { 9 | 10 | /// The "multipart/alternative" type defines each of the body parts as an pe"alternative" version of the same 11 | /// information. 12 | /// 13 | /// In general, user agents that compose "multipart/alternative" entities must place the body parts in 14 | /// increasing order of preference, that is, with the preferred format last. For fancy text, the sending user 15 | /// agent should put the plainest format first and the richest format last. 16 | /// - SeeAlso: Defined in [RFC 2046, Section 5.1.4](http://tools.ietf.org/html/rfc2046#section-5.1.4) 17 | case alternative = "multipart/alternative" 18 | 19 | /// The "multipart/byteranges" type is used to represent noncontiguous byte ranges of a single message. It is 20 | /// used by HTTP when a server returns multiple byte ranges. 21 | /// - SeeAlso: Defined in [RFC 2616](https://tools.ietf.org/html/rfc2616). 22 | case byteranges = "multipart/byteranges" 23 | 24 | /// The "multipart/digest" type changes the default Content-Type value for a body part from "text/plain" to 25 | /// "message/rfc822". This is done to allow a more readable digest format that is largely compatible (except for 26 | /// the quoting convention) with [RFC 934](https://tools.ietf.org/html/rfc934). 27 | /// 28 | /// - Note: Though it is possible to specify a Content-Type value for a body part in a digest which is other 29 | /// than "message/rfc822", such as a "text/plain" part containing a description of the material in the digest, 30 | /// actually doing so is undesireble. The "multipart/digest" Content-Type is intended to be used to send 31 | /// collections of messages. If a "text/plain" part is needed, it should be included as a seperate part of a 32 | /// "multipart/mixed" message. 33 | /// - SeeAlso: Defined in [RFC 2046, Section 5.1.5](http://tools.ietf.org/html/rfc2046#section-5.1.5) 34 | case digest = "multipart/digest" 35 | 36 | /// The "multipart/encrypted" content type must contain exactly two body parts. The first part has control 37 | /// information that is needed to decrypt the "application/octet-stream" second part. Similar to signed 38 | /// messages, there are different implementations which are identified by their separate content types for the 39 | /// control part. The most common types are "application/pgp-encrypted" and "application/pkcs7-mime". 40 | /// - SeeAlso: Defined in [RFC 1847, Section 2.2](http://tools.ietf.org/html/rfc1847#section-2.2) 41 | case encrypted = "multipart/encrypted" 42 | 43 | /// The "multipart/form-data" type can be used by a wide variety of applications and transported by a wide 44 | /// variety of protocols as a way of returning a set of values as the result of a user filling out a form. 45 | /// Originally defined as part of HTML 4.0, it is most commonly used for submitting files via HTTP. 46 | /// - SeeAlso: Defined in [RFC 7578](https://tools.ietf.org/html/rfc7578) 47 | case formData = "multipart/form-data" 48 | 49 | /// The "multipart/mixed" type is intended for use when the body parts are independent and need to be bundled in 50 | /// a particular order. Any "multipart" subtypes that an implementation does not recognize must be treated as 51 | /// being of subtype "mixed". 52 | /// 53 | /// It is commonly used for sending files with different "Content-Type" headers inline (or as attachments). 54 | /// If sending pictures or other easily readable files, most mail clients will display them inline (unless 55 | /// otherwise specified with the "Content-Disposition" header). Otherwise it will offer them as attachments. 56 | /// The default content-type for each part is "text/plain". 57 | /// - SeeAlso: Defined in [RFC 2046, Section 5.1.3](https://tools.ietf.org/html/rfc2046#section-5.1.3) 58 | case mixed = "multipart/mixed" 59 | 60 | /// The "multipart/x-mixed-replace" type was developed as part of a technology to emulate server push and 61 | /// streaming over HTTP. All parts of a mixed-replace message have the same semantic meaning. However, each part 62 | /// invalidates - "replaces" - the previous parts as soon as it is received completely. Clients should process 63 | /// the individual parts as soon as they arrive and should not wait for the whole message to finish. 64 | case mixedReplace = "multipart/x-mixed-replace" 65 | 66 | /// The "multipart/parallel" type indicates that the order of body parts is not significant. 67 | /// 68 | /// A common presentation of this type is to display all of the parts simultaneously on hardware and software 69 | /// that are capable of doing so. However, composing agents should be aware that many mail readers will lack 70 | /// this capability and will show the parts serially in any event. 71 | /// - SeeAlso: Defined in [RFC 2046, Section 5.1.6](https://tools.ietf.org/html/rfc2046#section-5.1.6) 72 | case parallel = "multipart/parallel" 73 | 74 | /// The "multipart/related" type provides a common mechanism for representing objects that are aggregates of 75 | /// related MIME body parts, where proper display cannot be achieved by individually displaying the constituent 76 | /// parts. 77 | /// 78 | /// The message consists of a root part (by default the first) which reference other parts inline, which may in 79 | /// turn reference other parts. Message parts are commonly referenced by the "Content-ID" part header. The 80 | /// syntax of a reference is unspecified and is instead dictated by the encoding or protocol used in the part. 81 | /// - SeeAlso: Defined in [RFC 2387](https://tools.ietf.org/html/rfc2387) 82 | case related = "multipart/related" 83 | 84 | /// The "multipart/report" type contains data formatted for a mail server to read. It is split between a 85 | /// human-readable message part and a machine-parsable body part containing an account of the reported message 86 | /// handling event. The purpose of this body part is to provide a machine-readable description of the 87 | /// condition(s) that caused the report to be generated, along with details not present in the first body part 88 | /// that might be useful to human experts. 89 | /// - SeeAlso: Defined in [RFC 2387](https://tools.ietf.org/html/rfc2387) 90 | case report = "multipart/report" 91 | 92 | /// The "multipart/signed" type contains exactly two body parts. The first body part is the body part over which 93 | /// the digital signature was created, including its MIME headers. The second body part contains the control 94 | /// information necessary to verify the digital signature. The first body part may contain any valid MIME 95 | /// content type, labeled accordingly. The second body part is labeled according to the value of the protocol 96 | /// parameter. 97 | /// - SeeAlso: Defined in [RFC 1847, Section 2.1](https://tools.ietf.org/html/rfc1847#section-2.1) 98 | case signed = "multipart/signed" 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Sources/Intramodular/HTTP/Request/Multipart/HTTPRequest.Multipart.Content.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Foundation 7 | import Swallow 8 | 9 | extension HTTPRequest.Multipart { 10 | public protocol ContentConvertible { 11 | func __conversion() throws -> HTTPRequest.Multipart.Content 12 | } 13 | 14 | /// Defines a message in which one or more different sets of data are combined according to the MIME standard. 15 | /// - SeeAlso: Defined in [RFC 2046, Section 5.1](https://tools.ietf.org/html/rfc2046#section-5.1) 16 | public struct Content: Initiable { 17 | static let CRLF = "\r\n" 18 | static let CRLFData = HTTPRequest.Multipart.Content.CRLF.data(using: .utf8)! 19 | 20 | /// A string that is optionally inserted before the first boundary delimiter. Can be used as an explanatory note for 21 | /// recipients who read the message with pre-MIME software, since such notes will be ignored by MIME-compliant software. 22 | public var preamble: String? = nil 23 | 24 | /// Message headers that apply to this body part. 25 | public var headers: [HTTPRequest.Multipart.HeaderField] = [] 26 | 27 | private let type: Subtype 28 | private let boundary = HTTPRequest.Multipart.Content.Boundary() 29 | private var entities: [HTTPRequestMultipartContentEntity] 30 | 31 | /// Creates and initializes a Multipart body with the given subtype. 32 | /// - Parameter type: The multipart subtype 33 | /// - Parameter parts: Array of body subparts to encapsulate 34 | private init( 35 | type: Subtype, 36 | parts: [HTTPRequestMultipartContentEntity] = [] 37 | ) { 38 | self.type = type 39 | self.entities = parts 40 | 41 | setValue("\(type.rawValue); boundary=\(self.boundary.stringValue)", for: .contentType) 42 | } 43 | 44 | /// Creates and initializes a Multipart body with the given subtype. 45 | /// - Parameter type: The multipart subtype 46 | /// - Parameter parts: Array of body subparts to encapsulate 47 | public init( 48 | type: Subtype, 49 | parts: [HTTPRequest.Multipart.Part] = [] 50 | ) { 51 | self.init(type: type, parts: parts.map({ $0 as HTTPRequestMultipartContentEntity })) 52 | } 53 | 54 | public init( 55 | _ parts: [HTTPRequest.Multipart.Part] 56 | ) { 57 | self.init(type: .formData, parts: parts) 58 | } 59 | 60 | public init() { 61 | self.init([HTTPRequest.Multipart.Part]()) 62 | } 63 | } 64 | } 65 | 66 | extension HTTPRequest.Multipart.Content { 67 | public mutating func append( 68 | _ element: HTTPRequest.Multipart.Part 69 | ) { 70 | _append(element) 71 | } 72 | 73 | private mutating func _append( 74 | _ element: any HTTPRequestMultipartContentEntity 75 | ) { 76 | entities.append(element) 77 | } 78 | 79 | public enum _PartContent { 80 | 81 | } 82 | } 83 | 84 | // MARK: - Conformances 85 | 86 | extension HTTPRequest.Multipart.Content: CustomStringConvertible { 87 | public var description: String { 88 | var result = self.headers.headerString() + HTTPRequest.Multipart.Content.CRLF 89 | 90 | if let preamble = self.preamble { 91 | result += String() 92 | + preamble 93 | + HTTPRequest.Multipart.Content.CRLF 94 | + HTTPRequest.Multipart.Content.CRLF 95 | } 96 | 97 | if entities.count > 0 { 98 | for entity in entities { 99 | result += String() 100 | + boundary.delimiter 101 | + HTTPRequest.Multipart.Content.CRLF 102 | + entity.description 103 | + HTTPRequest.Multipart.Content.CRLF 104 | } 105 | } else { 106 | result += String() 107 | + boundary.delimiter 108 | + HTTPRequest.Multipart.Content.CRLF 109 | + HTTPRequest.Multipart.Content.CRLF 110 | } 111 | 112 | result += self.boundary.distinguishedDelimiter 113 | 114 | return result 115 | } 116 | } 117 | 118 | extension HTTPRequest.Multipart.Content: HTTPRequestMultipartContentEntity { 119 | /// Complete message body, including boundaries and any nested multipart containers. 120 | public var body: Data { 121 | var data = Data() 122 | 123 | if let preamble = preamble?.data(using: .utf8) { 124 | data.append(preamble + HTTPRequest.Multipart.Content.CRLFData) 125 | data.append(HTTPRequest.Multipart.Content.CRLFData) 126 | } 127 | 128 | if entities.count > 0 { 129 | for entity in entities { 130 | data.append(boundary.delimiterData + HTTPRequest.Multipart.Content.CRLFData) 131 | 132 | if let headerData = entity.headers.headerData() { 133 | data.append(headerData) 134 | } 135 | 136 | data.append(HTTPRequest.Multipart.Content.CRLFData) 137 | data.append(entity.body + HTTPRequest.Multipart.Content.CRLFData) 138 | } 139 | } else { 140 | data.append(boundary.delimiterData) 141 | data.append(HTTPRequest.Multipart.Content.CRLFData) 142 | data.append(HTTPRequest.Multipart.Content.CRLFData) 143 | } 144 | 145 | data.append(boundary.distinguishedDelimiterData) 146 | 147 | return data 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /Sources/Intramodular/HTTP/Request/Multipart/HTTPRequest.Multipart.HeaderField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Foundation 6 | import Swift 7 | 8 | extension HTTPRequest.Multipart { 9 | /// A message header for use with multipart entities and subparts. 10 | public struct HeaderField { 11 | /// Header name such as "Content-Type". 12 | public let name: HTTPHeaderField.Key 13 | 14 | /// Header value, not including attributes. 15 | public var value: String 16 | 17 | /// Header attributes like "name" or "filename". 18 | public var attributes: [Attribute.Name: Attribute] 19 | 20 | public init( 21 | name: HTTPHeaderField.Key, 22 | value: String, 23 | attributes: [Attribute.Name: Attribute] = [:] 24 | ) { 25 | self.name = name 26 | self.value = value 27 | self.attributes = attributes 28 | } 29 | } 30 | } 31 | 32 | extension HTTPRequest.Multipart.HeaderField { 33 | public var headerValueIncludingAttributes: String { 34 | get { 35 | var strings = [value] 36 | 37 | for (name, attribute) in attributes { 38 | do { 39 | let attributeValue = try attribute.formattedValueForHeaderString() 40 | 41 | strings.append("\(name.rawValue)=\"\(attributeValue)\"") 42 | } catch { 43 | runtimeIssue(error) 44 | } 45 | } 46 | 47 | return strings.joined(separator: "; ") 48 | } 49 | } 50 | 51 | /// Return complete header including name, value and attributes. Does not include line break. 52 | public func headerKeyValueStringIncludingAttributes() -> String { 53 | "\(name.rawValue): \(headerValueIncludingAttributes)" 54 | } 55 | } 56 | 57 | // MARK: - Initializers 58 | 59 | extension HTTPRequest.Multipart.HeaderField { 60 | public init( 61 | name: HTTPHeaderField.Key, 62 | value: String, 63 | attributes: [String: String] 64 | ) { 65 | self.init( 66 | name: name, 67 | value: value, 68 | attributes: attributes.mapKeys({ Attribute.Name(rawValue: $0) }).mapValues({ Attribute(value: $0) }) 69 | ) 70 | } 71 | } 72 | 73 | // MARK: - Auxiliary 74 | 75 | extension HTTPRequest.Multipart.HeaderField { 76 | /// A type that represents an attribute value along with some options. 77 | public struct Attribute: Codable, Hashable, Sendable { 78 | public struct Name: Codable, Hashable, RawRepresentable, Sendable { 79 | public let rawValue: String 80 | 81 | public init(rawValue: String) { 82 | self.rawValue = rawValue 83 | } 84 | } 85 | 86 | public enum Option: Codable, Hashable, Sendable { 87 | case percentEncoded(allowedCharacters: CharacterSet) 88 | } 89 | 90 | public let value: String 91 | public var options: Set