parent
d121aed47b
commit
23cc09f481
@ -0,0 +1,19 @@ |
||||
//! Simple HTTP client
|
||||
|
||||
use crate::error::invalid_input_error; |
||||
use http::{Request, Response}; |
||||
use std::io; |
||||
|
||||
pub struct Client {} |
||||
|
||||
impl Client { |
||||
pub fn new() -> Self { |
||||
Self {} |
||||
} |
||||
|
||||
pub fn request(&self, _request: &Request<Option<Vec<u8>>>) -> io::Result<Response<Vec<u8>>> { |
||||
Err(invalid_input_error( |
||||
"HTTP client is not available. Enable the feature 'simple_http'", |
||||
)) |
||||
} |
||||
} |
@ -0,0 +1,9 @@ |
||||
#[cfg(not(feature = "http_client"))] |
||||
mod dummy; |
||||
#[cfg(feature = "http_client")] |
||||
mod simple; |
||||
|
||||
#[cfg(not(feature = "http_client"))] |
||||
pub use dummy::Client; |
||||
#[cfg(feature = "http_client")] |
||||
pub use simple::Client; |
@ -0,0 +1,425 @@ |
||||
//! Simple HTTP client
|
||||
|
||||
use crate::error::{invalid_data_error, invalid_input_error}; |
||||
use http::header::{CONNECTION, CONTENT_LENGTH, HOST, TRANSFER_ENCODING}; |
||||
use http::{Request, Response, Version}; |
||||
use httparse::Status; |
||||
use native_tls::TlsConnector; |
||||
use std::cmp::min; |
||||
use std::convert::TryInto; |
||||
use std::io; |
||||
use std::io::{BufRead, BufReader, Read, Write}; |
||||
use std::net::TcpStream; |
||||
|
||||
pub struct Client {} |
||||
|
||||
impl Client { |
||||
pub fn new() -> Self { |
||||
Self {} |
||||
} |
||||
|
||||
pub fn request( |
||||
&self, |
||||
request: &Request<Option<Vec<u8>>>, |
||||
) -> io::Result<Response<Box<dyn BufRead>>> { |
||||
let scheme = request |
||||
.uri() |
||||
.scheme_str() |
||||
.ok_or_else(|| invalid_input_error("No host provided"))?; |
||||
let port = if let Some(port) = request.uri().port_u16() { |
||||
port |
||||
} else { |
||||
match scheme { |
||||
"http" => 80, |
||||
"https" => 443, |
||||
_ => { |
||||
return Err(invalid_input_error(format!( |
||||
"No port provided for scheme '{}'", |
||||
scheme |
||||
))) |
||||
} |
||||
} |
||||
}; |
||||
let host = request |
||||
.uri() |
||||
.host() |
||||
.ok_or_else(|| invalid_input_error("No host provided"))?; |
||||
|
||||
match scheme { |
||||
"http" => { |
||||
let mut stream = TcpStream::connect((host, port))?; |
||||
self.encode(request, &mut stream)?; |
||||
self.decode(stream) |
||||
} |
||||
"https" => { |
||||
let connector = |
||||
TlsConnector::new().map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; |
||||
let stream = TcpStream::connect((host, port))?; |
||||
let mut stream = connector |
||||
.connect(host, stream) |
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; |
||||
self.encode(request, &mut stream)?; |
||||
self.decode(stream) |
||||
} |
||||
_ => Err(invalid_input_error(format!( |
||||
"Not supported URL scheme: {}", |
||||
scheme |
||||
))), |
||||
} |
||||
} |
||||
|
||||
fn encode( |
||||
&self, |
||||
request: &Request<Option<Vec<u8>>>, |
||||
mut writer: &mut impl Write, |
||||
) -> io::Result<()> { |
||||
if request.headers().contains_key(CONTENT_LENGTH) { |
||||
return Err(invalid_input_error( |
||||
"content-length header is set by the client library", |
||||
)); |
||||
} |
||||
if request.headers().contains_key(HOST) { |
||||
return Err(invalid_input_error( |
||||
"host header is set by the client library", |
||||
)); |
||||
} |
||||
if request.headers().contains_key(CONNECTION) { |
||||
return Err(invalid_input_error( |
||||
"connection header is set by the client library", |
||||
)); |
||||
} |
||||
if let Some(query) = request.uri().query() { |
||||
write!( |
||||
&mut writer, |
||||
"{} {}?{} HTTP/1.1\r\n", |
||||
request.method(), |
||||
request.uri().path(), |
||||
query |
||||
)?; |
||||
} else { |
||||
write!( |
||||
&mut writer, |
||||
"{} {} HTTP/1.1\r\n", |
||||
request.method(), |
||||
request.uri().path() |
||||
)?; |
||||
} |
||||
|
||||
// host
|
||||
let host = request |
||||
.uri() |
||||
.host() |
||||
.ok_or_else(|| invalid_input_error("No host provided"))?; |
||||
if let Some(port) = request.uri().port() { |
||||
write!(writer, "host: {}:{}\r\n", request.uri(), port) |
||||
} else { |
||||
write!(writer, "host: {}\r\n", host) |
||||
}?; |
||||
|
||||
// connection
|
||||
write!(writer, "connection: close\r\n")?; |
||||
|
||||
// headers
|
||||
for (name, value) in request.headers() { |
||||
write!(writer, "{}: ", name.as_str())?; |
||||
writer.write_all(value.as_bytes())?; |
||||
write!(writer, "\r\n")?; |
||||
} |
||||
|
||||
// body with content-length
|
||||
if let Some(payload) = request.body() { |
||||
write!(writer, "content-length: {}\r\n\r\n", payload.len())?; |
||||
writer.write_all(payload)?; |
||||
} else { |
||||
write!(writer, "\r\n")?; |
||||
} |
||||
Ok(()) |
||||
} |
||||
|
||||
fn decode<'a>(&self, reader: impl Read + 'a) -> io::Result<Response<Box<dyn BufRead + 'a>>> { |
||||
let mut reader = BufReader::new(reader); |
||||
|
||||
// Let's read the headers
|
||||
let mut buffer = Vec::new(); |
||||
let mut headers = [httparse::EMPTY_HEADER; 1024]; |
||||
let mut parsed_response = httparse::Response::new(&mut headers); |
||||
loop { |
||||
if reader.read_until(b'\n', &mut buffer)? == 0 { |
||||
return Err(invalid_data_error("Empty HTTP response")); |
||||
} |
||||
if buffer.len() > 8 * 1024 { |
||||
return Err(invalid_data_error("The headers size should fit in 8kb")); |
||||
} |
||||
|
||||
if buffer.ends_with(b"\r\n\r\n") || buffer.ends_with(b"\n\n") { |
||||
break; //end of buffer
|
||||
} |
||||
} |
||||
if parsed_response |
||||
.parse(&buffer) |
||||
.map_err(invalid_data_error)? |
||||
.is_partial() |
||||
{ |
||||
return Err(invalid_input_error( |
||||
"Partial HTTP headers containing two line jumps", |
||||
)); |
||||
} |
||||
|
||||
// Let's build the response
|
||||
let mut response = Response::builder() |
||||
.status( |
||||
parsed_response |
||||
.code |
||||
.ok_or_else(|| invalid_data_error("No status code in the HTTP response"))?, |
||||
) |
||||
.version(match parsed_response.version { |
||||
Some(0) => Version::HTTP_10, |
||||
Some(1) => Version::HTTP_11, |
||||
Some(id) => { |
||||
return Err(invalid_data_error(format!( |
||||
"Unsupported HTTP response version: 1.{}", |
||||
id |
||||
))) |
||||
} |
||||
None => return Err(invalid_data_error("No HTTP version in the HTTP response")), |
||||
}); |
||||
for header in parsed_response.headers { |
||||
response = response.header(header.name, header.value); |
||||
} |
||||
|
||||
let content_length = response.headers_ref().and_then(|h| h.get(CONTENT_LENGTH)); |
||||
let transfer_encoding = response |
||||
.headers_ref() |
||||
.and_then(|h| h.get(TRANSFER_ENCODING)); |
||||
if transfer_encoding.is_some() && content_length.is_some() { |
||||
return Err(invalid_data_error( |
||||
"Transfer-Encoding and Content-Length should not be set at the same time", |
||||
)); |
||||
} |
||||
|
||||
let body: Box<dyn BufRead> = if let Some(content_length) = content_length { |
||||
let len = content_length |
||||
.to_str() |
||||
.map_err(invalid_data_error)? |
||||
.parse::<u64>() |
||||
.map_err(invalid_data_error)?; |
||||
Box::new(reader.take(len)) |
||||
} else if let Some(transfer_encoding) = transfer_encoding { |
||||
let transfer_encoding = transfer_encoding.to_str().map_err(invalid_data_error)?; |
||||
if transfer_encoding.eq_ignore_ascii_case("chunked") { |
||||
buffer.clear(); |
||||
Box::new(BufReader::new(ChunkedResponse { |
||||
reader, |
||||
buffer, |
||||
is_start: true, |
||||
chunk_position: 1, |
||||
chunk_size: 1, |
||||
})) |
||||
} else { |
||||
return Err(invalid_data_error(format!( |
||||
"Transfer-Encoding: {} is not supported", |
||||
transfer_encoding |
||||
))); |
||||
} |
||||
} else { |
||||
Box::new(io::empty()) |
||||
}; |
||||
|
||||
response.body(body).map_err(invalid_data_error) |
||||
} |
||||
} |
||||
|
||||
struct ChunkedResponse<R: BufRead> { |
||||
reader: R, |
||||
buffer: Vec<u8>, |
||||
is_start: bool, |
||||
chunk_position: usize, |
||||
chunk_size: usize, |
||||
} |
||||
|
||||
impl<R: BufRead> Read for ChunkedResponse<R> { |
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { |
||||
loop { |
||||
// In case we still have data
|
||||
if self.chunk_position < self.chunk_size { |
||||
let inner_buf = self.reader.fill_buf()?; |
||||
let size = min( |
||||
min(buf.len(), inner_buf.len()), |
||||
self.chunk_size - self.chunk_position, |
||||
); |
||||
buf[..size].copy_from_slice(&inner_buf[..size]); |
||||
self.reader.consume(size); |
||||
self.chunk_position += size; |
||||
return Ok(size); // Won't be 0 if there is still some inner buffer
|
||||
} |
||||
|
||||
if self.chunk_size == 0 { |
||||
return Ok(0); // We know it's the end
|
||||
} |
||||
|
||||
if self.is_start { |
||||
self.is_start = false; |
||||
} else { |
||||
// chunk end
|
||||
self.buffer.clear(); |
||||
self.reader.read_until(b'\n', &mut self.buffer)?; |
||||
if self.buffer != b"\r\n" && self.buffer != b"\n" { |
||||
return Err(invalid_data_error("Invalid chunked element end")); |
||||
} |
||||
} |
||||
|
||||
// We load a new chunk
|
||||
self.buffer.clear(); |
||||
self.reader.read_until(b'\n', &mut self.buffer)?; |
||||
self.chunk_position = 0; |
||||
self.chunk_size = if let Ok(Status::Complete((read, chunk_size))) = |
||||
httparse::parse_chunk_size(&self.buffer) |
||||
{ |
||||
if read != self.buffer.len() { |
||||
return Err(invalid_data_error("Chuncked header containing a line jump")); |
||||
} |
||||
chunk_size.try_into().map_err(invalid_data_error)? |
||||
} else { |
||||
return Err(invalid_data_error("Invalid chuncked header")); |
||||
}; |
||||
|
||||
if self.chunk_size == 0 { |
||||
// we read the trailers
|
||||
loop { |
||||
if self.reader.read_until(b'\n', &mut self.buffer)? == 0 { |
||||
return Err(invalid_data_error("Missing chunked encoding end")); |
||||
} |
||||
if self.buffer.len() > 8 * 1024 { |
||||
return Err(invalid_data_error("The trailers size should fit in 8kb")); |
||||
} |
||||
if self.buffer.ends_with(b"\r\n\r\n") || self.buffer.ends_with(b"\n\n") { |
||||
break; //end of buffer
|
||||
} |
||||
} |
||||
return Ok(0); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
#[cfg(test)] |
||||
mod tests { |
||||
use super::*; |
||||
use http::header::{ACCEPT, CONTENT_TYPE}; |
||||
use http::{Method, StatusCode}; |
||||
use std::io::Cursor; |
||||
use std::str; |
||||
|
||||
#[test] |
||||
fn encode_get_request() -> io::Result<()> { |
||||
let mut buffer = Vec::new(); |
||||
Client::new().encode( |
||||
&Request::builder() |
||||
.method(Method::GET) |
||||
.uri("http://example.com/foo/bar?query#fragment") |
||||
.header(ACCEPT, "application/json") |
||||
.body(None) |
||||
.unwrap(), |
||||
&mut buffer, |
||||
)?; |
||||
assert_eq!( |
||||
str::from_utf8(&buffer).unwrap(), |
||||
"GET /foo/bar?query HTTP/1.1\r\nhost: example.com\r\nconnection: close\r\naccept: application/json\r\n\r\n" |
||||
); |
||||
Ok(()) |
||||
} |
||||
|
||||
#[test] |
||||
fn encode_post_request() -> io::Result<()> { |
||||
let mut buffer = Vec::new(); |
||||
Client::new().encode( |
||||
&Request::builder() |
||||
.method(Method::POST) |
||||
.uri("http://example.com/foo/bar?query#fragment") |
||||
.header(ACCEPT, "application/json") |
||||
.body(Some(b"testbody".to_vec())) |
||||
.unwrap(), |
||||
&mut buffer, |
||||
)?; |
||||
assert_eq!( |
||||
str::from_utf8(&buffer).unwrap(), |
||||
"POST /foo/bar?query HTTP/1.1\r\nhost: example.com\r\nconnection: close\r\naccept: application/json\r\ncontent-length: 8\r\n\r\ntestbody" |
||||
); |
||||
Ok(()) |
||||
} |
||||
|
||||
#[test] |
||||
fn decode_response_without_payload() -> io::Result<()> { |
||||
let response = Client::new() |
||||
.decode(Cursor::new("HTTP/1.1 404 Not Found\r\n\r\n")) |
||||
.unwrap(); |
||||
assert_eq!(response.status(), StatusCode::NOT_FOUND); |
||||
let mut buf = String::new(); |
||||
response.into_body().read_to_string(&mut buf)?; |
||||
assert!(buf.is_empty()); |
||||
Ok(()) |
||||
} |
||||
|
||||
#[test] |
||||
fn decode_response_with_fixed_payload() -> io::Result<()> { |
||||
let response = Client::new().decode(Cursor::new( |
||||
"HTTP/1.1 200 OK\r\ncontent-type: text/plain\r\ncontent-length:8\r\n\r\ntestbody", |
||||
))?; |
||||
assert_eq!(response.status(), StatusCode::OK); |
||||
assert_eq!( |
||||
response |
||||
.headers() |
||||
.get(CONTENT_TYPE) |
||||
.unwrap() |
||||
.to_str() |
||||
.unwrap(), |
||||
"text/plain" |
||||
); |
||||
let mut buf = String::new(); |
||||
response.into_body().read_to_string(&mut buf)?; |
||||
assert_eq!(buf, "testbody"); |
||||
Ok(()) |
||||
} |
||||
|
||||
#[test] |
||||
fn decode_response_with_chunked_payload() -> io::Result<()> { |
||||
let response = Client::new().decode(Cursor::new( |
||||
"HTTP/1.1 200 OK\r\ncontent-type: text/plain\r\ntransfer-encoding:chunked\r\n\r\n4\r\nWiki\r\n5\r\npedia\r\nE\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n", |
||||
))?; |
||||
assert_eq!(response.status(), StatusCode::OK); |
||||
assert_eq!( |
||||
response |
||||
.headers() |
||||
.get(CONTENT_TYPE) |
||||
.unwrap() |
||||
.to_str() |
||||
.unwrap(), |
||||
"text/plain" |
||||
); |
||||
let mut buf = String::new(); |
||||
response.into_body().read_to_string(&mut buf)?; |
||||
assert_eq!(buf, "Wikipedia in\r\n\r\nchunks."); |
||||
Ok(()) |
||||
} |
||||
|
||||
#[test] |
||||
fn decode_response_with_trailer() -> io::Result<()> { |
||||
let response = Client::new().decode(Cursor::new( |
||||
"HTTP/1.1 200 OK\r\ncontent-type: text/plain\r\ntransfer-encoding:chunked\r\n\r\n4\r\nWiki\r\n5\r\npedia\r\nE\r\n in\r\n\r\nchunks.\r\n0\r\ntest: foo\r\n\r\n", |
||||
))?; |
||||
assert_eq!(response.status(), StatusCode::OK); |
||||
assert_eq!( |
||||
response |
||||
.headers() |
||||
.get(CONTENT_TYPE) |
||||
.unwrap() |
||||
.to_str() |
||||
.unwrap(), |
||||
"text/plain" |
||||
); |
||||
let mut buf = String::new(); |
||||
response.into_body().read_to_string(&mut buf)?; |
||||
assert_eq!(buf, "Wikipedia in\r\n\r\nchunks."); |
||||
Ok(()) |
||||
} |
||||
} |
@ -0,0 +1,150 @@ |
||||
use crate::error::{invalid_data_error, invalid_input_error}; |
||||
use crate::model::NamedNode; |
||||
use crate::sparql::error::EvaluationError; |
||||
use crate::sparql::http::Client; |
||||
use crate::sparql::model::QueryResults; |
||||
use crate::sparql::parser::Query; |
||||
use crate::sparql::QueryResultsFormat; |
||||
use http::header::{ACCEPT, CONTENT_TYPE, USER_AGENT}; |
||||
use http::{Method, Request, StatusCode}; |
||||
use std::error::Error; |
||||
|
||||
/// Handler for [SPARQL 1.1 Federated Query](https://www.w3.org/TR/sparql11-federated-query/) SERVICE.
|
||||
///
|
||||
/// Should be given to [`QueryOptions`](struct.QueryOptions.html#method.with_service_handler)
|
||||
/// before evaluating a SPARQL query that uses SERVICE calls.
|
||||
///
|
||||
/// ```
|
||||
/// use oxigraph::MemoryStore;
|
||||
/// use oxigraph::model::*;
|
||||
/// use oxigraph::sparql::{QueryOptions, QueryResults, ServiceHandler, Query, EvaluationError};
|
||||
///
|
||||
/// #[derive(Default)]
|
||||
/// struct TestServiceHandler {
|
||||
/// store: MemoryStore
|
||||
/// }
|
||||
///
|
||||
/// impl ServiceHandler for TestServiceHandler {
|
||||
/// type Error = EvaluationError;
|
||||
///
|
||||
/// fn handle(&self,service_name: NamedNode, query: Query) -> Result<QueryResults,EvaluationError> {
|
||||
/// if service_name == "http://example.com/service" {
|
||||
/// self.store.query(query, QueryOptions::default())
|
||||
/// } else {
|
||||
/// panic!()
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// let store = MemoryStore::new();
|
||||
/// let service = TestServiceHandler::default();
|
||||
/// let ex = NamedNode::new("http://example.com")?;
|
||||
/// service.store.insert(Quad::new(ex.clone(), ex.clone(), ex.clone(), None));
|
||||
///
|
||||
/// if let QueryResults::Solutions(mut solutions) = store.query(
|
||||
/// "SELECT ?s WHERE { SERVICE <http://example.com/service> { ?s ?p ?o } }",
|
||||
/// QueryOptions::default().with_service_handler(service)
|
||||
/// )? {
|
||||
/// assert_eq!(solutions.next().unwrap()?.get("s"), Some(&ex.into()));
|
||||
/// }
|
||||
/// # Result::<_,Box<dyn std::error::Error>>::Ok(())
|
||||
/// ```
|
||||
pub trait ServiceHandler { |
||||
type Error: Error + Send + Sync + 'static; |
||||
|
||||
/// Evaluates a [`Query`](struct.Query.html) against a given service identified by a [`NamedNode`](../model/struct.NamedNode.html).
|
||||
fn handle(&self, service_name: NamedNode, query: Query) -> Result<QueryResults, Self::Error>; |
||||
} |
||||
|
||||
pub struct EmptyServiceHandler; |
||||
|
||||
impl ServiceHandler for EmptyServiceHandler { |
||||
type Error = EvaluationError; |
||||
|
||||
fn handle(&self, _: NamedNode, _: Query) -> Result<QueryResults, EvaluationError> { |
||||
Err(EvaluationError::msg( |
||||
"The SERVICE feature is not implemented", |
||||
)) |
||||
} |
||||
} |
||||
|
||||
pub struct ErrorConversionServiceHandler<S: ServiceHandler> { |
||||
handler: S, |
||||
} |
||||
|
||||
impl<S: ServiceHandler> ErrorConversionServiceHandler<S> { |
||||
pub fn wrap(handler: S) -> Self { |
||||
Self { handler } |
||||
} |
||||
} |
||||
|
||||
impl<S: ServiceHandler> ServiceHandler for ErrorConversionServiceHandler<S> { |
||||
type Error = EvaluationError; |
||||
|
||||
fn handle( |
||||
&self, |
||||
service_name: NamedNode, |
||||
query: Query, |
||||
) -> Result<QueryResults, EvaluationError> { |
||||
self.handler |
||||
.handle(service_name, query) |
||||
.map_err(EvaluationError::wrap) |
||||
} |
||||
} |
||||
|
||||
pub struct SimpleServiceHandler { |
||||
client: Client, |
||||
} |
||||
|
||||
impl SimpleServiceHandler { |
||||
pub fn new() -> Self { |
||||
Self { |
||||
client: Client::new(), |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl ServiceHandler for SimpleServiceHandler { |
||||
type Error = EvaluationError; |
||||
|
||||
fn handle( |
||||
&self, |
||||
service_name: NamedNode, |
||||
query: Query, |
||||
) -> Result<QueryResults, EvaluationError> { |
||||
let request = Request::builder() |
||||
.method(Method::POST) |
||||
.uri(service_name.as_str()) |
||||
.header(CONTENT_TYPE, "application/sparql-query") |
||||
.header(ACCEPT, QueryResultsFormat::Xml.media_type()) |
||||
.header(USER_AGENT, concat!("Oxigraph/", env!("CARGO_PKG_VERSION"))) |
||||
.body(Some(query.to_string().into_bytes())) |
||||
.map_err(invalid_input_error)?; |
||||
let response = self.client.request(&request)?; |
||||
if response.status() != StatusCode::OK { |
||||
return Err(EvaluationError::msg(format!( |
||||
"HTTP error code {} returned when querying service {}", |
||||
response.status(), |
||||
service_name |
||||
))); |
||||
} |
||||
let content_type = response |
||||
.headers() |
||||
.get(CONTENT_TYPE) |
||||
.ok_or_else(|| { |
||||
EvaluationError::msg(format!( |
||||
"No Content-Type header returned by {}", |
||||
service_name |
||||
)) |
||||
})? |
||||
.to_str() |
||||
.map_err(invalid_data_error)?; |
||||
let format = QueryResultsFormat::from_media_type(content_type).ok_or_else(|| { |
||||
EvaluationError::msg(format!( |
||||
"Unsupported Content-Type returned by {}: {}", |
||||
service_name, content_type |
||||
)) |
||||
})?; |
||||
Ok(QueryResults::read(response.into_body(), format)?) |
||||
} |
||||
} |
Loading…
Reference in new issue