diff --git a/client/src/server/server.ts b/client/src/server/server.ts index f24d5c3..faf17a9 100755 --- a/client/src/server/server.ts +++ b/client/src/server/server.ts @@ -16,11 +16,6 @@ type ListenFn = (resp: Recv[K]) => unknown export interface Options { timeout?: number - http: boolean -} - -const defaultSendOptions: Options = { - http: false } export interface Server { @@ -96,7 +91,7 @@ export class WebSocketServer extends EventDispatcher implements Server { history: History - private token: string | null = null + token: string | null = null constructor(cfg:ServerConfig) { super() @@ -122,8 +117,15 @@ export class WebSocketServer extends EventDispatcher implements Server { this.errorListener = fn } + fetch(path: string, init: RequestInit = {}) { + init.headers = { + ...init.headers, + ...(this.token && { 'Authorization': 'Bearer ' + this.token }) + } + return fetch(`${this.httpUrl}/${path}`, init) + } + query(type: K, content: Send[K], options: Partial = {}): Promise { - const http = options.http || this.socket.readyState !== WebSocket.OPEN let timeoutID = -1 let id = this.generateID() @@ -161,23 +163,7 @@ export class WebSocketServer extends EventDispatcher implements Server { // we predict an ok response from the server and dispatch right away. this.dispatch(type as any, content, promise.then()) - if (http) { - fetch(this.httpUrl + '/http', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - credentials: "include", - body: message, - }) - .then(async (resp) => { - const data = await resp.json() as RespPacket - this.handleResp(data) - }, async (resp) => { - const data = await resp.json() as RespPacket - this.handleResp(data) - }) - } else { - this.socket.send(message) - } + this.socket.send(message) return promise } @@ -193,10 +179,11 @@ export class WebSocketServer extends EventDispatcher implements Server { } private handleMessage(data: RespPacket | RecvPacket) { - if ('id' in data) { + if ('token' in data) { + this.token = data['token'] as string + } else if ('id' in data) { const ops = this.history.resp(data) if (ops) for (const [type, content] of ops) this.dispatch(type, content) - this.handleResp(data) } else if ('err' in data) { this.errorListener.call(undefined, data.err) diff --git a/client/src/ui/actions.ts b/client/src/ui/actions.ts index 7ecfa89..d955b0c 100644 --- a/client/src/ui/actions.ts +++ b/client/src/ui/actions.ts @@ -19,7 +19,6 @@ export async function saveMap() { export async function downloadMap() { const rmap_ = get(rmap) const server_ = get(server) - const httpUrl = server_.httpUrl download(`${httpUrl}/maps/${rmap_.map.name}`, `${rmap_.map.name}.map`) } diff --git a/client/src/ui/lib/editLayer.svelte b/client/src/ui/lib/editLayer.svelte index 886100f..c59c9a5 100644 --- a/client/src/ui/lib/editLayer.svelte +++ b/client/src/ui/lib/editLayer.svelte @@ -113,7 +113,7 @@ const url = externalImageUrl(e.detail) const embed = await showInfo('Do you wish to embed this image?', 'yesno') if (embed) { - const resp = await fetch(url, { credentials: 'include' }) + const resp = await $server.fetch(url) const file = await resp.blob() const name = e.detail uploadImageAndPick(file, name) diff --git a/client/src/ui/lib/editSharing.svelte b/client/src/ui/lib/editSharing.svelte index b184d09..5b88070 100644 --- a/client/src/ui/lib/editSharing.svelte +++ b/client/src/ui/lib/editSharing.svelte @@ -28,7 +28,6 @@ let resp = await fetch(httpUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - credentials: "include", body: JSON.stringify(json), }) if (!resp.ok) throw await resp.text() @@ -40,7 +39,7 @@ async function stopBridge(): Promise { const httpUrl = serverHttpUrl($serverCfg) + '/bridge_close' - await fetch(httpUrl, { credentials: 'include' }) + await fetch(httpUrl) } function onToggleSharing() { diff --git a/client/src/ui/lib/util.ts b/client/src/ui/lib/util.ts index 23e2fbc..128edd9 100644 --- a/client/src/ui/lib/util.ts +++ b/client/src/ui/lib/util.ts @@ -23,10 +23,10 @@ export type Ctor = new (...args: any[]) => T export type FormEvent = Event & { currentTarget: EventTarget & T } export type FormInputEvent = FormEvent -export async function download(file: string, name: string) { +export async function download(path: string, name: string) { const id = showInfo(`Downloading '${name}'…`, 'none') try { - const resp = await fetch(file, { credentials: 'include' }) + const resp = await fetch(path) const data = await resp.blob() const url = URL.createObjectURL(data) @@ -43,35 +43,25 @@ export async function download(file: string, name: string) { } } -export async function uploadMap(httpRoot: string, name: string, file: Blob) { - const resp = await fetch(`${httpRoot}/maps/${name}`, { +export async function uploadMap(url: string, name: string, file: Blob) { + const resp = await fetch(`${url}/maps/${name}`, { method: 'PUT', - credentials: 'include', body: file, }) if (!resp.ok) throw await resp.text() } -export async function createMap(httpRoot: string, name: string, create: MapCreation) { - const resp = await fetch(`${httpRoot}/maps/${name}`, { +export async function createMap(url: string, name: string, create: MapCreation) { + const resp = await fetch(`${url}/maps/${name}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - credentials: 'include', body: JSON.stringify(create), }) if (!resp.ok) throw await resp.text() } -// export async function uploadImage(httpRoot: string, mapName: string, imageName: string, file: Blob) { -// await fetch(`${httpRoot}/maps/${mapName}/images/${imageName}`, { -// method: 'POST', -// credentials: 'include', -// body: file -// }) -// } - export async function decodePng(file: Blob): Promise { return new Promise((resolve, reject) => { const img = document.createElement('img') @@ -97,7 +87,7 @@ export function externalImageUrl(name: string) { return '/mapres/' + name + '.png' } -export async function queryMaps(httpRoot: string): Promise { +export async function queryMaps(url: string): Promise { function sortMaps(maps: MapDetail[]): MapDetail[] { return maps.sort((a, b) => { if (a.users === b.users) return a.name.localeCompare(b.name) @@ -105,20 +95,20 @@ export async function queryMaps(httpRoot: string): Promise { }) } - const resp = await fetch(`${httpRoot}/maps`, { credentials: 'include' }) + const resp = await fetch(`${url}/maps`) const maps: MapDetail[] = await resp.json() sortMaps(maps) return maps } -export async function queryConfig(httpRoot: string, mapName: string): Promise { - const resp = await fetch(`${httpRoot}/maps/${mapName}/config`, { credentials: 'include' }) +export async function queryConfig(server: WebSocketServer, mapName: string): Promise { + const resp = await server.fetch(`maps/${mapName}/config`) const config: Config = await resp.json() return config } -export async function queryMap(httpRoot: string, mapName: string): Promise { - const resp = await fetch(`${httpRoot}/maps/${mapName}`, { credentials: 'include' }) +export async function queryMap(server: WebSocketServer, mapName: string): Promise { + const resp = await server.fetch(`maps/${mapName}`) const data = await resp.arrayBuffer() const map = new Map() map.load(mapName, data) @@ -126,11 +116,11 @@ export async function queryMap(httpRoot: string, mapName: string): Promise } export async function queryImageData( - httpRoot: string, + server: WebSocketServer, mapName: string, imageIndex: number ): Promise { - const resp = await fetch(`${httpRoot}/maps/${mapName}/images/${imageIndex}`, { credentials: 'include' }) + const resp = await server.fetch(`maps/${mapName}/images/${imageIndex}`) const data = await resp.blob() const image = await decodePng(data) return image @@ -138,11 +128,10 @@ export async function queryImageData( export async function queryImage( server: WebSocketServer, - httpRoot: string, mapName: string, imageIndex: number ): Promise { - const data = await queryImageData(httpRoot, mapName, imageIndex) + const data = await queryImageData(server, mapName, imageIndex) const img = new Image() const images = await server.query('get/images', undefined) img.loadEmbedded(data) diff --git a/client/src/ui/routes/edit.svelte b/client/src/ui/routes/edit.svelte index a087893..2ca5ccf 100644 --- a/client/src/ui/routes/edit.svelte +++ b/client/src/ui/routes/edit.svelte @@ -15,8 +15,7 @@ reset() await $server.query('join', { name, password }) - const httpUrl = $server.httpUrl - const map_ = await queryMap(httpUrl, name) + const map_ = await queryMap($server, name) const ams = await $server.query('get/automappers', undefined) $automappers = Object.fromEntries(ams.map(am => [am.name, am])) $map = map_ diff --git a/client/src/ui/routes/lobby.svelte b/client/src/ui/routes/lobby.svelte index d96b5a3..bbfd862 100644 --- a/client/src/ui/routes/lobby.svelte +++ b/client/src/ui/routes/lobby.svelte @@ -188,7 +188,6 @@ try { let resp = await fetch(`${httpUrl}/maps/${mapName}`, { method: 'DELETE', - credentials: "include", }) if (!resp.ok) { throw await resp.text() diff --git a/server/src/protocol.rs b/server/src/protocol.rs index eb26e2f..740b303 100644 --- a/server/src/protocol.rs +++ b/server/src/protocol.rs @@ -9,7 +9,7 @@ use serde_with::{ use twmap::{AutomapperConfig, EnvPoint, Position, Volume}; use vek::{Extent2, Rect, Rgba, Uv, Vec2}; -use crate::{base64::Base64, error::Error}; +use crate::{base64::Base64, error::Error, util::timestamp_now}; // Some documentation about the communication between clients and the server: // ---------- @@ -533,5 +533,15 @@ pub struct Packet { pub content: T, } +impl Packet { + pub fn new(id: Option, content: T) -> Self { + Self { + timestamp: timestamp_now(), + id, + content, + } + } +} + pub type SendPacket = Packet; pub type RecvPacket = Packet; diff --git a/server/src/router.rs b/server/src/router.rs index 63811db..3d0e63e 100644 --- a/server/src/router.rs +++ b/server/src/router.rs @@ -2,18 +2,17 @@ use std::{net::SocketAddr, sync::Arc}; use axum::{ body::Bytes, - extract::{ConnectInfo, DefaultBodyLimit, Path, State, WebSocketUpgrade}, - http::{header::CONTENT_TYPE, Method}, + extract::{ws, ConnectInfo, DefaultBodyLimit, Path, State, WebSocketUpgrade}, + http::{ + header::{AUTHORIZATION, CONTENT_TYPE}, + Method, + }, response::IntoResponse, routing::{delete, get, post}, Json, }; use axum_extra::{ - extract::{ - cookie::{Cookie, Expiration}, - CookieJar, - }, - headers::UserAgent, + headers::{authorization::Bearer, Authorization, UserAgent}, TypedHeader, }; use axum_server::tls_rustls::RustlsConfig; @@ -26,7 +25,7 @@ use tower_http::{ }; use vek::num_traits::clamp; -use crate::{base64::Base64, error::Error, protocol::*, server::User, util::timestamp_now}; +use crate::{base64::Base64, error::Error, protocol::*}; use crate::{Cli, Server}; pub struct Router { @@ -40,9 +39,8 @@ impl Router { let cors = cors::CorsLayer::new() .allow_methods([Method::GET, Method::PUT, Method::POST, Method::DELETE]) - .allow_headers([CONTENT_TYPE]) - .allow_origin(cors::AllowOrigin::mirror_request()) - .allow_credentials(true); + .allow_headers([CONTENT_TYPE, AUTHORIZATION]) + .allow_origin(cors::AllowOrigin::mirror_request()); let http_routes = axum::Router::new() .route("/http", post(route_http)) @@ -124,7 +122,7 @@ impl Router { ), }) .layer(DefaultBodyLimit::max( - clamp(args.max_map_size, 1 * 1024, 50 * 1024) * 1024, + clamp(args.max_map_size, 1024, 50 * 1024) * 1024, )) // allows uploading maps between 1MiB-50MiB .layer(cors) .with_state(server); @@ -179,7 +177,6 @@ fn gen_token() -> String { async fn route_websocket( State(server): State>, - jar: CookieJar, ws: WebSocketUpgrade, user_agent: Option>, ConnectInfo(addr): ConnectInfo, @@ -191,38 +188,37 @@ async fn route_websocket( }; let token = gen_token(); - let mut cookie = Cookie::new("twwe_session", token.clone()); - cookie.set_expires(Expiration::Session); - let jar = jar.add(cookie); log::info!("client {addr} connected as {token}"); log::debug!("client user-agent: `{user_agent}`"); - ( - jar, - ws.on_upgrade(move |socket| async move { - server.handle_websocket(token, socket).await; - log::info!("client {addr} disconnected"); - }), - ) -} - -fn user(jar: &CookieJar, server: &Server) -> Option> { - jar.get("twwe_session").and_then(|cookie| { - let token = cookie.value(); - server.user(token).ok() + ws.on_upgrade(move |mut socket| async move { + socket + .send(ws::Message::Text(format!("{{\"token\":\"{token}\"}}"))) + .await + .ok(); + server.handle_websocket(token, socket).await; + log::info!("client {addr} disconnected"); }) } -fn ensure_access_authorized(user: Option<&User>, map: &str, server: &Server) -> Result<(), Error> { +fn ensure_access_authorized( + auth: &Option>>, + map: &str, + server: &Server, +) -> Result<(), Error> { let room = server.room(map)?; + let token = auth.as_ref().map(|auth| auth.token()); + let user = token.and_then(|token| server.user(token).ok()); let authorized = room.config.password.is_none() - || user.and_then(|user| user.room()).is_some_and(|r| r == room); + || user + .as_ref() + .and_then(|user| user.room()) + .is_some_and(|r| r == room); if !authorized { log::debug!( "unauthorized: `{map}` for {}", - user.map(|user| user.token.as_str()) - .unwrap_or("nameless tee") + token.unwrap_or("nameless tee") ); } authorized.then_some(()).ok_or(Error::Unauthorized) @@ -230,10 +226,13 @@ fn ensure_access_authorized(user: Option<&User>, map: &str, server: &Server) -> async fn route_http( State(server): State>, - jar: CookieJar, + auth: Option>>, Json(req_packet): Json, ) -> impl IntoResponse { - let user = user(&jar, &server); + let user = auth + .as_ref() + .map(|auth| auth.token()) + .and_then(|token| server.user(token).ok()); let resp = server.do_request(user.clone(), req_packet.content.clone()); @@ -243,11 +242,7 @@ async fn route_http( } } - let resp_packet = SendPacket { - timestamp: timestamp_now(), - id: req_packet.id, - content: Message::Response(resp), - }; + let resp_packet = SendPacket::new(req_packet.id, Message::Response(resp)); Json(resp_packet) } @@ -257,10 +252,10 @@ async fn route_get_maps(State(server): State>) -> impl IntoResponse async fn route_get_map( State(server): State>, - jar: CookieJar, + auth: Option>>, Path(map): Path, ) -> impl IntoResponse { - ensure_access_authorized(user(&jar, &server).as_deref(), &map, &server)?; + ensure_access_authorized(&auth, &map, &server)?; server.get_map(&map) } @@ -288,28 +283,28 @@ async fn route_post_map( async fn route_delete_map( State(server): State>, - jar: CookieJar, + auth: Option>>, Path(map): Path, ) -> impl IntoResponse { - ensure_access_authorized(user(&jar, &server).as_deref(), &map, &server)?; + ensure_access_authorized(&auth, &map, &server)?; server.delete_map(&map) } async fn route_get_images( State(server): State>, - jar: CookieJar, + auth: Option>>, Path(map): Path, ) -> impl IntoResponse { - ensure_access_authorized(user(&jar, &server).as_deref(), &map, &server)?; + ensure_access_authorized(&auth, &map, &server)?; server.get_images(&map).map(Json) } async fn route_get_image( State(server): State>, - jar: CookieJar, + auth: Option>>, Path((map, image)): Path<(String, u16)>, ) -> impl IntoResponse { - ensure_access_authorized(user(&jar, &server).as_deref(), &map, &server)?; + ensure_access_authorized(&auth, &map, &server)?; server.get_image(&map, image) } @@ -322,142 +317,142 @@ async fn route_get_config( async fn route_post_config( State(server): State>, - jar: CookieJar, + auth: Option>>, Path(map): Path, Json(part_config): Json, ) -> impl IntoResponse { - ensure_access_authorized(user(&jar, &server).as_deref(), &map, &server)?; + ensure_access_authorized(&auth, &map, &server)?; server.edit_config(&map, part_config) } async fn route_get_info( State(server): State>, - jar: CookieJar, + auth: Option>>, Path(map): Path, ) -> impl IntoResponse { - ensure_access_authorized(user(&jar, &server).as_deref(), &map, &server)?; + ensure_access_authorized(&auth, &map, &server)?; server.get_info(&map).map(Json) } async fn route_post_info( State(server): State>, - jar: CookieJar, + auth: Option>>, Path(map): Path, Json(part_info): Json, ) -> impl IntoResponse { - ensure_access_authorized(user(&jar, &server).as_deref(), &map, &server)?; + ensure_access_authorized(&auth, &map, &server)?; server.edit_info(&map, part_info) } async fn route_get_envelopes( State(server): State>, - jar: CookieJar, + auth: Option>>, Path(map): Path, ) -> impl IntoResponse { - ensure_access_authorized(user(&jar, &server).as_deref(), &map, &server)?; + ensure_access_authorized(&auth, &map, &server)?; server.get_envelopes(&map).map(Json) } async fn route_put_envelope( State(server): State>, - jar: CookieJar, + auth: Option>>, Path(map): Path, Json(part_env): Json, ) -> impl IntoResponse { - ensure_access_authorized(user(&jar, &server).as_deref(), &map, &server)?; + ensure_access_authorized(&auth, &map, &server)?; server.put_envelope(&map, part_env) } async fn route_get_envelope( State(server): State>, - jar: CookieJar, + auth: Option>>, Path((map, env)): Path<(String, u16)>, ) -> impl IntoResponse { - ensure_access_authorized(user(&jar, &server).as_deref(), &map, &server)?; + ensure_access_authorized(&auth, &map, &server)?; server.get_envelope(&map, env).map(Json) } async fn route_post_envelope( State(server): State>, - jar: CookieJar, + auth: Option>>, Path((map, env)): Path<(String, u16)>, Json(part_env): Json, ) -> impl IntoResponse { - ensure_access_authorized(user(&jar, &server).as_deref(), &map, &server)?; + ensure_access_authorized(&auth, &map, &server)?; server.edit_envelope(&map, env, part_env) } async fn route_delete_envelope( State(server): State>, - jar: CookieJar, + auth: Option>>, Path((map, env)): Path<(String, u16)>, ) -> impl IntoResponse { - ensure_access_authorized(user(&jar, &server).as_deref(), &map, &server)?; + ensure_access_authorized(&auth, &map, &server)?; server.delete_envelope(&map, env) } async fn route_get_groups( State(server): State>, - jar: CookieJar, + auth: Option>>, Path(map): Path, ) -> impl IntoResponse { - ensure_access_authorized(user(&jar, &server).as_deref(), &map, &server)?; + ensure_access_authorized(&auth, &map, &server)?; server.get_groups(&map).map(Json) } async fn route_post_group( State(server): State>, - jar: CookieJar, + auth: Option>>, Path((map, group)): Path<(String, u16)>, Json(part_group): Json, ) -> impl IntoResponse { - ensure_access_authorized(user(&jar, &server).as_deref(), &map, &server)?; + ensure_access_authorized(&auth, &map, &server)?; server.edit_group(&map, group, part_group) } async fn route_put_group( State(server): State>, - jar: CookieJar, + auth: Option>>, Path(map): Path, Json(part_group): Json, ) -> impl IntoResponse { - ensure_access_authorized(user(&jar, &server).as_deref(), &map, &server)?; + ensure_access_authorized(&auth, &map, &server)?; server.put_group(&map, part_group) } async fn route_delete_group( State(server): State>, - jar: CookieJar, + auth: Option>>, Path((map, group)): Path<(String, u16)>, ) -> impl IntoResponse { - ensure_access_authorized(user(&jar, &server).as_deref(), &map, &server)?; + ensure_access_authorized(&auth, &map, &server)?; server.delete_group(&map, group) } async fn route_get_layers( State(server): State>, - jar: CookieJar, + auth: Option>>, Path((map, group)): Path<(String, u16)>, ) -> impl IntoResponse { - ensure_access_authorized(user(&jar, &server).as_deref(), &map, &server)?; + ensure_access_authorized(&auth, &map, &server)?; server.get_layers(&map, group).map(Json) } async fn route_put_layer( State(server): State>, - jar: CookieJar, + auth: Option>>, Path((map, group)): Path<(String, u16)>, Json(part_layer): Json, ) -> impl IntoResponse { - ensure_access_authorized(user(&jar, &server).as_deref(), &map, &server)?; + ensure_access_authorized(&auth, &map, &server)?; server.put_layer(&map, group, part_layer) } async fn route_delete_layer( State(server): State>, - jar: CookieJar, + auth: Option>>, Path((map, group, layer)): Path<(String, u16, u16)>, ) -> impl IntoResponse { - ensure_access_authorized(user(&jar, &server).as_deref(), &map, &server)?; + ensure_access_authorized(&auth, &map, &server)?; server.delete_layer(&map, group, layer) } diff --git a/server/src/server.rs b/server/src/server.rs index 29b6aff..2cada44 100644 --- a/server/src/server.rs +++ b/server/src/server.rs @@ -120,22 +120,13 @@ impl Server { impl Server { pub(crate) fn send(user: &User, id: Option, msg: Message) { - let packet = SendPacket { - timestamp: timestamp_now(), - id, - content: msg, - }; + let packet = SendPacket::new(id, msg); let str = serde_json::to_string(&packet).unwrap(); // this must not fail user.tx.unbounded_send(WebSocketMessage::Text(str)).ok(); // this is ok to fail (user logout) } pub(crate) fn broadcast_to_lobby(&self, msg: Message) { - let packet = SendPacket { - timestamp: timestamp_now(), - id: None, - content: msg, - }; - + let packet = SendPacket::new(None, msg); let str = serde_json::to_string(&packet).unwrap(); // this must not fail let _msg = WebSocketMessage::Text(str); @@ -143,12 +134,7 @@ impl Server { } pub(crate) fn broadcast_to_room(&self, room: &Room, content: Message) { - let packet = SendPacket { - timestamp: timestamp_now(), - id: None, - content, - }; - + let packet = SendPacket::new(None, content); let str = serde_json::to_string(&packet).unwrap(); // this must not fail let msg = WebSocketMessage::Text(str); @@ -158,12 +144,7 @@ impl Server { } pub(crate) fn broadcast_to_others(&self, user: &User, content: Message) { - let packet = SendPacket { - timestamp: timestamp_now(), - id: None, - content, - }; - + let packet = SendPacket::new(None, content); let str = serde_json::to_string(&packet).unwrap(); // this must not fail let msg = WebSocketMessage::Text(str); @@ -345,9 +326,9 @@ impl Server { pub(crate) fn handle_request(&self, user: Arc, packet: RecvPacket) { let resp = self.do_request(Some(user.clone()), packet.content.clone()); if resp.is_ok() { - self.do_broadcast(&*user, &packet); + self.do_broadcast(&user, &packet); } - Server::send(&*user, packet.id, Message::Response(resp)); + Server::send(&user, packet.id, Message::Response(resp)); } pub(crate) async fn handle_websocket(&self, token: String, socket: WebSocket) { @@ -363,7 +344,7 @@ impl Server { user_count + 1, self.max_users ); - if user_count as usize >= self.max_users { + if user_count >= self.max_users { Server::send(&user, None, Message::Response(Err(Error::MaxUsers))); fut_send.await.ok(); log::info!("kicked {}, too many connections", user.token); @@ -1803,11 +1784,11 @@ impl Server { let pwd_hash = self.room(&join.name)?.config.password.clone(); match (&join.password, &pwd_hash) { (Some(pwd), Some(hash)) => { - if !bcrypt::verify(pwd, &hash).map_err(|_| Error::Password)? { + if !bcrypt::verify(pwd, hash).map_err(|_| Error::Password)? { return Err(Error::Password); } } - (Some(pwd), None) if pwd == "" => (), + (Some(pwd), None) if pwd.is_empty() => (), (Some(_), None) | (None, Some(_)) => { return Err(Error::Password); }