This commit is contained in:
CyberRex
2026-07-31 16:13:09 +09:00
parent f4c2369057
commit 32f3c0a278
2 changed files with 78 additions and 3 deletions

View File

@@ -22,6 +22,10 @@ use crate::{
state::{AppState, RpcConnectionGuard},
};
// jsonrpc2-ws 1.0.0-beta23 passes JavaScript `null` as the browser
// WebSocket subprotocol, which Chromium serializes as the token "null".
const JSONRPC2_WS_BROWSER_PROTOCOL: &str = "null";
#[derive(Debug, Deserialize)]
struct RpcRequest {
#[serde(default)]
@@ -35,6 +39,7 @@ struct RpcRequest {
pub async fn upgrade(State(state): State<Arc<AppState>>, websocket: WebSocketUpgrade) -> Response {
websocket
.protocols([JSONRPC2_WS_BROWSER_PROTOCOL])
.max_message_size(1024 * 1024)
.on_upgrade(move |socket| connection(socket, state))
}
@@ -202,11 +207,21 @@ fn update_rooms(
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use std::{collections::HashSet, time::Duration};
use axum::{Router, routing::get};
use mirakurun_core::config::ConfigPaths;
use serde_json::json;
use tempfile::tempdir;
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
net::{TcpListener, TcpStream},
};
use tokio_util::sync::CancellationToken;
use super::update_rooms;
use crate::state::AppState;
use super::{JSONRPC2_WS_BROWSER_PROTOCOL, update_rooms, upgrade};
#[test]
fn joins_and_leaves_rooms() {
@@ -216,4 +231,64 @@ mod tests {
update_rooms(&mut rooms, &json!({"rooms": ["events:program"]}), false).expect("leave room");
assert!(rooms.is_empty());
}
#[tokio::test]
async fn selects_jsonrpc2_ws_browser_subprotocol() {
let directory = tempdir().expect("create tempdir");
let state = AppState::load(ConfigPaths {
server: directory.path().join("server.yml"),
tuners: directory.path().join("tuners.yml"),
channels: directory.path().join("channels.yml"),
services_db: directory.path().join("services.json"),
programs_db: directory.path().join("programs.json"),
logo_data_dir: directory.path().join("logo-data"),
})
.await
.expect("load state");
let app = Router::new().route("/rpc", get(upgrade)).with_state(state);
let listener = TcpListener::bind("127.0.0.1:0")
.await
.expect("bind listener");
let address = listener.local_addr().expect("listener address");
let shutdown = CancellationToken::new();
let server_shutdown = shutdown.clone();
let server = tokio::spawn(async move {
axum::serve(listener, app)
.with_graceful_shutdown(server_shutdown.cancelled_owned())
.await
});
let mut socket = TcpStream::connect(address).await.expect("connect server");
socket
.write_all(
format!(
"GET /rpc HTTP/1.1\r\n\
Host: {address}\r\n\
Connection: Upgrade\r\n\
Upgrade: websocket\r\n\
Sec-WebSocket-Version: 13\r\n\
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\
Sec-WebSocket-Protocol: {JSONRPC2_WS_BROWSER_PROTOCOL}\r\n\
\r\n"
)
.as_bytes(),
)
.await
.expect("write upgrade request");
let mut response = vec![0; 2048];
let length = tokio::time::timeout(Duration::from_secs(1), socket.read(&mut response))
.await
.expect("upgrade response timeout")
.expect("read upgrade response");
let response = String::from_utf8_lossy(&response[..length]).to_ascii_lowercase();
assert!(response.starts_with("http/1.1 101 switching protocols\r\n"));
assert!(response.contains("\r\nsec-websocket-protocol: null\r\n"));
drop(socket);
shutdown.cancel();
tokio::time::timeout(Duration::from_secs(1), server)
.await
.expect("server shutdown timeout")
.expect("join server")
.expect("run server");
}
}