golang實現一個簡單的http代理


轉載於https://staight.github.io/archives/

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

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

  • 正向代理:將客戶端作為網絡用戶。客戶端訪問服務端時,先訪問代理服務器,隨后代理服務器再訪問服務端。此過程需客戶端進行代理配置,對服務端透明。
  • 反向代理:將服務端作為網絡用戶。訪問過程與正向代理相同,不過此過程對客戶端透明,需服務端進行代理配置(也可不配置)。

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

  • 正向代理:
    • http
    • https
    • socks4
    • socks5
    • vpn:就功能而言,vpn也可以被認為是代理
  • 反向代理:
    • tcp
    • udp
    • http
    • https

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

http代理概述

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

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

http協議

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

1
2
3
4
5
6
7
8
9
// 直接連接
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代理比起直接連接:

  • url變成完整路徑,/->http://staight.github.io/
  • Connection字段變成Proxy-Connection字段
  • 其余保持原樣

為什么使用完整路徑?

為了識別目標服務器。如果沒有完整路徑,且沒有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協議交互過程如圖:

http代理http協議

https協議

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

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

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

  • 請求方法從GET變成CONNECT
  • url沒有protocol字段

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

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

http代理https協議

代碼實現

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// tcp連接,監聽8080端口
l, err := net.Listen("tcp", ":8080")
if err != nil {
log.Panic(err)
}

// 死循環,每當遇到連接時,調用handle
for {
client, err := l.Accept()
if err != nil {
log.Panic(err)
}

go handle(client)
}
 

然后將獲取的數據放入緩沖區:

1
2
3
4
5
6
7
8
// 用來存放客戶端數據的緩沖區
var b [1024]byte
//從客戶端獲取數據
n, err := client.Read(b[:])
if err != nil {
log.Println(err)
return
}
 

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

1
2
3
4
5
6
7
8
var method, URL, address string
// 從客戶端數據讀入method,url
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協議獲取地址的方式不同,分別處理:

1
2
3
4
5
6
7
8
9
10
// 如果方法是CONNECT,則為https協議
if method == "CONNECT" {
address = hostPortURL.Scheme + ":" + hostPortURL.Opaque
} else { //否則為http協議
address = hostPortURL.Host
// 如果host不帶端口,則默認為80
if strings.Index(hostPortURL.Host, ":") == -1 { //host不帶端口, 默認80
address = hostPortURL.Host + ":80"
}
}
 

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

1
2
3
4
5
6
7
8
9
10
11
12
//獲得了請求的host和port,向服務端發起tcp連接
server, err := net.Dial("tcp", address)
if err != nil {
log.Println(err)
return
}
//如果使用https協議,需先向客戶端表示連接建立完畢
if method == "CONNECT" {
fmt.Fprint(client, "HTTP/1.1 200 Connection established\r\n\r\n")
} else { //如果使用http協議,需將從客戶端得到的http請求轉發給服務端
server.Write(b[:n])
}
 

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

1
2
3
//將客戶端的請求轉發至服務端,將服務端的響應轉發給客戶端。io.Copy為阻塞函數,文件描述符不關閉就不停止
go io.Copy(server, client)
io.Copy(client, server
 

完整的源代碼:

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
package main

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

func main() {
// tcp連接,監聽8080端口
l, err := net.Listen("tcp", ":8080")
if err != nil {
log.Panic(err)
}

// 死循環,每當遇到連接時,調用handle
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
// 從客戶端數據讀入method,url
fmt.Sscanf(string(b[:bytes.IndexByte(b[:], '\n')]), "%s%s", &method, &URL)
hostPortURL, err := url.Parse(URL)
if err != nil {
log.Println(err)
return
}

// 如果方法是CONNECT,則為https協議
if method == "CONNECT" {
address = hostPortURL.Scheme + ":" + hostPortURL.Opaque
} else { //否則為http協議
address = hostPortURL.Host
// 如果host不帶端口,則默認為80
if strings.Index(hostPortURL.Host, ":") == -1 { //host不帶端口, 默認80
address = hostPortURL.Host + ":80"
}
}

//獲得了請求的host和port,向服務端發起tcp連接
server, err := net.Dial("tcp", address)
if err != nil {
log.Println(err)
return
}
//如果使用https協議,需先向客戶端表示連接建立完畢
if method == "CONNECT" {
fmt.Fprint(client, "HTTP/1.1 200 Connection established\r\n\r\n")
} else { //如果使用http協議,需將從客戶端得到的http請求轉發給服務端
server.Write(b[:n])
}

//將客戶端的請求轉發至服務端,將服務端的響應轉發給客戶端。io.Copy為阻塞函數,文件描述符不關閉就不停止
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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM