libfrizz/
lib.rs

1mod port_details;
2
3use ansi_term::Colour;
4use futures::{lock::Mutex, stream, StreamExt};
5use indicatif::{ProgressBar, ProgressStyle};
6use itertools::Itertools;
7use port_details::*;
8use reqwest::{Body, Client, Method, Request, Url};
9use std::cmp::min;
10use std::fs::File;
11use std::{
12    fs, io,
13    io::{Error, Write},
14    net::{IpAddr, SocketAddr},
15    str::FromStr,
16    sync::Arc,
17    time::Duration,
18    time::Instant,
19};
20use tokio::{
21    io::{AsyncReadExt, AsyncWriteExt, Interest},
22    net::TcpStream,
23    net::UdpSocket,
24};
25use tokio_util::codec::{BytesCodec, FramedRead};
26
27#[derive(Copy, Clone, PartialEq, Eq)]
28pub enum TransportLayerProtocol {
29    Udp,
30    Tcp,
31    Sctp,
32    None,
33}
34
35pub struct FizzResult {
36    pub status_code: String,
37    pub headers: String,
38    pub body: String,
39}
40
41pub struct ExecRequest {
42    pub url: String,
43    pub user_agent: String,
44    pub verbose: bool,
45    pub disable_cert_validation: bool,
46    pub disable_hostname_validation: bool,
47    pub post_data: String,
48    pub http_method: Method,
49    pub progress_bar: bool,
50}
51
52pub async fn upload_file(
53    url: String,
54    path: String,
55    client: Client,
56    http_method: Method,
57) -> Result<FizzResult, reqwest::Error> {
58    let file = tokio::fs::File::open(path).await.unwrap();
59    let total_size = file.metadata().await.unwrap().len();
60    let target_url = url.clone();
61
62    // Indicatif setup
63    let pb = ProgressBar::new(total_size);
64    pb.set_style(ProgressStyle::default_bar()
65        .template("{msg}\n{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})")
66        .progress_chars("#>-"));
67    pb.set_message(format!("Posting {}", url));
68
69    let mut uploaded = 0;
70
71    let mut reader_stream = FramedRead::new(file, BytesCodec::new());
72    let async_stream = async_stream::stream! {
73        while let Some(chunk) = reader_stream.next().await {
74            if let Ok(chunk) = &chunk {
75                let new = min(uploaded + (chunk.len() as u64), total_size);
76                uploaded = new;
77                pb.set_position(new);
78            }
79            yield chunk;
80        }
81        pb.finish_with_message(format!("Upload finished."));
82    };
83    let response = match http_method {
84        Method::POST => {
85            client
86                .post(target_url)
87                .body(Body::wrap_stream(async_stream))
88                .send()
89                .await
90        }
91        Method::PUT => {
92            client
93                .put(target_url)
94                .body(Body::wrap_stream(async_stream))
95                .send()
96                .await
97        }
98        _ => panic!("Can not upload the file with httpMethod:{}", http_method),
99    }
100    .unwrap();
101
102    let headers = response.headers().clone();
103    let status_code = response.status().to_string();
104    let result_text = response.text().await?;
105
106    return Ok(FizzResult {
107        status_code,
108        headers: format!("Headers:\n{:#?}", headers),
109        body: result_text,
110    });
111}
112
113pub async fn execute_request(exec: ExecRequest) -> Result<FizzResult, reqwest::Error> {
114    let client = reqwest::Client::builder()
115        .user_agent(exec.user_agent)
116        .danger_accept_invalid_certs(exec.disable_cert_validation)
117        .danger_accept_invalid_hostnames(exec.disable_hostname_validation)
118        .connection_verbose(exec.verbose)
119        .build()?;
120
121    let mut req = Request::new(exec.http_method.clone(), Url::from_str(&exec.url).unwrap());
122
123    if !exec.post_data.is_empty() {
124        let mut postdata = exec.post_data;
125        if postdata.starts_with('@') {
126            let file_name = postdata.split('@').last().unwrap();
127            log::info!("File opening for read:{}", file_name);
128            let file_size = fs::metadata(file_name).unwrap().len();
129
130            if file_size > 1_000_000 {
131                // if file size bigger then 1mb
132                return upload_file(
133                    exec.url,
134                    String::from(file_name).clone(),
135                    client,
136                    exec.http_method,
137                )
138                .await;
139            }
140
141            let contents = fs::read(file_name).expect("Something went wrong reading the file");
142            unsafe {
143                postdata = String::from_utf8_unchecked(contents);
144            }
145        }
146        req.body_mut().replace(Body::from(postdata));
147    }
148
149    let res = client.execute(req).await.unwrap();
150    let headers = res.headers().clone();
151    let status_code = res.status().to_string();
152    let content_length = res.content_length().unwrap_or(0);
153
154    if content_length > 1_000_000 || exec.progress_bar {
155        // if response content bigger then 1MB we download it with progress bar
156        let total_size = content_length;
157        let pb = ProgressBar::new(total_size);
158        pb.set_style(ProgressStyle::default_bar()
159            .template("{msg}\n{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})")
160            .progress_chars("#>-"));
161        pb.set_message(format!("Executing {}", exec.url));
162
163        let mut path = "frizz.out.file";
164        if exec.url.split('/').last().unwrap().chars().count() > 1 {
165            path = exec.url.split('/').last().unwrap();
166        };
167        // download chunks
168        let mut file = File::create(path).unwrap();
169        let mut downloaded: u64 = 0;
170        let mut stream = res.bytes_stream();
171
172        while let Some(item) = stream.next().await {
173            let chunk = item?;
174            file.write_all(&chunk).unwrap();
175            let new = min(downloaded + (chunk.len() as u64), total_size);
176            downloaded = new;
177            pb.set_position(new);
178        }
179
180        pb.finish_with_message(format!("Downloaded {} to {}", exec.url, path));
181
182        return Ok(FizzResult {
183            status_code,
184            headers: format!("Headers:\n{:#?}", headers),
185            body: format!("written to ./{}", path),
186        });
187    }
188
189    Ok(FizzResult {
190        status_code,
191        headers: format!("Headers:\n{:#?}", headers),
192        body: res.text().await?,
193    })
194}
195
196fn get_ports(
197    min_port: u16,
198    max_port: u16,
199    tl_protocol: TransportLayerProtocol,
200) -> (Box<dyn Iterator<Item = u16>>, u16) {
201    if min_port != max_port {
202        // ports present we scan only that area.
203        (Box::new(min_port..=max_port), max_port - min_port)
204    } else if tl_protocol != TransportLayerProtocol::None {
205        // if no port present and protocol given, scan common only for that protocol
206        (
207            Box::new(get_most_common_ports(tl_protocol).into_iter()),
208            get_most_common_ports(tl_protocol).len() as u16,
209        )
210    } else {
211        // no port given nor layer protocol, scan all common ports
212        let mut all_ports = get_most_common_ports(TransportLayerProtocol::Sctp);
213        all_ports.append(&mut get_most_common_ports(TransportLayerProtocol::Tcp));
214        all_ports.append(&mut get_most_common_ports(TransportLayerProtocol::Udp));
215        all_ports = all_ports.into_iter().unique().collect();
216
217        let count = all_ports.len();
218        (Box::new(all_ports.into_iter()), count as u16)
219    }
220}
221
222pub async fn scan(
223    target: IpAddr,
224    concurrency: usize,
225    timeout: u64,
226    min_port: u16,
227    max_port: u16,
228    tl_protocol: TransportLayerProtocol,
229    mut out_writer: Box<dyn Write>,
230) {
231    let (port_box, progress_size) = get_ports(min_port, max_port, tl_protocol);
232    let ports = stream::iter(port_box);
233    let output_values = Arc::new(Mutex::new(Vec::new()));
234    let before = Instant::now();
235
236    let pb = ProgressBar::new(progress_size.into());
237    pb.set_style(ProgressStyle::default_bar()
238        .template("{msg}\n{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {pos:>}/{len}  ({percent}%, {eta})")
239        .progress_chars("##-"));
240
241    pb.set_message(format!(
242        "Scanning ports for {} min-max ports:{},{}",
243        target, min_port, max_port
244    ));
245
246    ports
247        .for_each_concurrent(concurrency, |port| {
248            let output_values = output_values.clone();
249            pb.inc(1);
250            async move {
251                let result = scan_port(target, port, timeout, tl_protocol).await;
252                if result > 0 {
253                    output_values.lock().await.push(result);
254                }
255            }
256        })
257        .await;
258
259    pb.finish();
260    let d = port_details::get_details();
261    let details = d.lock().unwrap();
262    out_writer
263        .write(
264            Colour::Green
265                .paint("Port\tService\t\tProtocol\n".to_string())
266                .as_bytes(),
267        )
268        .ok();
269    for i in output_values.lock().await.iter() {
270        let d = match details.get(i) {
271            Some(detail) => detail,
272            None => "",
273        };
274        out_writer
275            .write(Colour::Blue.paint(format!("{:?}\t{:?}\n", i, d)).as_bytes())
276            .ok();
277    }
278
279    println!("Elapsed time to scan ports: {:.2?}", before.elapsed());
280}
281
282async fn scan_port(
283    target: IpAddr,
284    port: u16,
285    timeout: u64,
286    protocol: TransportLayerProtocol,
287) -> u16 {
288    let timeout = Duration::from_secs(timeout);
289    let socket_address = SocketAddr::new(target, port);
290
291    return match protocol {
292        TransportLayerProtocol::None
293        | TransportLayerProtocol::Sctp
294        | TransportLayerProtocol::Tcp => {
295            if let Ok(Ok(_)) =
296                tokio::time::timeout(timeout, TcpStream::connect(&socket_address)).await
297            {
298                return port;
299            }
300            0
301        }
302        TransportLayerProtocol::Udp => {
303            if let Ok(Ok(_)) = tokio::time::timeout(
304                timeout,
305                UdpSocket::bind("127.0.0.1:0")
306                    .await
307                    .expect("could not bind to address")
308                    .connect(&socket_address),
309            )
310            .await
311            {
312                return port;
313            }
314            0
315        }
316    };
317}
318
319pub async fn open_socket_target(target: &str) -> Result<(), Error> {
320    log::info!("Socket connection");
321
322    let t_url = Url::parse(target).unwrap();
323    let addrs = t_url.socket_addrs(|| None).unwrap();
324    let mut stream = TcpStream::connect(&*addrs).await?;
325
326    loop {
327        let ready = stream
328            .ready(Interest::READABLE | Interest::WRITABLE)
329            .await?;
330        let mut data = vec![];
331        if ready.is_writable() {
332            let prompt = format!("{}{:?}{}", "Connected ", stream.peer_addr(), ">");
333            print!("{}", Colour::Green.paint(prompt));
334            io::stdout().flush().ok();
335
336            let mut input = String::new();
337            io::stdin().read_line(&mut input).ok();
338            if input.trim().eq_ignore_ascii_case("exit") {
339                return Ok(());
340            }
341            stream.write_all(input.as_bytes()).await?;
342            stream.read_to_end(&mut data).await?;
343            println!("Response:{:?}", String::from_utf8(data));
344        }
345    }
346}
347
348#[cfg(test)]
349#[allow(non_snake_case)]
350mod tests {
351    use ansi_term::Colour;
352    use std::path::Path;
353
354    use super::*;
355    use rstest::rstest;
356
357    #[tokio::test]
358    async fn test_download_upload_big_file() {
359        println!("download is starting.");
360        execute_request(ExecRequest {
361            url: "https://github.com/ozkanpakdil/rust-examples/files/7689196/s.zip".to_string(),
362            user_agent: "frizz".to_string(),
363            verbose: true,
364            disable_cert_validation: true,
365            disable_hostname_validation: true,
366            post_data: "".to_string(),
367            http_method: Method::GET,
368            progress_bar: true,
369        })
370        .await
371        .unwrap();
372        println!("download finished.");
373        if !Path::new("s.zip").is_file() {
374            panic!("s.zip not found, fail.");
375        }
376
377        execute_request(ExecRequest {
378            url: "https://bashupload.com/s.zip".to_string(),
379            user_agent: "frizz".to_string(),
380            verbose: false,
381            disable_cert_validation: true,
382            disable_hostname_validation: true,
383            post_data: "".to_string(),
384            http_method: Method::POST,
385            progress_bar: true,
386        })
387        .await
388        .unwrap();
389        println!("upload finished.");
390        fs::remove_file("s.zip").unwrap();
391    }
392
393    #[tokio::test]
394    async fn test_get_header() {
395        let res = execute_request(ExecRequest {
396            url: "http://httpbin.org/get".to_string(),
397            user_agent: "rusty".to_string(),
398            verbose: true,
399            disable_cert_validation: true,
400            disable_hostname_validation: true,
401            post_data: "".to_string(),
402            http_method: Method::GET,
403            progress_bar: true,
404        })
405        .await
406        .unwrap();
407        println!("{}", Colour::Red.paint(res.status_code));
408        println!("{}", Colour::Green.paint(res.headers));
409        println!("{}", Colour::Blue.paint(res.body));
410        fs::remove_file("./get").unwrap();
411    }
412
413    #[tokio::test]
414    #[should_panic]
415    async fn test_get_header_error() {
416        let res = execute_request(ExecRequest {
417            url: "htasxatp://httpbin.org/get".to_string(),
418            user_agent: "rusty".to_string(),
419            verbose: true,
420            disable_cert_validation: true,
421            disable_hostname_validation: true,
422            post_data: "".to_string(),
423            http_method: Method::GET,
424            progress_bar: true,
425        })
426        .await
427        .unwrap();
428        println!("{}", Colour::Red.paint(res.status_code));
429        println!("{}", Colour::Green.paint(res.headers));
430        println!("{}", Colour::Blue.paint(res.body));
431    }
432
433    #[rstest]
434    #[case(0, 10, TransportLayerProtocol::Tcp)]
435    #[case(0, 21, TransportLayerProtocol::Udp)]
436    #[case(0, 32, TransportLayerProtocol::Sctp)]
437    #[case(10, 100, TransportLayerProtocol::None)]
438    fn test_get_ports(
439        #[case] min_p: u16,
440        #[case] max_p: u16,
441        #[case] proto: TransportLayerProtocol,
442    ) {
443        let (_port_box, progress_size) = get_ports(min_p, max_p, proto);
444        match proto {
445            _ => assert_eq!(progress_size, max_p - min_p),
446        }
447    }
448}