C# 使用Proxy代理請求資源的方法步驟
前言
這是上周在開發(fā) C# 中使用 Proxy 代理時開發(fā)的一些思考和實踐。主要需求是這樣的,用戶可以配置每次請求是否需要代理,用戶可以配置 HTTP代理,HTTPS代理和代理白名單。
還是太年輕
因為一直用的C# 網(wǎng)絡(luò)庫中的HttpWebRequest,所以自然而然先去找找看這個網(wǎng)絡(luò)庫有沒有封裝好我所需要的代理呀。果不其然,被我找到了。自從上次發(fā)現(xiàn)某些類對老版本不兼容后,每次在微軟官方文檔上找到都會翻到最后,查看一下支持的最低框架。

我需要的就是這個 Proxy 屬性,也就是說我最終在發(fā)送請求前,設(shè)置好這個 Proxy 屬性就可以了。先去看看 Proxy 類
The IWebProxy object to use to proxy the request. The default value is set by calling the Select property.
這樣的意思就是說我只要構(gòu)造一個WebProxy,然后賦值給 HttpWebRequest.Proxy就可以了。
看到了WebProxy 的構(gòu)造器,馬上鎖定了

因為我需要用戶傳的是 string ,所以直接這樣構(gòu)造就可以了。然后就是測試了,主管大佬寫的 Node.js的Proxy代理 o_o 先來測試測試
npm install o_o -g o_o
這樣就啟動全局安裝并啟動了代理,在控制臺上可以看到監(jiān)聽的是 8989 端口

[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);
}
如果經(jīng)過了代理,頭部會出現(xiàn) "HTTP/1.1 o_o" 字段 ,經(jīng)過FT測試,是成功的。
本來一切都沒有問題的,除了我自己想的比較簡單外,直到我 Code Review 了一下組里開發(fā)JAVA 的人實現(xiàn)這個功能的 Pull Request ,我才發(fā)現(xiàn)我還真的是想的太簡單!?。?/p>
開始重構(gòu)
首先發(fā)現(xiàn)的一點是,我連Constructor都用錯了,用ILSpy反編譯了一下,發(fā)現(xiàn)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,最后也是構(gòu)造成 Uri, 為什么會關(guān)注的這個呢?因為我發(fā)現(xiàn)有些Proxy地址是
http://username:password@localhost:8989 長這樣的,那么我如果直接以這種形式傳入到CreateProxy里面,它會自動給我分解,然后分Credential和 proxy 傳入到網(wǎng)絡(luò)庫中嗎?接下來就是驗證的過程。
首先需要了解到的一個概念: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 中棄用了,轉(zhuǎn)而代之的是 TLS SSL 那些協(xié)議。
問題來了, HttpWebRequest 中支持Basic Authentication嗎?我們可以看到WebProxy中有一個構(gòu)造方法最后一個參數(shù)是 ICredential 的

是的,就是它,知道前因后果和不足后,我繼續(xù)去重構(gòu) 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信息,然后分別重新構(gòu)造相應(yīng)的類型傳入到 WebProxy 中。上面也有一個坑,我之前還想用正則把username和password 分別提取出去了,沒想到 Uri 已經(jīng)封裝好了,直接取里面的userinfo 信息。哈哈,省力了。
StackOverFlow上也有挺多關(guān)于如何傳入 Credential 到Proxy中,基本上用的也是這個方法,按理說這樣就完事了,直到我做了測試,我發(fā)現(xiàn)微軟這個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);
}
我去測試了發(fā)現(xiàn),這個頭部里面根本沒有加這個 Authorization 屬性啊,尷尬了,是官方文檔坑還是我使用不正確呢,基于此,想到了之前 主管 開發(fā)的那個 Proxy 代理 o_o ,我又去找了一個驗證 basic-auth 的node.js 代理服務(wù)器 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文件,然后執(zhí)行
node tets.js
該代理服務(wù)器監(jiān)聽 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);
}
最后在測試經(jīng)過 3000 端口的代理服務(wù)器,確認是沒問題的,把問題想得簡單的結(jié)果就是發(fā)了一個新版本后,還沒有下載,然而已經(jīng)發(fā)了新版本說,用戶您好,我們又有新版本了。尷尬。需要以此為鑒啊。
后記
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
C# mysql 插入數(shù)據(jù),中文亂碼的解決方法
用C#操作mysql時, 插入數(shù)據(jù)中文都是亂碼,只顯示問號,數(shù)據(jù)庫本身使用的是utf-8字符2013-10-10
C#使用DateAndTime.DateDiff實現(xiàn)計算年齡
這篇文章主要為大家詳細介紹了C#如何使用DateAndTime.DateDiff實現(xiàn)根據(jù)生日計算年齡,文中的示例代碼講解詳細,感興趣的小伙伴可以了解一下2024-01-01

