Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Taskfile.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ tasks:
test:examples:
dir: ./examples
cmds:
- cargo test
- chmod +x ./test.sh
- ./test.sh

test:samples:
dir: ./samples
Expand Down
1 change: 1 addition & 0 deletions examples/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
resolver = "3"
members = [
"sse",
"jwt",
"form",
"hello",
"chatgpt",
Expand Down
1 change: 1 addition & 0 deletions examples/jwt/.env.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
JWT_SECRET=your-jwt-secret-key
1 change: 1 addition & 0 deletions examples/jwt/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.env
9 changes: 9 additions & 0 deletions examples/jwt/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[package]
name = "jwt"
version = "0.1.0"
edition = "2024"

[dependencies]
ohkami = { workspace = true }
tokio = { workspace = true }
dotenvy = "0.15"
105 changes: 105 additions & 0 deletions examples/jwt/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
use ohkami::prelude::*;
use ohkami::fang::{JWT, JWTToken};

fn jwt() -> JWT<JwtPayload> {
JWT::default(std::env::var("JWT_SECRET").unwrap())
}

#[derive(Serialize, Deserialize)]
struct JwtPayload {
sub: String,
exp: u64,
}

trait JwtSub: 'static {
fn sub() -> String;
}

struct DefaultJwtSub;
impl JwtSub for DefaultJwtSub {
fn sub() -> String {"ohkami".to_string()}
}

#[derive(Serialize)]
#[cfg_attr(test, derive(Deserialize))]
struct AuthResponse {
token: JWTToken,
}

async fn auth<S: JwtSub>() -> JSON<AuthResponse> {
let token = jwt().issue(JwtPayload {
sub: S::sub(),
exp: ohkami::util::unix_timestamp() + 86400,
});

JSON(AuthResponse { token })
}

async fn private(
Context(_): Context<'_, JwtPayload>,
) -> &'static str {
"Hello, private!"
}

fn ohkami<S: JwtSub>() -> Ohkami {
Ohkami::new((
"/auth".GET(auth::<S>),
"/private".GET((jwt(), private)),
))
}

#[tokio::main]
async fn main() {
dotenvy::dotenv().ok();
ohkami::<DefaultJwtSub>().howl("0.0.0.0:3000").await
}

#[cfg(test)]
mod test {
use super::*;
use ohkami::testing::*;

/// regression test for https://github.com/ohkami-rs/ohkami/issues/433
///
/// run with `OHKAMI_REQUEST_BUFSIZE=4096` or larger
#[tokio::test]
async fn test_large_jwt() {
struct LargeJwtSub;
impl JwtSub for LargeJwtSub {
fn sub() -> String {
const SENTENCE: &'static str = "\
Lorem ipsum dolor sit amet, consectetur adipiscing elit. \
Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris \
nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in \
reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla \
pariatur. Excepteur sint occaecat cupidatat non proident, sunt in \
culpa qui officia deserunt mollit anim id est laborum.\
";

let sub = SENTENCE.repeat((1 << 11) / SENTENCE.len() + 1);

// `sub` itself is already larger than default `request_bufsize`
assert!(sub.len() > (1 << 11));

sub
}
}

dotenvy::dotenv().ok();

let t = ohkami::<LargeJwtSub>().test();

let req = TestRequest::GET("/auth");
let res = t.oneshot(req).await;
let AuthResponse { token } = res.json()
.expect("`/auth` response doesn't contain a token")
.expect("`/auth` response is not `AuthResponse`");

let req = TestRequest::GET("/private")
.header("Authorization", format!("Bearer {token}"));
let res = t.oneshot(req).await;
assert_eq!(res.status().code(), 200);
assert_eq!(res.text(), Some("Hello, private!"));
}
}
12 changes: 12 additions & 0 deletions examples/test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
set -Cue

EXAMPLES=$(pwd)

cd $EXAMPLES/jwt && \
cp .env.sample .env
cd $EXAMPLES/jwt && \
cargo test 2>&1 | grep 'Unexpected end of headers' \
&& echo '---> expected error' \
|| exit 1
cd $EXAMPLES/jwt && \
OHKAMI_REQUEST_BUFSIZE=4096 cargo test
27 changes: 25 additions & 2 deletions ohkami/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
pub(crate) struct Config {
#[cfg(feature="__rt_native__")]
request_bufsize: std::sync::LazyLock<usize>,

#[cfg(feature="__rt_native__")]
keepalive_timeout: std::sync::LazyLock<u64>,

Expand All @@ -9,6 +12,13 @@ pub(crate) struct Config {

impl Config {
#[cfg(feature="__rt_native__")]
#[inline]
pub(crate) fn request_bufsize(&self) -> usize {
*(&*self.request_bufsize)
}

#[cfg(feature="__rt_native__")]
#[inline]
pub(crate) fn keepalive_timeout(&self) -> u64 {
*(&*self.keepalive_timeout)
}
Expand All @@ -23,15 +33,28 @@ impl Config {
impl Config {
pub(super) const fn new() -> Self {
Self {
#[cfg(feature="__rt_native__")]
request_bufsize: std::sync::LazyLock::new(|| std::env::var("OHKAMI_REQUEST_BUFSIZE")
.ok()
.map(|v| v.parse().ok())
.flatten()
.unwrap_or(1 << 11)
),

#[cfg(feature="__rt_native__")]
keepalive_timeout: std::sync::LazyLock::new(|| std::env::var("OHKAMI_KEEPALIVE_TIMEOUT")
.ok().map(|v| v.parse().ok()).flatten()
.ok()
.map(|v| v.parse().ok())
.flatten()
.unwrap_or(42)
),

#[cfg(feature="__rt_native__")]
#[cfg(feature="ws")]
websocket_timeout: std::sync::LazyLock::new(|| std::env::var("OHKAMI_WEBSOCKET_TIMEOUT")
.ok().map(|v| v.parse().ok()).flatten()
.ok()
.map(|v| v.parse().ok())
.flatten()
.unwrap_or(42)
),
}
Expand Down
4 changes: 2 additions & 2 deletions ohkami/src/fang/builtin/jwt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,7 @@ impl<Payload: for<'de> Deserialize<'de>> JWT<Payload> {
let mut req = unsafe {Pin::new_unchecked(&mut req)};
crate::__rt__::testing::block_on({
req.as_mut().read(&mut req_bytes)
});
}).unwrap();

assert_eq!(
my_jwt.verified(&req.as_ref()).unwrap(),
Expand All @@ -462,7 +462,7 @@ impl<Payload: for<'de> Deserialize<'de>> JWT<Payload> {
let mut req = unsafe {Pin::new_unchecked(&mut req)};
crate::__rt__::testing::block_on({
req.as_mut().read(&mut req_bytes)
});
}).unwrap();

assert_eq!(
my_jwt.verified(&req.as_ref()).unwrap_err().status,
Expand Down
18 changes: 10 additions & 8 deletions ohkami/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,27 +149,29 @@ mod __rt__ {

#[cfg(test)]
pub(crate) mod testing {
pub(crate) fn block_on(future: impl std::future::Future) {
pub(crate) fn block_on<F: Future>(future: F) -> F::Output {
#[cfg(feature="rt_tokio")]
tokio::runtime::Builder::new_current_thread()
return tokio::runtime::Builder::new_current_thread()
.enable_all()
.build().unwrap()
.build()
.unwrap()
.block_on(future);

#[cfg(feature="rt_async-std")]
async_std::task::block_on(future);
return async_std::task::block_on(future);

#[cfg(feature="rt_smol")]
smol::block_on(future);
return smol::block_on(future);

#[cfg(feature="rt_nio")]
nio::runtime::Builder::new_multi_thread()
return nio::runtime::Builder::new_multi_thread()
.enable_all()
.build().unwrap()
.build()
.unwrap()
.block_on(future);

#[cfg(feature="rt_glommio")]
glommio::LocalExecutor::default().run(future);
return glommio::LocalExecutor::default().run(future);
}

pub(crate) const PORT: u16 = {
Expand Down
85 changes: 44 additions & 41 deletions ohkami/src/request/_test_parse.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
#![cfg(all(test, feature="__rt_native__", feature="DEBUG"))]

#[allow(unused)]
use super::{Request, Method, BUF_SIZE, Path, QueryParams, Context};
use super::{Request, Method, Path, QueryParams, Context};

use super::{RequestHeader, RequestHeaders};
use std::pin::Pin;
use ohkami_lib::{Slice, CowSlice};

#[test]
fn parse_path() {
Expand All @@ -18,48 +22,47 @@ fn parse_path() {
assert_eq!(&*path, "/");
}

#[test] fn test_parse_request() {
use super::{RequestHeader, RequestHeaders};
use std::pin::Pin;
use ohkami_lib::{Slice, CowSlice};

fn metadataize(input: &str) -> Box<[u8; BUF_SIZE]> {
let mut buf = [0; BUF_SIZE];
buf[..input.len().min(BUF_SIZE)]
.copy_from_slice(&input.as_bytes()[..input.len().min(BUF_SIZE)]);
Box::new(buf)
}
macro_rules! assert_parse {
($case:expr, $expected:expr) => {
let mut case = $case.as_bytes();

macro_rules! assert_parse {
($case:expr, $expected:expr) => {
let mut case = $case.as_bytes();

let mut actual = Request::init(crate::util::IP_0000);
let mut actual = unsafe {Pin::new_unchecked(&mut actual)};

crate::__rt__::testing::block_on({
actual.as_mut().read(&mut case)
});

let expected = $expected;

println!("<assert_parse>");

let __panic_message = format!("\n\
===== actual =====\n\
{actual:#?}\n\
\n\
===== expected =====\n\
{expected:#?}\n\
\n\
");

if actual.get_mut() != &expected {
panic!("{__panic_message}")
}
};
}
let mut actual = Request::init(crate::util::IP_0000);
let mut actual = unsafe {Pin::new_unchecked(&mut actual)};

let result = crate::__rt__::testing::block_on({
actual.as_mut().read(&mut case)
});

assert_eq!(result, Ok(Some(())));

let expected = $expected;

println!("<assert_parse>");

let __panic_message = format!("\n\
===== actual =====\n\
{actual:#?}\n\
\n\
===== expected =====\n\
{expected:#?}\n\
\n\
");

if actual.get_mut() != &expected {
panic!("{__panic_message}")
}
};
}

fn metadataize(input: &str) -> Box<[u8]> {
let buf_size = crate::CONFIG.request_bufsize();
let mut buf = vec![0; buf_size];
buf[..input.len().min(buf_size)]
.copy_from_slice(&input.as_bytes()[..input.len().min(buf_size)]);
buf.into_boxed_slice()
}

#[test] fn test_parse_request() {
const CASE_1: &str = "\
GET /hello.html HTTP/1.1\r\n\
User-Agent: Mozilla/4.0\r\n\
Expand Down
Loading