C# 使用 Proxy 请求资源,基于 HttpWebRequest 类

前言

这是上周在开发 C# 中使用 Proxy 代理时开发的一些思考和实践。主要需求是这样的,用户可以配置每次请求是否需要代理,用户可以配置 HTTP代理,HTTPS代理和代理白名单。

还是太年轻

因为一直用的C# 网络库中的HttpWebRequest,所以自然而然先去找找看这个网络库有没有封装好我所需要的代理呀。果不其然,被我找到了。自从上次发现某些类对老版本不兼容后,每次在微软官方文档上找到都会翻到最后,查看一下支持的最低框架。

我需要的就是这个 Proxy 属性,也就是说我最终在发送请求前,设置好这个 Proxy 属性就可以了。先去看看 Proxy

这样的意思就是说我只要构造一个WebProxy,然后赋值给 HttpWebRequest.Proxy就可以了。

看到了WebProxy 的构造器,马上锁定了

因为我需要用户传的是 string ,所以直接这样构造就可以了。然后就是测试了,主管大佬写的 Node.jsProxy代理 o_o 先来测试测试

npm install o_o -g

o_o

这样就启动全局安装并启动了代理,在控制台上可以看到监听的是 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);
}

如果经过了代理,头部会出现 "HTTP/1.1 o_o" 字段 ,经过FT测试,是成功的。

本来一切都没有问题的,除了我自己想的比较简单外,直到我 Code Review 了一下组里开发JAVA 的人实现这个功能的 Pull Request ,我才发现我还真的是想的太简单!!!

开始重构

首先发现的一点是,我连Constructor都用错了,用ILSpy反编译了一下,发现WebProxy(string,bool,string[])所作的事。C# 使用 Proxy 代理请求资源-LMLPHP

// 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里面,它会自动给我分解,然后分Credentialproxy 传入到网络库中吗?接下来就是验证的过程。

首先需要了解到的一个概念:Basic access authentication

由于其不安全性,已在 RFC 中弃用了,转而代之的是 TLS SSL 那些协议。

问题来了, HttpWebRequest 中支持Basic Authentication吗?我们可以看到WebProxy中有一个构造方法最后一个参数是 ICredential 的

C# 使用 Proxy 代理请求资源-LMLPHP

是的,就是它,知道前因后果和不足后,我继续去重构 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 CredentialUri信息,然后分别重新构造相应的类型传入到 WebProxy 中。上面也有一个坑,我之前还想用正则把usernamepassword 分别提取出去了,没想到 Uri 已经封装好了,直接取里面的userinfo 信息。哈哈,省力了。

StackOverFlow上也有挺多关于如何传入 CredentialProxy中,基本上用的也是这个方法,按理说这样就完事了,直到我做了测试,我发现微软这个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-authnode.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 端口的代理服务器,确认是没问题的,把问题想得简单的结果就是发了一个新版本后,还没有下载,然而已经发了新版本说,用户您好,我们又有新版本了。尴尬。需要以此为鉴啊。

后记

姜还是老的辣,多看看别人的代码,来发现自己的不足。勤加练习!

03-23 21:38