基于Ktor框架的Android代理服务器HTTPS请求异常问题
Ktor Android代理服务器HTTPS隧道连接失败问题解决
问题现象
- HTTP请求处理正常,HTTPS请求通过代理时触发错误,curl返回
SEC_E_INVALID_TOKEN(令牌无效)错误 - 测试用curl命令:
curl -v -x http://aaa:sss@192.168.0.100:2222 https://www.google.com
完整日志:
* Trying 192.168.0.100:2222... * Connected to 192.168.0.100 (192.168.0.100) port 2222 (#0) * allocate connect buffer * Establish HTTP proxy tunnel to www.google.com:443 * Proxy auth using Basic with user 'aaa' > CONNECT www.google.com:443 HTTP/1.1 > Host: www.google.com:443 > Proxy-Authorization: Basic > User-Agent: curl/7.83.1 > Proxy-Connection: Keep-Alive > < HTTP/1.1 200 OK < Content-Type: application/octet-stream < * Proxy replied 200 to CONNECT request * CONNECT phase completed * schannel: disabled automatic use of client certificate * ALPN: offers http/1.1 * schannel: next InitializeSecurityContext failed: SEC_E_INVALID_TOKEN (0x80090308) - The token supplied to the function is invalid * Closing connection 0 curl: (35) schannel: next InitializeSecurityContext failed: SEC_E_INVALID_TOKEN (0x80090308) - The token supplied to the function is invalid
相关代码片段
代理连接处理逻辑
val tcpSocketBuilder = aSocket(ActorSelectorManager(Dispatchers.IO)).tcp() val server: Socket? try { // 连接目标服务器 server = tcpSocketBuilder.connect(call.request.host(), call.request.port()) } catch (e: Exception) { Log.v(TAG,"jothi Failed to connect to ${call.request.host()}:${call.request.port()}\n\t${e.printStackTrace()}") return } Log.v(TAG,"jothi Connected to ${call.request.host()}:${call.request.port()}") val successConnectionString = "HTTP/1.1 200 OK\r\nServer-test: https-proxy\r\n\r\n" call.respondBytesWriter(status = HttpStatusCode.OK){successConnectionString} Log.v(TAG,"jothi response send ") val serverReader = server.openReadChannel() val serverWriter = server.openWriteChannel() delay(20) val readChannel: ByteReadChannel = call.receiveChannel() val size = readChannel.availableForRead val byteArray: ByteArray = ByteArray(size) readChannel.readFully(byteArray,0,size) Log.v(TAG,"jothi channel is closed for read " +readChannel.isClosedForRead) Log.v(TAG,"jothi read size " +size)
TLS证书配置代码
val pass = "testpass" // keystore密码 val alias = "certificateAlias" // 证书别名 val filedir = applicationContext.getExternalFilesDir(null) // App专属文件目录 val destfolder = File(filedir, "jothi") if (!destfolder.exists()) { if (!destfolder.mkdirs()) { Log.v(TAG, "jothi Directory not created") } } val keyStoreFile = File(filedir,"keystore.Jks") // 生成带证书的keystore val keystore = buildKeyStore { certificate(alias) { hash = HashAlgorithm.SHA256 sign = SignatureAlgorithm.ECDSA keySizeInBits = 256 password = pass } } keystore.saveToFile(keyStoreFile, pass)
核心问题分析
- HTTPS隧道转发逻辑缺失:当前代码返回200 OK后,仅尝试读取一次客户端数据就终止,没有建立客户端与目标服务器之间的双向持续数据转发通道,导致客户端TLS握手时无数据通路,触发令牌无效错误。
- 证书配置冗余:HTTPS代理隧道是TCP透明转发,客户端会直接与目标服务器(如google.com)完成TLS握手,不需要代理服务器提供证书。当前的自签名证书仅用于代理自身的HTTPS服务,与隧道逻辑无关。
- 通道处理方式错误:使用
delay等待和单次读取的方式不可靠,会导致通道提前关闭,无法处理后续的TLS握手数据流。
解决方案
1. 修复双向数据转发逻辑
返回200 OK后,通过协程启动两个方向的数据流转发,持续处理客户端与目标服务器之间的数据传输:
// 返回隧道建立成功响应 call.respondBytesWriter(status = HttpStatusCode.OK) { writeFully(successConnectionString.toByteArray(Charsets.UTF_8)) } // 启动协程处理双向转发 launch(Dispatchers.IO) { // 客户端 -> 目标服务器 runCatching { call.receiveChannel().copyTo(serverWriter) }.onFailure { Log.v(TAG, "Client to server forward failed: ${it.message}") }.finally { serverWriter.close() call.response.close() } } launch(Dispatchers.IO) { // 目标服务器 -> 客户端 runCatching { serverReader.copyTo(call.responseChannel()) }.onFailure { Log.v(TAG, "Server to client forward failed: ${it.message}") }.finally { serverReader.close() call.response.close() } }
2. 移除隧道无关的TLS配置
如果仅需要实现HTTPS代理隧道功能,可移除当前的JKS证书生成代码(保留该代码仅当你需要代理服务器自身提供HTTPS服务时)。
3. 移除不可靠的延迟与单次读取逻辑
删除delay(20)和单次读取客户端数据的代码,改用copyTo方法自动处理通道的持续读写,直到任意一端关闭连接。
验证方式
重新运行代理服务器后,再次执行原curl测试命令,若握手成功,将正常返回目标网站的HTTPS响应内容。
内容的提问来源于stack exchange,提问作者user1123931
相关产品推荐
相关产品推荐

