Skip to content
This repository has been archived by the owner on Dec 14, 2024. It is now read-only.

Commit

Permalink
feat: Add x-saasify-secret verification & retry mechanism (#51)
Browse files Browse the repository at this point in the history
* feat: Add x-saasify-secret verification

* fix: Put e2e tests in rust file (fix #46)

* feat: Add try mechanism (fix #44)

* Fix clippy
  • Loading branch information
amaury1093 authored May 10, 2020
1 parent 52427d5 commit 5767e1e
Show file tree
Hide file tree
Showing 10 changed files with 212 additions and 88 deletions.
22 changes: 0 additions & 22 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,28 +77,6 @@ jobs:
command: clippy
args: -- -D warnings

# Simple E2E test
e2e:
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v2

- name: Install stable toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true

- name: Run cargo build
uses: actions-rs/cargo@v1
with:
command: build

- name: Run E2E tests
run: ./scripts/e2e.sh

# Build a binary with target `x86_64-unknown-linux-musl`, put in artifacts
build:
runs-on: ubuntu-latest
Expand Down
18 changes: 15 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 6 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
[package]
name = "reacher"
version = "0.1.8"
version = "0.1.9"
edition = "2018"
publish = false

[dependencies]
check-if-email-exists = "0.8"
async-recursion = "0.2"
check-if-email-exists = { git = "https://github.com/amaurymartiny/check-if-email-exists" }
env_logger = "0.7"
log = "0.4"
sentry = "0.17"
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "0.2", features = ["macros"] }
warp = "0.2"

[dev-dependencies]
serde_json = "1.0"
4 changes: 2 additions & 2 deletions Dockerfile.production
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ FROM alpine:latest

WORKDIR /reacher

ENV REACHER_VERSION 0.1.8
# Environment variables for Tor proxying in Reacher
ENV REACHER_VERSION 0.1.9
ENV RCH_HTTP_HOST = 0.0.0.0
ENV RCH_PROXY_HOST = 127.0.0.1
ENV RCH_PROXY_PORT = 9050

Expand Down
2 changes: 1 addition & 1 deletion Dockerfile.staging
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ FROM alpine:latest

WORKDIR /reacher

# Environment variables for Tor proxying in Reacher
ENV RCH_HTTP_HOST = 0.0.0.0
ENV RCH_PROXY_HOST = 127.0.0.1
ENV RCH_PROXY_PORT = 9050

Expand Down
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,13 @@ The server will then be listening on `http://127.0.0.1:8080`.

These are the environment variables used to configure the HTTP server:

| Env Var | Required? | Description |
| ---------------- | --------- | ------------------------------------------------------------------ |
| `RCH_FROM_EMAIL` | Yes | The email to use in the `MAIL FROM:` SMTP command. |
| `RCH_PROXY_HOST` | No | Use the specified SOCKS5 proxy host to perform email verification. |
| `RCH_PROXY_PORT` | No | Use the specified SOCKS5 proxy port to perform email verification. |
| `RCH_SENTRY_DSN` | No | [Sentry](https://sentry.io) DSN used for bug reports. |
| Env Var | Required? | Description |
| -------------------- | --------- | ---------------------------------------------------------------------------- |
| `RCH_FROM_EMAIL` | NO | The email to use in the `MAIL FROM:` SMTP command. |
| `RCH_PROXY_HOST` | No | Use the specified SOCKS5 proxy host to perform email verification. |
| `RCH_PROXY_PORT` | No | Use the specified SOCKS5 proxy port to perform email verification. |
| `RCH_SAASIFY_SECRET` | No | If set, all incoming requests will need to have a `x-saasify-secret` header. |
| `RCH_SENTRY_DSN` | No | [Sentry](https://sentry.io) DSN used for bug reports. |

## See also

Expand Down
34 changes: 0 additions & 34 deletions scripts/e2e.sh

This file was deleted.

35 changes: 29 additions & 6 deletions src/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
// You should have received a copy of the GNU General Public License
// along with Reacher. If not, see <http://www.gnu.org/licenses/>.

use async_recursion::async_recursion;
use check_if_email_exists::{check_email as ciee_check_email, CheckEmailInput, CheckEmailOutput};
use sentry::protocol::{Event, Value};
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -55,15 +56,39 @@ fn log_error(
))
}

/// A recursive async function to retry the `ciee_check_email` function
/// multiple times.
///
/// # Panics
///
/// The `input.to_emails` field is assumed to contain exactly email address to
/// check. The function will panic if this field is empty. If it contains more
/// than 1 field, subsequent emails will be ignored.
#[async_recursion]
async fn retry(input: &CheckEmailInput, count: u8) -> CheckEmailOutput {
let result = ciee_check_email(input)
.await
.pop()
.expect("The input has one element, so does the output. qed.");

// We retry if at least one of the misc, mx or smtp fields contains an
// error.
if count <= 1 || (result.misc.is_ok() && result.mx.is_ok() && result.smtp.is_ok()) {
result
} else {
retry(input, count - 1).await
}
}

/// Given an email address (and optionally some additional configuration
/// options), return if email verification details as given by
/// `check_if_email_exists`.
pub async fn check_email(body: EmailInput) -> Result<impl warp::Reply, Infallible> {
pub async fn check_email(_: (), body: EmailInput) -> Result<impl warp::Reply, Infallible> {
// Create EmailInput for check_if_email_exists from body
let mut input = CheckEmailInput::new(vec![body.to_email]);
input
.from_email(body.from_email.unwrap_or_else(|| {
env::var("RCH_FROM_EMAIL").expect("You must set a RCH_FROM_EMAIL env var.")
env::var("RCH_FROM_EMAIL").unwrap_or_else(|_| "user@example.org".into())
}))
.hello_name(body.hello_name.unwrap_or_else(|| "gmail.com".into()));

Expand All @@ -76,10 +101,8 @@ pub async fn check_email(body: EmailInput) -> Result<impl warp::Reply, Infallibl
}
}

let mut result = ciee_check_email(&input).await;
let result = result
.pop()
.expect("The input has one element, so does the output. qed.");
// Run `ciee_check_email` function 3 times max.
let result = retry(&input, 3).await;

// We consider `email_exists` failed if at least one of the misc, mx or smtp
// fields contains an error.
Expand Down
124 changes: 112 additions & 12 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,39 @@
// along with Reacher. If not, see <http://www.gnu.org/licenses/>.

mod handlers;
mod saasify_secret;

use std::env;
use saasify_secret::check_saasify_secret;
use std::{env, net::IpAddr};
use warp::Filter;

/// Create all the endpoints of our API.
fn create_api() -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
// POST /check_email
warp::path("check_email")
.and(warp::post())
// FIXME We should be able to just use warp::header::exact, and remove
// completely `./saasify_secret.rs`.
// https://github.com/seanmonstar/warp/issues/503
.and(check_saasify_secret())
// When accepting a body, we want a JSON body (and to reject huge
// payloads)...
.and(warp::body::content_length_limit(1024 * 16))
.and(warp::body::json())
.and_then(handlers::check_email)
// View access logs by setting `RUST_LOG=reacher`.
.with(warp::log("reacher"))
}

/// Run a HTTP server using warp.
///
/// # Panics
///
/// If at least one of the environment variables:
/// - RCH_HTTP_HOST
/// - RCH_PROXY_HOST
/// - RCH_PROXY_PORT
/// is malformed, then the program will panic.
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
env_logger::init();
Expand All @@ -33,21 +61,93 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
log::info!(target: "reacher", "Sentry is successfully set up.")
}

// POST /check_email
let routes = warp::post()
.and(warp::path("check_email"))
// When accepting a body, we want a JSON body (and to reject huge
// payloads)...
.and(warp::body::content_length_limit(1024 * 16))
.and(warp::body::json())
.and_then(handlers::check_email)
// View access logs by setting `RUST_LOG=reacher`.
.with(warp::log("reacher"));
let api = create_api();

log::info!(target: "reacher", "Server is listening on :8080.");

// Since we're running the HTTP server inside a Docker container, we
// use 0.0.0.0. The port is 8080 as per Fly documentation.
warp::serve(routes).run(([0, 0, 0, 0], 8080)).await;
warp::serve(api)
.run((
env::var("RCH_HTTP_HOST")
.unwrap_or_else(|_| "127.0.0.1".into())
.parse::<IpAddr>()
.expect("RCH_HTTP_HOST is malformed."),
8080,
))
.await;
Ok(())
}

#[cfg(test)]
mod tests {
use serde_json;
use warp::http::StatusCode;
use warp::test::request;

use super::{create_api, handlers::EmailInput};

#[tokio::test]
async fn test_missing_saasify_secret() {
let resp = request()
.path("/check_email")
.method("POST")
.reply(&create_api())
.await;

assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
assert_eq!(
resp.body(),
"Missing request header \"x-saasify-secret\"".as_bytes()
);
}
#[tokio::test]
async fn test_incorrect_saasify_secret() {
let resp = request()
.path("/check_email")
.method("POST")
.header("x-saasify-secret", "incorrect")
.reply(&create_api())
.await;

assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
assert_eq!(
resp.body(),
"Unhandled rejection: IncorrectSaasifySecret".as_bytes()
);
}

#[tokio::test]
async fn test_input_foo_bar() {
let resp = request()
.path("/check_email")
.method("POST")
.header("x-saasify-secret", "reacher_dev_secret")
.json(&serde_json::from_str::<EmailInput>(r#"{"to_email": "foo@bar"}"#).unwrap())
.reply(&create_api())
.await;

assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.body(),
r#"{"input":"foo@bar","misc":{"is_disposable":false},"mx":{"accepts_mail":false,"records":[]},"smtp":{"can_connect_smtp":false,"has_full_inbox":false,"is_catch_all":false,"is_deliverable":false,"is_disabled":false},"syntax":{"address":null,"domain":"","is_valid_syntax":false,"username":""}}"#.as_bytes()
);
}

#[tokio::test]
async fn test_input_foo_bar_baz() {
let resp = request()
.path("/check_email")
.method("POST")
.header("x-saasify-secret", "reacher_dev_secret")
.json(&serde_json::from_str::<EmailInput>(r#"{"to_email": "foo@bar.baz"}"#).unwrap())
.reply(&create_api())
.await;

assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.body(),
r#"{"input":"foo@bar.baz","misc":{"is_disposable":false},"mx":{"accepts_mail":false,"records":[]},"smtp":{"can_connect_smtp":false,"has_full_inbox":false,"is_catch_all":false,"is_deliverable":false,"is_disabled":false},"syntax":{"address":"foo@bar.baz","domain":"bar.baz","is_valid_syntax":true,"username":"foo"}}"#.as_bytes()
);
}
}
Loading

0 comments on commit 5767e1e

Please sign in to comment.