简单HTTP代理golang实现

  1. 基础框架搭建,直接上代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

const (
DefaultPort = 80 //默认端口号
)

var (
bindAddr = flag.String("bind", ":3128", "proxy bind addr")
)

var (
_server = &fasthttp.Server{
Name: "simple http server",
Handler: requestHandler,
}
)

//定义一个工具方法,免去烦人的返回值未使用
func voidClose(c io.Closer) {
_ = c.Close()
}

func copyHttpPayload(ctx *fasthttp.RequestCtx, rConn io.ReadWriteCloser) {
//TODO
}

func main() {
flag.Parse()
if *bindAddr == "" {
log.Panicln("绑定地址不能为空")
}
l, err := net.Listen("tcp", *bindAddr)
if err != nil {
log.Panicf("监听 %v 失败,err:%v\n", *bindAddr, err)
}
defer voidClose(l)
for {
c, err := l.Accept()
if err != nil {
log.Panicf("等待客户连接失败,err:%v\n", err)
}
_ = _server.ServeConn(c)
}
}

  1. 完善 requestHandler 实现HTTP代理请求处理

    1. 由于HTTP Request Header中的Host有可能是不带端口号的,先定义一个方法将HTTP Request中的Host 转为 host:port 格式

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      func hostToTcpAddr(host string, defPort int) (addr string, err error) {
      target, err := url.Parse("shs://" + host)
      if err != nil {
      return
      }
      port, err := strconv.Atoi(target.Port())
      if err != nil {
      port = defPort
      }
      return fmt.Sprintf("%s:%d", target.Hostname(), port), nil
      }
    2. 继续完善requestHandler方法

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      func requestHandler(ctx *fasthttp.RequestCtx) {
      // 暂时不支持CONNECT方法
      if "CONNECT" == string(ctx.Method()) {
      ctx.Error(http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
      return
      }

      target, err := hostToTcpAddr(string(ctx.Host()), DefaultPort)
      if err != nil {
      ctx.Error(err.Error(), http.StatusServiceUnavailable)
      return
      }
      //客户端连接代理服务器的时候可能会传递一些给代理服务专用的`Header`,我们需要在转发到目标服务的时候剔除
      for _, h := range []string{"Proxy-Connection", "Proxy-Authenticate"} {
      ctx.Request.Header.Del(h)
      }
      ctx.Request.SetConnectionClose()
      rConn, err := net.Dial("tcp", target)
      if err != nil {
      ctx.Error(err.Error(), fasthttp.StatusServiceUnavailable)
      log.Println("与目标服务器建立连接失败", err)
      return
      }
      copyHttpPayload(ctx, rConn)
      }
    3. 在客户端与目标服务器之间转发HTTP请求及响应,即copyHttpPayload方法
      在实现该方法前我们先回忆一下HTTP Response中Body长度的获取,通常Body的长度通过Header中的content-length来标识,但是有2种例外。

      • 使用HTTP/1.0时,可不返回content-length (HTTP/1.0 不支持连接复用,一个请求完成之后即关闭连接,客户端可通过EOF来判断body是否传输结束)
      • Response的transfer-encodingtruck时,无需返回 content-length

      了解了Body的长度的获取方式,继续完善copyHttpPayload方法

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      func copyHttpPayload(ctx *fasthttp.RequestCtx, rConn io.ReadWriteCloser) {
      n, err := ctx.Request.WriteTo(rConn)
      if err != nil {
      _ = rConn.Close()
      return
      }
      reader := bufio.NewReader(rConn)
      err = ctx.Response.Header.Read(reader)
      if err != nil {
      _ = rConn.Close()
      if err != io.EOF {
      ctx.Error(err.Error(), fasthttp.StatusServiceUnavailable)
      }
      return
      }
      cl := ctx.Response.Header.ContentLength()
      switch cl {
      case -1: //chunk
      ctx.SetConnectionClose()
      ctx.SetBodyStreamWriter(func(w *bufio.Writer) {
      defer func() {
      _ = rConn.Close()
      }()
      for {
      data, _, err := reader.ReadLine()
      if err != nil {
      log.Println(err)
      return
      }
      if strings.TrimSpace(string(data)) == "0" {
      break
      }
      cs, err := strconv.ParseInt(string(data), 16, 32)
      if err != nil {
      log.Println(err)
      return
      }
      _, copyErr := io.CopyN(w, reader, cs)
      if copyErr != nil {
      return
      }
      _, err = reader.Discard(2)
      if err != nil {
      log.Println(err)
      return
      }
      }
      })
      case -2: //EOF
      ctx.SetBodyStream(reader, -1)
      case 0:
      _ = rConn.Close()
      return
      default: //Fix len
      ctx.SetBodyStream(reader, cl)
      }
      }

      再来理解一下truck模式,即分块模式,适用于大文件 不确定长度数据 动态压缩或编码等场景,格式为:

      1
      2
      3
      4
      5
      6
      7
      0A
      xx xx xx xx xx xx xx xx xx xx
      01
      xx
      03
      xx xx xx
      0

      1,3,5,7行为truck数据长度,为16进制数字没有0x前缀的文本标识方式,0A标识块长度为10个byte,最后一个0表示为传输结束,
      2,4,6 行为数据块.

至此我们就完成了一个简单的HTTP代理服务器

三方依赖
github.com/valyala/fasthttp