如何使用 GO 实现一个简单的 HTTP(S) PROXY

2022-01-01 23:17:53 +08:00
 meiyoumingzi6

想实现一个简单的 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. 想知道为什么 curl 会断掉
  2. 有没有什么解决办法[在上述代码中修改]
  3. 或者有其他代码编写方式
2365 次点击
所在节点    程序员
11 条回复
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 ,严重怀疑是钩……
FrankAdler
2022-01-02 04:17:56 +08:00
2i2Re2PLMaDnghL
2022-01-02 05:42:17 +08:00
我想知道你响应 CONNECT 的时候为什么有 content-length 和内容 `foo`?
meiyoumingzi6
2022-01-02 08:07:44 +08:00
@2i2Re2PLMaDnghL 这个是我代码里面给的响应,如果没有,curl 不会发送接下来的信息
meiyoumingzi6
2022-01-02 08:09:03 +08:00
@yankebupt 感谢,我看看
0o0O0o0O0o
2022-01-02 08:12:39 +08:00
#3 说得对。而且这个 processSocket 很不 go ,一般两个 io.Copy 完事
meiyoumingzi6
2022-01-02 08:16:07 +08:00
@FrankAdler 感谢,看起来是我想要的
meiyoumingzi6
2022-01-02 08:17:49 +08:00
@0o0O0o0O0o 是的,当时是为了 debug 打印内容了😂
meiyoumingzi6
2022-01-02 10:02:11 +08:00
@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. 抓一个正常的请求看看

- 最后

还得得感谢各位大佬们
Codelike
2022-01-02 17:48:09 +08:00
vophan1ee
2022-01-02 19:47:02 +08:00
可以 github 看一下 adguard 写的 mitmproxy

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/825720

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX