Skip to content

Commit

Permalink
Add command line flag to disable websocket authentication for control…
Browse files Browse the repository at this point in the history
… plane + minor cleanups
  • Loading branch information
tmistele committed Jan 3, 2025
1 parent 4b5993f commit 854dac4
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 28 deletions.
92 changes: 72 additions & 20 deletions crates/tinymist/src/tool/preview.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,17 @@ impl EditorServer for CompileHandler {
}
}

/// Whether to use websocket authentication. This should ideally always be `Enable`.
/// `UnsafeDisable` is only temporarily supported to ease the transition for downstream packages.
// FIXME: Remove `UnsafeDisable` (and the whole enum) in a future version.
#[derive(Clone, Debug, clap::ValueEnum)]
pub enum AuthenticationMode {
/// Enable websocket authentication
Enable,
/// Disable websocket authentication
UnsafeDisable,
}

/// CLI Arguments for the preview tool.
#[derive(Debug, Clone, clap::Parser)]
pub struct PreviewCliArgs {
Expand Down Expand Up @@ -230,6 +241,12 @@ pub struct PreviewCliArgs {
/// Don't open the preview in the browser after compilation.
#[clap(long = "no-open")]
pub dont_open_in_browser: bool,

/// Set "unsafe-disable" to disable websocket authentication for the control plane server. Careful: Among other things, this allows any website you visit to use the control plane server.
///
/// This option is only meant to ease the transition to authentication for downstream packages. It will be removed in a future version of tinymist.
#[clap(long, value_enum, default_value_t=AuthenticationMode::Enable)]
pub control_plane_auth_mode: AuthenticationMode,
}

/// The global state of the preview tool.
Expand Down Expand Up @@ -345,8 +362,14 @@ impl PreviewState {

let secret = auth::generate_token();

let srv =
make_http_server(true, args.data_plane_host, secret.clone(), websocket_tx).await;
let srv = make_http_server(
true,
AuthenticationMode::Enable,
args.data_plane_host,
secret.clone(),
websocket_tx,
)
.await;
let addr = srv.addr;
log::info!("PreviewTask({task_id}): preview server listening on: {addr}");

Expand Down Expand Up @@ -404,6 +427,7 @@ pub struct HttpServer {
/// Create a http server for the previewer.
pub async fn make_http_server(
serve_frontend_html: bool,
websocket_auth_mode: AuthenticationMode,
static_file_addr: String,
secret: String,
websocket_tx: mpsc::UnboundedSender<HyperWebsocketStream>,
Expand All @@ -414,9 +438,11 @@ pub async fn make_http_server(

let make_service = move || {
let websocket_tx = websocket_tx.clone();
let websocket_auth_mode = websocket_auth_mode.clone();
let secret = secret.clone();
service_fn(move |mut req: hyper::Request<Incoming>| {
let websocket_tx = websocket_tx.clone();
let websocket_auth_mode = websocket_auth_mode.clone();
let secret = secret.clone();
async move {
// Check if the request is a websocket upgrade request.
Expand All @@ -438,12 +464,22 @@ pub async fn make_http_server(
//
// Note: We use authentication only for the websocket. The static HTML file server (see below)
// only serves a not secret static template, so we don't bother with authentication there.
if let Ok(websocket) =
auth::try_auth_websocket_client(websocket, &secret).await
{
let _ = websocket_tx.send(websocket);
} else {
log::error!("Websocket client authentication failed");
match websocket_auth_mode {
AuthenticationMode::Enable => {
if let Ok(websocket) =
auth::try_auth_websocket_client(websocket, &secret).await
{
let _ = websocket_tx.send(websocket);
} else {
log::error!("Websocket client authentication failed");
}
}
AuthenticationMode::UnsafeDisable => {
// We optionally allow to skip authentication upon explicit request to ease the transition to
// authentication for downstream packages.
// FIXME: Remove this is in a future version.
let _ = websocket_tx.send(websocket);
}
}
});

Expand Down Expand Up @@ -608,10 +644,9 @@ pub async fn preview_main(args: PreviewCliArgs) -> anyhow::Result<()> {
let control_plane_server_handle = tokio::spawn(async move {
let (control_sock_tx, mut control_sock_rx) = mpsc::unbounded_channel();

// TODO: How to test this control plane thing? Where is it used?

let srv = make_http_server(
false,
args.control_plane_auth_mode,
args.control_plane_host,
secret_for_control_plane,
control_sock_tx,
Expand Down Expand Up @@ -683,25 +718,42 @@ pub async fn preview_main(args: PreviewCliArgs) -> anyhow::Result<()> {

let static_server = if let Some(static_file_host) = static_file_host {
log::warn!("--static-file-host is deprecated, which will be removed in the future. Use --data-plane-host instead.");
Some(make_http_server(true, static_file_host, secret.clone(), websocket_tx.clone()).await)
Some(
make_http_server(
true,
AuthenticationMode::Enable,
static_file_host,
secret.clone(),
websocket_tx.clone(),
)
.await,
)
} else {
None
};

let srv = make_http_server(true, args.data_plane_host, secret.clone(), websocket_tx).await;
let srv = make_http_server(
true,
AuthenticationMode::Enable,
args.data_plane_host,
secret.clone(),
websocket_tx,
)
.await;
log::info!("Data plane server listening on: {}", srv.addr);

let static_server_addr = static_server.as_ref().map(|s| s.addr).unwrap_or(srv.addr);
log::info!("Static file server listening on: {static_server_addr}");
let preview_url = format!(
"http://{static_server_addr}/#secret={secret}&previewMode={}",
match args.preview_mode {
PreviewMode::Document => "Doc",
PreviewMode::Slide => "Slide",
}
);
log::info!("Static file server listening on: {preview_url}");

if !args.dont_open_in_browser {
if let Err(e) = open::that_detached(format!(
"http://{static_server_addr}/#secret={secret}&previewMode={}",
match args.preview_mode {
PreviewMode::Document => "Doc",
PreviewMode::Slide => "Slide",
}
)) {
if let Err(e) = open::that_detached(preview_url) {
log::error!("failed to open browser: {e}");
};
}
Expand Down
7 changes: 4 additions & 3 deletions editors/vscode/src/features/preview-compat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,11 +287,10 @@ export const launchPreviewCompat = async (task: LaunchInBrowserTask | LaunchInWe
const enableCursor =
vscode.workspace.getConfiguration().get<boolean>("typst-preview.cursorIndicator") || false;
await watchEditorFiles();
const { serverProcess, controlPlanePort, dataPlanePort, staticFilePort } = await launchCli(
const { serverProcess, controlPlanePort, dataPlanePort, secret, staticFilePort } = await launchCli(
task.kind === "browser",
);

const secret = '__no_secret_because_typst-preview_doesnt_support_it__';

const addonΠserver = new Addon2Server(
controlPlanePort,
Expand Down Expand Up @@ -415,11 +414,12 @@ export const launchPreviewCompat = async (task: LaunchInBrowserTask | LaunchInWe
console.log(
`Launched server, data plane port:${dataPlanePort}, control plane port:${controlPlanePort}, static file port:${staticFilePort}`,
);
const secret = '__no_secret_because_typst-preview_doesnt_support_it__';
if (openInBrowser) {
const wsUrl = `ws://127.0.0.1:${dataPlanePort}`;
const queryString = (new URLSearchParams({
previewMode: task.mode === "doc" ? "Doc" : "Slide",
secret: '__no_secret_because_typst-preview_doesnt_support_it__',
secret,
wsUrl,
})).toString();
vscode.env.openExternal(vscode.Uri.parse(`http://127.0.0.1:${staticFilePort}/#${queryString}`));
Expand All @@ -429,6 +429,7 @@ export const launchPreviewCompat = async (task: LaunchInBrowserTask | LaunchInWe
serverProcess,
dataPlanePort,
controlPlanePort,
secret,
staticFilePort,
};
}
Expand Down
7 changes: 2 additions & 5 deletions tools/typst-preview-frontend/src/ws/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ interface WebSocketAndSubject {
function getUnsafeSocketCompat(url: string): Promise<WebSocketAndSubject>
{
return new Promise((wsresolve) => {
let _sock: WebSocket | undefined = undefined;

let prews = webSocket<ArrayBuffer>({
url,
binaryType: "arraybuffer",
Expand All @@ -35,7 +33,7 @@ function getUnsafeSocketCompat(url: string): Promise<WebSocketAndSubject>
openObserver: {
next: (e) => {
console.log('WebSocket connection opened', e.target);
_sock = e.target as any;
wsresolve({ websocketSubject: prews, websocket: e.target as any});
}
},
});
Expand All @@ -45,13 +43,12 @@ function getUnsafeSocketCompat(url: string): Promise<WebSocketAndSubject>
});

console.log("Authentication skipped (for compat with typst-preview, triggered by special value of `secret`)");
wsresolve({ websocketSubject: prews, websocket: _sock });
});
}

export function getAuthenticatedSocket(url: string, secret: string, dec: TextDecoder, enc: TextEncoder): Promise<WebSocketAndSubject> {
// Typst-preview doesn't support authentication. For now, we then skip authentication.
// Remove this once we no longer support compatibility with external typst-preview.
// FIXME: Remove this once we no longer support compatibility with external typst-preview.
if('__no_secret_because_typst-preview_doesnt_support_it__' === secret) {
return getUnsafeSocketCompat(url);
}
Expand Down

0 comments on commit 854dac4

Please sign in to comment.