golang 實現一個簡單的 http 代理

代理是網絡中的一項重要的功能,其功能就是代理網絡用戶去取得網絡信息。形象的說:它是網絡信息的中轉站,對於客戶端來說,代理扮演的是服務器的角色,接收請求報文,返回響應報文;對於 web 服務器來說,代理扮演的是客戶端的角色,發送請求報文,接收響應報文。

代理具有多種類型,如果是根據網絡用戶劃分的話,可以劃分爲正向代理和反向代理:

針對正向代理和反向代理,分別有不同的代理協議,即代理服務器和網絡用戶之間通信所使用的協議:

接下來我們就說說 http 代理。

http 代理概述

http 代理是正向代理中較爲簡單的代理方式,它使用 http 協議作爲客戶端和代理服務器的傳輸協議。

http 代理可以承載 http 協議,https 協議,ftp 協議等等。對於不同的協議,客戶端和代理服務器間的數據格式略有不同。

http 協議

我們先來看看 http 協議下客戶端發送給代理服務器的 HTTP Header:

// 直接連接
GET / HTTP/1.1
Host: staight.github.io
Connection: keep-alive

// http代理
GET http://staight.github.io/ HTTP/1.1
Host: staight.github.io
Proxy-Connection: keep-alive

可以看到,http 代理比起直接連接:

爲什麼使用完整路徑?

爲了識別目標服務器。如果沒有完整路徑,且沒有 Host 字段的話,代理服務器將無法得知目標服務器的地址。

爲什麼使用 Proxy-Connection 字段代替 Connection 字段?

爲了兼容使用 HTTP/1.0 協議的過時的代理服務器。HTTP/1.1 纔開始有長連接功能,直接連接的情況下,客戶端發送的 HTTP Header 中如果有Connection: keep-alive字段,表示使用長連接和服務端進行 http 通信,但如果中間有過時的代理服務器,該代理服務器將無法與客戶端和服務端進行長連接,造成客戶端和服務端一直等待,白白浪費時間。因此使用Proxy-Connection字段代替Connection字段,如果代理服務器使用 HTTP/1.1 協議,能夠識別Proxy-Connection字段,則將該字段轉換成Connection再發送給服務端;如果不能識別,直接發送給服務端,因爲服務端也無法識別,則使用短連接進行通信。

http 代理 http 協議交互過程如圖:

https 協議

接下來我們來看看 https 協議下,客戶端發送給代理服務器的 HTTP Header:

CONNECT staight.github.io:443 HTTP/1.1
Host: staight.github.io:443
Proxy-Connection: keep-alive

如上,https 協議和 http 協議相比:

實際上,由於 https 下客戶端和服務端的通信除了開頭的協商以外都是密文,中間的代理服務器不再承擔修改http報文再轉發的功能,而是一開始就和客戶端協商好服務端的地址,隨後的 tcp 密文直接轉發即可。

http 代理 https 協議交互過程如圖:

代碼實現

首先,創建 tcp 服務,並且對於每個 tcp 請求,均調用 handle 函數:

l, err := net.Listen("tcp", ":8080")
if err != nil {
	log.Panic(err)
}


for {
	client, err := l.Accept()
	if err != nil {
		log.Panic(err)
	}

	go handle(client)
   }

然後將獲取的數據放入緩衝區:

var b [1024]byte

n, err := client.Read(b[:])
if err != nil {
	log.Println(err)
	return
   }

從緩衝區讀取 HTTP 請求方法,URL 等信息:

var method, URL, address string

fmt.Sscanf(string(b[:bytes.IndexByte(b[:], '\n')]), "%s%s", &method, &URL)
hostPortURL, err := url.Parse(URL)
if err != nil {
	log.Println(err)
	return
   }

http 協議和 https 協議獲取地址的方式不同,分別處理:

if method == "CONNECT" {
	address = hostPortURL.Scheme + ":" + hostPortURL.Opaque
} else { 
	address = hostPortURL.Host
	
	if strings.Index(hostPortURL.Host, ":") == -1 { 
		address = hostPortURL.Host + ":80"
	}
   }

用獲取到的地址向服務端發起請求。如果是 http 協議,將客戶端的請求直接轉發給服務端;如果是 https 協議,發送 http 響應:

server, err := net.Dial("tcp", address)
if err != nil {
	log.Println(err)
	return
}

if method == "CONNECT" {
	fmt.Fprint(client, "HTTP/1.1 200 Connection established\r\n\r\n")
} else { 
	server.Write(b[:n])
   }

最後,將所有客戶端的請求轉發至服務端,將所有服務端的響應轉發給客戶端:

go io.Copy(server, client)
   io.Copy(client, server

完整的源代碼:

package main

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"net"
	"net/url"
	"strings"
)

func main() {
	
	l, err := net.Listen("tcp", ":8080")
	if err != nil {
		log.Panic(err)
	}

	
	for {
		client, err := l.Accept()
		if err != nil {
			log.Panic(err)
		}

		go handle(client)
	}
}

func handle(client net.Conn) {
	if client == nil {
		return
	}
	defer client.Close()

	log.Printf("remote addr: %v\n", client.RemoteAddr())

	
	var b [1024]byte
	
	n, err := client.Read(b[:])
	if err != nil {
		log.Println(err)
		return
	}

	var method, URL, address string
	
	fmt.Sscanf(string(b[:bytes.IndexByte(b[:], '\n')]), "%s%s", &method, &URL)
	hostPortURL, err := url.Parse(URL)
	if err != nil {
		log.Println(err)
		return
	}

	
	if method == "CONNECT" {
		address = hostPortURL.Scheme + ":" + hostPortURL.Opaque
	} else { 
		address = hostPortURL.Host
		
		if strings.Index(hostPortURL.Host, ":") == -1 { 
			address = hostPortURL.Host + ":80"
		}
	}

	
	server, err := net.Dial("tcp", address)
	if err != nil {
		log.Println(err)
		return
	}
	
	if method == "CONNECT" {
		fmt.Fprint(client, "HTTP/1.1 200 Connection established\r\n\r\n")
	} else { 
		server.Write(b[:n])
	}

	
	go io.Copy(server, client)
	io.Copy(client, server)
}

添加代理,然後運行:

運行成功!

參考文檔

HTTP 代理原理及實現(一):https://imququ.com/post/web-proxy.html

Http 請求頭中的 Proxy-Connection:https://imququ.com/post/the-proxy-connection-header-in-http-request.html

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://staight.github.io/2019/10/24/golang%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84http%E4%BB%A3%E7%90%86/