OpenSSL Library - BIO 概论

转自:http://blog.roodo.com/rocksaying/archives/16263025.html


BIO 是 OpenSSL 庫為了處理資料輸出入所設計的輸出入抽象層,參考《bio(3) 》的說明。 OpenSSL 的程式碼經常利用 BIO 的多形性,故在使用 OpenSSL 開發應用程式時,必須先熟悉 BIO。

BIO 的設計模式是 C 語言 (不是C++) 實作個體導向程式設計多形性(polymorphism of OOP)時常見的設計方式。 在早期,程序員學了 OOP 的觀念可是還是要寫 C 程式的時代,我們需自己用 C 語言實作類別繼承、動態連結等內容。但我們用的是 C compiler 而非 C++ compiler ,所以很多事我們必須自己處理。 因此它們的程式碼與近代 C++ 式的表達方式有所差異。 例如我在《程式語言中的介面,在個體之間協議互動行為的多種形式》說的作法就是一例;GNOME Library 也是這種用 C 語言寫出來的「類別庫」。 所以 BIO 實際上是一種類別庫。

BIO 類別輪廓

既然 BIO 是一種類別庫,那麼我們最好還是用看待類別的方式去看 BIO ,才容易看清它的輪廓。 本節將使用 C++ 的語法表達 BIO 的類別內容,以便理解其繼承與多形關係。 為了方便各位參考 OpenSSL 說明文件草稿,我在類別名稱第一次出現的地方,都會在其後用角括號寫出其 C 語言的原名與文件連結。原名名稱括弧內的數字,是 Unix man page 的表達習慣,表示那屬於 man page 的第幾號分類。

因為 BIO 類別庫的內容非常多,本節只是選了常用的幾個來表達。完整的內容請查看 OpenSSL 的 bio 標頭文件 (/usr/include/openssl/bio*.h)。

BIO 基礎類別

BIO [BIO_new(3) ] 類別是所有 BIO 類別庫的基礎類別。它宣告了 BIO_METHOD 介面。所有 BIO 子類別都必須實作此介面。

在《程式語言中的介面,在個體之間協議互動行為的多種形式》說到介面宣告在 C 語言眼中其實就是一個以結構型態定義多個函數指標成員的「函數表」。BIO_METHOD 正是這種作為「介面」的函數表。程序員想配置新的 BIO 個體時,可以調用 BIO 子類別的建構子得之,亦可將子類別實作的介面傳遞給 BIO 類別的建構子得之。「介面」可以當成參數傳遞,這一點是初學者比較難以理解的特性。

bio.hpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class BIO_METHOD {
     int type;
     virtual int read( void *data, int len);
     virtual int gets ( char *buf, int size);
     virtual int write( const void *data, int len);
     virtual int puts ( const char *buf);
};
 
class BIO : BIO_METHOD {
   public :
     BIO(BIO_METHOD *type); // BIO_new()
     /*
     BIO_METHOD 是一個函數指標表,其用途即 OOPL 的 interface,
     每一個函數指標即 interface 中的抽象函數,
     當建構實體時,才會將指標指向實際的函數。
     最初的 C 語言便是透過這種模式實作繼承機制。
     type:
         BIO_s_file()
         BIO_s_mem()
         and more...
     */
 
     ~BIO(); // BIO_free()
 
     int read( void *data, int len); // BIO_read()
     int gets ( char *buf, int size); // BIO_gets()
     int write( const void *data, int len); // BIO_write()
     int puts ( const char *buf);  // BIO_puts()
     // manpage fread(),fgets(),fwrite(),fputs()
     
     int tell(); // BIO_tell()
     int seek( long offset); // BIO_seek()
     // manpage fseek(3), ftell(3)
 
     int printf (...); // BIO_printf()
     int snprintf(...); // BIO_snprintf()
     // manpage printf(3)
     
     int ctrl(...); // BIO_ctrl()
};
BIO_file 與 BIO_fd 類別

BIO_file [BIO_s_file(3) ] 是BIO 的子類別之一,它對應 ANSI C 標準庫的 FILE 處理函數。其主要對象是檔案系統中的文件與標準輸出入設備(stdin, stdout, stderr)。

BIO_file 定義了兩個建構旗標: BIO_CLOSE 與 BIO_NOCLOSE,用以表明解構個體時,其 FILE 對象是否需要關閉。通常 FILE 對象為標準輸出入設備時,因為它們是由系統開啟,所以都要使用 BIO_NOCLOSE 選項表示不要關閉。

BIO_fd [BIO_s_fd(3) ] 對應 POSIX 庫的檔案描述子(file descriptor)處理函數。其對象是所有可用檔案描述子開啟的設備。 它也同樣使用 BIO_CLOSE 與 BIO_NOCLOSE 表明解構時是否需要關閉設備。

bio_file.hpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// close_flag
#define BIO_NOCLOSE 0
#define BIO_CLOSE 1
 
class BIO_file: public BIO {
   public :
     BIO_file( FILE *stream, int close_flag); // BIO_new_fp()
     BIO_file( const char *filename, const char *mode); // BIO_new_file()
     // manpage fopen(3)
};
 
class BIO_fd: public BIO {
   public :
     BIO_fd( int fd, int close_flag); // BIO_new_fd()
     // manpage open(2)
};
BIO_mem 類別

BIO_mem [BIO_s_mem(3) ] 的對象是記憶體區塊。它把記憶體區塊當成一個設備,對它進行的讀取與寫入動作實際上是記憶體的資料複製行為。 配合大部份的 C 語言函數需要直接傳遞記憶體區塊的指標,故它也定義了一個 get_mem_ptr() 方法。

bio_mem.hpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class BIO_mem: public BIO {
   public :
     BIO_mem(); // BIO_new(BIO_s_mem())
 
     BIO_mem( void *buf, int len); // BIO_new_mem_buf()
     
     struct BUF_MEM {
         void *data;
         int length;
         int max;
     };
     
     int get_mem_ptr(BUF_MEM **ptr);
};

BIO_mem 的好處在於會自已配置並維護它持有的記憶體區塊,並隨寫入的資料量主動調整區塊大小。

除此之外,它還有一個比較特殊的預設行為,當你指定一個已配置的記憶體區塊給它時,它會是一個唯讀設備。此時你只能透過它從該記憶體區塊中讀取資料,但不能透過它寫入(改變)該區塊的內容。例如:

1
2
3
4
5
6
char buf[4096]; // fixed-size memory block
BIO *bio1 = new BIO_mem(buf, sizeof (buf)); // read-only
bio1-> puts ( "hello" ); // nothing changed.
 
BIO *bio2 = new BIO(BIO_s_mem()); // dynamic-allocate memory block
bio2-> puts ( "hello" ); // store "hello" into the memory block.
BIO_socket 等類別

BIO_socket [BIO_s_socket(3) ] 對應了 socket 設備處理函數,其對象是 socket 型態為 SOCK_STREAM 的設備。 另外還有 BIO_dgram 處理 socket 型態為 SOCK_DGRAM 的設備; BIO_accept [BIO_s_accept(3) ] 的對象是 socket 函數 accept(2) 開啟的設備;BIO_connect [BIO_s_connect(3) ] 的對象是 socket 函數 connect(2) 開啟的設備。

bio_socket.hpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class BIO_socket: public BIO {
   public :
     BIO_socket( int sockfd, int close_flag); // BIO_new_socket()
     // manpage socket(2), type SOCK_STREAM
};
 
class BIO_dgram: public BIO {
   public :
     BIO_dgram( int fd, int close_flag);  
     // manpage socket(2), type SOCK_DGRAM
};
 
class BIO_connect: public BIO {
   public :
     BIO_connect( char *host_port);
     // manpage connect(2)
};
 
class BIO_accept: public BIO {
   public :
     BIO_accept( char *host_port);        
     // manpage accept(2)
};
使用 BIO 類別
基本操作

我將寫一個基本的範例程式,分別用 BIO_file, BIO_mem 與 BIO_fd 類別開啟4個設備,並寫入一行文字。

我首先用 C++ 語法寫出範例程式的內容。接著再改寫為 C 語法。

C++ 偽碼

bio_pseudo.cpp 是用 C++ 語法表達 OpenSSL BIO 類別內容的偽碼,故雖可編譯但不能產生執行檔。

bio_pseudo.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// gcc -c bio_pseudo.cpp
#include <cstdio>
#include "bio.hpp"
#include "bio_file.hpp"
#include "bio_mem.hpp"
 
int foo(BIO *bio, const char *msg) {
     return bio-> puts (msg);
}
 
int main() {
     BIO *bio1 = new BIO_file(stdin, BIO_NOCLOSE);
     BIO *bio2 = new BIO_file( "/tmp/abc" , "w" );
     
     // dynamic allocate memory block.
     BIO_mem *bio3 = new BIO_mem();
 
     BIO *bio4 = new BIO_fd(1, BIO_NOCLOSE); // 1 is stdout
 
     foo(bio1, "bio1 say\n" ); // put to screen
     foo(bio2, "bio2 say\n" ); // put to file '/tmp/abc'
     foo(bio3, "bio3 say\n" ); // put to memory
     foo(bio4, "bio4 say\n" ); // put to screen
     
     BIO_mem::BUF_MEM *mem_ptr = NULL;
     
     bio3->get_mem_ptr(&mem_ptr);
     printf ( "size of mem ptr: %d; max: %d; data: %s" ,
         mem_ptr->length, mem_ptr->max, ( char *)mem_ptr->data);
     
     return 0;
}
C 程式碼

bio_example.c 的 C 語言程式碼才是真正調用 OpenSSL BIO 類別庫的範例程式。

bio_example.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// gcc -lssl -o bio_example bio_example.c
#include <stdio.h>
#include <openssl/bio.h>
#include <openssl/buffer.h>
 
int foo(BIO *bio, const char *msg) {
     return BIO_puts(bio, msg);
}
 
int main() {
     BIO *bio1 = BIO_new_fp(stdout, BIO_NOCLOSE);
     BIO *bio2 = BIO_new_file( "/tmp/abc" , "w" );
     
     // dynamic allocate memory block.
     BIO *bio3 = BIO_new(BIO_s_mem());
 
     BIO *bio4 = BIO_new_fd(1, BIO_NOCLOSE);
 
     foo(bio1, "bio1 say\n" ); // put to screen
     foo(bio2, "bio2 say\n" ); // put to file '/tmp/abc'
     foo(bio3, "bio3 say\n" ); // put to memory
     foo(bio4, "bio4 say\n" ); // put to screen
     
     BUF_MEM *mem_ptr = NULL;
 
     BIO_get_mem_ptr(bio3, &mem_ptr);
     printf ( "size of mem ptr: %d; max: %d; data: %s" ,
         mem_ptr->length, mem_ptr->max, mem_ptr->data);
 
     return 0;
}

編譯與執行結果如下所示。因為開啟的四個設備中,bio1, bio4 是標準輸出設備,故寫入的文字會直接出現在螢幕上。 bio2 則是檔案系統的檔案 /tmp/abc,文字被存入其中。bio3 是記憶體,所以文字被存入記憶體。

$ gcc -lssl -o bio_example bio_example.c
$ ./bio_example
bio1 say
bio4 say
size of mem ptr: 9; max: 16; data: bio3 say
$ cat /tmp/abc
bio2 say

我先用 C++ 寫出偽碼,再改寫成 C 語言碼。這是要讓大家了解操作 BIO 類別庫時,就應該要用 OOPL 的方式思考。撰寫實際的 C 語言程式碼時,更易清晰地掌握程式流程。

加入濾器

BIO 還提供一種濾器,可讓我們插入資料流中,幫我們在讀寫資料的過程中過濾資料內容。 最常見的就是資料編碼與解碼濾器,例如 BIO::Base64 濾器 – BIO_f_base64(3) 

範例程式碼 bio_base64.c 是我將 BIO_f_base64(3)  文件所附的範例程式加以擴充所得。 範例程式在資料流中加入了 BIO::Base64 濾器,因此寫入資料流的內容都將透過此濾器被編碼為 Base64 格式後才輸出。 修改後的範例程式,利用 BIO 的多形性,使其資料流兩端可以為標準輸出入設備亦或一般檔案。

bio_base64.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// gcc -lssl -o bio_base64 bio_base64.c
#include <openssl/bio.h>
#include <openssl/evp.h>
 
// program [input_filepath] [output_filepath]
// default behaviour is to read from stdin then write to stdout.
int main( int argc, char *argv[]) {
     BIO *bin, *bout, *b64filter;
     char buff[1024];
     int rc = 0;
 
     if (argc < 2)
         bin = BIO_new_fp(stdin, BIO_NOCLOSE);
     else
         bin = BIO_new_file(argv[1], "r" );
     
     if (bin == NULL) {
         printf ( "Failed to open input file.\n" );
         return 1;
     }
 
     if (argc > 2)
         bout = BIO_new_file(argv[2], "w" );
     else
         bout = BIO_new_fp(stdout, BIO_NOCLOSE);
 
     if (bin == NULL) {
         printf ( "Failed to open output file.\n" );
         return 1;
     }
 
     b64filter = BIO_new(BIO_f_base64());
     bout = BIO_push(b64filter, bout); // insert the filter.
     
     while ((rc = BIO_read(bin, buff, sizeof (buff))) > 0) {
         BIO_write(bout, buff, rc);
     }
     BIO_flush(bout);
 
     BIO_free_all(bout);
     BIO_free_all(bin);
 
     return 0;
}

此範例程式可接收兩個參數,第一個參數表示輸入的檔案名稱,第二個參數表示輸出的檔案名稱。 如果不指定第二個參數,則資料將寫入標準輸出設備。若連第一個參數也省略,則將自標準輸入設備讀取資料。

$ gcc -lssl -o bio_base64 bio_base64.c

$ cat bio_base64.c | ./bio_base64
# read from stdin, write to stdout

$ ./bio_base64 bio_base64.c
# read from bio_base64.c, write to stdout

$ ./bio_base64 bio_base64.c /tmp/b64.txt
# read from bio_base64.c, write to /tmp/b64.txt

若將 bio_base64.c 的輸出入來源改成 BIO::Socket 類別,就會支援從網路連線中讀寫資料。 當你開發支援 SSL 保密線路的網路應用程式時,資料傳輸的基本步驟也就是用 BIO::Socket 建立資料流,再插入 BIO 加密濾器,例如 BIO::Cipher – BIO_f_cipher(3) 

有興趣了解更多的人,可以參考以下三篇由 Kenneth Ballard 發表於 developerWorks 的《Secure programming with the OpenSSL API》系列文章是利用 OpenSSL 設計具有保密線路的網路程式。第一篇也是在教 BIO 的用法。

OpenSSL Library 的系列文章:


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值