Commits:
-
e8c774f3
by Elena at 2026-09-28T19:10:00+02:00
fixup! TB 44806: Implement the tor integration in Rust.
TB 44930: Implement the commands on the Rust control port
Implemented bridge line parsing.
-
1b15f99e
by Elena at 2026-09-28T19:10:00+02:00
fixup! TB 44806: Implement the tor integration in Rust.
TB 44930: Implement the commands on the Rust control port
Implemented the parsers to get PTs.
-
eb5427bb
by Elena at 2026-09-28T19:10:01+02:00
fixup! TB 44806: Implement the tor integration in Rust.
TB 44930: Implement the commands on the Rust control port
Implemented the GETCONF commands.
-
343a6dfa
by Elena at 2026-09-28T19:10:02+02:00
fixup! TB 44806: Implement the tor integration in Rust.
TB 44930: Implement the commands on the Rust control port
Implemented the SETCONF.
12 changed files:
Changes:
Cargo.lock
| ... |
... |
@@ -8173,8 +8173,12 @@ version = "0.1.0" |
|
8173
|
8173
|
dependencies = [
|
|
8174
|
8174
|
"bytes",
|
|
8175
|
8175
|
"hex",
|
|
|
8176
|
+ "itoa",
|
|
|
8177
|
+ "lazy_static",
|
|
8176
|
8178
|
"log",
|
|
8177
|
8179
|
"memchr",
|
|
|
8180
|
+ "regex",
|
|
|
8181
|
+ "serde",
|
|
8178
|
8182
|
"thiserror 2.0.12",
|
|
8179
|
8183
|
]
|
|
8180
|
8184
|
|
toolkit/components/tor-integration/tor_provider/Cargo.toml
| ... |
... |
@@ -7,6 +7,10 @@ edition = "2021" |
|
7
|
7
|
[dependencies]
|
|
8
|
8
|
bytes = "1.4.0"
|
|
9
|
9
|
hex = "0.4.3"
|
|
|
10
|
+itoa = "1.0.15"
|
|
|
11
|
+lazy_static = "1"
|
|
10
|
12
|
log = "0.4"
|
|
11
|
13
|
memchr = "2.7.4"
|
|
|
14
|
+regex = "1"
|
|
|
15
|
+serde = { version = "1.0", features = ["derive"] }
|
|
12
|
16
|
thiserror = "2" |
toolkit/components/tor-integration/tor_provider/src/ctor/controller/commands/get_conf.rs
|
|
1
|
+// Licensed under the Apache License, Version 2.0,
|
|
|
2
|
+// <http://apache.org/licenses/LICENSE-2.0> or the MIT license
|
|
|
3
|
+// <http://opensource.org/licenses/MIT>, at your option. This file may not be
|
|
|
4
|
+// copied, modified, or distributed except according to those terms.
|
|
|
5
|
+
|
|
|
6
|
+use std::iter::once;
|
|
|
7
|
+
|
|
|
8
|
+use super::Command;
|
|
|
9
|
+use crate::ctor::{
|
|
|
10
|
+ controller::{parsers::*, types::*, ControllerError},
|
|
|
11
|
+ reply_parser::{DetailReplyLine, Reply},
|
|
|
12
|
+};
|
|
|
13
|
+
|
|
|
14
|
+pub fn bridges() -> Result<Command<Vec<Bridge>>, ControllerError> {
|
|
|
15
|
+ get_conf("Bridge", |line| {
|
|
|
16
|
+ parse_bridge_line(line).map_err(|e| ControllerError::MalformedReply(e.to_string()))
|
|
|
17
|
+ })
|
|
|
18
|
+}
|
|
|
19
|
+
|
|
|
20
|
+pub fn client_transport_plugins() -> Result<Command<Vec<PluggableTransport>>, ControllerError> {
|
|
|
21
|
+ get_conf("ClientTransportPlugin", parse_client_transport_plugin)
|
|
|
22
|
+}
|
|
|
23
|
+
|
|
|
24
|
+fn get_conf<F, T>(key: &'static str, converter: F) -> Result<Command<Vec<T>>, ControllerError>
|
|
|
25
|
+where
|
|
|
26
|
+ F: Fn(&[u8]) -> Result<T, ControllerError> + 'static,
|
|
|
27
|
+{
|
|
|
28
|
+ Ok(Command {
|
|
|
29
|
+ command: format!("GETCONF {}\r\n", key),
|
|
|
30
|
+ handler: Box::new(move |reply| parse_get_conf(key.as_bytes(), &converter, reply)),
|
|
|
31
|
+ })
|
|
|
32
|
+}
|
|
|
33
|
+
|
|
|
34
|
+/// Parse and convert the output of the GETCONF command for a single keyword.
|
|
|
35
|
+///
|
|
|
36
|
+/// We currently query and parse only one key at the time, but this parser
|
|
|
37
|
+/// tolerates keywords we did not ask (it silently ignores them).
|
|
|
38
|
+fn parse_get_conf<F, T>(key: &[u8], converter: &F, reply: Reply) -> Result<Vec<T>, ControllerError>
|
|
|
39
|
+where
|
|
|
40
|
+ F: Fn(&[u8]) -> Result<T, ControllerError> + 'static,
|
|
|
41
|
+{
|
|
|
42
|
+ if let Some(e) = ControllerError::from_reply(&reply) {
|
|
|
43
|
+ return Err(e);
|
|
|
44
|
+ }
|
|
|
45
|
+
|
|
|
46
|
+ let check_code = |code| {
|
|
|
47
|
+ if code != 250 {
|
|
|
48
|
+ Err(ControllerError::MalformedReply(format!(
|
|
|
49
|
+ "Expected the 250 status code, got '{}'",
|
|
|
50
|
+ code
|
|
|
51
|
+ )))
|
|
|
52
|
+ } else {
|
|
|
53
|
+ Ok(())
|
|
|
54
|
+ }
|
|
|
55
|
+ };
|
|
|
56
|
+
|
|
|
57
|
+ check_code(reply.end_line().code)?;
|
|
|
58
|
+
|
|
|
59
|
+ // Special case: "default value semantically different from an empty string"
|
|
|
60
|
+ // is just "250 KEYWORD CRLF" without '=' (see the specs).
|
|
|
61
|
+ if reply.details().is_empty() && reply.end_line().line.eq_ignore_ascii_case(key) {
|
|
|
62
|
+ return Ok(Vec::new());
|
|
|
63
|
+ }
|
|
|
64
|
+
|
|
|
65
|
+ // Key followed by '='.
|
|
|
66
|
+ let prefix_len = key.len() + 1;
|
|
|
67
|
+
|
|
|
68
|
+ let details = reply.details().iter().map(|l| match l {
|
|
|
69
|
+ DetailReplyLine::MidReplyLine { code, line } => {
|
|
|
70
|
+ check_code(*code)?;
|
|
|
71
|
+ Ok(line.as_ref())
|
|
|
72
|
+ }
|
|
|
73
|
+ DetailReplyLine::DataReplyLine { .. } => Err(ControllerError::MalformedReply(
|
|
|
74
|
+ String::from("GETCONF does not allow data lines"),
|
|
|
75
|
+ )),
|
|
|
76
|
+ });
|
|
|
77
|
+
|
|
|
78
|
+ let all_lines = details.chain(once(Ok(reply.end_line().line.as_ref())));
|
|
|
79
|
+
|
|
|
80
|
+ all_lines
|
|
|
81
|
+ .filter_map(|line| match line {
|
|
|
82
|
+ Ok(l) => {
|
|
|
83
|
+ if l.len() >= prefix_len
|
|
|
84
|
+ && l[0..key.len()].eq_ignore_ascii_case(key)
|
|
|
85
|
+ && l[key.len()] == b'='
|
|
|
86
|
+ {
|
|
|
87
|
+ Some(converter(&l[prefix_len..]))
|
|
|
88
|
+ } else {
|
|
|
89
|
+ None
|
|
|
90
|
+ }
|
|
|
91
|
+ }
|
|
|
92
|
+ Err(e) => Some(Err(e)),
|
|
|
93
|
+ })
|
|
|
94
|
+ .collect()
|
|
|
95
|
+}
|
|
|
96
|
+
|
|
|
97
|
+#[cfg(test)]
|
|
|
98
|
+mod tests {
|
|
|
99
|
+ use std::assert_matches;
|
|
|
100
|
+
|
|
|
101
|
+ use super::*;
|
|
|
102
|
+ use crate::ctor::reply_parser::make_reply;
|
|
|
103
|
+
|
|
|
104
|
+ fn converter(v: &[u8]) -> Result<String, ControllerError> {
|
|
|
105
|
+ Ok(String::from_utf8_lossy(v).into_owned())
|
|
|
106
|
+ }
|
|
|
107
|
+
|
|
|
108
|
+ #[test]
|
|
|
109
|
+ fn simple() {
|
|
|
110
|
+ assert_eq!(
|
|
|
111
|
+ parse_get_conf(b"test", &converter, make_reply(b"250 test=value\r\n")).unwrap(),
|
|
|
112
|
+ vec!["value"]
|
|
|
113
|
+ );
|
|
|
114
|
+ }
|
|
|
115
|
+
|
|
|
116
|
+ #[test]
|
|
|
117
|
+ fn case_insensitivity() {
|
|
|
118
|
+ assert_eq!(
|
|
|
119
|
+ parse_get_conf(b"test", &converter, make_reply(b"250 TEST=value\r\n")).unwrap(),
|
|
|
120
|
+ vec!["value"]
|
|
|
121
|
+ );
|
|
|
122
|
+ assert_eq!(
|
|
|
123
|
+ parse_get_conf(b"Test", &converter, make_reply(b"250 TEST=value\r\n")).unwrap(),
|
|
|
124
|
+ vec!["value"]
|
|
|
125
|
+ );
|
|
|
126
|
+ assert_eq!(
|
|
|
127
|
+ parse_get_conf(b"TeSt", &converter, make_reply(b"250 tESt=value\r\n")).unwrap(),
|
|
|
128
|
+ vec!["value"]
|
|
|
129
|
+ );
|
|
|
130
|
+ }
|
|
|
131
|
+
|
|
|
132
|
+ #[test]
|
|
|
133
|
+ fn array() {
|
|
|
134
|
+ assert_eq!(
|
|
|
135
|
+ parse_get_conf(
|
|
|
136
|
+ b"test",
|
|
|
137
|
+ &converter,
|
|
|
138
|
+ make_reply(b"250-test=value 1\r\n250 test=value 2\r\n")
|
|
|
139
|
+ )
|
|
|
140
|
+ .unwrap(),
|
|
|
141
|
+ vec!["value 1", "value 2"]
|
|
|
142
|
+ );
|
|
|
143
|
+
|
|
|
144
|
+ assert_eq!(
|
|
|
145
|
+ parse_get_conf(
|
|
|
146
|
+ b"Test",
|
|
|
147
|
+ &converter,
|
|
|
148
|
+ make_reply(b"250-test=VALUE 1\r\n250 TEST=value 2\r\n")
|
|
|
149
|
+ )
|
|
|
150
|
+ .unwrap(),
|
|
|
151
|
+ vec!["VALUE 1", "value 2"]
|
|
|
152
|
+ );
|
|
|
153
|
+ }
|
|
|
154
|
+
|
|
|
155
|
+ #[test]
|
|
|
156
|
+ fn empty() {
|
|
|
157
|
+ assert!(
|
|
|
158
|
+ parse_get_conf(b"test", &converter, make_reply(b"250 test\r\n"))
|
|
|
159
|
+ .unwrap()
|
|
|
160
|
+ .is_empty(),
|
|
|
161
|
+ );
|
|
|
162
|
+ assert!(
|
|
|
163
|
+ parse_get_conf(b"Test", &converter, make_reply(b"250 TEST\r\n"))
|
|
|
164
|
+ .unwrap()
|
|
|
165
|
+ .is_empty(),
|
|
|
166
|
+ );
|
|
|
167
|
+ }
|
|
|
168
|
+
|
|
|
169
|
+ #[test]
|
|
|
170
|
+ fn other_keys() {
|
|
|
171
|
+ assert_eq!(
|
|
|
172
|
+ parse_get_conf(
|
|
|
173
|
+ b"test",
|
|
|
174
|
+ &converter,
|
|
|
175
|
+ make_reply(b"250-test 1=a\r\n250-test=b\r\n250-test=c\r\n250 test 2=d\r\n")
|
|
|
176
|
+ )
|
|
|
177
|
+ .unwrap(),
|
|
|
178
|
+ vec!["b", "c"]
|
|
|
179
|
+ );
|
|
|
180
|
+
|
|
|
181
|
+ assert!(parse_get_conf(
|
|
|
182
|
+ b"C",
|
|
|
183
|
+ &converter,
|
|
|
184
|
+ make_reply(b"250-A=Apple\r\n250 B=Banana\r\n"),
|
|
|
185
|
+ )
|
|
|
186
|
+ .unwrap()
|
|
|
187
|
+ .is_empty(),);
|
|
|
188
|
+ }
|
|
|
189
|
+
|
|
|
190
|
+ #[test]
|
|
|
191
|
+ fn tor_error() {
|
|
|
192
|
+ assert_eq!(
|
|
|
193
|
+ parse_get_conf(
|
|
|
194
|
+ b"test",
|
|
|
195
|
+ &converter,
|
|
|
196
|
+ make_reply(b"552 Unrecognized configuration key \"test\"\r\n")
|
|
|
197
|
+ )
|
|
|
198
|
+ .unwrap_err(),
|
|
|
199
|
+ ControllerError::TorError {
|
|
|
200
|
+ code: 552,
|
|
|
201
|
+ message: String::from("Unrecognized configuration key \"test\"")
|
|
|
202
|
+ }
|
|
|
203
|
+ );
|
|
|
204
|
+ }
|
|
|
205
|
+
|
|
|
206
|
+ #[test]
|
|
|
207
|
+ fn no_250() {
|
|
|
208
|
+ assert_matches!(
|
|
|
209
|
+ parse_get_conf(
|
|
|
210
|
+ b"test",
|
|
|
211
|
+ &converter,
|
|
|
212
|
+ make_reply(b"251 test=value\r\n")
|
|
|
213
|
+ )
|
|
|
214
|
+ .unwrap_err(),
|
|
|
215
|
+ ControllerError::MalformedReply(_)
|
|
|
216
|
+ );
|
|
|
217
|
+ assert_matches!(
|
|
|
218
|
+ parse_get_conf(
|
|
|
219
|
+ b"test",
|
|
|
220
|
+ &converter,
|
|
|
221
|
+ make_reply(b"251-test1=value\r\n250 test2=value2\r\n")
|
|
|
222
|
+ )
|
|
|
223
|
+ .unwrap_err(),
|
|
|
224
|
+ ControllerError::MalformedReply(_)
|
|
|
225
|
+ );
|
|
|
226
|
+ assert_matches!(
|
|
|
227
|
+ parse_get_conf(
|
|
|
228
|
+ b"test",
|
|
|
229
|
+ &converter,
|
|
|
230
|
+ make_reply(b"250-test1=value\r\n251 test1=value2\r\n")
|
|
|
231
|
+ )
|
|
|
232
|
+ .unwrap_err(),
|
|
|
233
|
+ ControllerError::MalformedReply(_)
|
|
|
234
|
+ );
|
|
|
235
|
+ }
|
|
|
236
|
+
|
|
|
237
|
+ #[test]
|
|
|
238
|
+ fn no_multiline_getconf() {
|
|
|
239
|
+ assert_matches!(
|
|
|
240
|
+ parse_get_conf(
|
|
|
241
|
+ b"test",
|
|
|
242
|
+ &converter,
|
|
|
243
|
+ make_reply(b"250+test=\r\nsome value\r\n.\r\n250 OK\r\n")
|
|
|
244
|
+ )
|
|
|
245
|
+ .unwrap_err(),
|
|
|
246
|
+ ControllerError::MalformedReply(_)
|
|
|
247
|
+ );
|
|
|
248
|
+ }
|
|
|
249
|
+
|
|
|
250
|
+ #[test]
|
|
|
251
|
+ fn converter_error() {
|
|
|
252
|
+ assert_eq!(
|
|
|
253
|
+ parse_get_conf::<_, String>(
|
|
|
254
|
+ b"test",
|
|
|
255
|
+ &(|_| Err(ControllerError::WrongFormat(String::from(
|
|
|
256
|
+ "Something is fishy"
|
|
|
257
|
+ )))),
|
|
|
258
|
+ make_reply(b"250 test=value\r\n")
|
|
|
259
|
+ )
|
|
|
260
|
+ .unwrap_err(),
|
|
|
261
|
+ ControllerError::WrongFormat(String::from("Something is fishy"))
|
|
|
262
|
+ );
|
|
|
263
|
+ }
|
|
|
264
|
+} |
toolkit/components/tor-integration/tor_provider/src/ctor/controller/commands/mod.rs
| ... |
... |
@@ -5,8 +5,10 @@ |
|
5
|
5
|
|
|
6
|
6
|
mod authenticate;
|
|
7
|
7
|
mod command;
|
|
|
8
|
+pub mod get_conf;
|
|
8
|
9
|
mod reset_conf;
|
|
9
|
10
|
mod save_conf;
|
|
|
11
|
+mod set_conf;
|
|
10
|
12
|
mod set_events;
|
|
11
|
13
|
mod signal;
|
|
12
|
14
|
mod take_ownership;
|
| ... |
... |
@@ -15,6 +17,7 @@ pub use authenticate::authenticate; |
|
15
|
17
|
pub use command::Command;
|
|
16
|
18
|
pub use reset_conf::reset_owning_controller_process;
|
|
17
|
19
|
pub use save_conf::save_conf;
|
|
|
20
|
+pub use set_conf::set_conf;
|
|
18
|
21
|
pub use set_events::set_events;
|
|
19
|
22
|
pub use signal::signal_newnym;
|
|
20
|
23
|
pub use take_ownership::take_ownership; |
toolkit/components/tor-integration/tor_provider/src/ctor/controller/commands/set_conf.rs
|
|
1
|
+// Licensed under the Apache License, Version 2.0,
|
|
|
2
|
+// <http://apache.org/licenses/LICENSE-2.0> or the MIT license
|
|
|
3
|
+// <http://opensource.org/licenses/MIT>, at your option. This file may not be
|
|
|
4
|
+// copied, modified, or distributed except according to those terms.
|
|
|
5
|
+
|
|
|
6
|
+use lazy_static::lazy_static;
|
|
|
7
|
+use regex::Regex;
|
|
|
8
|
+
|
|
|
9
|
+use super::Command;
|
|
|
10
|
+use crate::ctor::controller::{parsers::*, ConfValue, ControllerError};
|
|
|
11
|
+
|
|
|
12
|
+lazy_static! {
|
|
|
13
|
+ // The SETCONF command's documentation lists `keyword` for keys, and
|
|
|
14
|
+ // keyword is defined as `1*ALPHA`, so it should not include numbers.
|
|
|
15
|
+ // However, as a matter of fact, several settings (including the ones we
|
|
|
16
|
+ // use) have numbers, therefore we also accept them.
|
|
|
17
|
+ static ref KEY_REGEX: Regex =
|
|
|
18
|
+ Regex::new("^[a-zA-Z0-9_]+$").expect("The regex is hardcoded, it should be good.");
|
|
|
19
|
+}
|
|
|
20
|
+
|
|
|
21
|
+pub fn set_conf(values: &[(&str, ConfValue)]) -> Result<Command<u16>, ControllerError> {
|
|
|
22
|
+ Ok(Command {
|
|
|
23
|
+ command: make_setconf_command(values)?,
|
|
|
24
|
+ handler: Box::new(parse_ack),
|
|
|
25
|
+ })
|
|
|
26
|
+}
|
|
|
27
|
+
|
|
|
28
|
+fn make_setconf_command(values: &[(&str, ConfValue)]) -> Result<String, ControllerError> {
|
|
|
29
|
+ let mut res = String::from("SETCONF");
|
|
|
30
|
+ res.reserve(256);
|
|
|
31
|
+ for (key, value) in values.iter() {
|
|
|
32
|
+ if !KEY_REGEX.is_match(key) {
|
|
|
33
|
+ return Err(ControllerError::InvalidArgument(format!(
|
|
|
34
|
+ "invalid key: '{}'",
|
|
|
35
|
+ key
|
|
|
36
|
+ )));
|
|
|
37
|
+ }
|
|
|
38
|
+ match value {
|
|
|
39
|
+ ConfValue::Bool(v) => push_kv(key, if *v { "1" } else { "0" }, false, &mut res),
|
|
|
40
|
+ ConfValue::Int(i) => {
|
|
|
41
|
+ push_k(key, &mut res);
|
|
|
42
|
+ res.push('=');
|
|
|
43
|
+ let mut buf = itoa::Buffer::new();
|
|
|
44
|
+ res.push_str(buf.format(*i));
|
|
|
45
|
+ }
|
|
|
46
|
+ ConfValue::String(s) => push_kv(key, s, true, &mut res),
|
|
|
47
|
+ ConfValue::Array(a) => {
|
|
|
48
|
+ if a.is_empty() {
|
|
|
49
|
+ push_k(key, &mut res);
|
|
|
50
|
+ } else {
|
|
|
51
|
+ for v in a.iter() {
|
|
|
52
|
+ push_kv(key, v, true, &mut res);
|
|
|
53
|
+ }
|
|
|
54
|
+ }
|
|
|
55
|
+ }
|
|
|
56
|
+ ConfValue::Null => push_k(key, &mut res),
|
|
|
57
|
+ }
|
|
|
58
|
+ }
|
|
|
59
|
+ res.push_str("\r\n");
|
|
|
60
|
+ Ok(res)
|
|
|
61
|
+}
|
|
|
62
|
+
|
|
|
63
|
+fn push_k(key: &str, dest: &mut String) {
|
|
|
64
|
+ dest.push(' ');
|
|
|
65
|
+ dest.push_str(key);
|
|
|
66
|
+}
|
|
|
67
|
+
|
|
|
68
|
+fn push_kv(key: &str, val: &str, escape: bool, dest: &mut String) {
|
|
|
69
|
+ push_k(key, dest);
|
|
|
70
|
+ dest.push('=');
|
|
|
71
|
+ if escape {
|
|
|
72
|
+ tor_escape_into(val, dest);
|
|
|
73
|
+ } else {
|
|
|
74
|
+ dest.push_str(val);
|
|
|
75
|
+ }
|
|
|
76
|
+}
|
|
|
77
|
+
|
|
|
78
|
+#[cfg(test)]
|
|
|
79
|
+mod tests {
|
|
|
80
|
+ use std::assert_matches;
|
|
|
81
|
+
|
|
|
82
|
+ use super::*;
|
|
|
83
|
+
|
|
|
84
|
+ #[test]
|
|
|
85
|
+ fn test_bool() {
|
|
|
86
|
+ let vals = &[
|
|
|
87
|
+ ("test", ConfValue::Bool(true)),
|
|
|
88
|
+ ("AA", ConfValue::Bool(false)),
|
|
|
89
|
+ ];
|
|
|
90
|
+ assert_eq!(
|
|
|
91
|
+ make_setconf_command(vals).unwrap(),
|
|
|
92
|
+ "SETCONF test=1 AA=0\r\n"
|
|
|
93
|
+ );
|
|
|
94
|
+ }
|
|
|
95
|
+
|
|
|
96
|
+ #[test]
|
|
|
97
|
+ fn test_int() {
|
|
|
98
|
+ let vals = &[
|
|
|
99
|
+ ("aaa", ConfValue::Int(42)),
|
|
|
100
|
+ ("bBbB", ConfValue::Int(0)),
|
|
|
101
|
+ ("cc", ConfValue::Int(-3)),
|
|
|
102
|
+ ];
|
|
|
103
|
+ assert_eq!(
|
|
|
104
|
+ make_setconf_command(vals).unwrap(),
|
|
|
105
|
+ "SETCONF aaa=42 bBbB=0 cc=-3\r\n"
|
|
|
106
|
+ );
|
|
|
107
|
+ }
|
|
|
108
|
+
|
|
|
109
|
+ #[test]
|
|
|
110
|
+ fn test_string() {
|
|
|
111
|
+ let vals = &[("Test123", ConfValue::String("a b\\c"))];
|
|
|
112
|
+ assert_eq!(
|
|
|
113
|
+ make_setconf_command(vals).unwrap(),
|
|
|
114
|
+ "SETCONF Test123=\"a b\\\\c\"\r\n"
|
|
|
115
|
+ );
|
|
|
116
|
+ }
|
|
|
117
|
+
|
|
|
118
|
+ #[test]
|
|
|
119
|
+ fn test_array() {
|
|
|
120
|
+ let vals = &[
|
|
|
121
|
+ ("empty", ConfValue::Array(vec![])),
|
|
|
122
|
+ ("list", ConfValue::Array(vec!["one", "two two"])),
|
|
|
123
|
+ ];
|
|
|
124
|
+ assert_eq!(
|
|
|
125
|
+ make_setconf_command(vals).unwrap(),
|
|
|
126
|
+ "SETCONF empty list=\"one\" list=\"two two\"\r\n"
|
|
|
127
|
+ );
|
|
|
128
|
+ }
|
|
|
129
|
+
|
|
|
130
|
+ #[test]
|
|
|
131
|
+ fn test_null() {
|
|
|
132
|
+ let vals = &[("test", ConfValue::Null)];
|
|
|
133
|
+ assert_eq!(make_setconf_command(vals).unwrap(), "SETCONF test\r\n");
|
|
|
134
|
+ }
|
|
|
135
|
+
|
|
|
136
|
+ #[test]
|
|
|
137
|
+ fn mixed_values() {
|
|
|
138
|
+ let vals = &[
|
|
|
139
|
+ ("b", ConfValue::Bool(false)),
|
|
|
140
|
+ ("a", ConfValue::Int(-7)),
|
|
|
141
|
+ ("c", ConfValue::String("x y")),
|
|
|
142
|
+ ("d", ConfValue::Null),
|
|
|
143
|
+ ];
|
|
|
144
|
+ assert_eq!(
|
|
|
145
|
+ make_setconf_command(vals).unwrap(),
|
|
|
146
|
+ "SETCONF b=0 a=-7 c=\"x y\" d\r\n"
|
|
|
147
|
+ );
|
|
|
148
|
+ }
|
|
|
149
|
+
|
|
|
150
|
+ #[test]
|
|
|
151
|
+ fn repeated_key() {
|
|
|
152
|
+ let vals = &[
|
|
|
153
|
+ ("bridge", ConfValue::String("obfs4")),
|
|
|
154
|
+ ("bridge", ConfValue::String("snowflake")),
|
|
|
155
|
+ ("bridge", ConfValue::String("meek")),
|
|
|
156
|
+ ];
|
|
|
157
|
+ assert_eq!(
|
|
|
158
|
+ make_setconf_command(vals).unwrap(),
|
|
|
159
|
+ "SETCONF bridge=\"obfs4\" bridge=\"snowflake\" bridge=\"meek\"\r\n"
|
|
|
160
|
+ );
|
|
|
161
|
+ }
|
|
|
162
|
+
|
|
|
163
|
+ #[test]
|
|
|
164
|
+ fn realistic_configs() {
|
|
|
165
|
+ // No bridges
|
|
|
166
|
+ assert_eq!(
|
|
|
167
|
+ make_setconf_command(&[
|
|
|
168
|
+ ("UseBridges", ConfValue::Bool(false)),
|
|
|
169
|
+ ("Bridge", ConfValue::Null),
|
|
|
170
|
+ ])
|
|
|
171
|
+ .unwrap(),
|
|
|
172
|
+ "SETCONF UseBridges=0 Bridge\r\n"
|
|
|
173
|
+ );
|
|
|
174
|
+
|
|
|
175
|
+ // One bridge
|
|
|
176
|
+ assert_eq!(
|
|
|
177
|
+ make_setconf_command(&[
|
|
|
178
|
+ ("UseBridges", ConfValue::Bool(true)),
|
|
|
179
|
+ ("Bridge", ConfValue::Array(vec!["1.2.3.4:443"]))
|
|
|
180
|
+ ])
|
|
|
181
|
+ .unwrap(),
|
|
|
182
|
+ "SETCONF UseBridges=1 Bridge=\"1.2.3.4:443\"\r\n"
|
|
|
183
|
+ );
|
|
|
184
|
+
|
|
|
185
|
+ // Some bridges
|
|
|
186
|
+ assert_eq!(
|
|
|
187
|
+ make_setconf_command(&[
|
|
|
188
|
+ ("UseBridges", ConfValue::Bool(true)),
|
|
|
189
|
+ (
|
|
|
190
|
+ "Bridge",
|
|
|
191
|
+ ConfValue::Array(vec![
|
|
|
192
|
+ "1.2.3.4:443",
|
|
|
193
|
+ "obfs4 5.6.7.8:9999 0123456789012345678901234567890123456789 iat-mode=0",
|
|
|
194
|
+ ]),
|
|
|
195
|
+ ),
|
|
|
196
|
+ ])
|
|
|
197
|
+ .unwrap(),
|
|
|
198
|
+ concat!(
|
|
|
199
|
+ "SETCONF ",
|
|
|
200
|
+ "UseBridges=1 ",
|
|
|
201
|
+ "Bridge=\"1.2.3.4:443\" ",
|
|
|
202
|
+ "Bridge=\"obfs4 5.6.7.8:9999 0123456789012345678901234567890123456789 iat-mode=0\"",
|
|
|
203
|
+ "\r\n"
|
|
|
204
|
+ )
|
|
|
205
|
+ );
|
|
|
206
|
+
|
|
|
207
|
+ // No firewall
|
|
|
208
|
+ assert_eq!(
|
|
|
209
|
+ make_setconf_command(&[("ReachableAddresses", ConfValue::Null)]).unwrap(),
|
|
|
210
|
+ "SETCONF ReachableAddresses\r\n"
|
|
|
211
|
+ );
|
|
|
212
|
+
|
|
|
213
|
+ // Firewall that only allows 80 and 443
|
|
|
214
|
+ assert_eq!(
|
|
|
215
|
+ make_setconf_command(&[("ReachableAddresses", ConfValue::String("*:80,*:443"))])
|
|
|
216
|
+ .unwrap(),
|
|
|
217
|
+ "SETCONF ReachableAddresses=\"*:80,*:443\"\r\n"
|
|
|
218
|
+ );
|
|
|
219
|
+
|
|
|
220
|
+ // No proxies
|
|
|
221
|
+ assert_eq!(
|
|
|
222
|
+ make_setconf_command(&[
|
|
|
223
|
+ ("Socks4Proxy", ConfValue::Null),
|
|
|
224
|
+ ("Socks5Proxy", ConfValue::Null),
|
|
|
225
|
+ ("Socks5ProxyUsername", ConfValue::Null),
|
|
|
226
|
+ ("Socks5ProxyPassword", ConfValue::Null),
|
|
|
227
|
+ ("HTTPSProxy", ConfValue::Null),
|
|
|
228
|
+ ("HTTPSProxyAuthenticator", ConfValue::Null),
|
|
|
229
|
+ ])
|
|
|
230
|
+ .unwrap(),
|
|
|
231
|
+ "SETCONF Socks4Proxy Socks5Proxy Socks5ProxyUsername Socks5ProxyPassword HTTPSProxy HTTPSProxyAuthenticator\r\n"
|
|
|
232
|
+ );
|
|
|
233
|
+ }
|
|
|
234
|
+
|
|
|
235
|
+ #[test]
|
|
|
236
|
+ fn invalid_key() {
|
|
|
237
|
+ let val = &[("invalid\r\nkey", ConfValue::Null)];
|
|
|
238
|
+ assert_matches!(
|
|
|
239
|
+ make_setconf_command(val).unwrap_err(),
|
|
|
240
|
+ ControllerError::InvalidArgument(_)
|
|
|
241
|
+ );
|
|
|
242
|
+ }
|
|
|
243
|
+} |
toolkit/components/tor-integration/tor_provider/src/ctor/controller/controller.rs
| ... |
... |
@@ -8,6 +8,7 @@ use bytes::Bytes; |
|
8
|
8
|
use super::{
|
|
9
|
9
|
commands::{self, Command},
|
|
10
|
10
|
error::ControllerError,
|
|
|
11
|
+ types::*,
|
|
11
|
12
|
};
|
|
12
|
13
|
use crate::ctor::{
|
|
13
|
14
|
control_port::{ControlPortInterface, ControlSocketError},
|
| ... |
... |
@@ -102,16 +103,35 @@ impl<CP: ControlPortInterface> TorController<CP> { |
|
102
|
103
|
|
|
103
|
104
|
// Connection management
|
|
104
|
105
|
|
|
|
106
|
+ pub fn set_conf(
|
|
|
107
|
+ &self,
|
|
|
108
|
+ values: &[(&str, ConfValue)],
|
|
|
109
|
+ handler: Box<dyn FnOnce(Result<u16, ControllerError>)>,
|
|
|
110
|
+ ) {
|
|
|
111
|
+ self.send_command(commands::set_conf(values), handler);
|
|
|
112
|
+ }
|
|
|
113
|
+
|
|
105
|
114
|
pub fn save_conf(&self, handler: Box<dyn FnOnce(Result<u16, ControllerError>)>) {
|
|
106
|
115
|
self.send_command(commands::save_conf(), handler);
|
|
107
|
116
|
}
|
|
108
|
117
|
|
|
|
118
|
+ pub fn get_bridges(&self, handler: Box<dyn FnOnce(Result<Vec<Bridge>, ControllerError>)>) {
|
|
|
119
|
+ self.send_command(commands::get_conf::bridges(), handler);
|
|
|
120
|
+ }
|
|
|
121
|
+
|
|
109
|
122
|
// Circuit display
|
|
110
|
123
|
|
|
111
|
124
|
// Onion authentication
|
|
112
|
125
|
|
|
113
|
126
|
// Miscellaneous
|
|
114
|
127
|
|
|
|
128
|
+ pub fn get_pluggable_transports(
|
|
|
129
|
+ &self,
|
|
|
130
|
+ handler: Box<dyn FnOnce(Result<Vec<PluggableTransport>, ControllerError>)>,
|
|
|
131
|
+ ) {
|
|
|
132
|
+ self.send_command(commands::get_conf::client_transport_plugins(), handler);
|
|
|
133
|
+ }
|
|
|
134
|
+
|
|
115
|
135
|
pub fn signal_newnym(&self, handler: Box<dyn FnOnce(Result<u16, ControllerError>)>) {
|
|
116
|
136
|
self.send_command(commands::signal_newnym(), handler);
|
|
117
|
137
|
}
|
toolkit/components/tor-integration/tor_provider/src/ctor/controller/error.rs
| ... |
... |
@@ -32,6 +32,8 @@ pub enum ControllerError { |
|
32
|
32
|
KeyNotFound(String),
|
|
33
|
33
|
#[error("malformed reply: {0}")]
|
|
34
|
34
|
MalformedReply(String),
|
|
|
35
|
+ #[error("the argument passed to the command was not valid: {0}")]
|
|
|
36
|
+ InvalidArgument(String),
|
|
35
|
37
|
}
|
|
36
|
38
|
|
|
37
|
39
|
impl ControllerError {
|
toolkit/components/tor-integration/tor_provider/src/ctor/controller/mod.rs
| ... |
... |
@@ -7,6 +7,8 @@ mod commands; |
|
7
|
7
|
mod controller;
|
|
8
|
8
|
mod error;
|
|
9
|
9
|
mod parsers;
|
|
|
10
|
+mod types;
|
|
10
|
11
|
|
|
11
|
12
|
pub use controller::*;
|
|
12
|
13
|
pub use error::*;
|
|
|
14
|
+pub use types::*; |
toolkit/components/tor-integration/tor_provider/src/ctor/controller/parsers/bridge_line.rs
|
|
1
|
+// Licensed under the Apache License, Version 2.0,
|
|
|
2
|
+// <http://apache.org/licenses/LICENSE-2.0> or the MIT license
|
|
|
3
|
+// <http://opensource.org/licenses/MIT>, at your option. This file may not be
|
|
|
4
|
+// copied, modified, or distributed except according to those terms.
|
|
|
5
|
+
|
|
|
6
|
+use hex;
|
|
|
7
|
+use lazy_static::lazy_static;
|
|
|
8
|
+use regex::bytes::Regex;
|
|
|
9
|
+use std::net::SocketAddr;
|
|
|
10
|
+use thiserror::Error;
|
|
|
11
|
+
|
|
|
12
|
+use crate::ctor::controller::types::Bridge;
|
|
|
13
|
+
|
|
|
14
|
+lazy_static! {
|
|
|
15
|
+ // In C-tor's parse_bridge_line (src/app/config/config.c), the transport is
|
|
|
16
|
+ // parsed as a potential C identifier (starts with letter/underscores and
|
|
|
17
|
+ // can contain any additional number of letters, numbers and underscores,
|
|
|
18
|
+ // as defined in src/lib/string/util_string.c).
|
|
|
19
|
+ //
|
|
|
20
|
+ // The logic to distinguish between the fingerprint and args is the
|
|
|
21
|
+ // following one (from C-tor's source code):
|
|
|
22
|
+ // - if the bridge does not use a transport, it cannot have arguments.
|
|
|
23
|
+ // If after the address there is anything, it must be the fingerprint;
|
|
|
24
|
+ // - otherwise, if there is a = sign, there is not a fingerprint.
|
|
|
25
|
+ //
|
|
|
26
|
+ // We do not use arguments in any way, so we do not enforce their format.
|
|
|
27
|
+ // However, if we see a valid hex sequence, we still need to validate its
|
|
|
28
|
+ // length, to make sure it actually is a fingerprint.
|
|
|
29
|
+ //
|
|
|
30
|
+ // We are not strict on spaces in case this is used with data provided
|
|
|
31
|
+ // directly by users.
|
|
|
32
|
+ static ref BRIDGE_REGEX: Regex = Regex::new(r"^ *(?:(?<transport>[A-Za-z_][A-Za-z0-9_]*) +)?(?<addr>[0-9a-fA-F\.\[\]\:]+:\d{1,5})(?: +(?<fingerprint>[0-9a-fA-F]+))?(?: +(?<args>.+?))? *$")
|
|
|
33
|
+ .expect("The regex is hardcoded, it should be good.");
|
|
|
34
|
+}
|
|
|
35
|
+
|
|
|
36
|
+#[derive(Error, Debug, Clone, PartialEq, Eq)]
|
|
|
37
|
+pub enum BridgeParseError {
|
|
|
38
|
+ #[error("the bridge line has a wrong format and could not be parsed")]
|
|
|
39
|
+ InvalidFormat,
|
|
|
40
|
+ #[error("invalid address")]
|
|
|
41
|
+ InvalidAddress,
|
|
|
42
|
+ #[error("port 0 is not valid for bridges")]
|
|
|
43
|
+ InvalidPort,
|
|
|
44
|
+ #[error("invalid length of the bridge fingerprint")]
|
|
|
45
|
+ InvalidFingerprint,
|
|
|
46
|
+ #[error("arguments can be supplied only when a transport is in use")]
|
|
|
47
|
+ ArgsWithoutTransport,
|
|
|
48
|
+}
|
|
|
49
|
+
|
|
|
50
|
+pub fn parse_bridge_line(value: &[u8]) -> Result<Bridge, BridgeParseError> {
|
|
|
51
|
+ let caps = BRIDGE_REGEX
|
|
|
52
|
+ .captures(value)
|
|
|
53
|
+ .ok_or(BridgeParseError::InvalidFormat)?;
|
|
|
54
|
+
|
|
|
55
|
+ let transport = caps
|
|
|
56
|
+ .name("transport")
|
|
|
57
|
+ .map(|m| String::from_utf8_lossy(m.as_bytes()).into_owned());
|
|
|
58
|
+
|
|
|
59
|
+ let address: SocketAddr = caps
|
|
|
60
|
+ .name("addr")
|
|
|
61
|
+ .and_then(|a| str::from_utf8(a.as_bytes()).ok())
|
|
|
62
|
+ // This cannot really happen since the regex matched...
|
|
|
63
|
+ .ok_or(BridgeParseError::InvalidFormat)?
|
|
|
64
|
+ .parse()
|
|
|
65
|
+ // Rust validation is very generic, so it is not even worth to
|
|
|
66
|
+ // include in our error.
|
|
|
67
|
+ .map_err(|_| BridgeParseError::InvalidAddress)?;
|
|
|
68
|
+ if address.port() == 0 {
|
|
|
69
|
+ return Err(BridgeParseError::InvalidPort);
|
|
|
70
|
+ }
|
|
|
71
|
+
|
|
|
72
|
+ let fingerprint = caps
|
|
|
73
|
+ .name("fingerprint")
|
|
|
74
|
+ .and_then(|m| {
|
|
|
75
|
+ let mut fp = [0u8; 20];
|
|
|
76
|
+ // We match only hex characters in the regex, so this should error
|
|
|
77
|
+ // only if the size is wrong.
|
|
|
78
|
+ match hex::decode_to_slice(m.as_bytes(), &mut fp[..]) {
|
|
|
79
|
+ Ok(_) => Some(Ok(fp)),
|
|
|
80
|
+ Err(_) => Some(Err(BridgeParseError::InvalidFingerprint)),
|
|
|
81
|
+ }
|
|
|
82
|
+ })
|
|
|
83
|
+ .transpose()?;
|
|
|
84
|
+
|
|
|
85
|
+ let args = caps.name("args");
|
|
|
86
|
+ if transport.is_none() && args.is_some() {
|
|
|
87
|
+ // src/app/config/config.c, parse_bridge_line: "If transports are
|
|
|
88
|
+ // disabled, next field must be a fingerprint.".
|
|
|
89
|
+ return Err(BridgeParseError::ArgsWithoutTransport);
|
|
|
90
|
+ }
|
|
|
91
|
+
|
|
|
92
|
+ Ok(Bridge {
|
|
|
93
|
+ transport,
|
|
|
94
|
+ address,
|
|
|
95
|
+ fingerprint,
|
|
|
96
|
+ args: args.map(|m| m.as_bytes().to_vec()),
|
|
|
97
|
+ })
|
|
|
98
|
+}
|
|
|
99
|
+
|
|
|
100
|
+#[cfg(test)]
|
|
|
101
|
+mod tests {
|
|
|
102
|
+ use std::{
|
|
|
103
|
+ assert_eq,
|
|
|
104
|
+ net::{Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6},
|
|
|
105
|
+ };
|
|
|
106
|
+
|
|
|
107
|
+ use super::*;
|
|
|
108
|
+
|
|
|
109
|
+ #[test]
|
|
|
110
|
+ fn vanilla() {
|
|
|
111
|
+ {
|
|
|
112
|
+ let bridge = parse_bridge_line(b"192.168.1.4:443").unwrap();
|
|
|
113
|
+ assert_eq!(bridge.transport, None);
|
|
|
114
|
+ assert_eq!(
|
|
|
115
|
+ bridge.address,
|
|
|
116
|
+ SocketAddrV4::new(Ipv4Addr::new(192, 168, 1, 4), 443).into()
|
|
|
117
|
+ );
|
|
|
118
|
+ assert_eq!(bridge.fingerprint, None);
|
|
|
119
|
+ assert_eq!(bridge.args, None);
|
|
|
120
|
+ }
|
|
|
121
|
+ {
|
|
|
122
|
+ let bridge = parse_bridge_line(b"[dead:BEEF::1234]:443").unwrap();
|
|
|
123
|
+ assert_eq!(bridge.transport, None);
|
|
|
124
|
+ assert_eq!(
|
|
|
125
|
+ bridge.address,
|
|
|
126
|
+ SocketAddrV6::new(
|
|
|
127
|
+ Ipv6Addr::new(0xdead, 0xbeef, 0, 0, 0, 0, 0, 0x1234),
|
|
|
128
|
+ 443,
|
|
|
129
|
+ 0,
|
|
|
130
|
+ 0
|
|
|
131
|
+ )
|
|
|
132
|
+ .into()
|
|
|
133
|
+ );
|
|
|
134
|
+ assert_eq!(bridge.fingerprint, None);
|
|
|
135
|
+ assert_eq!(bridge.args, None);
|
|
|
136
|
+ }
|
|
|
137
|
+ }
|
|
|
138
|
+
|
|
|
139
|
+ #[test]
|
|
|
140
|
+ fn transport() {
|
|
|
141
|
+ {
|
|
|
142
|
+ let bridge = parse_bridge_line(b"test 1.2.3.4:567").unwrap();
|
|
|
143
|
+ assert_eq!(bridge.transport, Some(String::from("test")));
|
|
|
144
|
+ assert_eq!(
|
|
|
145
|
+ bridge.address,
|
|
|
146
|
+ SocketAddrV4::new(Ipv4Addr::new(1, 2, 3, 4), 567).into()
|
|
|
147
|
+ );
|
|
|
148
|
+ assert_eq!(bridge.fingerprint, None);
|
|
|
149
|
+ assert_eq!(bridge.args, None);
|
|
|
150
|
+ }
|
|
|
151
|
+ {
|
|
|
152
|
+ let bridge = parse_bridge_line(b"test [cafe:CAFE::BaBe]:567").unwrap();
|
|
|
153
|
+ assert_eq!(bridge.transport, Some(String::from("test")));
|
|
|
154
|
+ assert_eq!(
|
|
|
155
|
+ bridge.address,
|
|
|
156
|
+ SocketAddrV6::new(
|
|
|
157
|
+ Ipv6Addr::new(0xcafe, 0xcafe, 0, 0, 0, 0, 0, 0xbabe),
|
|
|
158
|
+ 567,
|
|
|
159
|
+ 0,
|
|
|
160
|
+ 0
|
|
|
161
|
+ )
|
|
|
162
|
+ .into()
|
|
|
163
|
+ );
|
|
|
164
|
+ assert_eq!(bridge.fingerprint, None);
|
|
|
165
|
+ assert_eq!(bridge.args, None);
|
|
|
166
|
+ }
|
|
|
167
|
+ }
|
|
|
168
|
+
|
|
|
169
|
+ #[test]
|
|
|
170
|
+ fn transport_fingerprint() {
|
|
|
171
|
+ {
|
|
|
172
|
+ let bridge = parse_bridge_line(
|
|
|
173
|
+ b"obfs4 37.218.245.14:38224 D9A82D2F9C2F65A18407B1D2B764F130847F8B5D",
|
|
|
174
|
+ )
|
|
|
175
|
+ .unwrap();
|
|
|
176
|
+ assert_eq!(bridge.transport, Some(String::from("obfs4")));
|
|
|
177
|
+ assert_eq!(
|
|
|
178
|
+ bridge.address,
|
|
|
179
|
+ SocketAddrV4::new(Ipv4Addr::new(37, 218, 245, 14), 38224).into()
|
|
|
180
|
+ );
|
|
|
181
|
+ assert_eq!(
|
|
|
182
|
+ bridge.fingerprint.as_ref().map(|fp| &fp[..]),
|
|
|
183
|
+ Some(
|
|
|
184
|
+ hex::decode("D9A82D2F9C2F65A18407B1D2B764F130847F8B5D")
|
|
|
185
|
+ .unwrap()
|
|
|
186
|
+ .as_slice()
|
|
|
187
|
+ )
|
|
|
188
|
+ );
|
|
|
189
|
+ assert_eq!(bridge.args, None);
|
|
|
190
|
+ }
|
|
|
191
|
+ {
|
|
|
192
|
+ let bridge = parse_bridge_line(
|
|
|
193
|
+ b"obfs4 [1234:56:789::abcd]:38224 D9A82D2F9C2F65A18407B1D2B764F130847F8B5D",
|
|
|
194
|
+ )
|
|
|
195
|
+ .unwrap();
|
|
|
196
|
+ assert_eq!(bridge.transport, Some(String::from("obfs4")));
|
|
|
197
|
+ assert_eq!(
|
|
|
198
|
+ bridge.address,
|
|
|
199
|
+ SocketAddrV6::new(
|
|
|
200
|
+ Ipv6Addr::new(0x1234, 0x56, 0x789, 0, 0, 0, 0, 0xabcd),
|
|
|
201
|
+ 38224,
|
|
|
202
|
+ 0,
|
|
|
203
|
+ 0
|
|
|
204
|
+ )
|
|
|
205
|
+ .into()
|
|
|
206
|
+ );
|
|
|
207
|
+ assert_eq!(
|
|
|
208
|
+ bridge.fingerprint.as_ref().map(|fp| &fp[..]),
|
|
|
209
|
+ Some(
|
|
|
210
|
+ hex::decode("D9A82D2F9C2F65A18407B1D2B764F130847F8B5D")
|
|
|
211
|
+ .unwrap()
|
|
|
212
|
+ .as_slice()
|
|
|
213
|
+ )
|
|
|
214
|
+ );
|
|
|
215
|
+ assert_eq!(bridge.args, None);
|
|
|
216
|
+ }
|
|
|
217
|
+ }
|
|
|
218
|
+
|
|
|
219
|
+ #[test]
|
|
|
220
|
+ fn all() {
|
|
|
221
|
+ {
|
|
|
222
|
+ let bridge = parse_bridge_line(
|
|
|
223
|
+ b"obfs4 37.218.245.14:38224 D9A82D2F9C2F65A18407B1D2B764F130847F8B5D iat-mode=0",
|
|
|
224
|
+ )
|
|
|
225
|
+ .unwrap();
|
|
|
226
|
+ assert_eq!(bridge.transport, Some(String::from("obfs4")));
|
|
|
227
|
+ assert_eq!(
|
|
|
228
|
+ bridge.address,
|
|
|
229
|
+ SocketAddrV4::new(Ipv4Addr::new(37, 218, 245, 14), 38224).into()
|
|
|
230
|
+ );
|
|
|
231
|
+ assert_eq!(
|
|
|
232
|
+ bridge.fingerprint.as_ref().map(|fp| &fp[..]),
|
|
|
233
|
+ Some(
|
|
|
234
|
+ hex::decode("D9A82D2F9C2F65A18407B1D2B764F130847F8B5D")
|
|
|
235
|
+ .unwrap()
|
|
|
236
|
+ .as_slice()
|
|
|
237
|
+ )
|
|
|
238
|
+ );
|
|
|
239
|
+ assert_eq!(&bridge.args.unwrap(), b"iat-mode=0");
|
|
|
240
|
+ }
|
|
|
241
|
+ {
|
|
|
242
|
+ let bridge = parse_bridge_line(
|
|
|
243
|
+ b"obfs4 [1234:56:789:0a:00b:000c:d:eeee]:38224 D9A82D2F9C2F65A18407B1D2B764F130847F8B5D iat-mode=0",
|
|
|
244
|
+ )
|
|
|
245
|
+ .unwrap();
|
|
|
246
|
+ assert_eq!(bridge.transport, Some(String::from("obfs4")));
|
|
|
247
|
+ assert_eq!(
|
|
|
248
|
+ bridge.address,
|
|
|
249
|
+ SocketAddrV6::new(
|
|
|
250
|
+ Ipv6Addr::new(0x1234, 0x56, 0x789, 0xa, 0xb, 0xc, 0xd, 0xeeee),
|
|
|
251
|
+ 38224,
|
|
|
252
|
+ 0,
|
|
|
253
|
+ 0
|
|
|
254
|
+ )
|
|
|
255
|
+ .into()
|
|
|
256
|
+ );
|
|
|
257
|
+ assert_eq!(
|
|
|
258
|
+ bridge.fingerprint.as_ref().map(|fp| &fp[..]),
|
|
|
259
|
+ Some(
|
|
|
260
|
+ hex::decode("D9A82D2F9C2F65A18407B1D2B764F130847F8B5D")
|
|
|
261
|
+ .unwrap()
|
|
|
262
|
+ .as_slice()
|
|
|
263
|
+ )
|
|
|
264
|
+ );
|
|
|
265
|
+ assert_eq!(&bridge.args.unwrap(), b"iat-mode=0");
|
|
|
266
|
+ }
|
|
|
267
|
+ }
|
|
|
268
|
+
|
|
|
269
|
+ #[test]
|
|
|
270
|
+ fn no_fingerprint() {
|
|
|
271
|
+ let bridge = parse_bridge_line(b"test 1.2.3.4:567 iat-mode=0").unwrap();
|
|
|
272
|
+ assert_eq!(bridge.transport.unwrap(), "test");
|
|
|
273
|
+ assert_eq!(
|
|
|
274
|
+ bridge.address,
|
|
|
275
|
+ SocketAddrV4::new(Ipv4Addr::new(1, 2, 3, 4), 567).into()
|
|
|
276
|
+ );
|
|
|
277
|
+ assert_eq!(bridge.fingerprint, None);
|
|
|
278
|
+ assert_eq!(&bridge.args.unwrap(), b"iat-mode=0");
|
|
|
279
|
+ }
|
|
|
280
|
+
|
|
|
281
|
+ #[test]
|
|
|
282
|
+ fn multiple_args_and_spaces() {
|
|
|
283
|
+ let bridge = parse_bridge_line(
|
|
|
284
|
+ b" obfs4 37.218.245.14:38224 D9A82D2F9C2F65A18407B1D2B764F130847F8B5D iat-mode=0 other-arg spaces-at-the-end ",
|
|
|
285
|
+ )
|
|
|
286
|
+ .unwrap();
|
|
|
287
|
+ assert_eq!(
|
|
|
288
|
+ &bridge.args.unwrap(),
|
|
|
289
|
+ b"iat-mode=0 other-arg spaces-at-the-end"
|
|
|
290
|
+ );
|
|
|
291
|
+ }
|
|
|
292
|
+
|
|
|
293
|
+ #[test]
|
|
|
294
|
+ fn failures() {
|
|
|
295
|
+ // Only IP address without a port. Does not even match the regex.
|
|
|
296
|
+ assert_eq!(
|
|
|
297
|
+ parse_bridge_line(b"192.168.1.4").unwrap_err(),
|
|
|
298
|
+ BridgeParseError::InvalidFormat,
|
|
|
299
|
+ );
|
|
|
300
|
+ // Invalid port (and again, does not match the regex).
|
|
|
301
|
+ assert_eq!(
|
|
|
302
|
+ parse_bridge_line(b"192.168.1.4:100000").unwrap_err(),
|
|
|
303
|
+ BridgeParseError::InvalidFormat,
|
|
|
304
|
+ );
|
|
|
305
|
+ // No address
|
|
|
306
|
+ assert_eq!(
|
|
|
307
|
+ parse_bridge_line(b"obfs4 D9A82D2F9C2F65A18407B1D2B764F130847F8B5D").unwrap_err(),
|
|
|
308
|
+ BridgeParseError::InvalidFormat,
|
|
|
309
|
+ );
|
|
|
310
|
+ // No address and bad fingerprint (bad no address prevails).
|
|
|
311
|
+ assert_eq!(
|
|
|
312
|
+ parse_bridge_line(b"obfs4 aaa1234").unwrap_err(),
|
|
|
313
|
+ BridgeParseError::InvalidFormat,
|
|
|
314
|
+ );
|
|
|
315
|
+
|
|
|
316
|
+ assert_eq!(
|
|
|
317
|
+ parse_bridge_line(b"192.168.1.375:1234").unwrap_err(),
|
|
|
318
|
+ BridgeParseError::InvalidAddress,
|
|
|
319
|
+ );
|
|
|
320
|
+ assert_eq!(
|
|
|
321
|
+ parse_bridge_line(b"192.168.1.4:0").unwrap_err(),
|
|
|
322
|
+ BridgeParseError::InvalidPort,
|
|
|
323
|
+ );
|
|
|
324
|
+ assert_eq!(
|
|
|
325
|
+ parse_bridge_line(b"192.168.1.4:99999").unwrap_err(),
|
|
|
326
|
+ BridgeParseError::InvalidAddress,
|
|
|
327
|
+ );
|
|
|
328
|
+ assert_eq!(
|
|
|
329
|
+ parse_bridge_line(b"192.168.1.4:65536").unwrap_err(),
|
|
|
330
|
+ BridgeParseError::InvalidAddress,
|
|
|
331
|
+ );
|
|
|
332
|
+
|
|
|
333
|
+ assert_eq!(
|
|
|
334
|
+ parse_bridge_line(b"1.2.3.4:443 aaaaaaaaa").unwrap_err(),
|
|
|
335
|
+ BridgeParseError::InvalidFingerprint
|
|
|
336
|
+ );
|
|
|
337
|
+ assert_eq!(
|
|
|
338
|
+ parse_bridge_line(
|
|
|
339
|
+ b"1.2.3.4:443 0123456789012345678901234567890123456789aaaaa"
|
|
|
340
|
+ )
|
|
|
341
|
+ .unwrap_err(),
|
|
|
342
|
+ BridgeParseError::InvalidFingerprint
|
|
|
343
|
+ );
|
|
|
344
|
+
|
|
|
345
|
+ assert_eq!(
|
|
|
346
|
+ parse_bridge_line(
|
|
|
347
|
+ b"1.2.3.4:443 0123456789012345678901234567890123456789 key=value"
|
|
|
348
|
+ ).unwrap_err(),
|
|
|
349
|
+ BridgeParseError::ArgsWithoutTransport,
|
|
|
350
|
+ );
|
|
|
351
|
+ assert_eq!(
|
|
|
352
|
+ parse_bridge_line(
|
|
|
353
|
+ b"1.2.3.4:443 key=value"
|
|
|
354
|
+ ).unwrap_err(),
|
|
|
355
|
+ BridgeParseError::ArgsWithoutTransport,
|
|
|
356
|
+ );
|
|
|
357
|
+ }
|
|
|
358
|
+} |
toolkit/components/tor-integration/tor_provider/src/ctor/controller/parsers/client_transport_plugin.rs
|
|
1
|
+// Licensed under the Apache License, Version 2.0,
|
|
|
2
|
+// <http://apache.org/licenses/LICENSE-2.0> or the MIT license
|
|
|
3
|
+// <http://opensource.org/licenses/MIT>, at your option. This file may not be
|
|
|
4
|
+// copied, modified, or distributed except according to those terms.
|
|
|
5
|
+
|
|
|
6
|
+use lazy_static::lazy_static;
|
|
|
7
|
+use regex::bytes::{Captures, Regex};
|
|
|
8
|
+
|
|
|
9
|
+use super::tor_unescape;
|
|
|
10
|
+use crate::ctor::controller::{ClientTransportPlugin, ControllerError, PluggableTransport};
|
|
|
11
|
+
|
|
|
12
|
+lazy_static! {
|
|
|
13
|
+ // man 1 tor: ClientTransportPlugin transport socks4|socks5 IP:PORT
|
|
|
14
|
+ static ref SOCKS_REGEX: Regex =
|
|
|
15
|
+ Regex::new(r"^(?<transport>[A-Za-z_][A-Za-z0-9_,]*) (?<protocol>socks[45]) (?<address>(?:[\d\.]{7,15}|\[[\da-fA-F:]+\]):\d{1,5})$")
|
|
|
16
|
+ .expect("The regex is hardcoded, it should be good.");
|
|
|
17
|
+ // man 1 tor: transport exec path-to-binary [options]
|
|
|
18
|
+ static ref EXEC_REGEX: Regex = Regex::new(r#"^(?<transport>[A-Za-z_][A-Za-z0-9_,]*) exec (?<path>"(?:[^"\\]|\\.)*"|[^ ]+)(?: (?<options>.*?) *)?$"#)
|
|
|
19
|
+ .expect("The regex is hardcoded, it should be good.");
|
|
|
20
|
+}
|
|
|
21
|
+
|
|
|
22
|
+pub fn parse_client_transport_plugin(line: &[u8]) -> Result<PluggableTransport, ControllerError> {
|
|
|
23
|
+ if let Some(socks_line) = SOCKS_REGEX.captures(line) {
|
|
|
24
|
+ parse_socks(&socks_line)
|
|
|
25
|
+ } else if let Some(exec_line) = EXEC_REGEX.captures(line) {
|
|
|
26
|
+ parse_exec(&exec_line)
|
|
|
27
|
+ } else {
|
|
|
28
|
+ Err(ControllerError::WrongFormat(
|
|
|
29
|
+ String::from_utf8_lossy(line).into_owned(),
|
|
|
30
|
+ ))
|
|
|
31
|
+ }
|
|
|
32
|
+}
|
|
|
33
|
+
|
|
|
34
|
+fn get_transports(c: &Captures) -> Result<Vec<String>, ControllerError> {
|
|
|
35
|
+ let transports = c
|
|
|
36
|
+ .name("transport")
|
|
|
37
|
+ .ok_or(ControllerError::KeyNotFound(String::from("transport")))?
|
|
|
38
|
+ .as_bytes()
|
|
|
39
|
+ .split(|b| *b == b',')
|
|
|
40
|
+ .map(|t| String::from_utf8_lossy(t).into_owned())
|
|
|
41
|
+ .collect::<Vec<_>>();
|
|
|
42
|
+ if transports.is_empty() || transports.iter().any(|t| t.is_empty()) {
|
|
|
43
|
+ return Err(ControllerError::MalformedReply(String::from(
|
|
|
44
|
+ "empty transports",
|
|
|
45
|
+ )));
|
|
|
46
|
+ }
|
|
|
47
|
+ Ok(transports)
|
|
|
48
|
+}
|
|
|
49
|
+
|
|
|
50
|
+fn parse_socks(socks_line: &Captures) -> Result<PluggableTransport, ControllerError> {
|
|
|
51
|
+ let address = str::from_utf8(
|
|
|
52
|
+ socks_line
|
|
|
53
|
+ .name("address")
|
|
|
54
|
+ .ok_or(ControllerError::KeyNotFound(String::from("address")))?
|
|
|
55
|
+ .as_bytes(),
|
|
|
56
|
+ )
|
|
|
57
|
+ .expect("The regex matches only ASCII characters (hence valid UTF-8)")
|
|
|
58
|
+ .parse()
|
|
|
59
|
+ .map_err(|e| ControllerError::MalformedReply(format!("{}", e)))?;
|
|
|
60
|
+ let plugin = match socks_line
|
|
|
61
|
+ .name("protocol")
|
|
|
62
|
+ .ok_or(ControllerError::KeyNotFound(String::from("protocol")))?
|
|
|
63
|
+ .as_bytes()
|
|
|
64
|
+ {
|
|
|
65
|
+ b"socks4" => ClientTransportPlugin::Socks4(address),
|
|
|
66
|
+ b"socks5" => ClientTransportPlugin::Socks5(address),
|
|
|
67
|
+ _ => unreachable!("We validated this with the regex"),
|
|
|
68
|
+ };
|
|
|
69
|
+ Ok(PluggableTransport {
|
|
|
70
|
+ transports: get_transports(&socks_line)?,
|
|
|
71
|
+ plugin,
|
|
|
72
|
+ })
|
|
|
73
|
+}
|
|
|
74
|
+
|
|
|
75
|
+fn parse_exec(exec_line: &Captures) -> Result<PluggableTransport, ControllerError> {
|
|
|
76
|
+ let options = exec_line
|
|
|
77
|
+ .name("options")
|
|
|
78
|
+ .map(|o| o.as_bytes())
|
|
|
79
|
+ .and_then(|s| if s.is_empty() { None } else { Some(s.into()) });
|
|
|
80
|
+ let path = tor_unescape(
|
|
|
81
|
+ exec_line
|
|
|
82
|
+ .name("path")
|
|
|
83
|
+ .ok_or(ControllerError::KeyNotFound(String::from("path")))?
|
|
|
84
|
+ .as_bytes(),
|
|
|
85
|
+ )
|
|
|
86
|
+ .map_err(|e| ControllerError::MalformedReply(e.to_string()))?
|
|
|
87
|
+ .into_owned();
|
|
|
88
|
+ if path.is_empty() {
|
|
|
89
|
+ return Err(ControllerError::MalformedReply(String::from("empty path")));
|
|
|
90
|
+ }
|
|
|
91
|
+ Ok(PluggableTransport {
|
|
|
92
|
+ transports: get_transports(&exec_line)?,
|
|
|
93
|
+ plugin: ClientTransportPlugin::Executable { path, options },
|
|
|
94
|
+ })
|
|
|
95
|
+}
|
|
|
96
|
+
|
|
|
97
|
+#[cfg(test)]
|
|
|
98
|
+mod tests {
|
|
|
99
|
+ use std::{
|
|
|
100
|
+ assert_matches,
|
|
|
101
|
+ net::{Ipv4Addr, SocketAddrV4},
|
|
|
102
|
+ };
|
|
|
103
|
+
|
|
|
104
|
+ use super::*;
|
|
|
105
|
+
|
|
|
106
|
+ #[test]
|
|
|
107
|
+ fn socks() {
|
|
|
108
|
+ {
|
|
|
109
|
+ let pt =
|
|
|
110
|
+ parse_client_transport_plugin(b"mytransport,mypt2 socks4 127.0.0.1:9052").unwrap();
|
|
|
111
|
+ assert_eq!(pt.transports, vec!["mytransport", "mypt2"]);
|
|
|
112
|
+ assert_eq!(
|
|
|
113
|
+ pt.plugin,
|
|
|
114
|
+ ClientTransportPlugin::Socks4(
|
|
|
115
|
+ SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 9052).into()
|
|
|
116
|
+ )
|
|
|
117
|
+ );
|
|
|
118
|
+ }
|
|
|
119
|
+ {
|
|
|
120
|
+ let pt = parse_client_transport_plugin(b"pt3,pt_4 socks5 10.0.1.2:9052").unwrap();
|
|
|
121
|
+ assert_eq!(pt.transports, vec!["pt3", "pt_4"]);
|
|
|
122
|
+ assert_eq!(
|
|
|
123
|
+ pt.plugin,
|
|
|
124
|
+ ClientTransportPlugin::Socks5(
|
|
|
125
|
+ SocketAddrV4::new(Ipv4Addr::new(10, 0, 1, 2), 9052).into()
|
|
|
126
|
+ )
|
|
|
127
|
+ );
|
|
|
128
|
+ }
|
|
|
129
|
+ {
|
|
|
130
|
+ let pt = parse_client_transport_plugin(
|
|
|
131
|
+ b"mytransport,mypt2 socks4 [38e5:09fc:2080:673d:9ab8:4545:08db:36c2]:1234",
|
|
|
132
|
+ )
|
|
|
133
|
+ .unwrap();
|
|
|
134
|
+ assert_eq!(pt.transports, vec!["mytransport", "mypt2"]);
|
|
|
135
|
+ assert_eq!(
|
|
|
136
|
+ pt.plugin,
|
|
|
137
|
+ ClientTransportPlugin::Socks4(
|
|
|
138
|
+ "[38e5:09fc:2080:673d:9ab8:4545:08db:36c2]:1234"
|
|
|
139
|
+ .parse()
|
|
|
140
|
+ .unwrap()
|
|
|
141
|
+ )
|
|
|
142
|
+ );
|
|
|
143
|
+ }
|
|
|
144
|
+ {
|
|
|
145
|
+ let pt = parse_client_transport_plugin(
|
|
|
146
|
+ b"s5pt socks5 [dad7:ddf4:7fc4:ef92:4bb2:ceb9:05b9:9161]:5555",
|
|
|
147
|
+ )
|
|
|
148
|
+ .unwrap();
|
|
|
149
|
+ assert_eq!(pt.transports, vec!["s5pt"]);
|
|
|
150
|
+ assert_eq!(
|
|
|
151
|
+ pt.plugin,
|
|
|
152
|
+ ClientTransportPlugin::Socks5(
|
|
|
153
|
+ "[dad7:ddf4:7fc4:ef92:4bb2:ceb9:05b9:9161]:5555"
|
|
|
154
|
+ .parse()
|
|
|
155
|
+ .unwrap()
|
|
|
156
|
+ )
|
|
|
157
|
+ );
|
|
|
158
|
+ }
|
|
|
159
|
+ }
|
|
|
160
|
+
|
|
|
161
|
+ #[test]
|
|
|
162
|
+ fn exec_multiple_transports() {
|
|
|
163
|
+ let pt = parse_client_transport_plugin(b"obfs4,meek exec lyrebird").unwrap();
|
|
|
164
|
+ assert_eq!(
|
|
|
165
|
+ pt.transports,
|
|
|
166
|
+ vec![String::from("obfs4"), String::from("meek")]
|
|
|
167
|
+ );
|
|
|
168
|
+ assert_eq!(
|
|
|
169
|
+ pt.plugin,
|
|
|
170
|
+ ClientTransportPlugin::Executable {
|
|
|
171
|
+ path: Vec::from(b"lyrebird"),
|
|
|
172
|
+ options: None,
|
|
|
173
|
+ }
|
|
|
174
|
+ );
|
|
|
175
|
+ }
|
|
|
176
|
+
|
|
|
177
|
+ #[test]
|
|
|
178
|
+ fn exec_with_spaces() {
|
|
|
179
|
+ let pt =
|
|
|
180
|
+ parse_client_transport_plugin(b"snowflake exec \"pluggable transports/snowflake\"")
|
|
|
181
|
+ .unwrap();
|
|
|
182
|
+ assert_eq!(pt.transports, vec![String::from("snowflake")]);
|
|
|
183
|
+ assert_eq!(
|
|
|
184
|
+ pt.plugin,
|
|
|
185
|
+ ClientTransportPlugin::Executable {
|
|
|
186
|
+ path: Vec::from("pluggable transports/snowflake"),
|
|
|
187
|
+ options: None,
|
|
|
188
|
+ }
|
|
|
189
|
+ )
|
|
|
190
|
+ }
|
|
|
191
|
+
|
|
|
192
|
+ #[test]
|
|
|
193
|
+ fn exec_windows_path() {
|
|
|
194
|
+ let pt = parse_client_transport_plugin(
|
|
|
195
|
+ b"snowflake exec \"C:\\\\Program Files\\\\Tor Project \\\\snowflake.exe\"",
|
|
|
196
|
+ )
|
|
|
197
|
+ .unwrap();
|
|
|
198
|
+ assert_eq!(pt.transports, vec![String::from("snowflake")]);
|
|
|
199
|
+ assert_eq!(
|
|
|
200
|
+ pt.plugin,
|
|
|
201
|
+ ClientTransportPlugin::Executable {
|
|
|
202
|
+ path: Vec::from("C:\\Program Files\\Tor Project \\snowflake.exe"),
|
|
|
203
|
+ options: None,
|
|
|
204
|
+ }
|
|
|
205
|
+ )
|
|
|
206
|
+ }
|
|
|
207
|
+
|
|
|
208
|
+ #[test]
|
|
|
209
|
+ fn exec_with_options() {
|
|
|
210
|
+ let pt = parse_client_transport_plugin(
|
|
|
211
|
+ b"conjure,coupdetat exec conjure-client -registerURL https://registration.refraction.network/api ",
|
|
|
212
|
+ )
|
|
|
213
|
+ .unwrap();
|
|
|
214
|
+ assert_eq!(
|
|
|
215
|
+ pt.transports,
|
|
|
216
|
+ vec![String::from("conjure"), String::from("coupdetat")]
|
|
|
217
|
+ );
|
|
|
218
|
+ assert_eq!(
|
|
|
219
|
+ pt.plugin,
|
|
|
220
|
+ ClientTransportPlugin::Executable {
|
|
|
221
|
+ path: Vec::from("conjure-client"),
|
|
|
222
|
+ options: Some(b"-registerURL https://registration.refraction.network/api".into())
|
|
|
223
|
+ }
|
|
|
224
|
+ )
|
|
|
225
|
+ }
|
|
|
226
|
+
|
|
|
227
|
+ #[test]
|
|
|
228
|
+ fn invalid() {
|
|
|
229
|
+ assert_matches!(
|
|
|
230
|
+ parse_client_transport_plugin(b"transport socks2 127.0.0.1:9052").unwrap_err(),
|
|
|
231
|
+ ControllerError::WrongFormat(_)
|
|
|
232
|
+ );
|
|
|
233
|
+ }
|
|
|
234
|
+
|
|
|
235
|
+ #[test]
|
|
|
236
|
+ fn socks_errors() {
|
|
|
237
|
+ assert_matches!(
|
|
|
238
|
+ parse_client_transport_plugin(b"transport socks4").unwrap_err(),
|
|
|
239
|
+ ControllerError::WrongFormat(_)
|
|
|
240
|
+ );
|
|
|
241
|
+
|
|
|
242
|
+ assert_matches!(
|
|
|
243
|
+ parse_client_transport_plugin(b"transport socks4 123.45.6.7.8:123456").unwrap_err(),
|
|
|
244
|
+ ControllerError::WrongFormat(_)
|
|
|
245
|
+ );
|
|
|
246
|
+
|
|
|
247
|
+ assert_matches!(
|
|
|
248
|
+ parse_client_transport_plugin(b", socks4 123.45.6.7.8:1234").unwrap_err(),
|
|
|
249
|
+ ControllerError::WrongFormat(_)
|
|
|
250
|
+ );
|
|
|
251
|
+
|
|
|
252
|
+ assert_matches!(
|
|
|
253
|
+ parse_client_transport_plugin(b"transport socks4 1234.5.6.7.8:123").unwrap_err(),
|
|
|
254
|
+ ControllerError::MalformedReply(_)
|
|
|
255
|
+ );
|
|
|
256
|
+
|
|
|
257
|
+ assert_matches!(
|
|
|
258
|
+ parse_client_transport_plugin(b"transport socks4 273.12.34.56:789").unwrap_err(),
|
|
|
259
|
+ ControllerError::MalformedReply(_)
|
|
|
260
|
+ );
|
|
|
261
|
+
|
|
|
262
|
+ assert_matches!(
|
|
|
263
|
+ parse_client_transport_plugin(b"transport socks4 123.12.34.56:78999").unwrap_err(),
|
|
|
264
|
+ ControllerError::MalformedReply(_)
|
|
|
265
|
+ );
|
|
|
266
|
+ }
|
|
|
267
|
+
|
|
|
268
|
+ #[test]
|
|
|
269
|
+ fn exec_errors() {
|
|
|
270
|
+ assert_matches!(
|
|
|
271
|
+ parse_client_transport_plugin(b"transport exec").unwrap_err(),
|
|
|
272
|
+ ControllerError::WrongFormat(_)
|
|
|
273
|
+ );
|
|
|
274
|
+
|
|
|
275
|
+ assert_matches!(
|
|
|
276
|
+ parse_client_transport_plugin(b"transport exec \"\"").unwrap_err(),
|
|
|
277
|
+ ControllerError::MalformedReply(_)
|
|
|
278
|
+ );
|
|
|
279
|
+
|
|
|
280
|
+ assert_matches!(
|
|
|
281
|
+ parse_client_transport_plugin(b"transport exec \"unterminated").unwrap_err(),
|
|
|
282
|
+ ControllerError::MalformedReply(_)
|
|
|
283
|
+ );
|
|
|
284
|
+
|
|
|
285
|
+ assert_matches!(
|
|
|
286
|
+ parse_client_transport_plugin(b"transport exec \"invalid hex \\xAz\"").unwrap_err(),
|
|
|
287
|
+ ControllerError::MalformedReply(_)
|
|
|
288
|
+ );
|
|
|
289
|
+
|
|
|
290
|
+ assert_matches!(
|
|
|
291
|
+ parse_client_transport_plugin(b", exec mypt.exe").unwrap_err(),
|
|
|
292
|
+ ControllerError::WrongFormat(_)
|
|
|
293
|
+ );
|
|
|
294
|
+ assert_matches!(
|
|
|
295
|
+ parse_client_transport_plugin(b"garbage trans,port exec mypt.exe").unwrap_err(),
|
|
|
296
|
+ ControllerError::WrongFormat(_)
|
|
|
297
|
+ );
|
|
|
298
|
+ }
|
|
|
299
|
+} |
toolkit/components/tor-integration/tor_provider/src/ctor/controller/parsers/mod.rs
| ... |
... |
@@ -4,6 +4,8 @@ |
|
4
|
4
|
// copied, modified, or distributed except according to those terms.
|
|
5
|
5
|
|
|
6
|
6
|
mod ack;
|
|
|
7
|
+mod bridge_line;
|
|
|
8
|
+mod client_transport_plugin;
|
|
7
|
9
|
mod escape;
|
|
8
|
10
|
mod unescape;
|
|
9
|
11
|
|
| ... |
... |
@@ -11,5 +13,7 @@ mod unescape; |
|
11
|
13
|
mod tests;
|
|
12
|
14
|
|
|
13
|
15
|
pub use ack::parse_ack;
|
|
|
16
|
+pub use bridge_line::parse_bridge_line;
|
|
|
17
|
+pub use client_transport_plugin::parse_client_transport_plugin;
|
|
14
|
18
|
pub use escape::tor_escape_into;
|
|
15
|
19
|
pub use unescape::tor_unescape; |
toolkit/components/tor-integration/tor_provider/src/ctor/controller/types.rs
|
|
1
|
+// Licensed under the Apache License, Version 2.0,
|
|
|
2
|
+// <http://apache.org/licenses/LICENSE-2.0> or the MIT license
|
|
|
3
|
+// <http://opensource.org/licenses/MIT>, at your option. This file may not be
|
|
|
4
|
+// copied, modified, or distributed except according to those terms.
|
|
|
5
|
+
|
|
|
6
|
+use serde::Serialize;
|
|
|
7
|
+use std::net::SocketAddr;
|
|
|
8
|
+
|
|
|
9
|
+#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
|
|
10
|
+pub struct Bridge {
|
|
|
11
|
+ pub transport: Option<String>,
|
|
|
12
|
+ pub address: SocketAddr,
|
|
|
13
|
+ pub fingerprint: Option<[u8; 20]>,
|
|
|
14
|
+ pub args: Option<Vec<u8>>,
|
|
|
15
|
+}
|
|
|
16
|
+
|
|
|
17
|
+#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
|
|
18
|
+pub enum ClientTransportPlugin {
|
|
|
19
|
+ Executable {
|
|
|
20
|
+ path: Vec<u8>,
|
|
|
21
|
+ options: Option<Vec<u8>>,
|
|
|
22
|
+ },
|
|
|
23
|
+ Socks4(SocketAddr),
|
|
|
24
|
+ Socks5(SocketAddr),
|
|
|
25
|
+}
|
|
|
26
|
+
|
|
|
27
|
+#[derive(Debug, Clone)]
|
|
|
28
|
+pub enum ConfValue<'a> {
|
|
|
29
|
+ Bool(bool),
|
|
|
30
|
+ Int(i32),
|
|
|
31
|
+ String(&'a str),
|
|
|
32
|
+ Array(Vec<&'a str>),
|
|
|
33
|
+ Null,
|
|
|
34
|
+}
|
|
|
35
|
+
|
|
|
36
|
+#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
|
|
37
|
+pub struct PluggableTransport {
|
|
|
38
|
+ pub transports: Vec<String>,
|
|
|
39
|
+ pub plugin: ClientTransportPlugin,
|
|
|
40
|
+} |
|