diff --git a/openlineplanner-backend/Cargo.lock b/openlineplanner-backend/Cargo.lock index c2c7214..60b414d 100644 --- a/openlineplanner-backend/Cargo.lock +++ b/openlineplanner-backend/Cargo.lock @@ -235,6 +235,21 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "actix-web-httpauth" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dda62cf04bc3a9ad2ea8f314f721951cfdb4cdacec4e984d20e77c7bb170991" +dependencies = [ + "actix-utils", + "actix-web", + "base64 0.13.1", + "futures-core", + "futures-util", + "log", + "pin-project-lite", +] + [[package]] name = "adler" version = "1.0.2" @@ -669,6 +684,16 @@ dependencies = [ "url", ] +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.4" @@ -903,6 +928,15 @@ dependencies = [ "libc", ] +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + [[package]] name = "fern" version = "0.6.2" @@ -979,6 +1013,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.1.0" @@ -1405,6 +1454,19 @@ dependencies = [ "tokio-rustls", ] +[[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" @@ -1466,6 +1528,15 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + [[package]] name = "io-lifetimes" version = "1.0.10" @@ -1545,6 +1616,23 @@ dependencies = [ "serde", ] +[[package]] +name = "jwtk" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6833c8be7e70530a018d6c48ef2a338b4d9df198ddb9d4ec0da436820a094526" +dependencies = [ + "base64 0.13.1", + "foreign-types", + "openssl", + "openssl-sys", + "reqwest", + "serde", + "serde_json", + "smallvec", + "tokio", +] + [[package]] name = "kissunits" version = "2.0.0" @@ -1752,6 +1840,24 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "nom" version = "7.1.3" @@ -1900,16 +2006,20 @@ dependencies = [ "acc_reader", "actix-cors", "actix-web", + "actix-web-httpauth", "anyhow", "bytes", "config", "datatypes", "fern", + "futures-util", "geo 0.23.1", "geojson 0.24.1", "json", + "jwtk", "log", "openhousepopulator 0.2.3", + "openssl-sys", "osmgraphing", "osmpbfreader 0.16.0", "petgraph", @@ -1924,6 +2034,60 @@ dependencies = [ "uuid", ] +[[package]] +name = "openssl" +version = "0.10.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.15", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-src" +version = "111.26.0+1.1.1u" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efc62c9f12b22b8f5208c23a7200a442b2e5999f8bdf80233852122b5a4f6f37" +dependencies = [ + "cc", +] + +[[package]] +name = "openssl-sys" +version = "0.9.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6" +dependencies = [ + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", +] + [[package]] name = "ordered-multimap" version = "0.4.3" @@ -2021,7 +2185,7 @@ checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.2.16", "smallvec", "windows-sys 0.45.0", ] @@ -2395,6 +2559,15 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags", +] + [[package]] name = "regex" version = "1.7.3" @@ -2451,10 +2624,12 @@ dependencies = [ "http-body", "hyper", "hyper-rustls", + "hyper-tls", "ipnet", "js-sys", "log", "mime", + "native-tls", "once_cell", "percent-encoding", "pin-project-lite", @@ -2464,6 +2639,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "tokio", + "tokio-native-tls", "tokio-rustls", "tower-service", "url", @@ -2597,6 +2773,15 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "schannel" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3" +dependencies = [ + "windows-sys 0.42.0", +] + [[package]] name = "scopeguard" version = "1.1.0" @@ -2619,6 +2804,29 @@ dependencies = [ "untrusted", ] +[[package]] +name = "security-framework" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc758eb7bffce5b308734e9b0c1468893cae9ff70ebf13e7090be8dcbcc83a8" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f51d0c0d83bec45f16480d0ce0058397a69e48fcdc52d1dc8855fb68acbd31a7" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "self_cell" version = "0.10.2" @@ -2859,6 +3067,20 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6" +dependencies = [ + "autocfg", + "cfg-if", + "fastrand", + "redox_syscall 0.3.5", + "rustix", + "windows-sys 0.48.0", +] + [[package]] name = "termcolor" version = "1.2.0" @@ -2978,6 +3200,16 @@ dependencies = [ "windows-sys 0.45.0", ] +[[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-rustls" version = "0.23.4" @@ -3118,6 +3350,12 @@ dependencies = [ "sha1_smol", ] +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "vec_map" version = "0.8.2" @@ -3303,6 +3541,21 @@ dependencies = [ "windows-targets 0.48.0", ] +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-sys" version = "0.45.0" diff --git a/openlineplanner-backend/Cargo.toml b/openlineplanner-backend/Cargo.toml index b6f948b..0733a58 100644 --- a/openlineplanner-backend/Cargo.toml +++ b/openlineplanner-backend/Cargo.toml @@ -31,3 +31,7 @@ predicates = "3.0.3" postcard = { version = "1.0.4", features = ["alloc"] } rayon = "1.7.0" datatypes = { path = "../datatypes" } +actix-web-httpauth = "0.8.0" +futures-util = "0.3.28" +jwtk = "0.2.4" +openssl-sys = { version = "0.9.65", features = ["vendored"] } \ No newline at end of file diff --git a/openlineplanner-backend/src/coverage.rs b/openlineplanner-backend/src/calculation/coverage.rs similarity index 80% rename from openlineplanner-backend/src/coverage.rs rename to openlineplanner-backend/src/calculation/coverage.rs index a6c6297..da27aa1 100644 --- a/openlineplanner-backend/src/coverage.rs +++ b/openlineplanner-backend/src/calculation/coverage.rs @@ -1,28 +1,20 @@ +use std::collections::HashMap; + use actix_web::body::BoxBody; use actix_web::http::header::ContentType; -use actix_web::web; -use actix_web::HttpResponse; -use actix_web::Responder; +use actix_web::{HttpResponse, Responder}; +use datatypes::Streets; use geo::Point; use geojson::de::deserialize_geometry; -use geojson::ser::serialize_geometry; -use geojson::ser::to_feature_collection_string; -use serde::Deserialize; -use serde::Serialize; +use geojson::ser::{serialize_geometry, to_feature_collection_string}; use rayon::prelude::*; -use datatypes::Streets; +use serde::{Deserialize, Serialize}; -use crate::error::OLPError; -use crate::geometry::DistanceCalculator; -use crate::geometry::DistanceFromPoint; -use crate::geometry::HaversineDistanceCalculator; -use crate::geometry::OsmDistanceCalculator; -use crate::layers::Layers; +use super::geometry::{ + DistanceCalculator, DistanceFromPoint, HaversineDistanceCalculator, OsmDistanceCalculator, +}; +use super::Station; use crate::layers::PopulatedCentroid; -use crate::Station; - -use std::collections::HashMap; -use std::sync::RwLock; #[derive(Serialize)] pub struct CoverageMap<'a, 'b>(pub HashMap<&'a str, StationCoverageInfo<'b>>); @@ -80,21 +72,28 @@ pub fn get_houses_in_coverage<'a, D: DistanceCalculator + Sync>( distance_calculator: D, possible_collision_stations: &[&Station], ) -> Vec> { - let distance_from_origin = distance_calculator.fix_point(origin); + let Some(distance_from_origin) = distance_calculator.fix_point(origin) else { + return Vec::new() + }; houses .par_iter() .filter_map(|house| { let distance = distance_from_origin.distance(house); if distance < coverage { - Some(PopulatedCentroidInfo{centroid: house, distance}) + Some(PopulatedCentroidInfo { + centroid: house, + distance, + }) } else { None } }) // PopulatedCentroid is in the radius of our station .filter(|hi| { possible_collision_stations.iter().all(|other| { - distance_calculator.distance(hi.centroid, &other.location) > other.coverage() // PopulatedCentroid is not in the coverage area of the other station or - || distance_calculator.distance(hi.centroid, &origin) < distance_calculator.distance(hi.centroid, &other.location) + // PopulatedCentroid is not in the coverage area of the other station or + distance_calculator.distance(hi.centroid, &other.location) > other.coverage() + || distance_calculator.distance(hi.centroid, &origin) + < distance_calculator.distance(hi.centroid, &other.location) // PopulatedCentroid is closer to the current station }) }) @@ -190,19 +189,3 @@ pub struct PopulatedCentroidCoverage { distance: f64, closest_station: String, } - -pub async fn coverage_info( - stations: web::Json>, - routing: web::Path, - layers: web::Data>, -) -> Result { - let layer = layers.read().map_err(OLPError::from_error)?.all_merged(); - let coverage_info = houses_for_stations( - &stations, - layer.get_centroids(), - &Method::Absolute, - &routing, - layer.get_streets(), - ); - Ok(PopulatedCentroidCoverageLayer::from(coverage_info)) -} diff --git a/openlineplanner-backend/src/geometry.rs b/openlineplanner-backend/src/calculation/geometry.rs similarity index 85% rename from openlineplanner-backend/src/geometry.rs rename to openlineplanner-backend/src/calculation/geometry.rs index c6af4f1..4c5205b 100644 --- a/openlineplanner-backend/src/geometry.rs +++ b/openlineplanner-backend/src/calculation/geometry.rs @@ -1,13 +1,14 @@ +use std::collections::HashMap; + +use datatypes::Streets; use geo::{ CoordFloat, HaversineDistance, HaversineLength, Line, LineInterpolatePoint, LineString, Point, }; -use std::collections::HashMap; -use datatypes::Streets; - -use crate::layers::PopulatedCentroid; use osmpbfreader::NodeId; use petgraph::algo::dijkstra; +use crate::layers::PopulatedCentroid; + pub trait DensifyHaversine { type Output; @@ -58,7 +59,7 @@ where pub trait DistanceCalculator { type FixedPoint: DistanceFromPoint + Sync; fn distance(&self, a: &PopulatedCentroid, b: &Point) -> f64; - fn fix_point(&self, point: &Point) -> Self::FixedPoint; + fn fix_point(&self, point: &Point) -> Option; } pub trait DistanceFromPoint { @@ -83,10 +84,10 @@ impl DistanceCalculator for HaversineDistanceCalculator { fn distance(&self, a: &PopulatedCentroid, b: &Point) -> f64 { a.haversine_distance(b) } - fn fix_point(&self, point: &Point) -> Self::FixedPoint { - HaversineFixedPoint { + fn fix_point(&self, point: &Point) -> Option { + Some(HaversineFixedPoint { point: point.clone(), - } + }) } } @@ -118,20 +119,24 @@ impl<'a> DistanceCalculator for OsmDistanceCalculator<'a> { type FixedPoint = OsmFixedPoint; fn distance(&self, a: &PopulatedCentroid, b: &Point) -> f64 { - let (origin_node, diff_distance) = self.find_closest_node_to_point(b); + let Some((origin_node, diff_distance)) = self.find_closest_node_to_point(b) else { + return f64::MAX + }; let osm_distance_matrix = dijkstra(&self.streets.streetgraph, origin_node, None, |e| *e.2); osm_distance_matrix .get(&a.street_graph_id.unwrap()) .unwrap_or(&f64::MAX) + diff_distance } - fn fix_point(&self, point: &Point) -> Self::FixedPoint { - let (origin_node, diff_distance) = self.find_closest_node_to_point(point); + fn fix_point(&self, point: &Point) -> Option { + let Some((origin_node, diff_distance)) = self.find_closest_node_to_point(point) else { + return None + }; let distance_matrix = dijkstra(&self.streets.streetgraph, origin_node, None, |e| *e.2); - OsmFixedPoint { + Some(OsmFixedPoint { diff_distance, distance_matrix, - } + }) } } @@ -140,12 +145,11 @@ impl<'a> OsmDistanceCalculator<'a> { Self { streets } } - fn find_closest_node_to_point(&self, origin: &Point) -> (NodeId, f64) { + fn find_closest_node_to_point(&self, origin: &Point) -> Option<(NodeId, f64)> { self.streets .nodes .iter() .min_by_key(|(_, node)| node.haversine_distance(&origin) as u32) .map(|(id, node)| (id.clone(), node.haversine_distance(&origin))) - .unwrap() } } diff --git a/openlineplanner-backend/src/calculation/mod.rs b/openlineplanner-backend/src/calculation/mod.rs new file mode 100644 index 0000000..96e80cf --- /dev/null +++ b/openlineplanner-backend/src/calculation/mod.rs @@ -0,0 +1,101 @@ +use std::sync::{Arc, RwLock}; + +use actix_web::{web, Scope}; +use anyhow::Result; +use coverage::{CoverageMap, Method, Routing}; +use geo::Point; +use population::InhabitantsMap; +use serde::Deserialize; +use station::{OptimalStationResult, Station}; + +use self::coverage::{houses_for_stations, PopulatedCentroidCoverageLayer}; +use crate::error::OLPError; +use crate::layers::{LayerType, Layers}; + +mod coverage; +mod geometry; +mod population; +mod station; + +pub fn calculation() -> Scope { + web::scope("/calculate") + .route("/station-info", web::post().to(station_info)) + .route("/coverage-info/{router}", web::post().to(coverage_info)) + .route("/find-station", web::post().to(find_station)) +} + +#[derive(Deserialize)] +pub struct StationInfoRequest { + stations: Vec, + _separation_distance: Option, + method: Option, + routing: Option, +} + +pub async fn station_info( + request: web::Json, + layers: web::ReqData>>, +) -> Result { + let merged_layers = layers + .read() + .map_err(OLPError::from_error)? + .all_merged_by_type(); + let coverage_info: Vec<(LayerType, CoverageMap)> = merged_layers + .iter() + .map(|layer| { + log::debug!("calculating for layer type: {}", layer.get_type()); + ( + layer.get_type().clone(), + coverage::houses_for_stations( + &request.stations, + layer.get_centroids(), + &request.method.as_ref().unwrap_or(&Method::Relative), + &request.routing.as_ref().unwrap_or(&Routing::Osm), + layer.get_streets(), + ), + ) + }) + .collect(); + let coverage_slice: &[(LayerType, CoverageMap)] = &coverage_info; + Ok(population::InhabitantsMap::from(coverage_slice)) +} + +#[derive(Deserialize)] +pub struct FindStationRequest { + stations: Vec, + route: Vec, + method: Option, + routing: Option, +} + +pub async fn find_station( + request: web::Json, + layers: web::ReqData>>, +) -> Result { + let layer = layers.read().map_err(OLPError::from_error)?.all_merged(); + Ok(station::find_optimal_station( + request.route.clone(), + 300f64, + layer.get_centroids(), + &request.stations, + &request.method.as_ref().unwrap_or(&Method::Relative), + &request.routing.as_ref().unwrap_or(&Routing::Osm), + layer.get_streets(), + )) +} + +pub async fn coverage_info( + stations: web::Json>, + routing: web::Path, + layers: web::ReqData>>, +) -> Result { + let layer = layers.read().map_err(OLPError::from_error)?.all_merged(); + let coverage_info = houses_for_stations( + &stations, + layer.get_centroids(), + &Method::Absolute, + &routing, + layer.get_streets(), + ); + Ok(PopulatedCentroidCoverageLayer::from(coverage_info)) +} diff --git a/openlineplanner-backend/src/population.rs b/openlineplanner-backend/src/calculation/population.rs similarity index 93% rename from openlineplanner-backend/src/population.rs rename to openlineplanner-backend/src/calculation/population.rs index b448bc9..1f7f1b3 100644 --- a/openlineplanner-backend/src/population.rs +++ b/openlineplanner-backend/src/calculation/population.rs @@ -1,14 +1,13 @@ +use std::collections::HashMap; + use actix_web::body::BoxBody; use actix_web::http::header::ContentType; -use actix_web::HttpResponse; -use actix_web::Responder; +use actix_web::{HttpResponse, Responder}; use serde::Serialize; -use crate::coverage::CoverageMap; +use super::coverage::CoverageMap; use crate::layers::LayerType; -use std::collections::HashMap; - #[derive(Serialize)] pub struct InhabitantsInfo { layer_type: LayerType, diff --git a/openlineplanner-backend/src/station.rs b/openlineplanner-backend/src/calculation/station.rs similarity index 84% rename from openlineplanner-backend/src/station.rs rename to openlineplanner-backend/src/calculation/station.rs index 756c12c..53fc3e1 100644 --- a/openlineplanner-backend/src/station.rs +++ b/openlineplanner-backend/src/calculation/station.rs @@ -1,16 +1,18 @@ use std::borrow::Borrow; -use actix_web::{body::BoxBody, http::header::ContentType, HttpResponse, Responder}; +use actix_web::body::BoxBody; +use actix_web::http::header::ContentType; +use actix_web::{HttpResponse, Responder}; +use datatypes::Streets; use geo::{HaversineDistance, LineString, Point}; use serde::{Deserialize, Serialize}; -use datatypes::Streets; +use rayon::prelude::*; -use crate::{ - coverage::StationCoverageInfo, - coverage::{get_houses_in_coverage, houses_for_stations, Method, Routing}, - geometry::{DensifyHaversine, OsmDistanceCalculator}, - layers::PopulatedCentroid, +use super::coverage::{ + get_houses_in_coverage, houses_for_stations, Method, Routing, StationCoverageInfo, }; +use super::geometry::{DensifyHaversine, OsmDistanceCalculator}; +use crate::layers::PopulatedCentroid; static DEFAULT_COVERAGE: f64 = 300f64; @@ -45,18 +47,18 @@ pub fn find_optimal_station( let original_coverage: Vec<&PopulatedCentroid> = houses_for_stations(other_stations, houses, method, routing, streets) .0 - .values() - .into_iter() - .flat_map(|elem| elem.houses.clone()) + .into_par_iter() + .flat_map(|(_,elem)| elem.houses.clone()) .map(|elem| elem.centroid) .collect(); let leftover_houses: Vec = houses - .iter() + .par_iter() .filter(|house| !original_coverage.contains(house)) .cloned() .collect(); let location = linestring .points() + .par_bridge() .max_by_key(|point| { StationCoverageInfo::from_houses_with_method( get_houses_in_coverage( diff --git a/openlineplanner-backend/src/error.rs b/openlineplanner-backend/src/error.rs index 0c45e51..659ff74 100644 --- a/openlineplanner-backend/src/error.rs +++ b/openlineplanner-backend/src/error.rs @@ -1,6 +1,8 @@ -use std::{error::Error, fmt::Display}; +use std::error::Error; +use std::fmt::Display; -use actix_web::{body::BoxBody, HttpResponse, Responder, ResponseError}; +use actix_web::body::BoxBody; +use actix_web::{HttpResponse, Responder, ResponseError}; #[derive(Debug)] pub enum OLPError { diff --git a/openlineplanner-backend/src/layers/loading/admin_area.rs b/openlineplanner-backend/src/layers/areas/admin_area.rs similarity index 93% rename from openlineplanner-backend/src/layers/loading/admin_area.rs rename to openlineplanner-backend/src/layers/areas/admin_area.rs index d48a2c3..d56aa04 100644 --- a/openlineplanner-backend/src/layers/loading/admin_area.rs +++ b/openlineplanner-backend/src/layers/areas/admin_area.rs @@ -1,16 +1,15 @@ -use actix_web::{body::BoxBody, http::header::ContentType, HttpResponse, Responder}; +use actix_web::body::BoxBody; +use actix_web::http::header::ContentType; +use actix_web::{HttpResponse, Responder}; use geo::{Point, Polygon}; -use geojson::{ - feature::Id, - ser::{serialize_geometry, to_feature_collection_string}, - Feature, GeoJson, -}; +use geojson::feature::Id; +use geojson::ser::{serialize_geometry, to_feature_collection_string}; +use geojson::{Feature, GeoJson}; use serde::Serialize; use tinytemplate::TinyTemplate; -use crate::error::OLPError; - use super::overpass::query_overpass; +use crate::error::OLPError; #[derive(Serialize)] pub struct AdminArea { diff --git a/openlineplanner-backend/src/layers/loading/mod.rs b/openlineplanner-backend/src/layers/areas/mod.rs similarity index 99% rename from openlineplanner-backend/src/layers/loading/mod.rs rename to openlineplanner-backend/src/layers/areas/mod.rs index ef1edf1..e65eda2 100644 --- a/openlineplanner-backend/src/layers/loading/mod.rs +++ b/openlineplanner-backend/src/layers/areas/mod.rs @@ -4,11 +4,9 @@ mod overpass; use actix_web::{web, Responder, Scope}; use geo::Point; -use crate::error::OLPError; - use self::admin_area::find_admin_boundaries_for_point; - pub use self::admin_area::AdminArea; +use crate::error::OLPError; /// Defining /osm endpoint for arcix-web router pub fn osm() -> Scope { diff --git a/openlineplanner-backend/src/layers/loading/overpass.rs b/openlineplanner-backend/src/layers/areas/overpass.rs similarity index 94% rename from openlineplanner-backend/src/layers/loading/overpass.rs rename to openlineplanner-backend/src/layers/areas/overpass.rs index ddbe6ee..68de213 100644 --- a/openlineplanner-backend/src/layers/loading/overpass.rs +++ b/openlineplanner-backend/src/layers/areas/overpass.rs @@ -1,7 +1,5 @@ -use std::{ - io::Write, - process::{Command, Stdio}, -}; +use std::io::Write; +use std::process::{Command, Stdio}; use anyhow::Result; use geojson::GeoJson; diff --git a/openlineplanner-backend/src/layers/mod.rs b/openlineplanner-backend/src/layers/mod.rs index 16d6aa4..5659816 100644 --- a/openlineplanner-backend/src/layers/mod.rs +++ b/openlineplanner-backend/src/layers/mod.rs @@ -1,45 +1,38 @@ -use std::{ - collections::HashMap, - fmt::Display, - fs, - path::PathBuf, - sync::RwLock, -}; - -use actix_web::{ - body::BoxBody, - http::header::ContentType, - web::{self, Data, Json}, - HttpResponse, Responder, Scope, -}; - +use std::collections::HashMap; +use std::fmt::Display; +use std::fs; +use std::path::PathBuf; +use std::sync::{Arc, RwLock}; + +use actix_web::body::BoxBody; +use actix_web::http::header::ContentType; +use actix_web::web::{self, Data, Json}; +use actix_web::{HttpRequest, HttpResponse, Responder, Scope}; use config::Config; +use datatypes::Streets; use geo::{ point, BooleanOps, Centroid, HaversineDistance, LineString, MultiPolygon, Point, Polygon, }; -use geojson::{ - de::deserialize_geometry, - ser::{serialize_geometry, to_feature_collection_string}, - Feature, -}; +use geojson::de::deserialize_geometry; +use geojson::ser::{serialize_geometry, to_feature_collection_string}; +use geojson::Feature; +use jwtk::jwk::JwkSetVerifier; use osmpbfreader::NodeId; use petgraph::prelude::*; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; -use datatypes::Streets; -mod loading; +mod areas; mod merge; -pub use loading::osm; +pub use areas::osm; pub use merge::*; +use openhousepopulator::{Building, GenericGeometry}; use uuid::Uuid; -use self::{loading::AdminArea}; -use crate::{ - error::OLPError, - persistence::{self, save_layers}, -}; -use openhousepopulator::{Building, GenericGeometry}; +use self::areas::AdminArea; +use crate::error::OLPError; +use crate::middleware::auth::Claims; +use crate::persistence::{self, save_layers}; pub fn layers() -> Scope { web::scope("/layer") @@ -53,10 +46,13 @@ pub fn layers() -> Scope { ) .route("/{layer_id}", web::get().to(get_layer)) .route("/{layer_id}", web::delete().to(delete_layer)) + .route("/transfer", web::post().to(transfer_layers)) .route("", web::get().to(summarize_layers)) } -pub async fn find_center(layers: web::Data>) -> Result>, OLPError> { +pub async fn find_center( + layers: web::ReqData>>, +) -> Result>, OLPError> { let point = layers .read() .map_err(OLPError::from_error)? @@ -68,7 +64,7 @@ pub async fn find_center(layers: web::Data>) -> Result>, + layers: web::ReqData>>, ) -> Result>, OLPError> { let layer_summary = layers .read() @@ -113,6 +109,33 @@ impl PopulatedCentroid { } } +#[derive(Debug, Clone, Default)] +pub struct LayersContainer(Arc>>>>); + +impl LayersContainer { + pub fn new() -> Self { + Self(Arc::new(RwLock::new(HashMap::new()))) + } + + pub fn get_or_empty(&self, user_id: &Uuid) -> Arc> { + self.0 + .write() + .unwrap() + .entry(user_id.clone()) + .or_insert(Arc::new(RwLock::new(Layers::new()))) + .clone() + } + + // Todo: Currently overwrites a user session if one already exists + pub fn transfer_layers(&self, from_user_id: Uuid, to_user_id: Uuid) -> Result<(), OLPError> { + let mut map = self.0.write().map_err(OLPError::from_error)?; + let entry = map + .remove(&from_user_id) + .ok_or(OLPError::GenericError("session does not exist".to_string()))?; + map.insert(to_user_id.clone(), entry); + Ok(()) + } +} #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct Layers(HashMap); @@ -167,7 +190,9 @@ impl Layers { .and_modify(|elem| { elem.centroids.append(&mut layer.centroids.clone()); elem.bbox.union(&layer.bbox); - elem.streets.streetgraph.extend(layer.streets.streetgraph.all_edges()); + elem.streets + .streetgraph + .extend(layer.streets.streetgraph.all_edges()); elem.streets.nodes.extend(layer.streets.nodes.iter()); }) .or_insert(layer.clone()); @@ -341,7 +366,7 @@ impl CalculateLayerRequest { async fn calculate_new_layer( request: web::Json, - layers: web::Data>, + layers: web::ReqData>>, config: web::Data, ) -> Result, OLPError> { let request = request.into_inner(); @@ -352,11 +377,9 @@ async fn calculate_new_layer( let new_layer_id = uuid::Uuid::new_v5(&uuid::Uuid::NAMESPACE_URL, &admin_area.id.to_le_bytes()); - if layers - .read() - .map_err(OLPError::from_error)? - .contains_key(&new_layer_id) - { + let mut layers = layers.write().map_err(OLPError::from_error)?; + + if layers.contains_key(&new_layer_id) { log::info!("layer {} is already calculated, reusing", new_layer_id); return Ok(Json(new_layer_id)); } @@ -396,7 +419,7 @@ async fn calculate_new_layer( centroid.street_graph_id = closest_street_node; } - layers.write().map_err(OLPError::from_error)?.push(Layer { + layers.push(Layer { id: new_layer_id.clone(), bbox: MultiPolygon::new(vec![admin_area.geometry]), streets: data.streets, @@ -409,9 +432,7 @@ async fn calculate_new_layer( match fs::create_dir_all(&path) { Ok(_) => { path.push("layers"); - if let Err(e) = - save_layers(layers.read().as_ref().map_err(OLPError::from_error)?, &path) - { + if let Err(e) = save_layers(&layers, &path) { log::error!("failed to save layers to cache: {}", e) } } @@ -423,7 +444,7 @@ async fn calculate_new_layer( async fn get_layer( id: web::Path, - layers: web::Data>, + layers: web::ReqData>>, ) -> Result, OLPError> { Ok(layers .read() @@ -435,10 +456,39 @@ async fn get_layer( async fn delete_layer( id: web::Path, - layers: web::Data>, + layers: web::ReqData>>, ) -> Result { match layers.write().map_err(OLPError::from_error)?.0.remove(&id) { Some(_) => Ok(HttpResponse::Ok().finish()), None => Ok(HttpResponse::NotFound().finish()), } } + +async fn transfer_layers( + token: String, + req: HttpRequest, + auth: web::ReqData, + jwks: web::Data, +) -> Result { + let verified_token: jwtk::Result> = jwks.verify(&token); + match verified_token { + Ok(token) => { + let Some(sub) = token.claims().sub.as_ref().and_then(|sub| Uuid::parse_str(sub).ok()) else { + return Err(OLPError::GenericError( + "failed to validate original token".to_string(), + )) + }; + let layers_container = req + .app_data::() + .ok_or(OLPError::GenericError("failed to get app data".to_string()))?; + layers_container.transfer_layers(sub, auth.sub)?; + Ok(HttpResponse::Ok().finish()) + } + Err(e) => { + log::error!("Failed to validate token: {}", e); + Err(OLPError::GenericError( + "failed to validate original token".to_string(), + )) + } + } +} diff --git a/openlineplanner-backend/src/main.rs b/openlineplanner-backend/src/main.rs index c69e9a1..d2849d4 100644 --- a/openlineplanner-backend/src/main.rs +++ b/openlineplanner-backend/src/main.rs @@ -1,87 +1,21 @@ -use std::path::PathBuf; -use std::sync::RwLock; - use actix_cors::Cors; +use actix_web::middleware::Logger; use actix_web::{web, App, HttpServer}; +use actix_web_httpauth::middleware::HttpAuthentication; use anyhow::Result; use config::Config; -use error::OLPError; -use geo::Point; use log::info; -use population::InhabitantsMap; -use serde::Deserialize; -mod coverage; +mod calculation; mod error; -mod geometry; mod layers; +mod middleware; mod persistence; -mod population; -mod station; - -use coverage::{CoverageMap, Method, Routing}; -use layers::{LayerType, Layers}; -use station::{OptimalStationResult, Station}; +mod users; -#[derive(Deserialize)] -struct StationInfoRequest { - stations: Vec, - _separation_distance: Option, - method: Option, - routing: Option, -} - -#[derive(Deserialize)] -struct FindStationRequest { - stations: Vec, - route: Vec, - method: Option, - routing: Option, -} - -async fn station_info( - request: web::Json, - layers: web::Data>, -) -> Result { - let merged_layers = layers - .read() - .map_err(OLPError::from_error)? - .all_merged_by_type(); - let coverage_info: Vec<(LayerType, CoverageMap)> = merged_layers - .iter() - .map(|layer| { - log::debug!("calculating for layer type: {}", layer.get_type()); - ( - layer.get_type().clone(), - coverage::houses_for_stations( - &request.stations, - layer.get_centroids(), - &request.method.as_ref().unwrap_or(&Method::Relative), - &request.routing.as_ref().unwrap_or(&Routing::Osm), - layer.get_streets(), - ), - ) - }) - .collect(); - let coverage_slice: &[(LayerType, CoverageMap)] = &coverage_info; - Ok(population::InhabitantsMap::from(coverage_slice)) -} - -async fn find_station( - request: web::Json, - layers: web::Data>, -) -> Result { - let layer = layers.read().map_err(OLPError::from_error)?.all_merged(); - Ok(station::find_optimal_station( - request.route.clone(), - 300f64, - layer.get_centroids(), - &request.stations, - &request.method.as_ref().unwrap_or(&Method::Relative), - &request.routing.as_ref().unwrap_or(&Routing::Osm), - layer.get_streets(), - )) -} +use crate::layers::LayersContainer; +use crate::middleware::auth::get_jwks; +use crate::middleware::data::UserDataExtraction; async fn health() -> &'static str { "ok" @@ -93,38 +27,36 @@ async fn main() -> std::io::Result<()> { info!("starting openlineplanner backend"); - #[rustfmt::skip] - let config = Config::builder() - .set_default("cache.dir", "./cache/").unwrap() - .set_default("data.dir", "./data/").unwrap() - .add_source(config::File::with_name("Config.toml").required(false)) - .build() - .unwrap(); - - let layers = load_layers(&config); - let config = web::Data::new(config); + let config = web::Data::new(build_config()); + let layers = LayersContainer::new(); + let jwks = get_jwks(&config); + let jwks_dex = web::Data::new(jwks.0); + let jwks_local = web::Data::new(jwks.1); + let jwk_key = web::Data::new(jwks.2); log::info!("loading data done"); HttpServer::new(move || { - #[cfg(debug_assertions)] - let cors = Cors::permissive(); - #[cfg(not(debug_assertions))] - let cors = Cors::default(); + let cors = match cfg!(debug_assertions) { + true => Cors::permissive(), + false => Cors::default(), + }; + + let auth = HttpAuthentication::bearer(middleware::auth::validator); + let data = UserDataExtraction::default(); App::new() .wrap(cors) + .wrap(Logger::default().exclude("/health")) .app_data(layers.clone()) .app_data(config.clone()) - .route("/station-info", web::post().to(station_info)) - .route( - "/coverage-info/{router}", - web::post().to(coverage::coverage_info), - ) - .route("/find-station", web::post().to(find_station)) + .app_data(jwks_dex.clone()) + .app_data(jwks_local.clone()) .route("/health", web::get().to(health)) - .service(layers::layers()) - .service(layers::osm()) + .service(calculation::calculation().wrap(data).wrap(auth.clone())) + .service(layers::layers().wrap(data).wrap(auth.clone())) + .service(layers::osm().wrap(data).wrap(auth.clone())) + .service(users::users(jwk_key.clone())) }) .bind(("0.0.0.0", 8080))? .run() @@ -133,7 +65,6 @@ async fn main() -> std::io::Result<()> { fn setup_logger() -> Result<()> { let colors = fern::colors::ColoredLevelConfig::new(); - fern::Dispatch::new() .format(move |out, message, record| { out.finish(format_args!( @@ -149,9 +80,13 @@ fn setup_logger() -> Result<()> { Ok(()) } -fn load_layers(config: &Config) -> web::Data> { - let mut path = PathBuf::from(config.get_string("cache.dir").unwrap()); - path.push("layers"); - let layers = persistence::load_layers(&path).unwrap_or_default(); - web::Data::new(RwLock::new(layers)) +#[rustfmt::skip] +fn build_config() -> Config { + Config::builder() + .set_default("cache.dir", "./cache/").unwrap() + .set_default("data.dir", "./data/").unwrap() + .set_default("oidc.issuer", "https://dex.prod.k8s.xatellite.space").unwrap() + .add_source(config::File::with_name("Config.toml").required(false)) + .build() + .unwrap() } diff --git a/openlineplanner-backend/src/middleware/auth.rs b/openlineplanner-backend/src/middleware/auth.rs new file mode 100644 index 0000000..cebbd2d --- /dev/null +++ b/openlineplanner-backend/src/middleware/auth.rs @@ -0,0 +1,118 @@ +use std::fs; +use std::path::PathBuf; +use std::time::Duration; + +use actix_web::dev::ServiceRequest; +use actix_web::web::Data; +use actix_web::HttpMessage; +use actix_web_httpauth::extractors::bearer::{self, BearerAuth}; +use actix_web_httpauth::extractors::AuthenticationError; +use config::Config; +use jwtk::jwk::{Jwk, JwkSet, JwkSetVerifier, RemoteJwksVerifier}; +use jwtk::rsa::{RsaAlgorithm, RsaPrivateKey, RsaPublicKey}; +use jwtk::{PrivateKeyToJwk, PublicKeyToJwk}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +#[derive(Debug, Serialize, Deserialize, Clone)] +struct ExtraClaims { + pub name: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Claims { + pub sub: Uuid, + pub name: String, +} + +pub async fn validator( + req: ServiceRequest, + credentials: BearerAuth, +) -> Result { + let config = req + .app_data::() + .cloned() + .unwrap_or_default(); + let dex_jwks = req.app_data::>().unwrap(); + let local_jwks = req.app_data::>().unwrap(); + + let verified_token = dex_jwks + .verify(credentials.token()) + .await + .or_else(|_| local_jwks.verify(credentials.token())); + match verified_token { + Ok(token) => { + let Some(sub) = token.claims().sub.as_ref().and_then(|sub| Uuid::parse_str(sub).ok()) else { + return Err((AuthenticationError::from(config).into(), req)) + }; + let extra_claims: &ExtraClaims = &token.claims().extra; + + log::debug!( + "Validated token for user {} with id {:?}", + extra_claims.name, + sub + ); + let claims = Claims { + sub, + name: extra_claims.name.clone(), + }; + req.extensions_mut().insert(claims); + Ok(req) + } + Err(e) => { + log::error!("Failed to validate token: {}", e); + Err((AuthenticationError::from(config).into(), req)) + } + } +} + +pub fn get_jwks(config: &Config) -> (RemoteJwksVerifier, JwkSetVerifier, Jwk) { + let dex = RemoteJwksVerifier::new( + format!("{}/keys", config.get_string("oidc.issuer").unwrap()), + None, + Duration::from_secs(3600), + ); + + let mut local = JwkSet { keys: Vec::new() }; + let mut cache_path = PathBuf::from(config.get_string("cache.dir").unwrap()); + cache_path.push("keys"); + + fs::create_dir_all(&cache_path).unwrap(); + for file in fs::read_dir(&cache_path).unwrap() { + let file = file.unwrap(); + log::info!("reading jwt key {:?}", file.file_name()); + let key_data = fs::read(file.path()).unwrap(); + let mut key = RsaPublicKey::from_pem(&key_data, Some(RsaAlgorithm::RS256)) + .unwrap() + .public_key_to_jwk() + .unwrap(); + key.kid = Some( + file.path() + .file_stem() + .unwrap() + .to_string_lossy() + .to_string(), + ); + local.keys.push(key); + } + + let current_kid = Uuid::new_v4().to_string(); + let current_key = RsaPrivateKey::generate(2048, jwtk::rsa::RsaAlgorithm::RS256).unwrap(); + cache_path.push(¤t_kid); + cache_path = cache_path.with_extension("pem"); + fs::write( + cache_path, + current_key.public_key_to_pem().unwrap().as_bytes(), + ) + .unwrap(); + + let mut current_public = current_key.public_key_to_jwk().unwrap(); + current_public.kid = Some(current_kid.clone()); + let mut current_private = current_key.private_key_to_jwk().unwrap(); + current_private.kid = Some(current_kid); + + local.keys.push(current_public); + let local = local.verifier(); + + (dex, local, current_private) +} diff --git a/openlineplanner-backend/src/middleware/data.rs b/openlineplanner-backend/src/middleware/data.rs new file mode 100644 index 0000000..c7c78b0 --- /dev/null +++ b/openlineplanner-backend/src/middleware/data.rs @@ -0,0 +1,64 @@ +use std::future::{ready, Ready}; + +use actix_web::dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}; +use actix_web::{Error, HttpMessage}; +use futures_util::future::LocalBoxFuture; + +use super::auth; +use crate::layers::LayersContainer; + +#[derive(Default, Clone, Copy)] +pub struct UserDataExtraction; + +impl Transform for UserDataExtraction +where + S: Service, Error = Error>, + S::Future: 'static, + B: 'static, +{ + type Response = ServiceResponse; + type Error = Error; + type InitError = (); + type Transform = UserDataExtractionMiddleware; + type Future = Ready>; + + fn new_transform(&self, service: S) -> Self::Future { + ready(Ok(UserDataExtractionMiddleware { service })) + } +} + +pub struct UserDataExtractionMiddleware { + service: S, +} + +impl Service for UserDataExtractionMiddleware +where + S: Service, Error = Error>, + S::Future: 'static, + B: 'static, +{ + type Response = ServiceResponse; + type Error = Error; + type Future = LocalBoxFuture<'static, Result>; + + forward_ready!(service); + + fn call(&self, req: ServiceRequest) -> Self::Future { + { + let mut extensions = req.extensions_mut(); + let auth = extensions.get::().unwrap(); + let user_data = req + .app_data::() + .unwrap() + .get_or_empty(&auth.sub); + extensions.insert(user_data); + }; + + let fut = self.service.call(req); + + Box::pin(async move { + let res = fut.await?; + Ok(res) + }) + } +} diff --git a/openlineplanner-backend/src/middleware/mod.rs b/openlineplanner-backend/src/middleware/mod.rs new file mode 100644 index 0000000..9cd3092 --- /dev/null +++ b/openlineplanner-backend/src/middleware/mod.rs @@ -0,0 +1,2 @@ +pub mod auth; +pub mod data; diff --git a/openlineplanner-backend/src/persistence/mod.rs b/openlineplanner-backend/src/persistence/mod.rs index 8d1f12f..08f0fe8 100644 --- a/openlineplanner-backend/src/persistence/mod.rs +++ b/openlineplanner-backend/src/persistence/mod.rs @@ -1,18 +1,14 @@ -use serde::{Deserialize, Serialize}; -use std::{ - fs::File, - io::{Read, Write}, - path::Path, -}; -use datatypes::Streets; +use std::fs::File; +use std::io::{Read, Write}; +use std::path::Path; -use crate::{ - error::OLPError, - layers::Layers, -}; +use anyhow::Result; +use datatypes::Streets; use openhousepopulator::Buildings; +use serde::{Deserialize, Serialize}; -use anyhow::Result; +use crate::error::OLPError; +use crate::layers::Layers; #[derive(Serialize, Deserialize)] pub(crate) struct PreProcessingData { @@ -37,10 +33,3 @@ pub(crate) fn save_layers(layers: &Layers, path: &Path) -> Result<(), OLPError> ) .map_err(OLPError::from_error) } - -pub(crate) fn load_layers(path: &Path) -> Result { - let mut file = File::open(path).map_err(OLPError::from_error)?; - let mut data: Vec = Vec::new(); - file.read_to_end(&mut data).map_err(OLPError::from_error)?; - serde_json::from_slice(&data).map_err(OLPError::from_error) -} diff --git a/openlineplanner-backend/src/users/mod.rs b/openlineplanner-backend/src/users/mod.rs new file mode 100644 index 0000000..022bf37 --- /dev/null +++ b/openlineplanner-backend/src/users/mod.rs @@ -0,0 +1,30 @@ +use std::time::Duration; + +use actix_web::{web, Scope}; +use jwtk::{jwk::Jwk, HeaderAndClaims}; +use uuid::Uuid; + +use crate::error::OLPError; + +pub fn users(jwk_key: web::Data) -> Scope { + web::scope("/user") + .app_data(jwk_key) + .route("/anon", web::get().to(get_anonymous_user_token)) +} + +async fn get_anonymous_user_token(jwk_key: web::Data) -> Result { + let mut token = HeaderAndClaims::new_dynamic(); + token + .set_exp_from_now(Duration::from_secs(86400)) + .set_iss("openlineplanner") + .set_sub(Uuid::new_v4().to_string()) + .set_kid(jwk_key.kid.as_ref().cloned().unwrap_or_default()) + .insert("name", "Anonymous User"); + jwtk::sign( + &mut token, + &jwk_key + .to_signing_key(jwtk::rsa::RsaAlgorithm::RS256) + .unwrap(), + ) + .map_err(OLPError::from_error) +}