A lightweight and powerful iOS framework for intercepting HTTP/HTTPS Traffic from your app. No more messing around with proxy, certificate config.
功能
- 自动 无需配置即可轻松拦截所有 HTTP/HTTPS 流量
- 从 URLSessionWebSocketTask 捕获 WS/WSS 流量
- 捕获 gRPC 流量
-
✅ 无需配置 HTTP 代理或安装和信任任何证书 - 支持 iOS 真机和模拟器
- 从 macOS Proxyman 应用程序(GitHub)审阅流量日志
- 按项目和设备对日志进行分类。
- 仅适用于流量检查器,不适用于调试工具
- 适用于生产环境
如何使用
- 通过 CocoaPod 或 SPM 安装 Atlantis,然后开始 Atlantis
默认情况下,Bonjour 服务将尝试连接同一网络中的所有 Proxyman 应用程序
- 如果您只有一个具有 Proxyman 的 MacOS 机器。请使用简单版本
#if DEBUG
import Atlantis
#endif
// Add to the end of `application(_:didFinishLaunchingWithOptions:)` in AppDelegate.swift or SceneDelegate.swift
#if DEBUG
Atlantis.start()
#endif
- 如果同事的 Mac 机器上有许多 Proxyman 应用程序,并且您想 Atlantis 连接到您的 macOS 桌面,请使用
Atlantis.start(hostName:)
版本
#if DEBUG
import Atlantis
#endif
// Add to the end of `application(_:didFinishLaunchingWithOptions:)` in AppDelegate.swift or SceneDelegate.swift
#if DEBUG
Atlantis.start(hostName: "_your_host_name")
#endif
您可以从 Proxyman -> 证书菜单 -> 为 iOS 安装 -> Atlantis -> 如何启动 Atlantis -> 复制 主机名
- 如果您的项目使用 Objective-C,请使用 CocoaPod 安装 Atlantis(通过 SPM 安装可能不起作用)。
#import "Atlantis-Swift.h"
// Or import Atlantis as a module, you can use:
@import Atlantis;
// Add to the end of `application(_:didFinishLaunchingWithOptions:)` in AppDelegate
[Atlantis startWithHostName:nil];
- 请确保您的 iOS 设备/模拟器和 macOS Proxyman 在同一 WiFi 网络 中,或者通过 USB 线缆 将您的 iOS 设备连接到 Mac
- 打开 macOS Proxyman(或在此处下载最新版)(GitHub)(2.11.0+)
- 打开您的iOS应用,并从Proxyman应用中检查流量日志
- 享受调试之旅
❤️
需求
- macOS Proxyman应用 2.11.0+
- iOS 13.0+ / macOS 10.15+ / Mac Catalyst 13.0+
- Xcode 11+
- Swift 5.0+
为iOS 14+配置的必需设置
从iOS 14开始,需要将NSLocalNetworkUsageDescription
和NSBonjourServices
添加到您的info.plist文件中
- 打开Info.plist文件并添加以下键和值
<key>NSLocalNetworkUsageDescription</key>
<string>Atlantis would use Bonjour Service to discover Proxyman app from your local network.</string>
<key>NSBonjourServices</key>
<array>
<string>_Proxyman._tcp</string>
</array>
安装
CocoaPods
- 在您的Podfile中添加以下行
pod 'atlantis-proxyman'
Swift Packages管理器
- 通过以下步骤将
https://github.com/ProxymanApp/atlantis
添加到您的项目中:打开 Xcode -> 文件菜单 -> Swift 包 -> 添加包依赖...
Carthage
- 将其添加到 Cartfile
github "ProxymanApp/atlantis"
- 运行
carthage update --use-xcframeworks
- 将 Atlantis.framework 从您的项目中拖拽出来
- 按照 Carthage 指南 创建 Carthage 脚本
对于 Xcode 12 的 Carthage,请查看以下解决方案:https://github.com/Carthage/Carthage/blob/master/Documentation/Xcode12Workaround.md
WS/WSS 流量
从 Atlantis 1.9.0+ 开始,Atlantis 能够捕获所有 WS/WSS 流量,这些流量是通过 URLSessionWebSocketTask 生成的,并发送到 Proxyman 应用。您无需进行任何配置,它默认即可工作。
运行示例应用
Atlantis 提供了一个简单的示例,展示如何集成和复用 Atlantis 与 Proxyman。请按照以下步骤操作
- 打开 macOS 上的 Proxyman
- 在
Example/Atlantis-Example-App.xcodeproj
中打开 iOS 项目 - 使用任意 iPhone/iPad 模拟器启动项目
- 点击主屏幕上的按钮
- 返回 Proxyman 应用并检查您的 HTTPS 请求/响应。
高级用法
默认情况下,如果您的iOS应用程序使用Apple的Networking类(例如URLSession)或使用受流行的Networking库(例如Alamofire和AFNetworking)来发起HTTP请求,Atlantis将 无需额外设置即可正常工作。
然而,如果您的应用程序不使用这些中的任何一个,Atlantis将无法自动捕获网络流量。
为了解决这个问题,Atlantis提供了一些功能,可以帮助您 手动 添加您的请求和响应,它们将在Proxyman应用程序上以常规方式呈现。
1. 我的应用程序使用C++网络库且不使用URLSession、NSURLSession或任何iOS网络库
您可以从以下函数构建Atlantis的请求和响应:
/// Handy func to manually add Atlantis' Request & Response, then sending to Proxyman for inspecting
/// It's useful if your Request & Response are not URLRequest and URLResponse
/// - Parameters:
/// - request: Atlantis' request model
/// - response: Atlantis' response model
/// - responseBody: The body data of the response
public class func add(request: Request,
response: Response,
responseBody: Data?) {
- 示例
@IBAction func getManualBtnOnClick(_ sender: Any) {
// Init Request and Response
let header = Header(key: "X-Data", value: "Atlantis")
let jsonType = Header(key: "Content-Type", value: "application/json")
let jsonObj: [String: Any] = ["country": "Singapore"]
let data = try! JSONSerialization.data(withJSONObject: jsonObj, options: [])
let request = Request(url: "https://proxyman.io/get/data", method: "GET", headers: [header, jsonType], body: data)
let response = Response(statusCode: 200, headers: [Header(key: "X-Response", value: "Internal Error server"), jsonType])
let responseObj: [String: Any] = ["error_response": "Not FOund"]
let responseData = try! JSONSerialization.data(withJSONObject: responseObj, options: [])
// Add to Atlantis and show it on Proxyman app
Atlantis.add(request: request, response: response, responseBody: responseData)
}
2. 我的应用程序使用GRPC
您可以使用grpc-swift提供的拦截器模式从GRPC模型构建归一化请求和响应,并利用它来获取完整的调用日志。
以下是一个用于AtlantisInterceptor的示例:
import Atlantis
import Foundation
import GRPC
import NIO
import NIOHPACK
import SwiftProtobuf
extension HPACKHeaders {
var atlantisHeaders: [Header] { map { Header(key: $0.name, value: $0.value) } }
}
public class AtlantisInterceptor<Request: Message, Response: Message>: ClientInterceptor<Request, Response> {
private struct LogEntry {
let id = UUID()
var path: String = ""
var started: Date?
var request: LogRequest = .init()
var response: LogResponse = .init()
}
private struct LogRequest {
var metadata: [Header] = []
var messages: [String] = []
var ended = false
}
private struct LogResponse {
var metadata: [Header] = []
var messages: [String] = []
var end: (status: GRPCStatus, metadata: String)?
}
private var logEntry = LogEntry()
override public func send(_ part: GRPCClientRequestPart<Request>,
promise: EventLoopPromise<Void>?,
context: ClientInterceptorContext<Request, Response>)
{
logEntry.path = context.path
if logEntry.started == nil {
logEntry.started = Date()
}
switch context.type {
case .clientStreaming, .serverStreaming, .bidirectionalStreaming:
streamingSend(part, type: context.type)
case .unary:
unarySend(part)
}
super.send(part, promise: promise, context: context)
}
private func streamingSend(_ part: GRPCClientRequestPart<Request>, type: GRPCCallType) {
switch part {
case .metadata(let metadata):
logEntry.request.metadata = metadata.atlantisHeaders
case .message(let messageRequest, _):
Atlantis.addGRPCStreaming(id: logEntry.id,
path: logEntry.path,
message: .data((try? messageRequest.jsonUTF8Data()) ?? Data()),
success: true,
statusCode: 0,
statusMessage: nil,
streamingType: type.streamingType,
type: .send,
startedAt: logEntry.started,
endedAt: Date(),
HPACKHeadersRequest: logEntry.request.metadata,
HPACKHeadersResponse: logEntry.response.metadata)
case .end:
logEntry.request.ended = true
switch type {
case .unary, .serverStreaming, .bidirectionalStreaming:
break
case .clientStreaming:
Atlantis.addGRPCStreaming(id: logEntry.id,
path: logEntry.path,
message: .string("end"),
success: true,
statusCode: 0,
statusMessage: nil,
streamingType: type.streamingType,
type: .send,
startedAt: logEntry.started,
endedAt: Date(),
HPACKHeadersRequest: logEntry.request.metadata,
HPACKHeadersResponse: logEntry.response.metadata)
}
}
}
private func unarySend(_ part: GRPCClientRequestPart<Request>) {
switch part {
case .metadata(let metadata):
logEntry.request.metadata = metadata.atlantisHeaders
case .message(let messageRequest, _):
logEntry.request.messages.append((try? messageRequest.jsonUTF8Data())?.prettyJson ?? "")
case .end:
logEntry.request.ended = true
}
}
override public func receive(_ part: GRPCClientResponsePart<Response>, context: ClientInterceptorContext<Request, Response>) {
logEntry.path = context.path
switch context.type {
case .unary:
unaryReceive(part)
case .bidirectionalStreaming, .serverStreaming, .clientStreaming:
streamingReceive(part, type: context.type)
}
super.receive(part, context: context)
}
private func streamingReceive(_ part: GRPCClientResponsePart<Response>, type: GRPCCallType) {
switch part {
case .metadata(let metadata):
logEntry.response.metadata = metadata.atlantisHeaders
case .message(let messageResponse):
Atlantis.addGRPCStreaming(id: logEntry.id,
path: logEntry.path,
message: .data((try? messageResponse.jsonUTF8Data()) ?? Data()),
success: true,
statusCode: 0,
statusMessage: nil,
streamingType: type.streamingType,
type: .receive,
startedAt: logEntry.started,
endedAt: Date(),
HPACKHeadersRequest: logEntry.request.metadata,
HPACKHeadersResponse: logEntry.response.metadata)
case .end(let status, _):
Atlantis.addGRPCStreaming(id: logEntry.id,
path: logEntry.path,
message: .string("end"),
success: status.isOk,
statusCode: status.code.rawValue,
statusMessage: status.message,
streamingType: type.streamingType,
type: .receive,
startedAt: logEntry.started,
endedAt: Date(),
HPACKHeadersRequest: logEntry.request.metadata,
HPACKHeadersResponse: logEntry.response.metadata)
}
}
private func unaryReceive(_ part: GRPCClientResponsePart<Response>) {
switch part {
case .metadata(let metadata):
logEntry.response.metadata = metadata.atlantisHeaders
case .message(let messageResponse):
logEntry.response.messages.append((try? messageResponse.jsonUTF8Data())?.prettyJson ?? "")
case .end(let status, _):
Atlantis.addGRPCUnary(path: logEntry.path,
requestObject: logEntry.request.messages.joined(separator: "\n").data(using: .utf8),
responseObject: logEntry.response.messages.joined(separator: "\n").data(using: .utf8),
success: status.isOk,
statusCode: status.code.rawValue,
statusMessage: status.message,
startedAt: logEntry.started,
endedAt: Date(),
HPACKHeadersRequest: logEntry.request.metadata,
HPACKHeadersResponse: logEntry.response.metadata)
}
}
override public func errorCaught(_ error: Error, context: ClientInterceptorContext<Request, Response>) {
logEntry.path = context.path
switch context.type {
case .unary, .bidirectionalStreaming, .serverStreaming, .clientStreaming:
Atlantis.addGRPCUnary(path: logEntry.path,
requestObject: logEntry.request.messages.joined(separator: "\n").data(using: .utf8),
responseObject: logEntry.response.messages.joined(separator: "\n").data(using: .utf8),
success: false,
statusCode: GRPCStatus(code: .unknown, message: "").code.rawValue,
statusMessage: error.localizedDescription,
startedAt: logEntry.started,
endedAt: Date(),
HPACKHeadersRequest: logEntry.request.metadata,
HPACKHeadersResponse: logEntry.response.metadata)
}
super.errorCaught(error, context: context)
}
override public func cancel(promise: EventLoopPromise<Void>?, context: ClientInterceptorContext<Request, Response>) {
logEntry.path = context.path
switch context.type {
case .unary, .bidirectionalStreaming, .serverStreaming, .clientStreaming:
Atlantis.addGRPCUnary(path: logEntry.path,
requestObject: logEntry.request.messages.joined(separator: "\n").data(using: .utf8),
responseObject: logEntry.response.messages.joined(separator: "\n").data(using: .utf8),
success: false,
statusCode: GRPCStatus(code: .cancelled, message: nil).code.rawValue,
statusMessage: "canceled",
startedAt: logEntry.started,
endedAt: Date(),
HPACKHeadersRequest: logEntry.request.metadata,
HPACKHeadersResponse: logEntry.response.metadata)
}
super.cancel(promise: promise, context: context)
}
}
extension GRPCCallType {
var streamingType: Atlantis.GRPCStreamingType {
switch self {
case .clientStreaming:
return .client
case .serverStreaming:
return .server
case .bidirectionalStreaming:
return .server
case .unary:
fatalError("Unary is not a streaming type")
}
}
}
private extension Data {
var prettyJson: String? {
guard let object = try? JSONSerialization.jsonObject(with: self),
let data = try? JSONSerialization.data(withJSONObject: object, options: [.prettyPrinted]),
let prettyPrintedString = String(data: data, encoding: .utf8) else {
return nil
}
return prettyPrintedString
}
}
- 示例
public class YourInterceptorFactory: YourClientInterceptorFactoryProtocol {
func makeGetYourCallInterceptors() -> [ClientInterceptor<YourRequest, YourResponse>] {
[AtlantisInterceptor()]
}
}
// Your GRPC services that is generated from SwiftGRPC
private let client = NoteServiceServiceClient.init(channel: connectionChannel, interceptors: YourInterceptorFactory())
3. 在Swift Playground上使用Atlantis
Atlantis能够捕获来自Swift Playground的HTTP/HTTPS和WS/WSS流量。
- 使用 Arena 生成包含Atlantis的新Swift Playground。如果您希望将Atlantis添加到现有Swift Playground,请遵循此教程 在这里。
- 启用Swift Playground模式
Atlantis.setIsRunningOniOSPlayground(true)
Atlantis.start()
- 信任Proxyman自签名证书
- 对于macOS:如果您已经通过“证书”菜单“>安装在此Mac上”安装并信任了Proxyman证书,则无需采取任何操作。
- 对于iOS: 由于iOS Playgrounds没有启动任何iOS模拟器,因此无法注入Proxyman证书。因此,我们必须手动信任证书。请使用NetworkSSLProxying类来完成此操作。
- 建立一个HTTP/HTTPS或WS/WSS连接,并在Proxyman应用中检查它。
常见问题解答
1. Atlantis是如何工作的?
Atlantis使用方法交换技术来交换NSURLSession的某些函数,这允许Atlantis能够即时捕获HTTP/HTTPS流量。
然后将其发送到Proxyman应用以供后续检查。
2. Atlantis如何将数据流式传输到Proxyman应用?
一旦您的iOS应用(Atlantis已启用)和Proxyman macOS应用处于相同的本地网络中,Atlantis就可以使用Bonjour服务发现Proxyman应用。一旦建立连接,Atlantis将通过Socket发送数据。
3. 将我的网络流量日志发送到Proxyman应用是否安全?
由于您的数据是在您的iOS应用和Proxyman应用之间本地传输的,不需要互联网,因此这是完全安全的。所有流量日志都会被捕获并发送到Proxyman应用以供即时检查。
“大西洋”和“代理管理器”应用程序不会在任何服务器上存储您的任何数据。
4. “大西洋”捕获了哪些数据?
- 您的iOS应用程序中集成“大西洋”框架的所有HTTP/HTTPS流量
- 您的iOS应用程序名称、bundle标识符和很小的徽标
- iPhone设备/模拟器名称和设备型号。
所有上述数据都不会存储在任何地方(除了内存)。它们将在您关闭应用程序后立即被清除。
这是为了根据项目名称和设备名称对代理管理器应用程序中的流量进行分类。因此,更容易知道请求/响应是从哪里来的。
故障排除
1. 为什么在Proxyman应用程序上看不到来自“大西洋”的任何请求?
由于某种原因,Bonjour服务可能无法找到Proxyman应用程序。
=> 确保您的iOS设备和Mac在相同的Wi-Fi网络上或通过USB线连接到Mac。
=> 请使用Atlantis.start(hostName: "_your_host_name")
版本显式告诉“大西洋”连接到您的Mac。
2. 我为什么不能在 Atlantis 的请求中使用调试工具?
Atlantis 是为了检查网络而构建的,而不是用于调试。如果您想使用调试工具,请考虑使用常规 HTTP 代理。
致谢
- FLEX 和维护团队:https://github.com/FLEXTool/FLEX
- @yagiz from Bagel 项目:https://github.com/yagiz/Bagel
许可证
Atlantis 是在 Apache-2.0 许可证下发布的。有关详细信息,请参阅 LICENSE。