Merge pull request #11 from clowzed/feature/cors

Feature/cors
This commit is contained in:
Dmitry Miasnenko 2023-11-12 19:05:31 +03:00 committed by GitHub
commit 0469257a59
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 510 additions and 155 deletions

27
.github/workflows/testable.yml vendored Normal file
View file

@ -0,0 +1,27 @@
name: docker build testable
on:
push:
branches-ignore:
- master
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: clowzed
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: true
tags: clowzed/sero:testable

63
Cargo.lock generated
View file

@ -1112,12 +1112,13 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
[[package]]
name = "futures"
version = "0.3.28"
version = "0.3.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40"
checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
@ -1126,9 +1127,9 @@ dependencies = [
[[package]]
name = "futures-channel"
version = "0.3.28"
version = "0.3.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2"
checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb"
dependencies = [
"futures-core",
"futures-sink",
@ -1136,15 +1137,15 @@ dependencies = [
[[package]]
name = "futures-core"
version = "0.3.28"
version = "0.3.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c"
checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c"
[[package]]
name = "futures-executor"
version = "0.3.28"
version = "0.3.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0"
checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc"
dependencies = [
"futures-core",
"futures-task",
@ -1164,9 +1165,9 @@ dependencies = [
[[package]]
name = "futures-io"
version = "0.3.28"
version = "0.3.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964"
checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa"
[[package]]
name = "futures-lite"
@ -1185,9 +1186,9 @@ dependencies = [
[[package]]
name = "futures-macro"
version = "0.3.28"
version = "0.3.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb"
dependencies = [
"proc-macro2",
"quote",
@ -1196,21 +1197,21 @@ dependencies = [
[[package]]
name = "futures-sink"
version = "0.3.28"
version = "0.3.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e"
checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817"
[[package]]
name = "futures-task"
version = "0.3.28"
version = "0.3.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65"
checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2"
[[package]]
name = "futures-util"
version = "0.3.28"
version = "0.3.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533"
checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104"
dependencies = [
"futures-channel",
"futures-core",
@ -1419,6 +1420,12 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "http-range-header"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "add0ab9360ddbd88cfeb3bd9574a1d85cfdfa14db10b3e21d3700dbc4328758f"
[[package]]
name = "httparse"
version = "1.8.0"
@ -2853,6 +2860,7 @@ dependencies = [
"dotenv",
"entity",
"envy",
"futures",
"futures-util",
"hyper",
"jsonwebtoken",
@ -2868,6 +2876,7 @@ dependencies = [
"tokio",
"tokio-postgres",
"tokio-util",
"tower-http",
"tower",
"tracing",
"tracing-subscriber",
@ -3542,6 +3551,24 @@ dependencies = [
"tracing",
]
[[package]]
name = "tower-http"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61c5bb1d698276a2443e5ecfabc1008bf15a36c12e6a7176e7bf089ea9131140"
dependencies = [
"bitflags 2.4.0",
"bytes",
"futures-core",
"futures-util",
"http",
"http-body",
"http-range-header",
"pin-project-lite",
"tower-layer",
"tower-service",
]
[[package]]
name = "tower-layer"
version = "0.3.2"

View file

@ -32,6 +32,8 @@ bytes = "1.5.0"
async-trait = "0.1.73"
tokio-util = { version = "0.7.9", features = ["io"] }
dotenv = "0.15.0"
tower-http = { version = "0.4.4", features = ["cors"] }
futures = "0.3.29"
tower = { version = "0.4.13", features = ["util"] }
serde_urlencoded = "0.7.1"
serde_json = "1.0.108"

38
entity/src/cors.rs Normal file
View file

@ -0,0 +1,38 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.3
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "cors")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub subdomain_id: i32,
pub origin: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::subdomain::Entity",
from = "Column::SubdomainId",
to = "super::subdomain::Column::Id",
on_update = "NoAction",
on_delete = "Cascade"
)]
Subdomain,
}
impl Related<super::subdomain::Entity> for Entity {
fn to() -> RelationDef {
Relation::Subdomain.def()
}
}
impl ActiveModelBehavior for ActiveModel {}
impl Model {
pub fn matches(&self, origin: &str) -> bool {
self.origin == "*" || self.origin == origin
}
}

View file

@ -1,5 +1,6 @@
pub mod prelude;
pub mod cors;
pub mod file;
pub mod subdomain;
pub mod user;

View file

@ -2,6 +2,7 @@
pub mod prelude;
pub mod cors;
pub mod file;
pub mod subdomain;
pub mod user;

View file

@ -1,15 +1,19 @@
pub use super::cors::Entity as CorsEntity;
pub use super::file::Entity as FileEntity;
pub use super::subdomain::Entity as SubdomainEntity;
pub use super::user::Entity as UserEntity;
pub use super::cors::ActiveModel as ActiveCors;
pub use super::file::ActiveModel as ActiveFile;
pub use super::subdomain::ActiveModel as ActiveSubdomain;
pub use super::user::ActiveModel as ActiveUser;
pub use super::cors::Model as Cors;
pub use super::file::Model as File;
pub use super::subdomain::Model as Subdomain;
pub use super::user::Model as User;
pub use super::cors::Column as CorsColumn;
pub use super::file::Column as FileColumn;
pub use super::subdomain::Column as SubdomainColumn;
pub use super::user::Column as UserColumn;

View file

@ -17,6 +17,8 @@ pub struct Model {
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::cors::Entity")]
Cors,
#[sea_orm(has_many = "super::file::Entity")]
File,
#[sea_orm(
@ -29,6 +31,12 @@ pub enum Relation {
User,
}
impl Related<super::cors::Entity> for Entity {
fn to() -> RelationDef {
Relation::Cors.def()
}
}
impl Related<super::file::Entity> for Entity {
fn to() -> RelationDef {
Relation::File.def()

View file

@ -1,10 +1,8 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.3
use std::fmt::Debug;
use sea_orm::entity::prelude::*;
#[derive(Clone, PartialEq, DeriveEntityModel, Eq)]
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "user")]
pub struct Model {
#[sea_orm(primary_key)]
@ -27,13 +25,3 @@ impl Related<super::subdomain::Entity> for Entity {
}
impl ActiveModelBehavior for ActiveModel {}
impl Debug for Model {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Model")
.field("id", &self.id)
.field("username", &self.username)
.field("password", &"...")
.finish()
}
}

View file

@ -3,6 +3,7 @@ pub use sea_orm_migration::prelude::*;
mod m20230927_162921_create_users;
mod m20230929_081415_create_subdomains;
mod m20230929_152215_create_files;
mod m20231105_171000_create_cors;
pub struct Migrator;
@ -13,6 +14,7 @@ impl MigratorTrait for Migrator {
Box::new(m20230927_162921_create_users::Migration),
Box::new(m20230929_081415_create_subdomains::Migration),
Box::new(m20230929_152215_create_files::Migration),
Box::new(m20231105_171000_create_cors::Migration),
]
}
}

View file

@ -0,0 +1,49 @@
use sea_orm_migration::prelude::*;
use crate::m20230929_081415_create_subdomains::Subdomain;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Cors::Table)
.if_not_exists()
.col(
ColumnDef::new(Cors::Id)
.integer()
.not_null()
.auto_increment()
.primary_key(),
)
.col(ColumnDef::new(Cors::SubdomainId).integer().not_null())
.foreign_key(
ForeignKey::create()
.from(Cors::Table, Cors::SubdomainId)
.to(Subdomain::Table, Subdomain::Id)
.on_delete(ForeignKeyAction::Cascade),
)
.col(ColumnDef::new(Cors::Origin).string().not_null())
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Cors::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
pub enum Cors {
Table,
Id,
SubdomainId,
Origin,
}

View file

@ -294,11 +294,6 @@ paths:
responses:
"200":
description: Site was successfully uploaded
content:
application/octet-stream:
schema:
type: string
format: binary
"403":
description: Subdomain is owned by another user
@ -324,6 +319,91 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/Details"
/api/cors/add:
post:
summary: Add origin
parameters:
- in: header
name: X-Subdomain
schema:
type: string
required: true
security:
- bearerAuth: []
requestBody:
required: true
content:
application/x-www-form-urlencoded:
schema:
$ref: "#/components/schemas/Origin"
responses:
"200":
description: Origin was successfully added
"400":
description: Bad request
content:
application/json:
schema:
$ref: "#/components/schemas/Details"
"401":
description: Authentication failed, unauthorized
content:
application/json:
schema:
$ref: "#/components/schemas/Details"
"403":
description: Subdomain is owned by another user
content:
application/json:
schema:
$ref: "#/components/schemas/Details"
"500":
description: Server error
content:
application/json:
schema:
$ref: "#/components/schemas/Details"
/api/cors/clear:
post:
summary: Clear related origins
parameters:
- in: header
name: X-Subdomain
schema:
type: string
required: true
security:
- bearerAuth: []
responses:
"200":
description: Origins were successfully removed
"400":
description: Bad request
content:
application/json:
schema:
$ref: "#/components/schemas/Details"
"401":
description: Authentication failed, unauthorized
content:
application/json:
schema:
$ref: "#/components/schemas/Details"
"403":
description: Subdomain is owned by another user
content:
application/json:
schema:
$ref: "#/components/schemas/Details"
"500":
description: Server error
content:
application/json:
schema:
$ref: "#/components/schemas/Details"
components:
schemas:
@ -344,6 +424,12 @@ components:
properties:
details:
type: string
Origin:
type: object
properties:
origin:
type: string
securitySchemes:
bearerAuth:
type: http

View file

@ -25,109 +25,67 @@ pub enum SeroError {
EmptyCredentials,
}
impl IntoResponse for SeroError {
fn into_response(self) -> axum::response::Response {
let response = match self {
SeroError::XSubdomainHeaderMissing => (
StatusCode::BAD_REQUEST,
Json(Details {
details: "X-Subdomain header is missing!".into(),
}),
),
SeroError::AuthorizationHeaderMissing => (
StatusCode::BAD_REQUEST,
Json(Details {
details: "Authorization header is missing!".into(),
}),
),
SeroError::AuthorizationHeaderBadSchema => (
StatusCode::BAD_REQUEST,
Json(Details {
details: "Authorization header does not match schema!
Required schema: Authorization: Bearer <token>"
.into(),
}),
),
SeroError::SubdomainIsOwnedByAnotherUser(subdomain_name) => (
StatusCode::FORBIDDEN,
Json(Details {
details: format!(
"Subdomain with name {} is owned by another user!",
subdomain_name
),
}),
),
SeroError::AuthorizationHeaderBabChars => (
StatusCode::BAD_REQUEST,
Json(Details {
details: "Authorization header contains invalid characters!".into(),
}),
),
impl std::fmt::Debug for SeroError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", match self {
SeroError::XSubdomainHeaderMissing => "X-Subdomain header is missing!".to_string(),
SeroError::AuthorizationHeaderMissing => "Authorization header is missing!".to_string(),
SeroError::AuthorizationHeaderBadSchema => "Authorization header does not match schema! Required schema: Authorization: Bearer <token>".to_string(),
SeroError::SubdomainIsOwnedByAnotherUser(subdomain_name) => format!("Subdomain with name {} is owned by another user!", subdomain_name),
SeroError::AuthorizationHeaderBabChars => "Authorization header contains invalid characters!".to_string(),
SeroError::InternalServerError(cause) => {
tracing::error!(%cause, "Error!");
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(Details {
details: "Some error occurred on the server!".into(),
}),
)
}
SeroError::UserWasNotFoundUsingJwt => (
StatusCode::UNAUTHORIZED,
Json(Details {
details: "User with id from jwt token was not found!".into(),
}),
),
SeroError::RegisteredUserLimitExceeded => (
StatusCode::FORBIDDEN,
Json(Details {
details: "Registered user limit exceeded!".into(),
}),
),
SeroError::Unauthorized => (
StatusCode::UNAUTHORIZED,
Json(Details {
details: "Unauthorized! Bad credentials were provided!".into(),
}),
),
SeroError::UserHasAlreadyBeenRegistered => (
StatusCode::CONFLICT,
Json(Details {
details: "User with this username has already been registered!".into(),
}),
),
SeroError::SubdomainWasNotFound(subdomain_name) => (
StatusCode::NOT_FOUND,
Json(Details {
details: format!("Subdomain with name {subdomain_name} was not found!"),
}),
),
SeroError::ArchiveFileWasNotFoundForSubdomain(subdomain_name) => (
StatusCode::NOT_FOUND,
Json(Details {
details: format!("Archive file was not found for subdomain {subdomain_name}"),
}),
),
SeroError::MaxSitesPerUserLimitExceeded => (
StatusCode::FORBIDDEN,
Json(Details {
details: "Max sites per this user limit exceeded!".into(),
}),
),
SeroError::SiteDisabled => (
StatusCode::SERVICE_UNAVAILABLE,
Json(Details {
details: "Service is currently unavailable!".into(),
}),
),
SeroError::EmptyCredentials => (
StatusCode::BAD_REQUEST,
Json(Details {
details: "Username or password is empty!".into(),
}),
),
};
"Some error occurred on the server!".to_string()
},
SeroError::UserWasNotFoundUsingJwt => "User with id from jwt token was not found!".to_string(),
SeroError::RegisteredUserLimitExceeded => "Registered user limit exceeded!".to_string(),
SeroError::Unauthorized => "Unauthorized! Bad credentials were provided!".to_string(),
SeroError::UserHasAlreadyBeenRegistered => "User with this username has already been registered!".to_string(),
SeroError::SubdomainWasNotFound(subdomain_name) => format!("Subdomain with name {} was not found!", subdomain_name),
SeroError::ArchiveFileWasNotFoundForSubdomain(subdomain_name) => format!("Archive file was not found for subdomain {}", subdomain_name),
SeroError::MaxSitesPerUserLimitExceeded => "Max sites per this user limit exceeded!".to_string(),
SeroError::SiteDisabled => "Site is disabled!".to_string(),
SeroError::EmptyCredentials => "Empty credentials were provided!".to_string(),
})
}
}
impl std::fmt::Display for SeroError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
impl From<&SeroError> for StatusCode {
fn from(val: &SeroError) -> Self {
match val {
SeroError::XSubdomainHeaderMissing => StatusCode::BAD_REQUEST,
SeroError::AuthorizationHeaderMissing => StatusCode::BAD_REQUEST,
SeroError::AuthorizationHeaderBadSchema => StatusCode::BAD_REQUEST,
SeroError::SubdomainIsOwnedByAnotherUser(_) => StatusCode::FORBIDDEN,
SeroError::AuthorizationHeaderBabChars => StatusCode::BAD_REQUEST,
SeroError::InternalServerError(_) => StatusCode::INTERNAL_SERVER_ERROR,
SeroError::UserWasNotFoundUsingJwt => StatusCode::UNAUTHORIZED,
SeroError::RegisteredUserLimitExceeded => StatusCode::FORBIDDEN,
SeroError::Unauthorized => StatusCode::UNAUTHORIZED,
SeroError::UserHasAlreadyBeenRegistered => StatusCode::CONFLICT,
SeroError::SubdomainWasNotFound(_) => StatusCode::NOT_FOUND,
SeroError::ArchiveFileWasNotFoundForSubdomain(_) => StatusCode::NOT_FOUND,
SeroError::MaxSitesPerUserLimitExceeded => StatusCode::FORBIDDEN,
SeroError::SiteDisabled => StatusCode::SERVICE_UNAVAILABLE,
SeroError::EmptyCredentials => StatusCode::BAD_REQUEST,
}
}
}
impl IntoResponse for SeroError {
fn into_response(self) -> axum::response::Response {
let response = (
Into::<StatusCode>::into(&self),
Json(Details {
details: format!("{:?}", self),
}),
);
tracing::error!(cause = response.1.details, "Response with error!");
response.into_response()
}

View file

@ -13,7 +13,7 @@ impl Default for Config {
let mut config: Self =
envy::from_env().expect("Failed to read config from environment variables!");
if config.jwt_secret.is_none() {
config.jwt_secret = Some(uuid::Uuid::new_v4().to_string())
config.jwt_secret = Some(uuid::Uuid::new_v4().to_string());
}
config
}

View file

@ -1,7 +1,7 @@
use axum::{
async_trait,
extract::{FromRef, FromRequestParts},
http::request::Parts,
http::{request::Parts, HeaderMap},
};
use sea_orm::prelude::*;
use std::sync::Arc;
@ -59,19 +59,10 @@ where
}
}
#[async_trait]
impl<S> FromRequestParts<S> for Subdomain
where
Arc<crate::AppState>: FromRef<S>,
S: Send + Sync,
{
type Rejection = SeroError;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
impl Subdomain {
pub fn from_headers(headers: &HeaderMap) -> Result<Self, SeroError> {
Ok(Self({
let header = parts
.headers
let header = headers
.get("X-Subdomain")
.ok_or(SeroError::XSubdomainHeaderMissing)?
.to_str()
@ -86,6 +77,20 @@ where
}
}
#[async_trait]
impl<S> FromRequestParts<S> for Subdomain
where
Arc<crate::AppState>: FromRef<S>,
S: Send + Sync,
{
type Rejection = SeroError;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
Self::from_headers(&parts.headers)
}
}
#[async_trait]
impl<S> FromRequestParts<S> for SubdomainModel
where
@ -96,10 +101,20 @@ where
#[tracing::instrument(skip(parts, state))]
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
Self::from_headers(&parts.headers, state).await
}
}
impl SubdomainModel {
pub async fn from_headers<S>(headers: &HeaderMap, state: &S) -> Result<Self, SeroError>
where
Arc<crate::AppState>: FromRef<S>,
S: Send + Sync,
{
let app_state = Arc::from_ref(state);
let subdomain_name = Subdomain::from_request_parts(parts, state).await?.0;
Ok(match entity::prelude::SubdomainEntity::find()
let subdomain_name = Subdomain::from_headers(headers)?.0;
match entity::prelude::SubdomainEntity::find()
.filter(entity::prelude::SubdomainColumn::Name.eq(&subdomain_name))
.one(&app_state.connection)
.await
@ -107,7 +122,7 @@ where
Ok(Some(subdomain)) => Ok(Self(subdomain)),
Ok(None) => Err(SeroError::SubdomainWasNotFound(subdomain_name)),
Err(cause) => Err(SeroError::InternalServerError(Box::new(cause))),
}?)
}
}
}

66
src/handlers/cors.rs Normal file
View file

@ -0,0 +1,66 @@
use crate::{
apperror::SeroError,
extractors::{AuthJWT, SubdomainModel},
AppState,
};
use axum::{
extract::State,
http::StatusCode,
response::{IntoResponse, Response},
Form,
};
use entity::prelude::*;
use sea_orm::{prelude::*, Set};
use std::sync::Arc;
#[derive(Debug, serde::Deserialize)]
pub struct OriginForm {
origin: String,
}
#[tracing::instrument(skip(state))]
pub async fn add_origin(
State(state): State<Arc<AppState>>,
SubdomainModel(subdomain_model): SubdomainModel,
AuthJWT(user): AuthJWT,
Form(origin_form): Form<OriginForm>,
) -> Response {
if subdomain_model.owner_id != user.id {
return SeroError::SubdomainIsOwnedByAnotherUser(subdomain_model.name).into_response();
}
let active_cors_origin = ActiveCors {
origin: Set(origin_form.origin),
subdomain_id: Set(subdomain_model.id),
..Default::default()
};
match CorsEntity::insert(active_cors_origin)
.exec(&state.connection)
.await
{
Ok(_) => StatusCode::NO_CONTENT.into_response(),
Err(cause) => SeroError::InternalServerError(Box::new(cause)).into_response(),
}
}
#[tracing::instrument(skip(state))]
pub async fn clear_all(
State(state): State<Arc<AppState>>,
SubdomainModel(subdomain_model): SubdomainModel,
AuthJWT(user): AuthJWT,
) -> Response {
if subdomain_model.owner_id != user.id {
return SeroError::SubdomainIsOwnedByAnotherUser(subdomain_model.name).into_response();
}
match CorsEntity::delete_many()
.filter(CorsColumn::SubdomainId.eq(subdomain_model.id))
.exec(&state.connection)
.await
{
Ok(_) => StatusCode::NO_CONTENT.into_response(),
Err(cause) => SeroError::InternalServerError(Box::new(cause)).into_response(),
}
}

View file

@ -1,2 +1,3 @@
pub mod auth;
pub mod cors;
pub mod sites;

View file

@ -1,4 +1,4 @@
use std::{fmt::Debug, net::SocketAddr};
use std::{fmt::Debug, net::SocketAddr, sync::mpsc};
use axum::{
extract::DefaultBodyLimit,
@ -6,6 +6,11 @@ use axum::{
routing::{get, post},
Router,
};
use extractors::SubdomainModel;
use services::cors::CorsService;
use tower_http::cors::{AllowHeaders, AllowMethods, AllowOrigin, CorsLayer};
use migration::{Migrator, MigratorTrait};
use sea_orm::{ConnectOptions, Database};
@ -39,13 +44,62 @@ async fn app() -> Router {
.route("/teardown", post(handlers::sites::teardown))
.route("/download", post(handlers::sites::download))
.route("/enable", post(handlers::sites::enable))
.route("/disable", post(handlers::sites::disable));
.route("/disable", post(handlers::sites::disable))
.route("/cors/add", post(handlers::cors::add_origin))
.route("/cors/clear", post(handlers::cors::clear_all));
let state = std::sync::Arc::new(AppState {
connection,
config: Default::default(),
});
let cloned_state = state.clone();
let cors_layer = CorsLayer::new()
.allow_methods(AllowMethods::any())
.allow_headers(AllowHeaders::any())
.allow_origin(AllowOrigin::predicate(move |origin, parts| {
let cloned_headers = parts.headers.clone();
let (tx, rx) = mpsc::channel();
let cloned_state = cloned_state.clone();
let cloned_origin = origin
.to_owned()
.to_str()
.map(|s| s.to_string())
.unwrap_or_default();
std::thread::spawn(move || {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async move {
tracing::info!("Starting cors!");
let subdomain_model_extractor =
SubdomainModel::from_headers(&cloned_headers, &cloned_state)
.await
.map_err(|cause| {
tracing::error!(%cause,
"Failed to extract subdomain model from headers for cors!");
});
if subdomain_model_extractor.is_err() {
tx.send(false).ok();
return;
}
let res = CorsService::check(
subdomain_model_extractor.unwrap().0,
&cloned_origin,
&cloned_state.connection,
)
.await
.unwrap_or(false);
tx.send(res).ok();
});
});
rx.recv().unwrap_or(false)
}));
let mut app = Router::new()
.nest("/api", api_router)
.route("/*path", get(handlers::sites::file))

22
src/services/cors.rs Normal file
View file

@ -0,0 +1,22 @@
use entity::prelude::*;
use sea_orm::{ConnectionTrait, DbErr, ModelTrait};
use sea_orm::TransactionTrait;
pub struct CorsService;
impl CorsService {
#[tracing::instrument(skip(connection))]
pub async fn check<T: ConnectionTrait + TransactionTrait>(
subdomain: Subdomain,
origin: &str,
connection: &T,
) -> Result<bool, DbErr> {
Ok(subdomain
.find_related(CorsEntity)
.all(connection)
.await?
.iter()
.any(|origin_model| origin_model.matches(origin)))
}
}

View file

@ -1,4 +1,5 @@
pub mod archive;
pub mod auth;
pub mod cors;
pub mod sites;
pub mod users;

View file

@ -88,6 +88,11 @@ impl SitesService {
}
};
CorsEntity::delete_many()
.filter(CorsColumn::SubdomainId.eq(subdomain.id))
.exec(connection)
.await?;
match new_archive_file.write_all(&contents).await {
Ok(()) => {
let mut active: ActiveSubdomain = subdomain.clone().into();