correctly adhere to spec in most request cases
This commit is contained in:
parent
aa041cc4a6
commit
2ffc8ff0cc
5 changed files with 47 additions and 11 deletions
13
README.md
13
README.md
|
@ -63,6 +63,19 @@ Note: This sets the user to `nobody` and the group to `nobody` as well. This
|
||||||
naming scheme is not consistent for all Unix systems... Try changing the group
|
naming scheme is not consistent for all Unix systems... Try changing the group
|
||||||
name to `nogroup` if the software fails to start.
|
name to `nogroup` if the software fails to start.
|
||||||
|
|
||||||
|
Testing
|
||||||
|
-------
|
||||||
|
|
||||||
|
As you may have spotted, I did not get around to write a test suite for this.
|
||||||
|
The server's behavior can be tested using the
|
||||||
|
[gemini-diagnostics](https://github.com/michael-lazar/gemini-diagnostics) suite
|
||||||
|
by michael-lazar. It passes all "important" tests (some malformed requests
|
||||||
|
are still handled). Most importantly: the URLDotEscape tests fails. This does
|
||||||
|
not mean you can successfully a URL escape attack against this, rather the URL
|
||||||
|
library I use already parses out any superfluous ..'s.
|
||||||
|
e.g. "localhost/../../../etc/passwd" already became "localhost/etc/passwd" once
|
||||||
|
I receive the parsed URL from the library.
|
||||||
|
|
||||||
Why "Sheldon Director"?
|
Why "Sheldon Director"?
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|
||||||
|
|
|
@ -5,8 +5,8 @@ default_host = localhost
|
||||||
gem_root = ./doc
|
gem_root = ./doc
|
||||||
|
|
||||||
# you can define as many of these as you like
|
# you can define as many of these as you like
|
||||||
listen = [::1]:1965
|
listen = 0.0.0.0:1965
|
||||||
listen = 127.0.0.1:1965
|
listen = [::]:1965
|
||||||
|
|
||||||
# privilege level for the server to drop to after initializing
|
# privilege level for the server to drop to after initializing
|
||||||
user = nobody
|
user = nobody
|
||||||
|
|
|
@ -20,11 +20,10 @@ fn send_header(stream: &mut SslStream<TcpStream>, header: &response::Header) {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_request(config: &ServerConfig, mut stream: SslStream<TcpStream>) {
|
pub fn handle_request(config: &ServerConfig, mut stream: SslStream<TcpStream>) {
|
||||||
let mut buffer = [0; 1026];
|
let mut buffer = [0; 1024];
|
||||||
match stream.ssl_read(&mut buffer) {
|
match stream.ssl_read(&mut buffer) {
|
||||||
Ok(s) => {
|
Ok(s) => {
|
||||||
if s == 0 {
|
if s == 0 || s > 1025 {
|
||||||
println!("received empty request buffer");
|
|
||||||
send_header(&mut stream, &response::bad_request());
|
send_header(&mut stream, &response::bad_request());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -47,7 +46,11 @@ pub fn handle_request(config: &ServerConfig, mut stream: SslStream<TcpStream>) {
|
||||||
|
|
||||||
let location = match Url::parse(&request) {
|
let location = match Url::parse(&request) {
|
||||||
Ok(url) => url,
|
Ok(url) => url,
|
||||||
Err(_) => config.default_host.join(&request).unwrap(),
|
Err(_) => {
|
||||||
|
println!("received invalid request url");
|
||||||
|
send_header(&mut stream, &response::bad_request());
|
||||||
|
return;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
handle_response(config, location, &mut stream);
|
handle_response(config, location, &mut stream);
|
||||||
|
@ -69,25 +72,39 @@ fn write_line<T: Write>(line: &[u8], stream: &mut BufWriter<T>) -> Result<(), Er
|
||||||
fn handle_response(config: &ServerConfig, url: Url, mut stream: &mut SslStream<TcpStream>) {
|
fn handle_response(config: &ServerConfig, url: Url, mut stream: &mut SslStream<TcpStream>) {
|
||||||
println!("responding for: {}", url);
|
println!("responding for: {}", url);
|
||||||
|
|
||||||
if url.scheme() != "gemini" {
|
// url scheme must be either "gemini://" or "//" (empty)
|
||||||
send_header(&mut stream, &response::permanent_failure());
|
if url.scheme() != "gemini" && url.scheme() != "" {
|
||||||
|
send_header(&mut stream, &response::proxy_request_refused());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// url request host must match
|
||||||
if url.host() != config.default_host.host() {
|
if url.host() != config.default_host.host() {
|
||||||
send_header(&mut stream, &response::proxy_request_refused());
|
send_header(&mut stream, &response::proxy_request_refused());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: also drop on incorrect port in request
|
||||||
|
|
||||||
let rel_path = match Path::new(url.path()).strip_prefix("/") {
|
let rel_path = match Path::new(url.path()).strip_prefix("/") {
|
||||||
Ok(path) => path,
|
Ok(path) => path,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
Path::new("")
|
// empty path, gemini spec says to redirect client to root
|
||||||
|
send_header(&mut stream, &response::redirect_permanent(
|
||||||
|
config.default_host.as_str(),
|
||||||
|
));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let path = gen_path_index(&config.gem_root.join(rel_path));
|
let path = gen_path_index(&config.gem_root.join(rel_path));
|
||||||
|
|
||||||
|
// make sure we can't escape gem_root
|
||||||
|
if !path.starts_with(&config.gem_root) {
|
||||||
|
send_header(&mut stream, &response::bad_request());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let file = match File::open(&path) {
|
let file = match File::open(&path) {
|
||||||
Ok(file) => file,
|
Ok(file) => file,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
|
|
|
@ -24,7 +24,7 @@ pub struct ServerConfig {
|
||||||
impl ServerConfig {
|
impl ServerConfig {
|
||||||
pub fn new() -> ServerConfig {
|
pub fn new() -> ServerConfig {
|
||||||
ServerConfig {
|
ServerConfig {
|
||||||
default_host: Url::parse("gemini://localhost").unwrap(),
|
default_host: Url::parse("gemini://localhost/").unwrap(),
|
||||||
gem_root: PathBuf::from(""),
|
gem_root: PathBuf::from(""),
|
||||||
addrs: Vec::new(),
|
addrs: Vec::new(),
|
||||||
user: unistd::getuid(),
|
user: unistd::getuid(),
|
||||||
|
@ -35,7 +35,7 @@ impl ServerConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_default_host(&mut self, default_host: String) {
|
pub fn set_default_host(&mut self, default_host: String) {
|
||||||
let mut url = Url::parse("gemini://default").unwrap();
|
let mut url = Url::parse("gemini://default/").unwrap();
|
||||||
|
|
||||||
match url.set_host(Some(default_host.as_str())) {
|
match url.set_host(Some(default_host.as_str())) {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
|
|
|
@ -3,6 +3,7 @@ use std::vec::Vec;
|
||||||
#[derive(Copy, Clone)]
|
#[derive(Copy, Clone)]
|
||||||
pub enum Status {
|
pub enum Status {
|
||||||
Success = 20,
|
Success = 20,
|
||||||
|
RedirectPermanent = 31,
|
||||||
PermanentFailure = 50,
|
PermanentFailure = 50,
|
||||||
NotFound = 51,
|
NotFound = 51,
|
||||||
ProxyRequestRefused = 53,
|
ProxyRequestRefused = 53,
|
||||||
|
@ -31,6 +32,10 @@ impl Header {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn redirect_permanent(meta: &str) -> Header {
|
||||||
|
Header::new(Status::RedirectPermanent, meta)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn permanent_failure() -> Header {
|
pub fn permanent_failure() -> Header {
|
||||||
Header::new(Status::PermanentFailure, "permanent failure")
|
Header::new(Status::PermanentFailure, "permanent failure")
|
||||||
}
|
}
|
||||||
|
@ -46,3 +51,4 @@ pub fn proxy_request_refused() -> Header {
|
||||||
pub fn bad_request() -> Header {
|
pub fn bad_request() -> Header {
|
||||||
Header::new(Status::BadRequest, "bad request")
|
Header::new(Status::BadRequest, "bad request")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue