33use crate :: error:: PythonError ;
44use crate :: policy:: Python3StudentFilePolicy ;
55use crate :: python_test_result:: PythonTestResult ;
6+ use hmac:: { Hmac , Mac , NewMac } ;
67use lazy_static:: lazy_static;
8+ use rand:: Rng ;
9+ use sha2:: Sha256 ;
710use std:: collections:: { HashMap , HashSet } ;
811use std:: env;
912use std:: io:: BufReader ;
@@ -102,6 +105,7 @@ impl Python3Plugin {
102105 path : & Path ,
103106 extra_args : & [ & str ] ,
104107 timeout : Option < Duration > ,
108+ stdin : Option < String > ,
105109 ) -> Result < Output , PythonError > {
106110 let minimum_python_version = TmcProjectYml :: from ( path) ?
107111 . minimum_python_version
@@ -139,6 +143,12 @@ impl Python3Plugin {
139143
140144 let command = Self :: get_local_python_command ( ) ;
141145 let command = command. with ( |e| e. args ( & common_args) . args ( extra_args) . cwd ( path) ) ;
146+ let command = if let Some ( stdin) = stdin {
147+ command. set_stdin_data ( stdin)
148+ } else {
149+ command
150+ } ;
151+
142152 let output = if let Some ( timeout) = timeout {
143153 command. output_with_timeout ( timeout) ?
144154 } else {
@@ -166,14 +176,25 @@ impl Python3Plugin {
166176 }
167177
168178 /// Parse test result file
169- fn parse_test_result (
179+ fn parse_and_verify_test_result (
170180 test_results_json : & Path ,
171181 logs : HashMap < String , String > ,
182+ hmac_data : Option < ( String , String ) > ,
172183 ) -> Result < RunResult , PythonError > {
173- let results_file = file_util:: open_file ( & test_results_json) ?;
174- let test_results: Vec < PythonTestResult > =
175- serde_json:: from_reader ( BufReader :: new ( results_file) )
176- . map_err ( |e| PythonError :: Deserialize ( test_results_json. to_path_buf ( ) , e) ) ?;
184+ let results = file_util:: read_file_to_string ( & test_results_json) ?;
185+
186+ // verify test results
187+ if let Some ( ( hmac_secret, test_runner_hmac_hex) ) = hmac_data {
188+ let mut mac = Hmac :: < Sha256 > :: new_varkey ( hmac_secret. as_bytes ( ) )
189+ . expect ( "HMAC can take key of any size" ) ;
190+ mac. update ( results. as_bytes ( ) ) ;
191+ let bytes =
192+ hex:: decode ( & test_runner_hmac_hex) . map_err ( |_| PythonError :: UnexpectedHmac ) ?;
193+ mac. verify ( & bytes) . map_err ( |_| PythonError :: InvalidHmac ) ?;
194+ }
195+
196+ let test_results: Vec < PythonTestResult > = serde_json:: from_str ( & results)
197+ . map_err ( |e| PythonError :: Deserialize ( test_results_json. to_path_buf ( ) , e) ) ?;
177198
178199 let mut status = RunStatus :: Passed ;
179200 let mut failed_points = HashSet :: new ( ) ;
@@ -209,7 +230,8 @@ impl LanguagePlugin for Python3Plugin {
209230 file_util:: remove_file ( & available_points_json) ?;
210231 }
211232
212- let run_result = Self :: run_tmc_command ( exercise_directory, & [ "available_points" ] , None ) ;
233+ let run_result =
234+ Self :: run_tmc_command ( exercise_directory, & [ "available_points" ] , None , None ) ;
213235 if let Err ( error) = run_result {
214236 log:: error!( "Failed to scan exercise. {}" , error) ;
215237 }
@@ -233,7 +255,24 @@ impl LanguagePlugin for Python3Plugin {
233255 file_util:: remove_file ( & test_results_json) ?;
234256 }
235257
236- let output = Self :: run_tmc_command ( exercise_directory, & [ ] , timeout) ;
258+ let ( output, random_string) = if exercise_directory. join ( "tmc/hmac_writer.py" ) . exists ( ) {
259+ // has hmac writer
260+ let random_string: String = rand:: thread_rng ( )
261+ . sample_iter ( rand:: distributions:: Alphanumeric )
262+ . take ( 32 )
263+ . map ( char:: from)
264+ . collect ( ) ;
265+ let output = Self :: run_tmc_command (
266+ exercise_directory,
267+ & [ "--wait-for-secret" ] ,
268+ timeout,
269+ Some ( random_string. clone ( ) ) ,
270+ ) ;
271+ ( output, Some ( random_string) )
272+ } else {
273+ let output = Self :: run_tmc_command ( exercise_directory, & [ ] , timeout, None ) ;
274+ ( output, None )
275+ } ;
237276
238277 match output {
239278 Ok ( output) => {
@@ -246,7 +285,17 @@ impl LanguagePlugin for Python3Plugin {
246285 "stderr" . to_string ( ) ,
247286 String :: from_utf8_lossy ( & output. stderr ) . into_owned ( ) ,
248287 ) ;
249- let parse_res = Self :: parse_test_result ( & test_results_json, logs) ;
288+
289+ let hmac_data = if let Some ( random_string) = random_string {
290+ let hmac_result_path = exercise_directory. join ( ".tmc_test_results.hmac.sha256" ) ;
291+ let test_runner_hmac = file_util:: read_file_to_string ( hmac_result_path) ?;
292+ Some ( ( random_string, test_runner_hmac) )
293+ } else {
294+ None
295+ } ;
296+
297+ let parse_res =
298+ Self :: parse_and_verify_test_result ( & test_results_json, logs, hmac_data) ;
250299 // remove file regardless of parse success
251300 if test_results_json. exists ( ) {
252301 file_util:: remove_file ( & test_results_json) ?;
@@ -366,7 +415,10 @@ impl LanguagePlugin for Python3Plugin {
366415#[ cfg( test) ]
367416mod test {
368417 use super :: * ;
369- use std:: path:: { Path , PathBuf } ;
418+ use std:: {
419+ io:: Write ,
420+ path:: { Path , PathBuf } ,
421+ } ;
370422 use tmc_langs_framework:: zip:: ZipArchive ;
371423 use tmc_langs_framework:: { domain:: RunStatus , plugin:: LanguagePlugin } ;
372424
@@ -752,4 +804,40 @@ class TestClass(unittest.TestCase):
752804 }
753805 assert ! ( got_point) ;
754806 }
807+
808+ #[ test]
809+ fn verifies_test_results_success ( ) {
810+ init ( ) ;
811+
812+ let mut temp = tempfile:: NamedTempFile :: new ( ) . unwrap ( ) ;
813+ temp. write_all ( br#"[{"name": "test.test_hello_world.HelloWorld.test_first", "status": "passed", "message": "", "passed": true, "points": ["p01-01.1"], "backtrace": []}]"# ) . unwrap ( ) ;
814+
815+ let hmac_secret = "047QzQx8RAYLR3lf0UfB75WX5EFnx7AV" . to_string ( ) ;
816+ let test_runner_hmac =
817+ "b379817c66cc7b1610d03ac263f02fa11f7b0153e6aeff3262ecc0598bf0be21" . to_string ( ) ;
818+ Python3Plugin :: parse_and_verify_test_result (
819+ temp. path ( ) ,
820+ HashMap :: new ( ) ,
821+ Some ( ( hmac_secret, test_runner_hmac) ) ,
822+ )
823+ . unwrap ( ) ;
824+ }
825+
826+ #[ test]
827+ fn verifies_test_results_failure ( ) {
828+ init ( ) ;
829+
830+ let mut temp = tempfile:: NamedTempFile :: new ( ) . unwrap ( ) ;
831+ temp. write_all ( br#"[{"name": "test.test_hello_world.HelloWorld.test_first", "status": "passed", "message": "", "passed": true, "points": ["p01-01.1"], "backtrace": []}]"# ) . unwrap ( ) ;
832+
833+ let hmac_secret = "047QzQx8RAYLR3lf0UfB75WX5EFnx7AV" . to_string ( ) ;
834+ let test_runner_hmac =
835+ "b379817c66cc7b1610d03ac263f02fa11f7b0153e6aeff3262ecc0598bf0be22" . to_string ( ) ;
836+ let res = Python3Plugin :: parse_and_verify_test_result (
837+ temp. path ( ) ,
838+ HashMap :: new ( ) ,
839+ Some ( ( hmac_secret, test_runner_hmac) ) ,
840+ ) ;
841+ assert ! ( res. is_err( ) ) ;
842+ }
755843}
0 commit comments