通过 Socket 获取网站 SSL 证书及公钥

通过 php curl 请求网页并不能获取到证书信息,此时需要使用 ssl socket 获取证书内容。

// 创建 stream context
$context = stream_context_create([
    'ssl' => [
        'capture_peer_cert' => true,
        'capture_peer_cert_chain' => true,
    ],
]);

$resource = stream_socket_client("ssl://$domain:$port", $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $context);
$cert = stream_context_get_params($resource);

$ssl = $cert['options']['ssl'];
$resource = $ssl['peer_certificate'];

// 网站证书中只有公钥,通过 openssl_pkey_get_details 导出公钥

$ret = [
    'crt' => '',
    'pub' => '',
];

$pkey = openssl_pkey_get_public($resource);
$ret['pub'] = openssl_pkey_get_details($pkey)['key'];

openssl_x509_export($resource, $pem);
$ret['crt'] = $pem;

foreach ($ssl['peer_certificate_chain'] as $resource)
{
    openssl_x509_export($resource, $pem);
    $ret['crt'] .= "\n" . $pem;
}

// 保存 $ret['crt'] 为 domain.crt
// 保存 $ret['pub'] 为 domain.pub

return $ret;

验证证书中的公钥A是否正确,通过私钥导出公钥B,比较两者发现一致。

$domain = 'blog.zhengxianjun.com';
$port = '443';
// ...
$pub_a = $ret['pub'];

$private_key_path = '/conf/ssl/blog.zhengxianjun.com.key';

// 证书没有设置密码,$passphrase 为空字符串
$pkey = openssl_pkey_get_private(file_get_content($private_key_path), $passphrase = '');
$pub_b = openssl_pkey_get_details($pkey)['key'];

// 两者一致
var_dump($pub_a === $pub_b);

函数 stream_socket_client 还有一个用途是当知道服务器 IP 时,能获取到服务器可能可以使用的域名。

$resource = stream_socket_client("ssl://$ip:$port", $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $context);
$cert = stream_context_get_params($resource);

// 解析 X.509 格式证书
$info = openssl_x509_parse($cert['options']['ssl']['peer_certificate']);

// 获取证书中的可信域名列表
$domain = str_replace('DNS:', '', $info['extensions']['subjectAltName']);

以上可以看到获取网站证书并不能获得私钥。

在一些使用 CDN 的站点,如果使用了 HTTPS 同时又希望使用自有域名,是否需要将自己的私钥提供给 CDN 厂商呢?实际上证书路径与使用者名称(支持 https 的域名)并不需要一致。

也就是使用自有域名并进行 CDN 加速时不需要使用自有的 ssl 证书,只需将自己的 CDN 域名加到厂商证书的域名列表即可。

PHP Socket 实现 TCP、UDP 报文的发送与接收

利用 PHP Socket 相关函数实现 TCP、UDP 端口监听。

需要注意,下面的示例代码中没有处理 Socket 错误。实际应用场景中每一步 Socket 的连接、写入、读取都需要进行错误判断和处理,相应的函数 socket_connect、socket_write、socket_read 以及 socket_bind、socket_listen 返回 false 时,需要调用 socket_last_error() 获取最新的 socket 错误号 $errno,并通过 socket_strerror($errno) 获取错误号对应的能够阅读的错误描述信息。

PHP Socket TCP 发送数据示例

$host = '127.0.0.1';
$port = '81';
$message = 'Hello TCP Server';

function send_tcp_message($host, $port, $message)
{
	$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
	@socket_connect($socket, $host, $port);

	$num = 0;
	$length = strlen($message);
	do
	{
		$buffer = substr($message, $num);
		$ret = @socket_write($socket, $buffer);
		$num += $ret;
	} while ($num < $length);

	$ret = '';
	do
	{
		$buffer = @socket_read($socket, 1024, PHP_BINARY_READ);
		$ret .= $buffer;
	} while (strlen($buffer) == 1024);

	socket_close($socket);

	return $ret;
}

$ret = send_tcp_message($host, $port, $message);

PHP Socket TCP 接收数据示例

创建一个 Server 接收 TCP 连接,需要先监听一个端口。

$host = '127.0.0.1';
$port = '81';
$callback = 'echo';

function receive_tcp_message($host, $port, $callback)
{
	$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);

	// socket_bind() 的参数 $host 必传, 由于是监听本机, 此处可以固定写本机地址
	// 注意: 监听本机地址和内网地址效果不一样
	@socket_bind($socket, $host, $port);
	@set_time_limit(0);

	// 绑定端口之后调用监听函数, 实现端口监听
	@socket_listen($socket, 5);

	// 接下来只需要一直读取, 检查是否有来源连接即可, 如果有, 则会得到一个新的 socket 资源
	while ($child = @socket_accept($socket))
	{
		// 休息 1 ms, 也可以不用休息
		usleep(1000);

		if (false === socket_getpeername($child, $remote_host, $remote_port))
		{
			@socket_close($child);
			continue;
		}

		// 读取请求数据
		// 例如是 http 报文, 则解析 http 报文
		$request = '';
		do
		{
			$buffer = @socket_read($child, 1024, PHP_BINARY_READ);
			if (false === $buffer)
			{
				@socket_close($child);
				continue 2;
			}
			$request .= $buffer;
		} while (strlen($buffer) == 1024);

		// 此处省略如何调用 $callback
		$response = $callback($remote_host, $remote_port, $request);

		if (!strlen($response))
		{
			// 至少返回含有一个空格的字符串
			$response = ' ';
		}

		// 因为是 TCP 链接, 需要返回给客户端处理数据
		$num = 0;
		$length = strlen($response);
		do
		{
			$buffer = substr($response, $num);
			$ret = @socket_write($child, $buffer);
			$num += $ret;
		} while ($num < $length);

		// 关闭 socket 资源, 继续循环
		@socket_close($child);
	}
}

// 客户端来的任何请求都会打印到屏幕上
receive_tcp_message($host, $port, $callback);
// 如果程序没有出现异常,该进程会一直存在

有一个快捷的函数 socket_create_listen($port),创建、绑定、监听一步到位。

PHP Socket UDP 发送数据示例

$host = '127.0.0.1';
$port = '82';
$message = 'Hello UDP Server';

function send_udp_message($host, $port, $message)
{
	$socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
	@socket_connect($socket, $host, $port);

	$num = 0;
	$length = strlen($message);
	do
	{
		$buffer = substr($message, $num);
		$ret = @socket_write($socket, $buffer);
		$num += $ret;
	} while ($num < $length);

	socket_close($socket);

	// UDP 是一种无链接的传输层协议, 不需要也无法获取返回消息
	return true;
}

send_udp_message($host, $port, $message);

PHP Socket UDP 接收数据示例

$host = '127.0.0.1';
$port = '82';
$callback = 'echo';

function receive_udp_message($host, $port, $callback)
{
	$socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);

	@socket_bind($socket, $host, $port);
	@set_time_limit(0);

	while (true)
	{
		usleep(1000);

		$ret = @socket_recvfrom($socket, $request, 16384, 0, $remote_host, $remote_port);
		if ($ret)
		{
			$callback($remote_host, $remote_port, $request);
		}

		// 不需要返回给客户端任何消息, 继续循环
	}
}

// 客户端来的任何请求都会打印到屏幕上
receive_udp_message($host, $port, $callback);
// 如果程序没有出现异常,该进程会一直存在

PHP Socket 相关函数

  1. socket_accept — Accepts a connection on a socket
  2. socket_bind — Binds a name to a socket
  3. socket_clear_error — Clears the error on the socket or the last error code
  4. socket_close — Closes a socket resource
  5. socket_cmsg_space — Calculate message buffer size
  6. socket_connect — Initiates a connection on a socket
  7. socket_create_listen — Opens a socket on port to accept connections
  8. socket_create_pair — Creates a pair of indistinguishable sockets and stores them in an array
  9. socket_create — Create a socket (endpoint for communication)
  10. socket_get_option — Gets socket options for the socket
  11. socket_getpeername — Queries the remote side of the given socket which may either result in host/port or in a Unix filesystem path, dependent on its type
  12. socket_getsockname — Queries the local side of the given socket which may either result in host/port or in a Unix filesystem path, dependent on its type
  13. socket_import_stream — Import a stream
  14. socket_last_error — Returns the last error on the socket
  15. socket_listen — Listens for a connection on a socket
  16. socket_read — Reads a maximum of length bytes from a socket
  17. socket_recv — Receives data from a connected socket
  18. socket_recvfrom — Receives data from a socket whether or not it is connection-oriented
  19. socket_recvmsg — Read a message
  20. socket_select — Runs the select() system call on the given arrays of sockets with a specified timeout
  21. socket_send — Sends data to a connected socket
  22. socket_sendmsg — Send a message
  23. socket_sendto — Sends a message to a socket, whether it is connected or not
  24. socket_set_block — Sets blocking mode on a socket resource
  25. socket_set_nonblock — Sets nonblocking mode for file descriptor fd
  26. socket_set_option — Sets socket options for the socket
  27. socket_shutdown — Shuts down a socket for receiving, sending, or both
  28. socket_strerror — Return a string describing a socket error
  29. socket_write — Write to a socket