Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions src/routes/api/internal/diagnostics/tls-handshake/+server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { errorManager } from '$lib/utils/error-manager';

interface TLSHandshakeRequest {
hostname: string;
servername?: string;
port?: number;
}

Expand All @@ -18,6 +19,7 @@ interface HandshakePhase {

interface TLSHandshakeResponse {
hostname: string;
servername?: string;
port: number;
success: boolean;
totalTime: number;
Expand All @@ -38,15 +40,15 @@ interface TLSHandshakeResponse {
export const POST: RequestHandler = async ({ request }) => {
try {
const body: TLSHandshakeRequest = await request.json();
const { hostname, port = 443 } = body;
const { hostname, port = 443, servername } = body;

if (!hostname || typeof hostname !== 'string' || !hostname.trim()) {
throw error(400, 'Hostname is required');
}

const trimmedHostname = hostname.trim();

const result = await performTLSHandshake(trimmedHostname, port);
const result = await performTLSHandshake(trimmedHostname, port, servername);
return json(result);
} catch (err) {
errorManager.captureException(err, 'error', { component: 'TLS Handshake API' });
Expand All @@ -57,7 +59,7 @@ export const POST: RequestHandler = async ({ request }) => {
}
};

function performTLSHandshake(hostname: string, port: number): Promise<TLSHandshakeResponse> {
function performTLSHandshake(hostname: string, port: number, servername?: string): Promise<TLSHandshakeResponse> {
return new Promise((resolve, reject) => {
const phases: HandshakePhase[] = [];
const startTime = Date.now();
Expand Down Expand Up @@ -101,7 +103,7 @@ function performTLSHandshake(hostname: string, port: number): Promise<TLSHandsha
const tlsSocket = (tls as any).connect(
{
socket: tcpSocket,
servername: hostname,
servername: servername || hostname,
// SECURITY: rejectUnauthorized must be false for this TLS diagnostic tool.
// This tool analyzes TLS handshakes including servers with certificate issues.
rejectUnauthorized: false,
Expand All @@ -128,6 +130,7 @@ function performTLSHandshake(hostname: string, port: number): Promise<TLSHandsha

const response: TLSHandshakeResponse = {
hostname,
servername: servername || hostname,
port,
success: true,
totalTime,
Expand Down
10 changes: 6 additions & 4 deletions src/routes/api/internal/diagnostics/tls/+server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ interface OCSPStaplingReq extends BaseReq {
interface CipherPresetsReq extends BaseReq {
action: 'cipher-presets';
hostname: string;
servername?: string;
port?: number;
}

Expand Down Expand Up @@ -388,14 +389,15 @@ async function checkOCSPStapling(hostname: string, port: number = 443): Promise<
}

// Cipher Presets test implementation
async function testCipherPresets(hostname: string, port: number = 443): Promise<any> {
async function testCipherPresets(hostname: string, port: number = 443, servername?: string): Promise<any> {
// First verify the host is reachable by attempting a basic TLS connection
try {
await new Promise<void>((resolve, reject) => {
const socket = (tls as any).connect(
{
host: hostname,
port,
servername: servername || hostname,
// SECURITY: rejectUnauthorized must be false to test cipher presets (see above)
rejectUnauthorized: false,
},
Expand Down Expand Up @@ -874,7 +876,7 @@ export const POST: RequestHandler = async ({ request }) => {
}

case 'cipher-presets': {
const { hostname, port = 443 } = body as CipherPresetsReq;
const { hostname, port = 443, servername } = body as CipherPresetsReq;

// Validate hostname
if (!hostname || typeof hostname !== 'string' || hostname.trim() === '') {
Expand All @@ -886,8 +888,8 @@ export const POST: RequestHandler = async ({ request }) => {
throw error(400, 'Invalid port number');
}

const result = await testCipherPresets(hostname, port);
return json({ ...result, hostname, port });
const result = await testCipherPresets(hostname, port, servername);
return json({ ...result, hostname, port, servername });
}

case 'banner': {
Expand Down
51 changes: 42 additions & 9 deletions src/routes/diagnostics/tls/banner/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

let host = $state('');
let port = $state<number | null>(null);
let servername = $state('');
let useCustomServername = $state(false);
let service = $state('custom');

const diagnosticState = useDiagnosticState<any>();
Expand Down Expand Up @@ -79,6 +81,7 @@
body: JSON.stringify({
action: 'banner',
host: host.trim(),
servername: useCustomServername && servername ? servername.trim() : undefined,
port: port,
}),
});
Expand Down Expand Up @@ -210,15 +213,45 @@
</div>
</div>

<button onclick={grabBanner} disabled={diagnosticState.loading || !isInputValid} class="primary">
{#if diagnosticState.loading}
<Icon name="loader" size="sm" animate="spin" />
Connecting...
{:else}
<Icon name="terminal" size="sm" />
Grab Banner
{/if}
</button>
<div class="form-row">
<div class="form-group">
<label class="checkbox-group">
<input
type="checkbox"
bind:checked={useCustomServername}
onchange={() => {
examples.clear();
if (isInputValid()) grabBanner();
}}
/>
Use custom SNI servername
</label>
{#if useCustomServername}
<input
type="text"
bind:value={servername}
placeholder="example.com"
use:tooltip={'Custom servername for SNI (Server Name Indication)'}

Check failure on line 234 in src/routes/diagnostics/tls/banner/+page.svelte

View workflow job for this annotation

GitHub Actions / 🧼 Lint

'tooltip' is not defined
onchange={() => {
examples.clear();
if (isInputValid()) grabBanner();
}}
/>
{/if}
</div>
</div>

<div class="action-section">
<button onclick={grabBanner} disabled={diagnosticState.loading || !isInputValid} class="primary">
{#if diagnosticState.loading}
<Icon name="loader" size="sm" animate="spin" />
Connecting...
{:else}
<Icon name="terminal" size="sm" />
Grab Banner
{/if}
</button>
</div>
</div>
</div>

Expand Down
111 changes: 79 additions & 32 deletions src/routes/diagnostics/tls/cipher-presets/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,26 @@

let hostname = $state('example.com');
let port = $state('443');
let servername = $state('');
let useCustomServername = $state(false);
const diagnosticState = useDiagnosticState<any>();
const examplesList = [
{ host: 'github.com', port: '443', description: 'GitHub cipher support' },
{ host: 'cloudflare.com', port: '443', description: 'Cloudflare cipher support' },
{ host: 'google.com', port: '443', description: 'Google cipher support' },
];
const examples = useExamples(examplesList);

// Reactive validation
const isInputValid = $derived(() => {
const trimmedHost = host.trim();

Check failure on line 22 in src/routes/diagnostics/tls/cipher-presets/+page.svelte

View workflow job for this annotation

GitHub Actions / 🧼 Lint

'host' is not defined
if (!trimmedHost) return false;
if (port === null || port < 1 || port > 65535) return false;
// Basic hostname/IP validation
const hostPattern =
/^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$|^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$|^\[?[a-fA-F0-9:]+\]?$/;
return hostPattern.test(trimmedHost);
});

async function testCiphers() {
if (!hostname?.trim()) {
Expand All @@ -30,6 +43,7 @@
body: JSON.stringify({
action: 'cipher-presets',
hostname: hostname.trim().toLowerCase(),
servername: useCustomServername && servername ? servername.trim() : undefined,
port: parseInt(port) || 443,
}),
});
Expand Down Expand Up @@ -94,40 +108,73 @@
<h3>Cipher Presets Configuration</h3>
</div>
<div class="card-content">
<div class="form-group">
<label for="hostname">Hostname and Port</label>
<div class="input-flex-container">
<input
id="hostname"
type="text"
bind:value={hostname}
placeholder="example.com"
disabled={diagnosticState.loading}
onchange={() => examples.clear()}
onkeydown={(e) => e.key === 'Enter' && testCiphers()}
class="flex-grow"
/>
<input
id="port"
type="text"
bind:value={port}
placeholder="443"
disabled={diagnosticState.loading}
onchange={() => examples.clear()}
onkeydown={(e) => e.key === 'Enter' && testCiphers()}
class="port-input"
/>
<button onclick={testCiphers} disabled={diagnosticState.loading} class="primary">
{#if diagnosticState.loading}
<Icon name="loader" size="sm" animate="spin" />
Testing...
{:else}
<Icon name="search" size="sm" />
Test
{/if}
</button>
<div class="form-row">
<div class="form-group">
<label for="hostname">Hostname and Port</label>
<div class="input-flex-container">
<input
id="hostname"
type="text"
bind:value={hostname}
placeholder="example.com"
disabled={diagnosticState.loading}
onchange={() => examples.clear()}
onkeydown={(e) => e.key === 'Enter' && testCiphers()}
class="flex-grow"
/>
<input
id="port"
type="text"
bind:value={port}
placeholder="443"
disabled={diagnosticState.loading}
onchange={() => examples.clear()}
onkeydown={(e) => e.key === 'Enter' && testCiphers()}
class="port-input"
/>
</div>
</div>
</div>

<div class="form-row">
<div class="form-group">
<label class="checkbox-group">
<input
type="checkbox"
bind:checked={useCustomServername}
onchange={() => {
examples.clear();
if (isInputValid()) testCiphers();
}}
/>
Use custom SNI servername
</label>
{#if useCustomServername}
<input
type="text"
bind:value={servername}
placeholder="example.com"
use:tooltip={'Custom servername for SNI (Server Name Indication)'}

Check failure on line 157 in src/routes/diagnostics/tls/cipher-presets/+page.svelte

View workflow job for this annotation

GitHub Actions / 🧼 Lint

'tooltip' is not defined
onchange={() => {
examples.clear();
if (isInputValid()) testCiphers();
}}
/>
{/if}
</div>
</div>

<div class="action-section">
<button onclick={testCiphers} disabled={diagnosticState.loading || !isInputValid} class="primary">
{#if diagnosticState.loading}
<Icon name="loader" size="sm" animate="spin" />
Testing...
{:else}
<Icon name="search" size="sm" />
Test
{/if}
</button>
</div>
</div>
</div>

Expand Down
Loading
Loading