Skip to content

Commit 4503c47

Browse files
committed
Fix mobile toolbar and LAN websocket auth
1 parent e6681bc commit 4503c47

4 files changed

Lines changed: 172 additions & 11 deletions

File tree

client/src/features/simulators/SimulatorMenu.tsx

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,16 @@ interface SimulatorMenuProps {
1818
isLoading: boolean;
1919
menuOpen: boolean;
2020
menuRef: RefObject<HTMLDivElement | null>;
21+
onBoot: () => void;
2122
onChangeSearch: (value: string) => void;
2223
onCloseMenu: () => void;
2324
onDismissKeyboard: () => void;
25+
onHome: () => void;
26+
onOpenAppSwitcher: () => void;
2427
onOpenBundlePrompt: () => void;
2528
onOpenUrlPrompt: () => void;
29+
onRotateRight: () => void;
30+
onShutdown: () => void;
2631
onStreamEncoderChange: (encoder: StreamEncoder) => void;
2732
onStreamFpsChange: (fps: StreamFps) => void;
2833
onStreamQualityChange: (quality: StreamQualityPreset) => void;
@@ -35,6 +40,8 @@ interface SimulatorMenuProps {
3540
search: string;
3641
selectedSimulator: SimulatorMetadata | null;
3742
setSelectedUDID: (udid: string) => void;
43+
showBootButton: boolean;
44+
showStopButton: boolean;
3845
streamConfig: StreamConfig;
3946
streamTransport: StreamTransport;
4047
touchOverlayVisible: boolean;
@@ -47,11 +54,16 @@ export function SimulatorMenu({
4754
isLoading,
4855
menuOpen,
4956
menuRef,
57+
onBoot,
5058
onChangeSearch,
5159
onCloseMenu,
5260
onDismissKeyboard,
61+
onHome,
62+
onOpenAppSwitcher,
5363
onOpenBundlePrompt,
5464
onOpenUrlPrompt,
65+
onRotateRight,
66+
onShutdown,
5567
onStreamEncoderChange,
5668
onStreamFpsChange,
5769
onStreamQualityChange,
@@ -64,6 +76,8 @@ export function SimulatorMenu({
6476
search,
6577
selectedSimulator,
6678
setSelectedUDID,
79+
showBootButton,
80+
showStopButton,
6781
streamConfig,
6882
streamTransport,
6983
touchOverlayVisible,
@@ -206,12 +220,61 @@ export function SimulatorMenu({
206220
</div>
207221
<div className="menu-divider" />
208222
<div className="menu-actions">
223+
{showBootButton ? (
224+
<button
225+
className="menu-action mobile-menu-action"
226+
onClick={() => {
227+
onBoot();
228+
onCloseMenu();
229+
}}
230+
>
231+
Boot
232+
</button>
233+
) : null}
234+
{showStopButton ? (
235+
<button
236+
className="menu-action mobile-menu-action"
237+
onClick={() => {
238+
onShutdown();
239+
onCloseMenu();
240+
}}
241+
>
242+
Stop
243+
</button>
244+
) : null}
209245
<button className="menu-action" onClick={onOpenUrlPrompt}>
210246
Open URL…
211247
</button>
212248
<button className="menu-action" onClick={onOpenBundlePrompt}>
213249
Launch Bundle…
214250
</button>
251+
<button
252+
className="menu-action mobile-menu-action"
253+
onClick={() => {
254+
onHome();
255+
onCloseMenu();
256+
}}
257+
>
258+
Home
259+
</button>
260+
<button
261+
className="menu-action mobile-menu-action"
262+
onClick={() => {
263+
onOpenAppSwitcher();
264+
onCloseMenu();
265+
}}
266+
>
267+
App Switcher
268+
</button>
269+
<button
270+
className="menu-action mobile-menu-action"
271+
onClick={() => {
272+
onRotateRight();
273+
onCloseMenu();
274+
}}
275+
>
276+
Rotate Right
277+
</button>
215278
<button
216279
className="menu-action"
217280
onClick={() => {

client/src/features/toolbar/Toolbar.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -142,11 +142,16 @@ export function Toolbar({
142142
isLoading={isLoading}
143143
menuOpen={menuOpen}
144144
menuRef={menuRef}
145+
onBoot={onBoot}
145146
onChangeSearch={onChangeSearch}
146147
onCloseMenu={closeMenu}
147148
onDismissKeyboard={onDismissKeyboard}
149+
onHome={onHome}
150+
onOpenAppSwitcher={onOpenAppSwitcher}
148151
onOpenBundlePrompt={onOpenBundlePrompt}
149152
onOpenUrlPrompt={onOpenUrlPrompt}
153+
onRotateRight={onRotateRight}
154+
onShutdown={onShutdown}
150155
onStreamEncoderChange={onStreamEncoderChange}
151156
onStreamFpsChange={onStreamFpsChange}
152157
onStreamQualityChange={onStreamQualityChange}
@@ -159,6 +164,8 @@ export function Toolbar({
159164
search={search}
160165
selectedSimulator={selectedSimulator}
161166
setSelectedUDID={setSelectedUDID}
167+
showBootButton={showBootButton}
168+
showStopButton={showStopButton}
162169
streamConfig={streamConfig}
163170
streamTransport={streamTransport}
164171
touchOverlayVisible={touchOverlayVisible}
@@ -211,23 +218,23 @@ export function Toolbar({
211218
) : null}
212219
<button
213220
aria-label="Open URL"
214-
className="tbtn icon-btn"
221+
className="tbtn icon-btn toolbar-mobile-hidden"
215222
onClick={onOpenUrlPrompt}
216223
title="Open URL"
217224
>
218225
<OpenUrlIcon />
219226
</button>
220227
<button
221228
aria-label="Home"
222-
className="tbtn icon-btn"
229+
className="tbtn icon-btn toolbar-mobile-hidden"
223230
onClick={onHome}
224231
title="Home"
225232
>
226233
<HomeIcon />
227234
</button>
228235
<button
229236
aria-label="App Switcher"
230-
className="tbtn icon-btn"
237+
className="tbtn icon-btn toolbar-mobile-hidden"
231238
onClick={onOpenAppSwitcher}
232239
title="App Switcher"
233240
>
@@ -243,7 +250,7 @@ export function Toolbar({
243250
</button>
244251
<button
245252
aria-label="Rotate Right"
246-
className="tbtn icon-btn"
253+
className="tbtn icon-btn toolbar-mobile-hidden"
247254
onClick={onRotateRight}
248255
title="Rotate Right"
249256
>

client/src/styles/layout.css

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
align-items: center;
1212
justify-content: space-between;
1313
gap: 8px;
14+
box-sizing: border-box;
15+
width: 100%;
16+
min-width: 0;
1417
height: 40px;
1518
padding: 0 8px;
1619
background: var(--toolbar-bg);
@@ -37,8 +40,10 @@
3740
.toolbar-sim-info {
3841
display: flex;
3942
align-items: center;
43+
flex: 1 1 0;
4044
gap: 8px;
4145
min-width: 0;
46+
max-width: 100%;
4247
overflow: hidden;
4348
}
4449

@@ -59,6 +64,7 @@
5964
.toolbar-sim-name {
6065
font-weight: 600;
6166
font-size: 13px;
67+
min-width: 0;
6268
white-space: nowrap;
6369
overflow: hidden;
6470
text-overflow: ellipsis;
@@ -122,7 +128,6 @@
122128

123129
.toolbar-right {
124130
flex: 0 0 auto;
125-
max-width: 58%;
126131
min-width: 0;
127132
overflow: visible;
128133
}
@@ -142,12 +147,12 @@
142147
max-width: 96px;
143148
}
144149

145-
.toolbar-mobile-hidden {
146-
display: inline-flex;
150+
.toolbar .toolbar-mobile-hidden {
151+
display: none;
147152
}
148153

149-
.mobile-menu-action {
150-
display: none;
154+
.menu-actions .mobile-menu-action {
155+
display: block;
151156
}
152157

153158
.debug-grid {

server/src/auth.rs

Lines changed: 88 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,15 +177,19 @@ fn origin_allowed_or_absent(config: &Config, headers: &HeaderMap) -> bool {
177177
headers
178178
.get(header::ORIGIN)
179179
.and_then(|value| value.to_str().ok())
180-
.map(|origin| origin_is_allowed(config, origin))
180+
.map(|origin| origin_is_allowed_for_request(config, headers, origin))
181181
.unwrap_or(true)
182182
}
183183

184184
fn origin_allowed(config: &Config, headers: &HeaderMap) -> bool {
185185
headers
186186
.get(header::ORIGIN)
187187
.and_then(|value| value.to_str().ok())
188-
.is_some_and(|origin| origin_is_allowed(config, origin))
188+
.is_some_and(|origin| origin_is_allowed_for_request(config, headers, origin))
189+
}
190+
191+
fn origin_is_allowed_for_request(config: &Config, headers: &HeaderMap, origin: &str) -> bool {
192+
origin_is_allowed(config, origin) || origin_matches_request_host(headers, origin)
189193
}
190194

191195
fn origin_is_allowed(config: &Config, origin: &str) -> bool {
@@ -204,6 +208,33 @@ fn origin_is_cors_allowed(config: &Config, origin: &str) -> bool {
204208
origin == "null" || origin_is_allowed(config, origin)
205209
}
206210

211+
fn origin_matches_request_host(headers: &HeaderMap, origin: &str) -> bool {
212+
let Some(origin_authority) = origin_authority(origin) else {
213+
return false;
214+
};
215+
headers
216+
.get(header::HOST)
217+
.and_then(|value| value.to_str().ok())
218+
.map(normalize_authority)
219+
.is_some_and(|host| host == origin_authority)
220+
}
221+
222+
fn origin_authority(origin: &str) -> Option<String> {
223+
let without_scheme = origin
224+
.strip_prefix("http://")
225+
.or_else(|| origin.strip_prefix("https://"))?;
226+
let authority = without_scheme
227+
.split_once('/')
228+
.map(|(authority, _)| authority)
229+
.unwrap_or(without_scheme)
230+
.trim();
231+
(!authority.is_empty()).then(|| normalize_authority(authority))
232+
}
233+
234+
fn normalize_authority(authority: &str) -> String {
235+
authority.trim().trim_end_matches('.').to_ascii_lowercase()
236+
}
237+
207238
fn extra_allowed_origins() -> impl Iterator<Item = String> {
208239
std::env::var("SIMDECK_ALLOWED_ORIGINS")
209240
.ok()
@@ -331,6 +362,38 @@ mod tests {
331362
));
332363
}
333364

365+
#[test]
366+
fn accepts_cookie_for_same_origin_tailscale_host() {
367+
let config = Config::new(
368+
4310,
369+
PathBuf::from("client/dist"),
370+
IpAddr::V4(Ipv4Addr::UNSPECIFIED),
371+
Some("192.168.1.50".to_owned()),
372+
"auto".to_owned(),
373+
false,
374+
Some("secret-token".to_owned()),
375+
Some("123456".to_owned()),
376+
);
377+
let mut headers = HeaderMap::new();
378+
headers.insert(
379+
header::COOKIE,
380+
HeaderValue::from_static("simdeck_token=secret-token"),
381+
);
382+
headers.insert(
383+
header::ORIGIN,
384+
HeaderValue::from_static("http://100.64.10.20:4310"),
385+
);
386+
headers.insert(header::HOST, HeaderValue::from_static("100.64.10.20:4310"));
387+
388+
assert!(api_request_authorized(
389+
&config,
390+
&Method::GET,
391+
&headers,
392+
false,
393+
None
394+
));
395+
}
396+
334397
#[test]
335398
fn rejects_cross_origin_cookie_without_header_token() {
336399
let config = config();
@@ -353,6 +416,29 @@ mod tests {
353416
));
354417
}
355418

419+
#[test]
420+
fn rejects_cross_origin_cookie_when_origin_does_not_match_host() {
421+
let config = config();
422+
let mut headers = HeaderMap::new();
423+
headers.insert(
424+
header::COOKIE,
425+
HeaderValue::from_static("simdeck_token=secret-token"),
426+
);
427+
headers.insert(
428+
header::ORIGIN,
429+
HeaderValue::from_static("http://100.64.10.20:4310"),
430+
);
431+
headers.insert(header::HOST, HeaderValue::from_static("127.0.0.1:4310"));
432+
433+
assert!(!api_request_authorized(
434+
&config,
435+
&Method::GET,
436+
&headers,
437+
false,
438+
None
439+
));
440+
}
441+
356442
#[test]
357443
fn accepts_loopback_same_origin_without_existing_token() {
358444
let config = config();

0 commit comments

Comments
 (0)