#![cfg(unix)] mod common; use std::io::{BufRead, BufReader, Write}; use std::os::unix::net::UnixStream; use std::process::{Command, Stdio}; #[test] fn unix_streams_need_only_write_authority_and_support_half_close() { let directory = tempfile::tempdir().unwrap(); let path = directory.path().join("stream.sock"); let label = serde_json::to_string(path.to_str().unwrap()).unwrap(); let output = common::run( &format!( r#" import {{listen,connect,pledge}} from 'avril:std'; pledge({{net:false,read:false,write:{{allow:[{label}]}}}}); const listener=listen({{transport:'unix',path:{label}}}),accept=listener.accept(),client=await connect({{transport:'unix',path:{label}}}),server=await accept; if(listener.addr.path!=={label}||client.remoteAddr.path!=={label}||client.localAddr.path!==null||server.remoteAddr.path!==null||server.localAddr.path!=={label})throw Error('addresses'); const reading=server.read(new Uint8Array(1));await client.write(new Uint8Array([7]));if(await reading!==1)throw Error('read'); await client.closeWrite();if(await server.read(new Uint8Array(1))!==null)throw Error('FIN'); await server.write(new Uint8Array([8]));const byte=new Uint8Array(1);if(await client.read(byte)!==1||byte[0]!==8)throw Error('half close'); await server.closeWrite();if(await client.read(byte)!==null)throw Error('EOF'); client.close();server.close();listener.close();console.log('unix'); "# ), &[], ); assert_eq!(output.stdout, b"unix\n"); assert!(!path.exists()); } #[test] fn unix_cleanup_preserves_replacements_and_existing_entries() { let directory = tempfile::tempdir().unwrap(); let path = directory.path().join("socket"); let label = serde_json::to_string(path.to_str().unwrap()).unwrap(); let (_source, mut child) = common::spawn(&format!( r#" import {{listen}} from 'avril:std'; const listener=listen({{transport:'unix',path:{label}}});console.log('ready');const connection=await listener.accept();await connection.read(new Uint8Array(1));listener.close();connection.close(); "# )); let mut stdout = BufReader::new(child.stdout.take().unwrap()); let mut line = String::new(); stdout.read_line(&mut line).unwrap(); assert_eq!(line, "ready\n"); child.stdout = Some(stdout.into_inner()); let mut socket = UnixStream::connect(&path).unwrap(); std::fs::remove_file(&path).unwrap(); std::fs::write(&path, "replacement").unwrap(); socket.write_all(b"x").unwrap(); let output = common::wait(child); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); assert_eq!(std::fs::read_to_string(&path).unwrap(), "replacement"); let output = common::run( &format!( r#" import {{listen,errors}} from 'avril:std'; try{{listen({{transport:'unix',path:{label}}});throw Error('existing entry replaced');}}catch(e){{if(e.message==='existing entry replaced')throw e;}} console.log('preserved'); "# ), &[], ); assert_eq!(output.stdout, b"preserved\n"); assert_eq!(std::fs::read_to_string(&path).unwrap(), "replacement"); } #[test] fn write_revocation_blocks_all_unix_io_but_not_cleanup() { let directory = tempfile::tempdir().unwrap(); let path = directory.path().join("revoke.sock"); let label = serde_json::to_string(path.to_str().unwrap()).unwrap(); let output = common::run( &format!( r#" import {{listen,connect,pledge,errors}} from 'avril:std'; const listener=listen({{transport:'unix',path:{label}}}),accept=listener.accept(),client=await connect({{transport:'unix',path:{label}}}),server=await accept; pledge({{write:false}}); for(const operation of [()=>client.read(new Uint8Array(1)),()=>client.write(new Uint8Array([1])),()=>client.closeWrite(),()=>listener.accept(),()=>connect({{transport:'unix',path:{label}}})]){{ try{{await operation();throw Error('authority escaped');}}catch(e){{if(!(e instanceof errors.PermissionDenied))throw e;}} }} client.close();server.close();listener.close();console.log('revoked'); "# ), &[], ); assert_eq!(output.stdout, b"revoked\n"); assert!(!path.exists()); } #[test] fn unix_connect_authorizes_the_resolved_symlink_target() { let directory = tempfile::tempdir().unwrap(); let path = directory.path().join("actual.sock"); let link = directory.path().join("alias.sock"); let label = serde_json::to_string(path.to_str().unwrap()).unwrap(); let alias = serde_json::to_string(link.to_str().unwrap()).unwrap(); let listener = std::os::unix::net::UnixListener::bind(&path).unwrap(); std::os::unix::fs::symlink(&path, &link).unwrap(); let output = common::run( &format!( r#" import {{connect,pledge,errors}} from 'avril:std'; pledge({{net:false,write:{{allow:[{alias}],deny:[{label}]}}}}); try{{await connect({{transport:'unix',path:{alias}}});throw Error('symlink escaped');}}catch(e){{if(!(e instanceof errors.PermissionDenied))throw e;}} console.log('denied'); "# ), &[], ); assert_eq!(output.stdout, b"denied\n"); drop(listener); } #[test] fn relative_unix_paths_resolve_against_the_working_directory() { let directory = tempfile::tempdir().unwrap(); let source = directory.path().join("main.js"); std::fs::write(&source,r#" import {listen,connect} from 'avril:std'; const listener=listen({transport:'unix',path:'relative.sock'}),accept=listener.accept(),client=await connect({transport:'unix',path:'relative.sock'}),server=await accept; if(!listener.addr.path.endsWith('/relative.sock'))throw Error('path');client.close();server.close();listener.close();console.log('relative'); "#).unwrap(); let child = Command::new(env!("CARGO_BIN_EXE_avril")) .args(["run", "main.js"]) .current_dir(directory.path()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() .unwrap(); let output = common::wait(child); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); assert_eq!(output.stdout, b"relative\n"); assert!(!directory.path().join("relative.sock").exists()); } #[test] fn closing_and_worker_termination_unlink_listeners_with_pending_accepts() { let directory = tempfile::tempdir().unwrap(); let path = directory.path().join("worker.sock"); let label = serde_json::to_string(path.to_str().unwrap()).unwrap(); let worker = directory.path().join("worker.js"); std::fs::write(&worker,format!("import {{listen}} from 'avril:std';const listener=listen({{transport:'unix',path:{label}}});postMessage('ready');await listener.accept();")).unwrap(); let url = url::Url::from_file_path(&worker).unwrap(); let output = common::run( &format!( r#" import {{listen,errors}} from 'avril:std'; const worker=new Worker('{url}',{{type:'module'}});await new Promise(resolve=>worker.onmessage=resolve);worker.terminate(); let listener;for(let i=0;i<100&&!listener;i++){{try{{listener=listen({{transport:'unix',path:{label}}});}}catch{{await new Promise(resolve=>setTimeout(resolve,5));}}}} if(!listener)throw Error('worker leaked socket');const accept=listener.accept().catch(e=>e);listener.close();if(!((await accept) instanceof errors.BadResource))throw Error('pending accept');console.log('terminated'); "# ), &[], ); assert_eq!(output.stdout, b"terminated\n"); assert!(!path.exists()); } #[test] fn unix_socket_options_reject_ambiguous_transports() { let output = common::run( r#" import {listen,connect} from 'avril:std'; for(const options of [{transport:'unix',path:'',port:0},{transport:'unix',path:'x',hostname:'localhost'},{transport:'unix'},{path:'x',port:0}]){ try{listen(options);throw Error('invalid Unix options');}catch(e){if(!(e instanceof TypeError))throw e;} } console.log('validated'); "#, &[], ); assert_eq!(output.stdout, b"validated\n"); } #[test] fn unix_writev_writes_ordered_vectors_and_validates() { let directory = tempfile::tempdir().unwrap(); let path = directory.path().join("vector.sock"); let label = serde_json::to_string(path.to_str().unwrap()).unwrap(); let output = common::run( &format!( r#" import {{listen,connect,errors}} from 'avril:std'; const listener=listen({{transport:'unix',path:{label}}}),accept=listener.accept(); const client=await connect({{transport:'unix',path:{label}}}),server=await accept; const full=new Uint8Array([5,6,7,8]); if(await client.writev([full.subarray(0,2),new Uint8Array(0),full.subarray(2)])!==4)throw Error('Count'); if(await client.writev([])!==0)throw Error('Empty'); await client.write(new Uint8Array([9])); await client.closeWrite(); const buf=new Uint8Array(5);let offset=0,count; while(offset<5&&(count=await server.read(buf.subarray(offset)))!==null)offset+=count; if(offset!==5||buf.join()!=='5,6,7,8,9')throw Error('Order '+buf.join()); try{{await client.writev([new Uint8Array([1])]);throw Error('FIN');}}catch(e){{if(!(e instanceof errors.BadResource))throw e;}} client.close();server.close(); const accept2=listener.accept(),client2=await connect({{transport:'unix',path:{label}}}),server2=await accept2; const wide=[];for(let i=0;i<20;i++){{const view=new Uint8Array(1000);view.fill(i+1);wide.push(view);}} const gathering=(async()=>{{ const rest=new Uint8Array(20000);let at=0,got; while(at<20000&&(got=await server2.read(rest.subarray(at)))!==null)at+=got; if(at!==20000)throw Error('Wide length'); return rest; }})(); if(await client2.writev(wide)!==20000)throw Error('Wide'); await client2.closeWrite(); const rest=await gathering; for(let i=0;i<20;i++)for(let j=0;j<1000;j++)if(rest[i*1000+j]!==i+1)throw Error('Wide order '+i); for(const bad of ['bytes',new Uint8Array([1]),[new Uint8Array([1]),'x']]){{ try{{await server2.writev(bad);throw Error('Accepted');}}catch(e){{if(!(e instanceof TypeError))throw e;}} }} client2.close();server2.close(); listener.close();console.log('unix-vectors'); "# ), &[], ); assert_eq!(output.stdout, b"unix-vectors\n"); assert!(!path.exists()); }