From 3f9efe0ae85689b8ee6d04f966a45e595034806f Mon Sep 17 00:00:00 2001 From: vshakitskiy Date: Fri, 8 May 2026 23:48:23 +0300 Subject: [PATCH] example changes --- README.md | 49 ++++++++++++++++++----------- examples/src/serving_files.gleam | 53 ++++++++++++++++++++++---------- 2 files changed, 67 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index c9ad9b8..f9f5434 100644 --- a/README.md +++ b/README.md @@ -235,30 +235,43 @@ Static files can be sent using [`ewe.file`](https://hexdocs.pm/ewe/ewe.html#file ```gleam import gleam/http/response -import gleam/option.{None} - -import ewe.{type Response} +import gleam/string fn serve_file(path: String) -> Response { - // Load file from disk using ewe.file(). This efficiently streams the file - // content without loading it entirely into memory. - // - // In production, make sure to validate paths to prevent directory traversal - // attacks! (e.g., requests to "../../../etc/passwd") + // Resolve the URL path against the `public` directory and confirm the result + // stays inside it. // - case ewe.file("public" <> path, offset: None, limit: None) { - Ok(file) -> { - response.new(200) - |> response.set_header("content-type", "application/octet-stream") - |> response.set_body(file) - } - Error(_) -> { - response.new(404) - |> response.set_header("content-type", "text/plain; charset=utf-8") - |> response.set_body(ewe.TextData("File not found")) + let dir = absname("public") + let relative = string.drop_start(path, 1) + let resolved = absname_join(dir, relative) + + case string.starts_with(resolved, dir <> "/") { + True -> { + // Load file from disk using ewe.file(). This efficiently streams the file + // content without loading it entirely into memory. + // + case ewe.file(resolved, offset: None, limit: None) { + Ok(file) -> { + // Using "application/octet-stream" is safe for any file type, but you + // may want to specify content-type based on file extension in + // production. + // + response.new(200) + |> response.set_header("content-type", "application/octet-stream") + |> response.set_body(file) + } + Error(_) -> not_found() + } } + False -> not_found() } } + +@external(erlang, "filename", "absname") +fn absname(path: String) -> String + +@external(erlang, "filename", "absname_join") +fn absname_join(dir: String, file: String) -> String ``` ### [WebSocket](examples/src/websocket.gleam) diff --git a/examples/src/serving_files.gleam b/examples/src/serving_files.gleam index 09f1a22..05d45da 100644 --- a/examples/src/serving_files.gleam +++ b/examples/src/serving_files.gleam @@ -2,6 +2,7 @@ import ewe.{type Response} import gleam/erlang/process import gleam/http/response import gleam/option.{None} +import gleam/string import logging pub fn main() { @@ -20,25 +21,43 @@ pub fn main() { } fn serve_file(path: String) -> Response { - // Load file from disk using ewe.file(). This efficiently streams the file - // content without loading it entirely into memory. + // Resolve the URL path against the `public` directory and confirm the result + // stays inside it. // - // In production, make sure to validate paths to prevent directory traversal - // attacks! (e.g., requests to "../../../etc/passwd") - // - case ewe.file("public/" <> path, offset: None, limit: None) { - Ok(file) -> { - // Using "application/octet-stream" is safe for any file type, but you - // may want to specify content-type based on file extension in production. + let dir = absname("public") + let relative = string.drop_start(path, 1) + let resolved = absname_join(dir, relative) + + case string.starts_with(resolved, dir <> "/") { + True -> { + // Load file from disk using ewe.file(). This efficiently streams the file + // content without loading it entirely into memory. // - response.new(200) - |> response.set_header("content-type", "application/octet-stream") - |> response.set_body(file) - } - Error(_) -> { - response.new(404) - |> response.set_header("content-type", "text/plain; charset=utf-8") - |> response.set_body(ewe.TextData("File not found")) + case ewe.file(resolved, offset: None, limit: None) { + Ok(file) -> { + // Using "application/octet-stream" is safe for any file type, but you + // may want to specify content-type based on file extension in + // production. + // + response.new(200) + |> response.set_header("content-type", "application/octet-stream") + |> response.set_body(file) + } + Error(_) -> not_found() + } } + False -> not_found() } } + +fn not_found() -> Response { + response.new(404) + |> response.set_header("content-type", "text/plain; charset=utf-8") + |> response.set_body(ewe.TextData("File not found")) +} + +@external(erlang, "filename", "absname") +fn absname(path: String) -> String + +@external(erlang, "filename", "absname_join") +fn absname_join(dir: String, file: String) -> String -- 2.51.2