22
33use std:: {
44 fs:: File ,
5- io:: { Read , Write } ,
5+ io:: { ErrorKind , Read , Write } ,
66 net:: { Ipv4Addr , TcpListener } ,
7- os:: fd:: { FromRawFd , RawFd } ,
7+ os:: {
8+ fd:: { FromRawFd , RawFd } ,
9+ unix:: net:: UnixListener ,
10+ } ,
811 str:: FromStr ,
912} ;
1013
1114use anyhow:: { anyhow, Context } ;
12- use clap:: Parser ;
1315
1416#[ link( name = "krun-efi" ) ]
1517extern "C" {
@@ -22,51 +24,84 @@ const HTTP_RUNNING: &str =
2224const HTTP_STOPPING : & str =
2325 "HTTP/1.1 200 OK\r \n Content-type: application/json\r \n \r \n {\" state\" : \" VirtualMachineStateStopping\" }\0 " ;
2426
25- /// Socket address in which the restful URI socket should listen on. Identical to Rust's
26- /// SocketAddrV4, but requires a modified FromStr implementation due to how the address is
27- /// presented on the command line.
28- #[ derive( Clone , Debug , Parser ) ]
29- pub struct RestfulUriAddr {
30- pub ip_addr : Ipv4Addr ,
31- pub port : u16 ,
27+ #[ derive( Clone , Copy , Debug , Default , PartialEq , Eq ) ]
28+ pub enum UriScheme {
29+ #[ default]
30+ Tcp ,
31+ Unix ,
32+ None ,
3233}
3334
34- impl FromStr for RestfulUriAddr {
35+ impl FromStr for UriScheme {
3536 type Err = anyhow:: Error ;
3637
3738 fn from_str ( s : & str ) -> Result < Self , Self :: Err > {
38- let mut string = String :: from ( s) ;
39-
40- if let Some ( removed) = string. strip_prefix ( "tcp://" ) {
41- string = String :: from ( removed) ;
39+ match s {
40+ "tcp" => Ok ( Self :: Tcp ) ,
41+ "unix" => Ok ( Self :: Unix ) ,
42+ "none" => Ok ( Self :: None ) ,
43+ _ => Err ( anyhow ! ( "invalid scheme" ) ) ,
4244 }
45+ }
46+ }
4347
44- let mut parts: Vec < String > = string. split ( ':' ) . map ( |s| s. to_string ( ) ) . collect ( ) ;
45- if parts. len ( ) != 2 {
46- return Err ( anyhow ! ( "restful URI formatted incorrectly" ) ) ;
47- }
48+ /// Socket address in which the restful URI socket should listen on. Identical to Rust's
49+ /// SocketAddrV4, but requires a modified FromStr implementation due to how the address is
50+ /// presented on the command line.
51+ #[ derive( Clone , Debug , PartialEq ) ]
52+ pub enum RestfulUri {
53+ Tcp ( Ipv4Addr , u16 ) ,
54+ Unix ( String ) ,
55+ None ,
56+ }
4857
49- // Ipv4Address's FromStr does not understand that the "localhost" IP address translates to
50- // 127.0.0.1, this must be manually translated.
51- if & parts[ 0 ] [ ..] == "localhost" {
52- parts[ 0 ] = String :: from ( "127.0.0.1" ) ;
58+ impl FromStr for RestfulUri {
59+ type Err = anyhow:: Error ;
60+
61+ fn from_str ( s : & str ) -> Result < Self , Self :: Err > {
62+ let expression = regex:: Regex :: new ( r"^(?P<scheme>none|tcp|unix)://(?P<value>.*)" ) . unwrap ( ) ;
63+ let Some ( cap) = expression. captures ( s) else {
64+ return Err ( anyhow ! ( "invalid scheme input" ) ) ;
65+ } ;
66+ let scheme = & cap[ "scheme" ] ;
67+ let value = & cap[ "value" ] ;
68+ match UriScheme :: from_str ( scheme) ? {
69+ UriScheme :: Tcp => {
70+ let ( ip_addr, port) = parse_tcp_input ( value) ?;
71+ Ok ( Self :: Tcp ( ip_addr, port) )
72+ }
73+ UriScheme :: Unix => {
74+ if value. is_empty ( ) {
75+ return Err ( anyhow ! ( "empty unix socket path" ) ) ;
76+ }
77+ Ok ( Self :: Unix ( value. to_string ( ) ) )
78+ }
79+ UriScheme :: None => Ok ( Self :: None ) ,
5380 }
81+ }
82+ }
5483
55- let ip_addr = Ipv4Addr :: from_str ( & parts[ 0 ] )
56- . context ( "restful URI IP address formatted incorrectly" ) ?;
57- let port =
58- u16:: from_str ( & parts[ 1 ] ) . context ( "restful URI port number formatted incorrectly" ) ?;
84+ fn parse_tcp_input ( input : & str ) -> Result < ( Ipv4Addr , u16 ) , anyhow:: Error > {
85+ let mut parts: Vec < String > = input. split ( ':' ) . map ( |s| s. to_string ( ) ) . collect ( ) ;
86+ if parts. len ( ) != 2 {
87+ return Err ( anyhow ! ( "restful URI formatted incorrectly" ) ) ;
88+ }
5989
60- Ok ( Self { ip_addr, port } )
90+ // Ipv4Address's FromStr does not understand that the "localhost" IP address translates to
91+ // 127.0.0.1, this must be manually translated.
92+ if & parts[ 0 ] [ ..] == "localhost" {
93+ parts[ 0 ] = String :: from ( "127.0.0.1" ) ;
6194 }
95+
96+ let ip_addr =
97+ Ipv4Addr :: from_str ( & parts[ 0 ] ) . context ( "restful URI IP address formatted incorrectly" ) ?;
98+ let port = u16:: from_str ( & parts[ 1 ] ) . context ( "restful URI port number formatted incorrectly" ) ?;
99+ Ok ( ( ip_addr, port) )
62100}
63101
64- impl Default for RestfulUriAddr {
102+ impl Default for RestfulUri {
65103 fn default ( ) -> Self {
66- Self {
67- ip_addr : Ipv4Addr :: new ( 127 , 0 , 0 , 1 ) ,
68- port : 8081 ,
69- }
104+ Self :: Tcp ( Ipv4Addr :: new ( 127 , 0 , 0 , 1 ) , 8081 )
70105 }
71106}
72107
@@ -79,42 +114,154 @@ pub unsafe fn get_shutdown_eventfd(ctx_id: u32) -> i32 {
79114 fd
80115}
81116
117+ fn handle_incoming_stream < T : Read + Write > ( stream : & mut T , shutdown_fd : & mut File ) {
118+ let mut buf = [ 0u8 ; 4096 ] ;
119+ match stream. read ( & mut buf) {
120+ Ok ( _sz) => {
121+ let request = String :: from_utf8_lossy ( & buf) ;
122+ if request. contains ( "POST" ) {
123+ // Send a VirtualMachineStateStopping message to the client.
124+ if let Err ( e) = stream. write_all ( HTTP_STOPPING . as_bytes ( ) ) {
125+ println ! ( "Error writing POST response: {e}" ) ;
126+ }
127+
128+ // Shut down the VM.
129+ if let Err ( e) = shutdown_fd. write_all ( & 1u64 . to_le_bytes ( ) ) {
130+ println ! ( "Error writing to shutdown fd: {e}" ) ;
131+ }
132+ } else if let Err ( e) = stream. write_all ( HTTP_RUNNING . as_bytes ( ) ) {
133+ println ! ( "Error writing GET response: {e}" ) ;
134+ }
135+ }
136+ Err ( e) => println ! ( "Error reading stream: {}" , e) ,
137+ }
138+ }
139+
82140/// Listen for status and shutdown requests from the client. Shut down the krun VM when prompted.
83141pub fn status_listener (
84142 shutdown_eventfd : RawFd ,
85- addr : Option < RestfulUriAddr > ,
143+ addr : Option < RestfulUri > ,
86144) -> Result < ( ) , anyhow:: Error > {
87145 // VM is shut down by writing to the shutdown event file.
88146 let mut shutdown = unsafe { File :: from_raw_fd ( shutdown_eventfd) } ;
89147
90148 let addr = addr. unwrap_or_default ( ) ;
91149
92- let listener = TcpListener :: bind ( ( addr. ip_addr , addr. port ) ) . unwrap ( ) ;
93-
94- for stream in listener. incoming ( ) {
95- let mut buf = [ 0u8 ; 4096 ] ;
96- let mut stream = stream. unwrap ( ) ;
97-
98- match stream. read ( & mut buf) {
99- Ok ( _sz) => {
100- let request = String :: from_utf8_lossy ( & buf) ;
101- if request. contains ( "POST" ) {
102- // Send a VirtualMachineStateStopping message to the client.
103- if let Err ( e) = stream. write_all ( HTTP_STOPPING . as_bytes ( ) ) {
104- println ! ( "Error writting POST response: {e}" ) ;
105- }
106-
107- // Shut down the VM.
108- if let Err ( e) = shutdown. write_all ( & 1u64 . to_le_bytes ( ) ) {
109- println ! ( "Error writting to shutdown fd: {e}" ) ;
110- }
111- } else if let Err ( e) = stream. write_all ( HTTP_RUNNING . as_bytes ( ) ) {
112- println ! ( "Error writting GET response: {e}" ) ;
150+ match addr {
151+ RestfulUri :: Tcp ( addr, port) => {
152+ let listener = TcpListener :: bind ( ( addr, port) )
153+ . map_err ( |e| anyhow ! ( "Unable to bind to TCP listener: {}" , e) ) ?;
154+
155+ for stream in listener. incoming ( ) {
156+ handle_incoming_stream ( & mut stream. unwrap ( ) , & mut shutdown)
157+ }
158+ }
159+ RestfulUri :: Unix ( path) => {
160+ if let Err ( e) = std:: fs:: remove_file ( & path) {
161+ if e. kind ( ) != ErrorKind :: NotFound {
162+ return Err ( anyhow ! ( "failed to remove socket with error {e}" ) ) ;
113163 }
114164 }
115- Err ( e) => println ! ( "Error reading stream: {}" , e) ,
165+ let listener = UnixListener :: bind ( path)
166+ . map_err ( |e| anyhow ! ( "Unable to bind to unix socket: {}" , e) ) ?;
167+
168+ for stream in listener. incoming ( ) {
169+ handle_incoming_stream ( & mut stream. unwrap ( ) , & mut shutdown)
170+ }
116171 }
172+ RestfulUri :: None => unreachable ! ( ) ,
117173 }
118174
119175 Ok ( ( ) )
120176}
177+
178+ #[ allow( unused_imports) ]
179+ mod tests {
180+ use super :: * ;
181+
182+ #[ test]
183+ fn parse_valid_unix_scheme ( ) {
184+ assert_eq ! (
185+ RestfulUri :: Unix ( "/tmp/path" . to_string( ) ) ,
186+ RestfulUri :: from_str( "unix:///tmp/path" ) . unwrap( )
187+ ) ;
188+ }
189+
190+ #[ test]
191+ fn parse_unix_scheme_missing_path ( ) {
192+ assert_eq ! (
193+ anyhow!( "empty unix socket path" ) . to_string( ) ,
194+ RestfulUri :: from_str( "unix://" ) . err( ) . unwrap( ) . to_string( )
195+ ) ;
196+ }
197+
198+ #[ test]
199+ fn parse_unix_scheme_missing_slashes ( ) {
200+ assert_eq ! (
201+ anyhow!( "invalid scheme input" ) . to_string( ) ,
202+ RestfulUri :: from_str( "unix:" ) . err( ) . unwrap( ) . to_string( )
203+ ) ;
204+ }
205+
206+ #[ test]
207+ fn parse_unix_scheme_misspelling ( ) {
208+ assert_eq ! (
209+ anyhow!( "invalid scheme input" ) . to_string( ) ,
210+ RestfulUri :: from_str( "uni://path" )
211+ . err( )
212+ . unwrap( )
213+ . to_string( )
214+ ) ;
215+ }
216+
217+ #[ test]
218+ fn parse_valid_tcp_scheme ( ) {
219+ assert_eq ! (
220+ RestfulUri :: Tcp ( Ipv4Addr :: new( 127 , 0 , 0 , 1 ) , 8080 ) ,
221+ RestfulUri :: from_str( "tcp://localhost:8080" ) . unwrap( ) ,
222+ ) ;
223+ }
224+
225+ #[ test]
226+ fn parse_tcp_scheme_missing_port ( ) {
227+ assert_eq ! (
228+ anyhow!( "restful URI formatted incorrectly" ) . to_string( ) ,
229+ RestfulUri :: from_str( "tcp://localhost" )
230+ . err( )
231+ . unwrap( )
232+ . to_string( )
233+ ) ;
234+ }
235+
236+ #[ test]
237+ fn parse_tcp_scheme_with_unix_path ( ) {
238+ assert_eq ! (
239+ anyhow!( "restful URI formatted incorrectly" ) . to_string( ) ,
240+ RestfulUri :: from_str( "tcp:///tmp/path" )
241+ . err( )
242+ . unwrap( )
243+ . to_string( ) ,
244+ ) ;
245+ }
246+
247+ #[ test]
248+ fn parse_valid_none_scheme ( ) {
249+ assert_eq ! ( RestfulUri :: None , RestfulUri :: from_str( "none://" ) . unwrap( ) ) ;
250+ }
251+
252+ #[ test]
253+ fn parse_none_scheme_missing_postfix ( ) {
254+ assert_eq ! (
255+ anyhow!( "invalid scheme input" ) . to_string( ) ,
256+ RestfulUri :: from_str( "none" ) . err( ) . unwrap( ) . to_string( ) ,
257+ ) ;
258+ }
259+
260+ #[ test]
261+ fn parse_random_string_scheme ( ) {
262+ assert_eq ! (
263+ anyhow!( "invalid scheme input" ) . to_string( ) ,
264+ RestfulUri :: from_str( "foobar" ) . err( ) . unwrap( ) . to_string( ) ,
265+ ) ;
266+ }
267+ }
0 commit comments