Estou tentando converter uma URLSession
requisição simples em Swift para usar NWConnection
. Isso porque quero fazer a requisição usando um Proxy que requer Autenticação. Postei esta Pergunta do SO sobre usar um proxy com URLSession
. Infelizmente ninguém respondeu, mas encontrei uma solução usando NWConnection
em vez disso.
Meu proxy funciona 100%, mas algo sobre minha configuração não está funcionando e não recebo resposta do servidor. Eu apreciaria alguma ajuda para converter esta solicitação GET básica para uma que utilize NWConnection
. O proxy que incluí funciona, sinta-se à vontade para usá-lo para testes. Esta URL específica deve retornar 18,5 KB de dados, então cerca de 18.000 bytes.
Seria ótimo se alguém pudesse ajudar, porque o Swift precisa de um wrapper básico que simplifique as solicitações usando Proxies que podem ou não exigir Auth. Vou criar um wrapper de SO NWConnection
para simplificar seu uso.
Solicitação de trabalho
func updateOrderStatus(completion: @escaping (Bool) -> Void) {
let orderLink = "https://shop.ccs.com/51913883831/orders/f3ef2745f2b06c6b410e2aa8a6135847"
guard let url = URL(string: orderLink) else {
completion(true)
return
}
let cookieStorage = HTTPCookieStorage.shared
let config = URLSessionConfiguration.default
config.httpCookieStorage = cookieStorage
config.httpCookieAcceptPolicy = .always
let session = URLSession(configuration: config)
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", forHTTPHeaderField: "Accept")
request.setValue("none", forHTTPHeaderField: "Sec-Fetch-Site")
request.setValue("navigate", forHTTPHeaderField: "Sec-Fetch-Mode")
request.setValue("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0.1 Safari/605.1.15", forHTTPHeaderField: "User-Agent")
request.setValue("en-US,en;q=0.9", forHTTPHeaderField: "Accept-Language")
request.setValue("gzip, deflate, br", forHTTPHeaderField: "Accept-Encoding")
request.setValue("document", forHTTPHeaderField: "Sec-Fetch-Dest")
request.setValue("u=0, i", forHTTPHeaderField: "Priority")
let task = session.dataTask(with: request) { data, response, error in
if let error = error {
print("Request error: \(error.localizedDescription)")
completion(false)
return
}
guard let data = data else {
completion(false)
print("No data received")
return
}
if let body = String(data: data, encoding: .utf8) {
completion(true)
} else {
completion(false)
print("Unable to decode response body")
}
}
task.resume()
}
Tentativa de conversão
func updateOrderStatusProxy(completion: @escaping (Bool) -> Void) {
let orderLink = "https://shop.ccs.com/51913883831/orders/f3ef2745f2b06c6b410e2aa8a6135847"
guard let url = URL(string: orderLink) else {
completion(true)
return
}
let proxy = "resi.wealthproxies.com:8000:akzaidan:x0if46jo-country-US-session-7cz6bpzy-duration-60"
let proxyDetails = proxy.split(separator: ":").map(String.init)
guard proxyDetails.count == 4, let port = UInt16(proxyDetails[1]) else {
print("Invalid proxy format")
completion(false)
return
}
let proxyEndpoint = NWEndpoint.hostPort(host: .init(proxyDetails[0]),
port: NWEndpoint.Port(integerLiteral: port))
let proxyConfig = ProxyConfiguration(httpCONNECTProxy: proxyEndpoint, tlsOptions: nil)
proxyConfig.applyCredential(username: proxyDetails[2], password: proxyDetails[3])
let parameters = NWParameters.tcp
let privacyContext = NWParameters.PrivacyContext(description: "ProxyConfig")
privacyContext.proxyConfigurations = [proxyConfig]
parameters.setPrivacyContext(privacyContext)
let host = url.host ?? ""
let path = url.path.isEmpty ? "/" : url.path
let query = url.query ?? ""
let fullPath = query.isEmpty ? path : "\(path)?\(query)"
let connection = NWConnection(
to: .hostPort(
host: .init(host),
port: .init(integerLiteral: UInt16(url.port ?? 80))
),
using: parameters
)
connection.stateUpdateHandler = { state in
switch state {
case .ready:
print("Connected to proxy: \(proxyDetails[0])")
let httpRequest = """
GET \(fullPath) HTTP/1.1\r
Host: \(host)\r
Connection: close\r
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0.1 Safari/605.1.15\r
Accept-Language: en-US,en;q=0.9\r
Accept-Encoding: gzip, deflate, br\r
Sec-Fetch-Dest: document\r
Sec-Fetch-Mode: navigate\r
Sec-Fetch-Site: none\r
Priority: u=0, i\r
\r
"""
connection.send(content: httpRequest.data(using: .utf8), completion: .contentProcessed({ error in
if let error = error {
print("Failed to send request: \(error)")
completion(false)
return
}
// Read data until the connection is complete
self.readAllData(connection: connection) { finalData, readError in
if let readError = readError {
print("Failed to receive response: \(readError)")
completion(false)
return
}
guard let data = finalData else {
print("No data received or unable to read data.")
completion(false)
return
}
if let body = String(data: data, encoding: .utf8) {
print("Received \(data.count) bytes")
print("\n\nBody is \(body)")
completion(true)
} else {
print("Unable to decode response body.")
completion(false)
}
}
}))
case .failed(let error):
print("Connection failed for proxy \(proxyDetails[0]): \(error)")
completion(false)
case .cancelled:
print("Connection cancelled for proxy \(proxyDetails[0])")
completion(false)
case .waiting(let error):
print("Connection waiting for proxy \(proxyDetails[0]): \(error)")
completion(false)
default:
break
}
}
connection.start(queue: .global())
}
private func readAllData(connection: NWConnection,
accumulatedData: Data = Data(),
completion: @escaping (Data?, Error?) -> Void) {
connection.receive(minimumIncompleteLength: 1, maximumLength: 65536) { data, context, isComplete, error in
if let error = error {
completion(nil, error)
return
}
// Append newly received data to what's been accumulated so far
let newAccumulatedData = accumulatedData + (data ?? Data())
if isComplete {
// If isComplete is true, the server closed the connection or ended the stream
completion(newAccumulatedData, nil)
} else {
// Still more data to read, so keep calling receive
self.readAllData(connection: connection,
accumulatedData: newAccumulatedData,
completion: completion)
}
}
}
Sequência de solicitação correta
let httpRequest = "GET \("/51913883831/orders/f3ef2745f2b06c6b410e2aa8a6135847") HTTP/1.1\r\nHost: \("shop.ccs.com")\r\nConnection: close\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\nUser-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0.1 Safari/605.1.15\r\nAccept-Language: en-US,en;q=0.9\r\nAccept-Encoding: gzip, deflate, br\r\nSec-Fetch-Dest: document\r\nSec-Fetch-Mode: navigate\r\nSec-Fetch-Site: none\r\nPriority: u=0, i\r\n\r\n"
Suspeito que isso seja mais fácil de fazer do que implementar diretamente o mecanismo de desafio-resposta (como o CouchDeveloper sugere nos comentários ):
NWConnection
irá abstrair para você os detalhes de baixo nível doHTTP CONNECT
método usado para estabelecer um túnel através do proxy.Não vejo nenhuma
init(tls:tcp:)
chamada no seu código: seu proxy, mesmo estando na porta 8000, quase certamente espera uma conexão criptografada , semelhante a esta pergunta .Concordo, quando você configura um proxy na maioria dos aplicativos ou configurações do sistema, você frequentemente vê uma URL de proxy que começa com
http://
, mesmo que o próprio proxy use TLS .Esse
http://
prefixo na URL do proxy não significa que a conexão com o proxy não seja criptografada. Ele indica que o proxy é um proxy HTTP (em vez de um proxy SOCKS, por exemplo) e que a interação inicial com o proxy usará o protocolo HTTP (especificamente, oCONNECT
método ).Observe que você precisa se conectar ao ponto de extremidade PROXY , não ao servidor de destino: O
NWConnection
fará o tunelamento através do proxy até o destino final (shop.ccs.com
).O
Host
cabeçalho deve conter o host de destino, não o host proxy. E dito cabeçalho deve incluir umConnection: close
para sinalizar o fim da solicitação.Finalmente, os dados brutos que você recebe do servidor incluem os cabeçalhos HTTP (linha de status, tipo de conteúdo, ...) e o corpo. Não trate a resposta inteira como o corpo!
Então, usar
NWParameters.tls
(que é equivalente aNWParameters(tls: .init())
) funciona, enquanto criar explicitamente parâmetros TCP comNWParameters(tls: .init(), tcp: .init())
não . Quando você usaNWParameters(tls: .init())
, o Network framework infere algumas configurações padrão que são adequadas para HTTPS, incluindo a porta correta (443).Ao especificar explicitamente
tcp: .init()
, você pode ter substituído esses padrões e causado uma incompatibilidade com as expectativas do proxy.Você também alterou a porta da conexão para 443: o proxy está esperando uma conexão TLS desde o início. Embora a configuração do proxy use a porta 8000, o servidor de destino
shop.ccs.com
( ) usa a porta HTTPS padrão 443. ComoNWConnection
está criando um túnel através do proxy, oNWConnection
precisa ser informado para usar a porta 443. O proxy está escutando em 8000, mas o túnel é criado para a porta 443 no servidor de destino.Você removeu o
Accept-Encoding
cabeçalho para obter o HTML descompactado: uma solução alternativa válida, mas menos eficiente. O ideal é que você manipule a resposta compactada.