标签: Socket

  • 通过 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