V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
• 请不要在回答技术问题时复制粘贴 AI 生成的内容
meiyoumingzi6
V2EX  ›  程序员

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

  •  
  •   meiyoumingzi6 · 2022-01-01 23:17:53 +08:00 · 2371 次点击
    这是一个创建于 1092 天前的主题,其中的信息可能已经有所发展或是发生改变。
    • 问题由来

    想实现一个简单的 proxy,仅作为玩具使用, 当然其实现成的 lib 有很多, 但是目标很明确,学习一下基本原理

    • 是否有其他的代码参考

    有的, 有一个 python 版本的, 在 github 上发现的 python proxy code

    • 是否有 GO 代码
    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
    
    • 是否自行 debug

    有的, 发现 local to server 的时候, read 出现了 error, err: ECONNRESET (54) 然后 接下来就 EOF 了, 所以就退出了, 但是拿到这个 error 的时候, curl 就已经结束了, 所以拿到 EOF 也是正常行为, 主要在于不知道为啥会 curl 会断掉

    • 主题目标
    1. 想知道为什么 curl 会断掉
    2. 有没有什么解决办法[在上述代码中修改]
    3. 或者有其他代码编写方式
    第 1 条附言  ·  2022-01-02 10:03:06 +08:00
    破案了 是因为没有遵循规范, 第一次相应修改成 `"HTTP/1.0 200 Connection Established\r\n\r\n"` 就可以解决了
    11 条回复    2022-01-02 19:47:02 +08:00
    yankebupt
        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 ,严重怀疑是钩……
    FrankAdler
        2
    FrankAdler  
       2022-01-02 04:17:56 +08:00
    2i2Re2PLMaDnghL
        3
    2i2Re2PLMaDnghL  
       2022-01-02 05:42:17 +08:00
    我想知道你响应 CONNECT 的时候为什么有 content-length 和内容 `foo`?
    meiyoumingzi6
        4
    meiyoumingzi6  
    OP
       2022-01-02 08:07:44 +08:00 via iPhone
    @2i2Re2PLMaDnghL 这个是我代码里面给的响应,如果没有,curl 不会发送接下来的信息
    meiyoumingzi6
        5
    meiyoumingzi6  
    OP
       2022-01-02 08:09:03 +08:00 via iPhone
    @yankebupt 感谢,我看看
    0o0O0o0O0o
        6
    0o0O0o0O0o  
       2022-01-02 08:12:39 +08:00 via iPhone
    #3 说得对。而且这个 processSocket 很不 go ,一般两个 io.Copy 完事
    meiyoumingzi6
        7
    meiyoumingzi6  
    OP
       2022-01-02 08:16:07 +08:00 via iPhone
    @FrankAdler 感谢,看起来是我想要的
    meiyoumingzi6
        8
    meiyoumingzi6  
    OP
       2022-01-02 08:17:49 +08:00 via iPhone
    @0o0O0o0O0o 是的,当时是为了 debug 打印内容了😂
    meiyoumingzi6
        9
    meiyoumingzi6  
    OP
       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
        10
    Codelike  
       2022-01-02 17:48:09 +08:00
    vophan1ee
        11
    vophan1ee  
       2022-01-02 19:47:02 +08:00 via Android
    可以 github 看一下 adguard 写的 mitmproxy
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2859 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 13:53 · PVG 21:53 · LAX 05:53 · JFK 08:53
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.