Skip to content

Commit

Permalink
Merge pull request #151 from portier/featNormalize
Browse files Browse the repository at this point in the history
Expose normalization as endpoint & upgrade dependencies
  • Loading branch information
Stéphan Kochen authored Jan 5, 2018
2 parents f0588de + 429d535 commit c56fa59
Show file tree
Hide file tree
Showing 15 changed files with 378 additions and 265 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# This file doubles as .gitignore and .dockerignore

# Version control
.git

# Build products
target
lang/*.mo
Expand Down
401 changes: 222 additions & 179 deletions Cargo.lock

Large diffs are not rendered by default.

32 changes: 17 additions & 15 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,24 +21,26 @@ path = "src/main.rs"
glob = "0.2.11"

[dependencies]
base64 = "0.7.0"
base64 = "0.8.0"
docopt = "0.8.1"
env_logger = "0.4.3"
futures = "0.1.14"
futures = "0.1.17"
gettext = "0.2.0"
hyper = "0.11.2"
hyper = "0.11.7"
hyper-staticfile = "0.1.1"
hyper-tls = "0.1.2"
lettre = "0.6.2"
log = "0.3.6"
mustache = "0.8.0"
openssl = "0.9.15"
rand = "0.3.15"
lettre = "0.7.0"
lettre_email = "0.7.0"
log = "0.3.8"
mustache = "0.8.2"
native-tls = "0.1.4"
openssl = "0.9.22"
rand = "0.3.18"
redis = "0.8.0"
serde = "1.0.11"
serde_derive = "1.0.11"
serde_json = "1.0.2"
time = "0.1.35"
tokio-core = "0.1.9"
toml = "0.4.4"
url = "1.5.1"
serde = "1.0.23"
serde_derive = "1.0.23"
serde_json = "1.0.7"
time = "0.1.38"
tokio-core = "0.1.10"
toml = "0.4.5"
url = "1.6.0"
62 changes: 42 additions & 20 deletions src/bridges/email.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
use bridges::{BridgeData, complete_auth};
use config::Config;
use crypto::{random_zbase32};
use email_address::EmailAddress;
use error::BrokerError;
use futures::future;
use http::{ContextHandle, HandlerResult};
use hyper::Response;
use hyper::header::ContentType;
use lettre::email::EmailBuilder;
use lettre::transport::EmailTransport;
use lettre::transport::smtp::SmtpTransportBuilder;
use lettre_email::EmailBuilder;
use lettre::EmailTransport;
use lettre::smtp::{ClientSecurity, SmtpTransport};
use lettre::smtp::client::net::ClientTlsParameters;
use lettre::smtp::authentication::Credentials;
use native_tls::TlsConnector;
use std::rc::Rc;
use url::percent_encoding::{utf8_percent_encode, QUERY_ENCODE_SET};

Expand Down Expand Up @@ -60,21 +64,12 @@ pub fn auth(ctx_handle: &ContextHandle, email_addr: &Rc<EmailAddress>) -> Handle
EmailBuilder::new()
.to(email_addr.as_str())
.from((&*ctx.app.from_address, &*ctx.app.from_name))
.alternative(&ctx.app.templates.email_html.render(params),
&ctx.app.templates.email_text.render(params))
.subject(&[catalog.gettext("Finish logging in to"), display_origin.as_str()].join(" "))
.alternative(ctx.app.templates.email_html.render(params),
ctx.app.templates.email_text.render(params))
.subject([catalog.gettext("Finish logging in to"), display_origin.as_str()].join(" "))
.build()
.unwrap_or_else(|err| panic!("unhandled error building email: {}", err))
};
let mut builder = match SmtpTransportBuilder::new(&ctx.app.smtp_server) {
Ok(builder) => builder,
Err(err) => return Box::new(future::err(BrokerError::Internal(
format!("could not create the smtp transport: {}", err)))),
};

if let (&Some(ref username), &Some(ref password)) = (&ctx.app.smtp_username, &ctx.app.smtp_password) {
builder = builder.credentials(username, password);
}

// Store the code in the session for use in the verify handler. We should never fail to claim
// the session, because we only get here after all other options have failed.
Expand All @@ -88,12 +83,14 @@ pub fn auth(ctx_handle: &ContextHandle, email_addr: &Rc<EmailAddress>) -> Handle
}

// Send the mail.
let mut mailer = builder.build();
if let Err(err) = mailer.send(email) {
let mut mailer = match build_transport(&ctx.app) {
Ok(mailer) => mailer,
Err(reason) => return Box::new(future::err(BrokerError::Internal(reason))),
};
if let Err(err) = mailer.send(&email) {
return Box::new(future::err(BrokerError::Internal(
format!("could not send mail: {}", err))))
}

mailer.close();

// Render a form for the user.
Expand All @@ -118,19 +115,44 @@ pub fn auth(ctx_handle: &ContextHandle, email_addr: &Rc<EmailAddress>) -> Handle
/// returns the resulting token to the relying party.
pub fn confirmation(ctx_handle: &ContextHandle) -> HandlerResult {
let mut ctx = ctx_handle.borrow_mut();
let mut params = ctx.query_params();

let session_id = try_get_provider_param!(ctx, "session");
let session_id = try_get_provider_param!(params, "session");
let bridge_data = match ctx.load_session(&session_id) {
Ok(BridgeData::Email(bridge_data)) => Rc::new(bridge_data),
Ok(_) => return Box::new(future::err(BrokerError::ProviderInput("invalid session".to_owned()))),
Err(e) => return Box::new(future::err(e)),
};

let code = try_get_provider_param!(ctx, "code")
let code = try_get_provider_param!(params, "code")
.replace(|c: char| c.is_whitespace(), "").to_lowercase();
if code != bridge_data.code {
return Box::new(future::err(BrokerError::ProviderInput("incorrect code".to_owned())));
}

Box::new(future::result(complete_auth(&*ctx)))
}


/// Build the SMTP transport from config.
fn build_transport(app: &Config) -> Result<SmtpTransport, String> {
// Extract domain, and build an address with a default port.
// Split the same way `to_socket_addrs` does.
let parts = app.smtp_server.rsplitn(2, ':').collect::<Vec<_>>();
let (domain, addr) = if parts.len() == 2 {
(parts[1].to_owned(), app.smtp_server.to_owned())
} else {
(parts[0].to_owned(), format!("{}:25", app.smtp_server))
};

// TODO: Configurable security.
let tls_connector = TlsConnector::builder().and_then(|builder| builder.build())
.map_err(|e| format!("could not initialize tls: {}", e))?;
let security = ClientSecurity::Opportunistic(ClientTlsParameters::new(domain, tls_connector));
let mut builder = SmtpTransport::builder(&addr, security)
.map_err(|e| format!("could not create the smtp transport: {}", e))?;
if let (&Some(ref username), &Some(ref password)) = (&app.smtp_username, &app.smtp_password) {
builder = builder.credentials(Credentials::new(username.to_owned(), password.to_owned()));
}
Ok(builder.build())
}
7 changes: 4 additions & 3 deletions src/bridges/oidc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use webfinger::{Link, Relation};


/// The origin of the Google identity provider.
pub const GOOGLE_IDP_ORIGIN: &'static str = "https://accounts.google.com";
pub const GOOGLE_IDP_ORIGIN: &str = "https://accounts.google.com";


/// Normalization to apply to an email address.
Expand Down Expand Up @@ -207,15 +207,16 @@ pub fn fragment_callback(ctx_handle: &ContextHandle) -> HandlerResult {
pub fn callback(ctx_handle: &ContextHandle) -> HandlerResult {
let (bridge_data, id_token) = {
let mut ctx = ctx_handle.borrow_mut();
let mut params = ctx.form_params();

let session_id = try_get_provider_param!(ctx, "state");
let session_id = try_get_provider_param!(params, "state");
let bridge_data = match ctx.load_session(&session_id) {
Ok(BridgeData::Oidc(bridge_data)) => Rc::new(bridge_data),
Ok(_) => return Box::new(future::err(BrokerError::ProviderInput("invalid session".to_owned()))),
Err(e) => return Box::new(future::err(e)),
};

let id_token = try_get_provider_param!(ctx, "id_token");
let id_token = try_get_provider_param!(params, "id_token");
(bridge_data, id_token)
};

Expand Down
2 changes: 1 addition & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ pub struct I18n {
}


const SUPPORTED_LANGUAGES: &'static [&'static str] = &["en", "de", "nl"];
const SUPPORTED_LANGUAGES: &[&str] = &["en", "de", "nl"];

impl Default for I18n {
fn default() -> I18n {
Expand Down
6 changes: 3 additions & 3 deletions src/crypto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ impl NamedKey {
let mut signer = Signer::new(MessageDigest::sha256(), &self.key)
.expect("could not initialize signer");
let sig = signer.update(&input)
.and_then(|_| signer.finish())
.and_then(|_| signer.sign_to_vec())
.expect("failed to sign jwt");

input.push(b'.');
Expand Down Expand Up @@ -159,7 +159,7 @@ pub fn nonce() -> String {
/// Helper function to create a random string consisting of
/// characters from the z-base-32 set.
pub fn random_zbase32(len: usize) -> String {
const CHARSET: &'static [u8] = b"13456789abcdefghijkmnopqrstuwxyz";
const CHARSET: &[u8] = b"13456789abcdefghijkmnopqrstuwxyz";
String::from_utf8((0..len).map(|_| {
CHARSET[random::<usize>() % CHARSET.len()]
}).collect()).expect("failed to build one-time pad")
Expand Down Expand Up @@ -207,7 +207,7 @@ pub fn verify_jws(jws: &str, key_set: &[ProviderKey]) -> Result<json::Value, ()>
let message_len = parts[0].len() + parts[1].len() + 1;
let mut verifier = Verifier::new(MessageDigest::sha256(), &pub_key).map_err(|_| ())?;
verifier.update(jws[..message_len].as_bytes())
.and_then(|_| verifier.finish(&decoded[2]))
.and_then(|_| verifier.verify(&decoded[2]))
.map_err(|_| ())
.and_then(|ok| {
if ok {
Expand Down
2 changes: 1 addition & 1 deletion src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ impl BrokerError {
// Internal errors should ring alarm bells.
ref err @ BrokerError::Internal(_) => {
let reference = random_zbase32(6);
error!("[REF:{}] {}", err.description(), reference);
error!("[REF:{}] {}", reference, err.description());
Some(reference)
},
}
Expand Down
18 changes: 12 additions & 6 deletions src/handlers/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use email_address::EmailAddress;
use error::BrokerError;
use futures::future::{self, Future, Either};
use http::{ContextHandle, HandlerResult, json_response};
use hyper::Method;
use std::rc::Rc;
use std::time::Duration;
use store_limits::addr_limiter;
Expand Down Expand Up @@ -59,10 +60,15 @@ pub fn key_set(ctx_handle: &ContextHandle) -> HandlerResult {
/// `email::request()` function to allow authentication through the email loop.
pub fn auth(ctx_handle: &ContextHandle) -> HandlerResult {
let mut ctx = ctx_handle.borrow_mut();
let mut params = match ctx.method {
Method::Get => ctx.query_params(),
Method::Post => ctx.form_params(),
_ => unreachable!(),
};

let redirect_uri = try_get_input_param!(ctx, "redirect_uri");
let client_id = try_get_input_param!(ctx, "client_id");
if try_get_input_param!(ctx, "response_mode", "fragment".to_owned()) != "form_post" {
let redirect_uri = try_get_input_param!(params, "redirect_uri");
let client_id = try_get_input_param!(params, "client_id");
if try_get_input_param!(params, "response_mode", "fragment".to_owned()) != "form_post" {
return Box::new(future::err(BrokerError::Input(
"unsupported response_mode, only form_post is supported".to_owned())));
}
Expand All @@ -88,9 +94,9 @@ pub fn auth(ctx_handle: &ContextHandle) -> HandlerResult {
}
}

let nonce = try_get_input_param!(ctx, "nonce");
let login_hint = try_get_input_param!(ctx, "login_hint");
if try_get_input_param!(ctx, "response_type") != "id_token" {
let nonce = try_get_input_param!(params, "nonce");
let login_hint = try_get_input_param!(params, "login_hint");
if try_get_input_param!(params, "response_type") != "id_token" {
return Box::new(future::err(BrokerError::Input(
"unsupported response_type, only id_token is supported".to_owned())));
}
Expand Down
1 change: 1 addition & 0 deletions src/handlers/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pub mod auth;
pub mod normalize;
pub mod pages;
33 changes: 33 additions & 0 deletions src/handlers/normalize.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
use email_address::EmailAddress;
use futures::future;
use http::{ContextHandle, HandlerResult};
use hyper::header::{CacheControl, CacheDirective, ContentType};
use hyper::server::Response;

/// Request handler for the email normalization endpoint.
///
/// Performs normalization of email addresses, for clients that cannot implement all the necessary
/// parts of the relevant specifications. (Unicode, WHATWG, etc.)
pub fn normalize(ctx_handle: &ContextHandle) -> HandlerResult {
let ctx = ctx_handle.borrow();

let result = String::from_utf8_lossy(&ctx.body)
.lines()
.map(|s| {
match s.parse::<EmailAddress>() {
Ok(addr) => addr.to_string(),
Err(_) => "".to_owned(),
}
})
.collect::<Vec<_>>()
.join("\n");

let res = Response::new()
.with_header(ContentType::plaintext())
.with_header(CacheControl(vec![
CacheDirective::NoCache,
CacheDirective::NoStore,
]))
.with_body(result);
Box::new(future::ok(res))
}
Loading

0 comments on commit c56fa59

Please sign in to comment.