WebSockets¶
WebSocket 回显示例
概览¶
什么是 WebSocket? WebSocket API
lighttpd 可以通过多种方式支持 WebSocket 连接,包括通过 mod_proxy、mod_cgi、mod_scgi、mod_fastcgi 和 mod_wstunnel。
- lighttpd 支持 HTTP/1.1 连接中的
Upgrade: websocket。 - lighttpd 支持 HTTP/2 扩展 CONNECT,通过单个 HTTP/2 连接在 HTTP/2 流上使用
:protocol: websocket。
出于安全原因并遵循最小惊喜原则,lighttpd 对 WebSocket 的支持默认是特意禁用的。可以配置 lighttpd 动态模块以启用连接升级到 WebSocket 协议。
自 lighttpd 1.4.46 版本起,lighttpd 的 mod_cgi、mod_proxy 和 mod_wstunnel 模块支持 WebSocket。在 lighttpd 1.4.74 版本中,又增加了对 mod_scgi 和 mod_fastcgi 这两个动态模块的支持。
本文档演示了配置 lighttpd 模块以运行 WebSocket “回显” 示例应用程序的多种方法。
基本配置¶
通过 mod_wstunnel 使用 WebSockets¶
配置 mod_wstunnel
mod_wstunnel 是一个 WebSocket 隧道端点,用于终止来自客户端的 WebSocket 隧道。mod_wstunnel 会解码 WebSocket 帧,然后将数据(不带 WebSocket 帧)传递给后端,而在相反方向上,它会在将响应发送给客户端之前,将后端响应编码成 WebSocket 帧。
通过 mod_proxy 使用 WebSockets¶
配置 mod_proxy 并启用 proxy.header += ("upgrade" => "enable")
从 lighttpd 1.4.74 及更高版本开始,"upgrade" => "enable" 也是 proxy.server 中的一个主机选项。
当启用升级时,mod_proxy 可以作为 WebSocket 连接的反向代理。
通过 mod_cgi 使用 WebSockets¶
配置 mod_cgi 并启用 cgi.upgrade = "enable"
mod_cgi 允许目标程序升级使用 WebSocket 协议。
通过 mod_scgi 使用 WebSockets¶
配置 mod_scgi 并在 scgi.server 中为每个主机添加 "upgrade" => "enable" 选项(lighttpd 1.4.74 及更高版本)
mod_scgi 允许目标程序升级使用 WebSocket 协议。
通过 mod_fastcgi 使用 WebSockets¶
配置 mod_fastcgi 并在 fastcgi.server 中为每个主机添加 "upgrade" => "enable" 选项(lighttpd 1.4.74 及更高版本)
mod_fastcgi 允许目标程序升级使用 WebSocket 协议。
虽然 lighttpd 目前不支持在单个 FastCGI 连接上多路复用多个请求,但 FastCGI 协议本身是允许的。即使请求升级到使用 WebSocket 协议后,lighttpd 的 mod_fastcgi 仍然会从后端发送和接收 FastCGI 数据包。WebSocket 帧再次被封装到 FastCGI 数据包中。因此,这可能不如使用其他支持升级到 WebSocket 协议的 lighttpd 模块效率高。
示例:ws_echo.py¶
ws_echo.py - 简单的“回显”WebSocket 服务器 CGI 示例程序
将文件安装到文档根目录和 ws/ 子目录中<docroot>/ws_echo.html<docroot>/ws/ws_echo.py
如果使用 HTTPS,请修改 ws_echo.html 中的 URL,将 ws:// 改为 wss://。var host = "ws://" + window.location.hostname + "/ws/ws_echo.py";
<docroot>/ws_echo.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<!-- favicon: none -->
<link rel="icon" href="data:,">
<!--
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self' 'unsafe-inline'" />
-->
<title>websocket echo client</title>
<style>
#output {
border: solid 1px #000;
}
</style>
</head>
<body>
<form id="form" accept-charset=utf-8>
<input type="text" id="message">
<button type="submit">Send</button>
</form>
<hr>
<div id="output"></div>
<script>
var inputBox = document.getElementById("message");
var output = document.getElementById("output");
var form = document.getElementById("form");
try {
var host = "ws://" + window.location.hostname + "/ws/ws_echo.py";
//var host = "wss://" + window.location.hostname + "/ws/ws_echo.py";
//var host = "ws://" + window.location.hostname + ":8080/ws/ws_echo.py";
//var host = "wss://" + window.location.hostname + ":8443/ws/ws_echo.py";
console.log("Host:", host);
var s = new WebSocket(host);
s.onopen = function (e) {
console.log("Socket opened.");
};
s.onclose = function (e) {
console.log("Socket closed.");
};
s.onmessage = function (e) {
console.log("Socket message:", e.data);
var p = document.createElement("p");
p.innerHTML = e.data;
output.appendChild(p);
};
s.onerror = function (e) {
console.log("Socket error:", e);
};
} catch (ex) {
console.log("Socket exception:", ex);
}
form.addEventListener("submit", function (e) {
e.preventDefault();
s.send(inputBox.value);
inputBox.value = "";
}, false)
</script>
</body>
</html>
<docroot>/ws/ws_echo.py
#!/usr/bin/env python3
#
# ws_echo.py - websocket echo; reflect back to peer the frames sent by peer
#
# Copyright(c) 2023 Glenn Strauss gstrauss()gluelogic.com All rights reserved
# License: BSD 3-clause (same as lighttpd)
import struct
import hashlib
import base64
def ws_upgrade(rd, wr, env):
if env.get('HTTP_UPGRADE', '') != 'websocket':
wr.write("Status: 204\n\n".encode())
return False
# (throws KeyError exception if any of these env vars are not set)
origin = env['HTTP_ORIGIN']
scheme = "wss" if env['REQUEST_SCHEME'] == "https" else "ws"
host = env['HTTP_HOST']
urlpath= env['SCRIPT_NAME']
key = env['HTTP_SEC_WEBSOCKET_KEY'].encode('utf-8')
GUID = b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
digest = str(base64.b64encode(hashlib.sha1(key + GUID).digest()), 'utf-8')
wr.write(("Status: 101\n"
+ "Upgrade: WebSocket\n"
+ "Connection: Upgrade\n"
+ f"Sec-WebSocket-Origin: {origin}\n"
+ f"Sec-WebSocket-Location: {scheme}://{host}{urlpath}\n"
+ f"Sec-WebSocket-Accept: {digest}\n\n"
).encode())
return True
## +-+-+-+-+-------++-+-------------+-------------------------------+
## |F|R|R|R| opcode||M| Payload len | Extended payload length |
## |I|S|S|S| (4) ||A| (7) | (16/63) |
## |N|V|V|V| ||S| | (if payload len==126/127) |
## | |1|2|3| ||K| | |
## +-+-+-+-+-------++-+-------------+ - - - - - - - - - - - - - - - +
## +-+-+-+-+--------------------------------------------------------+
## | Extended payload length continued, if payload len == 127 |
## + - - - - - - - - - - - - - - - +--------------------------------+
## + - - - - - - - - - - - - - - - +-------------------------------+
## | |Masking-key, if MASK set to 1 |
## +-------------------------------+-------------------------------+
## | Masking-key (continued) | Payload Data |
## +-------------------------------- - - - - - - - - - - - - - - - +
## : Payload Data continued ... :
## + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
## | Payload Data continued ... |
## +---------------------------------------------------------------+
def ws_recv_and_echo(rd, wr):
# (note: assumes well-formed websocket frame; not validating or paranoid)
# (note: inefficient small reads rather than read/buffering larger blocks)
# (note: no buffering; program exits if partial read of frame header)
# (note: no buffering; program loses state if partial read of frame data
# and subsequently will likely thrown an exception)
# (tl;dr: this toy program is sufficient for a demo of frames with small
# data payloads, but probably nothing more)
data = rd.read(2)
if len(data) == 0: # EOF
ws_send(wr, b'1000', opcode=0x8) # CLOSE "1000 = Normal Closure"
return False
head1, head2 = struct.unpack('!BB', data)
fin = bool(head1 & 0b10000000)
opcode = head1 & 0b00001111
if opcode == 0x8: # CLOSE (0x8)
# (note: not reading and parsing close reason sent by peer)
ws_send(wr, b'1000', opcode=opcode) # "1000 = Normal Closure"
return False
length = head2 & 0b01111111
if length == 126:
data = rd.read(2)
length, = struct.unpack('!H', data)
elif length == 127:
data = rd.read(8)
length, = struct.unpack('!Q', data)
mask_bit = bool(head2 & 0b10000000)
masking_key = rd.read(4) if mask_bit else False
data = bytearray(rd.read(length)) if length else b''
if opcode == 0xA: # PONG
# receive and ignore response to PING
return True
if opcode == 0x9: # PING
# reflect PING back to peer as PONG
opcode = 0xA # PONG
if mask_bit:
# unmask data
for i in range(0, len(data)):
data[i] ^= masking_key[i % 4]
# echo data back to peer using same masking_key
# (note: not validating continuation frame follows frame without fin set)
#if opcode == 0x0: # CONTINUATION
#if opcode == 0x1: # TEXT
#if opcode == 0x2: # BINARY
#if opcode == 0xA: # PONG (from PING above)
ws_send(wr, ''.join(map(chr, data)).encode(),
opcode=opcode, fin=fin, masking_key=masking_key)
return True
def ws_send(wr, data, opcode=1, fin=True, masking_key=False):
if 0x3 <= opcode <= 0x7 or 0xB <= opcode:
raise ValueError('Invalid opcode')
header = struct.pack('!B', ((bool(fin) << 7) | opcode))
mask_bit = (1 << 7) if masking_key else 0
length = len(data)
if length < 126:
header += struct.pack('!B', (mask_bit|length))
elif length < (1 << 16):
header += struct.pack('!B', (mask_bit|126)) + struct.pack('!H', length)
elif length < (1 << 63):
header += struct.pack('!B', (mask_bit|127)) + struct.pack('!Q', length)
else:
raise ValueError('Data too large')
if mask_bit:
# mask data
header += masking_key
data = bytearray(data)
for i in range(0, length):
data[i] ^= masking_key[i % 4]
wr.write(header + data)
def ws_echo_app(rd, wr, env):
if not ws_upgrade(rd, wr, env):
return
while True:
if not ws_recv_and_echo(rd, wr):
break
import os
import sys
#import errno
#import io
#from traceback import format_exc as format
#import logging
#logger = logging.getLogger('mylogger')
#logger.setLevel(logging.WARNING)
if __name__ == '__main__':
try:
sys.stdin = os.fdopen(sys.stdin.fileno(), 'r+b', buffering=0)
sys.stdout = os.fdopen(sys.stdout.fileno(), 'r+b', buffering=0)
ws_echo_app(sys.stdin, sys.stdout, os.environ)
#except IOError as e:
# if e.errno != errno.EPIPE:
# raise e
except KeyboardInterrupt: # Ctrl-C or SIGINT
try:
sys.exit(130)
except SystemExit:
os._exit(130)
except Exception as e:
err = e.args[0]
#logger.warning("err:{}\n".format(str(err)))
示例:配置 lighttpd 以在 /ws/ 下运行 CGI 程序¶
server.modules += ("mod_cgi")
$HTTP["url"] =^ "/ws/" {
cgi.assign = ("" => "")
cgi.upgrade = 1
#cgi.limits = ( "read-timeout" => 60, "write-timeout" => 60 )
}
#server.max-read-idle := 60
#server.max-write-idle := 60
示例:配置 lighttpd 以在 /ws/ 下运行 SCGI 程序¶
使用 scgi-cgi 运行 ws_echo.py
server.modules += ("mod_scgi")
scgi.server = ("/ws/" =>
((
"socket" => "/tmp/scgi-ws.sock", # should use more secure location
"bin-path" => "/usr/local/bin/scgi-cgi" # modify path to scgi-cgi
"check-local" => "disable",
"min-procs" => 1,
"max-procs" => 1,
"upgrade" => 1
#"read-timeout" => 60,
#"write-timeout" => 60,
))
)
#server.max-read-idle := 60
#server.max-write-idle := 60
示例:配置 lighttpd 以在 /ws/ 下运行 FastCGI 程序¶
使用 fcgi-cgi 运行 ws_echo.py
server.modules += ("mod_fastcgi")
fastcgi.server = ("/ws/" =>
((
"socket" => "/tmp/fcgi-ws.sock", # should use more secure location
"bin-path" => "/usr/local/bin/fcgi-cgi" # modify path to fcgi-cgi
"check-local" => "disable",
"min-procs" => 1,
"max-procs" => 1,
"upgrade" => 1
#"read-timeout" => 60,
#"write-timeout" => 60,
))
)
#server.max-read-idle := 60
#server.max-write-idle := 60
示例:echo.pl¶
echo.pl - 读取并逐行回显的简单“回显”脚本
这个脚本对 WebSocket 一无所知,它可以读/写 JSON 或其他任何数据。本示例是逐行读取和回显数据。
将文件安装到脚本位置和文档根目录/tmp/echo.pl<docroot>/count.html
echo.pl
#!/usr/bin/perl -Tw
$SIG{PIPE} = 'IGNORE';
for (my $FH; accept($FH, STDIN); close $FH) {
select($FH); $|=1; # $FH->autoflush;
print $FH $_ while (<$FH>);
}
<docroot>/count.html
<!DOCTYPE html>
<!-- modified from example in https://github.com/joewalnes/websocketd README.md -->
<pre id="log"></pre>
<script>
// helper function: log message to screen
var logelt = document.getElementById('log');
function log(msg) { logelt.textContent += msg + '\n'; }
// helper function: send websocket msg with count (1 .. 5)
var ll = 0;
function send_msg() { if (++ll <= 5) { log('SEND: '+ll); ws.send(ll+'\n'); } }
// setup websocket with callbacks
var ws = new WebSocket('ws://'+location.host+'/ws/');
ws.onopen = function() { log('CONNECT\n'); send_msg(); };
ws.onclose = function() { log('DISCONNECT'); };
ws.onmessage = function(event) { log('RECV: ' + event.data); send_msg(); };
</script>
echo-incremental.pl
这是一个替代的“回显”示例脚本,它是无缓冲的——而不是上面更简单的行缓冲示例——并且在每次读取之间会休眠 1 秒,以便您可以在浏览器中直观地看到多个 WebSocket 消息在服务器之间来回传递。此示例脚本将运行 5 秒,然后关闭 WebSocket 连接。(要运行此示例,请使用此脚本替换 echo.pl 并重新启动运行 lighttpd-wstunnel.conf 的 lighttpd。)
#!/usr/bin/perl -Tw
$SIG{PIPE} = 'IGNORE';
for (my $FH; accept($FH, STDIN); close $FH) {
foreach (1..5) {
sleep 1;
my $foo = "";
sysread($FH, $foo, 1024);
if (length($foo)) { syswrite($FH, $foo); }
}
close $FH;
}
示例:配置 lighttpd 作为 /ws/ 下 URL 的 WebSocket 端点¶
这是一个简单的示例,展示了 lighttpd 的 mod_wstunnel 如何终止 WebSocket 隧道并将负载发送/接收到后端“回显”脚本。对于此示例,请在备用端口上使用 mod_wstunnel 运行 lighttpd,此示例将在下面使用 mod_proxy 的示例中重复使用。
lighttpd-wstunnel.conf(在端口 8081 监听;使用 lighttpd -D -f /dev/shm/lighttpd-wstunnel.conf 启动;按 Ctrl-C 退出)
server.document-root = "/tmp" # place count.html here (better: use a more secure location)
server.bind = "127.0.0.1" # comment out if accessing from remote machine
server.port = 8081
server.modules += ("mod_wstunnel")
wstunnel.server = (
"/ws/" => (
(
"socket" => "/tmp/echo.sock", # should use more secure location
"bin-path" => "/tmp/echo.pl", # should use more secure location
"max-procs" => 1
)
)
)
在本地浏览器中加载为 https://:8081/count.html,并观察它从 1 数到 5。如果您是从远程系统连接,则修改 lighttpd-wstunnel.conf,注释掉
server.bind = "127.0.0.1" 并替换 count.html URL 中的 localhost。如果从远程机器访问,请替换 URL 中的 localhost。
示例:配置 lighttpd 反向代理 /ws/ 下的 URL¶
这是一个简单的示例,展示了 lighttpd 的 mod_proxy 如何支持 WebSocket 连接到另一个 lighttpd 实例,该实例使用 mod_wstunnel 终止 WebSocket 隧道并将负载发送/接收到后端“回显”脚本。它使用了上面 mod_wstunnel 的示例,因此请确保该示例也正在运行。
lighttpd-proxy.conf(在端口 8080 监听;在另一个 shell 中使用 lighttpd -D -f /dev/shm/lighttpd-proxy.conf 启动;按 Ctrl-C 退出)
server.document-root = "/tmp" # place count.html here (better: use a more secure location)
server.bind = "127.0.0.1" # comment out if accessing from remote machine
server.port = 8080
server.modules += ("mod_proxy")
proxy.server = ( "/" => (( "host" => "127.0.0.1", "port" => "8081" )))
proxy.header = ( "upgrade" => "enable" )
在本地浏览器中加载为 https://:8080/count.html,并观察它从 1 数到 5。如果您是从远程系统连接,则修改 lighttpd-proxy.conf,注释掉
server.bind = "127.0.0.1" 并替换 count.html URL 中的 localhost。如果从远程机器访问,请替换 URL 中的 localhost。
请注意,在此示例中,count.html 的目标现在是 https://:8080/count.html,使用端口 8080(在上面的示例中从 8081 更改)。