想实现一个简单的 proxy,仅作为玩具使用, 当然其实现成的 lib 有很多, 但是目标很明确,学习一下基本原理
有的, 有一个 python 版本的, 在 github 上发现的 python proxy code
package main
import (
"errors"
"fmt"
"github.com/valyala/fasthttp"
"io"
"log"
"net"
"sync"
)
func main() {
if err := fasthttp.ListenAndServe(":1234", requestHandler); err != nil {
log.Fatalf("Error in ListenAndServe: %s", err)
}
}
func processSocket(conn1, conn2 net.Conn, wg *sync.WaitGroup, s string) {
defer func() {
fmt.Println("END", s)
wg.Done()
}()
fmt.Println(s)
var buf []byte
buf = make([]byte, 4096)
i, err := conn1.Read(buf)
buf = buf[:i]
if err != nil {
return
}
for {
fmt.Println(s, string(buf))
conn2.Write(buf)
buf = buf[:]
buf = make([]byte, 2<<10<<10) // 10m
i, err := conn1.Read(buf)
buf = buf[:i]
if err != nil {
if len(buf) > 0 {
conn2.Write(buf)
}
fmt.Println(s, err)
if errors.Is(err, io.EOF) {
break
}
}
}
}
func haddleHTTPS(ctx *fasthttp.RequestCtx) {
h := string(ctx.Request.Host()) // host:port eg. www.baidu.com:443
curConn := ctx.Conn()
curConn.Write([]byte("HTTP/1.1 200 OK\r\nContent-Length: 6\r\n\r\nfoo"))
fmt.Println("haddle:", h)
remotConn, err := net.Dial("tcp", h)
if err != nil {
}
var wg sync.WaitGroup
wg.Add(2)
go processSocket(curConn, remotConn, &wg, "from local to remote")
go processSocket(remotConn, curConn, &wg, "from remote to local")
wg.Wait()
}
func haddleHTTP(ctx *fasthttp.RequestCtx) {
req := fasthttp.AcquireRequest()
req.SetRequestURIBytes(ctx.Request.RequestURI())
//req.Header.SetMethodBytes(ctx.Method())
req.Header = ctx.Request.Header
req.SetBody(ctx.Request.Body())
client := &fasthttp.Client{}
resp := fasthttp.AcquireResponse()
client.Do(req, resp)
body := resp.Body()
fmt.Println(body)
ctx.Write(body)
}
func requestHandler(ctx *fasthttp.RequestCtx) {
method := string(ctx.Method())
if method == "CONNECT" {
// https
haddleHTTPS(ctx)
return
}
// http
haddleHTTP(ctx)
return
}
不行, http 是没有问题的, https 存在问题
➜ ~ curl https://www.baidu.com -vvv
* Uses proxy env variable https_proxy == 'http://127.0.0.1:1234'
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 1234 (#0)
* allocate connect buffer!
* Establish HTTP proxy tunnel to www.baidu.com:443
> CONNECT www.baidu.com:443 HTTP/1.1
> Host: www.baidu.com:443
> User-Agent: curl/7.64.1
> Proxy-Connection: Keep-Alive
>
< HTTP/1.1 200 OK
< Content-Length: 6
* Ignoring Content-Length in CONNECT 200 response
<
* Proxy replied 200 to CONNECT request
* CONNECT phase completed!
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
* CAfile: /etc/ssl/cert.pem
CApath: none
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* CONNECT phase completed!
* CONNECT phase completed!
* error:1400410B:SSL routines:CONNECT_CR_SRVR_HELLO:wrong version number
* Closing connection 0
curl: (35) error:1400410B:SSL routines:CONNECT_CR_SRVR_HELLO:wrong version number
有的, 发现 local to server 的时候, read 出现了 error, err: ECONNRESET (54) 然后 接下来就 EOF 了, 所以就退出了, 但是拿到这个 error 的时候, curl 就已经结束了, 所以拿到 EOF 也是正常行为, 主要在于不知道为啥会 curl 会断掉
1
yankebupt 2022-01-02 00:23:00 +08:00
翻了下 curl 代码
https://github.com/curl/curl/blob/21248e052dbd0db33e8999aeeb919fb6f32c9567/lib/http.c 看见这么句注释 /* if(HTTPS on port 443) OR (HTTP on port 80) then don't include the port number in the host string */ 不知道是不是这个坑 如果不是,算我上钩成功好了……谁大过年的去翻 curl...... 看了看 haddle ,严重怀疑是钩…… |
2
FrankAdler 2022-01-02 04:17:56 +08:00
|
3
2i2Re2PLMaDnghL 2022-01-02 05:42:17 +08:00
我想知道你响应 CONNECT 的时候为什么有 content-length 和内容 `foo`?
|
4
meiyoumingzi6 OP @2i2Re2PLMaDnghL 这个是我代码里面给的响应,如果没有,curl 不会发送接下来的信息
|
5
meiyoumingzi6 OP @yankebupt 感谢,我看看
|
6
0o0O0o0O0o 2022-01-02 08:12:39 +08:00 via iPhone
#3 说得对。而且这个 processSocket 很不 go ,一般两个 io.Copy 完事
|
7
meiyoumingzi6 OP @FrankAdler 感谢,看起来是我想要的
|
8
meiyoumingzi6 OP @0o0O0o0O0o 是的,当时是为了 debug 打印内容了😂
|
9
meiyoumingzi6 OP @yankebupt
@FrankAdler @2i2Re2PLMaDnghL @0o0O0o0O0o 感谢大佬们, 破案了 - 原因: 第一次的时候响应不对,应该是 `HTTP/1.0 200 Connection Established\r\n\r\n`, 参考文档:https://datatracker.ietf.org/doc/html/draft-luotonen-web-proxy-tunneling-01#section-3.2 - 补充: 上述代码在 python request 下,因为 `h := string(ctx.Request.Host())` 这行拿到了空, 修改成 `h := string(ctx.Request.RequestURI())` 后可以正常工作, 并且可以有正常的相应 - 为什么 curl 不能用? 看起来是 curl 严格遵守了规范, https://github.com/curl/curl/search?p=3&q=Connection+Established - CODE ```golang package main import ( "fmt" "github.com/valyala/fasthttp" "io" "log" "net" "sync" ) func main() { if err := fasthttp.ListenAndServe(":1234", requestHandler); err != nil { log.Fatalf("Error in ListenAndServe: %s", err) } } func haddleHTTPS(ctx *fasthttp.RequestCtx) { // ctx.Request.RequestURI() , 不要使用 ctx.Request.Host() h := string(ctx.Request.RequestURI()) // host:port eg. www.baidu.com:443 curConn := ctx.Conn() // https://datatracker.ietf.org/doc/html/draft-luotonen-web-proxy-tunneling-01#section-3.2 curConn.Write([]byte("HTTP/1.0 200 Connection Established\r\n\r\n")) fmt.Println("haddle:", h) remotConn, err := net.Dial("tcp", h) if err != nil { } var wg sync.WaitGroup wg.Add(2) go func() { defer func() { wg.Done() }() io.Copy(curConn, remotConn) }() go func() { defer func() { wg.Done() }() io.Copy(remotConn, curConn) }() wg.Wait() } func haddleHTTP(ctx *fasthttp.RequestCtx) { req := fasthttp.AcquireRequest() req.SetRequestURIBytes(ctx.Request.RequestURI()) req.Header = ctx.Request.Header req.SetBody(ctx.Request.Body()) client := &fasthttp.Client{} resp := fasthttp.AcquireResponse() client.Do(req, resp) body := resp.Body() fmt.Println(body) ctx.Write(body) } func requestHandler(ctx *fasthttp.RequestCtx) { method := string(ctx.Method()) if method == "CONNECT" { // https haddleHTTPS(ctx) return } // http haddleHTTP(ctx) return } ``` - code change log 1. fix, 修复其他客户端可能拿不到 ctx.Request.Host() 的问题, 使用 ctx.Request.RequestURI() 代替 2. fix, 修复响应不符合规范的问题, 应该使用 `"HTTP/1.0 200 Connection Established\r\n\r\n"` 3. improve, 使用 `io.Copy` 代替手工读写 - 教训 /经验 1. 还是得多看文档 2. 抓一个正常的请求看看 - 最后 还得得感谢各位大佬们 |
10
Codelike 2022-01-02 17:48:09 +08:00
赞
|
11
vophan1ee 2022-01-02 19:47:02 +08:00 via Android
可以 github 看一下 adguard 写的 mitmproxy
|