首页 > 解决方案 > 使用 Winsock2 向具有域 HTTPS 的 Webstie 发送 GET 请求

问题描述

我正在尝试向 Web 服务器发出 GET 请求,我已经成功地访问了具有 HTTP 域的网站。剩下的问题是我无法成功向具有 HTTPS 域的网站发送请求。

我必须在 C++ 中使用 Winsock 来完成这个任务。这是我已经成功实现 HTTP 的代码:

void GET_request(URL url)
{
    WSADATA wsaData;
    SOCKET Socket;
    SOCKADDR_IN SockAddr;
    struct hostent* localHost;
    string request_header;
    char* localIP;
    int find_index;
    url.URL_detach();

    //Create a Request Header.
    request_header = "GET "+ url.path + " HTTP/1.0\r\n";
    request_header += "Host: " + url.host + "\r\n";
    request_header += "Connection: close\r\n";
    request_header += "Cache-Control: max-age=0\r\n";
    request_header += "Upgrade-Insecure-Requests: 1\r\n";
    request_header += "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36\r\n";
    request_header += "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\r\n";
    request_header += "Accept-Encoding: gzip, deflate\r\n";
    request_header += "\r\n";

    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
        cout << "WSAStartup failed.\n";
        system("pause");
    }

    Socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);         

    localHost = gethostbyname(url.host.c_str());                
    localIP = inet_ntoa(*(struct in_addr *)*localHost->h_addr_list);        //Return IP of HOST
    SockAddr.sin_port = htons(80);                                          //PORT 80 (HTTP)
    SockAddr.sin_family = AF_INET;                                          // TCP/IP
    SockAddr.sin_addr.s_addr = inet_addr(localIP);                          

    if (connect(Socket, (SOCKADDR*)(&SockAddr), sizeof(SockAddr)) != 0)     
    {
        cout << "Could not connect to Server";
        system("pause");
    }

    send(Socket, request_header.c_str(), strlen(request_header.c_str()), 0);    


    int nDataLength;
    while ((nDataLength = recv(Socket, buffer, 10000, 0)) > 0) {
        int i = 0;
        while (buffer[i] >= 32 || buffer[i] == '\n' || buffer[i] == '\r') {
            Response_Header += buffer[i];
            i += 1;
        }
        find_index = Response_Header.find("</html>");
    }
    Response_Header.erase(find_index + 7);
    closesocket(Socket);
    WSACleanup();

}

标签: c++socketshttpswinsock2

解决方案


让我感谢hugtech提供的问题、代码和使用 HTTP 协议的工作。也非常感谢Remy Lebeau向我们介绍了 SSL 和 TLS 握手。我的研究从他们那里起飞了。

在我的例子中,发送原始 HTTP GET 命令导致 WinSock send(...) 以零失败。想象一下,我什至不允许发送那些未加密的东西。使用从站点复制的 ClientHello 后,我让www.youtube.com向我发送了一个 ServerHello。我不断地调整、编译我的代码并进行测试;我希望 Youtube 不会感到沮丧并要求验证码。

然后我能够成功修改 ClientHello。我最后简单地取出了扩展,因为它们是可选的,而且我不会支持它们。并修改了长度变量以适应调整。

// Client Hello I copied from the site
//uint8_t clientHello[] = { 0x16, 0x03, 0x01, 0x00, 0xa5, 0x01, 0x00, 0x00, 0xa1, 0x03, 0x03, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x00, 0x00, 0x20, 0xcc, 0xa8, 0xcc, 0xa9, 0xc0, 0x2f, 0xc0, 0x30, 0xc0, 0x2b, 0xc0, 0x2c, 0xc0, 0x13, 0xc0, 0x09, 0xc0, 0x14, 0xc0, 0x0a, 0x00, 0x9c, 0x00, 0x9d, 0x00, 0x2f, 0x00, 0x35, 0xc0, 0x12, 0x00, 0x0a, 0x01, 0x00, 0x00, 0x58, 0x00, 0x00, 0x00, 0x18, 0x00, 0x16, 0x00, 0x00, 0x13, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x75, 0x6c, 0x66, 0x68, 0x65, 0x69, 0x6d, 0x2e, 0x6e, 0x65, 0x74, 0x00, 0x05, 0x00, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x0a, 0x00, 0x08, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x18, 0x00, 0x19, 0x00, 0x0b, 0x00, 0x02, 0x01, 0x00, 0x00, 0x0d, 0x00, 0x12, 0x00, 0x10, 0x04, 0x01, 0x04, 0x03, 0x05, 0x01, 0x05, 0x03, 0x06, 0x01, 0x06, 0x03, 0x02, 0x01, 0x02, 0x03, 0xff, 0x01, 0x00, 0x01, 0x00, 0x00, 0x12, 0x00, 0x00, };

// modified client hello
// unnecessary extension code has been erased,
// just as how stack overflow moderators will erase
// my unnecessary wordings and sentencing.
uint8_t clientHello[] =
{
 //Record Header
    0x16, //type: handshake
    0x03, 0x01, //TLS version:
    0x00, 75, //HandShake Length

 //Handshake Header
    0x01, //type: client hello
    0x00, 0x00, 71, //handshake data len

 // Client Version
    0x03, 0x03, // TLS 1.2 //actual TLS version client uses

 // Client Random Garbage 
 // Here I see 32 bytes being used; but I don't know if 32 bytes is mandatory.
 // But if it is not mandatory how would one know when the random garbage ends? 
    0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
    0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,

 //Session ID
 // This ID is used to logon back from a previous session.
 // This eliminates certain computations as per what I read.
 // Here we use zero to say this is a fresh session with the server.
    0x00,

 // Cypher Suites
 // A list of Cypher Suite used to do the encryption. 
 // Cypher Suites are represented by two bytes.
 // Example 0xCCA8 is TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 Cypher Suite.
    0x00, 0x20, // count of bytes
    0xcc, 0xa8, 0xcc, 0xa9, 0xc0, 0x2f, 0xc0, 0x30, 0xc0, 0x2b, 0xc0, 0x2c, 0xc0, 0x13, 0xc0, 0x09,
    0xc0, 0x14, 0xc0, 0x0a, 0x00, 0x9c, 0x00, 0x9d, 0x00, 0x2f, 0x00, 0x35, 0xc0, 0x12, 0x00, 0x0a,

 // Compression Methods
 // A list of compression methods.
 // Here we have only one compression method which is zero for no compression.
    0x01, //bytes of compression methods
    0x00,

// After this we have one long list of Extension information which
// I have no interest in at all.
};

size_t clientHelloLen = sizeof(clientHello);

待续...
ServerHello

我已经阅读了 Joshua Davis 的书 350 页,Implementing SSL_TLS,事实证明 TLS 握手比任何人都解释的要复杂得多。我设法将自己的 ClientHello 消息准备到www.youtube.com并取回有效的 ServerHello 和 ServerCertificate(s)。但是当我尝试使用www.example.orgwww.apachefriends.org时,我没有收到任何消息。然后我尝试了从https://tls.ulfheim.net/复制的原始 ClientHello我得到了一个格式正确的 ServerHello 和 ServerCertificate,我可以用我的自定义例程打印它们。问题是从服务器获得响应需要大约一分钟;我注意到证书看起来很可疑。它由同一个实体发行和发行。它还指出这是一个假证书控制。

然后我使用 OpenSSL 运行以下命令。openssl s_client -connect "www.apachefriends.org:https". 我立即收到了服务器的回复!这意味着该站点正在拒绝我的自定义 ClientHello 消息。这意味着我浪费了我的周末学习 Joshua Davis 的书,而应该阅读一本展示如何使用 OpenSSL 的书。

更新:
我终于可以开始工作了,但是 HTTPS、TLS/SSL 握手实在是太复杂了。我很生气,我研究了 ClientHello、ServerHello、ServerCertificate 的详细信息,但根本没有用。所有这些都被 OpenSSL 抽象出来了。并且出于某种原因,网站拒绝接受我的自定义 ClientHello,即使我完全按照 Joshua Davis 展示的那样做。我也对 OpenSSL 抽象出 Linux 和 Win32 SOCKETS 感到难过;所以学习这些细节只是为了学术。

以下是我使用 OpenSSL 从服务器获取文件的代码。请注意,在下载文件时,服务器通常会将您重定向到实际拥有该文件的另一台服务器。在这里,我对该重定向进行了硬编码。

#include <Jav/test.h> // rep(...)

// My custom library for handling files
// You can substitute this with std::fstream, FILE*, or
// any other file library
#include <Jav/file/file.h>

// Custom exception class, Jav::Error
// derived from std::exception
#include <Jav/error/error.h>

//Trying to get _fileno to be defined
// as it is needed by openssl/applink.c
#undef __STRICT_ANSI__ 

// Undefing strict ansi failed so I simply
// declared _fileno
extern "C"{
_CRTIMP int __cdecl __MINGW_NOTHROW _fileno (FILE*);
}

# include  <openssl/bio.h>
# include  <openssl/ssl.h>
# include  <openssl/err.h>

// Yep we actually include a source file. 
// Other wise openssl gives "no app link" error during runtime.
// I tried adding applink.c to src instead of including it
// but got linker errors.
# include  <openssl/applink.c>

void init_openssl_library()
{
 SSL_library_init();
 SSL_load_error_strings();
 OPENSSL_config(NULL);
}

void on_openssl_fail(int line)
{
 ERR_print_errors_fp(stderr);
 throw Jav::Error("problem at line %i\n",line);
}

#define HOST_REDIRECTED "redirectedexamle.org" 
#define RESOURCE        "/exampleresource.txt"

int main()
{
    SSL_CTX *ctx = NULL;
    BIO *web = NULL, *out = NULL;
    SSL *ssl = NULL;

    init_openssl_library();

    const SSL_METHOD *method = SSLv23_method();
    if(method == NULL) on_openssl_fail(__LINE__);

    ctx = SSL_CTX_new(method);
    if(ctx == NULL) on_openssl_fail(__LINE__);

    // Cannot fail???
    //SSL_CTX_set_verify(ctx,SSL_VERIFY_PEER,verify_callback);
    //SSL_CTX_set_verify_depth(ctx,4);

    const long flags = SSL_OP_NO_SSLv2|SSL_OP_NO_SSLv3|SSL_OP_NO_COMPRESSION;
    SSL_CTX_set_options(ctx,flags);

    web = BIO_new_ssl_connect(ctx);
    if(web == NULL) on_openssl_fail(__LINE__);

    if(BIO_set_conn_hostname(web,HOST":https") != 1)
        on_openssl_fail(__LINE__);

    BIO_get_ssl(web,&ssl);
    if(ssl == NULL) on_openssl_fail(__LINE__);

    const char *PREFERRED_CIPHERS = "HIGH:!aNULL:!kRSA:!PSK:!SRP:!MD5:!RC4";
    if(SSL_set_cipher_list(ssl,PREFERRED_CIPHERS) != 1)
        on_openssl_fail(__LINE__);

    if(SSL_set_tlsext_host_name(ssl,HOST) != 1)
        on_openssl_fail(__LINE__);

    //out = BIO_new_fp(stdout,BIO_NOCLOSE);
    //if(out == NULL) on_openssl_fail(__LINE__);

    if(BIO_do_connect(web) != 1) on_openssl_fail(__LINE__);
    if(BIO_do_handshake(web) != 1) on_openssl_fail(__LINE__);

    X509 *cert = SSL_get_peer_certificate(ssl);
    if(cert == NULL) on_openssl_fail(__LINE__);
    X509_free(cert);

    //if(SSL_get_verify_result(ssl) != X509_V_OK) on_openssl_fail(__LINE__);

    int len = 10000;
    Jav::cstring buf(len);

    auto cmd = "GET "RESOURCE" HTTP/1.1\r\n"
               "Host: "HOST"\r\n"
               "Connection: close\r\n\r\n";
      
    if(BIO_puts(web, cmd) <= 0)
    {
        rep("bio write failed");

        if(! BIO_should_retry(web))
        {
          rep("bio should retry");
        }
        else return 1;
    }

    int total = 0;
    Jav::wFile file = Jav::createNewFile<Jav::wFile>("download.txt");

    while(true)
    {
        int numRead = BIO_read(web, buf, len);
        total += numRead;

        if(numRead == 0)
        {
         rep("No more data read");
         if(total) break;
         else return 1;
        }
        else if(numRead < 0)
        {
            rep("bio read failed");

            if(! BIO_should_retry(web))
            {
              rep("bio should retry");
        int numRead = BIO_read(out, buf, len);
        rep("num read",numRead);
            on_openssl_fail(__LINE__);
            }
            else return 1;
        }
        else rep("num read",numRead);

        file.write(buf,numRead);
    }

//Jav::wFile file = Jav::createFile<Jav::wFile>("output.txt");
//file.write(buf,numRead);
    BIO_free(out);
    BIO_free(web);
    SSL_CTX_free(ctx);
}

推荐阅读