char/sorcery
static-files based git repo viewer
git clone https://git.t4t.associates/char/sorcery
8f07a0e
main
1use std:: fmt:: Write as _; 2use std:: fs; 3use std:: io::{ BufRead , BufReader , Write }; 4use std:: os:: unix:: net:: UnixStream ; 5use std:: path::{ Path , PathBuf }; 6 7use anyhow::{ Context , Result , bail}; 8 9# [ derive ( Clone )] 10pub struct Refresh { 11socket : PathBuf , 12token : Vec < u8 >, 13} 14 15impl Refresh { 16pub fn from_files ( 17socket : Option < PathBuf >, 18token_file : Option < PathBuf >, 19) ->Result < Option < Self >> { 20let ( Some ( socket), Some ( token_file)) =( socket, token_file) else { 21return Ok ( None ); 22}; 23let token = fs:: read ( & token_file) 24. with_context ( ||format! ( "reading refresh token {}" , token_file. display ())) ?; 25let mut token = token. as_slice (); 26if let Some ( without_lf) = token. strip_suffix ( b"\n" ) { 27 token = without_lf; 28} 29if let Some ( without_cr) = token. strip_suffix ( b"\r" ) { 30 token = without_cr; 31} 32if token. is_empty () || !token. iter (). all ( u8:: is_ascii_graphic) { 33bail! ( "refresh token must contain visible ASCII without whitespace" ); 34} 35Ok ( Some ( Self { 36 socket, 37token : token. to_vec (), 38})) 39} 40 41pub fn send ( & self , user : & str , repo : & str ) ->Result <()> { 42refresh ( & self . socket , & self . token , user, repo) 43} 44} 45 46fn refresh ( socket : & Path , token : & [ u8 ], user : & str , repo : & str ) ->Result <()> { 47let mut stream =UnixStream :: connect ( socket) 48. with_context ( ||format! ( "connecting to sorcery socket {}" , socket. display ())) ?; 49write! ( 50 stream, 51"POST /-/refresh/{}/{} HTTP/1.1\r\nHost: sorcery\r\nAuthorization: Bearer " , 52encode_component ( user), 53encode_component ( repo), 54) ?; 55 stream. write_all ( token) ?; 56 stream. write_all ( b"\r\nContent-Length: 0\r\nConnection: close\r\n\r\n" ) ?; 57 stream. flush () ?; 58 59let mut status =String :: new (); 60BufReader :: new ( stream). read_line ( & mut status) ?; 61let code = status. split_ascii_whitespace (). nth ( 1 ); 62if code !=Some ( "200" ) { 63bail! ( "sorcery refresh returned {}" , status. trim_end ()); 64} 65Ok (()) 66} 67 68fn encode_component ( component : & str ) ->String { 69let mut encoded =String :: with_capacity ( component. len ()); 70for bytein component. bytes () { 71if byte. is_ascii_alphanumeric () ||b"-._~" . contains ( & byte) { 72 encoded. push ( char:: from ( byte)); 73} else { 74write! ( encoded, "%{byte:02X}" ). expect ( "writing to a String cannot fail" ); 75} 76} 77 encoded 78} 79 80# [ cfg ( test )] 81mod tests{ 82use std:: os:: unix:: net:: UnixListener ; 83use std:: thread; 84 85use super :: * ; 86 87# [ test ] 88fn sends_authenticated_refresh_request () { 89let temp = tempfile:: tempdir (). unwrap (); 90let socket = temp. path (). join ( "socket" ); 91let listener =UnixListener :: bind ( & socket). unwrap (); 92let server = thread:: spawn ( move ||{ 93let ( mut connection, _) = listener. accept (). unwrap (); 94let mut request =String :: new (); 95let mut reader =BufReader :: new ( connection. try_clone (). unwrap ()); 96loop { 97let mut line =String :: new (); 98 reader. read_line ( & mut line). unwrap (); 99 request. push_str ( & line); 100if line =="\r\n" { 101break ; 102} 103} 104 connection 105. write_all ( b"HTTP/1.1 200 OK\r\nContent-Length: 10\r\n\r\nrefreshed\n" ) 106. unwrap (); 107 request 108}); 109 110refresh ( & socket, b"secret" , "an user" , "a/repo" ). unwrap (); 111let request = server. join (). unwrap (); 112assert! ( request. starts_with ( "POST /-/refresh/an%20user/a%2Frepo HTTP/1.1\r\n" )); 113assert! ( request. contains ( "Authorization: Bearer secret\r\n" )); 114} 115 116# [ test ] 117fn percent_encodes_path_components () { 118assert_eq! ( encode_component ( "hello world/ø" ), "hello%20world%2F%C3%B8" ); 119} 120 121# [ test ] 122fn incomplete_configuration_disables_refresh () { 123assert! ( Refresh :: from_files ( None , None ). unwrap (). is_none ()); 124assert! ( 125Refresh :: from_files ( Some ( "/tmp/sock" . into ()), None ) 126. unwrap () 127. is_none () 128); 129} 130 131# [ test ] 132fn rejects_whitespace_in_token () { 133let temp = tempfile:: NamedTempFile :: new (). unwrap (); 134 fs:: write ( temp. path (), "not valid\ninside" ). unwrap (); 135let error =Refresh :: from_files ( Some ( "/tmp/sock" . into ()), Some ( temp. path (). into ())); 136assert! ( error. is_err ()); 137} 138}