├── .gitignore ├── Cargo.toml ├── LICENSE-MIT ├── README.md ├── screenshot.png └── src ├── curl ├── easy.rs └── mod.rs ├── httpstat ├── app.rs ├── format.rs └── mod.rs ├── lib.rs └── main.rs /.gitignore: -------------------------------------------------------------------------------- 1 | ### https://raw.github.com/github/gitignore/8512c772d731ce256f81da49e627d293274fab8a/Rust.gitignore 2 | 3 | # Generated by Cargo 4 | # will have compiled files and executables 5 | /target/ 6 | 7 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 8 | # More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock 9 | Cargo.lock 10 | 11 | 12 | ### https://raw.github.com/github/gitignore/8512c772d731ce256f81da49e627d293274fab8a/Global/macOS.gitignore 13 | 14 | *.DS_Store 15 | .AppleDouble 16 | .LSOverride 17 | 18 | # Icon must end with two \r 19 | Icon 20 | 21 | 22 | # Thumbnails 23 | ._* 24 | 25 | # Files that might appear in the root of a volume 26 | .DocumentRevisions-V100 27 | .fseventsd 28 | .Spotlight-V100 29 | .TemporaryItems 30 | .Trashes 31 | .VolumeIcon.icns 32 | .com.apple.timemachine.donotpresent 33 | 34 | # Directories potentially created on remote AFP share 35 | .AppleDB 36 | .AppleDesktop 37 | Network Trash Folder 38 | Temporary Items 39 | .apdisk 40 | 41 | 42 | ### https://raw.github.com/github/gitignore/8512c772d731ce256f81da49e627d293274fab8a/Global/Vim.gitignore 43 | 44 | # swap 45 | [._]*.s[a-w][a-z] 46 | [._]s[a-w][a-z] 47 | # session 48 | Session.vim 49 | # temporary 50 | .netrwhist 51 | *~ 52 | # auto-generated tag files 53 | tags 54 | 55 | 56 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["woxtu "] 3 | license = "MIT" 4 | name = "httpstat" 5 | version = "0.1.0" 6 | 7 | [lib] 8 | name = "httpstat" 9 | path = "src/lib.rs" 10 | 11 | [[bin]] 12 | name = "httpstat" 13 | path = "src/main.rs" 14 | 15 | [dependencies] 16 | clap = "2.14.0" 17 | curl-sys = "0.4.1" 18 | libc = "0.2.16" 19 | rand = "0.3.14" 20 | regex = "0.1.77" 21 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 woxtu 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rust-httpstat 2 | 3 | ![screenshot](screenshot.png) 4 | 5 | curl statistics made simple. Rust implementation of [httpstat](https://github.com/reorx/httpstat). 6 | 7 | ## Installation 8 | 9 | ```bash 10 | $ cargo install --git https://github.com/woxtu/rust-httpstat 11 | ``` 12 | 13 | ## Usage 14 | as command-line tool: 15 | ``` 16 | $ httpstat https://www.rust-lang.org/ 17 | ``` 18 | 19 | as package: 20 | ``` 21 | extern crate httpstat; 22 | 23 | fn main() { 24 | let url = "https://www.rust-lang.org/"; 25 | match httpstat::request(url) { 26 | Ok((_resp, time)) => println!("{:?}", time), 27 | Err(e) => println!("fail httpstat request: {}", e), 28 | } 29 | } 30 | ``` 31 | 32 | 33 | ## License 34 | 35 | Copyright (c) 2016 woxtu 36 | 37 | Licensed under the MIT license. 38 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woxtu/rust-httpstat/a88e18467617dc09d9e74c400760ef30c0995ba7/screenshot.png -------------------------------------------------------------------------------- /src/curl/easy.rs: -------------------------------------------------------------------------------- 1 | use std::{error, mem, slice, str}; 2 | use std::cell::RefCell; 3 | use std::ffi::CString; 4 | use std::rc::{Rc, Weak}; 5 | use curl_sys::*; 6 | use libc::*; 7 | 8 | use super::{Response, Time}; 9 | 10 | type Error = Box; 11 | 12 | pub struct Easy { 13 | handle: *mut CURL, 14 | header_buffer: Rc>, 15 | body_buffer: Rc>, 16 | error_buffer: Vec, 17 | } 18 | 19 | pub enum HttpVersion { 20 | V10 = CURL_HTTP_VERSION_1_0 as isize, 21 | V11 = CURL_HTTP_VERSION_1_1 as isize, 22 | V2 = CURL_HTTP_VERSION_2_0 as isize, 23 | V2TLS = CURL_HTTP_VERSION_2TLS as isize, 24 | V2PriorKnowledge = CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE as isize, 25 | } 26 | 27 | impl Easy { 28 | pub fn new() -> Result { 29 | unsafe { 30 | match curl_easy_init().as_mut() { 31 | Some(handle) => { 32 | let header_buffer = Rc::new(RefCell::new(String::new())); 33 | curl_easy_setopt(handle, CURLOPT_HEADERFUNCTION, write_function as *mut c_void); 34 | curl_easy_setopt(handle, CURLOPT_HEADERDATA, Rc::downgrade(&header_buffer)); 35 | 36 | let body_buffer = Rc::new(RefCell::new(String::new())); 37 | curl_easy_setopt(handle, CURLOPT_WRITEFUNCTION, write_function as *mut c_void); 38 | curl_easy_setopt(handle, CURLOPT_WRITEDATA, Rc::downgrade(&body_buffer)); 39 | 40 | let error_buffer = vec!(0; CURL_ERROR_SIZE); 41 | curl_easy_setopt(handle, CURLOPT_ERRORBUFFER, error_buffer.as_ptr()); 42 | 43 | Ok(Easy { 44 | handle: handle, 45 | header_buffer: header_buffer, 46 | body_buffer: body_buffer, 47 | error_buffer: error_buffer, 48 | }) 49 | }, 50 | None => Err(From::from("Failed to create an easy handle")), 51 | } 52 | } 53 | } 54 | 55 | pub fn set_url(&self, url: &str) -> Result<(), Error> { 56 | let url = try!(CString::new(url)); 57 | self.set_option(CURLOPT_URL, url.as_ptr()) 58 | } 59 | 60 | pub fn http_version(&self, version: HttpVersion) -> Result<(), Error> { 61 | self.set_option(CURLOPT_HTTP_VERSION, version) 62 | } 63 | 64 | pub fn perform(&self) -> Result { 65 | self.get_result(unsafe { curl_easy_perform(self.handle) }) 66 | .map(|_| Response { 67 | header: self.header_buffer.borrow().trim().to_string(), 68 | body: self.body_buffer.borrow().trim().to_string(), 69 | }) 70 | } 71 | 72 | pub fn get_time(&self) -> Result { 73 | Ok(Time { 74 | namelookup: try!(self.get_info(CURLINFO_NAMELOOKUP_TIME)), 75 | connect: try!(self.get_info(CURLINFO_CONNECT_TIME)), 76 | pretransfer: try!(self.get_info(CURLINFO_PRETRANSFER_TIME)), 77 | starttransfer: try!(self.get_info(CURLINFO_STARTTRANSFER_TIME)), 78 | total: try!(self.get_info(CURLINFO_TOTAL_TIME)), 79 | }) 80 | } 81 | 82 | fn get_result(&self, code: CURLcode) -> Result<(), Error> { 83 | match code { 84 | CURLE_OK => Ok(()), 85 | _ => Err(From::from( 86 | str::from_utf8(&self.error_buffer).map(|s| s.trim_matches('\u{0}')).unwrap_or("Unknown error"))), 87 | } 88 | } 89 | 90 | fn set_option(&self, option: CURLoption, value: T) -> Result<(), Error> { 91 | self.get_result(unsafe { curl_easy_setopt(self.handle, option, value) }) 92 | } 93 | 94 | fn get_info(&self, info: CURLINFO) -> Result { 95 | let mut result = unsafe { mem::zeroed() }; 96 | self.get_result(unsafe { curl_easy_getinfo(self.handle, info, &mut result) }) 97 | .map(|_| result) 98 | } 99 | } 100 | 101 | impl Drop for Easy { 102 | fn drop(&mut self) { 103 | unsafe { 104 | curl_easy_cleanup(self.handle) 105 | } 106 | } 107 | } 108 | 109 | unsafe extern fn write_function(ptr: *mut c_char, size: size_t, nitems: size_t, userdata: *mut c_void) -> size_t { 110 | let buffer = mem::transmute::<_, Weak>>(userdata); 111 | 112 | if let Some(buffer) = buffer.upgrade() { 113 | if let Ok(line) = str::from_utf8(slice::from_raw_parts(ptr as *mut _, size * nitems)) { 114 | buffer.borrow_mut().push_str(line); 115 | } 116 | } 117 | 118 | size * nitems 119 | } 120 | -------------------------------------------------------------------------------- /src/curl/mod.rs: -------------------------------------------------------------------------------- 1 | extern crate curl_sys; 2 | extern crate libc; 3 | 4 | pub mod easy; 5 | 6 | pub struct Response { 7 | pub header: String, 8 | pub body: String, 9 | } 10 | 11 | #[derive(Debug)] 12 | pub struct Time { 13 | pub namelookup: f64, 14 | pub connect: f64, 15 | pub pretransfer: f64, 16 | pub starttransfer: f64, 17 | pub total: f64, 18 | } 19 | 20 | pub use self::easy::Easy; 21 | -------------------------------------------------------------------------------- /src/httpstat/app.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::error::Error; 3 | use std::fs::File; 4 | use std::io::Write; 5 | use clap::ArgMatches; 6 | use rand; 7 | use rand::Rng; 8 | 9 | use super::{Body, Header, Time}; 10 | use ::curl::easy::HttpVersion; 11 | 12 | pub fn run(args: &ArgMatches) -> Result<(), Box> { 13 | let url = args.value_of("url").unwrap(); 14 | let is_http2 = args.is_present("http2"); 15 | 16 | let client = try!(::httpstat::curl::easy::Easy::new()); 17 | try!(client.set_url(url)); 18 | if is_http2 { 19 | try!(client.http_version(HttpVersion::V2)); 20 | } 21 | 22 | let response = try!(client.perform()); 23 | 24 | // print header 25 | println!("{}", Header(response.header)); 26 | 27 | // print body 28 | let mut tempfile_path = env::temp_dir(); 29 | tempfile_path.set_file_name(rand::thread_rng().gen_ascii_chars().take(20).collect::()); 30 | let mut tempfile = try!(File::create(&tempfile_path)); 31 | try!(tempfile.write_all(response.body.as_bytes())); 32 | println!("{}", Body(tempfile_path.to_string_lossy().into_owned())); 33 | 34 | // print status 35 | let time = try!(client.get_time()); 36 | println!("{}", Time(url.starts_with("https"), time)); 37 | 38 | Ok(()) 39 | } 40 | -------------------------------------------------------------------------------- /src/httpstat/format.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use regex::Regex; 3 | 4 | use super::{Body, Header, Time}; 5 | 6 | const RESET: &'static str = "\u{1b}[0m"; 7 | const BOLD: &'static str = "\u{1b}[1m"; 8 | const GREEN: &'static str = "\u{1b}[32m"; 9 | const CYAN: &'static str = "\u{1b}[36m"; 10 | 11 | impl fmt::Display for Header { 12 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 13 | let mut buffer = Vec::new(); 14 | 15 | for (index, line) in self.0.lines().enumerate() { 16 | match index { 17 | 0 => { 18 | let re = Regex::new("(.+?)/(.*)").unwrap(); 19 | buffer.push(String::new()); 20 | buffer.push(re.replace(line, format!("{}$1{}/{}$2{}", GREEN, RESET, CYAN, RESET).as_str())); 21 | }, 22 | _ => { 23 | let re = Regex::new("(.+?):(.*)").unwrap(); 24 | buffer.push(re.replace(line, format!("$1:{}$2{}", CYAN, RESET).as_str())); 25 | }, 26 | } 27 | } 28 | 29 | writeln!(f, "{}", buffer.join("\n")) 30 | } 31 | } 32 | 33 | impl fmt::Display for Body { 34 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 35 | writeln!(f, "{}Body{} stored in: {}", GREEN, RESET, self.0) 36 | } 37 | } 38 | 39 | fn format_a(x: f64) -> String { 40 | format!("{}{:^7}{}", CYAN, format!("{:.0}ms", x * 1000.0), RESET) 41 | } 42 | 43 | fn format_b(x: f64) -> String { 44 | format!("{}{:<7}{}", CYAN, format!("{:.0}ms", x * 1000.0), RESET) 45 | } 46 | 47 | impl fmt::Display for Time { 48 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 49 | let Time(is_https, ref time) = *self; 50 | 51 | if is_https { 52 | writeln!(f, 53 | " {}DNS Lookup TCP Connection SSL Handshake Server Processing Content Transfer{} 54 | [ {a0000} | {a0001} | {a0002} | {a0003} | {a0004} ] 55 | | | | | | 56 | namelookup:{b0000} | | | | 57 | connect:{b0001} | | | 58 | pretransfer:{b0002} | | 59 | starttransfer:{b0003} | 60 | total:{b0004}", 61 | BOLD, RESET, 62 | a0000 = format_a(time.namelookup), 63 | a0001 = format_a(time.connect - time.namelookup), 64 | a0002 = format_a(time.pretransfer - time.connect), 65 | a0003 = format_a(time.starttransfer - time.pretransfer), 66 | a0004 = format_a(time.total - time.starttransfer), 67 | b0000 = format_b(time.namelookup), 68 | b0001 = format_b(time.connect), 69 | b0002 = format_b(time.pretransfer), 70 | b0003 = format_b(time.starttransfer), 71 | b0004 = format_b(time.total)) 72 | } else { 73 | writeln!(f, 74 | " {}DNS Lookup TCP Connection Server Processing Content Transfer{} 75 | [ {a0000} | {a0001} | {a0003} | {a0004} ] 76 | | | | | 77 | namelookup:{b0000} | | | 78 | connect:{b0001} | | 79 | starttransfer:{b0003} | 80 | total:{b0004}", 81 | BOLD, RESET, 82 | a0000 = format_a(time.namelookup), 83 | a0001 = format_a(time.connect - time.namelookup), 84 | a0003 = format_a(time.starttransfer - time.pretransfer), 85 | a0004 = format_a(time.total - time.starttransfer), 86 | b0000 = format_b(time.namelookup), 87 | b0001 = format_b(time.connect), 88 | b0003 = format_b(time.starttransfer), 89 | b0004 = format_b(time.total)) 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/httpstat/mod.rs: -------------------------------------------------------------------------------- 1 | use curl; 2 | 3 | pub mod app; 4 | 5 | mod format; 6 | 7 | pub struct Header(String); 8 | pub struct Body(String); 9 | pub struct Time(bool, curl::Time); 10 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate regex; 2 | extern crate curl_sys; 3 | extern crate libc; 4 | extern crate clap; 5 | extern crate rand; 6 | 7 | pub mod httpstat; 8 | pub mod curl; 9 | 10 | pub use curl::easy::HttpVersion; 11 | 12 | pub fn request(url: &str) -> Result<(curl::Response, curl::Time), String> { 13 | let client = curl::easy::Easy::new(); 14 | match client { 15 | Err(e) => return Err(format!("{}", e)), 16 | _ => {}, 17 | } 18 | let client = client.unwrap(); 19 | 20 | match client.set_url(url) { 21 | Err(e) => return Err(format!("{}", e)), 22 | _ => {}, 23 | } 24 | 25 | let response = client.perform(); 26 | match response { 27 | Err(e) => return Err(format!("{}", e)), 28 | _ => {}, 29 | } 30 | let response = response.unwrap(); 31 | 32 | let time = client.get_time(); 33 | match time { 34 | Err(e) => return Err(format!("{}", e)), 35 | _ => {} 36 | } 37 | let time = time.unwrap(); 38 | 39 | Ok((response, time)) 40 | } 41 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate clap; 2 | extern crate httpstat; 3 | extern crate rand; 4 | extern crate regex; 5 | 6 | use std::io::prelude::*; 7 | use clap::App; 8 | 9 | fn main() { 10 | let args = App::new("httpstat") 11 | .version(env!("CARGO_PKG_VERSION")) 12 | .about("curl statistics made simple") 13 | .args_from_usage( 14 | " 'URL to work with' 15 | --http2 'use HTTP version 2'" 16 | ) 17 | .get_matches(); 18 | 19 | match httpstat::httpstat::app::run(&args) { 20 | Ok(()) => (), 21 | Err(error) => { 22 | let _ = write!(std::io::stderr(), "{}", error.to_string()); 23 | }, 24 | } 25 | } 26 | --------------------------------------------------------------------------------