C# WebClient使用正确凭据进行HTTP基本身份验证时失败,返回401错误

3
我试图通过 自动配置无线路由器的SSID和密码。该路由器没有我所知道的API。它是一款无品牌的中国路由器。Web配置似乎是唯一的配置选项。它使用 (你浏览到路由器的IP地址,会弹出一个通用对话框,要求输入用户名和密码)。
我使用Wireshark获取了请求的头信息和表单字段,这是手动更新SSID和密码(两个单独的表单)时所使用的。然后我尝试使用模拟这些请求。
这是我正在使用的保存新SSID的代码片段(NameValueCollection在其他地方定义):
private const string FORM_SSID = "http://192.168.1.2/formWlanSetup.htm";
private const string REF_SSID = "http://192.168.1.2/formRedirect.htm?redirect-url=wlbasic.htm&wlan_id=0";
private NameValueCollection mFields = HttpUtility.ParseQueryString(string.Empty, Encoding.ASCII);

public string SaveConfigResponse()
{
    try
    {
        using (WebClient wc = new WebClient())
        {
            wc.Headers[HttpRequestHeader.Accept] = "text/html, application/xhtml+xml, */*";
            wc.Headers[HttpRequestHeader.Referer] = REF_SSID;
            wc.Headers[HttpRequestHeader.AcceptLanguage] = "en-US";
            wc.Headers[HttpRequestHeader.UserAgent] = "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko";
            wc.Headers[HttpRequestHeader.ContentType] = "application/x-www-form-urlencoded";
            wc.Headers[HttpRequestHeader.AcceptEncoding] = "gzip, deflate";
            wc.Headers[HttpRequestHeader.Host] = "192.168.1.2";
            wc.Headers[HttpRequestHeader.Connection] = "Keep-Alive";
            wc.Headers[HttpRequestHeader.ContentLength] = Encoding.ASCII.GetBytes(mFields.ToString()).Length.ToString();
            wc.Headers[HttpRequestHeader.CacheControl] = "no-cache";
            string credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes(config_user + ":" + config_pass));
            wc.Headers[HttpRequestHeader.Authorization] = string.Format("Basic {0}", credentials);
            //wc.Credentials = new NetworkCredential("admin", "admin");
            return Encoding.ASCII.GetString(wc.UploadValues(FORM_SSID, "POST", mFields));
        }
    }
    catch (Exception ex)
    {
        return ex.Message;
    }
}

这将导致一个未经授权的响应。我所尝试的事情是不可能的吗?

更新

这里是浏览器和WebClient的POST/RESPONSE的HTTP头信息。我尝试尽可能地匹配浏览器的POST请求,以便与我的WebClient POST请求相匹配。

浏览器:

POST /formWlanSetup.htm HTTP/1.1
Accept: text/html, application/xhtml+xml, */*
Referer: http://192.168.1.2/formRedirect.htm?redirect-url=wlbasic.htm&wlan_id=0
Accept-Language: en-US
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko
Content-Type: application/x-www-form-urlencoded
Accept-Encoding: gzip, deflate
Host: 192.168.1.2
Content-Length: 524
Connection: Keep-Alive
Cache-Control: no-cache
Authorization: Basic YWRtaW46YWRtaW4=

HTTP/1.1 302 Found
Location: wlbasic.htm
Content-Length: 183
Date: Thu, 23 Oct 2014 18:18:27 GMT
Server: eCos Embedded Web Server
Connection: close
Content-Type: text/html
Transfer-Encoding: chunked
Cache-Control: no-cache

WebClient:

POST /formWlanSetup.htm HTTP/1.1
Accept-Language: en-US
Accept-Encoding: gzip, deflate
Cache-Control: no-cache
Authorization: Basic YWRtaW46YWRtaW4=
Accept: text/html, application/xhtml+xml, */*
Content-Type: application/x-www-form-urlencoded
Referer: http://192.168.1.2/formRedirect.htm?redirect-url=wlbasic.htm&wlan_id=0
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko
Host: 192.168.1.2
Content-Length: 524
Connection: Keep-Alive

HTTP/1.1 401 Not Authorized
WWW-Authenticate: Basic realm="AP"
Date: Thu, 23 Oct 2014 18:18:41 GMT
Server: eCos Embedded Web Server
Connection: close
Content-Type: text/html
Transfer-Encoding: chunked
Cache-Control: no-cache

以上内容均来自Wireshark。我对Wireshark并不十分熟悉,但我已经做到了这一步。如果我知道如何正确提取原始数据并将其贴在pastebin上,那就好了。

重要的新观察

  • 浏览器和WebClient的后置数据包的Wireshark捕获显然在标题的顺序上有所不同。虽然每个标题的数据明显相同,但我不知道这可能或可能不重要。
  • 我注意到数据包之间的一个鲜明差异是,Wireshark报告浏览器数据包比WebClient数据包大得多。在查看详细视图时,我找不到任何明显的区别。我假设进行比较的原始数据会揭示很多东西,但同样,我不知道如何做到这一点。
  • 我有一个令人困惑的启示。尽管响应明确指出“(401)未经授权”,但路由器实际上接受了这个帖子!在我的WebClient发布之后进入路由器的Web配置后,显示设置已被接受并保存。
那最后一个问题非常重要。我发现自己处于这样一种情况:我可以通过WebClient提交来保存我的配置,但必须忽略401响应才能这么做。显然,这远非理想。如此接近,却又如此遥远!

最终更新(分辨率)

我已经解决了基本身份验证失败的问题,但并没有使用WebClient。我采用了@caesay的建议,并使用HttpWebRequest(与WebResponse一起)。我的表单提交会导致重定向,所以我必须允许这种情况。

这就是我实际采用的方法:

private bool ConfigureRouter()
{
    bool passed = false;
    string response = "";
    HttpWebRequest WEBREQ = null;
    WebResponse WEBRESP = null;            

    // Attempt to POST form to router that saves a new SSID.
    try
    {
        var uri = new Uri(FORM_SSID); // Create URI from URL string.
        WEBREQ = HttpWebRequest.Create(uri) as HttpWebRequest;

        // If POST will result in redirects, you won't see an "OK"
        // response if you don't allow those redirects
        WEBREQ.AllowAutoRedirect = true;

        // Basic authentication will first send the request without 
        // creds.  This is protocol standard.
        // When the server replies with 401, the HttpWebRequest will
        // automatically send the request again with the creds when
        // when PreAuthenticate is set.
        WEBREQ.PreAuthenticate = true;
        WEBREQ.AuthenticationLevel = System.Net.Security.AuthenticationLevel.MutualAuthRequested;

        // Mimic all headers known to satisfy the request
        // as discovered with a tool like Wireshark or Fiddler
        // when the form was submitted from a browser.
        WEBREQ.Method = "POST";
        WEBREQ.Accept = "text/html, application/xhtml+xml, */*";
        WEBREQ.Headers.Add("Accept-Language", "en-US"); // No AcceptLanguage property built-in to HttpWebRequest
        WEBREQ.UserAgent = USER_AGENT;
        WEBREQ.Referer = REF_SSID;
        WEBREQ.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
        WEBREQ.KeepAlive = true;
        WEBREQ.Headers.Add("Pragma", "no-cache"); // No Pragma property built-in to HttpWebRequest

        // Use a cached credential so that the creds are properly
        // submitted with subsequent redirect requests.
        CredentialCache creds = new CredentialCache();
        creds.Add(uri, "Basic", new NetworkCredential(config_user, config_pass));
        WEBREQ.Credentials = creds;

        // Submit the form.
        using (Stream stream = WEBREQ.GetRequestStream())
        {
            SSID ssid = new SSID(ssid_scanned); // Gets predefined form fields with new SSID inserted (NameValueCollection PostData)
            stream.Write(ssid.PostData, 0, ssid.PostData.Length);
        }

        // Get the response from the final redirect.
        WEBRESP = WEBREQ.GetResponse();
        response = ((HttpWebResponse)WEBRESP).StatusCode.ToString();
        if (response == "OK")
        {
            StatusUpdate("STATUS: SSID save was successful.");
            passed = true;
        }
        else
        {
            StatusUpdate("FAILED: SSID save was unsuccessful.");
            passed = false;
        }
        WEBRESP.Close();
    }
    catch (Exception ex)
    {
        StatusUpdate("ERROR: " + ex.Message);
        return false;
    }
    return passed;
}
1个回答

2
“我所尝试的事情是不可能的吗?”
不,这并不是不可能的。多年来,我在Web抓取方面也遇到了许多问题,因为有些Web服务器很挑剔,而您的路由器界面可能是一种定制的Web服务器实现,它不像Apache或IIS那样宽容。
我建议使用Wireshark捕获Chrome发送的原始数据包(包括有效负载等),然后对您的应用程序进行相同的捕获。确保数据包尽可能相似。如果仍然有问题,请将数据包捕获上传到Pastebin之类的网站以供查看。
编辑:
请尝试使用一些更低级别的项,而不是使用有限的WebClient API。我想知道以下代码是否适用于您:
var uri = new Uri("http://192.168.1.2/formWlanSetup.htm");
var cookies = new CookieContainer();
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(uri);
request.CookieContainer = cookies;
request.ServicePoint.Expect100Continue = false;
request.Method = "POST";
request.ContentType = "application/x-www-form-urlencoded";
request.UserAgent = "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko";
request.Referer = "http://192.168.1.2/formRedirect.htm?redirect-url=wlbasic.htm&wlan_id=0";
request.Credentials = new NetworkCredential(config_user, config_pass);
request.PreAuthenticate = true;
var response = request.GetResponse();
var reader = new StreamReader(response.GetResponseStream());
string htmlResponse = reader.ReadToEnd();

嗯,我将您的代码添加到了上面,并将uri传递给了UploadValues而不是字符串。现在我收到了一个WebClient异常:"在WebClient请求期间发生了异常"。内部异常:"必须使用适当的属性或方法修改'Content-Length'标头。\r\n参数名称:名称"。我的ContentLength标头现在出了些问题吗? - Solipcyst
@Solipcyst;哦,你不应该自己设置contentlength头...框架会自动将其设置为正确的值。 - caesay
感谢您迄今为止的帮助。我已经更新了我的帖子,并发现了一些有趣的新内容。如果您对如何从Wireshark中提取人类可读的“原始”数据有任何快速见解,我将非常乐意听取。这种事情超出了我的舒适区。我现在能做的最好的就是比较HTTP头和表单数据。 - Solipcyst
@Solipcyst:嘿,我有一个预感是什么出了问题,你为什么要使用UploadValues方法上传一个空的NameValueCollection?毕竟,如果没有要发布的值,浏览器不会将内容类型设置为application/x-www-form-urlencoded - caesay
我可能没有讲清楚NameValueCollection。这个列表并不是空的,它在这个方法之前的某个地方已经被定义过了。 - Solipcyst
感谢@caesay的建议。我已经放弃了WebClient,并且现在一切都正常工作了。更多信息请参见我的原始帖子。我将其标记为正确,因为虽然它没有解决使用WebClient完成此操作的想法,但它确实解决了完成此操作的问题。如果这是不允许的,我相信有人可以纠正。 - Solipcyst

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接