extern crate jsonrpc_core;
use std::str::Lines;
use std::net::TcpStream;
use std::io::{Read, Write};
use self::jsonrpc_core::{IoHandler, Params, Value, Error};
use self::jsonrpc_core::futures::{self, Future};
use super::*;
fn serve_hosts(hosts: Vec<Host>) -> Server {
ServerBuilder::new(IoHandler::default())
.cors(DomainsValidation::AllowOnly(vec![AccessControlAllowOrigin::Value("parity.io".into())]))
.allowed_hosts(DomainsValidation::AllowOnly(hosts))
.start_http(&"127.0.0.1:0".parse().unwrap())
.unwrap()
}
fn serve() -> Server {
use std::thread;
let mut io = IoHandler::default();
io.add_method("hello", |_params: Params| Ok(Value::String("world".into())));
io.add_async_method("hello_async", |_params: Params| {
Box::new(futures::finished(Value::String("world".into())))
});
io.add_async_method("hello_async2", |_params: Params| {
let (c, p) = futures::oneshot();
thread::spawn(move || {
thread::sleep(::std::time::Duration::from_millis(10));
c.send(Value::String("world".into())).unwrap();
});
Box::new(p.map_err(|_| Error::invalid_request()))
});
ServerBuilder::new(io)
.cors(DomainsValidation::AllowOnly(vec![
AccessControlAllowOrigin::Value("parity.io".into()),
AccessControlAllowOrigin::Null,
]))
.start_http(&"127.0.0.1:0".parse().unwrap())
.unwrap()
}
struct Response {
status: String,
headers: String,
body: String,
}
fn read_block(lines: &mut Lines) -> String {
let mut block = String::new();
loop {
let line = lines.next();
match line {
Some("") | None => break,
Some(v) => {
block.push_str(v);
block.push_str("\n");
},
}
}
block
}
fn request(server: Server, request: &str) -> Response {
let mut req = TcpStream::connect(server.address()).unwrap();
req.write_all(request.as_bytes()).unwrap();
let mut response = String::new();
req.read_to_string(&mut response).unwrap();
let mut lines = response.lines();
let status = lines.next().unwrap().to_owned();
let headers = read_block(&mut lines);
let body = read_block(&mut lines);
Response {
status: status,
headers: headers,
body: body,
}
}
#[test]
fn should_return_method_not_allowed_for_get() {
let server = serve();
let response = request(server,
"\
GET / HTTP/1.1\r\n\
Host: 127.0.0.1:8080\r\n\
Connection: close\r\n\
\r\n\
I shouldn't be read.\r\n\
"
);
assert_eq!(response.status, "HTTP/1.1 405 Method Not Allowed".to_owned());
assert_eq!(response.body, "3D\nUsed HTTP Method is not allowed. POST or OPTIONS is required\n".to_owned());
}
#[test]
fn should_return_unsupported_media_type_if_not_json() {
let server = serve();
let response = request(server,
"\
POST / HTTP/1.1\r\n\
Host: 127.0.0.1:8080\r\n\
Connection: close\r\n\
\r\n\
{}\r\n\
"
);
assert_eq!(response.status, "HTTP/1.1 415 Unsupported Media Type".to_owned());
assert_eq!(response.body, "51\nSupplied content type is not allowed. Content-Type: application/json is required\n".to_owned());
}
#[test]
fn should_return_error_for_malformed_request() {
let server = serve();
let req = r#"{"jsonrpc":"3.0","method":"x"}"#;
let response = request(server,
&format!("\
POST / HTTP/1.1\r\n\
Host: 127.0.0.1:8080\r\n\
Connection: close\r\n\
Content-Type: application/json\r\n\
Content-Length: {}\r\n\
\r\n\
{}\r\n\
", req.as_bytes().len(), req)
);
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
assert_eq!(response.body, invalid_request());
}
#[test]
fn should_return_error_for_malformed_request2() {
let server = serve();
let req = r#"{"jsonrpc":"2.0","metho1d":""}"#;
let response = request(server,
&format!("\
POST / HTTP/1.1\r\n\
Host: 127.0.0.1:8080\r\n\
Connection: close\r\n\
Content-Type: application/json\r\n\
Content-Length: {}\r\n\
\r\n\
{}\r\n\
", req.as_bytes().len(), req)
);
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
assert_eq!(response.body, invalid_request());
}
#[test]
fn should_return_empty_response_for_notification() {
let server = serve();
let req = r#"{"jsonrpc":"2.0","method":"x"}"#;
let response = request(server,
&format!("\
POST / HTTP/1.1\r\n\
Host: 127.0.0.1:8080\r\n\
Connection: close\r\n\
Content-Type: application/json\r\n\
Content-Length: {}\r\n\
\r\n\
{}\r\n\
", req.as_bytes().len(), req)
);
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
assert_eq!(response.body, "0\n".to_owned());
}
#[test]
fn should_return_method_not_found() {
let server = serve();
let req = r#"{"jsonrpc":"2.0","id":"1","method":"x"}"#;
let response = request(server,
&format!("\
POST / HTTP/1.1\r\n\
Host: 127.0.0.1:8080\r\n\
Connection: close\r\n\
Content-Type: application/json\r\n\
Content-Length: {}\r\n\
\r\n\
{}\r\n\
", req.as_bytes().len(), req)
);
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
assert_eq!(response.body, method_not_found());
}
#[test]
fn should_add_cors_headers() {
let server = serve();
let req = r#"{"jsonrpc":"2.0","id":"1","method":"x"}"#;
let response = request(server,
&format!("\
POST / HTTP/1.1\r\n\
Host: 127.0.0.1:8080\r\n\
Origin: http://parity.io\r\n\
Connection: close\r\n\
Content-Type: application/json\r\n\
Content-Length: {}\r\n\
\r\n\
{}\r\n\
", req.as_bytes().len(), req)
);
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
assert_eq!(response.body, method_not_found());
assert!(response.headers.contains("Access-Control-Allow-Origin: http://parity.io"), "Headers missing in {}", response.headers);
}
#[test]
fn should_not_add_cors_headers() {
let server = serve();
let req = r#"{"jsonrpc":"2.0","id":"1","method":"x"}"#;
let response = request(server,
&format!("\
POST / HTTP/1.1\r\n\
Host: 127.0.0.1:8080\r\n\
Origin: fake.io\r\n\
Connection: close\r\n\
Content-Type: application/json\r\n\
Content-Length: {}\r\n\
\r\n\
{}\r\n\
", req.as_bytes().len(), req)
);
assert_eq!(response.status, "HTTP/1.1 403 Forbidden".to_owned());
assert_eq!(response.body, cors_invalid());
}
#[test]
fn should_not_process_the_request_in_case_of_invalid_cors() {
let server = serve();
let req = r#"{"jsonrpc":"2.0","id":"1","method":"hello"}"#;
let response = request(server,
&format!("\
OPTIONS / HTTP/1.1\r\n\
Host: 127.0.0.1:8080\r\n\
Origin: fake.io\r\n\
Connection: close\r\n\
Content-Type: application/json\r\n\
Content-Length: {}\r\n\
\r\n\
{}\r\n\
", req.as_bytes().len(), req)
);
assert_eq!(response.status, "HTTP/1.1 403 Forbidden".to_owned());
assert_eq!(response.body, cors_invalid());
}
#[test]
fn should_return_proper_headers_on_options() {
let server = serve();
let response = request(server,
"\
OPTIONS / HTTP/1.1\r\n\
Host: 127.0.0.1:8080\r\n\
Connection: close\r\n\
Content-Length: 0\r\n\
\r\n\
"
);
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
assert!(response.headers.contains("Allow: OPTIONS, POST"), "Headers missing in {}", response.headers);
assert!(response.headers.contains("Accept: application/json"), "Headers missing in {}", response.headers);
assert_eq!(response.body, "0\n");
}
#[test]
fn should_add_cors_header_for_null_origin() {
let server = serve();
let req = r#"{"jsonrpc":"2.0","id":"1","method":"x"}"#;
let response = request(server,
&format!("\
POST / HTTP/1.1\r\n\
Host: 127.0.0.1:8080\r\n\
Origin: null\r\n\
Connection: close\r\n\
Content-Type: application/json\r\n\
Content-Length: {}\r\n\
\r\n\
{}\r\n\
", req.as_bytes().len(), req)
);
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
assert_eq!(response.body, method_not_found());
}
#[test]
fn should_reject_invalid_hosts() {
let server = serve_hosts(vec!["parity.io".into()]);
let req = r#"{"jsonrpc":"2.0","id":"1","method":"x"}"#;
let response = request(server,
&format!("\
POST / HTTP/1.1\r\n\
Host: 127.0.0.1:8080\r\n\
Connection: close\r\n\
Content-Type: application/json\r\n\
Content-Length: {}\r\n\
\r\n\
{}\r\n\
", req.as_bytes().len(), req)
);
assert_eq!(response.status, "HTTP/1.1 403 Forbidden".to_owned());
assert_eq!(response.body, invalid_host());
}
#[test]
fn should_reject_missing_host() {
let server = serve_hosts(vec!["parity.io".into()]);
let req = r#"{"jsonrpc":"2.0","id":"1","method":"x"}"#;
let response = request(server,
&format!("\
POST / HTTP/1.1\r\n\
Connection: close\r\n\
Content-Type: application/json\r\n\
Content-Length: {}\r\n\
\r\n\
{}\r\n\
", req.as_bytes().len(), req)
);
assert_eq!(response.status, "HTTP/1.1 403 Forbidden".to_owned());
assert_eq!(response.body, invalid_host());
}
#[test]
fn should_allow_if_host_is_valid() {
let server = serve_hosts(vec!["parity.io".into()]);
let req = r#"{"jsonrpc":"2.0","id":"1","method":"x"}"#;
let response = request(server,
&format!("\
POST / HTTP/1.1\r\n\
Host: parity.io\r\n\
Connection: close\r\n\
Content-Type: application/json\r\n\
Content-Length: {}\r\n\
\r\n\
{}\r\n\
", req.as_bytes().len(), req)
);
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
assert_eq!(response.body, method_not_found());
}
#[test]
fn should_allow_application_json_utf8() {
let server = serve_hosts(vec!["parity.io".into()]);
let req = r#"{"jsonrpc":"2.0","id":"1","method":"x"}"#;
let response = request(server,
&format!("\
POST / HTTP/1.1\r\n\
Host: parity.io\r\n\
Connection: close\r\n\
Content-Type: application/json; charset=utf-8\r\n\
Content-Length: {}\r\n\
\r\n\
{}\r\n\
", req.as_bytes().len(), req)
);
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
assert_eq!(response.body, method_not_found());
}
#[test]
fn should_always_allow_the_bind_address() {
let server = serve_hosts(vec!["parity.io".into()]);
let addr = server.address().clone();
let req = r#"{"jsonrpc":"2.0","id":"1","method":"x"}"#;
let response = request(server,
&format!("\
POST / HTTP/1.1\r\n\
Host: {}\r\n\
Connection: close\r\n\
Content-Type: application/json\r\n\
Content-Length: {}\r\n\
\r\n\
{}\r\n\
", addr, req.as_bytes().len(), req)
);
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
assert_eq!(response.body, method_not_found());
}
#[test]
fn should_always_allow_the_bind_address_as_localhost() {
let server = serve_hosts(vec![]);
let addr = server.address().clone();
let req = r#"{"jsonrpc":"2.0","id":"1","method":"x"}"#;
let response = request(server,
&format!("\
POST / HTTP/1.1\r\n\
Host: localhost:{}\r\n\
Connection: close\r\n\
Content-Type: application/json\r\n\
Content-Length: {}\r\n\
\r\n\
{}\r\n\
", addr.port(), req.as_bytes().len(), req)
);
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
assert_eq!(response.body, method_not_found());
}
#[test]
fn should_handle_sync_requests_correctly() {
let server = serve();
let addr = server.address().clone();
let req = r#"{"jsonrpc":"2.0","id":"1","method":"hello"}"#;
let response = request(server,
&format!("\
POST / HTTP/1.1\r\n\
Host: localhost:{}\r\n\
Connection: close\r\n\
Content-Type: application/json\r\n\
Content-Length: {}\r\n\
\r\n\
{}\r\n\
", addr.port(), req.as_bytes().len(), req)
);
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
assert_eq!(response.body, world());
}
#[test]
fn should_handle_async_requests_with_immediate_response_correctly() {
let server = serve();
let addr = server.address().clone();
let req = r#"{"jsonrpc":"2.0","id":"1","method":"hello_async"}"#;
let response = request(server,
&format!("\
POST / HTTP/1.1\r\n\
Host: localhost:{}\r\n\
Connection: close\r\n\
Content-Type: application/json\r\n\
Content-Length: {}\r\n\
\r\n\
{}\r\n\
", addr.port(), req.as_bytes().len(), req)
);
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
assert_eq!(response.body, world());
}
#[test]
fn should_handle_async_requests_correctly() {
let server = serve();
let addr = server.address().clone();
let req = r#"{"jsonrpc":"2.0","id":"1","method":"hello_async2"}"#;
let response = request(server,
&format!("\
POST / HTTP/1.1\r\n\
Host: localhost:{}\r\n\
Connection: close\r\n\
Content-Type: application/json\r\n\
Content-Length: {}\r\n\
\r\n\
{}\r\n\
", addr.port(), req.as_bytes().len(), req)
);
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
assert_eq!(response.body, world());
}
#[test]
fn should_handle_sync_batch_requests_correctly() {
let server = serve();
let addr = server.address().clone();
let req = r#"[{"jsonrpc":"2.0","id":"1","method":"hello"}]"#;
let response = request(server,
&format!("\
POST / HTTP/1.1\r\n\
Host: localhost:{}\r\n\
Connection: close\r\n\
Content-Type: application/json\r\n\
Content-Length: {}\r\n\
\r\n\
{}\r\n\
", addr.port(), req.as_bytes().len(), req)
);
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
assert_eq!(response.body, world_batch());
}
fn invalid_host() -> String {
"29\nProvided Host header is not whitelisted.\n".into()
}
fn cors_invalid() -> String {
"76\nOrigin of the request is not whitelisted. CORS headers would not be sent and any side-effects were cancelled as well.\n".into()
}
fn method_not_found() -> String {
"4E\n{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32601,\"message\":\"Method not found\"},\"id\":1}\n".into()
}
fn invalid_request() -> String {
"50\n{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32600,\"message\":\"Invalid request\"},\"id\":null}\n".into()
}
fn world() -> String {
"2A\n{\"jsonrpc\":\"2.0\",\"result\":\"world\",\"id\":1}\n".into()
}
fn world_batch() -> String {
"2C\n[{\"jsonrpc\":\"2.0\",\"result\":\"world\",\"id\":1}]\n".into()
}