mirror of
https://github.com/bitwarden/browser
synced 2026-02-01 17:23:37 +00:00
Expanded tunnel demo
This commit is contained in:
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
79
apps/browser/src/tools/popup/settings/tunnel.service.ts
Normal file
79
apps/browser/src/tools/popup/settings/tunnel.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user