mirror of
https://github.com/warp-tech/warpgate.git
synced 2024-09-20 06:46:17 +08:00
fixed #196 - HTTP tickets support
This commit is contained in:
parent
112a6581f0
commit
8ea3250d4b
|
@ -1,4 +1,4 @@
|
|||
use crate::common::SessionExt;
|
||||
use crate::common::{SessionExt, SessionAuthorization};
|
||||
use crate::session::SessionStore;
|
||||
use anyhow::Context;
|
||||
use poem::session::Session;
|
||||
|
@ -83,7 +83,7 @@ impl Api {
|
|||
.set_username(username.clone())
|
||||
.await?;
|
||||
info!(%username, "Authenticated");
|
||||
session.set_username(username);
|
||||
session.set_auth(SessionAuthorization::User(username));
|
||||
Ok(LoginResponse::Success)
|
||||
}
|
||||
x => {
|
||||
|
|
|
@ -6,7 +6,7 @@ use poem_openapi::{ApiResponse, Object, OpenApi};
|
|||
use serde::Serialize;
|
||||
use warpgate_common::Services;
|
||||
|
||||
use crate::common::SessionExt;
|
||||
use crate::common::{SessionAuthorization, SessionExt};
|
||||
|
||||
pub struct Api;
|
||||
|
||||
|
@ -24,6 +24,7 @@ pub struct Info {
|
|||
selected_target: Option<String>,
|
||||
external_host: Option<String>,
|
||||
ports: PortsInfo,
|
||||
authorized_via_ticket: bool,
|
||||
}
|
||||
|
||||
#[derive(ApiResponse)]
|
||||
|
@ -53,6 +54,10 @@ impl Api {
|
|||
username: session.get_username(),
|
||||
selected_target: session.get_target_name(),
|
||||
external_host: external_host.map(&str::to_string),
|
||||
authorized_via_ticket: matches!(
|
||||
session.get_auth(),
|
||||
Some(SessionAuthorization::Ticket { .. })
|
||||
),
|
||||
ports: if session.is_authenticated() {
|
||||
PortsInfo {
|
||||
ssh: if config.store.ssh.enable {
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
use futures::stream;
|
||||
use futures::StreamExt;
|
||||
use futures::{stream, StreamExt};
|
||||
use poem::web::Data;
|
||||
use poem_openapi::payload::Json;
|
||||
use poem_openapi::{ApiResponse, Enum, Object, OpenApi};
|
||||
use serde::Serialize;
|
||||
use warpgate_common::{Services, TargetOptions};
|
||||
|
||||
use crate::common::{endpoint_auth, SessionUsername};
|
||||
use crate::common::{endpoint_auth, SessionAuthorization};
|
||||
|
||||
pub struct Api;
|
||||
|
||||
|
@ -42,24 +41,30 @@ impl Api {
|
|||
async fn api_get_all_targets(
|
||||
&self,
|
||||
services: Data<&Services>,
|
||||
username: Data<&SessionUsername>,
|
||||
auth: Data<&SessionAuthorization>,
|
||||
) -> poem::Result<GetTargetsResponse> {
|
||||
let targets = {
|
||||
let mut config_provider = services.config_provider.lock().await;
|
||||
config_provider.list_targets().await?
|
||||
};
|
||||
let mut targets = stream::iter(targets)
|
||||
.filter_map(|t| {
|
||||
.filter(|t| {
|
||||
let services = services.clone();
|
||||
let username = &username;
|
||||
let auth = auth.clone();
|
||||
let name = t.name.clone();
|
||||
async move {
|
||||
let mut config_provider = services.config_provider.lock().await;
|
||||
match config_provider
|
||||
.authorize_target(&username.0 .0, &t.name)
|
||||
.await
|
||||
{
|
||||
Ok(true) => Some(t),
|
||||
_ => None,
|
||||
match auth {
|
||||
SessionAuthorization::Ticket { target_name, .. } => target_name == name,
|
||||
SessionAuthorization::User(_) => {
|
||||
let mut config_provider = services.config_provider.lock().await;
|
||||
match config_provider
|
||||
.authorize_target(auth.username(), &name)
|
||||
.await
|
||||
{
|
||||
Ok(true) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -9,7 +9,7 @@ use tokio::sync::Mutex;
|
|||
use tracing::*;
|
||||
use warpgate_common::{Services, Target, TargetHTTPOptions, TargetOptions, WarpgateServerHandle};
|
||||
|
||||
use crate::common::{gateway_redirect, SessionExt, SessionUsername};
|
||||
use crate::common::{gateway_redirect, SessionAuthorization, SessionExt};
|
||||
use crate::proxy::{proxy_normal_request, proxy_websocket_request};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
@ -24,7 +24,6 @@ pub async fn catchall_endpoint(
|
|||
ws: Option<WebSocket>,
|
||||
session: &Session,
|
||||
body: Body,
|
||||
username: Data<&SessionUsername>,
|
||||
services: Data<&Services>,
|
||||
server_handle: Option<Data<&Arc<Mutex<WarpgateServerHandle>>>>,
|
||||
) -> poem::Result<Response> {
|
||||
|
@ -34,16 +33,6 @@ pub async fn catchall_endpoint(
|
|||
|
||||
session.set_target_name(target.name.clone());
|
||||
|
||||
if !services
|
||||
.config_provider
|
||||
.lock()
|
||||
.await
|
||||
.authorize_target(&username.0 .0, &target.name)
|
||||
.await?
|
||||
{
|
||||
return Ok(gateway_redirect(req).into_response());
|
||||
}
|
||||
|
||||
if let Some(server_handle) = server_handle {
|
||||
server_handle.lock().await.set_target(&target).await?;
|
||||
}
|
||||
|
@ -68,8 +57,48 @@ async fn get_target_for_request(
|
|||
) -> poem::Result<Option<(Target, TargetHTTPOptions)>> {
|
||||
let session: &Session = <_>::from_request_without_body(req).await?;
|
||||
let params: QueryParams = req.params()?;
|
||||
let auth: Data<&SessionAuthorization> = <_>::from_request_without_body(req).await?;
|
||||
|
||||
if let Some(target_name) = params.warpgate_target.or(session.get_target_name()) {
|
||||
let selected_target_name;
|
||||
let need_role_auth;
|
||||
|
||||
let host_based_target_name = if let Some(host) = req.original_uri().host() {
|
||||
services
|
||||
.config
|
||||
.lock()
|
||||
.await
|
||||
.store
|
||||
.targets
|
||||
.iter()
|
||||
.filter_map(|t| match t.options {
|
||||
TargetOptions::Http(ref options) => Some((t, options)),
|
||||
_ => None,
|
||||
})
|
||||
.filter(|(_, o)| o.external_host.as_deref() == Some(host))
|
||||
.next()
|
||||
.map(|(t, _)| t.name.clone())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
match *auth {
|
||||
SessionAuthorization::Ticket { target_name, .. } => {
|
||||
selected_target_name = Some(target_name.clone());
|
||||
need_role_auth = false;
|
||||
}
|
||||
SessionAuthorization::User(_) => {
|
||||
need_role_auth = true;
|
||||
|
||||
selected_target_name =
|
||||
host_based_target_name.or(if let Some(warpgate_target) = params.warpgate_target {
|
||||
Some(warpgate_target)
|
||||
} else {
|
||||
session.get_target_name()
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(target_name) = selected_target_name {
|
||||
let target = {
|
||||
services
|
||||
.config
|
||||
|
@ -87,29 +116,21 @@ async fn get_target_for_request(
|
|||
.map(|(t, o)| (t.clone(), o.clone()))
|
||||
};
|
||||
|
||||
return Ok(target);
|
||||
if let Some(target) = target {
|
||||
if need_role_auth
|
||||
&& !services
|
||||
.config_provider
|
||||
.lock()
|
||||
.await
|
||||
.authorize_target(&auth.username(), &target.0.name)
|
||||
.await?
|
||||
{
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
return Ok(Some(target));
|
||||
}
|
||||
}
|
||||
|
||||
let Some(host) = req.original_uri().host() else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let target = {
|
||||
services
|
||||
.config
|
||||
.lock()
|
||||
.await
|
||||
.store
|
||||
.targets
|
||||
.iter()
|
||||
.filter_map(|t| match t.options {
|
||||
TargetOptions::Http(ref options) => Some((t, options)),
|
||||
_ => None,
|
||||
})
|
||||
.filter(|(_, o)| o.external_host.as_deref() == Some(host))
|
||||
.next()
|
||||
.map(|(t, o)| (t.clone(), o.clone()))
|
||||
};
|
||||
|
||||
return Ok(target);
|
||||
return Ok(None);
|
||||
}
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use http::StatusCode;
|
||||
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
|
||||
use poem::session::Session;
|
||||
use poem::web::{Data, Redirect};
|
||||
use poem::{Endpoint, EndpointExt, FromRequest, IntoResponse, Request, Response};
|
||||
use std::time::Duration;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use warpgate_common::{ProtocolName, Services, TargetOptions};
|
||||
|
||||
pub const PROTOCOL_NAME: ProtocolName = "HTTP";
|
||||
static USERNAME_SESSION_KEY: &str = "username";
|
||||
static TARGET_SESSION_KEY: &str = "target_name";
|
||||
static AUTH_SESSION_KEY: &str = "auth";
|
||||
pub static SESSION_MAX_AGE: Duration = Duration::from_secs(60 * 30);
|
||||
pub static COOKIE_MAX_AGE: Duration = Duration::from_secs(60 * 60 * 24);
|
||||
|
||||
|
@ -18,7 +21,8 @@ pub trait SessionExt {
|
|||
fn set_target_name(&self, target_name: String);
|
||||
fn is_authenticated(&self) -> bool;
|
||||
fn get_username(&self) -> Option<String>;
|
||||
fn set_username(&self, username: String);
|
||||
fn get_auth(&self) -> Option<SessionAuthorization>;
|
||||
fn set_auth(&self, auth: SessionAuthorization);
|
||||
}
|
||||
|
||||
impl SessionExt for Session {
|
||||
|
@ -27,7 +31,7 @@ impl SessionExt for Session {
|
|||
}
|
||||
|
||||
fn get_target_name(&self) -> Option<String> {
|
||||
self.get::<String>(TARGET_SESSION_KEY)
|
||||
self.get(TARGET_SESSION_KEY)
|
||||
}
|
||||
|
||||
fn set_target_name(&self, target_name: String) {
|
||||
|
@ -39,26 +43,49 @@ impl SessionExt for Session {
|
|||
}
|
||||
|
||||
fn get_username(&self) -> Option<String> {
|
||||
self.get::<String>(USERNAME_SESSION_KEY)
|
||||
return self.get_auth().map(|x| x.username().to_owned());
|
||||
}
|
||||
|
||||
fn set_username(&self, username: String) {
|
||||
self.set(USERNAME_SESSION_KEY, username);
|
||||
fn get_auth(&self) -> Option<SessionAuthorization> {
|
||||
self.get(AUTH_SESSION_KEY)
|
||||
}
|
||||
|
||||
fn set_auth(&self, auth: SessionAuthorization) {
|
||||
self.set(AUTH_SESSION_KEY, auth);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SessionUsername(pub String);
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub enum SessionAuthorization {
|
||||
User(String),
|
||||
Ticket {
|
||||
username: String,
|
||||
target_name: String,
|
||||
},
|
||||
}
|
||||
|
||||
async fn is_user_admin(req: &Request, username: &SessionUsername) -> poem::Result<bool> {
|
||||
impl SessionAuthorization {
|
||||
pub fn username(&self) -> &String {
|
||||
match self {
|
||||
SessionAuthorization::User(username) => username,
|
||||
SessionAuthorization::Ticket { username, .. } => username,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn is_user_admin(req: &Request, auth: &SessionAuthorization) -> poem::Result<bool> {
|
||||
let services: Data<&Services> = <_>::from_request_without_body(&req).await?;
|
||||
|
||||
let SessionAuthorization::User(username) = auth else {
|
||||
return Ok(false)
|
||||
};
|
||||
|
||||
let mut config_provider = services.config_provider.lock().await;
|
||||
let targets = config_provider.list_targets().await?;
|
||||
for target in targets {
|
||||
if matches!(target.options, TargetOptions::WebAdmin(_))
|
||||
&& config_provider
|
||||
.authorize_target(&username.0, &target.name)
|
||||
.authorize_target(&username, &target.name)
|
||||
.await?
|
||||
{
|
||||
drop(config_provider);
|
||||
|
@ -70,8 +97,8 @@ async fn is_user_admin(req: &Request, username: &SessionUsername) -> poem::Resul
|
|||
|
||||
pub fn endpoint_admin_auth<E: Endpoint + 'static>(e: E) -> impl Endpoint {
|
||||
e.around(|ep, req| async move {
|
||||
let username: Data<&SessionUsername> = <_>::from_request_without_body(&req).await?;
|
||||
if is_user_admin(&req, username.0).await? {
|
||||
let auth: Data<&SessionAuthorization> = <_>::from_request_without_body(&req).await?;
|
||||
if is_user_admin(&req, &auth).await? {
|
||||
return Ok(ep.call(req).await?.into_response());
|
||||
}
|
||||
Err(poem::Error::from_status(StatusCode::UNAUTHORIZED))
|
||||
|
@ -80,9 +107,9 @@ pub fn endpoint_admin_auth<E: Endpoint + 'static>(e: E) -> impl Endpoint {
|
|||
|
||||
pub fn page_admin_auth<E: Endpoint + 'static>(e: E) -> impl Endpoint {
|
||||
e.around(|ep, req| async move {
|
||||
let username: Data<&SessionUsername> = <_>::from_request_without_body(&req).await?;
|
||||
let auth: Data<&SessionAuthorization> = <_>::from_request_without_body(&req).await?;
|
||||
let session: &Session = <_>::from_request_without_body(&req).await?;
|
||||
if is_user_admin(&req, username.0).await? {
|
||||
if is_user_admin(&req, &auth).await? {
|
||||
return Ok(ep.call(req).await?.into_response());
|
||||
}
|
||||
session.clear();
|
||||
|
@ -90,29 +117,33 @@ pub fn page_admin_auth<E: Endpoint + 'static>(e: E) -> impl Endpoint {
|
|||
})
|
||||
}
|
||||
|
||||
pub async fn _inner_auth<E: Endpoint + 'static>(
|
||||
ep: Arc<E>,
|
||||
req: Request,
|
||||
) -> poem::Result<Option<E::Output>> {
|
||||
let session: &Session = FromRequest::from_request_without_body(&req).await?;
|
||||
|
||||
Ok(match session.get_auth() {
|
||||
Some(auth) => Some(ep.data(auth).call(req).await?),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn endpoint_auth<E: Endpoint + 'static>(e: E) -> impl Endpoint {
|
||||
e.around(|ep, req| async move {
|
||||
let session: &Session = FromRequest::from_request_without_body(&req).await?;
|
||||
|
||||
match session.get_username() {
|
||||
Some(username) => Ok(ep.data(SessionUsername(username)).call(req).await?),
|
||||
None => Err(poem::Error::from_status(StatusCode::UNAUTHORIZED)),
|
||||
}
|
||||
_inner_auth(ep, req)
|
||||
.await?
|
||||
.ok_or_else(|| poem::Error::from_status(StatusCode::UNAUTHORIZED))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn page_auth<E: Endpoint + 'static>(e: E) -> impl Endpoint {
|
||||
e.around(|ep, req| async move {
|
||||
let session: &Session = FromRequest::from_request_without_body(&req).await?;
|
||||
|
||||
match session.get_username() {
|
||||
Some(username) => Ok(ep
|
||||
.data(SessionUsername(username))
|
||||
.call(req)
|
||||
.await?
|
||||
.into_response()),
|
||||
None => Ok(gateway_redirect(&req).into_response()),
|
||||
}
|
||||
let err_resp = gateway_redirect(&req).into_response();
|
||||
Ok(_inner_auth(ep, req)
|
||||
.await?
|
||||
.map(IntoResponse::into_response)
|
||||
.unwrap_or(err_resp))
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ mod catchall;
|
|||
mod common;
|
||||
mod error;
|
||||
mod logging;
|
||||
mod middleware;
|
||||
mod proxy;
|
||||
mod session;
|
||||
mod session_handle;
|
||||
|
@ -21,7 +22,7 @@ use logging::{log_request_result, span_for_request};
|
|||
use poem::endpoint::{EmbeddedFileEndpoint, EmbeddedFilesEndpoint};
|
||||
use poem::listener::{Listener, RustlsConfig, TcpListener};
|
||||
use poem::middleware::SetHeader;
|
||||
use poem::session::MemoryStorage;
|
||||
use poem::session::{CookieConfig, MemoryStorage, ServerSession};
|
||||
use poem::web::Data;
|
||||
use poem::{Endpoint, EndpointExt, FromRequest, IntoEndpoint, IntoResponse, Route, Server};
|
||||
use poem_openapi::OpenApiService;
|
||||
|
@ -34,9 +35,10 @@ use warpgate_common::{
|
|||
};
|
||||
use warpgate_web::Assets;
|
||||
|
||||
use crate::common::{endpoint_admin_auth, endpoint_auth, page_auth};
|
||||
use crate::common::{endpoint_admin_auth, endpoint_auth, page_auth, COOKIE_MAX_AGE};
|
||||
use crate::error::error_page;
|
||||
use crate::session::{SessionMiddleware, SessionStore, SharedSessionStorage};
|
||||
use crate::middleware::{CookieHostMiddleware, TicketMiddleware};
|
||||
use crate::session::{SessionStore, SharedSessionStorage};
|
||||
|
||||
pub struct HTTPProtocolServer {
|
||||
services: Services,
|
||||
|
@ -121,7 +123,15 @@ impl ProtocolServer for HTTPProtocolServer {
|
|||
SetHeader::new()
|
||||
.overriding(http::header::STRICT_TRANSPORT_SECURITY, "max-age=31536000"),
|
||||
)
|
||||
.with(SessionMiddleware::new(session_storage.clone()))
|
||||
.with(TicketMiddleware::new())
|
||||
.with(ServerSession::new(
|
||||
CookieConfig::default()
|
||||
.secure(false)
|
||||
.max_age(COOKIE_MAX_AGE)
|
||||
.name("warpgate-http-session"),
|
||||
session_storage.clone(),
|
||||
))
|
||||
.with(CookieHostMiddleware::new())
|
||||
.data(self.services.clone())
|
||||
.data(session_store.clone())
|
||||
.data(session_storage);
|
||||
|
|
49
warpgate-protocol-http/src/middleware/cookie_host.rs
Normal file
49
warpgate-protocol-http/src/middleware/cookie_host.rs
Normal file
|
@ -0,0 +1,49 @@
|
|||
use async_trait::async_trait;
|
||||
use http::header::Entry;
|
||||
use poem::web::cookie::Cookie;
|
||||
use poem::{Endpoint, IntoResponse, Middleware, Request, Response};
|
||||
|
||||
pub struct CookieHostMiddleware {}
|
||||
|
||||
impl CookieHostMiddleware {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CookieHostMiddlewareEndpoint<E: Endpoint> {
|
||||
inner: E,
|
||||
}
|
||||
|
||||
impl<E: Endpoint> Middleware<E> for CookieHostMiddleware {
|
||||
type Output = CookieHostMiddlewareEndpoint<E>;
|
||||
|
||||
fn transform(&self, inner: E) -> Self::Output {
|
||||
CookieHostMiddlewareEndpoint { inner }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<E: Endpoint> Endpoint for CookieHostMiddlewareEndpoint<E> {
|
||||
type Output = Response;
|
||||
|
||||
async fn call(&self, req: Request) -> poem::Result<Self::Output> {
|
||||
let host = req.original_uri().host().map(|x| x.to_string());
|
||||
|
||||
let mut resp = self.inner.call(req).await?.into_response();
|
||||
|
||||
if let Some(host) = host {
|
||||
if let Entry::Occupied(mut entry) = resp.headers_mut().entry(http::header::SET_COOKIE) {
|
||||
if let Ok(cookie_str) = entry.get().to_str() {
|
||||
if let Ok(mut cookie) = Cookie::parse(cookie_str) {
|
||||
cookie.set_domain(host);
|
||||
if let Ok(value) = cookie.to_string().parse() {
|
||||
entry.insert(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(resp)
|
||||
}
|
||||
}
|
5
warpgate-protocol-http/src/middleware/mod.rs
Normal file
5
warpgate-protocol-http/src/middleware/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
mod cookie_host;
|
||||
mod ticket;
|
||||
|
||||
pub use cookie_host::*;
|
||||
pub use ticket::*;
|
91
warpgate-protocol-http/src/middleware/ticket.rs
Normal file
91
warpgate-protocol-http/src/middleware/ticket.rs
Normal file
|
@ -0,0 +1,91 @@
|
|||
use async_trait::async_trait;
|
||||
use poem::session::Session;
|
||||
use poem::web::{Data, FromRequest};
|
||||
use poem::{Endpoint, Middleware, Request};
|
||||
use serde::Deserialize;
|
||||
use warpgate_common::{authorize_ticket, Secret, Services};
|
||||
|
||||
use crate::common::{SessionExt};
|
||||
|
||||
pub struct TicketMiddleware {}
|
||||
|
||||
impl TicketMiddleware {
|
||||
pub fn new() -> Self {
|
||||
TicketMiddleware {}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TicketMiddlewareEndpoint<E: Endpoint> {
|
||||
inner: E,
|
||||
}
|
||||
|
||||
impl<E: Endpoint> Middleware<E> for TicketMiddleware {
|
||||
type Output = TicketMiddlewareEndpoint<E>;
|
||||
|
||||
fn transform(&self, inner: E) -> Self::Output {
|
||||
TicketMiddlewareEndpoint { inner }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct QueryParams {
|
||||
#[serde(rename = "warpgate-ticket")]
|
||||
ticket: Option<String>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<E: Endpoint> Endpoint for TicketMiddlewareEndpoint<E> {
|
||||
type Output = E::Output;
|
||||
|
||||
async fn call(&self, req: Request) -> poem::Result<Self::Output> {
|
||||
let mut session_is_temporary = false;
|
||||
let session: &Session = <_>::from_request_without_body(&req).await?;
|
||||
let session = session.clone();
|
||||
|
||||
{
|
||||
let params: QueryParams = req.params()?;
|
||||
|
||||
let mut ticket_value = None;
|
||||
if let Some(t) = params.ticket {
|
||||
ticket_value = Some(t);
|
||||
}
|
||||
for h in req.headers().get_all(http::header::AUTHORIZATION) {
|
||||
let header_value = h.to_str().unwrap_or("").to_string();
|
||||
if let Some((token_type, token_value)) = header_value.split_once(' ') {
|
||||
if &token_type.to_lowercase() == "warpgate" {
|
||||
ticket_value = Some(token_value.to_string());
|
||||
session_is_temporary = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ticket) = ticket_value {
|
||||
let services: Data<&Services> = <_>::from_request_without_body(&req).await?;
|
||||
|
||||
if let Some(ticket_model) = {
|
||||
let ticket = Secret::new(ticket);
|
||||
let mut cp = services.config_provider.lock().await;
|
||||
if let Some(res) = authorize_ticket(&services.db, &ticket).await? {
|
||||
cp.consume_ticket(&res.id).await?;
|
||||
Some(res)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} {
|
||||
session.set_auth(crate::common::SessionAuthorization::Ticket {
|
||||
username: ticket_model.username,
|
||||
target_name: ticket_model.target,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let resp = self.inner.call(req).await;
|
||||
|
||||
if session_is_temporary {
|
||||
session.clear();
|
||||
}
|
||||
|
||||
resp
|
||||
}
|
||||
}
|
|
@ -3,21 +3,15 @@ use std::sync::{Arc, Weak};
|
|||
use std::time::{Duration, Instant};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use http::header::Entry;
|
||||
use poem::middleware::CookieJarManagerEndpoint;
|
||||
use poem::session::{
|
||||
CookieConfig, ServerSession as PoemSessionMiddleware, ServerSessionEndpoint, Session,
|
||||
SessionStorage,
|
||||
};
|
||||
use poem::web::cookie::Cookie;
|
||||
use poem::session::{Session, SessionStorage};
|
||||
use poem::web::{Data, RemoteAddr};
|
||||
use poem::{Endpoint, FromRequest, IntoResponse, Middleware, Request, Response};
|
||||
use poem::{FromRequest, Request};
|
||||
use serde_json::Value;
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::*;
|
||||
use warpgate_common::{Services, SessionId, SessionStateInit, WarpgateServerHandle};
|
||||
|
||||
use crate::common::{COOKIE_MAX_AGE, PROTOCOL_NAME, SESSION_MAX_AGE};
|
||||
use crate::common::{PROTOCOL_NAME, SESSION_MAX_AGE};
|
||||
use crate::session_handle::{
|
||||
HttpSessionHandle, SessionHandleCommand, WarpgateServerHandleFromRequest,
|
||||
};
|
||||
|
@ -190,60 +184,3 @@ impl SessionStore {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SessionMiddleware {
|
||||
inner: PoemSessionMiddleware<SharedSessionStorage>,
|
||||
}
|
||||
|
||||
impl SessionMiddleware {
|
||||
pub fn new(session_storage: SharedSessionStorage) -> Self {
|
||||
Self {
|
||||
inner: PoemSessionMiddleware::new(
|
||||
CookieConfig::default()
|
||||
.secure(false)
|
||||
.max_age(COOKIE_MAX_AGE)
|
||||
.name("warpgate-http-session"),
|
||||
session_storage,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SessionMiddlewareEndpoint<E: Endpoint> {
|
||||
inner: E,
|
||||
}
|
||||
|
||||
impl<E: Endpoint> Middleware<E> for SessionMiddleware {
|
||||
type Output = SessionMiddlewareEndpoint<
|
||||
CookieJarManagerEndpoint<ServerSessionEndpoint<SharedSessionStorage, E>>,
|
||||
>;
|
||||
|
||||
fn transform(&self, ep: E) -> Self::Output {
|
||||
SessionMiddlewareEndpoint {
|
||||
inner: self.inner.transform(ep),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<E: Endpoint> Endpoint for SessionMiddlewareEndpoint<E> {
|
||||
type Output = Response;
|
||||
|
||||
async fn call(&self, req: Request) -> poem::Result<Self::Output> {
|
||||
let host = req.original_uri().host().map(|x| x.to_string());
|
||||
let mut resp = self.inner.call(req).await?.into_response();
|
||||
if let Some(host) = host {
|
||||
if let Entry::Occupied(mut entry) = resp.headers_mut().entry(http::header::SET_COOKIE) {
|
||||
if let Ok(cookie_str) = entry.get().to_str() {
|
||||
if let Ok(mut cookie) = Cookie::parse(cookie_str) {
|
||||
cookie.set_domain(host);
|
||||
if let Ok(value) = cookie.to_string().parse() {
|
||||
entry.insert(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(resp)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
$: exampleMySQLCommand = makeExampleMySQLCommand(opts)
|
||||
$: exampleMySQLURI = makeExampleMySQLURI(opts)
|
||||
$: targetURL = targetName ? makeTargetURL(opts) : ''
|
||||
$: authHeader = `Authorization: Warpgate ${ticketSecret}`
|
||||
</script>
|
||||
|
||||
{#if targetKind === TargetKind.Ssh}
|
||||
|
@ -43,6 +44,12 @@
|
|||
<input type="text" class="form-control" readonly value={targetURL} />
|
||||
<CopyButton text={targetURL} />
|
||||
</FormGroup>
|
||||
|
||||
Alternatively, set the <code>Authorization</code> header when accessing the URL:
|
||||
<FormGroup floating label="Authorization header" class="d-flex align-items-center">
|
||||
<input type="text" class="form-control" readonly value={authHeader} />
|
||||
<CopyButton text={authHeader} />
|
||||
</FormGroup>
|
||||
{/if}
|
||||
|
||||
{#if targetKind === TargetKind.MySql}
|
||||
|
|
|
@ -43,7 +43,12 @@ init()
|
|||
</div>
|
||||
|
||||
{#if $serverInfo?.username}
|
||||
<div class="ms-auto">{$serverInfo.username}</div>
|
||||
<div class="ms-auto">
|
||||
{$serverInfo.username}
|
||||
{#if $serverInfo.authorizedViaTicket}
|
||||
<span class="ml-2">(ticket auth)</span>
|
||||
{/if}
|
||||
</div>
|
||||
<button class="btn btn-link" on:click={logout} title="Log out">
|
||||
<Fa icon={faSignOut} fw />
|
||||
</button>
|
||||
|
|
|
@ -95,7 +95,8 @@
|
|||
"type": "object",
|
||||
"required": [
|
||||
"version",
|
||||
"ports"
|
||||
"ports",
|
||||
"authorized_via_ticket"
|
||||
],
|
||||
"properties": {
|
||||
"version": {
|
||||
|
@ -112,6 +113,9 @@
|
|||
},
|
||||
"ports": {
|
||||
"$ref": "#/components/schemas/PortsInfo"
|
||||
},
|
||||
"authorized_via_ticket": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue