基于 Gin 的 HTTP 中間人代理 Demo

前面實現的代理對于 HTTPS 流量是進行盲轉的,也就是說直接在 TCP 連接上傳輸 TLS 流量,但是我們無法查看或者修改它的內容。當然了,通常來說這也是不必要的。不過對于某些場景下還是有必要的,例如使用 Fiddler 進行抓包或者監控其它電腦的流量傳輸等,這就需要用到中間人代理了。所以說,學習一下這一塊的內容,對于更好的使用 Fiddler Charles 或者 mitmProxy 這類軟件是很有幫助的。遙想起來,好幾年前我還在大學的時候,學會使用 Fiddler 去抓包,當時就覺得很神奇。我當時特別喜歡使用它來學習 HTTP 協議的結構,或者開安卓模擬器來抓 APP 的包,不過我當時對于代理的概念幾乎是一無所知。現在,也很少再玩這些東西了,反而對代理的了解更進一步了。

在繼續閱讀之前,最好還是對一下概念有一個簡單的了解:

  • 非對稱加密和對稱加密
  • 公鑰和私鑰
  • 簽名
  • 證書

一、中間人代理的概念

一圖勝千言,我們先來看幾張圖片吧。
在這里插入圖片描述

這是前面介紹的通過 TCP 隧道的透明代理的一個簡單示意圖,數據是通過 TCP 上發送的二進制 TLS 流量,代理無法解密這些數據,它只是負責對這些數據進行盲轉發,所以說它是透明代理。所以即使經過了代理服務器,客戶端實際上也是和服務器進行的 TLS 握手,然后通信的,這種情況下通信是安全的。

在這里插入圖片描述

這是我們接下來要介紹的中間人代理了,它同樣是一個代理,但是這次它會假裝自己是客戶端需要訪問的服務器,然后客戶端實際上是同中間人進行通信,代理再同服務器進行通信(甚至沒有這一步!),這樣就可以查看、修改客戶端和服務器通信的內容了(這種情況下通信是不安全的)。

注:中間人,Man In The Middle,簡稱 MITM。

在這里插入圖片描述

網絡是分層的,這就是 HTTPS 協議的示意圖。所以知乎上有一個博主說了一句話,我認為挺有道理的,大致意思是:沒有 HTTPS 協議,只有 HTTP 協議。

所以對于我們接下來的內容,即實現一個中間人代理的關鍵是解決客戶端和中間人代理之間的 TLS 握手。我們先來使用命令行看一下訪問 https://www.baidu.com 的過程(大致了解一下,我也只是理解一點),首先是連接到目標主機的 443 端口,這是 TCP 連接。然后開始建立 TLS 連接,可以看到會進行協議握手(handshake)(握手就是進行協商一個雙方共同接受的條件,協議版本或者密碼套件之類的)。

在這里插入圖片描述

使用瀏覽器訪問百度,查看它的證書,這樣比在命令行里面更方便一些。這里我們主要關注這個 CN 字段,其它的我也不了解。

在這里插入圖片描述

前面這些圖應該可以有一個大致的概念了。我們要做的就是代替真正的服務器和客戶端進行握手(相當于是欺騙它我就是你需要訪問的服務器),但是我們不可能搞到真正服務器的證書(私鑰)的。不過,任何人都可以很方便的生成一個自己公鑰和私鑰,它和那些由CA簽發的證書來說,區別只是它不是受信任的。

節選自 《HTTP權威指南》第 14 章節 安全的 HTTP
在發送已加密的 HTTP 報文之前,客戶端需要和服務器進行一次 SSL 握手,在這個握手過程中,它們要完成以下工作:

  • 交換協議版本號
  • 選擇一個兩端都了解的密碼
  • 對兩端的身份進行驗證
  • 生成臨時的會話密鑰,以便加密信道。

這里的第三點,對兩端身份進行驗證,其實通常是沒有的。我們一般只對服務器的身份進行驗證,即它確實是我們要訪問的服務器(服務器一般不會要求驗證客戶端的身份)。當然也有要求客戶端進行驗證的,例如說 Fiddler 也抓不了支付寶的數據包。據說是因為它使用了雙端驗證,即使你可以假扮服務器,但是沒有假扮客戶端去和真正的服務器進行交互(我聽說支付寶的客戶端證書是內置的,所以中間人代理是沒法抓到它的數據包的)。所以中間人代理實際上也就是鉆了這個漏洞,否則大部分的 HTTPS 流量都難以解密了。

二、實現細節

所以我們要做的工作也就大致清晰了,下面是一個簡單的示意圖,圓角矩形表示這中間的協議變化。
首先客戶端通過 HTTP 協議和代理服務器建立 TCP 連接(在 Go 中,是通過 Hijack 獲取底層 TCP 連接的)。之后客戶端會在這個 TCP 連接上發送 TLS 的內容。所以我們把這個連接轉成 TLS 連接(就是說和客戶端進行握手),如果成功之后我們就會被認為是真的服務器了(實際上是假的),因為現在就是單純的 HTTP 通信了,所以可以自由的查看和修改報文的內容了。

在這里插入圖片描述

生成本地的自簽名證書,這里的 CN 字段,指定為 baidu.com

在這里插入圖片描述

然后我們來看一下,關于中間人代理這塊的代碼:

這里的變化也就是多加了這一個函數,也就是在劫持獲得 TCP 連接后,把它作為參數傳入這個函數。這個函數的功能主要就是加載公鑰和私鑰,然后把 TCP 連接變成 TLS 連接,接著就可以進行 HTTP 的請求和響應了。

func mitm(conn net.Conn) error {tlsCert, err := tls.LoadX509KeyPair(certFile, keyFile)if err != nil {return err}// 再把底層連接轉換成 tls 連接,然后再封裝成 http requesttlsConn := tls.Server(conn, &tls.Config{ // 這里有一個概念叫 客戶端連接和服務端連接PreferServerCipherSuites: true,MaxVersion:               tls.VersionTLS13,Certificates:             []tls.Certificate{tlsCert}, // 這里最關鍵的是這個證書的配置})defer tlsConn.Close()// 封裝成 http requestvar req *http.Requestif req, err = http.ReadRequest(bufio.NewReader(tlsConn)); err != nil {return err}req.RequestURI = ""      // 這里創建一個新的請求或者把這個設置為 空,不然會報錯req.URL.Scheme = "https" // 中途轉換的 Request 丟失了一些信息,居然沒有 scheme 了req.URL.Host = req.Hostvar resp *http.Responseif resp, err = proxyHttpClient.Do(req); err == nil {err = resp.Write(tlsConn)}return err
}

我之前一直沒有想明白這一塊,我也看了很多其它人的實現,但是就是這一點沒有想明白。然后昨天看了一個外國的博文,發現居然就是這樣的,還是對這個網絡這一塊的了解太少了,哈哈。我剛開始想到了把它轉成 TLS 連接,但是我在想那怎么再變成 HTTP 連接呢?但是,仔細一想,HTTP 是一個文本協議,實際上我可以直接對這個 TLS 連接上讀取或者返回符合 HTTP 報文格式的數據,所以最后寫入響應數據就是 resp.Write(tlsConn),雖然這只是簡短的一行,卻困惑了我好久!!!

Talk is cheap, show me your code

我就是看的這篇博客然后才了解了最后困擾我的那部分內容,這個作者寫的真的不錯!

Go and Proxy Servers: Part 2 - HTTPS Proxies

注:這里使用的是 tls.Server,這個包下還有一個 tls.Client 的方法。所以它這里是為了區分客戶端連接和服務端連接。我的理解是這樣的,如果你打算發起一個請求,你肯定需要的是客戶端連接。反過來,如果你已經得到了一個請求,你打算處理它,它就是服務端連接。

三、完整代碼和測試

package mainimport ("bufio""crypto/tls""io""log""net""net/http""strings""github.com/gin-gonic/gin"
)const (RPOXY_SERVER  = "CrazyDragonHttpProxy" // it is just a kidding, but Only HTTP!TUNNEL_PACKET = "HTTP/1.1 200 Connection Established\r\nProxy-agent: CrazyDragonHttpProxy\r\n\r\n"certFile      = "./server.crt"keyFile       = "./server.key"
)var proxyHttpClient = http.DefaultClientfunc main() {r := gin.Default()r.Use(recordReq)      // 使用 gin 的中間件簡單打印請求的信息r.NoRoute(routeProxy) // NO Route is every Routes!!!r.Run(":8888")
}// 這樣就可以處理所有的請求了,在這里區分是 http 還是 https,https 會通過 CONNECT 來建立隧道,所以就通過它來區分。
func routeProxy(c *gin.Context) {req := c.Requestif req.Method == http.MethodConnect {httpsProxy(c, req) // 處理 HTTPS 請求(HTTPS => TCP <-> TLS <-> HTTP )} else {httpProxy(c, req) // 處理 HTTP 請求}
}func httpsProxy(c *gin.Context, req *http.Request) {c.Status(200) // 不加也沒有問題,只是說記錄的日志狀態碼都會變成 404.// 這里接管這個 http 連接,然后將它劫持為一個 TCP 連接hj, ok := c.Writer.(http.Hijacker)if !ok {http.Error(c.Writer, "webserver doesn't support hijacking", http.StatusInternalServerError)return}clientConn, _, err := hj.Hijack()if err != nil {http.Error(c.Writer, err.Error(), http.StatusInternalServerError)return}// 建立隧道之后,發送一個連接建立的響應(其實,只要狀態碼是 200 就可以了)if _, err = clientConn.Write([]byte(TUNNEL_PACKET)); err != nil {log.Printf("Response Failed: %v", err.Error())} else {log.Println("Response Success.")}// 因為我們知道了客戶端需要訪問的 HOST+PORT,所以現在我們需要來模擬一個假的服務器(即中間人)來和它進行對話。// 所以這里的關鍵是這個模擬的服務需要有證書,不然無法和客戶端進行握手。err = mitm(clientConn)if err != nil {log.Println(err)return}
}func httpProxy(c *gin.Context, req *http.Request) {req.RequestURI = "" // 這里創建一個新的請求或者把這個設置為 空,不然會報錯resp, err := proxyHttpClient.Do(req)if err != nil {log.Println(err)return}defer resp.Body.Close()c.Status(resp.StatusCode) // 修改狀態碼,因為默認的是 404(我是在 NoRoute 里面處理的)。for k, v := range resp.Header {c.Header(k, strings.Join(v, ",")) // 寫入頭部(全部寫入嚴格來說是不對)。}c.Header("Server", RPOXY_SERVER) // haha, it just a kidding!!!io.Copy(c.Writer, resp.Body)     // 響應數據給客戶端
}func recordReq(ctx *gin.Context) {log.Printf("Method: %s, Host: %s, URL: %s, Version: %s\n", ctx.Request.Method, ctx.Request.Host, ctx.Request.URL.Path, ctx.Request.Proto)
}func mitm(conn net.Conn) error {tlsCert, err := tls.LoadX509KeyPair(certFile, keyFile)if err != nil {return err}// 再把底層連接轉換成 tls 連接,然后再封裝成 http requesttlsConn := tls.Server(conn, &tls.Config{ // 這里有一個概念叫 客戶端連接和服務端連接PreferServerCipherSuites: true,MaxVersion:               tls.VersionTLS13,Certificates:             []tls.Certificate{tlsCert}, // 這里最關鍵的是這個證書的配置})defer tlsConn.Close()// 封裝成 http requestvar req *http.Requestif req, err = http.ReadRequest(bufio.NewReader(tlsConn)); err != nil {return err}req.RequestURI = ""      // 這里創建一個新的請求或者把這個設置為 空,不然會報錯req.URL.Scheme = "https" // 中途轉換的 Request 丟失了一些信息,居然沒有 scheme 了req.URL.Host = req.Hostvar resp *http.Responseif resp, err = proxyHttpClient.Do(req); err == nil {err = resp.Write(tlsConn)}return err
}

啟動代碼后,使用 curl 訪問 https://www.baidu.com,這里要加 -k 參數跳過證書驗證(我這個自簽名證書是不受信任的,默認是關閉連接的)。

在這里插入圖片描述

如果不加 -k 的話,證書驗證過不去,連接會被終止。

在這里插入圖片描述

啟動代碼后,配置好代理的配置(可以看前面的博客的配置,注意端口號這里我換了,因為 8888 被占用了),使用瀏覽器訪問 https://www.baidu.com

在這里插入圖片描述
遇到問題了,使用不受信任的證書,瀏覽器禁止我訪問百度了,因為它采用了更嚴格的安全策略 HSTS。因為通常情況下,即使證書不安全,我也可以堅持訪問的。不過現在這樣還就是麻煩了呢。

HSTS(HTTP Strcit-Transport-Securit)即 HTTP 嚴格傳輸安全, 是一種 Web 安全策略機制,可保護網站免受協議降級攻擊和 cookie 劫持、中間人攻擊。它允許 Web 服務器聲明瀏覽器(或其他符合要求的用戶代理)應用使用安全的 HTTPS 連接與交互,而不是通過不安全的 HTTP 協議。

HSTS 詳解,讓 HTTPS 更安全

不過幸好,這個是可以關閉的。我用的是 edge 瀏覽器,輸入下面的網址進入這個頁面,在下面輸入百度的域名,刪除動態的 HSTS。不過它也提示了預加載的是沒有用的。這個應該是臨時的,最好還是開啟它,因為這是涉及安全的問題!

在這里插入圖片描述

好了,現在終于可以訪問了 www.baidu.com 了,這里只是打開了首頁截了幾張圖片。

在這里插入圖片描述

在這里插入圖片描述

在這里插入圖片描述

四、信任證書和證書鏈

證書通常是由某些很權威的機構來頒發的,一般稱為 CA。這里的頒發,指的是用 CA 的私鑰來對證書進行簽名(這樣別人就無法偽造證書了,因為沒有辦法簽名)。驗證證書就是用 CA 的公鑰對證書進行解密并驗證。 并且邏輯上證書是一條鏈的結構,首先是根證書,然后是中間證書,最后是服務端證書。

根證書 --> 中間證書 --> 服務端證書

這樣是從頒發證書的角度看的,實際上驗證證書的時候是反過來的,從服務端證書開始,一直到根證書,都驗證通過了才是通過。

現在這個代理服務器,還是有很多問題的,首先它只能訪問 https://www.baidu.com,其次它會報紅色警告。第一個是因為我只是生成了一個假的 baidu.com 的證書,所以對于其它的網址直接是過不去檢驗的(會檢查域名和CN的匹配關系,然后是校驗證書的有效性)。如果域名和證書不匹配,那么直接就是錯誤了,如下圖。

在這里插入圖片描述

這個紅色警告是因為這個證書是自簽名證書,天然就是不被信任的,當然也有解決辦法了。我們來看看 Fiddler 是怎么解決的,它是把自己的證書安裝到了系統的根證書(根證書是默認信任的)中,然后用這個來給我們訪問的域名簽發假的證書(可以看到全部是假的證書,而且它的名字叫 不要信任 Fiddler 根證書)。這里就沒有中間證書了,證書校驗就是服務端證書和根證書了。

在這里插入圖片描述

在這里插入圖片描述

我這里只是用了一個自簽名證書,因為我只演示了百度這一個域名,所以就不需要再弄一個根證書了。而且,我也不想安裝把這個安裝到系統的根證書,因為這樣很危險。所以,如果想要訪問各種不同的網站,需要自己來為每一個訪問的域名簽發證書,這就需要一個 CA 證書來操作了,這樣只需要把一個證書加入系統根證書就好了。如果想要擴展的話,應該會用到下面這個方法來動態的生成證書,不過這個主題的探索對于我來說已經差不多要結束了,因為關于證書這一塊還是挺多不熟悉的。如果你也閱讀到這里,相信你對于 Fiddler、Charles 和 MitmProxy 的也會有更多的理解,對于我們使用這些工具是很有幫助的。

在這里插入圖片描述

這一塊的內容還是挺多的,我也只是了解一點,如果想要看更多的內容的話,可以看下面的這兩個技術博客,寫得很好。

證書鏈簡介

HTTPS 精讀之 TLS 證書校驗

五、總結

這篇博客和上一篇博客之間已經隔了好久了。因為理解這個中間人代理的過程遇到了困難,再加上時間不是很充足,也就沒有繼續寫這個主題的內容。最近剛好又有了時間了,所以就集中時間看了很多內容,測試代碼(因為 TSL 發生了錯誤基本上看不懂什么意思,感覺自己掌握的知識和工具還是太少了,很多錯誤只能束手無策了),也算是對這個東西有了一個新的理解。我其實還是更喜歡盲轉發的代理,因為那樣實現起來更簡單,可以做一些上網行為統計的小工具玩一玩。對于這種中間人代理,因為已經有了很成熟的工具了,所以就當做是了解怎么樣更好的使用這些工具了。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/210023.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/210023.shtml
英文地址,請注明出處:http://en.pswp.cn/news/210023.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

觸想嵌入式工業一體機在智能垃圾分類站的應用

1、行業發展背景 根據住建部給出的目標&#xff0c;到2025年前&#xff0c;全國地級及以上城市要基本建成垃圾分類處理系統。隨著垃圾分類政策在全國強制落地&#xff0c;終端執行層面面臨的最迫切問題是垃圾分類的準確性與社會參與意愿&#xff0c;而這兩點與垃圾分類操作的簡…

CNN發展史脈絡 概述圖整理

CNN發展史脈絡概述圖整理&#xff0c;學習心得&#xff0c;供參考&#xff0c;錯誤請批評指正。 相關論文&#xff1a; LeNet&#xff1a;Handwritten Digit Recognition with a Back-Propagation Network&#xff1b; Gradient-Based Learning Applied to Document Recogniti…

Python 中的數學運算(Python Math)

更多資料獲取 &#x1f4da; 個人網站&#xff1a;ipengtao.com Python中的math模塊是數學運算的重要工具&#xff0c;提供了豐富的數學函數和常數。本文將深入探討math模塊的功能和用法&#xff0c;使您能夠更好地利用Python進行數學運算。 Python的math模塊是一個強大的工具…

C51--OLED

GME12864-12 OLED寫入指令數據&#xff1a; 1、start&#xff08;&#xff09;開始 2、slave address 存積地址&#xff1a;011110 xx 寫入&#xff1a;b 0111 1000 &#xff08;0x78&#xff09;&#xff08;R / W位置為0時&#xff0c;表示寫入&#xff09; 3、ACK 4、cotro…

操作系統———磁盤調度算法模擬

實驗目的 磁盤是可供多個進程共享的設備&#xff0c;當有多個進程都要求訪問磁盤是&#xff0c;應采用一種最佳調度算法&#xff0c;以使各進程對磁盤的平均訪問時間最小。目前最成用的磁盤調度算法有先來先服務&#xff08;FCFS&#xff09;&#xff0c;最短尋道時間優先&…

Spring Boot的配置文件

配置文件的作用 整個項目中所有重要的數據都是在配置文件中配置&#xff0c;如數據庫的連接信息&#xff0c;項目的啟動端口&#xff0c;用于發現和定位問題的普通日志和異常日志等等。配置文件可以分為兩類 系統使用的配置文件&#xff08;系統配置文件&#xff09;&#xf…

【Kotlin】

Lambda 就是一小段可以作為參數傳遞的代碼。 因為正常情況下&#xff0c;我們向某個函數傳參時只能傳入變量&#xff0c;而借助Lambda 卻允許傳入一小段代碼。 Lambda 表達式的語法結構&#xff1a; {參數名1: 參數類型, 參數名2: 參數類型 -> 函數體}首先&#xff0c;最外…

JS基礎源碼之手寫模擬new

JS基礎源碼之手寫模擬new 手寫模擬new初步實現最終實現 手寫模擬new new 運算符創建一個用戶定義的對象類型的實例或具有構造函數的內置對象類型之一。 我們先看看new實現了哪些功能&#xff1a; function Person (name,age){this.name name;this.age age;this.habit Games;…

開發猿的平平淡淡周末---2023/12/9

上周回顧 完成了遺留的開發任務&#xff0c;基本全部完成進一步了解了系統當時設計的原理熟悉了代碼的重構 2023.12.9 天氣晴 溫度適宜 前言 小伙伴們大家好&#xff0c;時間很快&#xff0c;又來到了周末&#xff0c;也是一個平平淡淡的周末。上周只更了一篇博客...原…

滲透測試 | 滲透測試之信息收集

滲透測試&#xff08;penetration test&#xff0c;pentest&#xff09;是實施安全評估&#xff08;即審計&#xff09;的具體手段。 滲透測試可能是單獨進行的一項工作&#xff0c;也可能是常規研發生命周期&#xff08;例如&#xff0c;Microsoft SDLC&#xff09;里 IT 安全…

Unicode編碼解碼

一、Unicode概述 Unicode是一種字符編碼標準&#xff0c;旨在解決不同字符集之間的兼容性問題。它為全球所有語言提供了一種統一的編碼方式&#xff0c;使得各種字符能夠在計算機系統中正確顯示和處理。Unicode字符集包含了世界上幾乎所有的字符&#xff0c;包括中文字符、英文…

算法Day23 簡單吃飯(0-1背包)

簡單吃飯&#xff08;0-1背包&#xff09; Description Input Output Sample 代碼 import java.util.Scanner; public class Main {public static void main(String[] args) {Scanner scanner new Scanner(System.in);int n scanner.nextInt();int total scanner.nextInt(…

WebDriver核心方法和屬性:掌握自動化測試的利器

在自動化測試中&#xff0c;Selenium WebDriver是一個非常重要的工具。它提供了一種方式來模擬用戶與瀏覽器的交互&#xff0c;從而進行各種操作&#xff0c;如點擊按鈕、輸入文本等。本文將介紹WebDriver的核心方法和屬性&#xff0c;以及如何使用它們。 1. 啟動和關閉瀏覽器…

使用es256算法生成jwt

1、使用hutool來做 1、先去jwt解密/加密 - bejson在線工具弄個公私鑰 2、導入hutool maven <dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.7.22</version></dependency><depe…

高項備考葵花寶典-項目進度管理輸入、輸出、工具和技術(中,很詳細考試必過)

項目進度管理的目標是使項目按時完成。有效的進度管理是項目管理成功的關鍵之一&#xff0c;進度問題在項目生命周期內引起的沖突最多。 小型項目中&#xff0c;定義活動、排列活動順序、估算活動持續時間及制定進度模型形成進度計劃等過程的聯系非常密切&#xff0c;可以視為一…

Pytorch中的resize和reshape

torch.reshape() 官方文檔的大致意思是&#xff1a; 返回與輸入具有相同數據和元素數量的張量&#xff0c;但是具有指定形狀。如果可能&#xff0c;返回的張量將是輸入的視圖&#xff0c;也就是說原本的tensor并沒有被改變&#xff0c;如果想要改變那么就將改變的tensor賦值給…

情深不必糾纏

那一年&#xff0c;男孩女孩在萬千人中相遇了。多年后女人的一封郵件&#xff0c;讓男人與女人的靈魂相遇了。他們無緣夫妻&#xff0c;卻發現彼此是靈魂的陪伴。不能攜手相守&#xff0c;卻懂得彼此的心靈。 有一天&#xff0c;女人告訴男人要回家了&#xff0c;問男人心里會不…

ejs —— 三目運算符的用法

EJS&#xff08;Embedded JavaScript&#xff09;是一種簡單的模板語言&#xff0c;它允許將JavaScript代碼嵌入到HTML中。在EJS中&#xff0c;<%、<%和<%-是用于將JavaScript代碼嵌入到模板中的語法。 <%&#xff1a;這是EJS的輸出表達式&#xff0c;用于將變量的…

阿里云安裝docker

文章目錄 一、 yum 進行安裝&#xff08;os版本 CentOS 7&#xff09; 推薦二、 apt-get 進行安裝(os版本 Ubuntu 14.04/16.04&#xff09;三、測試四、阿里云docker加速 一、 yum 進行安裝&#xff08;os版本 CentOS 7&#xff09; 推薦 # step 1: 安裝必要的一些系統工具 su…

<HarmonyOS第一課>應用服務上架【課后考核】

【習題】HarmonyOS應用/元服務上架 判斷題 元服務發布的國家與地區僅限于“中國大陸” 正確(True) 編譯打包的軟件包存放在項目目錄build > outputs > default下 正確(True) 單選題 創建應用時&#xff0c;應用包名需要和app.json5或者config.json文件中哪個字段保持…