Telegraph 是一个用 Swift 编写的适用于 iOS、tvOS 和 macOS 的安全 Web 服务器。
Swift Package Manager 是一个用于自动化分发 Swift 代码的工具。
您可以通过选择 File - Swift Packages - Add Package Dependency 选项将 Telegraph 添加到您的项目中。使用如下指定的存储库 URL,并选择您想要使用的版本。
或者,您可以手动将 Package.swift
文件添加到您的项目中,内容为:
dependencies: [
.package(url: "https://github.com/Building42/Telegraph.git")
]
CocoaPods 是 Cocoa 项目的依赖项管理器,它使依赖项成为您工作区的一部分。
pod 'Telegraph'
有关更多信息,请参见 CocoaPods - 入门。
您可以使用以下步骤构建 Telegraph 框架和示例
只有在您想更改框架或尝试示例时才需要这样做。
在 iOS 9 中,Apple 引入了 APS(App Transport Security),旨在通过要求应用程序使用 HTTPS 的安全网络连接来提高用户安全性和隐私。 这意味着,如果没有额外的配置,以 iOS 9 或更高版本为目标的应用程序中的不安全 HTTP 请求将失败。 不幸的是,在 iOS 9 中,APS 也为 LAN 连接激活。 Apple 在 iOS 10 中修复了此问题,添加了 NSAllowsLocalNetworking
。
即使我们正在使用 HTTPS,我们也必须考虑以下几点:
您可以通过将键 App Transport Security Settings
添加到您的 Info.plist 中来禁用 APS,其中的子键 Allow Arbitrary Loads
设置为 Yes
。 有关更多信息,请参见 ATS 配置基础。
对于安全 Web 服务器,您需要两件事:
Telegraph 包含一些类,可以更轻松地加载证书
let caCertificateURL = Bundle.main.url(forResource: "ca", withExtension: "der")!
let caCertificate = Certificate(derURL: caCertificateURL)!
let identityURL = Bundle.main.url(forResource: "localhost", withExtension: "p12")!
let identity = CertificateIdentity(p12URL: identityURL, passphrase: "test")!
注意:macOS 不接受没有密码的 P12 文件。
您很可能希望通过传入证书来创建一个安全服务器
serverHTTPs = Server(identity: identity, caCertificates: [caCertificate])
try! server.start(port: 9000)
或者,对于快速测试,创建一个不安全的服务器
serverHTTP = Server()
try! server.start(port: 9000)
您可以通过在启动服务器时指定一个接口来将服务器限制为 localhost 连接
try! server.start(port: 9000, interface: "localhost")
路由由三个部分组成:HTTP 方法、路径和一个处理程序
server.route(.POST, "test", handleTest)
server.route(.GET, "hello/:name", handleGreeting)
server.route(.GET, "secret/*") { .forbidden }
server.route(.GET, "status") { (.ok, "Server is running") }
server.serveBundle(.main, "/")
// You can also serve custom urls, for example the Demo folder in your bundle
let demoBundleURL = Bundle.main.url(forResource: "Demo", withExtension: nil)!
server.serveDirectory(demoBundleURL, "/demo")
路径开头的斜杠是可选的。 路由不区分大小写。 您可以为更高级的路由匹配指定自定义正则表达式。 如果没有任何路由匹配,服务器将返回 404 未找到。
上面的示例中的第一个路由有一个路由参数 (name)。 当服务器将传入的请求与该路由匹配时,它会将该参数放置在请求的 params
数组中。
func handleGreeting(request: HTTPRequest) -> HTTPResponse {
let name = request.params["name"] ?? "stranger"
return HTTPResponse(content: "Hello \(name.capitalized)")
}
当 HTTP 请求由服务器处理时,它会通过一系列消息处理程序。 如果您不更改默认配置,请求将首先传递到 HTTPWebSocketHandler
,然后再传递到 HTTPRouteHandler
。
以下是一个消息处理程序的示例
public class HTTPGETOnlyHandler: HTTPRequestHandler {
public func respond(to request: HTTPRequest, nextHandler: HTTPRequest.Handler) throws -> HTTPResponse? {
// If this is a GET request, pass it to the next handler
if request.method == .GET {
return try nextHandler(request)
}
// Otherwise return 403 - Forbidden
return HTTPResponse(.forbidden, content: "Only GET requests are allowed")
}
}
您可以通过在 HTTP 配置中设置消息处理程序来启用它
server.httpConfig.requestHandlers.insert(HTTPGETOnlyHandler(), at: 0)
请注意,请求处理程序的顺序非常重要。 您可能希望将 HTTPRouteHandler
作为最后一个请求处理程序,否则您的服务器将不处理任何路由请求。 HTTPRouteHandler
不调用任何处理程序,因此请不要在 HTTPRouteHandler
之后指定任何处理程序。
您还可以在处理程序中修改请求。 此处理程序将 QueryString 项复制到请求的 params
字典中
public class HTTPRequestParamsHandler: HTTPRequestHandler {
public func respond(to request: HTTPRequest, nextHandler: HTTPRequest.Handler) throws -> HTTPResponse? {
// Extract the query string items and put them in the HTTPRequest params
request.uri.queryItems?.forEach { item in
request.params[item.name] = item.value
}
// Continue with the rest of the handlers
return try nextHandler(request)
}
}
但是,如果您想向响应添加标头怎么办? 只需调用链并修改结果
public class HTTPAppDetailsHandler: HTTPRequestHandler {
public func respond(to request: HTTPRequest, nextHandler: HTTPRequest.Handler) throws -> HTTPResponse? {
// Let the other handlers create a response
let response = try nextHandler(request)
// Add our own bit of magic
response.headers["X-App-Version"] = "My App 1.0"
return response
}
}
CORS 机制控制哪些站点有权访问您服务器的资源。 您可以通过向客户端发送 Access-Control-Allow-Origin
标头来设置 CORS。 为了开发目的,允许所有具有 *
值的站点可能很有用。
response.headers.accessControlAllowOrigin = "*"
如果您想让它更漂亮,可以创建一个处理程序
public class HTTPCORSHandler: HTTPRequestHandler {
public func respond(to request: HTTPRequest, nextHandler: HTTPRequest.Handler) throws -> HTTPResponse? {
let response = try nextHandler(request)
// Add access control header for GET requests
if request.method == .GET {
response?.headers.accessControlAllowOrigin = "*"
}
return response
}
}
为了提高安全性,您可以添加其他检查,深入研究请求,并为不同的客户端发回不同的 CORS 标头。
对于客户端连接,我们将使用 Apple 的 URLSession 类。 Ray Wenderlich 有一个 优秀的教程。 我们将不得不手动验证 TLS 握手(需要禁用 App Transport Security)
let session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
let tlsPolicy = TLSPolicy(commonName: "localhost", certificates: [caCertificate])
extension YourClass: URLSessionDelegate {
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
// The TLSPolicy class will do most of the work for us
let credential = tlsPolicy.evaluateSession(trust: challenge.protectionSpace.serverTrust)
completionHandler(credential == nil ? .cancelAuthenticationChallenge : .useCredential, credential)
}
}
TLSPolicy
中的通用名称应与服务器证书的通用名称(在 P12 存档中提供)匹配。 尽管我不建议这样做,您可以通过提供一个空字符串来禁用通用名称检查(如果您提供 nil
,则通用名称将与设备的主机名进行比较)。
对于通用名称,您不限于设备的主机名或 IP 地址。 例如,您的后端可以生成一个通用名称与设备 UUID 匹配的证书。 如果客户端知道它连接到的设备的 UUID,您可以使其成为 TLSPolicy
检查的一部分。
由于默认情况下 HTTP 请求处理程序列表中包含 HTTPWebSocketHandler
,因此您的服务器将自动识别 WebSocket 请求。 设置 WebSocket 代理来处理传入的消息
server.webSocketDelegate = self
下一步是实现 ServerWebSocketDelegate
方法
func server(_ server: Server, webSocketDidConnect webSocket: WebSocket, handshake: HTTPRequest) {
// A web socket connected, you can extract additional information from the handshake request
webSocket.send(text: "Welcome!")
}
func server(_ server: Server, webSocketDidDisconnect webSocket: WebSocket, error: Error?) {
// One of our web sockets disconnected
}
func server(_ server: Server, webSocket: WebSocket, didReceiveMessage message: WebSocketMessage) {
// One of our web sockets sent us a message
}
func server(_ server: Server, webSocket: WebSocket, didSendMessage message: WebSocketMessage) {
// We sent one of our web sockets a message (often you won't need to implement this one)
}
Web socket 也可以有自定义处理程序,但您只需指定一个类,而不是整个处理程序链。 WebSocketMessageDefaultHandler
将响应连接关闭消息并处理 ping 消息。
我建议通过从默认处理程序继承来创建自定义处理程序
public class AwesomeWebSocketHandler: WebSocketMessageHandler {
public func incoming(message: WebSocketMessage, from webSocket: WebSocket) throws {
// Don't forget to call super (for ping-pong etc.)
super.incoming(message: message, from: webSocket)
// Echo incoming text messages
switch message.payload {
case let .text(text): webSocket.send(text: text)
default: break
}
}
}
现在我们有了一个安全的 WebSocket 服务器,我们也可以通过传递 CA 证书来使用安全的 WebSocket 客户端。 请注意,如果证书不是 Apple 信任的根 CA,或者您想从 证书绑定 中受益,您才需要指定 CA 证书。
client = try! WebSocketClient("wss://:9000", certificates: [caCertificate])
client.delegate = self
// You can specify headers too
client.headers.authorization = "Bearer secret-token"
代理方法如下所示
func webSocketClient(_ client: WebSocketClient, didConnectToHost host: String) {
// The connection starts off as a HTTP request and then is upgraded to a
// web socket connection. This method is called if the handshake was succesful.
}
func webSocketClient(_ client: WebSocketClient, didDisconnectWithError error: Error?) {
// We were disconnected from the server
}
func webSocketClient(_ client: WebSocketClient, didReceiveData data: Data) {
// We received a binary message. Ping, pong and the other opcodes are handled for us.
}
func webSocketClient(_ client: WebSocketClient, didReceiveText text: String) {
// We received a text message, let's send one back
client.send(text: "Message received")
}
iOS 只有少数可用的 Web 服务器,其中许多服务器没有 SSL 支持。 我们使用 Telegraph 的主要目标是在 iPad 之间提供安全的 HTTP 和 Web Socket 流量。 这个名字是对电报的致敬,电报是第一种形式的电气电信。
基本上,你想要的是一个证书颁发机构和一个由该机构签名的设备证书。 你可以在这里找到一个不错的教程:https://jamielinux.com/docs/openssl-certificate-authority/
请查看 Tools 文件夹,其中包含一个可以为您创建自签名证书的脚本。 该脚本非常基础,您可能需要编辑 config-ca.cnf
和 config-localhost.cnf
文件中的一些信息。 使用该脚本生成的证书仅用于开发目的。
Apple 在 iOS 13 和 macOS 10.15 中引入了新的安全要求。 有关更多信息,请参见:https://support.apple.com/en-us/HT210176
请检查以下内容:
请查看此存储库中的示例项目,以获取可用的起点。
在构建项目的过程中,Apple 会尝试优化您的图像以减少捆绑包的大小。 这种优化过程有时会导致 Chrome 无法读取图像。 在 Safari 中测试您的图像 URL 以仔细检查是否是这种情况。
要解决此问题,您可以转到 Xcode 中的属性检查器,并将资源的类型从 Default - PNG image
更改为 Data
。 之后,构建过程将不会优化您的文件。 我还在示例项目中使用 logo.png
完成了此操作。
如果您想减少图像的大小,我强烈建议使用 ImageOptim。
前 1024 个端口号仅限于 root 访问,您的应用程序在设备上没有 root 访问权限。 如果您尝试在这些端口上打开一个服务器,则在启动服务器时会收到权限被拒绝错误。 有关更多信息,请阅读 为什么前 1024 个端口仅限于 root 用户。
如果您的应用被发送到后台,或者设备进入待机状态,您通常有大约 3 分钟的时间来处理请求和关闭连接。 您可以使用 UIApplication.beginBackgroundTask
创建一个后台任务,以告知 iOS 您需要额外的时间来完成操作。 属性 UIApplication.shared.backgroundTimeRemaining
告诉您到强制终止您的应用程序之前剩余的时间。
是否曾经想知道远程服务器如何知道您的浏览器与 HTTP/2 兼容? 在 TLS 协商期间,应用层协议协商 (ALPN) 扩展字段包含 "h2" 以表示将使用 HTTP/2。 Apple 在 Secure Transport 或 CFNetwork 中不提供任何(公共)方法来配置 ALPN 扩展。 因此,目前无法实现安全的 HTTP/2 iOS 实现。
此库是用 Swift 编写的,并且出于性能原因,除非绝对必要(没有动态调度),否则我没有用 NSObject
修饰类。 如果您愿意将 Swift 代码添加到您的项目中,您可以通过添加从 NSObject
继承并具有 Telegraph Server
变量的 Swift 包装器类来集成服务器。
这个库是由
代码和设计灵感来自
感谢各位贡献者,我们非常欢迎并感谢您的 Pull Request!
Telegraph 使用 MIT 许可证发布。详情请参阅 LICENSE。