前回の[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()
関数は使用するプロトコルの種類です。
いくつか種類があって、SSLv2
、SSLv3
、TLSv1
があります。
今回はこのいずれかを利用するため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;