rest-http: add axum-based server

axum-login-upgrade
Manos Pitsidianakis 2023-05-12 16:07:55 +03:00
parent 243f4af198
commit 19860d2d87
Signed by: Manos Pitsidianakis
GPG Key ID: 7729C7707F7E09D0
13 changed files with 666 additions and 23 deletions

288
Cargo.lock generated
View File

@ -22,6 +22,21 @@ dependencies = [
"memchr",
]
[[package]]
name = "alloc-no-stdlib"
version = "2.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3"
[[package]]
name = "alloc-stdlib"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece"
dependencies = [
"alloc-no-stdlib",
]
[[package]]
name = "android_system_properties"
version = "0.1.5"
@ -55,6 +70,16 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
[[package]]
name = "assert-json-diff"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12"
dependencies = [
"serde",
"serde_json",
]
[[package]]
name = "assert_cmd"
version = "2.0.11"
@ -81,6 +106,19 @@ dependencies = [
"futures-core",
]
[[package]]
name = "async-compression"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "942c7cd7ae39e91bde4820d74132e9862e62c2f386c3aa90ccf55949f5bad63a"
dependencies = [
"brotli",
"futures-core",
"memchr",
"pin-project-lite",
"tokio",
]
[[package]]
name = "async-executor"
version = "1.5.1"
@ -398,6 +436,19 @@ dependencies = [
"byteorder",
]
[[package]]
name = "bcrypt"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9df288bec72232f78c1ec5fe4e8f1d108aa0265476e93097593c803c8c02062a"
dependencies = [
"base64 0.21.0",
"blowfish",
"getrandom",
"subtle",
"zeroize",
]
[[package]]
name = "bincode"
version = "1.3.3"
@ -461,6 +512,37 @@ dependencies = [
"log",
]
[[package]]
name = "blowfish"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7"
dependencies = [
"byteorder",
"cipher",
]
[[package]]
name = "brotli"
version = "3.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
"brotli-decompressor",
]
[[package]]
name = "brotli-decompressor"
version = "2.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b6561fd3f895a11e8f72af2cb7d22e08366bebc2b6b57f7744c4bda27034744"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
]
[[package]]
name = "bstr"
version = "1.4.0"
@ -631,6 +713,16 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "cipher"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [
"crypto-common",
"inout",
]
[[package]]
name = "clap"
version = "4.2.7"
@ -705,6 +797,25 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "config"
version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d379af7f68bfc21714c6c7dea883544201741d2ce8274bb12fa54f89507f52a7"
dependencies = [
"async-trait",
"json5",
"lazy_static",
"nom",
"pathdiff",
"ron",
"rust-ini",
"serde",
"serde_json",
"toml",
"yaml-rust",
]
[[package]]
name = "constant_time_eq"
version = "0.1.5"
@ -902,6 +1013,12 @@ dependencies = [
"subtle",
]
[[package]]
name = "dlv-list"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257"
[[package]]
name = "doc-comment"
version = "0.3.3"
@ -1474,6 +1591,19 @@ dependencies = [
"want",
]
[[package]]
name = "hyper-tls"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
dependencies = [
"bytes",
"hyper",
"native-tls",
"tokio",
"tokio-native-tls",
]
[[package]]
name = "iana-time-zone"
version = "0.1.56"
@ -1545,6 +1675,15 @@ dependencies = [
"libc",
]
[[package]]
name = "inout"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
dependencies = [
"generic-array",
]
[[package]]
name = "instant"
version = "0.1.12"
@ -1613,6 +1752,17 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "json5"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1"
dependencies = [
"pest",
"pest_derive",
"serde",
]
[[package]]
name = "kernel32-sys"
version = "0.2.2"
@ -1695,6 +1845,12 @@ dependencies = [
"cc",
]
[[package]]
name = "linked-hash-map"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]]
name = "linux-raw-sys"
version = "0.3.7"
@ -1816,8 +1972,23 @@ dependencies = [
name = "mailpot-http"
version = "0.1.1"
dependencies = [
"assert-json-diff",
"async-trait",
"axum",
"bcrypt",
"config",
"http",
"lazy_static",
"log",
"mailpot",
"mailpot-web",
"reqwest",
"serde",
"serde_json",
"stderrlog",
"thiserror",
"tokio",
"tower-http 0.4.0",
]
[[package]]
@ -2168,6 +2339,16 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "ordered-multimap"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a"
dependencies = [
"dlv-list",
"hashbrown",
]
[[package]]
name = "output_vt100"
version = "0.1.3"
@ -2206,12 +2387,62 @@ dependencies = [
"windows-sys 0.45.0",
]
[[package]]
name = "pathdiff"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
[[package]]
name = "percent-encoding"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
[[package]]
name = "pest"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e68e84bfb01f0507134eac1e9b410a12ba379d064eab48c50ba4ce329a527b70"
dependencies = [
"thiserror",
"ucd-trie",
]
[[package]]
name = "pest_derive"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b79d4c71c865a25a4322296122e3924d30bc8ee0834c8bfc8b95f7f054afbfb"
dependencies = [
"pest",
"pest_generator",
]
[[package]]
name = "pest_generator"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c435bf1076437b851ebc8edc3a18442796b30f1728ffea6262d59bbe28b077e"
dependencies = [
"pest",
"pest_meta",
"proc-macro2",
"quote",
"syn 2.0.15",
]
[[package]]
name = "pest_meta"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "745a452f8eb71e39ffd8ee32b3c5f51d03845f99786fa9b68db6ff509c505411"
dependencies = [
"once_cell",
"pest",
"sha2 0.10.6",
]
[[package]]
name = "pin-project"
version = "1.0.12"
@ -2443,10 +2674,12 @@ dependencies = [
"http",
"http-body",
"hyper",
"hyper-tls",
"ipnet",
"js-sys",
"log",
"mime",
"native-tls",
"once_cell",
"percent-encoding",
"pin-project-lite",
@ -2454,6 +2687,7 @@ dependencies = [
"serde_json",
"serde_urlencoded",
"tokio",
"tokio-native-tls",
"tower-service",
"url",
"wasm-bindgen",
@ -2483,6 +2717,17 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b833d8d034ea094b1ea68aa6d5c740e0d04bad9d16568d08ba6f76823a114316"
[[package]]
name = "ron"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88073939a61e5b7680558e6be56b419e208420c2adb92be54921fa6b72283f1a"
dependencies = [
"base64 0.13.1",
"bitflags",
"serde",
]
[[package]]
name = "rusqlite"
version = "0.28.0"
@ -2499,6 +2744,16 @@ dependencies = [
"smallvec",
]
[[package]]
name = "rust-ini"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df"
dependencies = [
"cfg-if 1.0.0",
"ordered-multimap",
]
[[package]]
name = "rustc_version"
version = "0.4.0"
@ -3006,9 +3261,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.28.0"
version = "1.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3c786bf8134e5a3a166db9b29ab8f48134739014a3eca7bc6bfa95d673b136f"
checksum = "0aa32867d44e6f2ce3385e89dceb990188b8bb0fb25b0cf576647a6f98ac5105"
dependencies = [
"autocfg",
"bytes",
@ -3034,6 +3289,16 @@ dependencies = [
"syn 2.0.15",
]
[[package]]
name = "tokio-native-tls"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
dependencies = [
"native-tls",
"tokio",
]
[[package]]
name = "tokio-util"
version = "0.7.8"
@ -3098,6 +3363,7 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d1d42a9b3f3ec46ba828e8d376aec14592ea199f70a06a548587ecd1c4ab658"
dependencies = [
"async-compression",
"bitflags",
"bytes",
"futures-core",
@ -3106,8 +3372,11 @@ dependencies = [
"http-body",
"http-range-header",
"pin-project-lite",
"tokio",
"tokio-util",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
@ -3167,6 +3436,12 @@ version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
[[package]]
name = "ucd-trie"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81"
[[package]]
name = "unicase"
version = "2.6.0"
@ -3636,6 +3911,15 @@ dependencies = [
"lzma-sys",
]
[[package]]
name = "yaml-rust"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
dependencies = [
"linked-hash-map",
]
[[package]]
name = "yansi"
version = "0.5.1"

2
rest-http/.gitignore vendored 100644
View File

@ -0,0 +1,2 @@
.env
config/local.json

View File

@ -16,5 +16,29 @@ name = "mpot-http"
path = "src/main.rs"
[dependencies]
async-trait = "0.1"
axum = { version = "0.6", features = ["headers"] }
#jsonwebtoken = "8.3"
bcrypt = "0.14"
config = "0.13"
http = "0.2"
lazy_static = "1.4"
log = "0.4"
mailpot = { version = "^0.1", path = "../core" }
tokio = { version = "^1", features = ["full"] }
mailpot-web = { version = "^0.1", path = "../web" }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
stderrlog = "^0.5"
thiserror = "1"
tokio = { version = "1", features = ["full"] }
tower-http = { version = "0.4", features = [
"trace",
"compression-br",
"propagate-header",
"sensitive-headers",
"cors",
] }
[dev-dependencies]
assert-json-diff = "2"
reqwest = { version = "0.11", features = ["json"] }

View File

@ -1,3 +1,2 @@
# mailpot REST http server
Not implemented.

View File

@ -0,0 +1,12 @@
{
"environment": "development",
"server": {
"port": 8080
},
"auth": {
"secret": "secret"
},
"logger": {
"level": "debug"
}
}

View File

@ -0,0 +1,6 @@
{
"environment": "production",
"logger": {
"level": "info"
}
}

View File

@ -0,0 +1,9 @@
{
"environment": "test",
"server": {
"port": 8088
},
"logger": {
"level": "error"
}
}

View File

@ -0,0 +1,98 @@
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
Json,
};
use bcrypt::BcryptError;
use serde_json::json;
use tokio::task::JoinError;
#[derive(thiserror::Error, Debug)]
#[error("...")]
pub enum Error {
#[error("Error parsing ObjectID {0}")]
ParseObjectID(String),
#[error("{0}")]
Authenticate(#[from] AuthenticateError),
#[error("{0}")]
BadRequest(#[from] BadRequest),
#[error("{0}")]
NotFound(#[from] NotFound),
#[error("{0}")]
RunSyncTask(#[from] JoinError),
#[error("{0}")]
HashPassword(#[from] BcryptError),
#[error("{0}")]
System(#[from] mailpot::Error),
}
impl Error {
fn get_codes(&self) -> (StatusCode, u16) {
match *self {
// 4XX Errors
Error::ParseObjectID(_) => (StatusCode::BAD_REQUEST, 40001),
Error::BadRequest(_) => (StatusCode::BAD_REQUEST, 40002),
Error::NotFound(_) => (StatusCode::NOT_FOUND, 40003),
Error::Authenticate(AuthenticateError::WrongCredentials) => {
(StatusCode::UNAUTHORIZED, 40004)
}
Error::Authenticate(AuthenticateError::InvalidToken) => {
(StatusCode::UNAUTHORIZED, 40005)
}
Error::Authenticate(AuthenticateError::Locked) => (StatusCode::LOCKED, 40006),
// 5XX Errors
Error::Authenticate(AuthenticateError::TokenCreation) => {
(StatusCode::INTERNAL_SERVER_ERROR, 5001)
}
Error::RunSyncTask(_) => (StatusCode::INTERNAL_SERVER_ERROR, 5005),
Error::HashPassword(_) => (StatusCode::INTERNAL_SERVER_ERROR, 5006),
Error::System(_) => (StatusCode::INTERNAL_SERVER_ERROR, 5007),
}
}
pub fn bad_request() -> Self {
Error::BadRequest(BadRequest {})
}
pub fn not_found() -> Self {
Error::NotFound(NotFound {})
}
}
impl IntoResponse for Error {
fn into_response(self) -> Response {
let (status_code, code) = self.get_codes();
let message = self.to_string();
let body = Json(json!({ "code": code, "message": message }));
(status_code, body).into_response()
}
}
#[derive(thiserror::Error, Debug)]
#[error("...")]
pub enum AuthenticateError {
#[error("Wrong authentication credentials")]
WrongCredentials,
#[error("Failed to create authentication token")]
TokenCreation,
#[error("Invalid authentication credentials")]
InvalidToken,
#[error("User is locked")]
Locked,
}
#[derive(thiserror::Error, Debug)]
#[error("Bad Request")]
pub struct BadRequest {}
#[derive(thiserror::Error, Debug)]
#[error("Not found")]
pub struct NotFound {}

View File

@ -16,3 +16,13 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
pub use std::{net::SocketAddr, sync::Arc};
pub use axum::Router;
pub use http::header;
pub use log::{debug, info, trace};
pub use mailpot::{models::*, Configuration, Connection};
pub mod errors;
pub mod routes;
pub mod settings;

View File

@ -1,20 +1,50 @@
/*
* This file is part of mailpot
*
* Copyright 2020 - Manos Pitsidianakis
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use mailpot_http::{settings::SETTINGS, *};
use tower_http::{
compression::CompressionLayer, cors::CorsLayer, propagate_header::PropagateHeaderLayer,
sensitive_headers::SetSensitiveHeadersLayer,
};
fn main() {}
use crate::routes;
#[tokio::main]
async fn main() {
let app = create_app().await;
let port = SETTINGS.server.port;
let address = SocketAddr::from(([127, 0, 0, 1], port));
info!("Server listening on {}", &address);
axum::Server::bind(&address)
.serve(app.into_make_service())
.await
.expect("Failed to start server");
}
pub async fn create_app() -> Router {
let config_path = std::env::args()
.nth(1)
.expect("Expected configuration file path as first argument.");
stderrlog::new()
.quiet(false)
.verbosity(15)
.show_module_names(true)
.timestamp(stderrlog::Timestamp::Millisecond)
.init()
.unwrap();
let conf = Arc::new(Configuration::from_file(config_path).unwrap());
Router::new()
.with_state(conf.clone())
.merge(Router::new().nest("/v1", Router::new().merge(routes::list::create_route(conf))))
.layer(SetSensitiveHeadersLayer::new(std::iter::once(
header::AUTHORIZATION,
)))
// Compress responses
.layer(CompressionLayer::new())
// Propagate `X-Request-Id`s from requests to responses
.layer(PropagateHeaderLayer::new(header::HeaderName::from_static(
"x-request-id",
)))
// CORS configuration. This should probably be more restrictive in
// production.
.layer(CorsLayer::permissive())
}

View File

@ -0,0 +1,107 @@
use std::sync::Arc;
pub use axum::extract::{Path, Query, State};
use axum::{
http::StatusCode,
routing::{get, post},
Json, Router,
};
use mailpot_web::{typed_paths::*, ResponseError, RouterExt};
use serde::{Deserialize, Serialize};
use crate::*;
pub fn create_route(conf: Arc<Configuration>) -> Router {
Router::new()
.route("/list/", get(all_lists))
.route("/list/", post(post_list))
.typed_get(get_list)
.with_state(conf)
}
async fn get_list(
ListPath(id): ListPath,
State(state): State<Arc<Configuration>>,
) -> Result<Json<DbVal<MailingList>>, ResponseError> {
let db = Connection::open_db(Configuration::clone(&state))?;
let Some(list) = (match id {
ListPathIdentifier::Pk(id) => db.list(id)?,
ListPathIdentifier::Id(id) => db.list_by_id(id)?,
}) else {
return Err(mailpot_web::ResponseError::new(
"Not found".to_string(),
StatusCode::NOT_FOUND,
));
};
Ok(Json(list))
}
async fn all_lists(
Query(GetRequest {
filter: _,
count,
page,
}): Query<GetRequest>,
State(state): State<Arc<Configuration>>,
) -> Result<Json<GetResponse>, ResponseError> {
let db = Connection::open_db(Configuration::clone(&state))?;
let lists_values = db.lists()?;
let page = page.unwrap_or(0);
let Some(count) = count else {
let mut stmt = db
.connection
.prepare("SELECT count(*) FROM list;")?;
return Ok(Json(GetResponse {
entries: vec![],
total: stmt.query_row([], |row| {
let count: usize = row.get(0)?;
Ok(count)
})?,
start: 0,
}));
};
let offset = page * count;
let res: Vec<_> = lists_values.into_iter().skip(offset).take(count).collect();
Ok(Json(GetResponse {
total: res.len(),
start: offset,
entries: res,
}))
}
async fn post_list(
State(state): State<Arc<Configuration>>,
Json(_body): Json<GetRequest>,
) -> Result<Json<()>, ResponseError> {
let _db = Connection::open_db(Configuration::clone(&state))?;
// let password_hash = list::hash_password(body.password).await?;
// let list = list::new(body.name, body.email, password_hash);
// let list = list::create(list).await?;
// let res = Publiclist::from(list);
//
Ok(Json(()))
}
#[derive(Debug, Serialize, Deserialize)]
enum GetFilter {
Pk(i64),
Address(String),
Id(String),
Name(String),
}
#[derive(Debug, Serialize, Deserialize)]
struct GetRequest {
filter: Option<GetFilter>,
count: Option<usize>,
page: Option<usize>,
}
#[derive(Debug, Serialize, Deserialize)]
struct GetResponse {
entries: Vec<DbVal<MailingList>>,
total: usize,
start: usize,
}

View File

@ -0,0 +1 @@
pub mod list;

View File

@ -0,0 +1,61 @@
use std::{env, fmt};
use config::{Config, ConfigError, Environment, File};
use lazy_static::lazy_static;
use serde::Deserialize;
lazy_static! {
pub static ref SETTINGS: Settings = Settings::new().expect("Failed to setup settings");
}
#[derive(Debug, Clone, Deserialize)]
pub struct Server {
pub port: u16,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Logger {
pub level: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Auth {
pub secret: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Settings {
pub environment: String,
pub server: Server,
pub logger: Logger,
pub auth: Auth,
}
impl Settings {
pub fn new() -> Result<Self, ConfigError> {
let run_mode = env::var("RUN_MODE").unwrap_or_else(|_| "development".into());
let mut builder = Config::builder()
.add_source(File::with_name("config/default"))
.add_source(File::with_name(&format!("config/{run_mode}")).required(false))
.add_source(File::with_name("config/local").required(false))
.add_source(Environment::default().separator("__"));
// Some cloud services like Heroku exposes a randomly assigned port in
// the PORT env var and there is no way to change the env var name.
if let Ok(port) = env::var("PORT") {
builder = builder.set_override("server.port", port)?;
}
builder
.build()?
// Deserialize (and thus freeze) the entire configuration.
.try_deserialize()
}
}
impl fmt::Display for Server {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "http://localhost:{}", &self.port)
}
}