馃 A practical web framework for Gleam
at main 55 kB view raw
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/// // -> "&lt;h1&gt;Hello, Joe!&lt;/h1&gt;" 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 = [<<"&lt;":utf8>>, ..acc] 347 do_escape_html(rest, skip + 1, original, acc) 348 } 349 350 <<">":utf8, rest:bits>> -> { 351 let acc = [<<"&gt;":utf8>>, ..acc] 352 do_escape_html(rest, skip + 1, original, acc) 353 } 354 355 <<"&":utf8, rest:bits>> -> { 356 let acc = [<<"&amp;":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 = [<<"&lt;":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 = [<<"&gt;":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 = [<<"&amp;":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}