能否在 TcpConnection 的 stream_socket_enable_crypto 之前, 提供一個(gè) beforeSslHandshake 回調(diào)方法來(lái)修改 socket 的 contentx, 來(lái)實(shí)現(xiàn)這個(gè)功能?
看了些資料, SSL在握手階段, 客戶(hù)端發(fā)的第一個(gè)Hello握手包里有域名, 要實(shí)現(xiàn)這個(gè)功能, 必須取得這個(gè)包里的Extension server_name數(shù)據(jù). 現(xiàn)有的socket和stream函數(shù)好像沒(méi)有這樣的功能. 不知道是不是要用openssl自己實(shí)現(xiàn)ssl握手過(guò)程才行.
區(qū)分域名的話看起來(lái)要openssl手動(dòng)ssl握手了,這個(gè)難度有點(diǎn)大。
我看了TLS協(xié)議握手過(guò)程, 硬編碼肯定可以實(shí)現(xiàn), 但是效率存疑.
況且這么大的東西要完全按照規(guī)范寫(xiě)到穩(wěn)定健壯, 對(duì)我這個(gè)小項(xiàng)目就是殺雞用牛刀了, 不太現(xiàn)實(shí).
下個(gè)項(xiàng)目會(huì)需要解析TLS握手包, 到時(shí)候再看.
$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).
這樣的話, 這個(gè)問(wèn)題可以解決一半了, 服務(wù)端可以支持針對(duì)不同的域名使用不同的證書(shū).
剩下的問(wèn)題就是, 證書(shū)數(shù)量變化時(shí), 在不重啟 Server 的前提下, 如何平滑地重載 SNI 證書(shū)列表.
關(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ù), 我還沒(méi)測(cè)試. 但是在php官方在線手冊(cè)里都找不到這個(gè)選項(xiàng)的描述
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)該沒(méi)問(wèn)題, 只是官方文檔沒(méi)更新而已.
這個(gè)方式可以用在worker的tcp監(jiān)聽(tīng)了嗎,代碼能貼一下嗎,我也在研究這個(gè).
我打算,只要能實(shí)現(xiàn)監(jiān)聽(tīng)多個(gè)https就行,正是變化的時(shí)候就在業(yè)務(wù)事件里熱重啟
此問(wèn)題已解決, 目前我這里運(yùn)行良好. 關(guān)鍵代碼和說(shuō)明如下:
第一步: 聲明 context, 啟動(dòng)服務(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';
第二步: 繼承并重寫(xiě) Worker 類(lèi), 以便于可以在運(yùn)行時(shí)設(shè)置 stream_context
原Worker類(lèi)中, 使用一個(gè) protected 的 _context 屬性保存socket上下文, 外部無(wú)法直接修改, 所以需要繼承 Worker后,在我們實(shí)現(xiàn)的子類(lèi)中修改.
另外, socket 上下文在php中是一個(gè) resource 類(lèi)型, 反映到php中可以視為內(nèi)存地址引用. 對(duì)此變量的賦值操作不會(huì)創(chuàng)建新的對(duì)象.
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)中, 通過(guò)Channel注冊(cè)事件, 允許外部通知服務(wù)器動(dòng)態(tài)載入證書(shū)信息.
可以啟動(dòng)另外一個(gè)專(zhuān)用的api服務(wù), api服務(wù)接收管理端的調(diào)用后, 發(fā)布 EVENT_REFRESH_CERT 事件, 事件數(shù)據(jù)中標(biāo)明需要重載哪個(gè)站點(diǎn)的證書(shū).
$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();
// 此處通過(guò) $siteId 查詢(xún)數(shù)據(jù)庫(kù), 取得站點(diǎn)綁定的域名, 和域名對(duì)應(yīng)的證書(shū)文件路徑
$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";
}
刷新頁(yè)面, 此時(shí)證書(shū)已經(jīng)在服務(wù)中生效, 功能完成.