### nginx支持ssl簡介[](http://tengine.taobao.org/book/chapter_12.html#nginxssl "永久鏈接至標題")
nginx-1.2.0編譯時默認是不支持ssl協議的,需要通過編譯指令來開啟對其支持:
[](http:// "點擊提交Issue,反饋你的意見...")
./configure --with-http_ssl_module
在nginx源碼中,ssl相關代碼用宏定義變量NGX_HTTP_SSL來控制是否開啟。這給我們查找和閱讀ssl相關代碼帶來了方便,如下:
ssl協議工作在tcp協議與http協議之間。nginx在支持ssl協議時,需要注意三點,其他時候只要正常處理http協議即可:
1. tcp連接建立時,在tcp連接上建立ssl連接
1. tcp數據接收后,將收到的數據解密并將解密后的數據交由正常http協議處理流程
1. tcp數據發送前,對(http)數據進行加密,然后再發送
以下章節將分別介紹這三點。
[](http:// "點擊提交Issue,反饋你的意見...")
### ssl連接建立(ssl握手)[](http://tengine.taobao.org/book/chapter_12.html#ssl-ssl "永久鏈接至標題")
[](http:// "點擊提交Issue,反饋你的意見...")
#### 對ssl連接建立的準備[](http://tengine.taobao.org/book/chapter_12.html#ssl "永久鏈接至標題")
更具ssl協議規定,在正式發起數據收發前,需要建立ssl連接,連接建立過程既ssl握手。nginx在創建和初始化http請求階段的同時為tcp連接建立做準備,主要流程在ngx_http_init_request函數中實現:
[](http:// "點擊提交Issue,反饋你的意見...")
static void
ngx_http_init_request(ngx_event_t *rev)
{
...
#if (NGX_HTTP_SSL)
{
ngx_http_ssl_srv_conf_t *sscf;
sscf = ngx_http_get_module_srv_conf(r, ngx_http_ssl_module);
if (sscf->enable || addr_conf->ssl) {
/* c->ssl不為空時,表示請求復用長連接(已經建立過ssl連接) */
if (c->ssl == NULL) {
c->log->action = "SSL handshaking";
/*
* nginx.conf中開啟ssl協議(listen 443 ssl;),
* 卻沒用設置服務器證書(ssl_certificate <certificate_path>;)
*/
if (addr_conf->ssl && sscf->ssl.ctx == NULL) {
ngx_log_error(NGX_LOG_ERR, c->log, 0,
"no \"ssl_certificate\" is defined "
"in server listening on SSL port");
ngx_http_close_connection(c);
return;
}
/*
* 創建ngx_ssl_connection_t并初始化
* openssl庫中關于ssl連接的初始化
*/
if (ngx_ssl_create_connection(&sscf->ssl, c, NGX_SSL_BUFFER)
!= NGX_OK)
{
ngx_http_close_connection(c);
return;
}
rev->handler = ngx_http_ssl_handshake;
}
/* ssl加密的數據必須讀到內存中 */
r->main_filter_need_in_memory = 1;
}
}
#endif
...
}
ngx_http_init_request大部分流程已經在前面章節分析過了,這個函數主要負責初始化http請求,此時并沒有實際解析http請求。若發來的請求是經由ssl協議加密的,直接解析http請求就會出錯。ngx_http_init_request中ssl協議相關處理流程:
1,首先判斷c->ssl是否為空。若不為空:說明這里是http長連接的情況,ssl連接已經在第一個請求進入時建立了。這里只要復用這個ssl連接即可,跳過ssl握手階段。
2.(1),若c->ssl為空:需要進行ssl握手來建立連接。此時調用ngx_ssl_create_connection為ssl連接建立做準備。
ngx_ssl_create_connection 簡化代碼如下:
[](http:// "點擊提交Issue,反饋你的意見...")
ngx_int_t
ngx_ssl_create_connection(ngx_ssl_t *ssl, ngx_connection_t *c, ngx_uint_t flags)
{
ngx_ssl_connection_t *sc;
/* ngx_ssl_connection_t是nginx對ssl連接的描述結構,記錄了ssl連接的信息和狀態 */
sc = ngx_pcalloc(c->pool, sizeof(ngx_ssl_connection_t));
sc->buffer = ((flags & NGX_SSL_BUFFER) != 0);
/* 創建openssl庫中對ssl連接的描述結構 */
sc->connection = SSL_new(ssl->ctx);
/* 關聯(openssl庫)ssl連接到tcp連接對應的socket */
SSL_set_fd(sc->connection, c->fd);
if (flags & NGX_SSL_CLIENT) {
/* upstream中發起對后端的ssl連接,指明nginx ssl連接是客戶端 */
SSL_set_connect_state(sc->connection);
} else {
/* 指明nginx ssl連接是服務端 */
SSL_set_accept_state(sc->connection);
}
/* 關聯(openssl庫)ssl連接到用戶數據(當前連接c) */
SSL_set_ex_data(sc->connection, ngx_ssl_connection_index, c);
c->ssl = sc;
return NGX_OK;
}
2.(2),設置連接讀事件處理函數為ngx_http_ssl_handshake,這將改變后續處理http請求的正常流程為:先進行ssl握手,再正常處理http請求。
3,標明當前待發送的數據須在內存中,以此可以讓ssl對數據進行加密。由于開啟了ssl協議,對發送出去的數據要進行加密,這就要求待發送的數據必須在內存中。 標識r->main_filter_need_in_memory為1,可以讓后續數據發送前,將數據讀取到內存中 (防止在文件中的數據通過sendfile直接發送出去,而沒有加密)。
[](http:// "點擊提交Issue,反饋你的意見...")
#### 實際ssl握手階段[](http://tengine.taobao.org/book/chapter_12.html#id18 "永久鏈接至標題")
由于在ngx_http_init_request中將連接讀事件處理函數設置成ngx_http_ssl_handshake,當連接中有可讀數據時,將會進入ngx_http_ssl_handshake來處理(若未開啟ssl,將進入ngx_http_process_request_line直接解析http請求)
在ngx_http_ssl_handshake中,來進行ssl握手:
1,首先判斷連接是否超時,如果超時則關閉連接
[](http:// "點擊提交Issue,反饋你的意見...")
static void
ngx_http_process_request(ngx_http_request_t *r)
{
if (rev->timedout) {
ngx_log_error(NGX_LOG_INFO, c->log, NGX_ETIMEDOUT, "client timed out");
c->timedout = 1;
ngx_http_close_request(r, NGX_HTTP_REQUEST_TIME_OUT);
return;
}
2,首字節預讀:從tcp連接中查看一個字節(通過MSG_PEEK查看tcp連接中數據,但不會實際讀取該數據),若tcp連接中沒有準備好的數據,則重新添加讀事件退出等待新數據到來。
[](http:// "點擊提交Issue,反饋你的意見...")
n = recv(c->fd, (char *) buf, 1, MSG_PEEK);
if (n == -1 && ngx_socket_errno == NGX_EAGAIN) {
if (!rev->timer_set) {
ngx_add_timer(rev, c->listening->post_accept_timeout);
}
if (ngx_handle_read_event(rev, 0) != NGX_OK) {
ngx_http_close_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);
}
return;
}
3,首字節探測:若成功查看1個字節數據,通過該首字節來探測接受到的數據是ssl握手包還是http數據。根據ssl協議規定,ssl握手包的首字節中包含有ssl協議的版本信息。nginx根據此來判斷是進行ssl握手還是返回正常處理http請求(實際返回應答400 BAD REQUEST)。
[](http:// "點擊提交Issue,反饋你的意見...")
if (n == 1) {
if (buf[0] & 0x80 /* SSLv2 */ || buf[0] == 0x16 /* SSLv3/TLSv1 */) {
ngx_log_debug1(NGX_LOG_DEBUG_HTTP, rev->log, 0,
"https ssl handshake: 0x%02Xd", buf[0]);
/*
* 調用ngx_ssl_handshake函數進行ssl握手,連接雙方會在ssl握手時交換相
* 關數據(ssl版本,ssl加密算法,server端的公鑰等) 并正式建立起ssl連接。
* ngx_ssl_handshake函數內部對openssl庫進行了封裝。
* 調用SSL_do_handshake()來進行握手,并根據其返回值判斷ssl握手是否完成
* 或者出錯。
*/
rc = ngx_ssl_handshake(c);
/*
* ssl握手可能需要多次數據交互才能完成。
* 如果ssl握手沒有完成,ngx_ssl_handshake會根據具體情況(如需要讀取更
* 多的握手數據包,或者需要發送握手數據包)來重新添加讀寫事件
*/
if (rc == NGX_AGAIN) {
if (!rev->timer_set) {
ngx_add_timer(rev, c->listening->post_accept_timeout);
}
c->ssl->handler = ngx_http_ssl_handshake_handler;
return;
}
/*
* 若ssl握手完成或者出錯,ngx_ssl_handshake會返回NGX_OK或者NGX_ERROR, 然后ngx_http_ssl_handshake調用
* ngx_http_ssl_handshake_handler以繼續處理
*/
ngx_http_ssl_handshake_handler(c);
return;
} else {
ngx_log_debug0(NGX_LOG_DEBUG_HTTP, rev->log, 0,
"plain http");
r->plain_http = 1;
}
}
需要特別注意,如果ssl握手完成,ngx_ssl_handshake會替換連接的讀寫接口。這樣,后續需要讀寫數據時,替換的接口會對數據進行加密解密。詳細代碼見下:
[](http:// "點擊提交Issue,反饋你的意見...")
ngx_int_t
ngx_ssl_handshake(ngx_connection_t *c)
{
n = SSL_do_handshake(c->ssl->connection);
/* 返回1表示ssl握手成功 */
if (n == 1) {
...
c->ssl->handshaked = 1;
c->recv = ngx_ssl_recv;
c->send = ngx_ssl_write;
c->recv_chain = ngx_ssl_recv_chain;
c->send_chain = ngx_ssl_send_chain;
return NGX_OK;
}
...
}
4,探測為http協議:正常的http協議包處理直接調用ngx_http_process_request_line處理http請求,并將讀事件處理函數設置成ngx_http_process_request_line。(實際處理結果是向客戶端返回400 BAD REQUET,在ngx_http_process_request中又對r->plain_http標志的單獨處理。)
[](http:// "點擊提交Issue,反饋你的意見...")
c->log->action = "reading client request line";
rev->handler = ngx_http_process_request_line;
ngx_http_process_request_line(rev);
} /* end of ngx_http_process_request() */
5,當ssl握手成功或者出錯時,調用ngx_http_ssl_handshake_handler函數。
5.(1),若ssl握手完成 (c->ssl->handshaked由ngx_ssl_handshake()確定握手完成后設為1),設置讀事件處理函數為ngx_http_process_request_line,并調用此函數正常處理http請求。
5.(2),若ssl握手沒完成(則說明ssl握手出錯),則返回400 BAD REQUST給客戶端。
至此,ssl連接已經建立,此后在ngx_http_process_request中會讀取數據并解密然后正常處理http請求。
[](http:// "點擊提交Issue,反饋你的意見...")
static void
ngx_http_ssl_handshake_handler(ngx_connection_t *c)
{
ngx_http_request_t *r;
if (c->ssl->handshaked) {
/*
* The majority of browsers do not send the "close notify" alert.
* Among them are MSIE, old Mozilla, Netscape 4, Konqueror,
* and Links. And what is more, MSIE ignores the server's alert.
*
* Opera and recent Mozilla send the alert.
*/
c->ssl->no_wait_shutdown = 1;
c->log->action = "reading client request line";
c->read->handler = ngx_http_process_request_line;
/* STUB: epoll edge */ c->write->handler = ngx_http_empty_handler;
ngx_http_process_request_line(c->read);
return;
}
r = c->data;
ngx_http_close_request(r, NGX_HTTP_BAD_REQUEST);
return;
}
[](http:// "點擊提交Issue,反饋你的意見...")
#### ssl協議接受數據[](http://tengine.taobao.org/book/chapter_12.html#id19 "永久鏈接至標題")
ngx_http_process_request中處理http請求,需要讀取和解析http協議。而實際數據讀取是通過c->recv()函數來讀取的,此函數已經在ngx_ssl_handshake中被替換成ngx_ssl_recv了。
ngx_ssl_recv函數中調用openssl庫函數SSL_read()來讀取并解密數據,簡化后如下:
[](http:// "點擊提交Issue,反饋你的意見...")
ssize_t ngx_ssl_recv(ngx_connection_t *c, u_char *buf, size_t size)
{
...
n = SSL_read(c->ssl->connection, buf, size);
...
return n;
}
[](http:// "點擊提交Issue,反饋你的意見...")
#### ssl協議發送數據[](http://tengine.taobao.org/book/chapter_12.html#id20 "永久鏈接至標題")
當nginx發送數據時,如使用ngx_output_chain函數發送緩存的http數據緩存鏈時,通過調用c->send_chain()來發送數據。這個函數已經在ngx_ssl_handshake中被設置成ngx_ssl_send_chain了。ngx_ssl_send_chain會進一步調用ngx_ssl_write。而ngx_ssl_write調用openssl庫SSL_write函數來加密并發送數據。
[](http:// "點擊提交Issue,反饋你的意見...")
/* ngx_output_chain
* -> ..
* -> ngx_chain_writer
* -> c->send_chain (ngx_ssl_send_chain)
* -> ngx_ssl_write
*/
ssize_t ngx_ssl_write(ngx_connection_t *c, u_char *data, size_t size)
{
...
n = SSL_write(c->ssl->connection, data, size);
...
return n;
}
- 上篇:nginx模塊開發篇
- nginx平臺初探
- 初探nginx架構
- nginx基礎概念
- connection
- request
- keepalive
- pipe
- lingering_close
- 基本數據結構
- ngx_str_t
- ngx_pool_t
- ngx_array_t
- ngx_hash_t
- ngx_hash_wildcard_t
- ngx_hash_combined_t
- ngx_hash_keys_arrays_t
- ngx_chain_t
- ngx_buf_t
- ngx_list_t
- ngx_queue_t
- nginx的配置系統
- 指令參數
- 指令上下文
- nginx的模塊化體系結構
- 模塊的分類
- nginx的請求處理
- handler模塊
- handler模塊簡介
- 模塊的基本結構
- 模塊配置結構
- 模塊配置指令
- 模塊上下文結構
- 模塊的定義
- handler模塊的基本結構
- handler模塊的掛載
- handler的編寫步驟
- 示例: hello handler 模塊
- handler模塊的編譯和使用
- 更多handler模塊示例分析
- http access module
- http static module
- http log module
- 過濾模塊
- 過濾模塊簡介
- 過濾模塊的分析
- upstream模塊
- upstream模塊
- upstream模塊接口
- memcached模塊分析
- 本節回顧
- 負載均衡模塊
- 配置
- 指令
- 鉤子
- 初始化配置
- 初始化請求
- peer.get和peer.free回調函數
- 本節回顧
- 其他模塊
- core模塊
- event模塊
- 模塊開發高級篇
- 變量
- 下篇:nginx原理解析篇
- nginx架構詳解
- nginx的源碼目錄結構
- nginx的configure原理
- 模塊編譯順序
- nginx基礎設施
- 內存池
- nginx的啟動階段
- 概述
- 共有流程
- 配置解析
- nginx的請求處理階段
- 接收請求流程
- http請求格式簡介
- 請求頭讀取
- 解析請求行
- 解析請求頭
- 請求體讀取
- 讀取請求體
- 丟棄請求體
- 多階段處理請求
- 多階段執行鏈
- POST_READ階段
- SERVER_REWRITE階段
- FIND_CONFIG階段
- REWRITE階段
- POST_REWRITE階段
- PREACCESS階段
- ACCESS階段
- POST_ACCESS階段
- TRY_FILES階段
- CONTENT階段
- LOG階段
- Nginx filter
- header filter分析
- body filter分析
- ngx_http_copy_filter_module分析
- ngx_http_write_filter_module分析
- subrequest原理解析
- https請求處理解析
- 附錄A 編碼風格
- 附錄B 常用API
- 附錄C 模塊編譯,調試與測試