這是上週在開發 C# 中使用 Proxy
代理時開發的一些思考和實踐。主要需求是這樣的,用戶能夠配置每次請求是否須要代理,用戶能夠配置 HTTP
代理,HTTPS
代理和代理白名單。html
由於一直用的C# 網絡庫中的HttpWebRequest,因此天然而然先去找找看這個網絡庫有沒有封裝好我所須要的代理呀。果不其然,被我找到了。自從上次發現某些類對老版本不兼容後,每次在微軟官方文檔上找到都會翻到最後,查看一下支持的最低框架。node
我須要的就是這個 Proxy
屬性,也就是說我最終在發送請求前,設置好這個 Proxy
屬性就能夠了。先去看看 Proxy
類git
The IWebProxy object to use to proxy the request. The default value is set by calling the Select property.github
這樣的意思就是說我只要構造一個WebProxy
,而後賦值給 HttpWebRequest.Proxy
就能夠了。web
看到了WebProxy
的構造器,立刻鎖定了npm
由於我須要用戶傳的是 string
,因此直接這樣構造就能夠了。而後就是測試了,主管大佬寫的 Node.js
的Proxy
代理 o_o 先來測試測試c#
npm install o_o -g o_o
這樣就啓動全局安裝並啓動了代理,在控制檯上能夠看到監聽的是 8989 端口api
[Fact] public void HttpProxy() { var request = new DescribeAccessPointsRequest(); client.SetHttpProxy("http://localhost:8989"); var response = client.GetAcsResponse(request); Assert.NotNull(response.HttpResponse.Content); var expectValue = "HTTP/1.1 o_o"; string actualValue; response.HttpResponse.Headers.TryGetValue("Via", out actualValue); Assert.Equal(expectValue, actualValue); }
若是通過了代理,頭部會出現 "HTTP/1.1 o_o"
字段 ,通過FT
測試,是成功的。安全
原本一切都沒有問題的,除了我本身想的比較簡單外,直到我 Code Review 了一下組裏開發JAVA 的人實現這個功能的 Pull Request ,我才發現我還真的是想的太簡單!!!bash
首先發現的一點是,我連Constructor
都用錯了,用ILSpy
反編譯了一下,發現WebProxy(string,bool,string[])
所做的事。
// System.Net.WebProxy private static Uri CreateProxyUri(string address) { if (address == null) { return null; } if (address.IndexOf("://") == -1) { address = "http://" + address; } return new Uri(address); }
即便傳進去的是string,最後也是構形成 Uri, 爲何會關注的這個呢?由於我發現有些Proxy地址是
http://username:password@localhost:8989
長這樣的,那麼我若是直接以這種形式傳入到CreateProxy
裏面,它會自動給我分解,而後分Credential
和 proxy
傳入到網絡庫中嗎?接下來就是驗證的過程。
首先須要瞭解到的一個概念:Basic access authentication
In the context of an HTTP transaction, basic access authentication is a method for an HTTP user agent (e.g. a web browser) to provide a user name and password when making a request. In basic HTTP authentication, a request contains a header field of the form
Authorization: Basic <credentials>
, where credentials is the base64 encoding of id and password joined by a colon.It is specified in RFC 7617 from 2015, which obsoletes RFC 2617 from 1999.
因爲其不安全性,已在 RFC 中棄用了,轉而代之的是 TLS SSL 那些協議。
問題來了, HttpWebRequest
中支持Basic Authentication
嗎?咱們能夠看到WebProxy
中有一個構造方法最後一個參數是 ICredential 的
是的,就是它,知道來龍去脈和不足後,我繼續去重構 Http Proxy
的代碼:
originProxyUri = new Uri(proxy); if (!String.IsNullOrEmpty(originProxyUri.UserInfo)) { authorization = Convert.ToBase64String(System.Text.Encoding.GetEncoding("ISO-8859-1").GetBytes(originProxyUri.UserInfo)); finalProxyUri = new Uri(originProxyUri.Scheme + "://" + originProxyUri.Authority); var userInfoArray = originProxyUri.UserInfo.Split(':'); credential = new NetworkCredential(userInfoArray[0], userInfoArray[1]); httpRequest.WebProxy = new WebProxy(finalProxyUri, false, noProxy, credential); }
先拆分出 UserInfo Credential
和 Uri
信息,而後分別從新構造相應的類型傳入到 WebProxy
中。上面也有一個坑,我以前還想用正則把username
和password
分別提取出去了,沒想到 Uri 已經封裝好了,直接取裏面的userinfo
信息。哈哈,省力了。
StackOverFlow
上也有挺多關於如何傳入 Credential
到Proxy
中,基本上用的也是這個方法,按理說這樣就完事了,直到我作了測試,我發現微軟這個Credential
根本沒有起做用,若是是正確的話,會在 HEADER
中添加
Authorization: Basic <credentials>
,和上面那段測試代碼同樣,
[Fact] public void HttpProxyWithCredential() { DescribeAccessPointsRequest request = new DescribeAccessPointsRequest(); client.SetHttpProxy("http://username:password@localhost:8989"); var response = client.GetAcsResponse(request); var expectValue = "HTTP/1.1 o_o"; string actualValue; response.HttpResponse.Headers.TryGetValue("Via", out actualValue); Assert.Equal(expectValue, actualValue); Assert.NotNull(response.HttpResponse.Content); }
我去測試了發現,這個頭部裏面根本沒有加這個 Authorization
屬性啊,尷尬了,是官方文檔坑仍是我使用不正確呢,基於此,想到了以前 主管 開發的那個 Proxy
代理 o_o
,我又去找了一個驗證 basic-auth
的node.js
代理服務器 basic-auth
npm install basic-auth
var http = require('http') var auth = require('basic-auth') var compare = require('tsscmp') // Create server var server = http.createServer(function (req, res) { var credentials = auth(req) // Check credentials // The "check" function will typically be against your user store if (!credentials || !check(credentials.name, credentials.pass)) { res.statusCode = 401 res.setHeader('WWW-Authenticate', 'Basic realm="example"') res.end('Access denied') } else { res.end('Access granted') } }) // Basic function to validate credentials for example function check (name, pass) { var valid = true // Simple method to prevent short-circut and use timing-safe compare valid = compare(name, 'john') && valid valid = compare(pass, 'secret') && valid return valid } // Listen server.listen(3000)
將上面那段 Js代碼打包成一個 js文件,而後執行
node tets.js
該代理服務器監聽 3000端口,我使用剛纔那段代碼,果不其然,返回的是 401 ,這不是坑嗎,官方文檔上這樣說能夠,然而都不行。
最後只能強制加上這個 Authorization
代碼
originProxyUri = new Uri(proxy); if (!String.IsNullOrEmpty(originProxyUri.UserInfo)) { authorization = Convert.ToBase64String(System.Text.Encoding.GetEncoding("ISO-8859-1").GetBytes(originProxyUri.UserInfo)); finalProxyUri = new Uri(originProxyUri.Scheme + "://" + originProxyUri.Authority); var userInfoArray = originProxyUri.UserInfo.Split(':'); credential = new NetworkCredential(userInfoArray[0], userInfoArray[1]); httpRequest.WebProxy = new WebProxy(finalProxyUri, false, noProxy, credential); httpRequest.Headers.Add("Authorization", "Basic " + authorization); }
最後在測試通過 3000 端口的代理服務器,確認是沒問題的,把問題想得簡單的結果就是發了一個新版本後,尚未下載,然而已經發了新版本說,用戶您好,咱們又有新版本了。尷尬。須要以此爲鑑啊。
姜仍是老的辣,多看看別人的代碼,來發現本身的不足。勤加練習!