import exception import gleam/string_builder.{StringBuilder} import gleam/bit_builder import gleam/bit_string import gleam/erlang import gleam/base import gleam/bool import gleam/crypto import gleam/http.{Method} import gleam/http/request.{Request as HttpRequest} import gleam/http/response.{Response as HttpResponse} import gleam/list import gleam/result import gleam/string import gleam/option.{Option} import gleam/uri import gleam/int import simplifile import mist import wisp/internal/logger // // Running the server // /// Convert a Wisp request handler into a function that can be run with the Mist /// web server. /// /// # Examples /// /// ```gleam /// pub fn main() { /// let secret_key_base = "..." /// let assert Ok(_) = /// handle_request /// |> wisp.mist_service(secret_key_base) /// |> mist.new /// |> mist.port(8000) /// |> mist.start_http /// process.sleep_forever() /// } /// ``` pub fn mist_service( handler: fn(Request) -> Response, secret_key_base: String, ) -> fn(HttpRequest(mist.Connection)) -> HttpResponse(mist.ResponseData) { fn(request: HttpRequest(_)) { let connection = make_connection(mist_body_reader(request), secret_key_base) let request = request.set_body(request, connection) use <- exception.defer(fn() { let assert Ok(_) = delete_temporary_files(request) }) let response = request |> handler |> mist_response response } } fn mist_body_reader(request: HttpRequest(mist.Connection)) -> Reader { case mist.stream(request) { Error(_) -> fn(_) { Ok(ReadingFinished) } Ok(stream) -> fn(size) { wrap_mist_chunk(stream(size)) } } } fn wrap_mist_chunk( chunk: Result(mist.Chunk, mist.ReadError), ) -> Result(Read, Nil) { chunk |> result.nil_error |> result.map(fn(chunk) { case chunk { mist.Done -> ReadingFinished mist.Chunk(data, consume) -> Chunk(data, fn(size) { wrap_mist_chunk(consume(size)) }) } }) } fn mist_response(response: Response) -> HttpResponse(mist.ResponseData) { let body = case response.body { Empty -> mist.Bytes(bit_builder.new()) Text(text) -> mist.Bytes(bit_builder.from_string_builder(text)) File(path) -> mist_send_file(path) } response |> response.set_body(body) } fn mist_send_file(path: String) -> mist.ResponseData { case mist.send_file(path, offset: 0, limit: option.None) { Ok(body) -> body Error(error) -> { log_error(string.inspect(error)) // TODO: return 500 mist.Bytes(bit_builder.new()) } } } // // Responses // /// The body of a HTTP response, to be sent to the client. /// pub type Body { /// A body of unicode text. /// /// The body is represented using a `StringBuilder`. If you have a `String` /// you can use the `string_builder.from_string` function to convert it. /// Text(StringBuilder) /// A body of the contents of a file. /// /// This will be sent efficiently using the `send_file` function of the /// underlying HTTP server. The file will not be read into memory so it is /// safe to send large files this way. /// File(path: String) /// An empty body. This may be returned by the `require_*` middleware /// functions in the event of a failure, invalid request, or other situation /// in which the request cannot be processed. /// /// Your application may wish to use a middleware to provide default responses /// in place of any with an empty body. /// Empty } /// An alias for a HTTP response containing a `Body`. pub type Response = HttpResponse(Body) /// Create an empty response with the given status code. /// /// # Examples /// /// ```gleam /// response(200) /// // -> Response(200, [], Empty) /// ``` /// pub fn response(status: Int) -> Response { HttpResponse(status, [], Empty) } /// Set the body of a response. /// /// # Examples /// /// ```gleam /// response(200) /// |> set_body(File("/tmp/myfile.txt")) /// // -> Response(200, [], File("/tmp/myfile.txt")) /// ``` /// pub fn set_body(response: Response, body: Body) -> Response { response |> response.set_body(body) } /// Create a HTML response. /// /// The body is expected to be valid HTML, though this is not validated. /// The `content-type` header will be set to `text/html`. /// /// # Examples /// /// ```gleam /// let body = string_builder.from_string("

Hello, Joe!

") /// html_response(body, 200) /// // -> Response(200, [#("content-type", "text/html")], Text(body)) /// ``` /// pub fn html_response(html: StringBuilder, status: Int) -> Response { HttpResponse(status, [#("content-type", "text/html")], Text(html)) } /// Set the body of a response to a given HTML document, and set the /// `content-type` header to `text/html`. /// /// The body is expected to be valid HTML, though this is not validated. /// /// # Examples /// /// ```gleam /// let body = string_builder.from_string("

Hello, Joe!

") /// response(201) /// |> html_body(body) /// // -> Response(201, [#("content-type", "text/html")], Text(body)) /// ``` /// pub fn html_body(response: Response, html: StringBuilder) -> Response { response |> response.set_body(Text(html)) |> response.set_header("content-type", "text/html") } /// Create an empty response with status code 405: Method Not Allowed. Use this /// when a request does not have an appropriate method to be handled. /// /// The `allow` header will be set to a comma separated list of the permitted /// methods. /// /// # Examples /// /// ```gleam /// method_not_allowed([Get, Post]) /// // -> Response(405, [#("allow", "GET, POST")], Empty) /// ``` /// pub fn method_not_allowed(permitted: List(Method)) -> Response { let allowed = permitted |> list.map(http.method_to_string) |> list.sort(string.compare) |> string.join(", ") |> string.uppercase HttpResponse(405, [#("allow", allowed)], Empty) } /// Create an empty response with status code 200: OK. /// /// # Examples /// /// ```gleam /// ok() /// // -> Response(200, [], Empty) /// ``` /// pub fn ok() -> Response { HttpResponse(200, [], Empty) } /// Create an empty response with status code 201: Created. /// /// # Examples /// /// ```gleam /// created() /// // -> Response(201, [], Empty) /// ``` /// pub fn created() -> Response { HttpResponse(201, [], Empty) } /// Create an empty response with status code 202: Accepted. /// /// # Examples /// /// ```gleam /// created() /// // -> Response(202, [], Empty) /// ``` /// pub fn accepted() -> Response { HttpResponse(202, [], Empty) } /// Create an empty response with status code 303: See Other, and the `location` /// header set to the given URL. Used to redirect the client to another page. /// /// # Examples /// /// ```gleam /// redirect(to: "https://example.com") /// // -> Response(303, [#("location", "https://example.com")], Empty) /// ``` /// pub fn redirect(to url: String) -> Response { HttpResponse(303, [#("location", url)], Empty) } /// Create an empty response with status code 308: Moved Permanently, and the /// `location` header set to the given URL. Used to redirect the client to /// another page. /// /// This redirect is permanent and the client is expected to cache the new /// location, using it for future requests. /// /// # Examples /// /// ```gleam /// moved_permanently(to: "https://example.com") /// // -> Response(308, [#("location", "https://example.com")], Empty) /// ``` /// pub fn moved_permanently(to url: String) -> Response { HttpResponse(308, [#("location", url)], Empty) } /// Create an empty response with status code 204: No content. /// /// # Examples /// /// ```gleam /// no_content() /// // -> Response(204, [], Empty) /// ``` /// pub fn no_content() -> Response { HttpResponse(204, [], Empty) } /// Create an empty response with status code 404: No content. /// /// # Examples /// /// ```gleam /// not_found() /// // -> Response(404, [], Empty) /// ``` /// pub fn not_found() -> Response { HttpResponse(404, [], Empty) } /// Create an empty response with status code 400: Bad request. /// /// # Examples /// /// ```gleam /// bad_request() /// // -> Response(400, [], Empty) /// ``` /// pub fn bad_request() -> Response { HttpResponse(400, [], Empty) } /// Create an empty response with status code 413: Entity too large. /// /// # Examples /// /// ```gleam /// entity_too_large() /// // -> Response(413, [], Empty) /// ``` /// pub fn entity_too_large() -> Response { HttpResponse(413, [], Empty) } /// Create an empty response with status code 415: Unsupported media type. /// /// The `allow` header will be set to a comma separated list of the permitted /// content-types. /// /// # Examples /// /// ```gleam /// unsupported_media_type(accept: ["application/json", "text/plain"]) /// // -> Response(415, [#("allow", "application/json, text/plain")], Empty) /// ``` /// pub fn unsupported_media_type(accept acceptable: List(String)) -> Response { let acceptable = string.join(acceptable, ", ") HttpResponse(415, [#("accept", acceptable)], Empty) } /// Create an empty response with status code 500: Internal server error. /// /// # Examples /// /// ```gleam /// internal_server_error() /// // -> Response(500, [], Empty) /// ``` /// pub fn internal_server_error() -> Response { HttpResponse(500, [], Empty) } // // Requests // /// The connection to the client for a HTTP request. /// /// The body of the request can be read from this connection using functions /// such as `require_multipart_body`. /// pub opaque type Connection { Connection( reader: Reader, max_body_size: Int, max_files_size: Int, read_chunk_size: Int, secret_key_base: String, temporary_directory: String, ) } fn make_connection(body_reader: Reader, secret_key_base: String) -> Connection { // TODO: replace `/tmp` with appropriate for the OS let prefix = "/tmp/gleam-wisp/" let temporary_directory = join_path(prefix, random_slug()) Connection( reader: body_reader, max_body_size: 8_000_000, max_files_size: 32_000_000, read_chunk_size: 1_000_000, temporary_directory: temporary_directory, secret_key_base: secret_key_base, ) } type BufferedReader { BufferedReader(reader: Reader, buffer: BitString) } type Quotas { Quotas(body: Int, files: Int) } fn decrement_body_quota(quotas: Quotas, size: Int) -> Result(Quotas, Response) { let quotas = Quotas(..quotas, body: quotas.body - size) case quotas.body < 0 { True -> Error(entity_too_large()) False -> Ok(quotas) } } fn decrement_quota(quota: Int, size: Int) -> Result(Int, Response) { case quota - size { quota if quota < 0 -> Error(entity_too_large()) quota -> Ok(quota) } } fn buffered_read(reader: BufferedReader, chunk_size: Int) -> Result(Read, Nil) { case reader.buffer { <<>> -> reader.reader(chunk_size) _ -> Ok(Chunk(reader.buffer, reader.reader)) } } type Reader = fn(Int) -> Result(Read, Nil) type Read { Chunk(BitString, next: Reader) ReadingFinished } /// Set the maximum permitted size of a request body of the request in bytes. /// /// If a body is larger than this size attempting to read the body will result /// in a response with status code 413: Entity too large will be returned to the /// client. /// /// This limit only applies for headers and bodies that get read into memory. /// Part of a multipart body that contain files and so are streamed to disc /// instead use the `max_files_size` limit. /// pub fn set_max_body_size(request: Request, size: Int) -> Request { Connection(..request.body, max_body_size: size) |> request.set_body(request, _) } /// Get the maximum permitted size of a request body of the request in bytes. /// pub fn get_max_body_size(request: Request) -> Int { request.body.max_body_size } /// Set the secret key base used to sign cookies and other sensitive data. /// /// This key must be at least 64 bytes long and should be kept secret. Anyone /// with this secret will be able to manipulate signed cookies and other sensitive /// data. /// /// # Panics /// /// This function will panic if the key is less than 64 bytes long. /// pub fn set_secret_key_base(request: Request, key: String) -> Request { case string.byte_size(key) < 64 { True -> panic as "Secret key base must be at least 64 bytes long" False -> Connection(..request.body, secret_key_base: key) |> request.set_body(request, _) } } /// Get the secret key base used to sign cookies and other sensitive data. /// pub fn get_secret_key_base(request: Request) -> String { request.body.secret_key_base } /// Set the maximum permitted size of all files uploaded by a request, in bytes. /// /// If a request contains fails which are larger in total than this size /// then attempting to read the body will result in a response with status code /// 413: Entity too large will be returned to the client. /// /// This limit only applies for files in a multipart body that get streamed to /// disc. For headers and other content that gets read into memory use the /// `max_files_size` limit. /// pub fn set_max_files_size(request: Request, size: Int) -> Request { Connection(..request.body, max_files_size: size) |> request.set_body(request, _) } /// Get the maximum permitted total size of a files uploaded by a request in /// bytes. /// pub fn get_max_files_size(request: Request) -> Int { request.body.max_files_size } /// The the size limit for each chunk of the request body when read from the /// client. /// /// This value is passed to the underlying web server when reading the body and /// the exact size of chunks read depends on the server implementation. It most /// likely will read chunks smaller than this size if not yet enough data has /// been received from the client. /// pub fn set_read_chunk_size(request: Request, size: Int) -> Request { Connection(..request.body, read_chunk_size: size) |> request.set_body(request, _) } /// Get the size limit for each chunk of the request body when read from the /// client. /// pub fn get_read_chunk_size(request: Request) -> Int { request.body.read_chunk_size } /// A convenient alias for a HTTP request with a Wisp connection as the body. /// pub type Request = HttpRequest(Connection) /// This middleware function ensures that the request has a specific HTTP /// method, returning an empty response with status code 405: Method not allowed /// if the method is not correct. /// /// # Examples /// /// ```gleam /// fn handle_request(request: Request) -> Response { /// use <- wisp.require_method(request, http.Patch) /// // ... /// } /// ``` /// pub fn require_method( request: HttpRequest(t), method: Method, next: fn() -> Response, ) -> Response { case request.method == method { True -> next() False -> method_not_allowed([method]) } } // TODO: re-export once Gleam has a syntax for that /// Return the non-empty segments of a request path. /// /// # Examples /// /// ```gleam /// > request.new() /// > |> request.set_path("/one/two/three") /// > |> wisp.path_segments /// ["one", "two", "three"] /// ``` /// pub const path_segments = request.path_segments /// This function overrides an incoming POST request with a method given in /// the request's `_method` query paramerter. This is useful as web browsers /// typically only support GET and POST requests, but our application may /// expect other HTTP methods that are more semantically correct. /// /// The methods PUT, PATCH, and DELETE are accepted for overriding, all others /// are ignored. /// /// The `_method` query paramerter can be specified in a HTML form like so: /// ///
/// ///
/// /// # Examples /// /// ```gleam /// fn handle_request(request: Request) -> Response { /// let request = wisp.method_override(request) /// // The method has now been overridden if appropriate /// } /// pub fn method_override(request: HttpRequest(a)) -> HttpRequest(a) { use <- bool.guard(when: request.method != http.Post, return: request) { use query <- result.try(request.get_query(request)) use pair <- result.try(list.key_pop(query, "_method")) use method <- result.map(http.parse_method(pair.0)) case method { http.Put | http.Patch | http.Delete -> request.set_method(request, method) _ -> request } } |> result.unwrap(request) } // TODO: don't always return entity to large. Other errors are possible, such as // network errors. /// A middleware function which reads the entire body of the request as a string. /// /// This function does not cache the body in any way, so if you call this /// function (or any other body reading function) more than once it may hang or /// return an incorrect value, depending on the underlying web server. It is the /// responsibility of the caller to cache the body if it is needed multiple /// times. /// /// If the body is larger than the `max_body_size` limit then an empty response /// with status code 413: Entity too large will be returned to the client. /// /// If the body is found not to be valid UTF-8 then an empty response with /// status code 400: Bad request will be returned to the client. /// /// # Examples /// /// ```gleam /// fn handle_request(request: Request) -> Response { /// use body <- wisp.require_string_body(request) /// // ... /// } /// ``` /// pub fn require_string_body( request: Request, next: fn(String) -> Response, ) -> Response { case read_body_to_bitstring(request) { Ok(body) -> or_400(bit_string.to_string(body), next) Error(_) -> entity_too_large() } } // TODO: don't always return entity to large. Other errors are possible, such as // network errors. /// A middleware function which reads the entire body of the request as a bit /// string. /// /// This function does not cache the body in any way, so if you call this /// function (or any other body reading function) more than once it may hang or /// return an incorrect value, depending on the underlying web server. It is the /// responsibility of the caller to cache the body if it is needed multiple /// times. /// /// If the body is larger than the `max_body_size` limit then an empty response /// with status code 413: Entity too large will be returned to the client. /// /// # Examples /// /// ```gleam /// fn handle_request(request: Request) -> Response { /// use body <- wisp.require_string_body(request) /// // ... /// } /// ``` /// pub fn require_bit_string_body( request: Request, next: fn(BitString) -> Response, ) -> Response { case read_body_to_bitstring(request) { Ok(body) -> next(body) Error(_) -> entity_too_large() } } // TODO: don't always return entity to large. Other errors are possible, such as // network errors. /// Read the entire body of the request as a bit string. /// /// You may instead wish to use the `require_bit_string_body` or the /// `require_string_body` middleware functions instead. /// /// This function does not cache the body in any way, so if you call this /// function (or any other body reading function) more than once it may hang or /// return an incorrect value, depending on the underlying web server. It is the /// responsibility of the caller to cache the body if it is needed multiple /// times. /// /// If the body is larger than the `max_body_size` limit then an empty response /// with status code 413: Entity too large will be returned to the client. /// pub fn read_body_to_bitstring(request: Request) -> Result(BitString, Nil) { let connection = request.body read_body_loop( connection.reader, connection.read_chunk_size, connection.max_body_size, <<>>, ) } fn read_body_loop( reader: Reader, read_chunk_size: Int, max_body_size: Int, accumulator: BitString, ) -> Result(BitString, Nil) { use chunk <- result.try(reader(read_chunk_size)) case chunk { ReadingFinished -> Ok(accumulator) Chunk(chunk, next) -> { let accumulator = bit_string.append(accumulator, chunk) case bit_string.byte_size(accumulator) > max_body_size { True -> Error(Nil) False -> read_body_loop(next, read_chunk_size, max_body_size, accumulator) } } } } /// A middleware which extracts form data from the body of a request that is /// encoded as either `application/x-www-form-urlencoded` or /// `multipart/form-data`. /// /// Extracted fields are sorted into alphabetical order by key, so if you wish /// to use pattern matching the order can be relied upon. /// /// ```gleam /// fn handle_request(request: Request) -> Response { /// use form <- wisp.require_form(request) /// case form.values { /// [#("password", pass), #("username", username)] -> // ... /// _ -> // ... /// } /// } /// ``` /// /// The `set_max_body_size`, `set_max_files_size`, and `set_read_chunk_size` can /// be used to configure the reading of the request body. /// /// Any file uploads will streamed into temporary files on disc. These files are /// automatically deleted when the request handler returns, so if you wish to /// use them after the request has completed you will need to move them to a new /// location. /// /// If the request does not have a recognised `content-type` header then an /// empty response with status code 415: Unsupported media type will be returned /// to the client. /// /// If the request body is larger than the `max_body_size` or `max_files_size` /// limits then an empty response with status code 413: Entity too large will be /// returned to the client. /// /// If the body cannot be parsed successfully then an empty response with status /// code 400: Bad request will be returned to the client. /// pub fn require_form( request: Request, next: fn(FormData) -> Response, ) -> Response { case list.key_find(request.headers, "content-type") { Ok("application/x-www-form-urlencoded") -> require_urlencoded_form(request, next) Ok("multipart/form-data; boundary=" <> boundary) -> require_multipart_form(request, boundary, next) Ok("multipart/form-data") -> bad_request() _ -> unsupported_media_type([ "application/x-www-form-urlencoded", "multipart/form-data", ]) } } fn require_urlencoded_form( request: Request, next: fn(FormData) -> Response, ) -> Response { use body <- require_string_body(request) use pairs <- or_400(uri.parse_query(body)) let pairs = sort_keys(pairs) next(FormData(values: pairs, files: [])) } fn require_multipart_form( request: Request, boundary: String, next: fn(FormData) -> Response, ) -> Response { let quotas = Quotas(files: request.body.max_files_size, body: request.body.max_body_size) let reader = BufferedReader(request.body.reader, <<>>) let result = read_multipart(request, reader, boundary, quotas, FormData([], [])) case result { Ok(form_data) -> next(form_data) Error(response) -> response } } fn read_multipart( request: Request, reader: BufferedReader, boundary: String, quotas: Quotas, data: FormData, ) -> Result(FormData, Response) { let read_size = request.body.read_chunk_size // First we read the headers of the multipart part. let header_parser = fn_with_bad_request_error(http.parse_multipart_headers(_, boundary)) let result = multipart_headers(reader, header_parser, read_size, quotas) use #(headers, reader, quotas) <- result.try(result) use #(name, filename) <- result.try(multipart_content_disposition(headers)) // Then we read the body of the part. let parse = fn_with_bad_request_error(http.parse_multipart_body(_, boundary)) use #(data, reader, quotas) <- result.try(case filename { // There is a file name, so we treat this as a file upload, streaming the // contents to a temporary file and using the dedicated files size quota. option.Some(file_name) -> { use path <- result.try(or_500(new_temporary_file(request))) let append = multipart_file_append let q = quotas.files let result = multipart_body(reader, parse, boundary, read_size, q, append, path) use #(reader, quota, _) <- result.map(result) let quotas = Quotas(..quotas, files: quota) let file = UploadedFile(path: path, file_name: file_name) let data = FormData(..data, files: [#(name, file), ..data.files]) #(data, reader, quotas) } // No file name, this is a regular form value that we hold in memory. option.None -> { let append = fn(data, chunk) { Ok(bit_string.append(data, chunk)) } let q = quotas.body let result = multipart_body(reader, parse, boundary, read_size, q, append, <<>>) use #(reader, quota, value) <- result.try(result) let quotas = Quotas(..quotas, body: quota) use value <- result.map(bit_string_to_string(value)) let data = FormData(..data, values: [#(name, value), ..data.values]) #(data, reader, quotas) } }) case reader { // There's at least one more part, read it. option.Some(reader) -> read_multipart(request, reader, boundary, quotas, data) // There are no more parts, we're done. option.None -> Ok(FormData(sort_keys(data.values), sort_keys(data.files))) } } fn bit_string_to_string(bits: BitString) -> Result(String, Response) { bit_string.to_string(bits) |> result.replace_error(bad_request()) } fn multipart_file_append( path: String, chunk: BitString, ) -> Result(String, Response) { chunk |> simplifile.append_bits(path) |> or_500 |> result.replace(path) } fn or_500(result: Result(a, b)) -> Result(a, Response) { case result { Ok(value) -> Ok(value) Error(error) -> { log_error(string.inspect(error)) Error(internal_server_error()) } } } fn multipart_body( reader: BufferedReader, parse: fn(BitString) -> Result(http.MultipartBody, Response), boundary: String, chunk_size: Int, quota: Int, append: fn(t, BitString) -> Result(t, Response), data: t, ) -> Result(#(Option(BufferedReader), Int, t), Response) { use #(chunk, reader) <- result.try(read_chunk(reader, chunk_size)) let size_read = bit_string.byte_size(chunk) use output <- result.try(parse(chunk)) case output { http.MultipartBody(parsed, done, remaining) -> { // Decrement the quota by the number of bytes consumed. let used = size_read - bit_string.byte_size(remaining) - 2 let used = case done { // If this is the last chunk, we need to account for the boundary. True -> used - 4 - string.byte_size(boundary) False -> used } use quota <- result.try(decrement_quota(quota, used)) let reader = BufferedReader(reader, remaining) let reader = case done { True -> option.None False -> option.Some(reader) } use value <- result.map(append(data, parsed)) #(reader, quota, value) } http.MoreRequiredForBody(chunk, parse) -> { let parse = fn_with_bad_request_error(parse(_)) let reader = BufferedReader(reader, <<>>) use data <- result.try(append(data, chunk)) multipart_body(reader, parse, boundary, chunk_size, quota, append, data) } } } fn fn_with_bad_request_error( f: fn(a) -> Result(b, c), ) -> fn(a) -> Result(b, Response) { fn(a) { f(a) |> result.replace_error(bad_request()) } } fn multipart_content_disposition( headers: List(http.Header), ) -> Result(#(String, Option(String)), Response) { { use header <- result.try(list.key_find(headers, "content-disposition")) use header <- result.try(http.parse_content_disposition(header)) use name <- result.map(list.key_find(header.parameters, "name")) let filename = option.from_result(list.key_find(header.parameters, "filename")) #(name, filename) } |> result.replace_error(bad_request()) } fn read_chunk( reader: BufferedReader, chunk_size: Int, ) -> Result(#(BitString, Reader), Response) { buffered_read(reader, chunk_size) |> result.replace_error(bad_request()) |> result.try(fn(chunk) { case chunk { Chunk(chunk, next) -> Ok(#(chunk, next)) ReadingFinished -> Error(bad_request()) } }) } fn multipart_headers( reader: BufferedReader, parse: fn(BitString) -> Result(http.MultipartHeaders, Response), chunk_size: Int, quotas: Quotas, ) -> Result(#(List(http.Header), BufferedReader, Quotas), Response) { use #(chunk, reader) <- result.try(read_chunk(reader, chunk_size)) use headers <- result.try(parse(chunk)) case headers { http.MultipartHeaders(headers, remaining) -> { let used = bit_string.byte_size(chunk) - bit_string.byte_size(remaining) use quotas <- result.map(decrement_body_quota(quotas, used)) let reader = BufferedReader(reader, remaining) #(headers, reader, quotas) } http.MoreRequiredForHeaders(parse) -> { let parse = fn(chunk) { parse(chunk) |> result.replace_error(bad_request()) } let reader = BufferedReader(reader, <<>>) multipart_headers(reader, parse, chunk_size, quotas) } } } fn sort_keys(pairs: List(#(String, t))) -> List(#(String, t)) { list.sort(pairs, fn(a, b) { string.compare(a.0, b.0) }) } fn or_400(result: Result(value, error), next: fn(value) -> Response) -> Response { case result { Ok(value) -> next(value) Error(_) -> bad_request() } } /// Data parsed from form sent in a request's body. /// pub type FormData { FormData( /// String values of the form's fields. values: List(#(String, String)), /// Uploaded files. files: List(#(String, UploadedFile)), ) } pub type UploadedFile { UploadedFile( /// The name that was given to the file in the form. /// This is user input and should not be trusted. file_name: String, /// The location of the file on the server. /// This is a temporary file and will be deleted when the request has /// finished being handled. path: String, ) } // // MIME types // // TODO: move to another package fn extension_to_mime_type(extension: String) -> String { case extension { "7z" -> "application/x-7z-compressed" "aac" -> "audio/aac" "abw" -> "application/x-abiword" "ai" -> "application/postscript" "arc" -> "application/x-freearc" "asice" -> "application/vnd.etsi.asic-e+zip" "asics" -> "application/vnd.etsi.asic-s+zip" "atom" -> "application/atom+xml" "avi" -> "video/x-msvideo" "avif" -> "image/avif" "azw" -> "application/vnd.amazon.ebook" "bin" -> "application/octet-stream" "bmp" -> "image/bmp" "bz" -> "application/x-bzip" "bz2" -> "application/x-bzip2" "cda" -> "application/x-cdf" "csh" -> "application/x-csh" "css" -> "text/css" "csv" -> "text/csv" "doc" -> "application/msword" "docx" -> "application/vnd.openxmlformats-officedocument.wordprocessingml.document" "eot" -> "application/vnd.ms-fontobject" "eps" -> "application/postscript" "epub" -> "application/epub+zip" "gif" -> "image/gif" "gz" -> "application/gzip" "heic" -> "image/heic" "heif" -> "image/heif" "htm" -> "text/html" "html" -> "text/html" "ico" -> "image/vnd.microsoft.icon" "ics" -> "text/calendar" "jar" -> "application/java-archive" "jpeg" -> "image/jpeg" "jpg" -> "image/jpeg" "js" -> "text/javascript" "json" -> "application/json" "json-api" -> "application/vnd.api+json" "json-patch" -> "application/json-patch+json" "jsonld" -> "application/ld+json" "jxl" -> "image/jxl" "markdown" -> "text/markdown" "md" -> "text/markdown" "mdb" -> "application/x-msaccess" "mid" -> "audio/midi" "midi" -> "audio/midi" "mjs" -> "text/javascript" "mov" -> "video/quicktime" "mp3" -> "audio/mpeg" "mp4" -> "video/mp4" "mpeg" -> "video/mpeg" "mpg" -> "video/mpeg" "mpkg" -> "application/vnd.apple.installer+xml" "odp" -> "application/vnd.oasis.opendocument.presentation" "ods" -> "application/vnd.oasis.opendocument.spreadsheet" "odt" -> "application/vnd.oasis.opendocument.text" "oga" -> "audio/ogg" "ogv" -> "video/ogg" "ogx" -> "application/ogg" "opus" -> "audio/opus" "otf" -> "font/otf" "pdf" -> "application/pdf" "php" -> "application/x-httpd-php" "png" -> "image/png" "ppt" -> "application/vnd.ms-powerpoint" "pptx" -> "application/vnd.openxmlformats-officedocument.presentationml.presentation" "ps" -> "application/postscript" "psd" -> "image/vnd.adobe.photoshop" "rar" -> "application/vnd.rar" "rss" -> "application/rss+xml" "rtf" -> "application/rtf" "sce" -> "application/vnd.etsi.asic-e+zip" "scs" -> "application/vnd.etsi.asic-s+zip" "sh" -> "application/x-sh" "svg" -> "image/svg+xml" "svgz" -> "image/svg+xml" "swf" -> "application/x-shockwave-flash" "tar" -> "application/x-tar" "text" -> "text/plain" "tif" -> "image/tiff" "tiff" -> "image/tiff" "ts" -> "video/mp2t" "ttf" -> "font/ttf" "txt" -> "text/plain" "vsd" -> "application/vnd.visio" "wasm" -> "application/wasm" "wav" -> "audio/wav" "weba" -> "audio/webm" "webm" -> "video/webm" "webmanifest" -> "application/manifest+json" "webp" -> "image/webp" "wmv" -> "video/x-ms-wmv" "woff" -> "font/woff" "woff2" -> "font/woff2" "xhtml" -> "application/xhtml+xml" "xls" -> "application/vnd.ms-excel" "xlsx" -> "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" "xml" -> "application/xml" "xul" -> "application/vnd.mozilla.xul+xml" "zip" -> "application/zip" _ -> "application/octet-stream" } } // // Middleware // /// A middleware function that rescues crashes and returns an empty response /// with status code 500: Internal server error. /// /// # Examples /// /// ```gleam /// fn handle_request(req: Request) -> Response { /// use <- wisp.rescue_crashes /// // ... /// } /// ``` /// pub fn rescue_crashes(handler: fn() -> Response) -> Response { case erlang.rescue(handler) { Ok(response) -> response Error(error) -> { log_error(string.inspect(error)) internal_server_error() } } } // TODO: test, somehow. /// A middleware function that logs details about the request and response. /// /// The format used logged by this middleware may change in future versions of /// Wisp. /// /// # Examples /// /// ```gleam /// fn handle_request(req: Request) -> Response { /// use <- wisp.log_request(req) /// // ... /// } /// ``` /// pub fn log_request(req: Request, handler: fn() -> Response) -> Response { let response = handler() [ int.to_string(response.status), " ", string.uppercase(http.method_to_string(req.method)), " ", req.path, ] |> string.concat |> log_info response } fn remove_preceeding_slashes(string: String) -> String { case string { "/" <> rest -> remove_preceeding_slashes(rest) _ -> string } } // TODO: replace with simplifile function when it exists fn join_path(a: String, b: String) -> String { let b = remove_preceeding_slashes(b) case string.ends_with(a, "/") { True -> a <> b False -> a <> "/" <> b } } /// A middleware function that serves files from a directory, along with a /// suitable `content-type` header for known file extensions. /// /// Files are sent using the `File` response body type, so they will be sent /// directly to the client from the disc, without being read into memory. /// /// The `under` parameter is the request path prefix that must match for the /// file to be served. /// /// | `under` | `from` | `request.path` | `file` | /// |-----------|---------|--------------------|-------------------------| /// | `/static` | `/data` | `/static/file.txt` | `/data/file.txt` | /// | `` | `/data` | `/static/file.txt` | `/data/static/file.txt` | /// | `/static` | `` | `/static/file.txt` | `file.txt` | /// /// This middleware will discard any `..` path segments in the request path to /// prevent the client from accessing files outside of the directory. It is /// advised not to serve a directory that contains your source code, application /// configuration, database, or other private files. /// /// # Examples /// /// ```gleam /// fn handle_request(req: Request) -> Response { /// use <- wisp.serve_static(req, under: "/static", from: "/public") /// // ... /// } /// ``` /// pub fn serve_static( req: Request, under prefix: String, from directory: String, next handler: fn() -> Response, ) -> Response { let path = remove_preceeding_slashes(req.path) let prefix = remove_preceeding_slashes(prefix) case req.method, string.starts_with(path, prefix) { http.Get, True -> { let path = path |> string.drop_left(string.length(prefix)) |> string.replace(each: "..", with: "") |> join_path(directory, _) let mime_type = req.path |> string.split(on: ".") |> list.last |> result.unwrap("") |> extension_to_mime_type case simplifile.is_file(path) { False -> handler() True -> response.new(200) |> response.set_header("content-type", mime_type) |> response.set_body(File(path)) } } _, _ -> handler() } } /// A middleware function that converts `HEAD` requests to `GET` requests, /// handles the request, and then discards the response body. This is useful so /// that your application can handle `HEAD` requests without having to implement /// handlers for them. /// /// The `x-original-method` header is set to `"HEAD"` for requests that were /// originally `HEAD` requests. /// /// # Examples /// /// ```gleam /// fn handle_request(req: Request) -> Response { /// use req <- wisp.handle_head(req) /// // ... /// } /// ``` /// pub fn handle_head( req: Request, next handler: fn(Request) -> Response, ) -> Response { case req.method { http.Head -> req |> request.set_method(http.Get) |> request.prepend_header("x-original-method", "HEAD") |> handler |> response.set_body(Empty) _ -> handler(req) } } // // File uploads // /// Create a new temporary directory for the given request. /// /// If you are using the `mist_service` function or another compliant web server /// adapter then this file will be deleted for you when the request is complete. /// Otherwise you will need to call the `delete_temporary_files` function /// yourself. /// pub fn new_temporary_file( request: Request, ) -> Result(String, simplifile.FileError) { let directory = request.body.temporary_directory use _ <- result.try(simplifile.create_directory_all(directory)) let path = join_path(directory, random_slug()) use _ <- result.map(simplifile.create_file(path)) path } /// Delete any temporary files created for the given request. /// /// If you are using the `mist_service` function or another compliant web server /// adapter then this file will be deleted for you when the request is complete. /// Otherwise you will need to call this function yourself. /// pub fn delete_temporary_files( request: Request, ) -> Result(Nil, simplifile.FileError) { case simplifile.delete(request.body.temporary_directory) { Error(simplifile.Enoent) -> Ok(Nil) other -> other } } // // Logging // /// Configure the Erlang logger, setting the minimum log level to `info`, to be /// called when your application starts. /// /// You may wish to use an alternative for this such as one provided by a more /// sophisticated logging library. /// /// In future this function may be extended to change the output format. /// pub fn configure_logger() -> Nil { logger.configure_logger() } /// Log a message to the Erlang logger with the level of `emergency`. /// /// See the [Erlang logger documentation][1] for more information. /// /// [1]: https://www.erlang.org/doc/man/logger /// pub fn log_emergency(message: String) -> Nil { logger.log(logger.Emergency, message) } /// Log a message to the Erlang logger with the level of `alert`. /// /// See the [Erlang logger documentation][1] for more information. /// /// [1]: https://www.erlang.org/doc/man/logger /// pub fn log_alert(message: String) -> Nil { logger.log(logger.Alert, message) } /// Log a message to the Erlang logger with the level of `critical`. /// /// See the [Erlang logger documentation][1] for more information. /// /// [1]: https://www.erlang.org/doc/man/logger /// pub fn log_critical(message: String) -> Nil { logger.log(logger.Critical, message) } /// Log a message to the Erlang logger with the level of `error`. /// /// See the [Erlang logger documentation][1] for more information. /// /// [1]: https://www.erlang.org/doc/man/logger /// pub fn log_error(message: String) -> Nil { logger.log(logger.Error, message) } /// Log a message to the Erlang logger with the level of `warning`. /// /// See the [Erlang logger documentation][1] for more information. /// /// [1]: https://www.erlang.org/doc/man/logger /// pub fn log_warning(message: String) -> Nil { logger.log(logger.Warning, message) } /// Log a message to the Erlang logger with the level of `notice`. /// /// See the [Erlang logger documentation][1] for more information. /// /// [1]: https://www.erlang.org/doc/man/logger /// pub fn log_notice(message: String) -> Nil { logger.log(logger.Notice, message) } /// Log a message to the Erlang logger with the level of `info`. /// /// See the [Erlang logger documentation][1] for more information. /// /// [1]: https://www.erlang.org/doc/man/logger /// pub fn log_info(message: String) -> Nil { logger.log(logger.Info, message) } /// Log a message to the Erlang logger with the level of `debug`. /// /// See the [Erlang logger documentation][1] for more information. /// /// [1]: https://www.erlang.org/doc/man/logger /// pub fn log_debug(message: String) -> Nil { logger.log(logger.Debug, message) } // // Cryptography // /// Generate a random string of the given length. /// pub fn random_string(length: Int) -> String { crypto.strong_random_bytes(length) |> base.url_encode64(False) |> string.slice(0, length) } /// Sign a message which can later be verified using the `verify_signed_message` /// function to detect if the message has been tampered with. /// /// Signed messages are not encrypted and can be read by anyone. They are not /// suitable for storing sensitive information. /// /// This function uses the secret key base from the request. If the secret /// changes then the signature will no longer be verifiable. /// pub fn sign_message( request: Request, message: BitString, algorithm: crypto.HashAlgorithm, ) -> String { crypto.sign_message(message, <>, algorithm) } /// Verify a signed message which was signed using the `sign_message` function. /// /// Returns the content of the message if the signature is valid, otherwise /// returns an error. /// /// This function uses the secret key base from the request. If the secret /// changes then the signature will no longer be verifiable. /// pub fn verify_signed_message( request: Request, message: String, ) -> Result(BitString, Nil) { crypto.verify_signed_message(message, <>) } fn random_slug() -> String { random_string(16) } // // Testing // // TODO: chunk the body /// Create a connection which will return the given body when read. /// /// This function is intended for use in tests, though you probably want the /// `wisp/testing` module instead. /// pub fn create_canned_connection( body: BitString, secret_key_base: String, ) -> Connection { make_connection( fn(_size) { Ok(Chunk(body, fn(_size) { Ok(ReadingFinished) })) }, secret_key_base, ) }