Add postgres-tokio
This commit is contained in:
parent
ba3b54afb0
commit
492d5d0c27
@ -1,2 +1,2 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = ["codegen", "postgres", "postgres-shared"]
|
members = ["codegen", "postgres", "postgres-shared", "postgres-tokio"]
|
||||||
|
12
postgres-tokio/Cargo.toml
Normal file
12
postgres-tokio/Cargo.toml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
[package]
|
||||||
|
name = "postgres-tokio"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Steven Fackler <sfackler@gmail.com>"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
futures = "0.1.7"
|
||||||
|
postgres-shared = { path = "../postgres-shared" }
|
||||||
|
postgres-protocol = "0.2"
|
||||||
|
tokio-core = "0.1"
|
||||||
|
tokio-dns-unofficial = "0.1"
|
||||||
|
tokio-uds = "0.1"
|
260
postgres-tokio/src/lib.rs
Normal file
260
postgres-tokio/src/lib.rs
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
extern crate postgres_shared;
|
||||||
|
extern crate postgres_protocol;
|
||||||
|
extern crate tokio_core;
|
||||||
|
extern crate tokio_dns;
|
||||||
|
extern crate tokio_uds;
|
||||||
|
|
||||||
|
#[macro_use]
|
||||||
|
extern crate futures;
|
||||||
|
|
||||||
|
use futures::{Future, IntoFuture, BoxFuture, Stream, Sink, Poll, StartSend};
|
||||||
|
use futures::future::Either;
|
||||||
|
use postgres_shared::params::{ConnectParams, IntoConnectParams};
|
||||||
|
use postgres_protocol::authentication;
|
||||||
|
use postgres_protocol::message::{backend, frontend};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::error::Error;
|
||||||
|
use std::io;
|
||||||
|
use tokio_core::reactor::Handle;
|
||||||
|
|
||||||
|
use stream::PostgresStream;
|
||||||
|
|
||||||
|
mod stream;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ConnectError {
|
||||||
|
Params(Box<Error + Sync + Send>),
|
||||||
|
Io(io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub struct CancelData {
|
||||||
|
pub process_id: i32,
|
||||||
|
pub secret_key: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<io::Error> for ConnectError {
|
||||||
|
fn from(e: io::Error) -> ConnectError {
|
||||||
|
ConnectError::Io(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct InnerConnectionState {
|
||||||
|
parameters: HashMap<String, String>,
|
||||||
|
cancel_data: CancelData,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct InnerConnection {
|
||||||
|
stream: PostgresStream,
|
||||||
|
state: InnerConnectionState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InnerConnection {
|
||||||
|
fn read(self) -> BoxFuture<(backend::Message<Vec<u8>>, InnerConnection), (io::Error, InnerConnection)> {
|
||||||
|
self.into_future()
|
||||||
|
.then(|r| {
|
||||||
|
let (m, mut s) = match r {
|
||||||
|
Ok((m, s)) => (m, s),
|
||||||
|
Err((e, s)) => return Either::A(Err((e, s)).into_future()),
|
||||||
|
};
|
||||||
|
|
||||||
|
match m {
|
||||||
|
Some(backend::Message::ParameterStatus(body)) => {
|
||||||
|
let name = match body.name() {
|
||||||
|
Ok(name) => name.to_owned(),
|
||||||
|
Err(e) => return Either::A(Err((e, s)).into_future()),
|
||||||
|
};
|
||||||
|
let value = match body.value() {
|
||||||
|
Ok(value) => value.to_owned(),
|
||||||
|
Err(e) => return Either::A(Err((e, s)).into_future()),
|
||||||
|
};
|
||||||
|
s.state.parameters.insert(name, value);
|
||||||
|
Either::B(s.read())
|
||||||
|
}
|
||||||
|
Some(backend::Message::NoticeResponse(_)) => {
|
||||||
|
// TODO forward the error
|
||||||
|
Either::B(s.read())
|
||||||
|
}
|
||||||
|
Some(m) => Either::A(Ok((m, s)).into_future()),
|
||||||
|
None => Either::A(Err((eof(), s)).into_future()),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Stream for InnerConnection {
|
||||||
|
type Item = backend::Message<Vec<u8>>;
|
||||||
|
type Error = io::Error;
|
||||||
|
|
||||||
|
fn poll(&mut self) -> Poll<Option<backend::Message<Vec<u8>>>, io::Error> {
|
||||||
|
self.stream.poll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sink for InnerConnection {
|
||||||
|
type SinkItem = Vec<u8>;
|
||||||
|
type SinkError = io::Error;
|
||||||
|
|
||||||
|
fn start_send(&mut self, item: Vec<u8>) -> StartSend<Vec<u8>, io::Error> {
|
||||||
|
self.stream.start_send(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_complete(&mut self) -> Poll<(), io::Error> {
|
||||||
|
self.stream.poll_complete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Connection(InnerConnection);
|
||||||
|
|
||||||
|
impl Connection {
|
||||||
|
pub fn connect<T>(params: T, handle: &Handle) -> BoxFuture<Connection, ConnectError>
|
||||||
|
where T: IntoConnectParams
|
||||||
|
{
|
||||||
|
let params = match params.into_connect_params() {
|
||||||
|
Ok(params) => params,
|
||||||
|
Err(e) => return futures::failed(ConnectError::Params(e)).boxed(),
|
||||||
|
};
|
||||||
|
|
||||||
|
stream::connect(params.host(), params.port(), handle)
|
||||||
|
.map_err(ConnectError::Io)
|
||||||
|
.map(|s| {
|
||||||
|
Connection(InnerConnection {
|
||||||
|
stream: s,
|
||||||
|
state: InnerConnectionState {
|
||||||
|
parameters: HashMap::new(),
|
||||||
|
cancel_data: CancelData {
|
||||||
|
process_id: 0,
|
||||||
|
secret_key: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.and_then(|s| s.startup(params))
|
||||||
|
.and_then(|(s, params)| s.handle_auth(params))
|
||||||
|
.and_then(|s| s.finish_startup())
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn startup(self, params: ConnectParams) -> BoxFuture<(Connection, ConnectParams), ConnectError> {
|
||||||
|
let mut buf = vec![];
|
||||||
|
let result = {
|
||||||
|
let options = [("client_encoding", "UTF8"), ("timezone", "GMT")];
|
||||||
|
let options = options.iter().cloned();
|
||||||
|
let options = options.chain(params.user().map(|u| ("user", u.name())));
|
||||||
|
let options = options.chain(params.database().map(|d| ("database", d)));
|
||||||
|
let options = options.chain(params.options().iter().map(|e| (&*e.0, &*e.1)));
|
||||||
|
|
||||||
|
frontend::startup_message(options, &mut buf)
|
||||||
|
};
|
||||||
|
|
||||||
|
result
|
||||||
|
.into_future()
|
||||||
|
.and_then(move |()| self.0.send(buf))
|
||||||
|
.and_then(|s| s.flush())
|
||||||
|
.map_err(ConnectError::Io)
|
||||||
|
.map(move |s| (Connection(s), params))
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_auth(self, params: ConnectParams) -> BoxFuture<Connection, ConnectError> {
|
||||||
|
self.0.read()
|
||||||
|
.map_err(|e| e.0.into())
|
||||||
|
.and_then(move |(m, s)| {
|
||||||
|
let response = match m {
|
||||||
|
backend::Message::AuthenticationOk => Ok(None),
|
||||||
|
backend::Message::AuthenticationCleartextPassword => {
|
||||||
|
match params.user().and_then(|u| u.password()) {
|
||||||
|
Some(pass) => {
|
||||||
|
let mut buf = vec![];
|
||||||
|
frontend::password_message(pass, &mut buf)
|
||||||
|
.map(|()| Some(buf))
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
Err(ConnectError::Params(
|
||||||
|
"password was required but not provided".into()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
backend::Message::AuthenticationMd5Password(body) => {
|
||||||
|
match params.user().and_then(|u| u.password().map(|p| (u.name(), p))) {
|
||||||
|
Some((user, pass)) => {
|
||||||
|
let pass = authentication::md5_hash(user.as_bytes(),
|
||||||
|
pass.as_bytes(),
|
||||||
|
body.salt());
|
||||||
|
let mut buf = vec![];
|
||||||
|
frontend::password_message(&pass, &mut buf)
|
||||||
|
.map(|()| Some(buf))
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
Err(ConnectError::Params(
|
||||||
|
"password was required but not provided".into()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Err(bad_message()),
|
||||||
|
};
|
||||||
|
|
||||||
|
response.map(|m| (m, Connection(s)))
|
||||||
|
})
|
||||||
|
.and_then(|(m, s)| {
|
||||||
|
match m {
|
||||||
|
Some(m) => Either::A(s.handle_auth_response(m)),
|
||||||
|
None => Either::B(Ok(s).into_future())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_auth_response(self, message: Vec<u8>) -> BoxFuture<Connection, ConnectError> {
|
||||||
|
self.0.send(message)
|
||||||
|
.and_then(|s| s.flush())
|
||||||
|
.and_then(|s| s.read().map_err(|e| e.0))
|
||||||
|
.map_err(ConnectError::Io)
|
||||||
|
.and_then(|(m, s)| {
|
||||||
|
match m {
|
||||||
|
backend::Message::AuthenticationOk => Ok(Connection(s)),
|
||||||
|
_ => Err(bad_message()),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish_startup(self) -> BoxFuture<Connection, ConnectError> {
|
||||||
|
self.0.read()
|
||||||
|
.map_err(|e| ConnectError::Io(e.0))
|
||||||
|
.and_then(|(m, mut s)| {
|
||||||
|
match m {
|
||||||
|
backend::Message::BackendKeyData(body) => {
|
||||||
|
s.state.cancel_data.process_id = body.process_id();
|
||||||
|
s.state.cancel_data.secret_key = body.secret_key();
|
||||||
|
Either::A(Connection(s).finish_startup())
|
||||||
|
}
|
||||||
|
backend::Message::ReadyForQuery(_) => Either::B(Ok(Connection(s)).into_future()),
|
||||||
|
_ => Either::B(Err(bad_message()).into_future()),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cancel_data(&self) -> CancelData {
|
||||||
|
self.0.state.cancel_data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bad_message<T>() -> T
|
||||||
|
where T: From<io::Error>
|
||||||
|
{
|
||||||
|
io::Error::new(io::ErrorKind::InvalidInput, "unexpected message").into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn eof<T>() -> T
|
||||||
|
where T: From<io::Error>
|
||||||
|
{
|
||||||
|
io::Error::new(io::ErrorKind::UnexpectedEof, "unexpected EOF").into()
|
||||||
|
}
|
100
postgres-tokio/src/stream.rs
Normal file
100
postgres-tokio/src/stream.rs
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
use futures::{BoxFuture, Future, IntoFuture, Async};
|
||||||
|
use postgres_shared::params::Host;
|
||||||
|
use postgres_protocol::message::backend::{self, ParseResult};
|
||||||
|
use std::io::{self, Read, Write};
|
||||||
|
use tokio_core::io::{Io, Codec, EasyBuf, Framed};
|
||||||
|
use tokio_core::net::TcpStream;
|
||||||
|
use tokio_core::reactor::Handle;
|
||||||
|
use tokio_dns;
|
||||||
|
use tokio_uds::UnixStream;
|
||||||
|
|
||||||
|
pub type PostgresStream = Framed<InnerStream, PostgresCodec>;
|
||||||
|
|
||||||
|
pub fn connect(host: &Host,
|
||||||
|
port: u16,
|
||||||
|
handle: &Handle)
|
||||||
|
-> BoxFuture<PostgresStream, io::Error> {
|
||||||
|
match *host {
|
||||||
|
Host::Tcp(ref host) => {
|
||||||
|
tokio_dns::tcp_connect((&**host, port), handle.remote().clone())
|
||||||
|
.map(|s| InnerStream::Tcp(s).framed(PostgresCodec))
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
Host::Unix(ref host) => {
|
||||||
|
let addr = host.join(format!(".s.PGSQL.{}", port));
|
||||||
|
UnixStream::connect(addr, handle)
|
||||||
|
.map(|s| InnerStream::Unix(s).framed(PostgresCodec))
|
||||||
|
.into_future()
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum InnerStream {
|
||||||
|
Tcp(TcpStream),
|
||||||
|
Unix(UnixStream),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Read for InnerStream {
|
||||||
|
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||||
|
match *self {
|
||||||
|
InnerStream::Tcp(ref mut s) => s.read(buf),
|
||||||
|
InnerStream::Unix(ref mut s) => s.read(buf),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Write for InnerStream {
|
||||||
|
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||||
|
match *self {
|
||||||
|
InnerStream::Tcp(ref mut s) => s.write(buf),
|
||||||
|
InnerStream::Unix(ref mut s) => s.write(buf),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> io::Result<()> {
|
||||||
|
match *self {
|
||||||
|
InnerStream::Tcp(ref mut s) => s.flush(),
|
||||||
|
InnerStream::Unix(ref mut s) => s.flush(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Io for InnerStream {
|
||||||
|
fn poll_read(&mut self) -> Async<()> {
|
||||||
|
match *self {
|
||||||
|
InnerStream::Tcp(ref mut s) => s.poll_read(),
|
||||||
|
InnerStream::Unix(ref mut s) => s.poll_read(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_write(&mut self) -> Async<()> {
|
||||||
|
match *self {
|
||||||
|
InnerStream::Tcp(ref mut s) => s.poll_write(),
|
||||||
|
InnerStream::Unix(ref mut s) => s.poll_write(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PostgresCodec;
|
||||||
|
|
||||||
|
impl Codec for PostgresCodec {
|
||||||
|
type In = backend::Message<Vec<u8>>;
|
||||||
|
type Out = Vec<u8>;
|
||||||
|
|
||||||
|
// FIXME ideally we'd avoid re-copying the data
|
||||||
|
fn decode(&mut self, buf: &mut EasyBuf) -> io::Result<Option<Self::In>> {
|
||||||
|
match try!(backend::Message::parse_owned(buf.as_ref())) {
|
||||||
|
ParseResult::Complete { message, consumed } => {
|
||||||
|
buf.drain_to(consumed);
|
||||||
|
Ok(Some(message))
|
||||||
|
}
|
||||||
|
ParseResult::Incomplete { .. } => Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode(&mut self, msg: Vec<u8>, buf: &mut Vec<u8>) -> io::Result<()> {
|
||||||
|
buf.extend_from_slice(&msg);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
28
postgres-tokio/src/test.rs
Normal file
28
postgres-tokio/src/test.rs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
use tokio_core::reactor::Core;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn basic() {
|
||||||
|
let mut l = Core::new().unwrap();
|
||||||
|
let handle = l.handle();
|
||||||
|
let done = Connection::connect("postgres://postgres@localhost", &handle);
|
||||||
|
let conn = l.run(done).unwrap();
|
||||||
|
assert!(conn.cancel_data().process_id != 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn md5_user() {
|
||||||
|
let mut l = Core::new().unwrap();
|
||||||
|
let handle = l.handle();
|
||||||
|
let done = Connection::connect("postgres://md5_user:password@localhost/postgres", &handle);
|
||||||
|
l.run(done).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pass_user() {
|
||||||
|
let mut l = Core::new().unwrap();
|
||||||
|
let handle = l.handle();
|
||||||
|
let done = Connection::connect("postgres://pass_user:password@localhost/postgres", &handle);
|
||||||
|
l.run(done).unwrap();
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user