C
openssl

[C言語] HTTPSクライアントを作ってみる

More than 3 years have passed since last update.

前回の[C言語] HTTPクライアントを作ってみるの続きです。

こちらの記事(C 言語: OpenSSL を使って HTTPS クライアントをつくる)を参考にさせていただきました。

まずは写経してコンパイル・・と思ったらdeprecatedのwarningの山がッ。

どうやらMac標準で入ってるものを使うとそうなるらしい。

こちらの記事(How to solve this error 'BIO_new' is deprecated in cocoa?)を参考に、OpenSSLのサイトから最新版を落としてきてコンパイルしたらwarningでなくなった。

ちなみに記事中に書いてあるパスと違うところにインストールされたので、以下のようにしてビルドしました。

$ gcc -g https-client.c -o https-client -L/usr/local/lib -I/usr/local/include -lssl -lcrypto

最後にソースコード全文を載せています。


基本はHTTP

前回作ったHTTPクライアントで行っているコネクション処理はそのまま使います。

処理の流れとしては、まずは通常のHTTPコネクションを張り、そのあとでOpenSSLを使ってソケットとSSLを結びつける、という手順で実装を行います。

なので今回の例のコネクションの例は以下のように、前回とほぼ同じになります。

char *host = "web.lobi.co";

char *path = "/";
int port = 443;

// IPアドレスの解決
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
char *service = "https";

int err = 0;
if ((err = getaddrinfo(host, service, &hints, &res)) != 0) {
fprintf(stderr, "Failed to resolve an ip address - %d\n", err);
return EXIT_FAILURE;
}

if ((mysocket = socket(res->ai_family, res->ai_socktype, res->ai_protocol)) < 0) {
fprintf(stderr, "Failed to create a socket.\n");
return EXIT_FAILURE;
}

if (connect(mysocket, res->ai_addr, res->ai_addrlen) != 0) {
printf("Connection error.\n");
return EXIT_FAILURE;
}

char *service = "https";と、int port = 443;としているところ以外はほぼ同じですね。


SSLのセットアップ

コネクションが成功したら、次にSSLのセットアップを行います。

セットアップ手順は以下のようになります。

// エラー文言の読み込みとライブラリの初期化

SSL_load_error_strings();
SSL_library_init();

SSL_load_error_strings()でOpenSSLのエラー文言を読み込みます。

これは、OpenSSLではエラーが起こった際、エラー内容が数値で管理されているためその数値とひもづけることでエラーの詳細が分かる、という具合になっています。

(OpenGLでも似たようなアプローチでエラーをハンドリングしますね。きっと管理しやすいのでしょう)

次のSSL_library_init()はSSLの初期化を行います。

これを実行することで、暗号化方式やメッセージダイジェスト関数などが登録されます。

// SSL_CTX構造体の生成

ctx = SSL_CTX_new(SSLv23_client_method());

初期化が終わったらいよいよSSLの構造体を作ります。

ctx = SSL_CTX_new(SSLv23_client_method());の部分がSSL_CTX構造体の生成です。(CTXはcontextだろうか?)

newとか付いているあたり、オブジェクト指向的な匂いがしますw

引数に渡しているSSLv23_client_method()関数は使用するプロトコルの種類です。

いくつか種類があって、SSLv2SSLv3TLSv1があります。

今回はこのいずれかを利用するためSSLv23_client_method()を実行します。

もしどれかを明示的に使う場合は


  • SSLv2_client_method()

  • SSLv3_client_method()

  • TLSv1_client_method()

のいずれかを実行します。

// SSL構造体の生成

ssl = SSL_new(ctx);

SSL_newの引数に、直前で生成したSSL_CTX構造体にセットしたプロトコルや暗号化方式を元に、コネクションを管理するSSL構造体を生成します。

SSLはサーバとのコネクションの管理、SSL_CTXはSSLにおける暗号化や認証方法などを管理します。

SSLがサーバとのコネクションをするインスタンス、SSL_CTXがその制御のためのモデルと考えると分かりやすいかもしれません。

(同じ認証方式で2サーバにコネクション張る場合は、SSLは2つ必要だけど、SSL_CTXはひとつでいい、みたいな)

// SSL構造体とソケットとの関連付け

int ret = SSL_set_fd(ssl, mysocket);
if (ret == 0) {
ERR_print_errors_fp(stderr);
return EXIT_FAILURE;
}

SSL_set_fb()を使って、コネクション済のソケットとSSL構造体を結びつけます。

(ちなみにコネクションに失敗したソケットを指定するとエラーとなります)

// コネクション

int ret = SSL_connect(ssl);
if (ret != 1) {
ERR_print_errors_fp(stderr);
return EXIT_FAILURE;
}

SSL_connectを呼ぶことで、自動的にサーバとハンドシェイクが行われます。

この時点で以下のことが決定します。


  • 使用するプロトコル(SSLv2 / SSLv3 / TLSv1)

  • 使用する暗号化方式・鍵交換方式・ハッシュ方式

  • サーバ証明書の取得

  • 使用する共通鍵

以上でSSLでのコネクションが確立しました。

あとは通常と同じようにHTTPリクエストを送ります。

sprintf(msg, "GET %s HTTP/1.1\r\nHost: %s\r\n\r\n", path, host);

SSL_write(ssl, msg, strlen(msg));

ただし、通常のHTTP接続ではwriteを使っていたものをSSL_writeにしています。

引数は似たような感じになっていて、ssl構造体がソケットの役割を果たし、あとはメッセージとその長さを渡して実行しています。

また読み込みもread関数の代わりにSSL_readを使うだけで、それ以外はほぼ同じとなります。

do {

read_size = SSL_read(ssl, buf, buf_size);
write(1, buf, read_size);
} while(read_size > 0);


後処理

通常のHTTP通信ではソケットを閉じるだけでしたが、今回はSSLについての処理を行っているので、それらも適切に処理しないとなりません。

SSL_shutdown(ssl);

SSL_free(ssl);
SSL_CTX_free(ctx);
ERR_free_strings();

close(mysocket);

SSLを閉じて、それぞれの構造体をfreeしています。

最後にソケットを閉じて処理が終了となります。



ソースコード全体

今回実装したデモは以下になります。

#include <stdio.h>

#include <string.h>
#include <stdlib.h>
#include <unistd.h>

#include <netdb.h>
#include <sys/socket.h>
#include <netinet/in.h>

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

int main(void) {
int mysocket;
struct sockaddr_in server;
struct addrinfo hints, *res;

SSL *ssl;
SSL_CTX *ctx;

char msg[100];

char *host = "web.lobi.co";
char *path = "/";
int port = 443;

// IPアドレスの解決
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
char *service = "https";

int err = 0;
if ((err = getaddrinfo(host, service, &hints, &res)) != 0) {
fprintf(stderr, "Fail to resolve ip address - %d\n", err);
return EXIT_FAILURE;
}

if ((mysocket = socket(res->ai_family, res->ai_socktype, res->ai_protocol)) < 0) {
fprintf(stderr, "Fail to create a socket.\n");
return EXIT_FAILURE;
}

if (connect(mysocket, res->ai_addr, res->ai_addrlen) != 0) {
printf("Connection error.\n");
return EXIT_FAILURE;
}

SSL_load_error_strings();
SSL_library_init();

ctx = SSL_CTX_new(SSLv23_client_method());
ssl = SSL_new(ctx);
err = SSL_set_fd(ssl, mysocket);
SSL_connect(ssl);

printf("Conntect to %s\n", host);

sprintf(msg, "GET %s HTTP/1.0\r\nHost: %s\r\n\r\n", path, host);

SSL_write(ssl, msg, strlen(msg));

int buf_size = 256;
char buf[buf_size];
int read_size;

do {
read_size = SSL_read(ssl, buf, buf_size);
write(1, buf, read_size);
} while(read_size > 0);

SSL_shutdown(ssl);
SSL_free(ssl);
SSL_CTX_free(ctx);
ERR_free_strings();

close(mysocket);

return EXIT_SUCCESS;
}



スタックからのエラー取得

エラーの取得にはいくつか方法があります。

簡単にエラーを標準出力やファイルに出力する場合はERR_print_errors_fp()関数を使います。

if (ssl == NULL) {

ERR_print_errors_fp(stderr);
return EXIT_FAILURE;
}

あるいは以下の関数を使って出力することもできます。

関数名
説明

ERR_reason_error_string
静的ストリングへのポインターを返します。この静的ストリングは、画面に表示したり、ファイルに書き込んだりするなど、任意の方法で処理することができます。

ERR_lib_error_string
エラーが発生したライブラリーを通知します。

ERR_func_error_string
エラーの原因となった OpenSSL 関数を返します。

同じ動作をさせる場合は以下のようにします。(出力内容は若干異なります)

const char *err = ERR_reason_error_string(ERR_get_error());

printf("%s\n", err);
return EXIT_FAILURE;