11import type { ShipnodeConfig } from '../shared/types.js' ;
22import type { RemoteExecutor } from '../domain/remote/executor.js' ;
33import { CloudflareApi } from '../infrastructure/cloudflare/api.js' ;
4- import { getWebApp } from '../domain/pm2/apps .js' ;
4+ import { Tunnel } from '../domain/cloudflare/tunnel .js' ;
55
66export class CloudflareOrchestrator {
77 private api : CloudflareApi ;
@@ -19,21 +19,27 @@ export class CloudflareOrchestrator {
1919 if ( ! cf ) throw new Error ( 'No cloudflare config found.' ) ;
2020
2121 await this . ensureCloudflaredInstalled ( ) ;
22-
2322 const tunnelName = cf . tunnelName ?? `shipnode-${ this . config . ssh . host . replace ( / \. / g, '-' ) } ` ;
2423
2524 await this . executor . exec ( `cloudflared tunnel create "${ tunnelName } " 2>&1` ) ;
2625 const tunnelId = await this . resolveTunnelId ( ) ;
2726
28- if ( cf . appHostname ) {
29- await this . executor . exec ( `cloudflared tunnel route dns "${ tunnelName } " "${ cf . appHostname } "` ) ;
27+ const tunnel = new Tunnel ( tunnelName , tunnelId , `/root/.cloudflared/${ tunnelId } .json` ) ;
28+
29+ for ( const app of this . config . apps ) {
30+ const webApp = app . pm2 ?. apps . find ( ( a ) => a . port !== undefined ) ;
31+ if ( app . domain && webApp ) {
32+ tunnel . addIngress ( app . domain , `http://localhost:${ webApp . port } ` ) ;
33+ await this . executor . exec ( `cloudflared tunnel route dns "${ tunnelName } " "${ app . domain } "` ) ;
34+ }
3035 }
3136
3237 if ( cf . sshHostname ) {
38+ tunnel . addIngress ( cf . sshHostname , 'ssh://localhost:22' ) ;
3339 await this . executor . exec ( `cloudflared tunnel route dns "${ tunnelName } " "${ cf . sshHostname } "` ) ;
3440 }
3541
36- await this . writeTunnelConfig ( tunnelId , cf ) ;
42+ await this . writeConfig ( tunnel ) ;
3743 await this . installAndStartService ( ) ;
3844
3945 if ( cf . lockdownFirewall ) {
@@ -45,18 +51,19 @@ export class CloudflareOrchestrator {
4551 }
4652 }
4753
48- async audit ( ) : Promise < { zone : { name : string ; id : string ; status : string } ; dns : string ; tunnelList : string ; service : string } > {
54+ async audit ( ) : Promise < { zone : { name : string ; id : string ; status : string } ; apps : { domain : string ; port : number | undefined } [ ] ; tunnelList : string ; service : string } > {
4955 const cf = this . config . cloudflare ;
5056 if ( ! cf ) throw new Error ( 'No cloudflare config found.' ) ;
5157
5258 const zoneId = await this . api . getZoneId ( cf . zone ) ;
5359 const zoneInfo = await this . api . fetch < { id : string ; name : string ; status : string } > ( `/zones/${ zoneId } ` ) ;
5460
55- let dns = '' ;
56- if ( cf . appHostname ) {
57- const records = await this . api . getDnsRecords ( zoneId , cf . appHostname ) ;
58- dns = records . length ? `${ records [ 0 ] . type } → ${ records [ 0 ] . content } ` : 'NOT FOUND' ;
59- }
61+ const apps = this . config . apps
62+ . map ( ( a ) => {
63+ const web = a . pm2 ?. apps . find ( ( p ) => p . port !== undefined ) ;
64+ return web && a . domain ? { domain : a . domain , port : web . port } : null ;
65+ } )
66+ . filter ( ( x ) : x is NonNullable < typeof x > => x !== null ) ;
6067
6168 const tunnelResult = await this . executor . exec ( 'cloudflared tunnel list 2>/dev/null || echo MISSING' ) ;
6269 const tunnelList = tunnelResult . stdout . includes ( 'MISSING' ) ? 'not installed' : tunnelResult . stdout . trim ( ) ;
@@ -65,7 +72,7 @@ export class CloudflareOrchestrator {
6572
6673 return {
6774 zone : { name : zoneInfo . name , id : zoneInfo . id , status : zoneInfo . status } ,
68- dns ,
75+ apps ,
6976 tunnelList,
7077 service : svcResult . stdout . trim ( ) ,
7178 } ;
@@ -100,28 +107,9 @@ export class CloudflareOrchestrator {
100107 return tunnelId ;
101108 }
102109
103- private async writeTunnelConfig ( tunnelId : string , cf : NonNullable < ShipnodeConfig [ 'cloudflare' ] > ) : Promise < void > {
104- const webApp = getWebApp ( this . config ) ;
105- if ( cf . appHostname && ! webApp ) {
106- throw new Error ( 'cloudflare.appHostname is set but no pm2.apps entry declares a port — nothing to route HTTP to.' ) ;
107- }
108- const appIngress = cf . appHostname && webApp
109- ? ` - hostname: ${ cf . appHostname } \n service: http://localhost:${ webApp . port } `
110- : '' ;
111- const sshIngress = cf . sshHostname
112- ? ` - hostname: ${ cf . sshHostname } \n service: ssh://localhost:22`
113- : '' ;
114-
115- const cfConfig = `tunnel: ${ tunnelId }
116- credentials-file: /root/.cloudflared/${ tunnelId } .json
117-
118- ingress:
119- ${ appIngress }
120- ${ sshIngress }
121- - service: http_status:404
122- ` ;
123-
124- const b64 = Buffer . from ( cfConfig ) . toString ( 'base64' ) ;
110+ private async writeConfig ( tunnel : Tunnel ) : Promise < void > {
111+ const yaml = tunnel . toYaml ( ) ;
112+ const b64 = Buffer . from ( yaml ) . toString ( 'base64' ) ;
125113 await this . executor . exec (
126114 `SUDO=""; [ "$EUID" -ne 0 ] && SUDO="sudo"; ` +
127115 `mkdir -p /etc/cloudflared && ` +
@@ -137,19 +125,25 @@ ${sshIngress}
137125 }
138126
139127 private async lockdownFirewall ( ) : Promise < void > {
140- const webApp = getWebApp ( this . config ) ;
141- if ( ! webApp ) return ;
128+ const webPorts = this . config . apps
129+ . flatMap ( ( a ) => a . pm2 ?. apps ?? [ ] )
130+ . filter ( ( a ) => a . port !== undefined )
131+ . map ( ( a ) => a . port ! ) ;
132+
133+ if ( ! webPorts . length ) return ;
142134
143135 const cfIps = await this . api . getCloudflareIps ( ) ;
144- for ( const cidr of cfIps . ipv4_cidrs ?? [ ] ) {
136+ for ( const port of webPorts ) {
137+ for ( const cidr of cfIps . ipv4_cidrs ?? [ ] ) {
138+ await this . executor . exec (
139+ `SUDO=""; [ "$EUID" -ne 0 ] && SUDO="sudo"; ` +
140+ `$SUDO ufw allow from ${ cidr } to any port ${ port } 2>/dev/null || true` ,
141+ ) ;
142+ }
145143 await this . executor . exec (
146144 `SUDO=""; [ "$EUID" -ne 0 ] && SUDO="sudo"; ` +
147- `$SUDO ufw allow from ${ cidr } to any port ${ webApp . port } 2>/dev/null || true` ,
145+ `$SUDO ufw deny ${ port } 2>/dev/null || true` ,
148146 ) ;
149147 }
150- await this . executor . exec (
151- `SUDO=""; [ "$EUID" -ne 0 ] && SUDO="sudo"; ` +
152- `$SUDO ufw deny ${ webApp . port } 2>/dev/null || true` ,
153- ) ;
154148 }
155149}
0 commit comments