1import exception
2import gleam/bit_array
3import gleam/bool
4import gleam/bytes_tree.{type BytesTree}
5import gleam/crypto
6import gleam/dict.{type Dict}
7import gleam/dynamic.{type Dynamic}
8import gleam/erlang
9import gleam/erlang/atom.{type Atom}
10import gleam/http.{type Method}
11import gleam/http/cookie
12import gleam/http/request.{type Request as HttpRequest}
13import gleam/http/response.{
14 type Response as HttpResponse, Response as HttpResponse,
15}
16import gleam/int
17import gleam/json
18import gleam/list
19import gleam/option.{type Option}
20import gleam/result
21import gleam/string
22import gleam/string_tree.{type StringTree}
23import gleam/uri
24import logging
25import marceau
26import simplifile
27import wisp/internal
28
29//
30// Responses
31//
32
33/// The body of a HTTP response, to be sent to the client.
34///
35pub type Body {
36 /// A body of unicode text.
37 ///
38 /// The body is represented using a `StringTree`. If you have a `String`
39 /// you can use the `string_tree.from_string` function to convert it.
40 ///
41 Text(StringTree)
42 /// A body of binary data.
43 ///
44 /// The body is represented using a `BytesTree`. If you have a `BitArray`
45 /// you can use the `bytes_tree.from_bit_array` function to convert it.
46 ///
47 Bytes(BytesTree)
48 /// A body of the contents of a file.
49 ///
50 /// This will be sent efficiently using the `send_file` function of the
51 /// underlying HTTP server. The file will not be read into memory so it is
52 /// safe to send large files this way.
53 ///
54 File(path: String)
55 /// An empty body. This may be returned by the `require_*` middleware
56 /// functions in the event of a failure, invalid request, or other situation
57 /// in which the request cannot be processed.
58 ///
59 /// Your application may wish to use a middleware to provide default responses
60 /// in place of any with an empty body.
61 ///
62 Empty
63}
64
65/// An alias for a HTTP response containing a `Body`.
66pub type Response =
67 HttpResponse(Body)
68
69/// Create an empty response with the given status code.
70///
71/// # Examples
72///
73/// ```gleam
74/// response(200)
75/// // -> Response(200, [], Empty)
76/// ```
77///
78pub fn response(status: Int) -> Response {
79 HttpResponse(status, [], Empty)
80}
81
82/// Set the body of a response.
83///
84/// # Examples
85///
86/// ```gleam
87/// response(200)
88/// |> set_body(File("/tmp/myfile.txt"))
89/// // -> Response(200, [], File("/tmp/myfile.txt"))
90/// ```
91///
92pub fn set_body(response: Response, body: Body) -> Response {
93 response
94 |> response.set_body(body)
95}
96
97/// Send a file from the disc as a file download.
98///
99/// The operating system `send_file` function is used to efficiently send the
100/// file over the network socket without reading the entire file into memory.
101///
102/// The `content-disposition` header will be set to `attachment;
103/// filename="name"` to ensure the file is downloaded by the browser. This is
104/// especially good for files that the browser would otherwise attempt to open
105/// as this can result in cross-site scripting vulnerabilities.
106///
107/// If you wish to not set the `content-disposition` header you could use the
108/// `set_body` function with the `File` body variant.
109///
110/// # Examples
111///
112/// ```gleam
113/// response(200)
114/// |> file_download(named: "myfile.txt", from: "/tmp/myfile.txt")
115/// // -> Response(
116/// // 200,
117/// // [#("content-disposition", "attachment; filename=\"myfile.txt\"")],
118/// // File("/tmp/myfile.txt"),
119/// // )
120/// ```
121///
122pub fn file_download(
123 response: Response,
124 named name: String,
125 from path: String,
126) -> Response {
127 let name = uri.percent_encode(name)
128 response
129 |> response.set_header(
130 "content-disposition",
131 "attachment; filename=\"" <> name <> "\"",
132 )
133 |> response.set_body(File(path))
134}
135
136/// Send a file from memory as a file download.
137///
138/// If your file is already on the disc use `file_download` instead, to avoid
139/// having to read the file into memory to send it.
140///
141/// The `content-disposition` header will be set to `attachment;
142/// filename="name"` to ensure the file is downloaded by the browser. This is
143/// especially good for files that the browser would otherwise attempt to open
144/// as this can result in cross-site scripting vulnerabilities.
145///
146/// # Examples
147///
148/// ```gleam
149/// let content = bytes_tree.from_string("Hello, Joe!")
150/// response(200)
151/// |> file_download_from_memory(named: "myfile.txt", containing: content)
152/// // -> Response(
153/// // 200,
154/// // [#("content-disposition", "attachment; filename=\"myfile.txt\"")],
155/// // File("/tmp/myfile.txt"),
156/// // )
157/// ```
158///
159pub fn file_download_from_memory(
160 response: Response,
161 named name: String,
162 containing data: BytesTree,
163) -> Response {
164 let name = uri.percent_encode(name)
165 response
166 |> response.set_header(
167 "content-disposition",
168 "attachment; filename=\"" <> name <> "\"",
169 )
170 |> response.set_body(Bytes(data))
171}
172
173/// Create a HTML response.
174///
175/// The body is expected to be valid HTML, though this is not validated.
176/// The `content-type` header will be set to `text/html`.
177///
178/// # Examples
179///
180/// ```gleam
181/// let body = string_tree.from_string("<h1>Hello, Joe!</h1>")
182/// html_response(body, 200)
183/// // -> Response(200, [#("content-type", "text/html")], Text(body))
184/// ```
185///
186pub fn html_response(html: StringTree, status: Int) -> Response {
187 HttpResponse(
188 status,
189 [#("content-type", "text/html; charset=utf-8")],
190 Text(html),
191 )
192}
193
194/// Create a JSON response.
195///
196/// The body is expected to be valid JSON, though this is not validated.
197/// The `content-type` header will be set to `application/json`.
198///
199/// # Examples
200///
201/// ```gleam
202/// let body = string_tree.from_string("{\"name\": \"Joe\"}")
203/// json_response(body, 200)
204/// // -> Response(200, [#("content-type", "application/json")], Text(body))
205/// ```
206///
207pub fn json_response(json: StringTree, status: Int) -> Response {
208 HttpResponse(
209 status,
210 [#("content-type", "application/json; charset=utf-8")],
211 Text(json),
212 )
213}
214
215/// Set the body of a response to a given HTML document, and set the
216/// `content-type` header to `text/html`.
217///
218/// The body is expected to be valid HTML, though this is not validated.
219///
220/// # Examples
221///
222/// ```gleam
223/// let body = string_tree.from_string("<h1>Hello, Joe!</h1>")
224/// response(201)
225/// |> html_body(body)
226/// // -> Response(201, [#("content-type", "text/html; charset=utf-8")], Text(body))
227/// ```
228///
229pub fn html_body(response: Response, html: StringTree) -> Response {
230 response
231 |> response.set_body(Text(html))
232 |> response.set_header("content-type", "text/html; charset=utf-8")
233}
234
235/// Set the body of a response to a given JSON document, and set the
236/// `content-type` header to `application/json`.
237///
238/// The body is expected to be valid JSON, though this is not validated.
239///
240/// # Examples
241///
242/// ```gleam
243/// let body = string_tree.from_string("{\"name\": \"Joe\"}")
244/// response(201)
245/// |> json_body(body)
246/// // -> Response(201, [#("content-type", "application/json; charset=utf-8")], Text(body))
247/// ```
248///
249pub fn json_body(response: Response, json: StringTree) -> Response {
250 response
251 |> response.set_body(Text(json))
252 |> response.set_header("content-type", "application/json; charset=utf-8")
253}
254
255/// Set the body of a response to a given string tree.
256///
257/// You likely want to also set the request `content-type` header to an
258/// appropriate value for the format of the content.
259///
260/// # Examples
261///
262/// ```gleam
263/// let body = string_tree.from_string("Hello, Joe!")
264/// response(201)
265/// |> string_tree_body(body)
266/// // -> Response(201, [], Text(body))
267/// ```
268///
269pub fn string_tree_body(response: Response, content: StringTree) -> Response {
270 response
271 |> response.set_body(Text(content))
272}
273
274/// Set the body of a response to a given string.
275///
276/// You likely want to also set the request `content-type` header to an
277/// appropriate value for the format of the content.
278///
279/// # Examples
280///
281/// ```gleam
282/// let body =
283/// response(201)
284/// |> string_body("Hello, Joe!")
285/// // -> Response(
286/// // 201,
287/// // [],
288/// // Text(string_tree.from_string("Hello, Joe"))
289/// // )
290/// ```
291///
292pub fn string_body(response: Response, content: String) -> Response {
293 response
294 |> response.set_body(Text(string_tree.from_string(content)))
295}
296
297/// Escape a string so that it can be safely included in a HTML document.
298///
299/// Any content provided by the user should be escaped before being included in
300/// a HTML document to prevent cross-site scripting attacks.
301///
302/// # Examples
303///
304/// ```gleam
305/// escape_html("<h1>Hello, Joe!</h1>")
306/// // -> "<h1>Hello, Joe!</h1>"
307/// ```
308///
309pub fn escape_html(content: String) -> String {
310 let bits = <<content:utf8>>
311 let acc = do_escape_html(bits, 0, bits, [])
312
313 list.reverse(acc)
314 |> bit_array.concat
315 // We know the bit array produced by `do_escape_html` is still a valid utf8
316 // string so we coerce it without passing through the validation steps of
317 // `bit_array.to_string`.
318 |> coerce_bit_array_to_string
319}
320
321@external(erlang, "wisp_ffi", "coerce")
322fn coerce_bit_array_to_string(bit_array: BitArray) -> String
323
324// A possible way to escape chars would be to split the string into graphemes,
325// traverse those one by one and accumulate them back into a string escaping
326// ">", "<", etc. as we see them.
327//
328// However, we can be a lot more performant by working directly on the
329// `BitArray` representing a Gleam UTF-8 String.
330// This means that, instead of popping a grapheme at a time, we can work
331// directly on BitArray slices: this has the big advantage of making sure we
332// share as much as possible with the original string without having to build
333// a new one from scratch.
334//
335fn do_escape_html(
336 bin: BitArray,
337 skip: Int,
338 original: BitArray,
339 acc: List(BitArray),
340) -> List(BitArray) {
341 case bin {
342 // If we find a char to escape we just advance the `skip` counter so that
343 // it will be ignored in the following slice, then we append the escaped
344 // version to the accumulator.
345 <<"<":utf8, rest:bits>> -> {
346 let acc = [<<"<":utf8>>, ..acc]
347 do_escape_html(rest, skip + 1, original, acc)
348 }
349
350 <<">":utf8, rest:bits>> -> {
351 let acc = [<<">":utf8>>, ..acc]
352 do_escape_html(rest, skip + 1, original, acc)
353 }
354
355 <<"&":utf8, rest:bits>> -> {
356 let acc = [<<"&":utf8>>, ..acc]
357 do_escape_html(rest, skip + 1, original, acc)
358 }
359
360 // For any other bit that doesn't need to be escaped we go into an inner
361 // loop, consuming as much "non-escapable" chars as possible.
362 <<_char, rest:bits>> -> do_escape_html_regular(rest, skip, original, acc, 1)
363
364 <<>> -> acc
365
366 _ -> panic as "non byte aligned string, all strings should be byte aligned"
367 }
368}
369
370fn do_escape_html_regular(
371 bin: BitArray,
372 skip: Int,
373 original: BitArray,
374 acc: List(BitArray),
375 len: Int,
376) -> List(BitArray) {
377 // Remember, if we're here it means we've found a char that doesn't need to be
378 // escaped, so what we want to do is advance the `len` counter until we reach
379 // a char that _does_ need to be escaped and take the slice going from
380 // `skip` with size `len`.
381 //
382 // Imagine we're escaping this string: "abc<def&ghi" and we've reached 'd':
383 // ```
384 // abc<def&ghi
385 // ^ `skip` points here
386 // ```
387 // We're going to be increasing `len` until we reach the '&':
388 // ```
389 // abc<def&ghi
390 // ^^^ len will be 3 when we reach the '&' that needs escaping
391 // ```
392 // So we take the slice corresponding to "def".
393 //
394 case bin {
395 // If we reach a char that has to be escaped we append the slice starting
396 // from `skip` with size `len` and the escaped char.
397 // This is what allows us to share as much of the original string as
398 // possible: we only allocate a new BitArray for the escaped chars,
399 // everything else is just a slice of the original String.
400 <<"<":utf8, rest:bits>> -> {
401 let assert Ok(slice) = bit_array.slice(original, skip, len)
402 let acc = [<<"<":utf8>>, slice, ..acc]
403 do_escape_html(rest, skip + len + 1, original, acc)
404 }
405
406 <<">":utf8, rest:bits>> -> {
407 let assert Ok(slice) = bit_array.slice(original, skip, len)
408 let acc = [<<">":utf8>>, slice, ..acc]
409 do_escape_html(rest, skip + len + 1, original, acc)
410 }
411
412 <<"&":utf8, rest:bits>> -> {
413 let assert Ok(slice) = bit_array.slice(original, skip, len)
414 let acc = [<<"&":utf8>>, slice, ..acc]
415 do_escape_html(rest, skip + len + 1, original, acc)
416 }
417
418 // If a char doesn't need escaping we keep increasing the length of the
419 // slice we're going to take.
420 <<_char, rest:bits>> ->
421 do_escape_html_regular(rest, skip, original, acc, len + 1)
422
423 <<>> ->
424 case skip {
425 0 -> [original]
426 _ -> {
427 let assert Ok(slice) = bit_array.slice(original, skip, len)
428 [slice, ..acc]
429 }
430 }
431
432 _ -> panic as "non byte aligned string, all strings should be byte aligned"
433 }
434}
435
436/// Create an empty response with status code 405: Method Not Allowed. Use this
437/// when a request does not have an appropriate method to be handled.
438///
439/// The `allow` header will be set to a comma separated list of the permitted
440/// methods.
441///
442/// # Examples
443///
444/// ```gleam
445/// method_not_allowed(allowed: [Get, Post])
446/// // -> Response(405, [#("allow", "GET, POST")], Empty)
447/// ```
448///
449pub fn method_not_allowed(allowed methods: List(Method)) -> Response {
450 let allowed =
451 methods
452 |> list.map(http.method_to_string)
453 |> list.sort(string.compare)
454 |> string.join(", ")
455 |> string.uppercase
456 HttpResponse(405, [#("allow", allowed)], Empty)
457}
458
459/// Create an empty response with status code 200: OK.
460///
461/// # Examples
462///
463/// ```gleam
464/// ok()
465/// // -> Response(200, [], Empty)
466/// ```
467///
468pub fn ok() -> Response {
469 HttpResponse(200, [], Empty)
470}
471
472/// Create an empty response with status code 201: Created.
473///
474/// # Examples
475///
476/// ```gleam
477/// created()
478/// // -> Response(201, [], Empty)
479/// ```
480///
481pub fn created() -> Response {
482 HttpResponse(201, [], Empty)
483}
484
485/// Create an empty response with status code 202: Accepted.
486///
487/// # Examples
488///
489/// ```gleam
490/// accepted()
491/// // -> Response(202, [], Empty)
492/// ```
493///
494pub fn accepted() -> Response {
495 HttpResponse(202, [], Empty)
496}
497
498/// Create an empty response with status code 303: See Other, and the `location`
499/// header set to the given URL. Used to redirect the client to another page.
500///
501/// # Examples
502///
503/// ```gleam
504/// redirect(to: "https://example.com")
505/// // -> Response(303, [#("location", "https://example.com")], Empty)
506/// ```
507///
508pub fn redirect(to url: String) -> Response {
509 HttpResponse(303, [#("location", url)], Empty)
510}
511
512/// Create an empty response with status code 308: Moved Permanently, and the
513/// `location` header set to the given URL. Used to redirect the client to
514/// another page.
515///
516/// This redirect is permanent and the client is expected to cache the new
517/// location, using it for future requests.
518///
519/// # Examples
520///
521/// ```gleam
522/// moved_permanently(to: "https://example.com")
523/// // -> Response(308, [#("location", "https://example.com")], Empty)
524/// ```
525///
526pub fn moved_permanently(to url: String) -> Response {
527 HttpResponse(308, [#("location", url)], Empty)
528}
529
530/// Create an empty response with status code 204: No content.
531///
532/// # Examples
533///
534/// ```gleam
535/// no_content()
536/// // -> Response(204, [], Empty)
537/// ```
538///
539pub fn no_content() -> Response {
540 HttpResponse(204, [], Empty)
541}
542
543/// Create an empty response with status code 404: No content.
544///
545/// # Examples
546///
547/// ```gleam
548/// not_found()
549/// // -> Response(404, [], Empty)
550/// ```
551///
552pub fn not_found() -> Response {
553 HttpResponse(404, [], Empty)
554}
555
556/// Create an empty response with status code 400: Bad request.
557///
558/// # Examples
559///
560/// ```gleam
561/// bad_request()
562/// // -> Response(400, [], Empty)
563/// ```
564///
565pub fn bad_request() -> Response {
566 HttpResponse(400, [], Empty)
567}
568
569/// Create an empty response with status code 413: Entity too large.
570///
571/// # Examples
572///
573/// ```gleam
574/// entity_too_large()
575/// // -> Response(413, [], Empty)
576/// ```
577///
578pub fn entity_too_large() -> Response {
579 HttpResponse(413, [], Empty)
580}
581
582/// Create an empty response with status code 415: Unsupported media type.
583///
584/// The `allow` header will be set to a comma separated list of the permitted
585/// content-types.
586///
587/// # Examples
588///
589/// ```gleam
590/// unsupported_media_type(accept: ["application/json", "text/plain"])
591/// // -> Response(415, [#("allow", "application/json, text/plain")], Empty)
592/// ```
593///
594pub fn unsupported_media_type(accept acceptable: List(String)) -> Response {
595 let acceptable = string.join(acceptable, ", ")
596 HttpResponse(415, [#("accept", acceptable)], Empty)
597}
598
599/// Create an empty response with status code 422: Unprocessable entity.
600///
601/// # Examples
602///
603/// ```gleam
604/// unprocessable_entity()
605/// // -> Response(422, [], Empty)
606/// ```
607///
608pub fn unprocessable_entity() -> Response {
609 HttpResponse(422, [], Empty)
610}
611
612/// Create an empty response with status code 500: Internal server error.
613///
614/// # Examples
615///
616/// ```gleam
617/// internal_server_error()
618/// // -> Response(500, [], Empty)
619/// ```
620///
621pub fn internal_server_error() -> Response {
622 HttpResponse(500, [], Empty)
623}
624
625//
626// Requests
627//
628
629/// The connection to the client for a HTTP request.
630///
631/// The body of the request can be read from this connection using functions
632/// such as `require_multipart_body`.
633///
634pub type Connection =
635 internal.Connection
636
637type BufferedReader {
638 BufferedReader(reader: internal.Reader, buffer: BitArray)
639}
640
641type Quotas {
642 Quotas(body: Int, files: Int)
643}
644
645fn decrement_body_quota(quotas: Quotas, size: Int) -> Result(Quotas, Response) {
646 let quotas = Quotas(..quotas, body: quotas.body - size)
647 case quotas.body < 0 {
648 True -> Error(entity_too_large())
649 False -> Ok(quotas)
650 }
651}
652
653fn decrement_quota(quota: Int, size: Int) -> Result(Int, Response) {
654 case quota - size {
655 quota if quota < 0 -> Error(entity_too_large())
656 quota -> Ok(quota)
657 }
658}
659
660fn buffered_read(
661 reader: BufferedReader,
662 chunk_size: Int,
663) -> Result(internal.Read, Nil) {
664 case reader.buffer {
665 <<>> -> reader.reader(chunk_size)
666 _ -> Ok(internal.Chunk(reader.buffer, reader.reader))
667 }
668}
669
670/// Set the maximum permitted size of a request body of the request in bytes.
671///
672/// If a body is larger than this size attempting to read the body will result
673/// in a response with status code 413: Entity too large will be returned to the
674/// client.
675///
676/// This limit only applies for headers and bodies that get read into memory.
677/// Part of a multipart body that contain files and so are streamed to disc
678/// instead use the `max_files_size` limit.
679///
680pub fn set_max_body_size(request: Request, size: Int) -> Request {
681 internal.Connection(..request.body, max_body_size: size)
682 |> request.set_body(request, _)
683}
684
685/// Get the maximum permitted size of a request body of the request in bytes.
686///
687pub fn get_max_body_size(request: Request) -> Int {
688 request.body.max_body_size
689}
690
691/// Set the secret key base used to sign cookies and other sensitive data.
692///
693/// This key must be at least 64 bytes long and should be kept secret. Anyone
694/// with this secret will be able to manipulate signed cookies and other sensitive
695/// data.
696///
697/// # Panics
698///
699/// This function will panic if the key is less than 64 bytes long.
700///
701pub fn set_secret_key_base(request: Request, key: String) -> Request {
702 case string.byte_size(key) < 64 {
703 True -> panic as "Secret key base must be at least 64 bytes long"
704 False ->
705 internal.Connection(..request.body, secret_key_base: key)
706 |> request.set_body(request, _)
707 }
708}
709
710/// Get the secret key base used to sign cookies and other sensitive data.
711///
712pub fn get_secret_key_base(request: Request) -> String {
713 request.body.secret_key_base
714}
715
716/// Set the maximum permitted size of all files uploaded by a request, in bytes.
717///
718/// If a request contains fails which are larger in total than this size
719/// then attempting to read the body will result in a response with status code
720/// 413: Entity too large will be returned to the client.
721///
722/// This limit only applies for files in a multipart body that get streamed to
723/// disc. For headers and other content that gets read into memory use the
724/// `max_body_size` limit.
725///
726pub fn set_max_files_size(request: Request, size: Int) -> Request {
727 internal.Connection(..request.body, max_files_size: size)
728 |> request.set_body(request, _)
729}
730
731/// Get the maximum permitted total size of a files uploaded by a request in
732/// bytes.
733///
734pub fn get_max_files_size(request: Request) -> Int {
735 request.body.max_files_size
736}
737
738/// The the size limit for each chunk of the request body when read from the
739/// client.
740///
741/// This value is passed to the underlying web server when reading the body and
742/// the exact size of chunks read depends on the server implementation. It most
743/// likely will read chunks smaller than this size if not yet enough data has
744/// been received from the client.
745///
746pub fn set_read_chunk_size(request: Request, size: Int) -> Request {
747 internal.Connection(..request.body, read_chunk_size: size)
748 |> request.set_body(request, _)
749}
750
751/// Get the size limit for each chunk of the request body when read from the
752/// client.
753///
754pub fn get_read_chunk_size(request: Request) -> Int {
755 request.body.read_chunk_size
756}
757
758/// A convenient alias for a HTTP request with a Wisp connection as the body.
759///
760pub type Request =
761 HttpRequest(internal.Connection)
762
763/// This middleware function ensures that the request has a specific HTTP
764/// method, returning an empty response with status code 405: Method not allowed
765/// if the method is not correct.
766///
767/// # Examples
768///
769/// ```gleam
770/// fn handle_request(request: Request) -> Response {
771/// use <- wisp.require_method(request, http.Patch)
772/// // ...
773/// }
774/// ```
775///
776pub fn require_method(
777 request: HttpRequest(t),
778 method: Method,
779 next: fn() -> Response,
780) -> Response {
781 case request.method == method {
782 True -> next()
783 False -> method_not_allowed(allowed: [method])
784 }
785}
786
787// TODO: re-export once Gleam has a syntax for that
788/// Return the non-empty segments of a request path.
789///
790/// # Examples
791///
792/// ```gleam
793/// > request.new()
794/// > |> request.set_path("/one/two/three")
795/// > |> wisp.path_segments
796/// ["one", "two", "three"]
797/// ```
798///
799pub const path_segments = request.path_segments
800
801// TODO: re-export once Gleam has a syntax for that
802/// Set a given header to a given value, replacing any existing value.
803///
804/// # Examples
805///
806/// ```gleam
807/// > wisp.ok()
808/// > |> wisp.set_header("content-type", "application/json")
809/// Request(200, [#("content-type", "application/json")], Empty)
810/// ```
811///
812pub const set_header = response.set_header
813
814/// Parse the query parameters of a request into a list of key-value pairs. The
815/// `key_find` function in the `gleam/list` stdlib module may be useful for
816/// finding values in the list.
817///
818/// Query parameter names do not have to be unique and so may appear multiple
819/// times in the list.
820///
821pub fn get_query(request: Request) -> List(#(String, String)) {
822 request.get_query(request)
823 |> result.unwrap([])
824}
825
826/// This function overrides an incoming POST request with a method given in
827/// the request's `_method` query paramerter. This is useful as web browsers
828/// typically only support GET and POST requests, but our application may
829/// expect other HTTP methods that are more semantically correct.
830///
831/// The methods PUT, PATCH, and DELETE are accepted for overriding, all others
832/// are ignored.
833///
834/// The `_method` query paramerter can be specified in a HTML form like so:
835///
836/// <form method="POST" action="/item/1?_method=DELETE">
837/// <button type="submit">Delete item</button>
838/// </form>
839///
840/// # Examples
841///
842/// ```gleam
843/// fn handle_request(request: Request) -> Response {
844/// let request = wisp.method_override(request)
845/// // The method has now been overridden if appropriate
846/// }
847/// ```
848///
849pub fn method_override(request: HttpRequest(a)) -> HttpRequest(a) {
850 use <- bool.guard(when: request.method != http.Post, return: request)
851 {
852 use query <- result.try(request.get_query(request))
853 use value <- result.try(list.key_find(query, "_method"))
854 use method <- result.map(http.parse_method(value))
855
856 case method {
857 http.Put | http.Patch | http.Delete -> request.set_method(request, method)
858 _ -> request
859 }
860 }
861 |> result.unwrap(request)
862}
863
864// TODO: don't always return entity too large. Other errors are possible, such as
865// network errors.
866/// A middleware function which reads the entire body of the request as a string.
867///
868/// This function does not cache the body in any way, so if you call this
869/// function (or any other body reading function) more than once it may hang or
870/// return an incorrect value, depending on the underlying web server. It is the
871/// responsibility of the caller to cache the body if it is needed multiple
872/// times.
873///
874/// If the body is larger than the `max_body_size` limit then an empty response
875/// with status code 413: Entity too large will be returned to the client.
876///
877/// If the body is found not to be valid UTF-8 then an empty response with
878/// status code 400: Bad request will be returned to the client.
879///
880/// # Examples
881///
882/// ```gleam
883/// fn handle_request(request: Request) -> Response {
884/// use body <- wisp.require_string_body(request)
885/// // ...
886/// }
887/// ```
888///
889pub fn require_string_body(
890 request: Request,
891 next: fn(String) -> Response,
892) -> Response {
893 case read_body_to_bitstring(request) {
894 Ok(body) -> or_400(bit_array.to_string(body), next)
895 Error(_) -> entity_too_large()
896 }
897}
898
899// TODO: don't always return entity too large. Other errors are possible, such as
900// network errors.
901/// A middleware function which reads the entire body of the request as a bit
902/// string.
903///
904/// This function does not cache the body in any way, so if you call this
905/// function (or any other body reading function) more than once it may hang or
906/// return an incorrect value, depending on the underlying web server. It is the
907/// responsibility of the caller to cache the body if it is needed multiple
908/// times.
909///
910/// If the body is larger than the `max_body_size` limit then an empty response
911/// with status code 413: Entity too large will be returned to the client.
912///
913/// # Examples
914///
915/// ```gleam
916/// fn handle_request(request: Request) -> Response {
917/// use body <- wisp.require_string_body(request)
918/// // ...
919/// }
920/// ```
921///
922pub fn require_bit_array_body(
923 request: Request,
924 next: fn(BitArray) -> Response,
925) -> Response {
926 case read_body_to_bitstring(request) {
927 Ok(body) -> next(body)
928 Error(_) -> entity_too_large()
929 }
930}
931
932// TODO: don't always return entity to large. Other errors are possible, such as
933// network errors.
934/// Read the entire body of the request as a bit string.
935///
936/// You may instead wish to use the `require_bit_array_body` or the
937/// `require_string_body` middleware functions instead.
938///
939/// This function does not cache the body in any way, so if you call this
940/// function (or any other body reading function) more than once it may hang or
941/// return an incorrect value, depending on the underlying web server. It is the
942/// responsibility of the caller to cache the body if it is needed multiple
943/// times.
944///
945/// If the body is larger than the `max_body_size` limit then an empty response
946/// with status code 413: Entity too large will be returned to the client.
947///
948pub fn read_body_to_bitstring(request: Request) -> Result(BitArray, Nil) {
949 let connection = request.body
950 read_body_loop(
951 connection.reader,
952 connection.read_chunk_size,
953 connection.max_body_size,
954 <<>>,
955 )
956}
957
958fn read_body_loop(
959 reader: internal.Reader,
960 read_chunk_size: Int,
961 max_body_size: Int,
962 accumulator: BitArray,
963) -> Result(BitArray, Nil) {
964 use chunk <- result.try(reader(read_chunk_size))
965 case chunk {
966 internal.ReadingFinished -> Ok(accumulator)
967 internal.Chunk(chunk, next) -> {
968 let accumulator = bit_array.append(accumulator, chunk)
969 case bit_array.byte_size(accumulator) > max_body_size {
970 True -> Error(Nil)
971 False ->
972 read_body_loop(next, read_chunk_size, max_body_size, accumulator)
973 }
974 }
975 }
976}
977
978/// A middleware which extracts form data from the body of a request that is
979/// encoded as either `application/x-www-form-urlencoded` or
980/// `multipart/form-data`.
981///
982/// Extracted fields are sorted into alphabetical order by key, so if you wish
983/// to use pattern matching the order can be relied upon.
984///
985/// ```gleam
986/// fn handle_request(request: Request) -> Response {
987/// use form <- wisp.require_form(request)
988/// case form.values {
989/// [#("password", pass), #("username", username)] -> // ...
990/// _ -> // ...
991/// }
992/// }
993/// ```
994///
995/// The `set_max_body_size`, `set_max_files_size`, and `set_read_chunk_size` can
996/// be used to configure the reading of the request body.
997///
998/// Any file uploads will streamed into temporary files on disc. These files are
999/// automatically deleted when the request handler returns, so if you wish to
1000/// use them after the request has completed you will need to move them to a new
1001/// location.
1002///
1003/// If the request does not have a recognised `content-type` header then an
1004/// empty response with status code 415: Unsupported media type will be returned
1005/// to the client.
1006///
1007/// If the request body is larger than the `max_body_size` or `max_files_size`
1008/// limits then an empty response with status code 413: Entity too large will be
1009/// returned to the client.
1010///
1011/// If the body cannot be parsed successfully then an empty response with status
1012/// code 400: Bad request will be returned to the client.
1013///
1014pub fn require_form(
1015 request: Request,
1016 next: fn(FormData) -> Response,
1017) -> Response {
1018 case list.key_find(request.headers, "content-type") {
1019 Ok("application/x-www-form-urlencoded")
1020 | Ok("application/x-www-form-urlencoded;" <> _) ->
1021 require_urlencoded_form(request, next)
1022
1023 Ok("multipart/form-data; boundary=" <> boundary) ->
1024 require_multipart_form(request, boundary, next)
1025
1026 Ok("multipart/form-data") -> bad_request()
1027
1028 _ ->
1029 unsupported_media_type([
1030 "application/x-www-form-urlencoded", "multipart/form-data",
1031 ])
1032 }
1033}
1034
1035/// This middleware function ensures that the request has a value for the
1036/// `content-type` header, returning an empty response with status code 415:
1037/// Unsupported media type if the header is not the expected value
1038///
1039/// # Examples
1040///
1041/// ```gleam
1042/// fn handle_request(request: Request) -> Response {
1043/// use <- wisp.require_content_type(request, "application/json")
1044/// // ...
1045/// }
1046/// ```
1047///
1048pub fn require_content_type(
1049 request: Request,
1050 expected: String,
1051 next: fn() -> Response,
1052) -> Response {
1053 case list.key_find(request.headers, "content-type") {
1054 Ok(content_type) ->
1055 // This header may have further such as `; charset=utf-8`, so discard
1056 // that if it exists.
1057 case string.split_once(content_type, ";") {
1058 Ok(#(content_type, _)) if content_type == expected -> next()
1059 _ if content_type == expected -> next()
1060 _ -> unsupported_media_type([expected])
1061 }
1062
1063 _ -> unsupported_media_type([expected])
1064 }
1065}
1066
1067/// A middleware which extracts JSON from the body of a request.
1068///
1069/// ```gleam
1070/// fn handle_request(request: Request) -> Response {
1071/// use json <- wisp.require_json(request)
1072/// // decode and use JSON here...
1073/// }
1074/// ```
1075///
1076/// The `set_max_body_size` and `set_read_chunk_size` can be used to configure
1077/// the reading of the request body.
1078///
1079/// If the request does not have the `content-type` set to `application/json` an
1080/// empty response with status code 415: Unsupported media type will be returned
1081/// to the client.
1082///
1083/// If the request body is larger than the `max_body_size` or `max_files_size`
1084/// limits then an empty response with status code 413: Entity too large will be
1085/// returned to the client.
1086///
1087/// If the body cannot be parsed successfully then an empty response with status
1088/// code 400: Bad request will be returned to the client.
1089///
1090pub fn require_json(request: Request, next: fn(Dynamic) -> Response) -> Response {
1091 use <- require_content_type(request, "application/json")
1092 use body <- require_string_body(request)
1093 use json <- or_400(json.decode(body, Ok))
1094 next(json)
1095}
1096
1097fn require_urlencoded_form(
1098 request: Request,
1099 next: fn(FormData) -> Response,
1100) -> Response {
1101 use body <- require_string_body(request)
1102 use pairs <- or_400(uri.parse_query(body))
1103 let pairs = sort_keys(pairs)
1104 next(FormData(values: pairs, files: []))
1105}
1106
1107fn require_multipart_form(
1108 request: Request,
1109 boundary: String,
1110 next: fn(FormData) -> Response,
1111) -> Response {
1112 let quotas =
1113 Quotas(files: request.body.max_files_size, body: request.body.max_body_size)
1114 let reader = BufferedReader(request.body.reader, <<>>)
1115
1116 let result =
1117 read_multipart(request, reader, boundary, quotas, FormData([], []))
1118 case result {
1119 Ok(form_data) -> next(form_data)
1120 Error(response) -> response
1121 }
1122}
1123
1124fn read_multipart(
1125 request: Request,
1126 reader: BufferedReader,
1127 boundary: String,
1128 quotas: Quotas,
1129 data: FormData,
1130) -> Result(FormData, Response) {
1131 let read_size = request.body.read_chunk_size
1132
1133 // First we read the headers of the multipart part.
1134 let header_parser =
1135 fn_with_bad_request_error(http.parse_multipart_headers(_, boundary))
1136 let result = multipart_headers(reader, header_parser, read_size, quotas)
1137 use #(headers, reader, quotas) <- result.try(result)
1138 use #(name, filename) <- result.try(multipart_content_disposition(headers))
1139
1140 // Then we read the body of the part.
1141 let parse = fn_with_bad_request_error(http.parse_multipart_body(_, boundary))
1142 use #(data, reader, quotas) <- result.try(case filename {
1143 // There is a file name, so we treat this as a file upload, streaming the
1144 // contents to a temporary file and using the dedicated files size quota.
1145 option.Some(file_name) -> {
1146 use path <- result.try(or_500(new_temporary_file(request)))
1147 let append = multipart_file_append
1148 let q = quotas.files
1149 let result =
1150 multipart_body(reader, parse, boundary, read_size, q, append, path)
1151 use #(reader, quota, _) <- result.map(result)
1152 let quotas = Quotas(..quotas, files: quota)
1153 let file = UploadedFile(path: path, file_name: file_name)
1154 let data = FormData(..data, files: [#(name, file), ..data.files])
1155 #(data, reader, quotas)
1156 }
1157
1158 // No file name, this is a regular form value that we hold in memory.
1159 option.None -> {
1160 let append = fn(data, chunk) { Ok(bit_array.append(data, chunk)) }
1161 let q = quotas.body
1162 let result =
1163 multipart_body(reader, parse, boundary, read_size, q, append, <<>>)
1164 use #(reader, quota, value) <- result.try(result)
1165 let quotas = Quotas(..quotas, body: quota)
1166 use value <- result.map(bit_array_to_string(value))
1167 let data = FormData(..data, values: [#(name, value), ..data.values])
1168 #(data, reader, quotas)
1169 }
1170 })
1171
1172 case reader {
1173 // There's at least one more part, read it.
1174 option.Some(reader) ->
1175 read_multipart(request, reader, boundary, quotas, data)
1176 // There are no more parts, we're done.
1177 option.None -> Ok(FormData(sort_keys(data.values), sort_keys(data.files)))
1178 }
1179}
1180
1181fn bit_array_to_string(bits: BitArray) -> Result(String, Response) {
1182 bit_array.to_string(bits)
1183 |> result.replace_error(bad_request())
1184}
1185
1186fn multipart_file_append(
1187 path: String,
1188 chunk: BitArray,
1189) -> Result(String, Response) {
1190 simplifile.append_bits(path, chunk)
1191 |> or_500
1192 |> result.replace(path)
1193}
1194
1195fn or_500(result: Result(a, b)) -> Result(a, Response) {
1196 case result {
1197 Ok(value) -> Ok(value)
1198 Error(error) -> {
1199 log_error(string.inspect(error))
1200 Error(internal_server_error())
1201 }
1202 }
1203}
1204
1205fn multipart_body(
1206 reader: BufferedReader,
1207 parse: fn(BitArray) -> Result(http.MultipartBody, Response),
1208 boundary: String,
1209 chunk_size: Int,
1210 quota: Int,
1211 append: fn(t, BitArray) -> Result(t, Response),
1212 data: t,
1213) -> Result(#(Option(BufferedReader), Int, t), Response) {
1214 use #(chunk, reader) <- result.try(read_chunk(reader, chunk_size))
1215 let size_read = bit_array.byte_size(chunk)
1216 use output <- result.try(parse(chunk))
1217
1218 case output {
1219 http.MultipartBody(parsed, done, remaining) -> {
1220 // Decrement the quota by the number of bytes consumed.
1221 let used = size_read - bit_array.byte_size(remaining) - 2
1222 let used = case done {
1223 // If this is the last chunk, we need to account for the boundary.
1224 True -> used - 4 - string.byte_size(boundary)
1225 False -> used
1226 }
1227 use quota <- result.try(decrement_quota(quota, used))
1228
1229 let reader = BufferedReader(reader, remaining)
1230 let reader = case done {
1231 True -> option.None
1232 False -> option.Some(reader)
1233 }
1234 use value <- result.map(append(data, parsed))
1235 #(reader, quota, value)
1236 }
1237
1238 http.MoreRequiredForBody(chunk, parse) -> {
1239 let parse = fn_with_bad_request_error(parse(_))
1240 let reader = BufferedReader(reader, <<>>)
1241 use data <- result.try(append(data, chunk))
1242 multipart_body(reader, parse, boundary, chunk_size, quota, append, data)
1243 }
1244 }
1245}
1246
1247fn fn_with_bad_request_error(
1248 f: fn(a) -> Result(b, c),
1249) -> fn(a) -> Result(b, Response) {
1250 fn(a) {
1251 f(a)
1252 |> result.replace_error(bad_request())
1253 }
1254}
1255
1256fn multipart_content_disposition(
1257 headers: List(http.Header),
1258) -> Result(#(String, Option(String)), Response) {
1259 {
1260 use header <- result.try(list.key_find(headers, "content-disposition"))
1261 use header <- result.try(http.parse_content_disposition(header))
1262 use name <- result.map(list.key_find(header.parameters, "name"))
1263 let filename =
1264 option.from_result(list.key_find(header.parameters, "filename"))
1265 #(name, filename)
1266 }
1267 |> result.replace_error(bad_request())
1268}
1269
1270fn read_chunk(
1271 reader: BufferedReader,
1272 chunk_size: Int,
1273) -> Result(#(BitArray, internal.Reader), Response) {
1274 buffered_read(reader, chunk_size)
1275 |> result.replace_error(bad_request())
1276 |> result.try(fn(chunk) {
1277 case chunk {
1278 internal.Chunk(chunk, next) -> Ok(#(chunk, next))
1279 internal.ReadingFinished -> Error(bad_request())
1280 }
1281 })
1282}
1283
1284fn multipart_headers(
1285 reader: BufferedReader,
1286 parse: fn(BitArray) -> Result(http.MultipartHeaders, Response),
1287 chunk_size: Int,
1288 quotas: Quotas,
1289) -> Result(#(List(http.Header), BufferedReader, Quotas), Response) {
1290 use #(chunk, reader) <- result.try(read_chunk(reader, chunk_size))
1291 use headers <- result.try(parse(chunk))
1292
1293 case headers {
1294 http.MultipartHeaders(headers, remaining) -> {
1295 let used = bit_array.byte_size(chunk) - bit_array.byte_size(remaining)
1296 use quotas <- result.map(decrement_body_quota(quotas, used))
1297 let reader = BufferedReader(reader, remaining)
1298 #(headers, reader, quotas)
1299 }
1300 http.MoreRequiredForHeaders(parse) -> {
1301 let parse = fn(chunk) {
1302 parse(chunk)
1303 |> result.replace_error(bad_request())
1304 }
1305 let reader = BufferedReader(reader, <<>>)
1306 multipart_headers(reader, parse, chunk_size, quotas)
1307 }
1308 }
1309}
1310
1311fn sort_keys(pairs: List(#(String, t))) -> List(#(String, t)) {
1312 list.sort(pairs, fn(a, b) { string.compare(a.0, b.0) })
1313}
1314
1315fn or_400(result: Result(value, error), next: fn(value) -> Response) -> Response {
1316 case result {
1317 Ok(value) -> next(value)
1318 Error(_) -> bad_request()
1319 }
1320}
1321
1322/// Data parsed from form sent in a request's body.
1323///
1324pub type FormData {
1325 FormData(
1326 /// String values of the form's fields.
1327 values: List(#(String, String)),
1328 /// Uploaded files.
1329 files: List(#(String, UploadedFile)),
1330 )
1331}
1332
1333pub type UploadedFile {
1334 UploadedFile(
1335 /// The name that was given to the file in the form.
1336 /// This is user input and should not be trusted.
1337 file_name: String,
1338 /// The location of the file on the server.
1339 /// This is a temporary file and will be deleted when the request has
1340 /// finished being handled.
1341 path: String,
1342 )
1343}
1344
1345//
1346// Middleware
1347//
1348
1349/// A middleware function that rescues crashes and returns an empty response
1350/// with status code 500: Internal server error.
1351///
1352/// # Examples
1353///
1354/// ```gleam
1355/// fn handle_request(req: Request) -> Response {
1356/// use <- wisp.rescue_crashes
1357/// // ...
1358/// }
1359/// ```
1360///
1361pub fn rescue_crashes(handler: fn() -> Response) -> Response {
1362 case exception.rescue(handler) {
1363 Ok(response) -> response
1364 Error(error) -> {
1365 let #(kind, detail) = case error {
1366 exception.Errored(detail) -> #(Errored, detail)
1367 exception.Thrown(detail) -> #(Thrown, detail)
1368 exception.Exited(detail) -> #(Exited, detail)
1369 }
1370 case dynamic.dict(atom.from_dynamic, Ok)(detail) {
1371 Ok(details) -> {
1372 let c = atom.create_from_string("class")
1373 log_error_dict(dict.insert(details, c, dynamic.from(kind)))
1374 Nil
1375 }
1376 Error(_) -> log_error(string.inspect(error))
1377 }
1378 internal_server_error()
1379 }
1380 }
1381}
1382
1383type DoNotLeak
1384
1385@external(erlang, "logger", "error")
1386fn log_error_dict(o: Dict(Atom, Dynamic)) -> DoNotLeak
1387
1388type ErrorKind {
1389 Errored
1390 Thrown
1391 Exited
1392}
1393
1394// TODO: test, somehow.
1395/// A middleware function that logs details about the request and response.
1396///
1397/// The format used logged by this middleware may change in future versions of
1398/// Wisp.
1399///
1400/// # Examples
1401///
1402/// ```gleam
1403/// fn handle_request(req: Request) -> Response {
1404/// use <- wisp.log_request(req)
1405/// // ...
1406/// }
1407/// ```
1408///
1409pub fn log_request(req: Request, handler: fn() -> Response) -> Response {
1410 let response = handler()
1411 [
1412 int.to_string(response.status),
1413 " ",
1414 string.uppercase(http.method_to_string(req.method)),
1415 " ",
1416 req.path,
1417 ]
1418 |> string.concat
1419 |> log_info
1420 response
1421}
1422
1423/// A middleware function that serves files from a directory, along with a
1424/// suitable `content-type` header for known file extensions.
1425///
1426/// Files are sent using the `File` response body type, so they will be sent
1427/// directly to the client from the disc, without being read into memory.
1428///
1429/// The `under` parameter is the request path prefix that must match for the
1430/// file to be served.
1431///
1432/// | `under` | `from` | `request.path` | `file` |
1433/// |-----------|---------|--------------------|-------------------------|
1434/// | `/static` | `/data` | `/static/file.txt` | `/data/file.txt` |
1435/// | `` | `/data` | `/static/file.txt` | `/data/static/file.txt` |
1436/// | `/static` | `` | `/static/file.txt` | `file.txt` |
1437///
1438/// This middleware will discard any `..` path segments in the request path to
1439/// prevent the client from accessing files outside of the directory. It is
1440/// advised not to serve a directory that contains your source code, application
1441/// configuration, database, or other private files.
1442///
1443/// # Examples
1444///
1445/// ```gleam
1446/// fn handle_request(req: Request) -> Response {
1447/// use <- wisp.serve_static(req, under: "/static", from: "/public")
1448/// // ...
1449/// }
1450/// ```
1451///
1452/// Typically you static assets may be kept in your project in a directory
1453/// called `priv`. The `priv_directory` function can be used to get a path to
1454/// this directory.
1455///
1456/// ```gleam
1457/// fn handle_request(req: Request) -> Response {
1458/// let assert Ok(priv) = priv_directory("my_application")
1459/// use <- wisp.serve_static(req, under: "/static", from: priv)
1460/// // ...
1461/// }
1462/// ```
1463///
1464pub fn serve_static(
1465 req: Request,
1466 under prefix: String,
1467 from directory: String,
1468 next handler: fn() -> Response,
1469) -> Response {
1470 let path = internal.remove_preceeding_slashes(req.path)
1471 let prefix = internal.remove_preceeding_slashes(prefix)
1472 case req.method, string.starts_with(path, prefix) {
1473 http.Get, True -> {
1474 let path =
1475 path
1476 |> string.drop_start(string.length(prefix))
1477 |> string.replace(each: "..", with: "")
1478 |> internal.join_path(directory, _)
1479
1480 let mime_type =
1481 req.path
1482 |> string.split(on: ".")
1483 |> list.last
1484 |> result.unwrap("")
1485 |> marceau.extension_to_mime_type
1486
1487 let content_type = case mime_type {
1488 "application/json" | "text/" <> _ -> mime_type <> "; charset=utf-8"
1489 _ -> mime_type
1490 }
1491
1492 case simplifile.is_file(path) {
1493 Ok(True) ->
1494 response.new(200)
1495 |> response.set_header("content-type", content_type)
1496 |> response.set_body(File(path))
1497 _ -> handler()
1498 }
1499 }
1500 _, _ -> handler()
1501 }
1502}
1503
1504/// A middleware function that converts `HEAD` requests to `GET` requests,
1505/// handles the request, and then discards the response body. This is useful so
1506/// that your application can handle `HEAD` requests without having to implement
1507/// handlers for them.
1508///
1509/// The `x-original-method` header is set to `"HEAD"` for requests that were
1510/// originally `HEAD` requests.
1511///
1512/// # Examples
1513///
1514/// ```gleam
1515/// fn handle_request(req: Request) -> Response {
1516/// use req <- wisp.handle_head(req)
1517/// // ...
1518/// }
1519/// ```
1520///
1521pub fn handle_head(
1522 req: Request,
1523 next handler: fn(Request) -> Response,
1524) -> Response {
1525 case req.method {
1526 http.Head ->
1527 req
1528 |> request.set_method(http.Get)
1529 |> request.prepend_header("x-original-method", "HEAD")
1530 |> handler
1531 |> response.set_body(Empty)
1532 _ -> handler(req)
1533 }
1534}
1535
1536//
1537// File uploads
1538//
1539
1540/// Create a new temporary directory for the given request.
1541///
1542/// If you are using the Mist adapter or another compliant web server
1543/// adapter then this file will be deleted for you when the request is complete.
1544/// Otherwise you will need to call the `delete_temporary_files` function
1545/// yourself.
1546///
1547pub fn new_temporary_file(
1548 request: Request,
1549) -> Result(String, simplifile.FileError) {
1550 let directory = request.body.temporary_directory
1551 use _ <- result.try(simplifile.create_directory_all(directory))
1552 let path = internal.join_path(directory, internal.random_slug())
1553 use _ <- result.map(simplifile.create_file(path))
1554 path
1555}
1556
1557/// Delete any temporary files created for the given request.
1558///
1559/// If you are using the Mist adapter or another compliant web server
1560/// adapter then this file will be deleted for you when the request is complete.
1561/// Otherwise you will need to call this function yourself.
1562///
1563pub fn delete_temporary_files(
1564 request: Request,
1565) -> Result(Nil, simplifile.FileError) {
1566 case simplifile.delete(request.body.temporary_directory) {
1567 Error(simplifile.Enoent) -> Ok(Nil)
1568 other -> other
1569 }
1570}
1571
1572/// Returns the path of a package's `priv` directory, where extra non-Gleam
1573/// or Erlang files are typically kept.
1574///
1575/// Returns an error if no package was found with the given name.
1576///
1577/// # Example
1578///
1579/// ```gleam
1580/// > erlang.priv_directory("my_app")
1581/// // -> Ok("/some/location/my_app/priv")
1582/// ```
1583///
1584pub const priv_directory = erlang.priv_directory
1585
1586//
1587// Logging
1588//
1589
1590/// Configure the Erlang logger, setting the minimum log level to `info`, to be
1591/// called when your application starts.
1592///
1593/// You may wish to use an alternative for this such as one provided by a more
1594/// sophisticated logging library.
1595///
1596/// In future this function may be extended to change the output format.
1597///
1598pub fn configure_logger() -> Nil {
1599 logging.configure()
1600}
1601
1602/// Type to set the log level of the Erlang's logger
1603///
1604/// See the [Erlang logger documentation][1] for more information.
1605///
1606/// [1]: https://www.erlang.org/doc/man/logger
1607///
1608pub type LogLevel {
1609 EmergencyLevel
1610 AlertLevel
1611 CriticalLevel
1612 ErrorLevel
1613 WarningLevel
1614 NoticeLevel
1615 InfoLevel
1616 DebugLevel
1617}
1618
1619fn log_level_to_logging_log_level(log_level: LogLevel) -> logging.LogLevel {
1620 case log_level {
1621 EmergencyLevel -> logging.Emergency
1622 AlertLevel -> logging.Alert
1623 CriticalLevel -> logging.Critical
1624 ErrorLevel -> logging.Error
1625 WarningLevel -> logging.Warning
1626 NoticeLevel -> logging.Notice
1627 InfoLevel -> logging.Info
1628 DebugLevel -> logging.Debug
1629 }
1630}
1631
1632/// Set the log level of the Erlang logger to `log_level`.
1633///
1634/// See the [Erlang logger documentation][1] for more information.
1635///
1636/// [1]: https://www.erlang.org/doc/man/logger
1637///
1638pub fn set_logger_level(log_level: LogLevel) -> Nil {
1639 logging.set_level(log_level_to_logging_log_level(log_level))
1640}
1641
1642/// Log a message to the Erlang logger with the level of `emergency`.
1643///
1644/// See the [Erlang logger documentation][1] for more information.
1645///
1646/// [1]: https://www.erlang.org/doc/man/logger
1647///
1648pub fn log_emergency(message: String) -> Nil {
1649 logging.log(logging.Emergency, message)
1650}
1651
1652/// Log a message to the Erlang logger with the level of `alert`.
1653///
1654/// See the [Erlang logger documentation][1] for more information.
1655///
1656/// [1]: https://www.erlang.org/doc/man/logger
1657///
1658pub fn log_alert(message: String) -> Nil {
1659 logging.log(logging.Alert, message)
1660}
1661
1662/// Log a message to the Erlang logger with the level of `critical`.
1663///
1664/// See the [Erlang logger documentation][1] for more information.
1665///
1666/// [1]: https://www.erlang.org/doc/man/logger
1667///
1668pub fn log_critical(message: String) -> Nil {
1669 logging.log(logging.Critical, message)
1670}
1671
1672/// Log a message to the Erlang logger with the level of `error`.
1673///
1674/// See the [Erlang logger documentation][1] for more information.
1675///
1676/// [1]: https://www.erlang.org/doc/man/logger
1677///
1678pub fn log_error(message: String) -> Nil {
1679 logging.log(logging.Error, message)
1680}
1681
1682/// Log a message to the Erlang logger with the level of `warning`.
1683///
1684/// See the [Erlang logger documentation][1] for more information.
1685///
1686/// [1]: https://www.erlang.org/doc/man/logger
1687///
1688pub fn log_warning(message: String) -> Nil {
1689 logging.log(logging.Warning, message)
1690}
1691
1692/// Log a message to the Erlang logger with the level of `notice`.
1693///
1694/// See the [Erlang logger documentation][1] for more information.
1695///
1696/// [1]: https://www.erlang.org/doc/man/logger
1697///
1698pub fn log_notice(message: String) -> Nil {
1699 logging.log(logging.Notice, message)
1700}
1701
1702/// Log a message to the Erlang logger with the level of `info`.
1703///
1704/// See the [Erlang logger documentation][1] for more information.
1705///
1706/// [1]: https://www.erlang.org/doc/man/logger
1707///
1708pub fn log_info(message: String) -> Nil {
1709 logging.log(logging.Info, message)
1710}
1711
1712/// Log a message to the Erlang logger with the level of `debug`.
1713///
1714/// See the [Erlang logger documentation][1] for more information.
1715///
1716/// [1]: https://www.erlang.org/doc/man/logger
1717///
1718pub fn log_debug(message: String) -> Nil {
1719 logging.log(logging.Debug, message)
1720}
1721
1722//
1723// Cryptography
1724//
1725
1726/// Generate a random string of the given length.
1727///
1728pub fn random_string(length: Int) -> String {
1729 internal.random_string(length)
1730}
1731
1732/// Sign a message which can later be verified using the `verify_signed_message`
1733/// function to detect if the message has been tampered with.
1734///
1735/// Signed messages are not encrypted and can be read by anyone. They are not
1736/// suitable for storing sensitive information.
1737///
1738/// This function uses the secret key base from the request. If the secret
1739/// changes then the signature will no longer be verifiable.
1740///
1741pub fn sign_message(
1742 request: Request,
1743 message: BitArray,
1744 algorithm: crypto.HashAlgorithm,
1745) -> String {
1746 crypto.sign_message(message, <<request.body.secret_key_base:utf8>>, algorithm)
1747}
1748
1749/// Verify a signed message which was signed using the `sign_message` function.
1750///
1751/// Returns the content of the message if the signature is valid, otherwise
1752/// returns an error.
1753///
1754/// This function uses the secret key base from the request. If the secret
1755/// changes then the signature will no longer be verifiable.
1756///
1757pub fn verify_signed_message(
1758 request: Request,
1759 message: String,
1760) -> Result(BitArray, Nil) {
1761 crypto.verify_signed_message(message, <<request.body.secret_key_base:utf8>>)
1762}
1763
1764//
1765// Cookies
1766//
1767
1768/// Set a cookie on the response. After `max_age` seconds the cookie will be
1769/// expired by the client.
1770///
1771/// This function will sign the value if the `security` parameter is set to
1772/// `Signed`, making it so the cookie cannot be tampered with by the client.
1773///
1774/// Values are base64 encoded so they can contain any characters you want, even
1775/// if they would not be permitted directly in a cookie.
1776///
1777/// Cookies are set using `gleam_http`'s default attributes for HTTPS. If you
1778/// wish for more control over the cookie attributes then you may want to use
1779/// the `gleam/http/cookie` module from the `gleam_http` package instead of this
1780/// function. Be sure to sign and escape the cookie value as needed.
1781///
1782/// # Examples
1783///
1784/// Setting a plain text cookie that the client can read and modify:
1785///
1786/// ```gleam
1787/// wisp.ok()
1788/// |> wisp.set_cookie(request, "id", "123", wisp.PlainText, 60 * 60)
1789/// ```
1790///
1791/// Setting a signed cookie that the client can read but not modify:
1792///
1793/// ```gleam
1794/// wisp.ok()
1795/// |> wisp.set_cookie(request, "id", value, wisp.Signed, 60 * 60)
1796/// ```
1797///
1798pub fn set_cookie(
1799 response response: Response,
1800 request request: Request,
1801 name name: String,
1802 value value: String,
1803 security security: Security,
1804 max_age max_age: Int,
1805) -> Response {
1806 let attributes =
1807 cookie.Attributes(
1808 ..cookie.defaults(http.Https),
1809 max_age: option.Some(max_age),
1810 )
1811 let value = case security {
1812 PlainText -> bit_array.base64_encode(<<value:utf8>>, False)
1813 Signed -> sign_message(request, <<value:utf8>>, crypto.Sha512)
1814 }
1815 response
1816 |> response.set_cookie(name, value, attributes)
1817}
1818
1819pub type Security {
1820 /// The value is store as plain text without any additional security.
1821 /// The client will be able to read and modify the value, and create new values.
1822 PlainText
1823 /// The value is signed to prevent modification.
1824 /// The client will be able to read the value but not modify it, or create new
1825 /// values.
1826 Signed
1827}
1828
1829/// Get a cookie from the request.
1830///
1831/// If a cookie is missing, found to be malformed, or the signature is invalid
1832/// for a signed cookie, then `Error(Nil)` is returned.
1833///
1834/// ```gleam
1835/// wisp.get_cookie(request, "group", wisp.PlainText)
1836/// // -> Ok("A")
1837/// ```
1838///
1839pub fn get_cookie(
1840 request: Request,
1841 name: String,
1842 security: Security,
1843) -> Result(String, Nil) {
1844 use value <- result.try(
1845 request
1846 |> request.get_cookies
1847 |> list.key_find(name),
1848 )
1849 use value <- result.try(case security {
1850 PlainText -> bit_array.base64_decode(value)
1851 Signed -> verify_signed_message(request, value)
1852 })
1853 bit_array.to_string(value)
1854}
1855
1856//
1857// Testing
1858//
1859
1860// TODO: chunk the body
1861/// Create a connection which will return the given body when read.
1862///
1863/// This function is intended for use in tests, though you probably want the
1864/// `wisp/testing` module instead.
1865///
1866pub fn create_canned_connection(
1867 body: BitArray,
1868 secret_key_base: String,
1869) -> internal.Connection {
1870 internal.make_connection(
1871 fn(_size) {
1872 Ok(internal.Chunk(body, fn(_size) { Ok(internal.ReadingFinished) }))
1873 },
1874 secret_key_base,
1875 )
1876}