diff --git a/Cargo.lock b/Cargo.lock index 31d07e9..1c51888 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -746,7 +746,7 @@ dependencies = [ [[package]] name = "rustbuster" -version = "1.1.0" +version = "1.2.0" dependencies = [ "chrono 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -759,6 +759,7 @@ dependencies = [ "pretty_env_logger 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.91 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)", + "terminal_size 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -904,6 +905,15 @@ dependencies = [ "wincolor 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "terminal_size" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.54 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "termion" version = "1.5.2" @@ -1271,6 +1281,7 @@ dependencies = [ "checksum syn 0.15.34 (registry+https://github.com/rust-lang/crates.io-index)" = "a1393e4a97a19c01e900df2aec855a29f71cf02c402e2f443b8d2747c25c5dbe" "checksum tempfile 3.0.7 (registry+https://github.com/rust-lang/crates.io-index)" = "b86c784c88d98c801132806dadd3819ed29d8600836c4088e855cdf3e178ed8a" "checksum termcolor 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "4096add70612622289f2fdcdbd5086dc81c1e2675e6ae58d6c4f62a16c6d7f2f" +"checksum terminal_size 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "023345d35850b69849741bd9a5432aa35290e3d8eb76af8717026f270d1cf133" "checksum termion 1.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dde0593aeb8d47accea5392b39350015b5eccb12c0d98044d856983d89548dea" "checksum termios 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "72b620c5ea021d75a735c943269bb07d30c9b77d6ac6b236bc8b5c496ef05625" "checksum textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" diff --git a/Cargo.toml b/Cargo.toml index 4babad2..47a30df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustbuster" -version = "1.1.0" +version = "1.2.0" authors = ["phra ", "ps1dr3x "] edition = "2018" @@ -13,10 +13,11 @@ hyper-tls = "^0.3.2" native-tls = "^0.2.3" serde = { version = "^1.0.91", features = ["derive"] } serde_json = "^1.0.39" -indicatif = "0.11.0" -chrono = "0.4.6" +indicatif = "^0.11.0" +chrono = "^0.4.6" +terminal_size = "^0.1.8" [dependencies.clap] -version = "2.33" +version = "^2.33" default-features = false features = [ "suggestions", "color" ] diff --git a/README.md b/README.md index 831aed6..aaf6074 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,17 @@ DirBuster for Rust ## Usage +There are three modules currently implemented: + +1. Dirbuster (default) +`rustbuster -m dir -u http://localhost:3000/ -w examples/wordlist -e php` + +1. Dnsbuster +`rustbuster -m dns -u google.com -w examples/wordlist` + +1. Vhostbuster +`rustbuster -m vhost -u http://localhost:3000/ -w examples/wordlist -d test.local -x "Hello"` + ```shell _ _ _ _ _ _ _ _ _ _ @@ -20,7 +31,7 @@ DirBuster for Rust / / / \ \ \/ / /____\/ /\ \/___/ / /_/ / / / /__________/ / /____\/ /\ \/___/ / /_/ / / / /_______/ / / \ \ \ \/_/ \_\/\/_________/ \_____\/ \_\/ \/_____________\/_________/ \_____\/ \_\/ \/__________\/_/ \_\/ -~ rustbuster v. 1.0.0 ~ by phra & ps1dr3x ~ +~ rustbuster v. 1.2.0 ~ by phra & ps1dr3x ~ USAGE: rustbuster [FLAGS] [OPTIONS] --url --wordlist @@ -36,18 +47,19 @@ FLAGS: -v, --verbose Sets the level of verbosity OPTIONS: + -d, --domain Uses the specified domain -e, --extensions Sets the extensions [default: ] -b, --http-body Uses the specified HTTP method [default: ] -H, --http-header ... Appends the specified HTTP header -X, --http-method Uses the specified HTTP method [default: GET] -S, --ignore-status-codes Sets the list of status codes to ignore [default: 404] + -x, --ignore-string ... Ignores results with specified string in vhost mode -s, --include-status-codes Sets the list of status codes to include [default: ] - -m, --mode Sets the mode of operation (dir, dns, vhost) [default: dir] + -m, --mode Sets the mode of operation (dir, dns, fuzz) [default: dir] -o, --output Saves the results in the specified file [default: ] -t, --threads Sets the amount of concurrent requests [default: 10] -u, --url Sets the target URL -a, --user-agent Uses the specified User-Agent [default: rustbuster] -w, --wordlist Sets the wordlist - ``` diff --git a/examples/.gitignore b/examples/.gitignore new file mode 100644 index 0000000..d5f19d8 --- /dev/null +++ b/examples/.gitignore @@ -0,0 +1,2 @@ +node_modules +package-lock.json diff --git a/examples/vhosts-server.js b/examples/vhosts-server.js new file mode 100644 index 0000000..63b8a8d --- /dev/null +++ b/examples/vhosts-server.js @@ -0,0 +1,18 @@ +var express = require("express"); +var app = express(); + +var DEFAULT = "Hello World!"; +var vhosts = ["1.test.local", "10.test.local", "15.test.local"]; + +app.all("/*", function (req, res) { + if (vhosts.some(x => x === req.hostname)) { + return res.send(req.hostname); + } + + res.send("Hello World!"); +}); + +app.listen(3000, function () { + console.log("Example app listening on port 3000!"); +}); + diff --git a/examples/wordlist b/examples/wordlist index c14f744..fbe083a 100644 --- a/examples/wordlist +++ b/examples/wordlist @@ -21,3 +21,6 @@ 19 20 www +src +target +LICENSE diff --git a/src/banner.rs b/src/banner.rs index d7f1c99..cef590d 100644 --- a/src/banner.rs +++ b/src/banner.rs @@ -15,8 +15,15 @@ pub fn generate() -> String { / / / \\ \\ \\/ / /____\\/ /\\ \\/___/ / /_/ / / / /__________/ / /____\\/ /\\ \\/___/ / /_/ / / / /_______/ / / \\ \\ \\ \\/_/ \\_\\/\\/_________/ \\_____\\/ \\_\\/ \\/_____________\\/_________/ \\_____\\/ \\_\\/ \\/__________\\/_/ \\_\\/ -~ rustbuster v. {} ~ by phra & ps1dr3x ~ -", VERSION) +") +} + +pub fn copyright() -> String { + format!( + "~ rustbuster v{} ~ by phra & ps1dr3x ~ +", + VERSION + ) } pub fn configuration(mode: &str, url: &str, threads: &str, wordlist: &str) -> String { diff --git a/src/dirbuster/mod.rs b/src/dirbuster/mod.rs index 8e99212..2354bee 100644 --- a/src/dirbuster/mod.rs +++ b/src/dirbuster/mod.rs @@ -44,7 +44,8 @@ fn make_request_future( request_builder.header(header_tuple.0.as_str(), header_tuple.1.as_str()); } - let request = request_builder.header("User-Agent", &config.user_agent[..]) + let request = request_builder + .header("User-Agent", &config.user_agent[..]) .method(&config.http_method[..]) .uri(&url) .header("Host", url.host().unwrap()) @@ -57,7 +58,14 @@ fn make_request_future( let status = res.status(); target.status = status.to_string(); if status.is_redirection() { - target.extra = Some(res.headers().get("Location").unwrap().to_str().unwrap().to_owned()); + target.extra = Some( + res.headers() + .get("Location") + .unwrap() + .to_str() + .unwrap() + .to_owned(), + ); } tx.send(target).unwrap(); diff --git a/src/dirbuster/utils.rs b/src/dirbuster/utils.rs index 0e2600d..61be4a9 100644 --- a/src/dirbuster/utils.rs +++ b/src/dirbuster/utils.rs @@ -1,7 +1,4 @@ -use std::{ - fs, fs::File, path::Path, str, - io::Write -}; +use std::{fs, fs::File, io::Write, path::Path, str}; use super::result_processor::SingleDirScanResult; @@ -13,7 +10,8 @@ pub fn build_urls( ) -> Vec { debug!("building urls"); let mut urls: Vec = Vec::new(); - let wordlist = fs::read_to_string(wordlist_path).expect("Something went wrong reading the wordlist file"); + let wordlist = + fs::read_to_string(wordlist_path).expect("Something went wrong reading the wordlist file"); let urls_iter = wordlist .lines() .filter(|word| !word.starts_with('#') && !word.starts_with(' ')) @@ -94,6 +92,6 @@ pub fn save_dir_results(path: &str, results: &Vec) { pub fn split_http_headers(header: &str) -> (String, String) { let index = header.find(':').unwrap_or(0); let header_name = header[..index].to_owned(); - let header_value = header[index+2..].to_owned(); + let header_value = header[index + 2..].to_owned(); (header_name, header_value) } diff --git a/src/dnsbuster/mod.rs b/src/dnsbuster/mod.rs index dd2f855..ba6c35c 100644 --- a/src/dnsbuster/mod.rs +++ b/src/dnsbuster/mod.rs @@ -1,7 +1,7 @@ use futures::{future, Future, Stream}; use hyper::rt; -use std::{sync::mpsc::Sender, net::ToSocketAddrs}; +use std::{net::ToSocketAddrs, sync::mpsc::Sender}; pub mod result_processor; pub mod utils; @@ -10,12 +10,12 @@ use result_processor::SingleDnsScanResult; #[derive(Debug, Clone)] pub struct DnsConfig { - pub n_threads: usize + pub n_threads: usize, } fn make_request_future( tx: Sender, - domain: String + domain: String, ) -> impl Future { future::lazy(move || { match domain.to_socket_addrs() { diff --git a/src/dnsbuster/utils.rs b/src/dnsbuster/utils.rs index 1c83c44..a162931 100644 --- a/src/dnsbuster/utils.rs +++ b/src/dnsbuster/utils.rs @@ -1,10 +1,11 @@ -use std::{fs, path, io::Write}; +use std::{fs, io::Write, path}; use super::result_processor::SingleDnsScanResult; pub fn build_domains(wordlist_path: &str, url: &str) -> Vec { debug!("building urls"); - fs::read_to_string(wordlist_path).expect("Something went wrong reading the wordlist file") + fs::read_to_string(wordlist_path) + .expect("Something went wrong reading the wordlist file") .lines() .filter(|word| !word.starts_with('#') && !word.starts_with(' ')) .map(|word| format!("{}.{}:80", word, url)) diff --git a/src/main.rs b/src/main.rs index 1f63765..5368fa4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,13 @@ -#[macro_use] extern crate log; -#[macro_use] extern crate clap; +#[macro_use] +extern crate log; +#[macro_use] +extern crate clap; use clap::{App, Arg}; use indicatif::{ProgressBar, ProgressStyle}; +use terminal_size::{terminal_size, Height, Width}; -use std::{ - str::FromStr, sync::mpsc::channel, - time::SystemTime, thread -}; +use std::{str::FromStr, sync::mpsc::channel, thread, time::SystemTime}; mod banner; mod dirbuster; @@ -16,18 +16,18 @@ mod vhostbuster; use dirbuster::{ result_processor::{ResultProcessorConfig, ScanResult, SingleDirScanResult}, - DirConfig, utils::*, + DirConfig, }; use dnsbuster::{ - result_processor::{SingleDnsScanResult, DnsScanResult}, - DnsConfig, + result_processor::{DnsScanResult, SingleDnsScanResult}, utils::*, + DnsConfig, }; use vhostbuster::{ result_processor::{SingleVhostScanResult, VhostScanResult}, - VhostConfig, utils::*, + VhostConfig, }; fn main() { @@ -109,7 +109,7 @@ fn main() { .help("Sets the list of status codes to include") .short("s") .default_value("") - .use_delimiter(true) + .use_delimiter(true), ) .arg( Arg::with_name("ignore-status-codes") @@ -117,7 +117,7 @@ fn main() { .help("Sets the list of status codes to ignore") .short("S") .default_value("404") - .use_delimiter(true) + .use_delimiter(true), ) .arg( Arg::with_name("output") @@ -125,12 +125,12 @@ fn main() { .help("Saves the results in the specified file") .short("o") .default_value("") - .takes_value(true) + .takes_value(true), ) .arg( Arg::with_name("no-progress-bar") .long("no-progress-bar") - .help("Disables the progress bar") + .help("Disables the progress bar"), ) .arg( Arg::with_name("http-method") @@ -138,7 +138,7 @@ fn main() { .help("Uses the specified HTTP method") .short("X") .default_value("GET") - .takes_value(true) + .takes_value(true), ) .arg( Arg::with_name("http-body") @@ -146,7 +146,7 @@ fn main() { .help("Uses the specified HTTP method") .short("b") .default_value("") - .takes_value(true) + .takes_value(true), ) .arg( Arg::with_name("http-header") @@ -154,7 +154,7 @@ fn main() { .help("Appends the specified HTTP header") .short("H") .multiple(true) - .takes_value(true) + .takes_value(true), ) .arg( Arg::with_name("user-agent") @@ -162,20 +162,20 @@ fn main() { .help("Uses the specified User-Agent") .short("a") .default_value("rustbuster") - .takes_value(true) + .takes_value(true), ) .arg( Arg::with_name("domain") .long("domain") .help("Uses the specified domain") .short("d") - .takes_value(true) + .takes_value(true), ) .arg( Arg::with_name("append-slash") .long("append-slash") .help("Tries to also append / to the base request") - .short("f") + .short("f"), ) .arg( Arg::with_name("ignore-string") @@ -183,7 +183,7 @@ fn main() { .help("Ignores results with specified string in vhost mode") .short("x") .multiple(true) - .takes_value(true) + .takes_value(true), ) .get_matches(); @@ -196,15 +196,24 @@ fn main() { let wordlist_path = matches.value_of("wordlist").unwrap(); let mode = matches.value_of("mode").unwrap(); let ignore_certificate = matches.is_present("ignore-certificate"); - let no_progress_bar = matches.is_present("no-progress-bar"); + let mut no_banner = matches.is_present("no-banner"); + let mut no_progress_bar = matches.is_present("no-progress-bar"); let exit_on_connection_errors = matches.is_present("exit-on-error"); let http_headers: Vec<(String, String)> = if matches.is_present("http-header") { - matches.values_of("http-header").unwrap().map(|h| dirbuster::utils::split_http_headers(h)).collect() + matches + .values_of("http-header") + .unwrap() + .map(|h| dirbuster::utils::split_http_headers(h)) + .collect() } else { Vec::new() }; let ignore_strings: Vec = if matches.is_present("ignore-string") { - matches.values_of("ignore-string").unwrap().map(|h| h.to_owned()).collect() + matches + .values_of("ignore-string") + .unwrap() + .map(|h| h.to_owned()) + .collect() } else { Vec::new() }; @@ -227,7 +236,7 @@ fn main() { } let valid = hyper::StatusCode::from_str(s).is_ok(); if !valid { - error!("Ignoring invalid status code for '-s' param: {}", s); + warn!("Ignoring invalid status code for '-s' param: {}", s); } valid }) @@ -242,7 +251,7 @@ fn main() { } let valid = hyper::StatusCode::from_str(s).is_ok(); if !valid { - error!("Ignoring invalid status code for '-S' param: {}", s); + warn!("Ignoring invalid status code for '-S' param: {}", s); } valid }) @@ -258,6 +267,11 @@ fn main() { Ok(_) => (), } + if std::fs::metadata(wordlist_path).is_err() { + error!("Specified wordlist does not exist: {}", wordlist_path); + return; + } + debug!("Using mode: {:?}", mode); debug!("Using url: {:?}", url); debug!("Using wordlist: {:?}", wordlist_path); @@ -289,20 +303,38 @@ fn main() { 3 | _ => trace!("Don't be crazy"), } - if !matches.is_present("no-banner") { + if let Some((Width(w), Height(h))) = terminal_size() { + trace!("Your terminal is {} cols wide and {} lines tall", w, h); + if w < 122 { + no_banner = true; + } + + if w < 104 { + no_progress_bar = true; + } + } else { + warn!("Unable to get terminal size"); + no_banner = true; + no_progress_bar = true; + } + + if !no_banner { println!("{}", banner::generate()); - println!( - "{}", - banner::configuration( - mode, - url, - matches.value_of("threads").unwrap(), - wordlist_path - ) - ); - println!("{}", banner::starting_time()); } + println!("{}", banner::copyright()); + + println!( + "{}", + banner::configuration( + mode, + url, + matches.value_of("threads").unwrap(), + wordlist_path + ) + ); + println!("{}", banner::starting_time()); + let mut current_numbers_of_request = 0; let start_time = SystemTime::now(); @@ -384,14 +416,30 @@ fn main() { _ => 0, }; - bar.println(format!("{}\t{}{}{}{}", msg.method, msg.status, "\t".repeat(n_tabs), msg.url, extra)); + if no_progress_bar { + println!( + "{}\t{}{}{}{}", + msg.method, + msg.status, + "\t".repeat(n_tabs), + msg.url, + extra + ); + } else { + bar.println(format!( + "{}\t{}{}{}{}", + msg.method, + msg.status, + "\t".repeat(n_tabs), + msg.url, + extra + )); + } } } bar.finish(); - if !matches.is_present("no-banner") { - println!("{}", banner::ending_time()); - } + println!("{}", banner::ending_time()); if !output.is_empty() { save_dir_results(output, &result_processor.results); @@ -413,7 +461,7 @@ fn main() { bar.set_style(ProgressStyle::default_bar() .template("{spinner} [{elapsed_precise}] {bar:40.red/white} {pos:>7}/{len:7} ETA: {eta_precise} req/s: {msg}") .progress_chars("#>-")); - + thread::spawn(move || dnsbuster::run(tx, domains, config)); while current_numbers_of_request != total_numbers_of_request { @@ -440,38 +488,60 @@ fn main() { result_processor.maybe_add_result(msg.clone()); match msg.status { - true => match msg.extra { - Some(v) => { - bar.println(format!("OK\t{}", &msg.domain[..msg.domain.len()-3])); - for addr in v { - let string_repr = addr.ip().to_string(); - match addr.is_ipv4() { - true => bar.println(format!("\t\tIPv4: {}", string_repr)), - false => bar.println(format!("\t\tIPv6: {}", string_repr)), + true => { + if no_progress_bar { + println!("OK\t{}", &msg.domain[..msg.domain.len() - 3]); + } else { + bar.println(format!("OK\t{}", &msg.domain[..msg.domain.len() - 3])); + } + + match msg.extra { + Some(v) => { + for addr in v { + let string_repr = addr.ip().to_string(); + match addr.is_ipv4() { + true => { + if no_progress_bar { + println!("\t\tIPv4: {}", string_repr); + } else { + bar.println(format!("\t\tIPv4: {}", string_repr)); + } + } + false => { + if no_progress_bar { + println!("\t\tIPv6: {}", string_repr); + } else { + bar.println(format!("\t\tIPv6: {}", string_repr)); + } + } + } } } - }, - None => bar.println(format!("OK\t{}", &msg.domain[..msg.domain.len()-3])), + None => (), + } } false => (), } } bar.finish(); - if !matches.is_present("no-banner") { - println!("{}", banner::ending_time()); - } + println!("{}", banner::ending_time()); if !output.is_empty() { save_dns_results(output, &result_processor.results); } - }, + } "vhost" => { if domain.is_empty() { error!("domain not specified (-d)"); return; } + if ignore_strings.is_empty() { + error!("ignore_strings not specified (-x)"); + return; + } + let vhosts = build_vhosts(wordlist_path, domain); let total_numbers_of_request = vhosts.len(); let (tx, rx) = channel::(); @@ -529,29 +599,42 @@ fn main() { } let n_tabs = match msg.status.len() / 8 { - 3 => 1, - 2 => 2, - 1 => 3, - 0 => 4, - _ => 0, - }; + 3 => 1, + 2 => 2, + 1 => 3, + 0 => 4, + _ => 0, + }; if !msg.ignored { result_processor.maybe_add_result(msg.clone()); - bar.println(format!("{}\t{}{}{}", msg.method, msg.status, "\t".repeat(n_tabs), msg.vhost)); + if no_progress_bar { + println!( + "{}\t{}{}{}", + msg.method, + msg.status, + "\t".repeat(n_tabs), + msg.vhost + ); + } else { + bar.println(format!( + "{}\t{}{}{}", + msg.method, + msg.status, + "\t".repeat(n_tabs), + msg.vhost + )); + } } - } bar.finish(); - if !matches.is_present("no-banner") { - println!("{}", banner::ending_time()); - } + println!("{}", banner::ending_time()); if !output.is_empty() { save_vhost_results(output, &result_processor.results); } - }, + } _ => (), } } diff --git a/src/vhostbuster/mod.rs b/src/vhostbuster/mod.rs index 885efc5..1615de8 100644 --- a/src/vhostbuster/mod.rs +++ b/src/vhostbuster/mod.rs @@ -2,7 +2,7 @@ use futures::Stream; use hyper::{ client::HttpConnector, rt::{self, Future}, - Body, Client, Request, StatusCode, Uri + Body, Client, Request, StatusCode, Uri, }; use hyper_tls::{self, HttpsConnector}; use native_tls; @@ -31,20 +31,19 @@ fn make_request_future( config: &VhostConfig, ) -> impl Future { let tx_err = tx.clone(); - let target = Arc::new(Mutex::new( - SingleVhostScanResult { - vhost: url.to_string(), - status: StatusCode::default().to_string(), - error: None, - method: config.http_method.clone(), - ignored: false - } - )); + let target = Arc::new(Mutex::new(SingleVhostScanResult { + vhost: url.to_string(), + status: StatusCode::default().to_string(), + error: None, + method: config.http_method.clone(), + ignored: false, + })); let target_res = target.clone(); let mut target_err = (*target.lock().unwrap()).clone(); let mut request_builder = Request::builder(); let ignore_strings = config.ignore_strings.clone(); - let request = request_builder.header("User-Agent", &config.user_agent[..]) + let request = request_builder + .header("User-Agent", &config.user_agent[..]) .method(&config.http_method[..]) .uri(&config.original_url) .header("Host", url.host().unwrap()) @@ -68,10 +67,7 @@ fn make_request_future( } } - let target = Arc::try_unwrap(target_res) - .unwrap() - .into_inner() - .unwrap(); + let target = Arc::try_unwrap(target_res).unwrap().into_inner().unwrap(); tx.send(target).unwrap(); Ok(()) }) diff --git a/src/vhostbuster/utils.rs b/src/vhostbuster/utils.rs index 9f61cd8..2fa6d91 100644 --- a/src/vhostbuster/utils.rs +++ b/src/vhostbuster/utils.rs @@ -1,23 +1,16 @@ -use std::{ - fs, fs::File, path::Path, str, - io::Write -}; +use std::{fs, fs::File, io::Write, path::Path, str}; use super::result_processor::SingleVhostScanResult; -pub fn build_vhosts( - wordlist_path: &str, - url: &str, -) -> Vec { +pub fn build_vhosts(wordlist_path: &str, url: &str) -> Vec { debug!("building urls"); let mut urls: Vec = Vec::new(); - let wordlist = fs::read_to_string(wordlist_path).expect("Something went wrong reading the wordlist file"); + let wordlist = + fs::read_to_string(wordlist_path).expect("Something went wrong reading the wordlist file"); let urls_iter = wordlist .lines() .filter(|word| !word.starts_with('#') && !word.starts_with(' ')) - .map(|word| { - format!("{}.{}", word, url) - }); + .map(|word| format!("{}.{}", word, url)); for url in urls_iter { match url.parse::() {