diff --git a/Cargo.lock b/Cargo.lock index d0c44a9..ac1f30d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -228,6 +228,12 @@ dependencies = [ "libc", ] +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + [[package]] name = "heck" version = "0.4.0" @@ -341,6 +347,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", + "tempdir", ] [[package]] @@ -556,6 +563,43 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" +dependencies = [ + "fuchsia-cprng", + "libc", + "rand_core 0.3.1", + "rdrand", + "winapi", +] + +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + [[package]] name = "regex" version = "1.6.0" @@ -573,6 +617,15 @@ version = "0.6.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + [[package]] name = "ring" version = "0.16.20" @@ -680,6 +733,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempdir" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" +dependencies = [ + "rand", + "remove_dir_all", +] + [[package]] name = "termcolor" version = "1.1.3" diff --git a/Cargo.toml b/Cargo.toml index 04916d5..57bf494 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,3 +23,6 @@ chrono = "0.4" parse_duration = "2.1.1" atty = "0.2" base64 = "0.21.0" + +[dev-dependencies] +tempdir = "0.3.7" diff --git a/src/cli_config.rs b/src/cli_config.rs index b6f70db..d5b9317 100644 --- a/src/cli_config.rs +++ b/src/cli_config.rs @@ -2,6 +2,7 @@ use crate::translators::{PayloadItem, SupportedTypes}; use crate::utils::parse_duration_string; use clap::{Parser, Subcommand, ValueEnum}; use jsonwebtoken::Algorithm; +use std::path::PathBuf; #[derive(Parser, Debug)] #[clap(name = "jwt")] @@ -94,6 +95,11 @@ pub struct EncodeArgs { #[clap(long, short = 'S')] #[clap(value_parser)] pub secret: String, + + /// The path of the file to write the result to (suppresses default standard output) + #[clap(long = "out", short = 'o')] + #[clap(value_parser)] + pub output_path: Option, } #[derive(Debug, Clone, Parser)] @@ -130,6 +136,11 @@ pub struct DecodeArgs { #[clap(long = "ignore-exp")] #[clap(value_parser)] pub ignore_exp: bool, + + /// The path of the file to write the result to (suppresses default standard output, implies JSON format) + #[clap(long = "out", short = 'o')] + #[clap(value_parser)] + pub output_path: Option, } #[allow(clippy::upper_case_acronyms)] diff --git a/src/main.rs b/src/main.rs index 2d9a7cc..fcc8c2c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ use clap::Parser; use cli_config::{App, Commands, EncodeArgs}; +use std::process::exit; use translators::decode::{decode_token, print_decoded_token}; use translators::encode::{encode_token, print_encoded_token}; @@ -22,13 +23,23 @@ fn main() { warn_unsupported(arguments); let token = encode_token(arguments); + let output_path = &arguments.output_path; - print_encoded_token(token); + exit(match print_encoded_token(token, output_path) { + Ok(_) => 0, + _ => 1, + }); } Commands::Decode(arguments) => { let (validated_token, token_data, format) = decode_token(arguments); + let output_path = &arguments.output_path; - print_decoded_token(validated_token, token_data, format); + exit( + match print_decoded_token(validated_token, token_data, format, output_path) { + Ok(_) => 0, + _ => 1, + }, + ); } - } + }; } diff --git a/src/translators/decode.rs b/src/translators/decode.rs index 3c393ca..24566af 100644 --- a/src/translators/decode.rs +++ b/src/translators/decode.rs @@ -1,6 +1,6 @@ use crate::cli_config::{translate_algorithm, DecodeArgs}; use crate::translators::Payload; -use crate::utils::slurp_file; +use crate::utils::{slurp_file, write_file}; use base64::engine::general_purpose::STANDARD as base64_engine; use base64::Engine as _; use jsonwebtoken::errors::{ErrorKind, Result as JWTResult}; @@ -9,7 +9,7 @@ use serde_derive::{Deserialize, Serialize}; use serde_json::to_string_pretty; use std::collections::HashSet; use std::io; -use std::process::exit; +use std::path::PathBuf; #[derive(Debug, PartialEq, Eq)] pub enum OutputFormat { @@ -18,9 +18,9 @@ pub enum OutputFormat { } #[derive(Debug, Serialize, Deserialize, PartialEq)] -struct TokenOutput { - header: Header, - payload: Payload, +pub struct TokenOutput { + pub header: Header, + pub payload: Payload, } impl TokenOutput { @@ -147,7 +147,8 @@ pub fn print_decoded_token( validated_token: JWTResult>, token_data: JWTResult>, format: OutputFormat, -) { + output_path: &Option, +) -> JWTResult<()> { if let Err(err) = &validated_token { match err.kind() { ErrorKind::InvalidToken => { @@ -193,23 +194,26 @@ pub fn print_decoded_token( err ), }; + return Err(validated_token.err().unwrap()); } - match (format, token_data) { - (OutputFormat::Json, Ok(token)) => { - println!("{}", to_string_pretty(&TokenOutput::new(token)).unwrap()) + match (output_path.as_ref(), format, token_data) { + (Some(path), _, Ok(token)) => { + let json = to_string_pretty(&TokenOutput::new(token)).unwrap(); + write_file(path, json.as_bytes()); + println!("Wrote jwt to file {}", path.display()); + } + (None, OutputFormat::Json, Ok(token)) => { + println!("{}", to_string_pretty(&TokenOutput::new(token)).unwrap()); } - (_, Ok(token)) => { + (None, _, Ok(token)) => { bunt::println!("\n{$bold}Token header\n------------{/$}"); println!("{}\n", to_string_pretty(&token.header).unwrap()); bunt::println!("{$bold}Token claims\n------------{/$}"); println!("{}", to_string_pretty(&token.claims).unwrap()); } - (_, Err(_)) => exit(1), + (_, _, Err(err)) => return Err(err), } - exit(match validated_token { - Err(_) => 1, - Ok(_) => 0, - }) + Ok(()) } diff --git a/src/translators/encode.rs b/src/translators/encode.rs index df577c1..c8977a2 100644 --- a/src/translators/encode.rs +++ b/src/translators/encode.rs @@ -1,6 +1,6 @@ use crate::cli_config::{translate_algorithm, EncodeArgs}; use crate::translators::{Payload, PayloadItem}; -use crate::utils::slurp_file; +use crate::utils::{slurp_file, write_file}; use atty::Stream; use base64::engine::general_purpose::STANDARD as base64_engine; use base64::Engine as _; @@ -9,7 +9,7 @@ use jsonwebtoken::errors::Result as JWTResult; use jsonwebtoken::{encode, Algorithm, EncodingKey, Header}; use serde_json::{from_str, Value}; use std::io; -use std::process::exit; +use std::path::PathBuf; fn create_header(alg: Algorithm, kid: Option<&String>) -> Header { let mut header = Header::new(alg); @@ -113,20 +113,27 @@ pub fn encode_token(arguments: &EncodeArgs) -> JWTResult { .and_then(|secret| encode(&header, &claims, &secret)) } -pub fn print_encoded_token(token: JWTResult) { - match token { - Ok(jwt) => { +pub fn print_encoded_token( + token: JWTResult, + output_path: &Option, +) -> JWTResult<()> { + match (output_path.as_ref(), token) { + (Some(path), Ok(jwt)) => { + write_file(path, jwt.as_bytes()); + println!("Wrote jwt to file {}", path.display()); + } + (None, Ok(jwt)) => { if atty::is(Stream::Stdout) { println!("{}", jwt); } else { print!("{}", jwt); - } - exit(0); + }; } - Err(err) => { + (_, Err(err)) => { bunt::eprintln!("{$red+bold}Something went awry creating the jwt{/$}\n"); eprintln!("{}", err); - exit(1); + return Err(err); } } + Ok(()) } diff --git a/src/utils.rs b/src/utils.rs index 3cc3b45..9ce0771 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,9 +1,14 @@ use std::fs; +use std::path::Path; pub fn slurp_file(file_name: &str) -> Vec { fs::read(file_name).unwrap_or_else(|_| panic!("Unable to read file {}", file_name)) } +pub fn write_file(path: &Path, content: &[u8]) { + fs::write(path, content).unwrap_or_else(|_| panic!("Unable to write file {}", path.display())) +} + pub fn parse_duration_string(val: &str) -> Result { let mut base_val = val.replace(" ago", ""); diff --git a/tests/main_test.rs b/tests/main_test.rs index 9a9c5a4..da1da25 100644 --- a/tests/main_test.rs +++ b/tests/main_test.rs @@ -3,12 +3,16 @@ include!("../src/main.rs"); #[cfg(test)] mod tests { use super::cli_config::{App, DecodeArgs, EncodeArgs}; - use super::translators::decode::{decode_token, OutputFormat}; - use super::translators::encode::encode_token; + use super::translators::decode::{ + decode_token, print_decoded_token, OutputFormat, TokenOutput, + }; + use super::translators::encode::{encode_token, print_encoded_token}; + use super::utils::slurp_file; use chrono::{Duration, TimeZone, Utc}; use clap::{CommandFactory, FromArgMatches}; use jsonwebtoken::{Algorithm, TokenData}; - use serde_json::from_value; + use serde_json::{from_value, Result as JsonResult}; + use tempdir::TempDir; #[test] fn encodes_a_token() { @@ -665,4 +669,98 @@ mod tests { Some(&Utc.timestamp_opt(exp, 0).unwrap().to_rfc3339().into()) ); } + + #[test] + fn writes_output_to_file() { + let tmp_dir_result = TempDir::new("jwtclitest"); + assert!(tmp_dir_result.is_ok()); + + let tmp_dir = tmp_dir_result.unwrap(); + let out_path = tmp_dir.path().join("jwt.out"); + println!("jwt output path: {}", out_path.to_str().unwrap()); + + let secret = "1234567890"; + let kid = "1234"; + let exp = (Utc::now() + Duration::minutes(60)).timestamp(); + let nbf = Utc::now().timestamp(); + let encode_matcher = App::command() + .try_get_matches_from(vec![ + "jwt", + "encode", + "-S", + secret, + "-A", + "HS256", + &format!("-e={}", &exp.to_string()), + "-k", + kid, + "-n", + &nbf.to_string(), + "-o", + out_path.to_str().unwrap(), + ]) + .unwrap(); + let encode_matches = encode_matcher.subcommand_matches("encode").unwrap(); + let encode_arguments = EncodeArgs::from_arg_matches(encode_matches).unwrap(); + + let out_path_from_args = &encode_arguments.output_path; + assert!(out_path_from_args.is_some()); + assert_eq!(out_path, *out_path_from_args.as_ref().unwrap()); + + let encoded_token = encode_token(&encode_arguments); + let print_encoded_result = print_encoded_token(encoded_token, out_path_from_args); + assert!(print_encoded_result.is_ok()); + + let out_content_buf = slurp_file(out_path.to_str().unwrap()); + let out_content_str = std::str::from_utf8(&out_content_buf); + assert!(out_content_str.is_ok()); + println!("jwt: {}", out_content_str.unwrap()); + + let json_path = tmp_dir.path().join("decoded.json"); + println!("decoded json path: {}", json_path.to_str().unwrap()); + + let decode_matcher = App::command() + .try_get_matches_from(vec![ + "jwt", + "decode", + "-S", + secret, + &out_content_str.unwrap(), + "-o", + json_path.to_str().unwrap(), + ]) + .unwrap(); + let decode_matches = decode_matcher.subcommand_matches("decode").unwrap(); + let decode_arguments = DecodeArgs::from_arg_matches(decode_matches).unwrap(); + let (decoded_token, decoded_token_data, decoded_output_format) = + decode_token(&decode_arguments); + assert!(decoded_token.is_ok()); + + let json_path_from_args = &decode_arguments.output_path; + assert!(json_path_from_args.is_some()); + assert_eq!(json_path, *json_path_from_args.as_ref().unwrap()); + + let json_print_result = print_decoded_token( + decoded_token, + decoded_token_data, + decoded_output_format, + json_path_from_args, + ); + assert!(json_print_result.is_ok()); + + let json_content_buf = slurp_file(json_path.to_str().unwrap()); + let json_content_str = std::str::from_utf8(&json_content_buf); + assert!(json_content_str.is_ok()); + + let json_result: JsonResult = serde_json::from_str(json_content_str.unwrap()); + assert!(json_result.is_ok()); + let json = json_result.unwrap(); + println!("json: {:#?}", json); + + let TokenOutput { header, payload } = json; + assert_eq!(header.alg, Algorithm::HS256); + assert_eq!(header.kid, Some(kid.to_string())); + assert_eq!(payload.0["nbf"], nbf); + assert_eq!(payload.0["exp"], exp); + } }