Skip to content

Commit 4dab2fc

Browse files
committed
Add support for inverting the TLS passthrough UI to include-only mode
1 parent 437645d commit 4dab2fc

4 files changed

Lines changed: 142 additions & 37 deletions

File tree

src/components/settings/proxy-settings-card.tsx

Lines changed: 103 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ import { IconButton } from '../common/icon-button';
3232
import {
3333
SettingsButton,
3434
SettingsExplanation,
35-
SettingsSubheading
35+
SettingsSubheading,
36+
SettingsSubheadingRow
3637
} from './settings-components';
3738
import { StringSettingsList } from './string-settings-list';
3839

@@ -91,6 +92,24 @@ const Http2Select = styled(Select)`
9192
padding: 3px;
9293
`;
9394

95+
const TlsPassthroughModeToggle = styled.button.attrs(() => ({
96+
type: 'button' as const
97+
}))`
98+
display: flex;
99+
align-items: center;
100+
gap: 6px;
101+
cursor: pointer;
102+
103+
margin-left: auto;
104+
105+
background: none;
106+
border: none;
107+
padding: 0;
108+
color: ${p => p.theme.mainColor};
109+
font-family: ${p => p.theme.fontFamily};
110+
font-size: ${p => p.theme.textSize};
111+
`;
112+
94113
const TlsKeyLogInputContainer = styled.div`
95114
margin: 10px 0;
96115
display: flex;
@@ -177,22 +196,45 @@ export class ProxySettingsCard extends React.Component<
177196
else this.props.proxyStore!.setPortConfig(this.portConfig);
178197
}
179198

199+
private get tlsHostnames() {
200+
const config = this.props.proxyStore!.tlsInterceptionConfig;
201+
return 'tlsPassthrough' in config
202+
? config.tlsPassthrough
203+
: config.tlsInterceptOnly;
204+
}
205+
206+
private get tlsMode(): 'passthrough' | 'intercept-only' {
207+
return 'tlsInterceptOnly' in this.props.proxyStore!.tlsInterceptionConfig
208+
? 'intercept-only'
209+
: 'passthrough';
210+
}
211+
180212
@action.bound
181213
addTlsPassthroughHostname(hostname: string) {
182-
const { tlsPassthroughConfig } = this.props.proxyStore!;
183-
tlsPassthroughConfig.push({ hostname });
214+
this.tlsHostnames.push({ hostname });
184215
}
185216

186217
@action.bound
187218
removeTlsPassthroughHostname(hostname: string) {
188-
const { tlsPassthroughConfig } = this.props.proxyStore!;
189-
const hostnameIndex = _.findIndex(tlsPassthroughConfig, (passthroughItem) =>
190-
passthroughItem.hostname === hostname
219+
const hostnames = this.tlsHostnames;
220+
const hostnameIndex = _.findIndex(hostnames, (item) =>
221+
item.hostname === hostname
191222
);
192223

193224
if (hostnameIndex === -1) return;
194225

195-
tlsPassthroughConfig.splice(hostnameIndex, 1);
226+
hostnames.splice(hostnameIndex, 1);
227+
}
228+
229+
@action.bound
230+
toggleTlsMode() {
231+
const proxyStore = this.props.proxyStore!;
232+
const hostnames = this.tlsHostnames;
233+
if (this.tlsMode === 'passthrough') {
234+
proxyStore.tlsInterceptionConfig = { tlsInterceptOnly: hostnames };
235+
} else {
236+
proxyStore.tlsInterceptionConfig = { tlsPassthrough: hostnames };
237+
}
196238
}
197239

198240
@observable
@@ -233,13 +275,16 @@ export class ProxySettingsCard extends React.Component<
233275
http2Enabled,
234276
http2CurrentlyEnabled,
235277

236-
tlsPassthroughConfig,
237-
currentTlsPassthroughConfig,
278+
tlsInterceptionConfig,
279+
currentTlsInterceptionConfig,
238280

239281
keyLogFilePath,
240282
currentKeyLogFilePath
241283
} = proxyStore!;
242284

285+
const tlsMode = this.tlsMode;
286+
const tlsHostnames = this.tlsHostnames;
287+
243288
return <CollapsibleCard {...cardProps}>
244289
<header>
245290
<CollapsibleCardHeading onCollapseToggled={
@@ -252,7 +297,7 @@ export class ProxySettingsCard extends React.Component<
252297
visible={
253298
(this.isCurrentPortConfigValid && !this.isCurrentPortInRange) ||
254299
http2Enabled !== http2CurrentlyEnabled ||
255-
!_.isEqual(tlsPassthroughConfig, currentTlsPassthroughConfig) ||
300+
!_.isEqual(tlsInterceptionConfig, currentTlsInterceptionConfig) ||
256301
keyLogFilePath !== currentKeyLogFilePath
257302
}
258303
/>
@@ -301,36 +346,67 @@ export class ProxySettingsCard extends React.Component<
301346

302347
{
303348
versionSatisfies(serverVersion.value, TLS_PASSTHROUGH_SUPPORTED) && <>
304-
<SettingsSubheading>
305-
TLS Passthrough { !_.isEqual(tlsPassthroughConfig, currentTlsPassthroughConfig) &&
349+
<SettingsSubheadingRow>
350+
<SettingsSubheading>TLS Passthrough</SettingsSubheading>
351+
352+
{ !_.isEqual(tlsInterceptionConfig, currentTlsInterceptionConfig) &&
306353
<UnsavedIcon title="Restart app to apply changes" />
307354
}
308-
</SettingsSubheading>
355+
356+
<TlsPassthroughModeToggle
357+
title={tlsMode === 'passthrough'
358+
? "Listed hostnames are not intercepted. Click to switch to intercept-only mode."
359+
: "Only listed hostnames are intercepted. Click to switch to passthrough mode."
360+
}
361+
onClick={this.toggleTlsMode}
362+
>
363+
{ tlsMode === 'passthrough'
364+
? 'Exclude hostnames'
365+
: 'Intercept only these hostnames'
366+
}
367+
<Icon icon={['fas',
368+
tlsMode === 'passthrough' ? 'toggle-on' : 'toggle-off'
369+
]} />
370+
</TlsPassthroughModeToggle>
371+
</SettingsSubheadingRow>
309372

310373
<StringSettingsList
311-
values={tlsPassthroughConfig.map(c => c.hostname)}
374+
values={tlsHostnames.map(c => c.hostname)}
312375
onAdd={this.addTlsPassthroughHostname}
313376
onDelete={this.removeTlsPassthroughHostname}
314-
placeholder='A hostname whose TLS connections should not be intercepted'
377+
placeholder={tlsMode === 'intercept-only'
378+
? 'A hostname whose TLS connections should be intercepted'
379+
: 'A hostname whose TLS connections should not be intercepted'
380+
}
315381
validationFn={hostnameValidation}
316382
/>
317383

318384
<SettingsExplanation>
319-
Incoming TLS connections to these hostnames will bypass HTTP Toolkit, and will
320-
be forwarded upstream untouched instead of being intercepted. Clients will not see
321-
HTTP Toolkit's certificate, which may solve some connection issues, but traffic
322-
within these TLS connections will not be accessible.
385+
{ tlsMode === 'intercept-only'
386+
? <>
387+
Only TLS connections to these hostnames will be intercepted by HTTP Toolkit.
388+
All other TLS connections will be forwarded upstream without
389+
interception. Non-TLS traffic is always visible regardless of this setting.
390+
</>
391+
: <>
392+
TLS connections to these hostnames will be forwarded upstream untouched, without
393+
interception. This may solve some certificate trust connectivity issues,
394+
but traffic within these TLS connections will not be accessible.
395+
</>
396+
}
323397
</SettingsExplanation>
324398
</>
325399
}
326400

327401
{
328402
versionSatisfies(serverVersion.value, INITIAL_HTTP2_RANGE) && <>
329-
<SettingsSubheading>
330-
HTTP/2 Support { http2Enabled !== http2CurrentlyEnabled &&
403+
<SettingsSubheadingRow>
404+
<SettingsSubheading>HTTP/2 Support</SettingsSubheading>
405+
406+
{ http2Enabled !== http2CurrentlyEnabled &&
331407
<UnsavedIcon title="Restart app to apply changes" />
332408
}
333-
</SettingsSubheading>
409+
</SettingsSubheadingRow>
334410

335411
<Http2Select
336412
value={JSON.stringify(http2Enabled)}
@@ -350,11 +426,13 @@ export class ProxySettingsCard extends React.Component<
350426

351427
{
352428
versionSatisfies(serverVersion.value, KEY_LOG_FILE_SUPPORTED) && <>
353-
<SettingsSubheading>
354-
TLS Key Log File { keyLogFilePath !== currentKeyLogFilePath &&
429+
<SettingsSubheadingRow>
430+
<SettingsSubheading>TLS Key Log File</SettingsSubheading>
431+
432+
{ keyLogFilePath !== currentKeyLogFilePath &&
355433
<UnsavedIcon title="Restart app to apply changes" />
356434
}
357-
</SettingsSubheading>
435+
</SettingsSubheadingRow>
358436

359437
<TlsKeyLogInputContainer>
360438
{ versionSatisfies(desktopVersion.value, DESKTOP_SELECT_SAVE_FILE_SUPPORTED)

src/components/settings/settings-components.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,22 @@ export const SettingsExplanation = styled.p`
2525
`;
2626

2727
export const SettingsSubheading = styled(ContentLabel)`
28+
&:not(header + &):not(:first-child) {
29+
margin-top: 40px;
30+
}
31+
`;
32+
33+
// Subheaders can be wrapped with this if there are other controls
34+
export const SettingsSubheadingRow = styled.div`
35+
display: flex;
36+
align-items: center;
37+
justify-content: start;
38+
2839
&:not(header + &) {
2940
margin-top: 40px;
3041
}
42+
43+
${SettingsSubheading} {
44+
margin-top: 0;
45+
}
3146
`;

src/components/view/tls/tls-tunnel-details-pane.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ export class TlsTunnelDetailsPane extends React.Component<{
2727
<ContentLabelBlock>Details</ContentLabelBlock>
2828
<Content>
2929
<p>
30-
This TLS connection was not intercepted by HTTP Toolkit, as it matched
31-
a hostname that is configured for TLS passthrough in your settings.
30+
This TLS connection was not intercepted by HTTP Toolkit, as it is excluded
31+
from interception by the TLS passthrough configuration in your settings.
3232
</p>
3333
</Content>
3434
<Content>

src/model/proxy-store.ts

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ import { lazyObservablePromise } from '../util/observable';
3636
import { persist, hydrate } from '../util/mobx-persist/persist';
3737
import { isValidPort } from './network';
3838
import { serverVersion } from '../services/service-versions';
39-
import { DesktopApi } from '../services/desktop-api';
4039

4140
type HtkAdminClient =
4241
// WebRTC is only supported for new servers:
@@ -91,6 +90,10 @@ function startServer(
9190
}) as Promise<void>;
9291
}
9392

93+
type TlsInterceptionConfig =
94+
| Required<Pick<MockttpHttpsOptions, 'tlsPassthrough'>>
95+
| Required<Pick<MockttpHttpsOptions, 'tlsInterceptOnly'>>;
96+
9497
export function isValidPortConfiguration(portConfig: PortRange | undefined) {
9598
return portConfig === undefined || (
9699
portConfig.endPort >= portConfig.startPort &&
@@ -155,14 +158,23 @@ export class ProxyStore {
155158
if (!accountStore.user.isPaidUser()) {
156159
this.setPortConfig(undefined);
157160
this.http2Enabled = 'fallback';
158-
this.tlsPassthroughConfig = [];
161+
this.tlsInterceptionConfig = { tlsPassthrough: [] };
159162
}
160163
});
161164

162165
// Load all persisted settings from storage
163166
await hydrate({
164167
key: 'server-store',
165-
store: this
168+
store: this,
169+
dataTransform: (data: any) => {
170+
// Migrate old separate tlsPassthroughConfig data to tlsInterceptionConfig field:
171+
if (data.tlsPassthroughConfig && !data.tlsInterceptionConfig) {
172+
const hostnames = data.tlsPassthroughConfig as Array<{ hostname: string }>;
173+
data.tlsInterceptionConfig = { tlsPassthrough: hostnames };
174+
delete data.tlsPassthroughConfig;
175+
}
176+
return data;
177+
}
166178
});
167179

168180
console.log('Proxy settings loaded');
@@ -180,7 +192,7 @@ export class ProxyStore {
180192
// These are persisted initially, so we know if the user updates them that we
181193
// need to restart the proxy:
182194
this._http2CurrentlyEnabled = this.http2Enabled;
183-
this._currentTlsPassthroughConfig = _.cloneDeep(this.tlsPassthroughConfig);
195+
this._currentTlsInterceptionConfig = _.cloneDeep(this.tlsInterceptionConfig);
184196
this._currentKeyLogFilePath = this.keyLogFilePath;
185197

186198
this.monitorRemoteClientConnection(this.adminClient);
@@ -193,7 +205,7 @@ export class ProxyStore {
193205
// User configurable settings:
194206
http2: this._http2CurrentlyEnabled,
195207
https: {
196-
tlsPassthrough: this._currentTlsPassthroughConfig,
208+
...this._currentTlsInterceptionConfig,
197209
keyLogFile: this._currentKeyLogFilePath
198210
} as MockttpHttpsOptions, // Cert/Key options are set by the server
199211
socks: true,
@@ -303,11 +315,11 @@ export class ProxyStore {
303315
return this._http2CurrentlyEnabled;
304316
}
305317

306-
@persist('list') @observable
307-
tlsPassthroughConfig: Array<{ hostname: string }> = [];
308-
private _currentTlsPassthroughConfig: Array<{ hostname: string }> = [];
309-
get currentTlsPassthroughConfig() {
310-
return this._currentTlsPassthroughConfig;
318+
@persist('object') @observable
319+
tlsInterceptionConfig: TlsInterceptionConfig = { tlsPassthrough: [] };
320+
private _currentTlsInterceptionConfig: TlsInterceptionConfig = _.cloneDeep(this.tlsInterceptionConfig);
321+
get currentTlsInterceptionConfig() {
322+
return this._currentTlsInterceptionConfig;
311323
}
312324

313325
@persist @observable

0 commit comments

Comments
 (0)