能否在 TcpConnection 的 stream_socket_enable_crypto 之前, 提供一個 beforeSslHandshake 回調(diào)方法來修改 socket 的 contentx, 來實現(xiàn)這個功能?
看了些資料, SSL在握手階段, 客戶端發(fā)的第一個Hello握手包里有域名, 要實現(xiàn)這個功能, 必須取得這個包里的Extension server_name數(shù)據(jù). 現(xiàn)有的socket和stream函數(shù)好像沒有這樣的功能. 不知道是不是要用openssl自己實現(xiàn)ssl握手過程才行.
區(qū)分域名的話看起來要openssl手動ssl握手了,這個難度有點大。
我看了TLS協(xié)議握手過程, 硬編碼肯定可以實現(xiàn), 但是效率存疑.
況且這么大的東西要完全按照規(guī)范寫到穩(wěn)定健壯, 對我這個小項目就是殺雞用牛刀了, 不太現(xiàn)實.
下個項目會需要解析TLS握手包, 到時候再看.
$ctx = stream_context_create(["ssl" => [
"local_cert" => "/path/to/cert.pem",
"SNI_server_certs" => [
"domain1.com" => "/path/to/domain1.pem",
"*.domain2.com" => "/path/to/domain2.pem",
"domain3.com" => "/path/to/domain3.pem"
]
]]);
找到一些資料, PHP 5.6以后, stream_socket_server 的 context 可以支持 SNI (Server Name Indication).
這樣的話, 這個問題可以解決一半了, 服務(wù)端可以支持針對不同的域名使用不同的證書.
剩下的問題就是, 證書數(shù)量變化時, 在不重啟 Server 的前提下, 如何平滑地重載 SNI 證書列表.
關(guān)于 SNI_server_certs 在 https://github.com/php/php-src/blob/PHP-5.6/NEWS 2349行有描述, 用法在 https://stackoverflow.com/questions/20865301/php-server-side-sni-support 有人回復(fù), 我還沒測試. 但是在php官方在線手冊里都找不到這個選項的描述
https://github.com/php/php-src/blob/PHP-7.2.25/ext/openssl/xp_ssl.c 查看發(fā)現(xiàn)5.6和7.2的源碼中都有SNI_server_certs的支持,, 應(yīng)該沒問題, 只是官方文檔沒更新而已.
這個方式可以用在worker的tcp監(jiān)聽了嗎,代碼能貼一下嗎,我也在研究這個.
我打算,只要能實現(xiàn)監(jiān)聽多個https就行,正是變化的時候就在業(yè)務(wù)事件里熱重啟
此問題已解決, 目前我這里運行良好. 關(guān)鍵代碼和說明如下:
第一步: 聲明 context, 啟動服務(wù).
$context 的 SNI_server_certs 部分留空, 但最終要將 SNI_server_certs 部分填充為注釋所示的樣子.
$context = [
'ssl' => [
'verify_peer' => false,
'disable_compression' => true,
'SNI_enabled' => true,
'SNI_server_certs' => [
/*
"*.domain1.com" => [
'local_cert' => "{$this->certFileRoot}/domain1.com/_.domain1.com.pem",
'local_pk' => "{$this->certFileRoot}/domain1.com/_.domain1.com.key",
],
"*.domain2.com" => [
'local_cert' => "{$this->certFileRoot}/domain2.com/_.domain2.com.crt",
'local_pk' => "{$this->certFileRoot}/domain2.com/_.domain2.com.key",
],
"domain3.com" => [
'local_cert' => "{$this->certFileRoot}/domain3.com/domain3.com.crt",
'local_pk' => "{$this->certFileRoot}/domain3.com/domain3.com.key",
],
"www.domain3.com" => [
'local_cert' => "{$this->certFileRoot}/domain3.com/www.domain3.com.crt",
'local_pk' => "{$this->certFileRoot}/domain3.com/www.domain3.com.key",
],
*/
],
],
];
$server = new WorkerX("http://0.0.0.0:443", $context);
$server->count = 10;
$server->transport = 'ssl';
$server->name = 'Https Server';
第二步: 繼承并重寫 Worker 類, 以便于可以在運行時設(shè)置 stream_context
原Worker類中, 使用一個 protected 的 _context 屬性保存socket上下文, 外部無法直接修改, 所以需要繼承 Worker后,在我們實現(xiàn)的子類中修改.
另外, socket 上下文在php中是一個 resource 類型, 反映到php中可以視為內(nèi)存地址引用. 對此變量的賦值操作不會創(chuàng)建新的對象.
class WorkerX extends \Workerman\Worker
{
public function contextGetOptions()
: array
{
if(is_resource($this->_context))
{
return stream_context_get_options($this->_context);
}
return [];
}
public function contextSetOptions(array $options)
: bool
{
if(is_resource($this->_context))
{
return stream_context_set_option($this->_context, $options);
}
return false;
}
}
第三步: 在$server的onWorkerStart回調(diào)中, 通過Channel注冊事件, 允許外部通知服務(wù)器動態(tài)載入證書信息.
可以啟動另外一個專用的api服務(wù), api服務(wù)接收管理端的調(diào)用后, 發(fā)布 EVENT_REFRESH_CERT 事件, 事件數(shù)據(jù)中標(biāo)明需要重載哪個站點的證書.
$server->onWorkerStart = function($worker)
{
WorkerDI::init($worker);
$this->refreshCert($worker, 0);
\Channel\Client::on('EVENT_REFRESH_CERT', function($eventData) use ($worker)
{
$siteId = intval($eventData['site_id']);
$this->refreshCert($worker, $siteId);
});
};
private function refreshCert(WorkerX $worker, int $siteId = 0)
{
$sslContextOptions = $worker->contextGetOptions();
// 此處通過 $siteId 查詢數(shù)據(jù)庫, 取得站點綁定的域名, 和域名對應(yīng)的證書文件路徑
$domain = '*.domain1.com';
$certFilePath = '/path/to/certFile.crt';
$keyFilePath = '/path/to/certFile.key';
$sslContextOptions['ssl']['SNI_server_certs'][$domain] = [
'local_cert' => $certFilePath,
'local_pk' => $pkFilePath,
];
$setResult = $worker->contextSetOptions($sslContextOptions);
echo "設(shè)置" . ($setResult ? "成功" : "失敗") . "\n";
}
刷新頁面, 此時證書已經(jīng)在服務(wù)中生效, 功能完成.