New signer token RPC & Initial signer connection without token. (#2096)
* Allowing signer to use initial token * Generating new tokens via RPC * Fixing RPC tests * Fixing signer doctest * whitespace [ci:skip] * whitespace [ci:skip]
This commit is contained in:
@@ -48,6 +48,7 @@ impl TimeProvider for DefaultTimeProvider {
|
||||
/// No of seconds the hash is valid
|
||||
const TIME_THRESHOLD: u64 = 7;
|
||||
const TOKEN_LENGTH: usize = 16;
|
||||
const INITIAL_TOKEN: &'static str = "initial";
|
||||
|
||||
/// Manages authorization codes for `SignerUIs`
|
||||
pub struct AuthCodes<T: TimeProvider = DefaultTimeProvider> {
|
||||
@@ -98,7 +99,7 @@ impl<T: TimeProvider> AuthCodes<T> {
|
||||
}
|
||||
|
||||
/// Checks if given hash is correct identifier of `SignerUI`
|
||||
pub fn is_valid(&self, hash: &H256, time: u64) -> bool {
|
||||
pub fn is_valid(&mut self, hash: &H256, time: u64) -> bool {
|
||||
let now = self.now.now();
|
||||
// check time
|
||||
if time >= now + TIME_THRESHOLD || time <= now - TIME_THRESHOLD {
|
||||
@@ -106,9 +107,21 @@ impl<T: TimeProvider> AuthCodes<T> {
|
||||
return false;
|
||||
}
|
||||
|
||||
let as_token = |code| format!("{}:{}", code, time).sha3();
|
||||
|
||||
// Check if it's the initial token.
|
||||
if self.is_empty() {
|
||||
let initial = &as_token(INITIAL_TOKEN) == hash;
|
||||
// Initial token can be used only once.
|
||||
if initial {
|
||||
let _ = self.generate_new();
|
||||
}
|
||||
return initial;
|
||||
}
|
||||
|
||||
// look for code
|
||||
self.codes.iter()
|
||||
.any(|code| &format!("{}:{}", code, time).sha3() == hash)
|
||||
.any(|code| &as_token(code) == hash)
|
||||
}
|
||||
|
||||
/// Generates and returns a new code that can be used by `SignerUIs`
|
||||
@@ -124,6 +137,11 @@ impl<T: TimeProvider> AuthCodes<T> {
|
||||
self.codes.push(code);
|
||||
Ok(readable_code)
|
||||
}
|
||||
|
||||
/// Returns true if there are no tokens in this store
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.codes.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -137,12 +155,28 @@ mod tests {
|
||||
format!("{}:{}", val, time).sha3()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_return_true_if_code_is_initial_and_store_is_empty() {
|
||||
// given
|
||||
let code = "initial";
|
||||
let time = 99;
|
||||
let mut codes = AuthCodes::new(vec![], || 100);
|
||||
|
||||
// when
|
||||
let res1 = codes.is_valid(&generate_hash(code, time), time);
|
||||
let res2 = codes.is_valid(&generate_hash(code, time), time);
|
||||
|
||||
// then
|
||||
assert_eq!(res1, true);
|
||||
assert_eq!(res2, false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_return_true_if_hash_is_valid() {
|
||||
// given
|
||||
let code = "23521352asdfasdfadf";
|
||||
let time = 99;
|
||||
let codes = AuthCodes::new(vec![code.into()], || 100);
|
||||
let mut codes = AuthCodes::new(vec![code.into()], || 100);
|
||||
|
||||
// when
|
||||
let res = codes.is_valid(&generate_hash(code, time), time);
|
||||
@@ -156,7 +190,7 @@ mod tests {
|
||||
// given
|
||||
let code = "23521352asdfasdfadf";
|
||||
let time = 99;
|
||||
let codes = AuthCodes::new(vec!["1".into()], || 100);
|
||||
let mut codes = AuthCodes::new(vec!["1".into()], || 100);
|
||||
|
||||
// when
|
||||
let res = codes.is_valid(&generate_hash(code, time), time);
|
||||
@@ -171,7 +205,7 @@ mod tests {
|
||||
let code = "23521352asdfasdfadf";
|
||||
let time = 107;
|
||||
let time2 = 93;
|
||||
let codes = AuthCodes::new(vec![code.into()], || 100);
|
||||
let mut codes = AuthCodes::new(vec![code.into()], || 100);
|
||||
|
||||
// when
|
||||
let res1 = codes.is_valid(&generate_hash(code, time), time);
|
||||
|
||||
@@ -14,24 +14,48 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use std::env;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use std::time::{self, Duration};
|
||||
use std::sync::Arc;
|
||||
use devtools::http_client;
|
||||
use devtools::{http_client, RandomTempPath};
|
||||
use rpc::ConfirmationsQueue;
|
||||
use util::Hashable;
|
||||
use rand;
|
||||
|
||||
use ServerBuilder;
|
||||
use Server;
|
||||
use AuthCodes;
|
||||
|
||||
pub fn serve() -> Server {
|
||||
pub struct GuardedAuthCodes {
|
||||
authcodes: AuthCodes,
|
||||
path: RandomTempPath,
|
||||
}
|
||||
impl Deref for GuardedAuthCodes {
|
||||
type Target = AuthCodes;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.authcodes
|
||||
}
|
||||
}
|
||||
impl DerefMut for GuardedAuthCodes {
|
||||
fn deref_mut(&mut self) -> &mut AuthCodes {
|
||||
&mut self.authcodes
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serve() -> (Server, usize, GuardedAuthCodes) {
|
||||
let mut path = RandomTempPath::new();
|
||||
path.panic_on_drop_failure = false;
|
||||
let queue = Arc::new(ConfirmationsQueue::default());
|
||||
let builder = ServerBuilder::new(queue, env::temp_dir());
|
||||
let builder = ServerBuilder::new(queue, path.to_path_buf());
|
||||
let port = 35000 + rand::random::<usize>() % 10000;
|
||||
let res = builder.start(format!("127.0.0.1:{}", port).parse().unwrap()).unwrap();
|
||||
thread::sleep(Duration::from_millis(25));
|
||||
res
|
||||
|
||||
(res, port, GuardedAuthCodes {
|
||||
authcodes: AuthCodes::from_file(&path).unwrap(),
|
||||
path: path,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn request(server: Server, request: &str) -> http_client::Response {
|
||||
@@ -41,7 +65,7 @@ pub fn request(server: Server, request: &str) -> http_client::Response {
|
||||
#[test]
|
||||
fn should_reject_invalid_host() {
|
||||
// given
|
||||
let server = serve();
|
||||
let server = serve().0;
|
||||
|
||||
// when
|
||||
let response = request(server,
|
||||
@@ -62,7 +86,7 @@ fn should_reject_invalid_host() {
|
||||
#[test]
|
||||
fn should_serve_styles_even_on_disallowed_domain() {
|
||||
// given
|
||||
let server = serve();
|
||||
let server = serve().0;
|
||||
|
||||
// when
|
||||
let response = request(server,
|
||||
@@ -79,3 +103,103 @@ fn should_serve_styles_even_on_disallowed_domain() {
|
||||
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_block_if_authorization_is_incorrect() {
|
||||
// given
|
||||
let (server, port, _) = serve();
|
||||
|
||||
// when
|
||||
let response = request(server,
|
||||
&format!("\
|
||||
GET / HTTP/1.1\r\n\
|
||||
Host: 127.0.0.1:{}\r\n\
|
||||
Connection: Upgrade\r\n\
|
||||
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==\r\n\
|
||||
Sec-WebSocket-Protocol: wrong\r\n\
|
||||
Sec-WebSocket-Version: 13\r\n\
|
||||
\r\n\
|
||||
{{}}
|
||||
", port)
|
||||
);
|
||||
|
||||
// then
|
||||
assert_eq!(response.status, "HTTP/1.1 403 FORBIDDEN".to_owned());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_allow_if_authorization_is_correct() {
|
||||
// given
|
||||
let (server, port, mut authcodes) = serve();
|
||||
let code = authcodes.generate_new().unwrap().replace("-", "");
|
||||
authcodes.to_file(&authcodes.path).unwrap();
|
||||
let timestamp = time::UNIX_EPOCH.elapsed().unwrap().as_secs();
|
||||
|
||||
// when
|
||||
let response = request(server,
|
||||
&format!("\
|
||||
GET / HTTP/1.1\r\n\
|
||||
Host: 127.0.0.1:{}\r\n\
|
||||
Connection: Close\r\n\
|
||||
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==\r\n\
|
||||
Sec-WebSocket-Protocol: {:?}_{}\r\n\
|
||||
Sec-WebSocket-Version: 13\r\n\
|
||||
\r\n\
|
||||
{{}}
|
||||
",
|
||||
port,
|
||||
format!("{}:{}", code, timestamp).sha3(),
|
||||
timestamp,
|
||||
)
|
||||
);
|
||||
|
||||
// then
|
||||
assert_eq!(response.status, "HTTP/1.1 101 Switching Protocols".to_owned());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_allow_initial_connection_but_only_once() {
|
||||
// given
|
||||
let (server, port, authcodes) = serve();
|
||||
let code = "initial";
|
||||
let timestamp = time::UNIX_EPOCH.elapsed().unwrap().as_secs();
|
||||
assert!(authcodes.is_empty());
|
||||
|
||||
// when
|
||||
let response1 = http_client::request(server.addr(),
|
||||
&format!("\
|
||||
GET / HTTP/1.1\r\n\
|
||||
Host: 127.0.0.1:{}\r\n\
|
||||
Connection: Close\r\n\
|
||||
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==\r\n\
|
||||
Sec-WebSocket-Protocol:{:?}_{}\r\n\
|
||||
Sec-WebSocket-Version: 13\r\n\
|
||||
\r\n\
|
||||
{{}}
|
||||
",
|
||||
port,
|
||||
format!("{}:{}", code, timestamp).sha3(),
|
||||
timestamp,
|
||||
)
|
||||
);
|
||||
let response2 = http_client::request(server.addr(),
|
||||
&format!("\
|
||||
GET / HTTP/1.1\r\n\
|
||||
Host: 127.0.0.1:{}\r\n\
|
||||
Connection: Close\r\n\
|
||||
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==\r\n\
|
||||
Sec-WebSocket-Protocol:{:?}_{}\r\n\
|
||||
Sec-WebSocket-Version: 13\r\n\
|
||||
\r\n\
|
||||
{{}}
|
||||
",
|
||||
port,
|
||||
format!("{}:{}", code, timestamp).sha3(),
|
||||
timestamp,
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
// then
|
||||
assert_eq!(response1.status, "HTTP/1.1 101 Switching Protocols".to_owned());
|
||||
assert_eq!(response2.status, "HTTP/1.1 403 FORBIDDEN".to_owned());
|
||||
}
|
||||
|
||||
@@ -180,7 +180,6 @@ impl Drop for Server {
|
||||
self.queue.finish();
|
||||
self.broadcaster_handle.take().unwrap().join().unwrap();
|
||||
self.handle.take().unwrap().join().unwrap();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ fn origin_is_allowed(self_origin: &str, header: Option<&[u8]>) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
fn auth_is_valid(codes: &Path, protocols: ws::Result<Vec<&str>>) -> bool {
|
||||
fn auth_is_valid(codes_path: &Path, protocols: ws::Result<Vec<&str>>) -> bool {
|
||||
match protocols {
|
||||
Ok(ref protocols) if protocols.len() == 1 => {
|
||||
protocols.iter().any(|protocol| {
|
||||
@@ -69,8 +69,15 @@ fn auth_is_valid(codes: &Path, protocols: ws::Result<Vec<&str>>) -> bool {
|
||||
|
||||
if let (Some(auth), Some(time)) = (auth, time) {
|
||||
// Check if the code is valid
|
||||
AuthCodes::from_file(codes)
|
||||
.map(|codes| codes.is_valid(&auth, time))
|
||||
AuthCodes::from_file(codes_path)
|
||||
.map(|mut codes| {
|
||||
let res = codes.is_valid(&auth, time);
|
||||
// make sure to save back authcodes - it might have been modified
|
||||
if let Err(_) = codes.to_file(codes_path) {
|
||||
warn!(target: "signer", "Couldn't save authorization codes to file.");
|
||||
}
|
||||
res
|
||||
})
|
||||
.unwrap_or(false)
|
||||
} else {
|
||||
false
|
||||
|
||||
Reference in New Issue
Block a user