1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-01 17:23:37 +00:00

Expanded tunnel demo

This commit is contained in:
Katherine Reynolds
2025-11-11 12:50:08 -08:00
parent a4e79b1341
commit a49303b102
2 changed files with 103 additions and 7 deletions

View File

@@ -23,6 +23,8 @@ import { PopOutComponent } from "../../../platform/popup/components/pop-out.comp
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
import { TunnelService } from "./tunnel.service";
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@Component({
@@ -54,6 +56,7 @@ export class TunnelDemoComponent {
private cipherService: CipherService,
private accountService: AccountService,
private formBuilder: FormBuilder,
private tunnelService: TunnelService,
) {}
async submit() {
@@ -96,12 +99,26 @@ export class TunnelDemoComponent {
const username = tunnelDemoCipher.login.username || "(none)";
const password = tunnelDemoCipher.login.password || "(none)";
await this.dialogService.openSimpleDialog({
title: "Tunnel Demo - Credentials Retrieved",
content: `Username: ${username}\nPassword: ${password}`,
type: "info",
acceptButtonText: { key: "ok" },
cancelButtonText: null,
});
// Send credentials to the localhost tunnel server
try {
await this.tunnelService.sendCredentials({ username, password });
await this.dialogService.openSimpleDialog({
title: "Tunnel Demo - Success",
content: `Credentials successfully sent to tunnel server.\n\nUsername:
${username}\nPassword: ${password.replace(/./g, "*")}`,
type: "success",
acceptButtonText: { key: "ok" },
cancelButtonText: null,
});
} catch (error) {
await this.dialogService.openSimpleDialog({
title: "Tunnel Demo - Error",
content: `Failed to send credentials to tunnel server: ${error.message || error}`,
type: "danger",
acceptButtonText: { key: "ok" },
cancelButtonText: null,
});
}
}
}

View File

@@ -0,0 +1,79 @@
import { Injectable } from "@angular/core";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
export interface TunnelCredentials {
username: string;
password: string;
}
/**
* Service for securely sending credentials to a localhost tunnel server.
* SECURITY: Only allows connections to loopback addresses (localhost/127.0.0.1/[::1]).
*/
@Injectable({
providedIn: "root",
})
export class TunnelService {
private readonly TUNNEL_PORT = 8086;
private readonly TUNNEL_ENDPOINT = "/bwTunnelDemo";
private readonly ALLOWED_LOOPBACK_HOSTS = ["localhost", "127.0.0.1", "[::1]", "::1"];
constructor(private logService: LogService) {}
/**
* Sends credentials to the local tunnel server.
* @param credentials The username and password to send
* @throws Error if the connection is not to a loopback address or if the request fails
*/
async sendCredentials(credentials: TunnelCredentials): Promise<void> {
const url = `http://localhost:${this.TUNNEL_PORT}${this.TUNNEL_ENDPOINT}`;
// Validate that we're only connecting to localhost
const urlObj = new URL(url);
if (!this.isLoopbackAddress(urlObj.hostname)) {
const error = `Security violation: Attempted to connect to non-loopback address: ${urlObj.hostname}`;
this.logService.error(error);
throw new Error("Tunnel service only allows connections to localhost");
}
try {
const request = new Request(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(credentials),
mode: "cors",
credentials: "omit",
cache: "no-store",
});
const response = await fetch(request);
if (!response.ok) {
throw new Error(
`Tunnel server responded with status ${response.status}: ${response.statusText}`,
);
}
this.logService.info("Credentials successfully sent to tunnel server");
} catch (error) {
this.logService.error(`Failed to send credentials to tunnel server: ${error}`);
throw error;
}
}
/**
* Validates that a hostname is a loopback address.
* @param hostname The hostname to validate
* @returns true if the hostname is a loopback address
*/
private isLoopbackAddress(hostname: string): boolean {
// Normalize hostname to lowercase for comparison
const normalizedHost = hostname.toLowerCase();
// Check against known loopback addresses
return this.ALLOWED_LOOPBACK_HOSTS.includes(normalizedHost);
}
}