Telegraph 是一个用 Swift 编写的安全 Web 服务器,适用于 iOS、tvOS 和 macOS。
特性
- 处理 HTTP 1.0/1.1 请求
- 安全流量,HTTPS/TLS 加密
- WebSocket 客户端和服务器
- 使用经过良好测试的套接字库 CocoaAsyncSocket
- 使用高效、低内存的 HTTP 解析器 C 库 http-parser
- 可定制,从超时到消息处理程序
- 简单,有良好注释的代码
平台
- iOS 9.0+
- tvOS 9.0+
- macOS 10.10+
版本
- Swift 5.x: main 分支
- Swift 4.2: swift-4.2 分支
- Swift 4.0: swift-4 分支
- Swift 3.0: swift-3 分支
安装
Swift 包管理器
Swift 包管理器(Swift Package Manager)是一个用于自动化 Swift 代码分发的工具。
Xcode 11 及以后的版本已集成 Swift 包管理器的支持。您可以通过选择“文件” - “Swift 包” - “添加包依赖”选项将 Telegraph 添加到您的项目中。使用以下指定的仓库 URL,并选择您想要使用的版本。
或者,您可以手动将一个 Package.swift
文件添加到您的项目中。
dependencies: [
.package(url: "https://github.com/Building42/Telegraph.git")
]
Carthage
Carthage 是一个去中心化的依赖管理器,它会将您的依赖项构建成二进制框架。
github "Building42/Telegraph"
有关更多信息,请参阅 Carthage - 快速入门。
CocoaPods
CocoaPods 是一个 Cocoa 项目的依赖管理器,使依赖项成为您工作空间的一部分。
source 'https://cdn.cocoapods.org/'
use_frameworks!
target '<Your Target Name>'
pod 'Telegraph'
有关更多信息,请参阅 CocoaPods - 入门。
构建
您可以使用以下步骤构建 Telegraph 框架和示例
- 克隆仓库
- 打开 Telegraph.xcworkspace
- 确保 Xcode 下载 Swift 包依赖
- 选择一个示例方案并构建
如果您想要对框架进行更改或尝试示例,则此步骤是必要的。
用法
配置App Transport Security
Apple在iOS 9中引入了App Transport Security (APS),旨在通过要求应用使用安全的HTTPS网络连接来提高用户的安全性和隐私性。这意味着,在不进行额外配置的情况下,针对iOS 9或更高版本的应用中的不安全HTTP请求将失败。在iOS 9中,APS不幸地也激活了对LAN连接。Apple在iOS 10中修复了这个问题,通过添加NSAllowsLocalNetworking
。
尽管我们使用HTTPS,但仍需考虑以下几点
- 当我们通过iPad进行通信时,我们很可能会在IP地址上连接,或者至少设备的计算机名不会与证书的通用名匹配。
- 我们的服务器将使用由我们自己的证书颁发机构签名的证书,而不是一个公认的根证书颁发机构的证书。
您可以通过向Info.plist中添加密钥App Transport Security Settings
并设置子键Allow Arbitrary Loads
为是
来禁用APS。有关更多信息,请参阅ATS配置基础知识。
准备证书
为了一个安全的网络服务器,你需要两样东西
- 一个或多个证书颁发机构的证书,格式为DER。
- 一个包含私钥和证书(由CA签名)的PKCS12包。
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文件。
HTTP服务器
您可能希望通过传递证书来创建一个安全服务器
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:路由
路由由三部分组成: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未找到。
上述示例中的第一个路由有一个路由参数(名称)。当服务器将该路由与传入的请求匹配时,它将参数放置在请求的params
数组中
func handleGreeting(request: HTTPRequest) -> HTTPResponse {
let name = request.params["name"] ?? "stranger"
return HTTPResponse(content: "Hello \(name.capitalized)")
}
HTTP:中间件
当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
之后不要指定任何处理器。
您还可以修改处理器中的请求。此处理器将查询字符串项复制到请求的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
}
}
HTTP:跨源资源共享(CORS)
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标题。
HTTP: 客户端
对于客户端连接,我们将使用苹果的 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
检查的一部分。
WebSockets: 服务器端
由于默认情况下 HTTPWebSocketHandler
已包含在 HTTP 请求处理程序列表中,因此您的服务器将自动识别 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)
}
WebSockets: 中间件
WebSocket 还可以有自定义处理程序,尽管您需要指定单个类而不是整个处理程序链。默认情况下,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
}
}
}
WebSockets: 客户端
现在我们有了安全的 WebSocket 服务器,我们可以通过传递 CA 证书来使用安全的 WebSocket 客户端。请注意,只有在证书不是苹果信任的根 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/
在“工具”文件夹中有一个脚本,可以为您创建自签名的证书。该脚本非常基础,您可能需要编辑config-ca.cnf
和config-localhost.cnf
文件中的某些信息。该脚本生成的证书仅适用于开发目的。
为什么我的证书在iOS 13或macOS 10.15(或更高版本)上不起作用?
苹果在iOS 13和macOS 10.15中引入了新的安全要求。更多信息请参阅:https://support.apple.com/en-us/HT210176
我的服务器为何无法正常工作
请检查以下内容
- 应用程序传输安全已禁用吗?
- 您的证书有效吗?
- 您的路由有效吗?
- 您是否自定义了任何处理程序?路由处理程序仍然被包含吗?
查看本存储库中的示例项目以获得有效的起始点。
我的浏览器不显示我的bundle中的图片
在构建您的项目时,苹果公司试图优化您的图片以减小bundle的大小。此优化过程有时会导致Chrome无法读取图片。您可以通过在Safari中测试您的图片url来检查是否属于此类情况。
要解决这个问题,您可以在Xcode中的属性检查器中将资源的类型从“默认 - PNG图像”更改为“数据”。之后,构建过程将不会优化您的文件。我还在示例项目中用logo.png
做了这件事。
如果您想减小图片的大小,我强烈推荐使用ImageOptim。
为什么我不能使用端口80和443?
前1024个端口号仅限于root访问,您的应用程序在设备上没有root访问权限。如果您尝试在这些端口上打开服务器,当您启动服务器时会收到权限拒绝错误。有关更多信息,请阅读为什么前1024个端口仅限于root用户。
如果设备进入待机状态怎么办?
如果您的应用程序被发送到后台或设备进入待机状态,通常您有大约3分钟的时间来处理请求和关闭连接。您可以使用UIApplication.beginBackgroundTask
创建一个后台任务,让iOS知道您需要额外的时间来完成您的操作。属性UIApplication.shared.backgroundTimeRemaining
告诉您在可能强制销毁应用程序之前剩余的时间。
关于HTTP/2支持的问题?
你是否曾想过远程服务器如何知道您的浏览器是否兼容HTTP/2?在TLS协商期间,应用层协议协商(ALPN)扩展字段包含“h2”,表示将使用HTTP/2。苹果没有在Secure Transport或CFNetwork中提供任何(公共)配置ALPN扩展的方法。因此,目前无法实现安全的HTTP/2 iOS实现。
我可以在我的Objective-C项目中使用这个吗?
这个库是用Swift编写的,出于性能考虑,除非绝对必要(没有动态分发),我没有为类添加NSObject
装饰。如果您愿意将Swift代码添加到项目中,您可以通过添加一个继承自NSObject
并包含Telegraph Server
变量的Swift包装类来集成服务器。
作者
此库由以下人员创建
代码和设计受到了以下内容的启发
- CocoaHTTPServer - 用Objective-C编写的Web服务器
- Vapor - 一款Swift Web框架
感谢贡献者,我们非常欢迎和赞赏您的pull请求!
许可证
Telegraph是在MIT许可下发布的。有关详细信息,请参阅LICENSE。