hyperacme 0.0.3

Async library for requesting certificates from an ACME provider (acme-micro fork).
Documentation
#![allow(clippy::trivial_regex)]

use futures::Future;
use hyper::{service::service_fn_ok, Body, Method, Request, Response, Server};
use lazy_static::lazy_static;
use std::net::TcpListener;
use std::thread;

lazy_static! {
    static ref RE_URL: regex::Regex = regex::Regex::new("<URL>").unwrap();
}

pub struct TestServer {
    pub dir_url: String,
    shutdown: Option<futures::sync::oneshot::Sender<()>>,
}

impl Drop for TestServer {
    fn drop(&mut self) {
        self.shutdown.take().unwrap().send(()).ok();
    }
}

fn get_directory(url: &str) -> Response<Body> {
    const BODY: &str = r#"{
    "keyChange": "<URL>/acme/key-change",
    "newAccount": "<URL>/acme/new-acct",
    "newNonce": "<URL>/acme/new-nonce",
    "newOrder": "<URL>/acme/new-order",
    "revokeCert": "<URL>/acme/revoke-cert",
    "meta": {
        "caaIdentities": [
        "testdir.org"
        ]
    }
    }"#;
    Response::new(Body::from(RE_URL.replace_all(BODY, url)))
}

fn head_new_nonce() -> Response<Body> {
    Response::builder()
        .status(204)
        .header(
            "Replay-Nonce",
            "8_uBBV3N2DBRJczhoiB46ugJKUkUHxGzVe6xIMpjHFM",
        )
        .body(Body::empty())
        .unwrap()
}

fn post_new_acct(url: &str) -> Response<Body> {
    const BODY: &str = r#"{
    "id": 7728515,
    "key": {
        "use": "sig",
        "kty": "EC",
        "crv": "P-256",
        "alg": "ES256",
        "x": "ttpobTRK2bw7ttGBESRO7Nb23mbIRfnRZwunL1W6wRI",
        "y": "h2Z00J37_2qRKH0-flrHEsH0xbit915Tyvd2v_CAOSk"
    },
    "contact": [
        "mailto:foo@bar.com"
    ],
    "initialIp": "90.171.37.12",
    "createdAt": "2018-12-31T17:15:40.399104457Z",
    "status": "valid"
    }"#;
    let location: String = RE_URL.replace_all("<URL>/acme/acct/7728515", url).into();
    Response::builder()
        .status(201)
        .header("Location", location)
        .body(Body::from(BODY))
        .unwrap()
}

fn post_new_order(url: &str) -> Response<Body> {
    const BODY: &str = r#"{
    "status": "pending",
    "expires": "2019-01-09T08:26:43.570360537Z",
    "identifiers": [
        {
        "type": "dns",
        "value": "acmetest.example.com"
        }
    ],
    "authorizations": [
        "<URL>/acme/authz/YTqpYUthlVfwBncUufE8IRWLMSRqcSs"
    ],
    "finalize": "<URL>/acme/finalize/7738992/18234324"
    }"#;
    let location: String = RE_URL
        .replace_all("<URL>/acme/order/YTqpYUthlVfwBncUufE8", url)
        .into();
    Response::builder()
        .status(201)
        .header("Location", location)
        .body(Body::from(RE_URL.replace_all(BODY, url)))
        .unwrap()
}

fn post_get_order(url: &str) -> Response<Body> {
    const BODY: &str = r#"{
    "status": "<STATUS>",
    "expires": "2019-01-09T08:26:43.570360537Z",
    "identifiers": [
        {
        "type": "dns",
        "value": "acmetest.example.com"
        }
    ],
    "authorizations": [
        "<URL>/acme/authz/YTqpYUthlVfwBncUufE8IRWLMSRqcSs"
    ],
    "finalize": "<URL>/acme/finalize/7738992/18234324",
    "certificate": "<URL>/acme/cert/fae41c070f967713109028"
    }"#;
    let b = RE_URL.replace_all(BODY, url).to_string();
    Response::builder().status(200).body(Body::from(b)).unwrap()
}

fn post_authz(url: &str) -> Response<Body> {
    const BODY: &str = r#"{
        "identifier": {
            "type": "dns",
            "value": "acmetest.algesten.se"
        },
        "status": "pending",
        "expires": "2019-01-09T08:26:43Z",
        "challenges": [
        {
            "type": "http-01",
            "status": "pending",
            "url": "<URL>/acme/challenge/YTqpYUthlVfwBncUufE8IRWLMSRqcSs/216789597",
            "token": "MUi-gqeOJdRkSb_YR2eaMxQBqf6al8dgt_dOttSWb0w"
        },
        {
            "type": "tls-alpn-01",
            "status": "pending",
            "url": "<URL>/acme/challenge/YTqpYUthlVfwBncUufE8IRWLMSRqcSs/216789598",
            "token": "WCdRWkCy4THTD_j5IH4ISAzr59lFIg5wzYmKxuOJ1lU"
        },
        {
            "type": "dns-01",
            "status": "pending",
            "url": "<URL>/acme/challenge/YTqpYUthlVfwBncUufE8IRWLMSRqcSs/216789599",
            "token": "RRo2ZcXAEqxKvMH8RGcATjSK1KknLEUmauwfQ5i3gG8"
        }
        ]
    }"#;
    Response::builder()
        .status(201)
        .body(Body::from(RE_URL.replace_all(BODY, url)))
        .unwrap()
}

fn post_finalize(_url: &str) -> Response<Body> {
    Response::builder().status(200).body(Body::empty()).unwrap()
}

fn post_certificate(_url: &str) -> Response<Body> {
    Response::builder()
        .status(200)
        .body("CERT HERE".into())
        .unwrap()
}

fn route_request(req: Request<Body>, url: &str) -> Response<Body> {
    match (req.method(), req.uri().path()) {
        (&Method::GET, "/directory") => get_directory(url),
        (&Method::HEAD, "/acme/new-nonce") => head_new_nonce(),
        (&Method::POST, "/acme/new-acct") => post_new_acct(url),
        (&Method::POST, "/acme/new-order") => post_new_order(url),
        (&Method::POST, "/acme/order/YTqpYUthlVfwBncUufE8") => post_get_order(url),
        (&Method::POST, "/acme/authz/YTqpYUthlVfwBncUufE8IRWLMSRqcSs") => post_authz(url),
        (&Method::POST, "/acme/finalize/7738992/18234324") => post_finalize(url),
        (&Method::POST, "/acme/cert/fae41c070f967713109028") => post_certificate(url),
        (_, _) => Response::builder().status(404).body(Body::empty()).unwrap(),
    }
}

pub fn with_directory_server() -> TestServer {
    let tcp = TcpListener::bind("127.0.0.1:0").unwrap();
    let port = tcp.local_addr().unwrap().port();

    let url = format!("http://127.0.0.1:{}", port);
    let dir_url = format!("{}/directory", url);

    let make_service = move || {
        let url2 = url.clone();
        service_fn_ok(move |req| route_request(req, &url2))
    };
    let server = Server::from_tcp(tcp).unwrap().serve(make_service);

    let (tx, rx) = futures::sync::oneshot::channel::<()>();

    let graceful = server
        .with_graceful_shutdown(rx)
        .map_err(|err| eprintln!("server error: {}", err));

    thread::spawn(move || {
        hyper::rt::run(graceful);
    });

    TestServer {
        dir_url,
        shutdown: Some(tx),
    }
}

#[tokio::test]
pub async fn test_make_directory() {
    let server = with_directory_server();
    let res = reqwest::get(&server.dir_url).await.unwrap();
    assert!(res.status() == 200 || res.status() == 204);
}