@@ -266,6 +266,15 @@ fn compute_hash(cryptify_token: &[u8], data: &[u8]) -> String {
266266 format ! ( "{:x}" , hash. finalize( ) )
267267}
268268
269+ fn check_cryptify_token ( header : & str , expected : & str ) -> Result < ( ) , Error > {
270+ if header != expected {
271+ return Err ( Error :: BadRequest ( Some (
272+ "Cryptify Token header does not match" . to_owned ( ) ,
273+ ) ) ) ;
274+ }
275+ Ok ( ( ) )
276+ }
277+
269278#[ put( "/fileupload/<uuid>" , data = "<data>" ) ]
270279async fn upload_chunk (
271280 config : & State < CryptifyConfig > ,
@@ -319,11 +328,7 @@ async fn upload_chunk(
319328 } ) ) ;
320329 }
321330
322- if headers. cryptify_token != state. cryptify_token {
323- return Err ( Error :: BadRequest ( Some (
324- "Cryptify Token header does not match" . to_owned ( ) ,
325- ) ) ) ;
326- }
331+ check_cryptify_token ( & headers. cryptify_token , & state. cryptify_token ) ?;
327332
328333 let mut file = match OpenOptions :: new ( )
329334 . write ( true )
@@ -364,6 +369,7 @@ async fn upload_chunk(
364369}
365370
366371struct FinalizeHeaders {
372+ cryptify_token : String ,
367373 content_range : ContentRange ,
368374}
369375
@@ -374,6 +380,17 @@ impl<'r> FromRequest<'r> for FinalizeHeaders {
374380 async fn from_request (
375381 request : & ' r rocket:: Request < ' _ > ,
376382 ) -> rocket:: request:: Outcome < Self , Self :: Error > {
383+ let cryptify_token = match request. headers ( ) . get_one ( "CryptifyToken" ) {
384+ Some ( cryptify_token) => cryptify_token,
385+ None => {
386+ return rocket:: request:: Outcome :: Error ( (
387+ rocket:: http:: Status :: BadRequest ,
388+ "Missing Cryptify Token header" . into ( ) ,
389+ ) )
390+ }
391+ }
392+ . to_string ( ) ;
393+
377394 let content_range = match request. headers ( ) . get_one ( "Content-Range" ) {
378395 Some ( content_range) => content_range,
379396 None => {
@@ -390,7 +407,10 @@ impl<'r> FromRequest<'r> for FinalizeHeaders {
390407 return rocket:: request:: Outcome :: Error ( ( rocket:: http:: Status :: BadRequest , e) )
391408 }
392409 } ;
393- rocket:: request:: Outcome :: Success ( FinalizeHeaders { content_range } )
410+ rocket:: request:: Outcome :: Success ( FinalizeHeaders {
411+ cryptify_token,
412+ content_range,
413+ } )
394414 }
395415}
396416
@@ -408,6 +428,8 @@ async fn upload_finalize(
408428 } ;
409429 let mut state = state. lock ( ) . await ;
410430
431+ check_cryptify_token ( & headers. cryptify_token , & state. cryptify_token ) ?;
432+
411433 if headers. content_range . size != Some ( state. uploaded ) {
412434 return Err ( Error :: UnprocessableEntity ( None ) ) ;
413435 }
@@ -559,3 +581,155 @@ async fn rocket() -> _ {
559581 . manage ( Store :: new ( ) )
560582 . manage ( vk)
561583}
584+
585+ #[ cfg( test) ]
586+ mod tests {
587+ use super :: * ;
588+ use rocket:: http:: { Header , Status } ;
589+ use rocket:: local:: asynchronous:: Client ;
590+
591+ // Test-only route exercising the FinalizeHeaders extractor in isolation.
592+ // Echoes the extracted fields so the test can verify successful parsing.
593+ #[ post( "/__test/finalize_headers" ) ]
594+ fn finalize_headers_echo ( h : FinalizeHeaders ) -> String {
595+ format ! (
596+ "{}|{}" ,
597+ h. cryptify_token,
598+ h. content_range. size. unwrap_or( 0 )
599+ )
600+ }
601+
602+ async fn headers_client ( ) -> Client {
603+ let r = rocket:: build ( ) . mount ( "/" , routes ! [ finalize_headers_echo] ) ;
604+ Client :: tracked ( r) . await . expect ( "valid rocket" )
605+ }
606+
607+ #[ rocket:: async_test]
608+ async fn finalize_headers_reject_missing_cryptify_token ( ) {
609+ let client = headers_client ( ) . await ;
610+ let res = client
611+ . post ( "/__test/finalize_headers" )
612+ . header ( Header :: new ( "Content-Range" , "bytes 0-99/100" ) )
613+ . dispatch ( )
614+ . await ;
615+ assert_eq ! ( res. status( ) , Status :: BadRequest ) ;
616+ }
617+
618+ #[ rocket:: async_test]
619+ async fn finalize_headers_reject_missing_content_range ( ) {
620+ let client = headers_client ( ) . await ;
621+ let res = client
622+ . post ( "/__test/finalize_headers" )
623+ . header ( Header :: new ( "CryptifyToken" , "abc123" ) )
624+ . dispatch ( )
625+ . await ;
626+ assert_eq ! ( res. status( ) , Status :: BadRequest ) ;
627+ }
628+
629+ #[ rocket:: async_test]
630+ async fn finalize_headers_reject_malformed_content_range ( ) {
631+ let client = headers_client ( ) . await ;
632+ let res = client
633+ . post ( "/__test/finalize_headers" )
634+ . header ( Header :: new ( "CryptifyToken" , "abc123" ) )
635+ . header ( Header :: new ( "Content-Range" , "not a real range" ) )
636+ . dispatch ( )
637+ . await ;
638+ assert_eq ! ( res. status( ) , Status :: BadRequest ) ;
639+ }
640+
641+ #[ rocket:: async_test]
642+ async fn finalize_headers_extract_both_headers ( ) {
643+ let client = headers_client ( ) . await ;
644+ let res = client
645+ . post ( "/__test/finalize_headers" )
646+ . header ( Header :: new ( "CryptifyToken" , "deadbeef" ) )
647+ . header ( Header :: new ( "Content-Range" , "bytes 0-99/100" ) )
648+ . dispatch ( )
649+ . await ;
650+ assert_eq ! ( res. status( ) , Status :: Ok ) ;
651+ assert_eq ! ( res. into_string( ) . await . as_deref( ) , Some ( "deadbeef|100" ) ) ;
652+ }
653+
654+ #[ test]
655+ fn content_range_parses_well_formed_range ( ) {
656+ let cr: ContentRange = "bytes 0-99/100" . parse ( ) . unwrap ( ) ;
657+ assert_eq ! ( cr. start, Some ( 0 ) ) ;
658+ assert_eq ! ( cr. end, Some ( 99 ) ) ;
659+ assert_eq ! ( cr. size, Some ( 100 ) ) ;
660+ }
661+
662+ #[ test]
663+ fn content_range_accepts_wildcard_range ( ) {
664+ let cr: ContentRange = "bytes */100" . parse ( ) . unwrap ( ) ;
665+ assert_eq ! ( cr. start, None ) ;
666+ assert_eq ! ( cr. end, None ) ;
667+ assert_eq ! ( cr. size, Some ( 100 ) ) ;
668+ }
669+
670+ #[ test]
671+ fn content_range_accepts_wildcard_size ( ) {
672+ let cr: ContentRange = "bytes 0-99/*" . parse ( ) . unwrap ( ) ;
673+ assert_eq ! ( cr. start, Some ( 0 ) ) ;
674+ assert_eq ! ( cr. end, Some ( 99 ) ) ;
675+ assert_eq ! ( cr. size, None ) ;
676+ }
677+
678+ #[ test]
679+ fn content_range_rejects_wrong_unit ( ) {
680+ assert ! ( "items 0-99/100" . parse:: <ContentRange >( ) . is_err( ) ) ;
681+ }
682+
683+ #[ test]
684+ fn content_range_rejects_empty_string ( ) {
685+ assert ! ( "" . parse:: <ContentRange >( ) . is_err( ) ) ;
686+ }
687+
688+ #[ test]
689+ fn check_cryptify_token_accepts_matching_token ( ) {
690+ assert ! ( check_cryptify_token( "abc123" , "abc123" ) . is_ok( ) ) ;
691+ }
692+
693+ #[ test]
694+ fn check_cryptify_token_rejects_mismatched_token ( ) {
695+ let result = check_cryptify_token ( "wrong" , "expected" ) ;
696+ match result {
697+ Err ( Error :: BadRequest ( Some ( msg) ) ) => {
698+ assert_eq ! ( msg, "Cryptify Token header does not match" ) ;
699+ }
700+ other => panic ! ( "expected BadRequest, got {:?}" , other) ,
701+ }
702+ }
703+
704+ #[ test]
705+ fn check_cryptify_token_rejects_empty_header_when_token_expected ( ) {
706+ assert ! ( matches!(
707+ check_cryptify_token( "" , "expected" ) ,
708+ Err ( Error :: BadRequest ( _) )
709+ ) ) ;
710+ }
711+
712+ #[ test]
713+ fn check_cryptify_token_is_case_sensitive ( ) {
714+ assert ! ( matches!(
715+ check_cryptify_token( "ABC123" , "abc123" ) ,
716+ Err ( Error :: BadRequest ( _) )
717+ ) ) ;
718+ }
719+
720+ #[ test]
721+ fn compute_hash_is_deterministic ( ) {
722+ let h1 = compute_hash ( b"token" , b"data" ) ;
723+ let h2 = compute_hash ( b"token" , b"data" ) ;
724+ assert_eq ! ( h1, h2) ;
725+ assert_eq ! ( h1. len( ) , 64 ) ;
726+ }
727+
728+ #[ test]
729+ fn compute_hash_differs_for_different_tokens ( ) {
730+ assert_ne ! (
731+ compute_hash( b"token-a" , b"data" ) ,
732+ compute_hash( b"token-b" , b"data" )
733+ ) ;
734+ }
735+ }
0 commit comments