johnandersen777.bsky.social/atlibs-webrtc
WebRTC transport for atlibs: com.fedfork.exec.webrtc.connect in the browser, and webrtc-peer, its guest side
0
0
// SSH over an open WebRTC datachannel (a tag bridged to the guest's sshd on 22), with
// @microsoft/dev-tunnels-ssh (pure TS, WebCrypto) injected as `ssh` ({...namespace, Buffer}).
// The library speaks ecdsa/rsa host keys, not ed25519, so the host key is reported, not
// pinned: the guest is authenticated by the DTLS fingerprint the bidder signed (vm.onNetwork).
// ponytail: one exec channel per command (no pty); add a shell channel + xterm if a real terminal is wanted.
// received: what the channel delivered before ssh took it over (sshd speaks first: its banner).
export async function connectSsh(ssh, dc, keyPair, user, received = []) {
class DataChannelStream extends ssh.BaseStream {
constructor() {
super();
for (const data of received) this.onData(ssh.Buffer.from(data));
dc.onmessage = ({ data }) => this.onData(ssh.Buffer.from(data));
dc.onclose = () => this.onEnd();
}
async write(data) {
dc.send(data);
}
async close() {
dc.close();
}
}
const session = new ssh.SshClientSession(new ssh.SshSessionConfiguration());
let hostKey;
session.onAuthenticating((e) => {
hostKey = e.publicKey;
e.authenticationPromise = Promise.resolve({});
});
await session.connect(new DataChannelStream());
if (!(await session.authenticateServer())) throw new Error("ssh: server authentication failed");
const digest = await crypto.subtle.digest("SHA-256", await hostKey.getPublicKeyBytes());
const fingerprint = `SHA256:${new Uint8Array(digest).toBase64({ omitPadding: true })}`;
if (!(await session.authenticateClient({ username: user, publicKeys: [keyPair] }))) throw new Error(`ssh: ${user} rejected the key`);
return {
hostKey: { type: hostKey.keyAlgorithmName, fingerprint },
// stdin (optional): written to the command, then EOF; secrets go here, never in command
// (the guest's shell runs the command as an argument, so ps shows it).
async exec(command, stdin) {
const channel = await session.openChannel();
let output = "";
const decoder = new TextDecoder();
channel.onDataReceived((data) => {
output += decoder.decode(data, { stream: true });
channel.adjustWindow(data.length);
});
const closed = new Promise((resolve) => channel.onClosed(resolve));
const request = new ssh.CommandRequestMessage();
request.requestType = "exec";
request.command = command;
request.wantReply = true;
if (!(await channel.request(request))) throw new Error("ssh: exec refused");
if (stdin !== undefined) {
if (stdin) await channel.send(ssh.Buffer.from(stdin));
await channel.send(ssh.Buffer.alloc(0)); // an empty send is EOF
}
await closed;
return output;
},
close: () => session.close(),
};
}
// An ecdsa-sha2-nistp256 key pair and its authorized_keys line.
export async function sshKeyPair(ssh) {
const keyPair = await ssh.SshAlgorithms.publicKey.ecdsaSha2Nistp256.generateKeyPair();
return { keyPair, authorizedKey: `ecdsa-sha2-nistp256 ${(await keyPair.getPublicKeyBytes()).toString("base64")} webrtc-page` };
}