11import { createServer as createHttpServer , request as httpRequest , type IncomingHttpHeaders , type IncomingMessage , type ServerResponse } from "node:http"
22import { createServer as createNetServer } from "node:net"
3+ import { StringDecoder } from "node:string_decoder"
4+ import { Transform } from "node:stream"
35import type { PreviewLease } from "@automattic/wp-codebox-core"
46
57export interface PlaygroundServerRunResponse {
@@ -81,13 +83,19 @@ export async function withPreviewProxy(server: PlaygroundCliServer, port: number
8183async function startPreviewProxy ( targetUrl : string , port : number , bind : string ) : Promise < PlaygroundPreviewProxy > {
8284 const target = new URL ( targetUrl )
8385 const routes = createPreviewRouteRegistry ( )
84- const proxy = previewProxyServer ( target , routes )
86+ let proxyOrigin : string | undefined
87+ const proxy = previewProxyServer ( target , routes , ( ) => proxyOrigin )
8588 const servers = [ proxy ]
8689
8790 await listenPreviewProxy ( proxy , port , bind )
8891
92+ const address = proxy . address ( )
93+ const resolvedPort = address && typeof address === "object" ? address . port : port
94+ const reportedHost = bind === "0.0.0.0" ? "127.0.0.1" : bind
95+ proxyOrigin = `http://${ formatPreviewHost ( reportedHost ) } :${ resolvedPort } `
96+
8997 if ( bind === "127.0.0.1" ) {
90- const ipv6Proxy = previewProxyServer ( target , routes )
98+ const ipv6Proxy = previewProxyServer ( target , routes , ( ) => `http://[::1]: ${ resolvedPort } ` )
9199 try {
92100 await listenPreviewProxy ( ipv6Proxy , port , "::1" )
93101 servers . push ( ipv6Proxy )
@@ -99,12 +107,8 @@ async function startPreviewProxy(targetUrl: string, port: number, bind: string):
99107 }
100108 }
101109
102- const address = proxy . address ( )
103- const resolvedPort = address && typeof address === "object" ? address . port : port
104- const reportedHost = bind === "0.0.0.0" ? "127.0.0.1" : bind
105-
106110 return {
107- serverUrl : `http:// ${ formatPreviewHost ( reportedHost ) } : ${ resolvedPort } ` ,
111+ serverUrl : proxyOrigin ,
108112 previewRoutes : routes ,
109113 diagnostics : {
110114 schema : "wp-codebox/preview-proxy-diagnostics/v1" ,
@@ -120,7 +124,7 @@ async function startPreviewProxy(targetUrl: string, port: number, bind: string):
120124 }
121125}
122126
123- function previewProxyServer ( target : URL , routes : InternalPreviewRouteRegistry ) : PreviewProxyServer {
127+ function previewProxyServer ( target : URL , routes : InternalPreviewRouteRegistry , proxyOrigin : ( ) => string | undefined ) : PreviewProxyServer {
124128 const upstreamQueue = createPreviewProxyQueue ( )
125129
126130 return createHttpServer ( async ( incoming , outgoing ) => {
@@ -133,7 +137,7 @@ function previewProxyServer(target: URL, routes: InternalPreviewRouteRegistry):
133137 return
134138 }
135139
136- upstreamQueue ( ( ) => proxyPreviewRequest ( target , incoming , outgoing ) ) . catch ( ( error : Error ) => writeProxyError ( outgoing , error ) )
140+ upstreamQueue ( ( ) => proxyPreviewRequest ( target , proxyOrigin ( ) , incoming , outgoing ) ) . catch ( ( error : Error ) => writeProxyError ( outgoing , error ) )
137141 } )
138142}
139143
@@ -159,7 +163,7 @@ function createPreviewRouteRegistry(): InternalPreviewRouteRegistry {
159163 }
160164}
161165
162- function proxyPreviewRequest ( target : URL , incoming : IncomingMessage , outgoing : ServerResponse ) : Promise < void > {
166+ function proxyPreviewRequest ( target : URL , proxyOrigin : string | undefined , incoming : IncomingMessage , outgoing : ServerResponse ) : Promise < void > {
163167 return new Promise ( ( resolve ) => {
164168 let settled = false
165169 let targetResponse : IncomingMessage | undefined
@@ -187,7 +191,27 @@ function proxyPreviewRequest(target: URL, incoming: IncomingMessage, outgoing: S
187191 } ,
188192 ( response ) => {
189193 targetResponse = response
190- outgoing . writeHead ( response . statusCode ?? 502 , response . statusMessage , proxyResponseHeaders ( response . headers ) )
194+ if ( proxyOrigin && shouldRewriteProxyResponse ( response . headers ) ) {
195+ const headers = proxyResponseHeaders ( response . headers , target . origin , proxyOrigin )
196+ delete headers [ "content-length" ]
197+ delete headers . etag
198+ outgoing . writeHead ( response . statusCode ?? 502 , response . statusMessage , headers )
199+ const rewrite = previewOriginRewriteStream ( target . origin , proxyOrigin )
200+ response . on ( "error" , ( error ) => {
201+ outgoing . destroy ( error )
202+ settle ( )
203+ } )
204+ rewrite . on ( "error" , ( error ) => {
205+ outgoing . destroy ( error )
206+ settle ( )
207+ } )
208+ outgoing . on ( "finish" , settle )
209+ outgoing . on ( "close" , settle )
210+ response . pipe ( rewrite ) . pipe ( outgoing )
211+ return
212+ }
213+
214+ outgoing . writeHead ( response . statusCode ?? 502 , response . statusMessage , proxyResponseHeaders ( response . headers , target . origin , proxyOrigin ) )
191215 response . on ( "error" , ( error ) => {
192216 outgoing . destroy ( error )
193217 settle ( )
@@ -211,6 +235,43 @@ function proxyPreviewRequest(target: URL, incoming: IncomingMessage, outgoing: S
211235 } )
212236}
213237
238+ function shouldRewriteProxyResponse ( headers : IncomingHttpHeaders ) : boolean {
239+ const contentType = String ( headers [ "content-type" ] ?? "" ) . toLowerCase ( )
240+ const contentEncoding = String ( headers [ "content-encoding" ] ?? "identity" ) . toLowerCase ( )
241+ return ( contentEncoding === "" || contentEncoding === "identity" )
242+ && ( contentType . startsWith ( "text/" ) || / (?: ^ | \/ ) (?: [ ^ ; ] + \+ ) ? (?: j s o n | j a v a s c r i p t | x m l ) (?: ; | $ ) / . test ( contentType ) )
243+ }
244+
245+ function rewritePreviewOrigin ( body : string , targetOrigin : string , proxyOrigin : string ) : string {
246+ return body
247+ . replaceAll ( targetOrigin , proxyOrigin )
248+ . replaceAll ( targetOrigin . replaceAll ( "/" , "\\/" ) , proxyOrigin . replaceAll ( "/" , "\\/" ) )
249+ }
250+
251+ function previewOriginRewriteStream ( targetOrigin : string , proxyOrigin : string ) : Transform {
252+ const decoder = new StringDecoder ( "utf8" )
253+ const patterns = [ targetOrigin , targetOrigin . replaceAll ( "/" , "\\/" ) ]
254+ const retainedCharacters = Math . max ( ...patterns . map ( ( pattern ) => pattern . length ) ) - 1
255+ let pending = ""
256+
257+ return new Transform ( {
258+ transform ( chunk : Buffer , _encoding , callback ) {
259+ const text = pending + decoder . write ( chunk )
260+ let splitAt = Math . max ( 0 , text . length - retainedCharacters )
261+ for ( const pattern of patterns ) {
262+ for ( let index = Math . max ( 0 , splitAt - pattern . length + 1 ) ; index < splitAt ; index += 1 ) {
263+ if ( pattern . startsWith ( text . slice ( index , splitAt ) ) ) splitAt = index
264+ }
265+ }
266+ pending = text . slice ( splitAt )
267+ callback ( null , rewritePreviewOrigin ( text . slice ( 0 , splitAt ) , targetOrigin , proxyOrigin ) )
268+ } ,
269+ flush ( callback ) {
270+ callback ( null , rewritePreviewOrigin ( pending + decoder . end ( ) , targetOrigin , proxyOrigin ) )
271+ } ,
272+ } )
273+ }
274+
214275function createPreviewProxyQueue ( ) : ( task : ( ) => Promise < void > ) => Promise < void > {
215276 let active = false
216277 const pending : Array < ( ) => void > = [ ]
@@ -277,11 +338,18 @@ function proxyRequestHeaders(headers: IncomingHttpHeaders): IncomingHttpHeaders
277338 }
278339}
279340
280- function proxyResponseHeaders ( headers : IncomingHttpHeaders ) : IncomingHttpHeaders {
341+ function proxyResponseHeaders ( headers : IncomingHttpHeaders , targetOrigin ?: string , proxyOrigin ?: string ) : IncomingHttpHeaders {
281342 const forwarded = { ...headers }
282343 delete forwarded . connection
283344 delete forwarded [ "transfer-encoding" ]
284345
346+ if ( targetOrigin && proxyOrigin ) {
347+ for ( const [ name , value ] of Object . entries ( forwarded ) ) {
348+ if ( typeof value === "string" ) forwarded [ name ] = rewritePreviewOrigin ( value , targetOrigin , proxyOrigin )
349+ else if ( Array . isArray ( value ) ) forwarded [ name ] = value . map ( ( item ) => rewritePreviewOrigin ( item , targetOrigin , proxyOrigin ) )
350+ }
351+ }
352+
285353 return forwarded
286354}
287355
0 commit comments