@@ -34,7 +34,13 @@ function listen(server, host = '127.0.0.1') {
3434}
3535
3636function closeServer ( server ) {
37- return new Promise ( resolve => server . close ( ( ) => resolve ( ) ) ) ;
37+ return new Promise ( resolve => {
38+ let settled = false ;
39+ const done = ( ) => { if ( ! settled ) { settled = true ; resolve ( ) ; } } ;
40+ server . close ( done ) ;
41+ if ( typeof server . closeAllConnections === 'function' ) server . closeAllConnections ( ) ;
42+ setTimeout ( done , 1000 ) ;
43+ } ) ;
3844}
3945
4046async function waitForHTTP ( url , timeoutMs = 15000 ) {
@@ -96,7 +102,7 @@ class SocketReader {
96102
97103function createTargetServer ( requests ) {
98104 const server = http . createServer ( ( req , res ) => {
99- requests . push ( { url : req . url , method : req . method , host : req . headers . host || '' , userAgent : req . headers [ 'user-agent' ] || '' , cookie : req . headers . cookie || '' } ) ;
105+ requests . push ( { url : req . url , method : req . method , host : req . headers . host || '' , userAgent : req . headers [ 'user-agent' ] || '' , cookie : req . headers . cookie || '' , contentType : req . headers [ 'content-type' ] || '' } ) ;
100106 const url = new URL ( req . url , 'http://target.local' ) ;
101107 if ( url . pathname === '/' ) {
102108 res . writeHead ( 200 , { 'Content-Type' : 'text/html; charset=utf-8' } ) ;
@@ -224,6 +230,23 @@ function createTargetServer(requests) {
224230 res . end ( 'data: sse-ok\n\n' ) ;
225231 return ;
226232 }
233+ if ( url . pathname === '/form-echo' ) {
234+ const chunks = [ ] ;
235+ req . on ( 'data' , chunk => chunks . push ( chunk ) ) ;
236+ req . on ( 'end' , ( ) => {
237+ const body = Buffer . concat ( chunks ) . toString ( 'utf8' ) ;
238+ const kind = url . searchParams . get ( 'kind' ) || '' ;
239+ res . writeHead ( 200 , { 'Content-Type' : 'text/html; charset=utf-8' , 'Cache-Control' : 'no-store' } ) ;
240+ res . end ( `<!doctype html><html><head><title>Form Echo ${ kind } </title></head><body>
241+ <main id="form-result" data-method="${ req . method } " data-kind="${ kind } " data-content-type="${ req . headers [ 'content-type' ] || '' } ">
242+ <pre id="form-body">${ body . replace ( / [ & < > ] / g, ch => ( { '&' :'&' , '<' :'<' , '>' :'>' } [ ch ] ) ) } </pre>
243+ <a id="next" href="/next">Next page</a>
244+ </main>
245+ <script>window.__formEcho=${ JSON . stringify ( { kind, method : req . method , contentType : req . headers [ 'content-type' ] || '' , body } ) } ;</script>
246+ </body></html>` ) ;
247+ } ) ;
248+ return ;
249+ }
227250 if ( url . pathname === '/post-echo' ) {
228251 const chunks = [ ] ;
229252 req . on ( 'data' , chunk => chunks . push ( chunk ) ) ;
@@ -434,7 +457,7 @@ test('browser traffic uses internal SOCKS5 mode and covers proxied runtime integ
434457 phase2EvalLocation : window . __phase2EvalLocation ,
435458 } ) ) ;
436459 assert . equal ( home . title , 'E2E Home' ) ;
437- assert . equal ( home . hash , '' ) ;
460+ assert . match ( home . hash , / ^ # k = / ) ;
438461 assert . equal ( home . shellVisible , false ) ;
439462 assert . equal ( home . userAgent , TARGET_UA ) ;
440463 assert . equal ( home . appVersion , TARGET_UA . replace ( / ^ M o z i l l a \/ / , '' ) ) ;
@@ -801,23 +824,84 @@ test('browser traffic uses internal SOCKS5 mode and covers proxied runtime integ
801824 assert . equal ( requests . filter ( r => r . userAgent && r . userAgent !== TARGET_UA ) . length , 0 , `target requests: ${ JSON . stringify ( requests ) } ` ) ;
802825 assert . ok ( requests . some ( r => r . url . startsWith ( '/direct-fetch' ) && r . userAgent === TARGET_UA ) , `target requests: ${ JSON . stringify ( requests ) } ` ) ;
803826
804- const forgedMessage = await page . evaluate ( ( ) => new Promise ( resolve => {
805- const channel = new MessageChannel ( ) ;
806- const timer = setTimeout ( ( ) => resolve ( { timeout : true } ) , 3000 ) ;
807- channel . port1 . onmessage = ev => {
808- clearTimeout ( timer ) ;
809- resolve ( ev . data || null ) ;
827+ const serviceWorkerPolicy = await page . evaluate ( async ( ) => {
828+ const out = {
829+ exposed : 'serviceWorker' in navigator ,
830+ controller : navigator . serviceWorker && navigator . serviceWorker . controller ,
831+ registrationCount : null ,
832+ registerError : '' ,
810833 } ;
811- navigator . serviceWorker . controller . postMessage ( { type : 'ZP_RESOLVE_ENTRY' , path : location . pathname } , [ channel . port2 ] ) ;
812- } ) ) ;
813- assert . deepEqual ( forgedMessage , { ok : false , error : 'POLICY_BLOCKED' } ) ;
834+ if ( navigator . serviceWorker && navigator . serviceWorker . getRegistrations ) out . registrationCount = ( await navigator . serviceWorker . getRegistrations ( ) ) . length ;
835+ try { await navigator . serviceWorker . register ( '/target-sw.js' ) ; }
836+ catch ( err ) { out . registerError = err && err . name || String ( err ) ; }
837+ return out ;
838+ } ) ;
839+ assert . equal ( serviceWorkerPolicy . exposed , true ) ;
840+ assert . equal ( serviceWorkerPolicy . controller , null ) ;
841+ assert . equal ( serviceWorkerPolicy . registrationCount , 0 ) ;
842+ assert . equal ( serviceWorkerPolicy . registerError , 'NotSupportedError' ) ;
814843 const bootLeak = await page . evaluate ( ( ) => ( {
815844 bootType : typeof window . __ZP_BOOT ,
816845 scriptContainsRuntimeToken : Array . from ( document . scripts ) . some ( s => s . textContent . includes ( 'runtimeToken' ) ) ,
817846 } ) ) ;
818847 assert . equal ( bootLeak . bootType , 'undefined' ) ;
819848 assert . equal ( bootLeak . scriptContainsRuntimeToken , false ) ;
820849
850+
851+ async function submitFormFixture ( kind ) {
852+ await page . evaluate ( kind => {
853+ const f = document . createElement ( 'form' ) ;
854+ f . method = 'POST' ;
855+ f . enctype = kind === 'multipart' ? 'multipart/form-data' : kind === 'plain' ? 'text/plain' : 'application/x-www-form-urlencoded' ;
856+ f . action = '/form-echo?kind=wrong' ;
857+ const input = document . createElement ( 'input' ) ;
858+ input . name = 'alpha' ;
859+ input . value = 'one' ;
860+ f . appendChild ( input ) ;
861+ if ( kind === 'multipart' ) {
862+ const file = document . createElement ( 'input' ) ;
863+ file . type = 'file' ;
864+ file . name = 'upload' ;
865+ const dt = new DataTransfer ( ) ;
866+ dt . items . add ( new File ( [ 'file-body' ] , 'hello.txt' , { type : 'text/plain' } ) ) ;
867+ file . files = dt . files ;
868+ f . appendChild ( file ) ;
869+ }
870+ const button = document . createElement ( 'button' ) ;
871+ button . type = 'submit' ;
872+ button . name = 'submitter' ;
873+ button . value = kind ;
874+ button . setAttribute ( 'formaction' , '/form-echo?kind=' + kind ) ;
875+ f . appendChild ( button ) ;
876+ document . body . appendChild ( f ) ;
877+ f . requestSubmit ( button ) ;
878+ } , kind ) ;
879+ await page . waitForFunction ( k => window . __formEcho && window . __formEcho . kind === k , { timeout : 30000 } , kind ) ;
880+ return page . evaluate ( ( ) => { const loc = __zp_get ( globalThis , 'location' ) ; return { echo : window . __formEcho , virtualHref : loc . href , virtualHash : loc . hash , documentURL : __zp_get ( document , 'URL' ) , baseURI : __zp_get ( document , 'baseURI' ) } ; } ) ;
881+ }
882+ const urlencodedForm = await submitFormFixture ( 'urlencoded' ) ;
883+ assert . equal ( urlencodedForm . echo . method , 'POST' ) ;
884+ assert . match ( urlencodedForm . echo . contentType , / ^ a p p l i c a t i o n \/ x - w w w - f o r m - u r l e n c o d e d / ) ;
885+ assert . equal ( urlencodedForm . echo . body , 'alpha=one&submitter=urlencoded' ) ;
886+ const plainForm = await submitFormFixture ( 'plain' ) ;
887+ assert . match ( plainForm . echo . contentType , / ^ t e x t \/ p l a i n / ) ;
888+ assert . match ( plainForm . echo . body , / a l p h a = o n e / ) ;
889+ assert . match ( plainForm . echo . body , / s u b m i t t e r = p l a i n / ) ;
890+ const multipartForm = await submitFormFixture ( 'multipart' ) ;
891+ assert . match ( multipartForm . echo . contentType , / ^ m u l t i p a r t \/ f o r m - d a t a ; b o u n d a r y = / ) ;
892+ assert . match ( multipartForm . echo . body , / n a m e = " u p l o a d " ; f i l e n a m e = " h e l l o .t x t " / ) ;
893+ assert . match ( multipartForm . echo . body , / f i l e - b o d y / ) ;
894+ const rawAfterSubmit = page . url ( ) ;
895+ const rawKey = new URL ( rawAfterSubmit ) . hash ? new URLSearchParams ( new URL ( rawAfterSubmit ) . hash . slice ( 1 ) ) . get ( 'k' ) : '' ;
896+ assert . match ( rawAfterSubmit , / # k = / ) ;
897+ assert . match ( rawAfterSubmit , / \? z p _ s u b m i t = / ) ;
898+ for ( const surface of [ multipartForm . virtualHref , multipartForm . virtualHash , multipartForm . documentURL , multipartForm . baseURI ] ) {
899+ assert . equal ( surface . includes ( 'zp_submit=' ) , false , surface ) ;
900+ if ( rawKey ) assert . equal ( surface . includes ( rawKey ) , false , surface ) ;
901+ }
902+ assert . ok ( requests . some ( r => r . url . startsWith ( '/form-echo?kind=urlencoded' ) && r . contentType . startsWith ( 'application/x-www-form-urlencoded' ) ) , `target requests: ${ JSON . stringify ( requests ) } ` ) ;
903+ assert . ok ( requests . some ( r => r . url . startsWith ( '/form-echo?kind=plain' ) && r . contentType . startsWith ( 'text/plain' ) ) , `target requests: ${ JSON . stringify ( requests ) } ` ) ;
904+ assert . ok ( requests . some ( r => r . url . startsWith ( '/form-echo?kind=multipart' ) && r . contentType . startsWith ( 'multipart/form-data' ) ) , `target requests: ${ JSON . stringify ( requests ) } ` ) ;
821905 await page . click ( '#next' ) ;
822906 await page . waitForFunction ( ( ) => document . title === 'E2E Next' , { timeout : 30000 } ) ;
823907 const next = await page . evaluate ( ( ) => ( {
@@ -828,7 +912,7 @@ test('browser traffic uses internal SOCKS5 mode and covers proxied runtime integ
828912 userAgent : navigator . userAgent ,
829913 } ) ) ;
830914 assert . equal ( next . title , 'E2E Next' ) ;
831- assert . equal ( next . hash , '' ) ;
915+ assert . match ( next . hash , / ^ # k = / ) ;
832916 assert . equal ( next . shellVisible , false ) ;
833917 assert . equal ( next . userAgent , TARGET_UA ) ;
834918 assert . match ( next . href , new RegExp ( `^http://proxy\\.localhost:${ proxyPort } /p/` ) ) ;
0 commit comments