(2018.7) 実装をやり直し。スッキリ。
HTTP/2 を理解するために, 簡単に実装してみました。実用性は乏しいです。
このページでは, TLSで接続し, 暗号化します。
ソースコードの全部, Makefile
はこちら; network · master · netsphere / my-cpp-lib · GitLab
実装の方針
Linux と Windows (mingw32/64) でテストしています。
次の記事を出発点として利用しました; HTTP2最速実装をmain()関数だけで簡単に説明する(SSL編)
特に TLS版は, main()
に全部詰め込もうとすると, 非常に冗長になるため, 適宜, クラス・関数に分けて, 処理を纏めています。
実行結果
以降で説明するコードを実行すると, 次のようになります。できました!!
今回は, 決め打ちで, https://www.yahoo.co.jp/
にアクセスするようにしました。
TLS版は, 非TLS版と異なり, HTTP/1.1 からの upgrade はありません (できません)。TLSで接続してから, リクエストを送信します。
Connected to www.yahoo.co.jp
main:275 S payload length:0 bytes, type:SETTINGS, flags:00, stream_id:0
main:287 R payload length:12 bytes, type:SETTINGS, flags:00, stream_id:0
main:298 S payload length:0 bytes, type:SETTINGS, flags:01, stream_id:0
send_HEADERS_frame:197 S payload length:65 bytes, type:HEADERS, flags:04, stream_id:1
:authority: www.yahoo.co.jp
:method: GET
:path: /
:scheme: https
recv_streams:338 R payload length:4 bytes, type:WINDOW_UPDATE, flags:00, stream_id:0
recv_streams:338 R payload length:0 bytes, type:SETTINGS, flags:01, stream_id:0
recv_streams:338 R payload length:746 bytes, type:HEADERS, flags:04, stream_id:1
:status: 200
date: Sun, 01 Jul 2018 09:21:08 GMT
p3p: policyref="http://privacy.yahoo.co.jp/w3c/p3p_jp.xml", CP="CAO DSP COR CUR ADM DEV TAI PSA PSD IVAi IVDi CONi TELo OTPi OUR DELi SAMi OTRi UNRi PUBi IND PHY ONL UNI PUR FIN COM NAV INT DEM CNT STA POL HEA PRE GOV"
x-content-type-options: nosniff
x-xss-protection: 1; mode=block
x-frame-options: SAMEORIGIN
vary: Accept-Encoding
expires: -1
pragma: no-cache
cache-control: private, no-cache, no-store, must-revalidate
x-xrds-location: https://open.login.yahooapis.jp/openid20/www.yahoo.co.jp/xrds
content-type: text/html; charset=UTF-8
age: 0
via: http/1.1 edge2253.img.umd.yahoo.co.jp (ApacheTrafficServer [c sSf ])
server: ATS
set-cookie: TLS=v=1.2&r=1; path=/; domain=.yahoo.co.jp; Secure
recv_streams:338 R payload length:8183 bytes, type:DATA, flags:00, stream_id:1
====DATA====
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd====/DATA====
recv_streams:338 R payload length:5625 bytes, type:DATA, flags:00, stream_id:1
====DATA====
dWppLz9jcGlkPXByX3N1bmRheSZtZW51PXRvcHBhZ2UmdGFyPXRvcCZjcj10b3A-">ヤフオク!くじで落札額分====/DATA====
main:352 S payload length:0 bytes, type:GOAWAY, flags:00, stream_id:0
実装: TLSでの送信, 受信
main()
の解説を始める前に, TLS でのソケット通信の部分から。OpenSSL を使います。
標準C++の流儀のために, ソケット通信は, basic_streambuf
を拡張したものを使います; streambuf を拡張し, ソケット対応 [C++]
SocketStreamBuf
を拡張して, TLS で通信できるようにします。
SSL_write()
, SSL_read()
は, SSL_ERROR_WANT_WRITE
, SSL_ERROR_WANT_READ
の考慮が必要.
やや長くなりますが、全文を掲載します。
tls_socket_streambuf.h
:
C++
- #include "socket_streambuf.h"
-
- #include <openssl/ssl.h>
- #include <openssl/err.h>
- #include <openssl/x509v3.h>
-
-
-
- class TlsSocketStreamBuf: public SocketStreamBuf
- {
-
- SSL_CTX* m_ctx;
-
-
- SSL* m_ssl;
-
- typedef SocketStreamBuf super;
-
- public:
- TlsSocketStreamBuf(SSL_CTX* ctx): m_ctx(ctx), m_ssl(nullptr) {
- assert(ctx);
- }
-
- virtual ~TlsSocketStreamBuf() {
- close();
- }
-
-
-
-
-
-
-
-
- virtual void connected( SOCKET sock, const char* host ) {
- assert( sock != -1 );
- assert( host && *host );
- assert( m_ssl == nullptr );
-
- super::connected(sock, host);
-
- m_ssl = SSL_new(m_ctx);
- if ( !m_ssl ) {
- ERR_print_errors_fp(stderr);
- return;
- }
- SSL_set_fd(m_ssl, sock);
-
-
- SSL_set_hostflags(m_ssl, X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS);
- if ( !SSL_set1_host(m_ssl, host) ) {
- ERR_print_errors_fp(stderr);
- return ;
- }
-
-
-
-
-
-
-
- if ( !SSL_set_tlsext_host_name(m_ssl, host) ) {
- ERR_print_errors_fp(stderr);
- return ;
- }
- }
connected()
で TLS ハンドシェイクの事前設定を行います。OpenSSL はデフォルトでサーバ証明書内のホスト名の検証を行わないので, SSL_set1_host()
が必須です。また、SNI もデフォルトで有効になっていないので、呼び出します。
C++
- SSL* ssl() const {
- return m_ssl;
- }
-
-
-
- virtual int close() {
- if (m_ssl) {
-
- SSL_shutdown(m_ssl);
- SSL_free(m_ssl);
- m_ssl = nullptr;
- }
- return super::close();
- }
-
- protected:
- virtual ssize_t sysread(void* buffer, size_t length)
- {
- assert(m_ssl);
-
- while (true) {
- int r = SSL_read(m_ssl, buffer, length);
- if (r > 0)
- return r;
-
- int err = SSL_get_error(m_ssl, r);
- switch (err)
- {
- case SSL_ERROR_WANT_READ:
- continue;
- default:
- throw err;
- }
- }
- }
-
-
-
-
- virtual std::streamsize xsputn( const char_type* buffer,
- std::streamsize length )
- {
- if (!is_open())
- return 0;
-
- assert( m_ssl );
-
- while (true) {
- int r = SSL_write(m_ssl, buffer, length);
- if (r > 0)
- return r;
-
- int err = SSL_get_error(m_ssl, r);
- switch (err)
- {
- case SSL_ERROR_WANT_WRITE:
- continue;
- default:
- throw err;
- }
- }
- }
- };
HTTP/2実装
初期化 〜 接続
では, main()
の最初から。
OpenSSL v1.1.0 から, プロトコルの無効化の書き方が変わっています。v1.0.2 では SSL_CTX_set_min_proto_version()
がないので, 個別に無効化します。
ルートCA証明書は, 環境によって場所が違うので, 適宜調整してください。よくあるWebの解説では, 証明書の検証をすっ飛ばしていることがままありますが、ダメです。
C++
- int main(int argc, char* argv[])
- {
-
-
-
-
-
-
- unique_ptr<URI> uri( URI::parse("https://www.yahoo.co.jp/", nullptr) );
-
-
-
-
-
-
-
- #ifdef _WIN32
- WSADATA wsaData;
- if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
- return 1;
- #endif
-
-
-
-
-
-
- SSL_library_init();
-
-
- SSL_load_error_strings();
-
-
- #if OPENSSL_VERSION_NUMBER >= 0x10100000L
-
- SSL_CTX* _ctx = SSL_CTX_new( TLS_client_method() );
-
-
- SSL_CTX_set_min_proto_version(_ctx, TLS1_1_VERSION);
- #else
-
- SSL_CTX* _ctx = SSL_CTX_new( SSLv23_client_method() );
- SSL_CTX_set_options(_ctx,
- SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_NO_TLSv1);
- #endif
-
-
-
- int r = SSL_CTX_load_verify_locations(_ctx, CA_BUNDLE_FILE, nullptr);
- if (!r) {
- ERR_print_errors_fp(stderr);
- fprintf(stderr, "loading certificate failed.\n");
- return 1;
- }
-
-
-
-
- addrinfo* res = ngethostbyname(uri->getHost().c_str(), uri->getPort(),
- SOCK_STREAM);
- if (!res) {
- fprintf(stderr, "host not found: %s\n", uri->getHost().c_str());
- return 1;
- }
-
- int error = 0;
-
- SOCKET _socket = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
- if (_socket < 0) {
- fprintf(stderr, "Fail to create a socket.\n");
- freeaddrinfo(res);
- return 1;
- }
-
-
-
- int flag = 1;
- r = setsockopt(_socket, IPPROTO_TCP, TCP_NODELAY, (char*) &flag, sizeof(flag));
- if ( r < 0 ) {
- error = get_error();
-
-
- return 1;
- }
-
- if (connect(_socket, res->ai_addr, res->ai_addrlen) < 0 ) {
- fprintf(stderr, "connect() error.\n");
- freeaddrinfo(res);
- return 1;
- }
-
-
- freeaddrinfo(res);
- res = nullptr;
-
-
- SSL* _ssl = SSL_new(_ctx);
-
-
- r = SSL_set_fd(_ssl, _socket);
- if (!r) {
- ERR_print_errors_fp(stderr);
- return 1;
- }
-
- TlsSocketStreamBuf conn;
- conn.attach(_socket, _ssl);
- Http2Reader reader;
- reader.attach(&conn);
ハンドシェイク
TLS には ALPN という仕組みがあり, クライアントから対応プロトコルの一覧を送信し, サーバがそのなかから選ぶことで, 最初から合意したプロトコルを確立できます。
"h2"
が HTTP/2 over TLS です。
サーバから返信された対応リストに "h2"
がない場合は, HTTP/1.1 になります。
Web上のサンプルでは忘れられがちですが, サーバ証明書の検証は必須です。X509_free()
で解放を忘れずに。これを抜かすと, SSL_free()
でメモリリークします。
C++
-
-
-
-
-
-
- SSL_set_alpn_protos(_ssl, protos, protos_len + 1);
-
-
- if (SSL_connect(_ssl) <= 0){
- error = get_error();
- conn.close();
- return 1;
- }
-
-
-
-
- X509* x509 = SSL_get_peer_certificate(_ssl);
- if ( !x509 ) {
- fprintf(stderr, "certificate missing.\n");
- conn.close();
- return 1;
- }
-
-
-
-
-
- r = SSL_get_verify_result(_ssl);
- if (r != X509_V_OK) {
- fprintf(stderr, "verification failed.\n");
- conn.close();
- return 1;
- }
- printf("Connected to %s\n", uri->getHost().c_str());
-
- X509_free(x509);
-
-
- const unsigned char* ret_alpn = nullptr;
- unsigned int alpn_len = 0;
- SSL_get0_alpn_selected(_ssl, &ret_alpn, &alpn_len);
- if (!ret_alpn || !alpn_len) {
- printf("Not HTTP/2\n");
- return 1;
- }
- if (alpn_len != protos_len ||
- memcmp(ret_alpn, protos + 1, alpn_len) != 0){
- error = get_error();
- conn.close();
- return 1;
- }
後は, ほぼほぼ, 非TLS版と同じです。最初の HTTP/1.1 リクエストがないので, 下の HEADERS
フレームが最初のリクエストになります。
C++
-
-
-
- string pri = CLIENT_CONNECTION_PREFACE;
- r = conn.sputn(pri.c_str(), pri.length() );
- if (r < 0) {
- error = get_error();
- conn.close();
- return 1;
- }
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- const char settingframe[FRAME_HDR_LENGTH] = {
- 0x00, 0x00, 0x00,
- 0x04,
- 0x00, 0x00, 0x00, 0x00, 0x00};
- dump_frame_header('S', settingframe);
- r = conn.sputn(settingframe, FRAME_HDR_LENGTH);
- if ( r < 0 ) {
- error = get_error();
- conn.close();
- return 1;
- }
-
-
-
-
- char* frame = reader.read_frame();
- dump_frame_header('R', frame);
- assert(*(frame + 3) == 0x4);
-
-
-
-
-
-
-
-
-
- dump_frame_header('S', settingframeAck);
- r = conn.sputn(settingframeAck, FRAME_HDR_LENGTH);
- if ( r < 0 ) {
- error = get_error();
- conn.close();
- return 1;
- }
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- map<string, string> m;
- m.insert(make_pair(":method", "GET"));
- m.insert(make_pair(":path", uri->getPath()) );
- m.insert(make_pair(":scheme", "https"));
- m.insert(make_pair(":authority", uri->getHost()));
- r = send_HEADERS_frame(&conn, 1, m);
- if ( r < 0 ) {
- int error = get_error();
- conn.close();
- return 1;
- }
-
-
- recv_streams(reader);
-
- recv_streams(reader);
recv_streams()
については, 共通ルーチンのページをご覧ください。2回呼び出しているのは, DATA
フレームが一つのフレームに収まらず, 複数に分割されたフレームを読むためです。
後処理
あとは, 後処理です。
C++
-
-
-
-
-
-
-
- static const char goawayframe[17] = {
- 0x00, 0x00, 0x00,
- 0x07,
- 0x00, 0x00, 0x00, 0x00, 0x00,
- 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00 };
- dump_frame_header('S', goawayframe);
- r = conn.sputn( goawayframe, 17 );
- if (r < 0 ) {
- error = get_error();
- conn.close();
- return 1;
- }
-
-
-
-
- conn.close();
-
- SSL_CTX_free(_ctx);
- ERR_free_strings();
-
- return 0;
- }