https://github.com/ProxymanApp/atlantis
添加到您的项目pod 'atlantis-proxyman'
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>
AppDelegate.swift
#if DEBUG
import Atlantis
#endif
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Auto connect to a current Macbook
// Add to the end of `application(_:didFinishLaunchingWithOptions:)` in AppDelegate.swift or SceneDelegate.swift
#if DEBUG
Atlantis.start()
#endif
return true
}
Atlantis.start(hostName:)
版本#if DEBUG
import Atlantis
#endif
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Auto connect to a current Macbook
// Add to the end of `application(_:didFinishLaunchingWithOptions:)` in AppDelegate.swift or SceneDelegate.swift
#if DEBUG
Atlantis.start(hostName: "Your_host_name")
#endif
return true
}
您可以获取 hostName
: 打开 Proxyman macOS -> 证书菜单 -> 为 iOS 安装 -> Atlantis -> 如何启动 Atlantis -> 并复制 HostName
#import "Atlantis-Swift.h"
// Or import Atlantis as a module, you can use:
@import Atlantis;
// Add to the end of `application(_:didFinishLaunchingWithOptions:)` in AppDelegate.m file
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
[Atlantis startWithHostName:nil shouldCaptureWebSocketTraffic:YES];
return YES;
}
URLSessionWebsocketTask
捕获 Websocket。URLSessionWebsocketTask
。Atlantis 提供了一个简单的 iOS 应用程序,可以演示如何集成和使用 Atlantis 和 Proxyman。 请按照以下步骤操作
./Example/Atlantis-Example-App.xcodeproj
打开 iOS 项目默认情况下,如果您的 iOS 应用程序使用 Apple 的网络类(例如 URLSession)或使用流行的网络库(例如 Alamofire 和 AFNetworking)发出 HTTP 请求,Atlantis 将 开箱即用。
但是,如果您的应用程序未使用其中任何一个,则 Atlantis 无法自动捕获网络流量。
为了解决这个问题,Atlantis 提供了一些函数来帮助您 手动* 添加您的 Request 和 Response,这些 Request 和 Response 将像往常一样在 Proxyman 应用程序上显示。
您可以从以下 func 为 Atlantis 构建 Request 和 Response
/// 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.com/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)
}
您可以通过 grpc-swift 提供的拦截器模式从 GRPC 模型构建一元 Request 和 Response,并利用它来获取您调用的完整日志。
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())
Atlantis 能够捕获来自您的 Swift Playground 的 HTTP/HTTPS 和 WS/WSS 流量。
Atlantis.setIsRunningOniOSPlayground(true)
Atlantis.start()
Atlantis 使用 Method Swizzling 技术来交换 NSURLSession 的某些功能,这使 Atlantis 能够动态捕获 HTTP/HTTPS 流量。
然后它通过本地 Bonjour 服务发送到 Proxyman app 以进行检查。
一旦您的 iOS 应用程序(启用 Atlantis)和 Proxyman macOS 应用程序位于相同的本地网络中,Atlantis 就可以使用 Bonjour Service 发现 Proxyman 应用程序。 建立连接后,Atlantis 将通过 Socket 发送数据。
这是完全安全的,因为您的数据是在您的 iOS 应用程序和 Proxyman 应用程序之间本地传输的,不需要 Internet。 所有流量日志都会被捕获并发送到 Proxyman 应用程序以进行动态检查。
Atlantis 和 Proxyman 应用程序不会将您的任何数据存储在任何服务器上。
以上所有数据均未存储在任何地方(除了内存中)。 一旦您关闭应用程序,它将被清除。
它们是按项目和设备名称对 Proxyman 应用程序上的流量进行分类所必需的。 因此,更容易知道请求/响应来自哪里。
由于某些原因,Bonjour 服务可能无法找到 Proxyman 应用程序。
=> 确保您的 iOS 设备和 Mac 位于同一 Wi-Fi 网络中,或者使用 USB 数据线连接到您的 Mac
=> 请使用 Atlantis.start(hostName: "_your_host_name")
版本来显式告诉 Atlantis 连接到您的 Mac。
Atlantis 旨在用于检查网络,而不是用于调试目的。 如果您想使用调试工具,请考虑使用普通的 HTTP 代理
Atlantis 在 Apache-2.0 许可下发布。 有关详细信息,请参见 LICENSE。