use actix_cors::Cors; use actix_files as fs; use actix_identity::{CookieIdentityPolicy, Identity, IdentityService, RequestIdentity}; use actix_service::{Service, Transform}; use actix_web::{ dev::{ServiceRequest, ServiceResponse}, http::{header, StatusCode}, middleware, web, App, Error, HttpResponse, HttpServer, }; use futures::{ future::{ok, Either, FutureResult}, Future, }; use serde_json; use std::env; use crate::api; use lootalot_db as db; type AppPool = web::Data; type PlayerId = web::Path; type ItemId = web::Json; type IdList = web::Json; type BuySellParams = web::Json; type NewGroupLoot = web::Json; type MaybeForbidden = actix_web::Either>, HttpResponse>; /// Wraps call to the database query and convert its result as a async HttpResponse fn db_call( pool: AppPool, query: api::ApiActions, ) -> impl Future { let conn = pool.get().unwrap(); web::block(move || api::execute(&conn, query)).then(|res| match res { Ok(r) => HttpResponse::Ok().json(r), Err(e) => { dbg!(&e); HttpResponse::InternalServerError().finish() } }) } fn restricted_to_group(id: i32, params: (AppPool, api::ApiActions)) -> MaybeForbidden { if id != 0 { actix_web::Either::B(HttpResponse::Forbidden().finish()) } else { actix_web::Either::A(Box::new(db_call(params.0, params.1))) } } struct RestrictedAccess; impl Transform for RestrictedAccess where S: Service, Error = Error>, S::Future: 'static, { type Request = ServiceRequest; type Response = ServiceResponse; type Error = Error; type InitError = (); type Transform = RestrictedAccessMiddleware; type Future = FutureResult; fn new_transform(&self, service: S) -> Self::Future { ok(RestrictedAccessMiddleware { service }) } } struct RestrictedAccessMiddleware { service: S, } impl Service for RestrictedAccessMiddleware where S: Service, Error = Error>, S::Future: 'static, { type Request = ServiceRequest; type Response = ServiceResponse; type Error = Error; type Future = Either>; fn poll_ready(&mut self) -> futures::Poll<(), Self::Error> { self.service.poll_ready() } fn call(&mut self, req: ServiceRequest) -> Self::Future { let is_logged_in = req.get_identity().is_some(); if is_logged_in { Either::A(self.service.call(req)) } else { Either::B(ok( req.into_response(HttpResponse::Forbidden().finish().into_body()) )) } } } fn configure_api(config: &mut web::ServiceConfig) { use api::ApiActions as Q; config.service( web::scope("/api") .wrap(RestrictedAccess) .service( web::scope("/players") .service( web::resource("/") .route(web::get().to_async(|pool| db_call(pool, Q::FetchPlayers))) .route(web::post().to_async( |pool, player: web::Json| { db_call(pool, Q::AddPlayer(player.into_inner())) }, )), ) // List of players .service( web::scope("/{player_id}") .route( "/", web::get().to_async(|pool, player: PlayerId| { db_call(pool, Q::FetchPlayer(*player)) }), ) .route( "/notifications", web::get().to_async(|pool, player: PlayerId| { db_call(pool, Q::FetchNotifications(*player)) }), ) .service( web::resource("/claims") .route(web::get().to_async(|pool, player: PlayerId| { db_call(pool, Q::FetchPlayerClaims(*player)) })) .route(web::post().to_async( |pool, (player, data): (PlayerId, IdList)| { db_call(pool, Q::ClaimItems(*player, data.into_inner())) }, )), ) .service( web::resource("/wealth") //.route(web::get().to_async(...)) .route(web::put().to_async( |pool, (player, data): (PlayerId, web::Json)| { db_call(pool, Q::UpdateWealth(*player, *data)) }, )), ) .service( web::resource("/loot") .route(web::get().to_async(|pool, player: PlayerId| { db_call(pool, Q::FetchLoot(*player)) })) .route(web::put().to_async( move |pool, (player, data): (PlayerId, BuySellParams)| { db_call(pool, Q::BuyItems(*player, data.into_inner())) }, )) .route(web::post().to( move |pool, (player, data): (PlayerId, NewGroupLoot)| { restricted_to_group( *player, (pool, Q::AddLoot(data.into_inner())), ) }, )) .route(web::delete().to_async( move |pool, (player, data): (PlayerId, BuySellParams)| { db_call(pool, Q::SellItems(*player, data.into_inner())) }, )), ) .service(web::scope("/events").route( "/last", web::delete().to_async(|pool, player: PlayerId| { db_call(pool, Q::UndoLastAction(*player)) }), )), ), ) .route( "/claims", web::get().to_async(|pool| db_call(pool, Q::FetchClaims)), ) .service( web::resource("/shop") .route(web::get().to_async(|pool| db_call(pool, Q::FetchShopInventory))) .route( web::post().to_async(|pool, items: web::Json| { db_call(pool, Q::RefreshShopInventory(items.into_inner())) }), ), ) .service( web::resource("/items") .route( web::get().to_async(move |pool: AppPool| db_call(pool, Q::FetchInventory)), ) .route(web::post().to_async( move |pool: AppPool, items: web::Json>| { db_call(pool, Q::CheckItemList(items.into_inner())) }, )), ), ); } #[derive(Deserialize)] struct AuthRequest { key: String, } #[derive(Debug, Copy, Clone, Serialize, Deserialize)] enum SessionKind { Player(i32), Admin, } use std::collections::HashMap; fn check_key(key: &str, db: HashMap<&str, SessionKind>) -> Option { db.get(&key).map(Clone::clone) } fn login(id: Identity, key: web::Query) -> HttpResponse { if let Some(session_kind) = check_key( &key.key.to_string(), [ ("0", SessionKind::Player(0)), ("1", SessionKind::Player(1)), ("2", SessionKind::Player(2)), ("admin", SessionKind::Admin), ] .iter() .cloned() .collect::>(), ) { id.remember(serde_json::to_string(&session_kind).expect("Serialize SessionKind error")); HttpResponse::build(StatusCode::TEMPORARY_REDIRECT) .header(header::LOCATION, "/") .finish() } else { HttpResponse::Forbidden().finish() } } fn logout(id: Identity) -> HttpResponse { id.forget(); HttpResponse::build(StatusCode::TEMPORARY_REDIRECT) .header(header::LOCATION, "/") .finish() } /// This endpoint shall be called by client, /// at initialization, to retrieve the current /// logging session info. /// /// # Returns /// /// The player data if a player is logged in /// The admin data if the admin is logged in /// A Forbidden response otherwise fn enter_session(id: Identity, pool: AppPool) -> impl Future { let conn = pool.get().unwrap(); let logged: SessionKind = id .identity() .map(|s| serde_json::from_str(&s).expect("Deserialize SessionKind error")) // This will fail, fastest way to handle // unlogged case with web::block below .unwrap_or(SessionKind::Player(-1)); web::block(move || { api::execute( &conn, match logged { SessionKind::Player(id) => api::ApiActions::FetchPlayer(id), SessionKind::Admin => api::ApiActions::FetchPlayers, }, ) }) .then(|res| match res { Ok(r) => HttpResponse::Ok().json(r.value), Err(e) => { dbg!(&e); HttpResponse::Forbidden().finish() } }) } pub fn serve() -> std::io::Result<()> { let domain: String = env::var("DOMAIN").expect("DOMAIN must be set"); let pool = db::create_pool(); println!("Serving Loot-a-lot on {}", domain); let key = [0; 32]; // TODO: Use a real key HttpServer::new(move || { App::new() .data(pool.clone()) .configure(configure_api) .wrap( Cors::new() .allowed_origin(&domain) .allowed_methods(vec!["GET", "POST", "PUT", "DELETE", "OPTIONS"]) .max_age(3600), ) .wrap(IdentityService::new( CookieIdentityPolicy::new(&key) .name("logged-in") .secure(false), )) //.wrap(middleware::Logger::default()) .wrap(middleware::Logger::new("%r -> %s (%{User-Agent}i)")) .route("/session", web::get().to_async(enter_session)) .route("/login", web::get().to(login)) .route("/logout", web::get().to(logout)) //.service(fs::Files::new("/", www_root.clone()).index_file("index.html")) }) .bind("127.0.0.1:8088")? .run() }