#web-framework #async-http #actix-web #async #web-apps #framework #web

actix-cloud

Actix Cloud is an all-in-one web framework based on Actix Web

14 unstable releases (3 breaking)

0.4.10 Nov 22, 2024
0.4.9 Nov 15, 2024
0.4.4 Oct 22, 2024
0.4.1 Aug 30, 2024
0.1.0 Aug 13, 2024

#306 in HTTP server

Download history 240/week @ 2024-08-12 18/week @ 2024-08-19 645/week @ 2024-08-26 38/week @ 2024-09-02 34/week @ 2024-09-09 50/week @ 2024-09-16 104/week @ 2024-09-23 14/week @ 2024-09-30 165/week @ 2024-10-07 193/week @ 2024-10-14 220/week @ 2024-10-21 5/week @ 2024-10-28 217/week @ 2024-11-04 452/week @ 2024-11-11 229/week @ 2024-11-18 76/week @ 2024-11-25

974 downloads per month
Used in 3 crates

MIT license

120KB
2.5K SLoC

actix-cloud

Actix Cloud is an all-in-one web framework based on Actix Web.

Features

Actix Cloud is highly configurable. You can only enable needed features, implement your own feature backend or even use other libraries.

Guide

Quick Start

You can refer to Hello world example for basic usage.

Application

Since application configuration can be quite dynamic, you need to build on your own. Here are some useful middlewares:

App::new()
    .wrap(middleware::Compress::default()) // compress page
    .wrap(SecurityHeader::default().build()) // default security header
    .wrap(SessionMiddleware::builder(memorydb.clone(), Key::generate()).build()) // session
    ...
    .app_data(state_cloned.clone())

logger

We use tracing as our logger library. It is thread safe. You can use it everywhere.

Start logger:

LoggerBuilder::new().level(Level::DEBUG).start()        // colorful output
LoggerBuilder::new().json().start() // json output

You can also customize the logger with filter, transformer, etc.

Reinit logger (e.g., in plugins), or manually send logs:

logger.init(LoggerBuilder::new());
logger.sender().send(...);

Reserved field:

  • _time: timestamp in microseconds, override the log timestamp.

i18n

We use rust-i18n-support from rust-i18n as our i18n core.

Load locale:

let locale = Locale::new("en-US").add_locale(i18n!("locale"));

Translate:

t!(locale, "hello.world")
t!(locale, "hello.name", name = "MEME")

See examples for more usage.

security

Middleware to add security headers:

app.wrap(SecurityHeader::default().build())

Default header:

X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
Cross-Origin-Opener-Policy: same-origin
Content-Security-Policy: default-src 'none'; script-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'

Enable HSTS when using HTTPS:

security_header.set_default_hsts();
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

memorydb-default

Actix Cloud has a default memory database backend used for sessions. You can also use your own backend if you implement actix_cloud::memorydb::MemoryDB.

Note: the default backend does not have memory limitation, DDoS is possible if gateway rate limiting is not implemented

DefaultBackend::new()

memorydb-redis

Redis can be used as another backend for memory database.

RedisBackend::new("redis://user:pass@127.0.0.1:6379/0").await.unwrap(),

auth

Authentication is quite simple, you only need to implement a checker.

Checker is used to check the permission, the server will return 403 if the return value is false:

struct AuthChecker {
    need_admin: bool,
}

impl AuthChecker {
    fn new(need_admin: bool) -> Self {
        Self { need_admin }
    }
}

#[async_trait(?Send)]
impl Checker for AuthChecker {
    async fn check(&self, req: &mut ServiceRequest) -> Result<bool> {
        let qs = QString::from(req.query_string());
        let is_admin = if qs.get("admin").is_some_and(|x| x == "1") {
            true
        } else {
            false
        };
        if (is_admin && self.need_admin) || !self.need_admin {
            Ok(true)
        } else {
            Ok(false)
        }
    }
}

Then build the Router and configure in the App using build_router:

app.service(scope("/api").configure(build_router(...)))

session

Most features and usages are based on actix-session. Except for these:

  • MemoryDB is the only supported storage.
  • Error uses actix-cloud::error::Error.
  • You can set _ttl in the session to override the TTL of the session.
  • You can set _id in the session for reverse search.
    • Quote(") will be trimmed.
    • Another key will be set in memorydb: {_id}_{session_key}. You can use keys function to find all session key binding to a specific id.
app.wrap(SessionMiddleware::builder(memorydb.clone(), Key::generate()).build())

config

config-rs is the underlying library.

Supported features:

  • config-json: Support for JSON files.
  • config-yaml: Support for YAML files.
  • config-toml: Support for TOML files.

request

Provide per-request extension.

Built-in middleware:

  • Store in extensions.
  • If i18n feature is enabled, language is identified through the callback, or locale.default in GlobalState.

Enable built-in middleware:

app.wrap(request::Middleware::new())

Usage:

async fn handler(req: HttpRequest) -> impl Responder {
    let ext = req.extensions();
    let ext = ext.get::<Arc<actix_cloud::request::Extension>>().unwrap();
    ...
}

async fn handler(ext: ReqData<Arc<actix_cloud::request::Extension>>) -> impl Responder {
    ...
}

response

Provide useful response type.

If i18n feature is enabled, response message will be translated automatically.

If response-json feature is enabled, response message will be converted to JSON automatically.

  1. Create response yml files.
  2. Use build.rs to generate source files.
  3. Use include! to include generated files.

See examples for detailed usage.

traceid

Add trace ID for each request based on tracing-actix-web.

app.wrap(request::Middleware::new())
   .wrap(TracingLogger::default())      // This should be after request::Middleware

If you enable request feature, make sure it is before TracingLogger since the trace_id field is based on it.

seaorm

Provide useful macros for seaorm.

#[derive(...)]
#[sea_orm(...)]
pub struct Model {
    #[sea_orm(primary_key, auto_increment = false)]
    pub id: Uuid,
    pub created_at: i64,
    pub updated_at: i64,
}

#[entity_id(Uuid::new_v4())]    // generate new for `id` field.
#[entity_timestamp]             // automatically handle `created_at` and `updated_at` field.
impl ActiveModel {}

#[entity_behavior]              // enable `entity_id` and `entity_timestamp`.
impl ActiveModelBehavior for ActiveModel {}

csrf

We use double submit to protect against CSRF attacks.

You can use memorydb to store and check CSRF tokens.

By default, CSRF checker is applied to:

  • All unsafe methods unless CSRFType is Disabled.
  • All methods if CSRFType is ForceHeader or ForceParam.

Generally, Param and ForceParam type should only be used for websocket.

build_router(
    route,
    csrf::Middleware::new(
        String::from("CSRF_TOKEN"),     // csrf cookie
        String::from("X-CSRF-Token"),   // csrf header/param
        |req, token| Box::pin(async { Ok(true) })          // csrf checker
    ),
);

License

This project is licensed under the MIT license.

Dependencies

~15–30MB
~576K SLoC