电报 0.30.0

电报 0.30.0

测试已测试
语言语言 SwiftSwift
许可证 MIT
发布日期最新版本2022年8月
SPM支持 SPM

Yvo van BeekBernd de Graaf维护。



 
依赖
CocoaAsyncSocket~> 7.6
HTTPParserC~> 2.9
 

电报 0.30.0

  • Building42

Telegraph: Secure Web Server for iOS, tvOS and macOS

Telegraph CI CocoaPods compatible Carthage compatible License Platform

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 包管理器

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 框架和示例

  1. 克隆仓库
  2. 打开 Telegraph.xcworkspace
  3. 确保 Xcode 下载 Swift 包依赖
  4. 选择一个示例方案并构建

如果您想要对框架进行更改或尝试示例,则此步骤是必要的。

用法

配置App Transport Security

Apple在iOS 9中引入了App Transport Security (APS),旨在通过要求应用使用安全的HTTPS网络连接来提高用户的安全性和隐私性。这意味着,在不进行额外配置的情况下,针对iOS 9或更高版本的应用中的不安全HTTP请求将失败。在iOS 9中,APS不幸地也激活了对LAN连接。Apple在iOS 10中修复了这个问题,通过添加NSAllowsLocalNetworking

尽管我们使用HTTPS,但仍需考虑以下几点

  1. 当我们通过iPad进行通信时,我们很可能会在IP地址上连接,或者至少设备的计算机名不会与证书的通用名匹配。
  2. 我们的服务器将使用由我们自己的证书颁发机构签名的证书,而不是一个公认的根证书颁发机构的证书。

您可以通过向Info.plist中添加密钥App Transport Security Settings并设置子键Allow Arbitrary Loads来禁用APS。有关更多信息,请参阅ATS配置基础知识

准备证书

为了一个安全的网络服务器,你需要两样东西

  1. 一个或多个证书颁发机构的证书,格式为DER。
  2. 一个包含私钥和证书(由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.cnfconfig-localhost.cnf文件中的某些信息。该脚本生成的证书仅适用于开发目的。

为什么我的证书在iOS 13或macOS 10.15(或更高版本)上不起作用?

苹果在iOS 13和macOS 10.15中引入了新的安全要求。更多信息请参阅:https://support.apple.com/en-us/HT210176

我的服务器为何无法正常工作

请检查以下内容

  1. 应用程序传输安全已禁用吗?
  2. 您的证书有效吗?
  3. 您的路由有效吗?
  4. 您是否自定义了任何处理程序?路由处理程序仍然被包含吗?

查看本存储库中的示例项目以获得有效的起始点。

我的浏览器不显示我的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包装类来集成服务器。

作者

此库由以下人员创建

代码和设计受到了以下内容的启发

感谢贡献者,我们非常欢迎和赞赏您的pull请求!

许可证

Telegraph是在MIT许可下发布的。有关详细信息,请参阅LICENSE