UNPKG

pg-browser-proxy

Version:

A WebSocket-TCP proxy that enables PostgreSQL clients to connect to a Postgres database running in the browser

41 lines (32 loc) 8 kB
#!/usr/bin/env node import{build as K}from"gluegun";import d from"chalk";var l={tcpPort:5432,wsPort:443};function W(e){return{tcpPort:(e==null?void 0:e.tcpPort)??l.tcpPort,wsPort:(e==null?void 0:e.wsPort)??l.wsPort}}var O={name:"help",alias:"h",dashed:!0,description:"Display help information",run:e=>{let{print:t}=e;t.info(` ${d.bold.blue("\u{1F50C} PG Browser Proxy")} ${d.blue("A WebSocket-TCP proxy that enables PostgreSQL clients to connect to in-browser databases")} ${d.bold("Options:")} -t, --tcp-port <port> TCP port for PostgreSQL clients ${d.dim(`(default: ${l.tcpPort})`)} -w, --ws-port <port> WebSocket port for browser connections ${d.dim(`(default: ${l.wsPort})`)} -s, --silent Suppress all output -h, --help Display help -v, --version Display version ${d.bold("Examples:")} # Start with default settings $ pg-proxy # Use custom ports $ pg-proxy --tcp-port 5433 --ws-port 8080 ${d.bold("Documentation:")} ${d.blue("https://github.com/f0rr0/pg-browser-proxy#readme")} ${d.bold("Environment Variables:")} ${d.dim("DEBUG")} Enable debug logs (e.g., DEBUG=pg-proxy:*) ${d.bold("Common PostgreSQL Client Commands:")} ${d.dim("psql")} psql -h localhost -p <tcp-port> ${d.dim("pg_dump")} pg_dump -h localhost -p <tcp-port> > backup.sql `)}},T=O;import u from"chalk";import D from"debug";D.formatters.e=e=>e();var g=D("pg-browser-proxy");import{createServer as V}from"node:net";import{BackendError as A}from"pg-gateway";import{fromNodeSocket as X}from"pg-gateway/node";var M=class{connection;websocket;connectionId;hasConnection(){return!!this.connection}getConnection(){return this.connection}getConnectionId(){return this.connectionId}setConnection(t,n){this.connectionId=t,this.connection=n}deleteConnection(){this.connection=void 0,this.connectionId=void 0}hasWebsocket(){return!!this.websocket}getWebsocket(){return this.websocket}setWebsocket(t){this.websocket=t}deleteWebsocket(){this.websocket=void 0,this.deleteConnection()}},p=new M;function G(e,t,n={}){let a=new TextEncoder,s=196608,r={user:e,database:t,...n},o=4;for(let[m,h]of Object.entries(r))o+=m.length+1+h.length+1;o+=1;let i=new Uint8Array(4+o),v=new DataView(i.buffer),c=0;v.setInt32(c,o+4,!1),c+=4,v.setInt32(c,s,!1),c+=4;for(let[m,h]of Object.entries(r))i.set(a.encode(m),c),c+=m.length,i.set([0],c++),i.set(a.encode(h),c),c+=h.length,i.set([0],c++);return i.set([0],c),i}function L(){let e=new Uint8Array(5),t=new DataView(e.buffer);return t.setUint8(0,88),t.setUint32(1,4,!1),e}import{customAlphabet as R}from"nanoid";var j=R("0123456789abcdefghijklmnopqrstuvwxyz",16);function F(){return j()}function N(e){let t=e.subarray(0,16),n=new TextDecoder().decode(t),a=e.subarray(16);return{connectionId:n,message:a}}function C(e,t){let a=new TextEncoder().encode(e),s=new Uint8Array(a.length+t.length);return s.set(a,0),s.set(t,a.length),s}var Q=g.extend("telemetry"),w=class{event_message;metadata;timestamp;constructor(t,n){this.event_message=t,this.metadata=n,this.timestamp=new Date().toISOString()}},S=class extends w{constructor(t){super("database-shared",t)}},k=class extends w{constructor(t){super("database-unshared",t)}},I=class extends w{constructor(t){super("user-connected",t)}},U=class extends w{constructor(t){super("user-disconnected",t)}};async function b(e){Q(JSON.stringify(e,null,2))}var E=g.extend("tcp-server"),x=V(async e=>{var t;try{E("New TCP connection");let n={ip:e.localAddress,port:((t=e.localPort)==null?void 0:t.toString())||""},a=await X(e,{serverVersion(){return"16.3"},auth:{method:"trust"},onStartup(s){let r=p.getWebsocket();if(!r)throw A.create({code:"XX000",message:"the browser is not sharing the database",severity:"FATAL"});if(p.hasConnection())throw A.create({code:"53300",message:"sorry, too many clients already",severity:"FATAL"});let o=F();p.setConnection(o,a),b(new I({connectionId:o,userMetadata:{...n,...s.clientParams}}));let i=G("postgres","postgres",{client_ip:e.localAddress??"unknown"});r.send(C(o,i))},onMessage(s,r){if(!r.isAuthenticated)return;let o=p.getWebsocket();if(!o)throw A.create({code:"XX000",message:"the browser is not sharing the database",severity:"FATAL"});let i=p.getConnectionId();if(!i)throw A.create({code:"08003",message:"connection does not exist",severity:"FATAL"});return E("tcp message: %e",()=>Buffer.from(s).toString("hex")),o.send(C(i,s)),new Uint8Array}});e.on("close",()=>{let s=p.getConnectionId();if(s){b(new U({connectionId:s,userMetadata:n}));let r=p.getWebsocket();r==null||r.send(C(s,L())),p.deleteConnection()}})}catch(n){E("Error in TCP connection:",n),e.end()}});import*as _ from"node:http";import{WebSocketServer as z}from"ws";var f=g.extend("websocket-server"),y=_.createServer(),$=new z({server:y});$.on("error",e=>{f("websocket server error",e)});function J(e){let t=e.headers;return{origin:t.origin,userAgent:t["user-agent"],host:t.host}}$.on("connection",async(e,t)=>{try{if(f("websocket connection"),!t.headers.host){f("No host header present"),e.close();return}if(p.hasWebsocket()){f("Database already shared"),e.close();return}p.setWebsocket(e);let n=J(t);f("User connected with metadata:",n),b(new S({userMetadata:n})),e.on("message",a=>{var o;let{message:s}=N(a),r=p.getConnection();r&&(f("websocket message: %e",()=>s.toString("hex")),(o=r.streamWriter)==null||o.write(s))}),e.on("close",()=>{p.deleteWebsocket(),b(new k({userMetadata:n}))})}catch(n){f("Error in WebSocket connection:",n),e.close()}});var P=g.extend("cli");function q(e){let t=W(e),n=!1;return{httpServer:y,tcpServer:x,start:async()=>{await Promise.all([new Promise((r,o)=>{y.listen(t.wsPort,()=>{r()}),y.on("error",o)}),new Promise((r,o)=>{x.listen(t.tcpPort,()=>{r()}),x.on("error",o)})])},shutdown:async r=>{if(!n){n=!0,P("Shutting down servers%s",r?` (signal: ${r})`:"");for(let o of $.clients)try{o.terminate()}catch(i){P("Error terminating WebSocket client:",i)}await Promise.allSettled([new Promise(o=>{y.close(()=>{P("WebSocket server closed"),o()})}),new Promise(o=>{x.close(()=>{P("TCP server closed"),o()})})]),P("Shutdown complete")}}}}var H={name:"pg-browser-proxy",description:"PostgreSQL proxy for browser native Potgres",run:async e=>{let{parameters:t,print:n}=e,a=t.options["tcp-port"]||t.options.t||l.tcpPort,s=t.options["ws-port"]||t.options.w||l.wsPort,r={tcpPort:Number(a),wsPort:Number(s)},o=n.spin("Starting proxy servers..."),i=q(r),v=["SIGTERM","SIGINT","SIGQUIT"];for(let c of v)process.on(c,async()=>{console.log();try{await i.shutdown(),o.succeed(u.yellow("Servers shut down successfully")),await new Promise(m=>setTimeout(m,100)),process.exit(0)}catch(m){o.fail(u.red("Error during shutdown:")),console.error(m),await new Promise(h=>setTimeout(h,100)),process.exit(1)}});process.on("uncaughtException",async c=>{o.fail("Uncaught exception:"),console.error(c),await i.shutdown(),o.fail(u.red("Error during shutdown:")),await new Promise(m=>setTimeout(m,100)),process.exit(1)}),process.on("unhandledRejection",async c=>{o.fail("Unhandled rejection:"),console.error(c),await i.shutdown(),o.fail(u.red("Error during shutdown:")),await new Promise(m=>setTimeout(m,100)),process.exit(1)});try{await i.start(),o.succeed("Proxy servers started successfully"),n.info(` ${u.bold("Server Information")} ${u.cyan("TCP Port:")} localhost:${r.tcpPort} ${u.cyan("WebSocket:")} ws://localhost:${r.wsPort} ${u.bold("Example Commands")} ${u.dim("psql")} psql -h localhost -p ${r.tcpPort} ${u.dim("pg_dump")} pg_dump -h localhost -p ${r.tcpPort} ${u.dim("Press Ctrl+C to stop")} `)}catch(c){o.fail("Failed to start servers:"),console.error(c),await i.shutdown(),o.fail(u.red("Error during shutdown:")),process.exit(1)}}},B=H;async function Y(){await K().brand("pg-browser-proxy").src(import.meta.dirname,{required:!1,preloadedCommands:[B,T]}).help({command:T}).version().checkForUpdates(20).create().run()}Y();export{Y as runCLI}; //# sourceMappingURL=bin.js.map