Add an authentication trait
This commit is contained in:
101
src/authentication.rs
Normal file
101
src/authentication.rs
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
// Copyright 2014 Alexis Mousset. See the COPYRIGHT
|
||||||
|
// file at the top-level directory of this distribution.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||||
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
||||||
|
// option. This file may not be copied, modified, or distributed
|
||||||
|
// except according to those terms.
|
||||||
|
|
||||||
|
//! Provides authentication mecanisms
|
||||||
|
|
||||||
|
use serialize::base64::{self, ToBase64, FromBase64};
|
||||||
|
use serialize::hex::ToHex;
|
||||||
|
use crypto::hmac::Hmac;
|
||||||
|
use crypto::md5::Md5;
|
||||||
|
use crypto::mac::Mac;
|
||||||
|
|
||||||
|
use NUL;
|
||||||
|
use error::Error;
|
||||||
|
use extension::Extension;
|
||||||
|
|
||||||
|
/// Represents an authentication mecanism
|
||||||
|
pub trait Mecanism {
|
||||||
|
/// Returns the matching `Extension`
|
||||||
|
fn extension() -> Extension;
|
||||||
|
/// Returns the initial response support
|
||||||
|
fn supports_initial_response() -> bool;
|
||||||
|
/// Returns the response
|
||||||
|
fn response(username: &str, password: &str, challenge: Option<&str>) -> Result<String, Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// PLAIN authentication mecanism
|
||||||
|
/// RFC 4616: https://tools.ietf.org/html/rfc4616
|
||||||
|
#[derive(Copy,Clone)]
|
||||||
|
pub struct Plain;
|
||||||
|
|
||||||
|
impl Mecanism for Plain {
|
||||||
|
fn extension() -> Extension {
|
||||||
|
Extension::PlainAuthentication
|
||||||
|
}
|
||||||
|
|
||||||
|
fn supports_initial_response() -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn response(username: &str, password: &str, challenge: Option<&str>) -> Result<String, Error> {
|
||||||
|
match challenge {
|
||||||
|
Some(_) => Err(Error::ClientError("This mecanism does not expect a challenge")),
|
||||||
|
None => Ok(format!("{}{}{}{}", NUL, username, NUL, password).as_bytes().to_base64(base64::STANDARD)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// CRAM-MD5 authentication mecanism
|
||||||
|
/// RFC 2195: https://tools.ietf.org/html/rfc2195
|
||||||
|
#[derive(Copy,Clone)]
|
||||||
|
pub struct CramMd5;
|
||||||
|
|
||||||
|
impl Mecanism for CramMd5 {
|
||||||
|
fn extension() -> Extension {
|
||||||
|
Extension::CramMd5Authentication
|
||||||
|
}
|
||||||
|
|
||||||
|
fn supports_initial_response() -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn response(username: &str, password: &str, challenge: Option<&str>) -> Result<String, Error> {
|
||||||
|
let encoded_challenge = match challenge {
|
||||||
|
Some(challenge) => challenge,
|
||||||
|
None => return Err(Error::ClientError("This mecanism does expect a challenge")),
|
||||||
|
};
|
||||||
|
|
||||||
|
let decoded_challenge = match encoded_challenge.from_base64() {
|
||||||
|
Ok(challenge) => challenge,
|
||||||
|
Err(error) => return Err(Error::ChallengeParsingError(error)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut hmac = Hmac::new(Md5::new(), password.as_bytes());
|
||||||
|
hmac.input(&decoded_challenge);
|
||||||
|
|
||||||
|
Ok(format!("{} {}", username, hmac.result().code().to_hex()).as_bytes().to_base64(base64::STANDARD))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::{Mecanism, Plain, CramMd5};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_plain() {
|
||||||
|
assert_eq!(Plain::response("username", "password", None).unwrap(), "AHVzZXJuYW1lAHBhc3N3b3Jk");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cram_md5() {
|
||||||
|
assert_eq!(CramMd5::response("alice", "wonderland",
|
||||||
|
Some("PDE3ODkzLjEzMjA2NzkxMjNAdGVzc2VyYWN0LnN1c2FtLmluPg==")).unwrap(),
|
||||||
|
"YWxpY2UgNjRiMmE0M2MxZjZlZDY4MDZhOTgwOTE0ZTIzZTc1ZjA=");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
// Copyright 2014 Alexis Mousset. See the COPYRIGHT
|
|
||||||
// file at the top-level directory of this distribution.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
|
||||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
|
||||||
// option. This file may not be copied, modified, or distributed
|
|
||||||
// except according to those terms.
|
|
||||||
|
|
||||||
//! Provides authentication functions
|
|
||||||
|
|
||||||
use serialize::base64::{self, ToBase64, FromBase64};
|
|
||||||
use serialize::hex::ToHex;
|
|
||||||
use crypto::hmac::Hmac;
|
|
||||||
use crypto::md5::Md5;
|
|
||||||
use crypto::mac::Mac;
|
|
||||||
|
|
||||||
use NUL;
|
|
||||||
use error::Error;
|
|
||||||
|
|
||||||
/// Returns a PLAIN mecanism response
|
|
||||||
pub fn plain(username: &str, password: &str) -> String {
|
|
||||||
format!("{}{}{}{}", NUL, username, NUL, password).as_bytes().to_base64(base64::STANDARD)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a CRAM-MD5 mecanism response
|
|
||||||
pub fn cram_md5(username: &str, password: &str, encoded_challenge: &str) -> Result<String, Error> {
|
|
||||||
let challenge = match encoded_challenge.from_base64() {
|
|
||||||
Ok(challenge) => challenge,
|
|
||||||
Err(error) => return Err(Error::ChallengeParsingError(error)),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut hmac = Hmac::new(Md5::new(), password.as_bytes());
|
|
||||||
hmac.input(&challenge);
|
|
||||||
|
|
||||||
Ok(format!("{} {}", username, hmac.result().code().to_hex()).as_bytes().to_base64(base64::STANDARD))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use super::{plain, cram_md5};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_plain() {
|
|
||||||
assert_eq!(plain("username", "password"), "AHVzZXJuYW1lAHBhc3N3b3Jk");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_cram_md5() {
|
|
||||||
assert_eq!(cram_md5("alice", "wonderland",
|
|
||||||
"PDE3ODkzLjEzMjA2NzkxMjNAdGVzc2VyYWN0LnN1c2FtLmluPg==").unwrap(),
|
|
||||||
"YWxpY2UgNjRiMmE0M2MxZjZlZDY4MDZhOTgwOTE0ZTIzZTc1ZjA=");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -16,13 +16,12 @@ use std::io::{BufRead, Read, Write};
|
|||||||
use bufstream::BufStream;
|
use bufstream::BufStream;
|
||||||
|
|
||||||
use response::ResponseParser;
|
use response::ResponseParser;
|
||||||
|
use authentication::{Mecanism, CramMd5, Plain};
|
||||||
use error::{Error, SmtpResult};
|
use error::{Error, SmtpResult};
|
||||||
use client::net::{Connector, SmtpStream};
|
use client::net::{Connector, SmtpStream};
|
||||||
use client::authentication::{plain, cram_md5};
|
|
||||||
use {CRLF, MESSAGE_ENDING};
|
use {CRLF, MESSAGE_ENDING};
|
||||||
|
|
||||||
pub mod net;
|
pub mod net;
|
||||||
mod authentication;
|
|
||||||
|
|
||||||
/// Returns the string after adding a dot at the beginning of each line starting with a dot
|
/// Returns the string after adding a dot at the beginning of each line starting with a dot
|
||||||
///
|
///
|
||||||
@@ -170,7 +169,7 @@ impl<S: Connector + Write + Read = SmtpStream> Client<S> {
|
|||||||
|
|
||||||
/// Sends an AUTH command with PLAIN mecanism
|
/// Sends an AUTH command with PLAIN mecanism
|
||||||
pub fn auth_plain(&mut self, username: &str, password: &str) -> SmtpResult {
|
pub fn auth_plain(&mut self, username: &str, password: &str) -> SmtpResult {
|
||||||
self.command(&format!("AUTH PLAIN {}", plain(username, password)))
|
self.command(&format!("AUTH PLAIN {}", try!(Plain::response(username, password, None))))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sends an AUTH command with CRAM-MD5 mecanism
|
/// Sends an AUTH command with CRAM-MD5 mecanism
|
||||||
@@ -180,7 +179,7 @@ impl<S: Connector + Write + Read = SmtpStream> Client<S> {
|
|||||||
None => return Err(Error::ResponseParsingError("Could not read CRAM challenge")),
|
None => return Err(Error::ResponseParsingError("Could not read CRAM challenge")),
|
||||||
};
|
};
|
||||||
|
|
||||||
let cram_response = try!(cram_md5(username, password, &encoded_challenge));
|
let cram_response = try!(CramMd5::response(username, password, Some(&encoded_challenge)));
|
||||||
|
|
||||||
self.command(&format!("AUTH CRAM-MD5 {}", cram_response))
|
self.command(&format!("AUTH CRAM-MD5 {}", cram_response))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -155,6 +155,7 @@ pub mod response;
|
|||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod sendable_email;
|
pub mod sendable_email;
|
||||||
pub mod email;
|
pub mod email;
|
||||||
|
pub mod authentication;
|
||||||
|
|
||||||
// Registrated port numbers:
|
// Registrated port numbers:
|
||||||
// https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml
|
// https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml
|
||||||
|
|||||||
Reference in New Issue
Block a user