From aac381e55b26b3162532f12c1c63c9258bab0130 Mon Sep 17 00:00:00 2001 From: julien-cpsn Date: Mon, 15 Apr 2024 16:37:38 +0200 Subject: [PATCH] Added Multipart form, UrlEncoded form and settings in Postman import --- Cargo.lock | 4 +- Cargo.toml | 2 +- README.md | 3 +- .../Test Collection.postman_collection.json | 87 +++++++++++++ src/app/app_logic/request/body.rs | 2 +- src/app/files/postman.rs | 116 ++++++++++++++++-- src/request/request.rs | 2 +- 7 files changed, 198 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 040a637..8a98d0d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -972,9 +972,9 @@ dependencies = [ [[package]] name = "parse_postman_collection" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36e8f13064ec4d862569f13e3fc3dbca48a04799f026fd0ad555f9f4c31f1f6d" +checksum = "f8af63eab6a89f7d035f4339637d0b32e3a3b15a3c3c77c577c95a4d9f75cff1" dependencies = [ "error-chain", "semver", diff --git a/Cargo.toml b/Cargo.toml index 49bf503..8b1fe05 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ serde = { version = "1.0.197", features = ["derive", "rc"] } serde_json = "1.0.114" toml = "0.8.11" envfile = "0.2.1" -parse_postman_collection = "0.2.1" +parse_postman_collection = "0.2.2" clap = { version = "4.5.0", features = ["derive", "color"] } tokio = { version = "1.36.0", features = ["rt", "rt-multi-thread", "macros"] } strum = "0.26.2" diff --git a/README.md b/README.md index 36ae267..f8aeb4b 100644 --- a/README.md +++ b/README.md @@ -141,11 +141,11 @@ cargo run -- -h - **To add** - Create a repo wiki - Document whole code + - File body content-type - **To improve** - Pretty print output - Sign binary - - Add Multipart form, URL encoded form and request settings from Postman import - **To fix** - Query parameters bug @@ -163,6 +163,7 @@ cargo run -- -h - Editing cookies - Insomnia import - Auto-completion on env file variables + - Manage multipart Content-type header (auto-generated for now) ### Ideas (will think about it later) diff --git a/import_tests/Test Collection.postman_collection.json b/import_tests/Test Collection.postman_collection.json index 391d07f..6a812ab 100644 --- a/import_tests/Test Collection.postman_collection.json +++ b/import_tests/Test Collection.postman_collection.json @@ -243,6 +243,93 @@ } }, "response": [] + }, + { + "name": "Test Multipart Form", + "protocolProfileBehavior": { + "followRedirects": false, + "disableCookies": true + }, + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "custname", + "value": "ddddd", + "type": "text" + }, + { + "key": "custtel", + "value": "", + "type": "text" + }, + { + "key": "custemail", + "value": "", + "type": "text" + }, + { + "key": "delivery", + "value": "", + "type": "text" + }, + { + "key": "comments", + "value": "", + "type": "text" + }, + { + "key": "some_file", + "type": "file", + "src": "/C:/Users/u248244/Documents/Rust/ATAC/LICENSE" + } + ] + }, + "url": { + "raw": "https://httpbin.org/post", + "protocol": "https", + "host": [ + "httpbin", + "org" + ], + "path": [ + "post" + ] + } + }, + "response": [] + }, + { + "name": "Test URL encoded form", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "some_key", + "value": "some_value", + "type": "text" + } + ] + }, + "url": { + "raw": "https://httpbin.org/post", + "protocol": "https", + "host": [ + "httpbin", + "org" + ], + "path": [ + "post" + ] + } + }, + "response": [] } ] } \ No newline at end of file diff --git a/src/app/app_logic/request/body.rs b/src/app/app_logic/request/body.rs index 35dbc5d..90fc6c0 100644 --- a/src/app/app_logic/request/body.rs +++ b/src/app/app_logic/request/body.rs @@ -164,7 +164,7 @@ impl App<'_> { ContentType::NoBody => { selected_request.find_and_delete_header(CONTENT_TYPE.as_str()) }, - // Impossible to set the header for multipart yet, because of boundary and content-length that are computed on reqwest's side + // TODO: Impossible to set the header for multipart yet, because of boundary and content-length that are computed on reqwest's side ContentType::Multipart(_) => {}, // Create or replace Content-Type header with new body content type ContentType::Form(_) | ContentType::Raw(_) | ContentType::Json(_) | ContentType::Xml(_) | ContentType::Html(_) => { diff --git a/src/app/files/postman.rs b/src/app/files/postman.rs index 45769ab..8afdcdf 100644 --- a/src/app/files/postman.rs +++ b/src/app/files/postman.rs @@ -1,7 +1,9 @@ use std::path::PathBuf; use std::str::FromStr; use std::sync::{Arc, RwLock}; -use parse_postman_collection::v2_1_0::{AuthType, Body, HeaderUnion, Items, Language, Mode, RequestClass, RequestUnion, Url}; + +use parse_postman_collection::v2_1_0::{AuthType, Body, FormParameterSrcUnion, HeaderUnion, Items, Language, Mode, RequestClass, RequestUnion, Url}; + use crate::app::app::App; use crate::app::startup::args::ARGS; use crate::request::auth::Auth; @@ -9,7 +11,7 @@ use crate::request::body::ContentType; use crate::request::collection::Collection; use crate::request::method::Method; use crate::request::request::{DEFAULT_HEADERS, KeyValue, Request}; - +use crate::request::settings::RequestSettings; impl App<'_> { pub fn import_postman_collection(&mut self, path_buf: &PathBuf, max_depth: u16) { @@ -148,7 +150,7 @@ fn is_folder(folder: &Items) -> bool { } fn parse_request(item: Items) -> Request { - let item_name = item.name.unwrap(); + let item_name = item.name.clone().unwrap(); println!("\t\tFound request \"{}\"", item_name); @@ -156,6 +158,16 @@ fn parse_request(item: Items) -> Request { request.name = item_name; + /* SETTINGS */ + + // TODO: update parse_postman_collection to handle "protocolProfileBehavior" + match retrieve_settings(&item) { + None => {} + Some(request_settings) => request.settings = request_settings + } + + /* REQUEST */ + let item_request = item.request.unwrap(); match &item_request { @@ -189,18 +201,28 @@ fn parse_request(item: Items) -> Request { Some(auth) => request.auth = auth } - /* BODY */ + /* HEADERS */ - match retrieve_body(&request_class) { + match retrieve_headers(&request_class) { None => {} - Some(content_type) => request.body = content_type + Some(headers) => request.headers = headers } - /* HEADERS */ + /* BODY */ - match retrieve_headers(&request_class) { + match retrieve_body(&request_class) { None => {} - Some(headers) => request.headers = headers + Some(body) => { + match &body { + ContentType::Multipart(_) => {} // TODO: Not handled yet + body_type => { + let content_type = body_type.to_content_type().clone(); + request.modify_or_create_header("content-type", &content_type); + } + } + + request.body = body; + } } } RequestUnion::String(_) => {} @@ -235,11 +257,12 @@ fn retrieve_body(request_class: &RequestClass) -> Option { match body { Body::String(body_as_raw) => Some(ContentType::Raw(body_as_raw)), Body::BodyClass(body) => { - let body_as_raw = body.raw?; let body_mode = body.mode?; return match body_mode { Mode::Raw => { + let body_as_raw = body.raw?; + if let Some(options) = body.options { let language = options.raw?.language?; @@ -257,8 +280,61 @@ fn retrieve_body(request_class: &RequestClass) -> Option { } }, Mode::File => None, - Mode::Formdata => None, - Mode::Urlencoded => None + Mode::Formdata => { + let form_data = body.formdata?; + + let mut multipart: Vec = vec![]; + + for param in form_data { + let param_type = param.form_parameter_type?; + + let key_value = match param_type.as_str() { + "text" => KeyValue { + enabled: true, + data: (param.key, param.value.unwrap_or(String::new())), + }, + "file" => { + let file = match param.src? { + FormParameterSrcUnion::File(file) => file, + // If there are many files, tries to get the first one + FormParameterSrcUnion::Files(files) => files.get(0)?.to_string() + }; + + KeyValue { + enabled: true, + data: (param.key, format!("!!{file}")), + } + }, + param_type => { + println!("\t\t\tUnknown Multipart form type \"{param_type}\""); + return None; + } + }; + + multipart.push(key_value); + } + + Some(ContentType::Multipart(multipart)) + }, + Mode::Urlencoded => { + let form_data = body.urlencoded?; + + let mut url_encoded: Vec = vec![]; + + for param in form_data { + let value = param.value.unwrap_or(String::new()); + let is_disabled = param.disabled.unwrap_or(false); + + let key_value = KeyValue { + enabled: !is_disabled, + data: (param.key, value), + }; + + url_encoded.push(key_value); + } + + Some(ContentType::Form(url_encoded)) + } } } } @@ -326,4 +402,20 @@ fn retrieve_headers(request_class: &RequestClass) -> Option> { } HeaderUnion::String(_) => None } +} + +fn retrieve_settings(item: &Items) -> Option { + let protocol_profile_behavior = item.protocol_profile_behavior.clone()?; + + let mut settings = RequestSettings::default(); + + if let Some(follow_redirects) = protocol_profile_behavior.follow_redirects { + settings.allow_redirects = follow_redirects; + } + + if let Some(disable_cookies) = protocol_profile_behavior.disable_cookies { + settings.store_received_cookies = !disable_cookies; + } + + Some(settings) } \ No newline at end of file diff --git a/src/request/request.rs b/src/request/request.rs index 499ca03..f619ca8 100644 --- a/src/request/request.rs +++ b/src/request/request.rs @@ -158,7 +158,7 @@ impl Request { let mut was_header_found = false; for header in &mut self.headers { - if &header.data.0 == input_header { + if header.data.0.to_lowercase() == input_header.to_lowercase() { header.data.1 = value.to_string(); was_header_found = true; }