A CORS Builder, performing validation and injection of CORS for misp, wisp and any framework!

First commit

+23
.github/workflows/test.yml
··· 1 + name: test 2 + 3 + on: 4 + push: 5 + branches: 6 + - master 7 + - main 8 + pull_request: 9 + 10 + jobs: 11 + test: 12 + runs-on: ubuntu-latest 13 + steps: 14 + - uses: actions/checkout@v4 15 + - uses: erlef/setup-beam@v1 16 + with: 17 + otp-version: "26.0.2" 18 + gleam-version: "1.1.0" 19 + rebar3-version: "3" 20 + # elixir-version: "1.15.4" 21 + - run: gleam deps download 22 + - run: gleam test 23 + - run: gleam format --check src test
+4
.gitignore
··· 1 + *.beam 2 + *.ez 3 + /build 4 + erl_crash.dump
+1
.prettierrc
··· 1 + { "printWidth": 80, "proseWrap": "always" }
+108
README.md
··· 1 + # cors 2 + 3 + ## Middlewares 4 + 5 + ```gleam 6 + import gleam/http 7 + import mist 8 + import simple_cors as cors 9 + import wisp.{type Request, type Response} 10 + 11 + fn cors() { 12 + cors.new() 13 + |> cors.allow_origin("http://localhost:3000") 14 + |> cors.allow_origin("http://localhost:4000") 15 + |> cors.allow_method(http.Get) 16 + |> cors.allow_method(http.Post) 17 + } 18 + 19 + fn handler(req: Request) -> Response { 20 + use req <- cors.wisp_handle(req, cors()) 21 + wisp.ok() 22 + } 23 + 24 + fn main() { 25 + handler 26 + |> wisp.mist_handler(secret_key) 27 + |> mist.new() 28 + |> mist.port(3000) 29 + |> mist.start_http() 30 + } 31 + ``` 32 + 33 + ## What is CORS? 34 + 35 + Browsers apply a simple rules for every HTTP request: when the request 36 + originates from a different origin than the target server URL — and if it's not 37 + a simple request — the browser needs to authorize the cross-origin call. 38 + 39 + > From the HTTP point of view, a simple request respects the following 40 + > conditions: 41 + > 42 + > - Allowed methods are `GET`, `HEAD` or `POST` 43 + > - Allowed headers are `Accept`, `Accept-Language`, `Content-Language` and 44 + > `Content-Type` 45 + > - `Content-Type` should be: 46 + > - `application/x-www-form-urlencoded` 47 + > - `multipart/form-data` 48 + > - `text/plain` 49 + > - No event listener has been added on `XMLHttpRequestUpload`. 50 + > `XMLHttpRequestUpload.upload` is preferred. 51 + > - No `ReadableStream` is used in the request. 52 + 53 + To authorize the call, the browser will issue a first request, called a 54 + "preflight" request. This request takes the form of an `OPTIONS` request, which 55 + should be answered positively by the server (meaning the response status code 56 + should be 2XX) and should contains the appropriate CORS headers 57 + (`Access-Control` headers). 58 + 59 + In case the preflight request is not successful, the server will simply cancel 60 + the HTTP request. But if the preflight request is successful, then the browser 61 + will then launch the real request, and the server will be able to handle it. 62 + 63 + ## What are the headers? 64 + 65 + We distinguish different types of headers: the headers concerning the request 66 + issuer (the caller) and the headers responded by the server. 67 + 68 + ### Response headers 69 + 70 + Response headers are not automatically set by the server, and you should handle 71 + them according on what you want to do. This package tries to abstract it to 72 + simplify your development and let you focus on your application. We count 6 CORS 73 + response headers: 74 + 75 + - `Access-Control-Allow-Origin`, indicates which origins are allowed to access 76 + the server. It can be a joker (`"*"`) or a unique domain 77 + (`https://gleam.run`). It cannot contains multiple domains, but can response 78 + to multiple different domains with the `VARY` header. You should not have to 79 + take care of this, because the library provides it for you. 80 + - `Access-Control-Expose-Headers`, provides a whitelist of allowed headers for 81 + the browsers. Only the headers in the whitelist will be able to be used in the 82 + response object in the JS code. It means if the response contains headers you 83 + want to cache to the client, you can use this header. 84 + - `Access-Control-Max-Age`, allows to put the preflight response in cache, for a 85 + specified amount of time. This avoids to rerun the `OPTIONS` request multiple 86 + times. 87 + - `Access-Control-Allow-Credentials`, allows the request to includes credentials 88 + authorizations. This can expose you to CSRF attack. Never activate this option 89 + unless you carefully know what you're doing. 90 + - `Access-Control-Allow-Methods`, provides a whitelist of subsequent authorized 91 + methods in the future requests. 92 + - `Access-Control-Allow-Headers`, indicates which headers are accepted by the 93 + server, and thus, which headers the browser will be able to send in subsequent 94 + requests. 95 + 96 + ### Request headers 97 + 98 + Request headers are headers automatically set by the browser, when issuing a 99 + request with `XMLHttpRequest` or `fetch`. You should not bother about it, but 100 + they're still referenced it, in case you encounter them.We count 3 CORS request 101 + headers: 102 + 103 + - `Origin` contains the origin of the request. The browser will _always_ fill 104 + this header automatically. 105 + - `Access-Control-Request-Method` contains the desired methods to use when 106 + talking with the server. 107 + - `Access-Control-Request-Header` contains the desired headers that the request 108 + want to have.
+23
e2e/mist_test/.github/workflows/test.yml
··· 1 + name: test 2 + 3 + on: 4 + push: 5 + branches: 6 + - master 7 + - main 8 + pull_request: 9 + 10 + jobs: 11 + test: 12 + runs-on: ubuntu-latest 13 + steps: 14 + - uses: actions/checkout@v4 15 + - uses: erlef/setup-beam@v1 16 + with: 17 + otp-version: "26.0.2" 18 + gleam-version: "1.1.0" 19 + rebar3-version: "3" 20 + # elixir-version: "1.15.4" 21 + - run: gleam deps download 22 + - run: gleam test 23 + - run: gleam format --check src test
+4
e2e/mist_test/.gitignore
··· 1 + *.beam 2 + *.ez 3 + /build 4 + erl_crash.dump
+25
e2e/mist_test/README.md
··· 1 + # mist_test 2 + 3 + [![Package Version](https://img.shields.io/hexpm/v/mist_test)](https://hex.pm/packages/mist_test) 4 + [![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/mist_test/) 5 + 6 + ```sh 7 + gleam add mist_test 8 + ``` 9 + ```gleam 10 + import mist_test 11 + 12 + pub fn main() { 13 + // TODO: An example of the project in use 14 + } 15 + ``` 16 + 17 + Further documentation can be found at <https://hexdocs.pm/mist_test>. 18 + 19 + ## Development 20 + 21 + ```sh 22 + gleam run # Run the project 23 + gleam test # Run the tests 24 + gleam shell # Run an Erlang shell 25 + ```
+23
e2e/mist_test/gleam.toml
··· 1 + name = "mist_test" 2 + version = "1.0.0" 3 + 4 + # Fill out these fields if you intend to generate HTML documentation or publish 5 + # your project to the Hex package manager. 6 + # 7 + # description = "" 8 + # licences = ["Apache-2.0"] 9 + # repository = { type = "github", user = "username", repo = "project" } 10 + # links = [{ title = "Website", href = "https://gleam.run" }] 11 + # 12 + # For a full reference of all the available options, you can have a look at 13 + # https://gleam.run/writing-gleam/gleam-toml/. 14 + 15 + [dependencies] 16 + gleam_http = ">= 3.6.0 and < 4.0.0" 17 + gleam_stdlib = ">= 0.34.0 and < 2.0.0" 18 + mist = ">= 1.0.0 and < 2.0.0" 19 + simple_cors = {path = "../.."} 20 + gleam_erlang = ">= 0.25.0 and < 1.0.0" 21 + 22 + [dev-dependencies] 23 + gleeunit = ">= 1.0.0 and < 2.0.0"
+33
e2e/mist_test/manifest.toml
··· 1 + # This file was generated by Gleam 2 + # You typically do not need to edit this file 3 + 4 + packages = [ 5 + { name = "birl", version = "1.6.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "976CFF85D34D50F7775896615A71745FBE0C325E50399787088F941B539A0497" }, 6 + { name = "exception", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "F5580D584F16A20B7FCDCABF9E9BE9A2C1F6AC4F9176FA6DD0B63E3B20D450AA" }, 7 + { name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" }, 8 + { name = "gleam_crypto", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "ADD058DEDE8F0341F1ADE3AAC492A224F15700829D9A3A3F9ADF370F875C51B7" }, 9 + { name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" }, 10 + { name = "gleam_http", version = "3.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8C07DF9DF8CC7F054C650839A51C30A7D3C26482AC241C899C1CEA86B22DBE51" }, 11 + { name = "gleam_json", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "8B197DD5D578EA6AC2C0D4BDC634C71A5BCA8E7DB5F47091C263ECB411A60DF3" }, 12 + { name = "gleam_otp", version = "0.10.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "0B04FE915ACECE539B317F9652CAADBBC0F000184D586AAAF2D94C100945D72B" }, 13 + { name = "gleam_stdlib", version = "0.37.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "5398BD6C2ABA17338F676F42F404B9B7BABE1C8DC7380031ACB05BBE1BCF3742" }, 14 + { name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" }, 15 + { name = "glisten", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "glisten", source = "hex", outer_checksum = "CF3A9383E9BA4A8CBAF2F7B799716290D02F2AC34E7A77556B49376B662B9314" }, 16 + { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" }, 17 + { name = "logging", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "82C112ED9B6C30C1772A6FE2613B94B13F62EA35F5869A2630D13948D297BD39" }, 18 + { name = "marceau", version = "1.1.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "1AAD727A30BE0F95562C3403BB9B27C823797AD90037714255EEBF617B1CDA81" }, 19 + { name = "mist", version = "1.0.0", build_tools = ["gleam"], requirements = ["birl", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "7765E53DCC9ACCACF217B8E0CA3DE7E848C783BFAE5118B75011E81C2C80385C" }, 20 + { name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" }, 21 + { name = "simple_cors", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_stdlib", "mist", "wisp"], source = "local", path = "../.." }, 22 + { name = "simplifile", version = "1.7.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "1D5DFA3A2F9319EC85825F6ED88B8E449F381B0D55A62F5E61424E748E7DDEB0" }, 23 + { name = "thoas", version = "0.4.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "4918D50026C073C4AB1388437132C77A6F6F7C8AC43C60C13758CC0ADCE2134E" }, 24 + { name = "wisp", version = "0.14.0", build_tools = ["gleam"], requirements = ["exception", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_json", "gleam_stdlib", "logging", "marceau", "mist", "simplifile"], otp_app = "wisp", source = "hex", outer_checksum = "9F5453AF1F9275E6F8707BC815D6A6A9DF41551921B16FBDBA52883773BAE684" }, 25 + ] 26 + 27 + [requirements] 28 + gleam_erlang = { version = ">= 0.25.0 and < 1.0.0" } 29 + gleam_http = { version = ">= 3.6.0 and < 4.0.0" } 30 + gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" } 31 + gleeunit = { version = ">= 1.0.0 and < 2.0.0" } 32 + mist = { version = ">= 1.0.0 and < 2.0.0" } 33 + simple_cors = { path = "../.." }
+30
e2e/mist_test/src/mist_test.gleam
··· 1 + import gleam/bytes_builder 2 + import gleam/erlang/process 3 + import gleam/http 4 + import gleam/http/request.{type Request} 5 + import gleam/http/response.{type Response} 6 + import mist.{type Connection, type ResponseData} 7 + import simple_cors as cors 8 + 9 + fn cors() { 10 + cors.new() 11 + |> cors.allow_origin("http://localhost:3000") 12 + |> cors.allow_method(http.Get) 13 + |> cors.allow_method(http.Post) 14 + } 15 + 16 + fn main_handler(req: Request(Connection)) -> Response(ResponseData) { 17 + use _req <- cors.mist_handle(req, cors()) 18 + let empty = mist.Bytes(bytes_builder.new()) 19 + response.new(200) 20 + |> response.set_body(empty) 21 + } 22 + 23 + pub fn main() { 24 + let assert Ok(_) = 25 + main_handler 26 + |> mist.new() 27 + |> mist.port(8080) 28 + |> mist.start_http() 29 + process.sleep(5000) 30 + }
+12
e2e/mist_test/test/mist_test_test.gleam
··· 1 + import gleeunit 2 + import gleeunit/should 3 + 4 + pub fn main() { 5 + gleeunit.main() 6 + } 7 + 8 + // gleeunit test functions end in `_test` 9 + pub fn hello_world_test() { 10 + 1 11 + |> should.equal(1) 12 + }
+23
e2e/wisp_test/.github/workflows/test.yml
··· 1 + name: test 2 + 3 + on: 4 + push: 5 + branches: 6 + - master 7 + - main 8 + pull_request: 9 + 10 + jobs: 11 + test: 12 + runs-on: ubuntu-latest 13 + steps: 14 + - uses: actions/checkout@v4 15 + - uses: erlef/setup-beam@v1 16 + with: 17 + otp-version: "26.0.2" 18 + gleam-version: "1.1.0" 19 + rebar3-version: "3" 20 + # elixir-version: "1.15.4" 21 + - run: gleam deps download 22 + - run: gleam test 23 + - run: gleam format --check src test
+4
e2e/wisp_test/.gitignore
··· 1 + *.beam 2 + *.ez 3 + /build 4 + erl_crash.dump
+25
e2e/wisp_test/README.md
··· 1 + # wisp_test 2 + 3 + [![Package Version](https://img.shields.io/hexpm/v/wisp_test)](https://hex.pm/packages/wisp_test) 4 + [![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/wisp_test/) 5 + 6 + ```sh 7 + gleam add wisp_test 8 + ``` 9 + ```gleam 10 + import wisp_test 11 + 12 + pub fn main() { 13 + // TODO: An example of the project in use 14 + } 15 + ``` 16 + 17 + Further documentation can be found at <https://hexdocs.pm/wisp_test>. 18 + 19 + ## Development 20 + 21 + ```sh 22 + gleam run # Run the project 23 + gleam test # Run the tests 24 + gleam shell # Run an Erlang shell 25 + ```
+24
e2e/wisp_test/gleam.toml
··· 1 + name = "wisp_test" 2 + version = "1.0.0" 3 + 4 + # Fill out these fields if you intend to generate HTML documentation or publish 5 + # your project to the Hex package manager. 6 + # 7 + # description = "" 8 + # licences = ["Apache-2.0"] 9 + # repository = { type = "github", user = "username", repo = "project" } 10 + # links = [{ title = "Website", href = "https://gleam.run" }] 11 + # 12 + # For a full reference of all the available options, you can have a look at 13 + # https://gleam.run/writing-gleam/gleam-toml/. 14 + 15 + [dependencies] 16 + gleam_http = ">= 3.6.0 and < 4.0.0" 17 + gleam_stdlib = ">= 0.34.0 and < 2.0.0" 18 + mist = ">= 1.0.0 and < 2.0.0" 19 + simple_cors = {path = "../.."} 20 + wisp = ">= 0.14.0 and < 1.0.0" 21 + gleam_erlang = ">= 0.25.0 and < 1.0.0" 22 + 23 + [dev-dependencies] 24 + gleeunit = ">= 1.0.0 and < 2.0.0"
+34
e2e/wisp_test/manifest.toml
··· 1 + # This file was generated by Gleam 2 + # You typically do not need to edit this file 3 + 4 + packages = [ 5 + { name = "birl", version = "1.6.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "976CFF85D34D50F7775896615A71745FBE0C325E50399787088F941B539A0497" }, 6 + { name = "exception", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "F5580D584F16A20B7FCDCABF9E9BE9A2C1F6AC4F9176FA6DD0B63E3B20D450AA" }, 7 + { name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" }, 8 + { name = "gleam_crypto", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "ADD058DEDE8F0341F1ADE3AAC492A224F15700829D9A3A3F9ADF370F875C51B7" }, 9 + { name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" }, 10 + { name = "gleam_http", version = "3.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8C07DF9DF8CC7F054C650839A51C30A7D3C26482AC241C899C1CEA86B22DBE51" }, 11 + { name = "gleam_json", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "8B197DD5D578EA6AC2C0D4BDC634C71A5BCA8E7DB5F47091C263ECB411A60DF3" }, 12 + { name = "gleam_otp", version = "0.10.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "0B04FE915ACECE539B317F9652CAADBBC0F000184D586AAAF2D94C100945D72B" }, 13 + { name = "gleam_stdlib", version = "0.37.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "5398BD6C2ABA17338F676F42F404B9B7BABE1C8DC7380031ACB05BBE1BCF3742" }, 14 + { name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" }, 15 + { name = "glisten", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "glisten", source = "hex", outer_checksum = "CF3A9383E9BA4A8CBAF2F7B799716290D02F2AC34E7A77556B49376B662B9314" }, 16 + { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" }, 17 + { name = "logging", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "82C112ED9B6C30C1772A6FE2613B94B13F62EA35F5869A2630D13948D297BD39" }, 18 + { name = "marceau", version = "1.1.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "1AAD727A30BE0F95562C3403BB9B27C823797AD90037714255EEBF617B1CDA81" }, 19 + { name = "mist", version = "1.0.0", build_tools = ["gleam"], requirements = ["birl", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "7765E53DCC9ACCACF217B8E0CA3DE7E848C783BFAE5118B75011E81C2C80385C" }, 20 + { name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" }, 21 + { name = "simple_cors", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_stdlib", "mist", "wisp"], source = "local", path = "../.." }, 22 + { name = "simplifile", version = "1.7.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "1D5DFA3A2F9319EC85825F6ED88B8E449F381B0D55A62F5E61424E748E7DDEB0" }, 23 + { name = "thoas", version = "0.4.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "4918D50026C073C4AB1388437132C77A6F6F7C8AC43C60C13758CC0ADCE2134E" }, 24 + { name = "wisp", version = "0.14.0", build_tools = ["gleam"], requirements = ["exception", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_json", "gleam_stdlib", "logging", "marceau", "mist", "simplifile"], otp_app = "wisp", source = "hex", outer_checksum = "9F5453AF1F9275E6F8707BC815D6A6A9DF41551921B16FBDBA52883773BAE684" }, 25 + ] 26 + 27 + [requirements] 28 + gleam_erlang = { version = ">= 0.25.0 and < 1.0.0"} 29 + gleam_http = { version = ">= 3.6.0 and < 4.0.0" } 30 + gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" } 31 + gleeunit = { version = ">= 1.0.0 and < 2.0.0" } 32 + mist = { version = ">= 1.0.0 and < 2.0.0" } 33 + simple_cors = { path = "../.." } 34 + wisp = { version = ">= 0.14.0 and < 1.0.0" }
+29
e2e/wisp_test/src/wisp_test.gleam
··· 1 + import gleam/erlang/process 2 + import gleam/http 3 + import mist 4 + import simple_cors as cors 5 + import wisp.{type Request, type Response} 6 + 7 + fn cors() { 8 + cors.new() 9 + |> cors.allow_origin("http://localhost:3000") 10 + |> cors.allow_origin("http://localhost:4000") 11 + |> cors.allow_method(http.Get) 12 + |> cors.allow_method(http.Post) 13 + } 14 + 15 + fn main_handler(req: Request) -> Response { 16 + use _req <- cors.wisp_handle(req, cors()) 17 + wisp.ok() 18 + } 19 + 20 + pub fn main() { 21 + let secret_key = wisp.random_string(64) 22 + let assert Ok(_) = 23 + main_handler 24 + |> wisp.mist_handler(secret_key) 25 + |> mist.new() 26 + |> mist.port(8080) 27 + |> mist.start_http() 28 + process.sleep(5000) 29 + }
+12
e2e/wisp_test/test/wisp_test_test.gleam
··· 1 + import gleeunit 2 + import gleeunit/should 3 + 4 + pub fn main() { 5 + gleeunit.main() 6 + } 7 + 8 + // gleeunit test functions end in `_test` 9 + pub fn hello_world_test() { 10 + 1 11 + |> should.equal(1) 12 + }
+22
gleam.toml
··· 1 + name = "simple_cors" 2 + version = "1.0.0" 3 + 4 + # Fill out these fields if you intend to generate HTML documentation or publish 5 + # your project to the Hex package manager. 6 + # 7 + # description = "" 8 + # licences = ["Apache-2.0"] 9 + # repository = { type = "github", user = "username", repo = "project" } 10 + # links = [{ title = "Website", href = "https://gleam.run" }] 11 + # 12 + # For a full reference of all the available options, you can have a look at 13 + # https://gleam.run/writing-gleam/gleam-toml/. 14 + 15 + [dependencies] 16 + gleam_http = ">= 3.6.0 and < 4.0.0" 17 + gleam_stdlib = ">= 0.34.0 and < 2.0.0" 18 + mist = ">= 1.0.0 and < 2.0.0" 19 + wisp = ">= 0.14.0 and < 1.0.0" 20 + 21 + [dev-dependencies] 22 + gleeunit = ">= 1.0.0 and < 2.0.0"
+31
manifest.toml
··· 1 + # This file was generated by Gleam 2 + # You typically do not need to edit this file 3 + 4 + packages = [ 5 + { name = "birl", version = "1.6.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "976CFF85D34D50F7775896615A71745FBE0C325E50399787088F941B539A0497" }, 6 + { name = "exception", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "F5580D584F16A20B7FCDCABF9E9BE9A2C1F6AC4F9176FA6DD0B63E3B20D450AA" }, 7 + { name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" }, 8 + { name = "gleam_crypto", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "ADD058DEDE8F0341F1ADE3AAC492A224F15700829D9A3A3F9ADF370F875C51B7" }, 9 + { name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" }, 10 + { name = "gleam_http", version = "3.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8C07DF9DF8CC7F054C650839A51C30A7D3C26482AC241C899C1CEA86B22DBE51" }, 11 + { name = "gleam_json", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "8B197DD5D578EA6AC2C0D4BDC634C71A5BCA8E7DB5F47091C263ECB411A60DF3" }, 12 + { name = "gleam_otp", version = "0.10.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "0B04FE915ACECE539B317F9652CAADBBC0F000184D586AAAF2D94C100945D72B" }, 13 + { name = "gleam_stdlib", version = "0.37.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "5398BD6C2ABA17338F676F42F404B9B7BABE1C8DC7380031ACB05BBE1BCF3742" }, 14 + { name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" }, 15 + { name = "glisten", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "glisten", source = "hex", outer_checksum = "CF3A9383E9BA4A8CBAF2F7B799716290D02F2AC34E7A77556B49376B662B9314" }, 16 + { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" }, 17 + { name = "logging", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "82C112ED9B6C30C1772A6FE2613B94B13F62EA35F5869A2630D13948D297BD39" }, 18 + { name = "marceau", version = "1.1.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "1AAD727A30BE0F95562C3403BB9B27C823797AD90037714255EEBF617B1CDA81" }, 19 + { name = "mist", version = "1.0.0", build_tools = ["gleam"], requirements = ["birl", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "7765E53DCC9ACCACF217B8E0CA3DE7E848C783BFAE5118B75011E81C2C80385C" }, 20 + { name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" }, 21 + { name = "simplifile", version = "1.7.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "1D5DFA3A2F9319EC85825F6ED88B8E449F381B0D55A62F5E61424E748E7DDEB0" }, 22 + { name = "thoas", version = "0.4.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "4918D50026C073C4AB1388437132C77A6F6F7C8AC43C60C13758CC0ADCE2134E" }, 23 + { name = "wisp", version = "0.14.0", build_tools = ["gleam"], requirements = ["exception", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_json", "gleam_stdlib", "logging", "marceau", "mist", "simplifile"], otp_app = "wisp", source = "hex", outer_checksum = "9F5453AF1F9275E6F8707BC815D6A6A9DF41551921B16FBDBA52883773BAE684" }, 24 + ] 25 + 26 + [requirements] 27 + gleam_http = { version = ">= 3.6.0 and < 4.0.0" } 28 + gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" } 29 + gleeunit = { version = ">= 1.0.0 and < 2.0.0" } 30 + mist = { version = ">= 1.0.0 and < 2.0.0"} 31 + wisp = { version = ">= 0.14.0 and < 1.0.0" }
+227
src/simple_cors.gleam
··· 1 + import gleam/bool 2 + import gleam/bytes_builder 3 + import gleam/function 4 + import gleam/http.{type Method} 5 + import gleam/http/request.{type Request} 6 + import gleam/http/response.{type Response, set_header} 7 + import gleam/int 8 + import gleam/list 9 + import gleam/option.{type Option, None, Some} 10 + import gleam/pair 11 + import gleam/result 12 + import gleam/set.{type Set} 13 + import gleam/string 14 + import mist 15 + import wisp 16 + 17 + pub opaque type Origin { 18 + Wildcard 19 + Origin(Set(String)) 20 + } 21 + 22 + pub opaque type Cors { 23 + Cors( 24 + allow_origin: Option(Origin), 25 + expose_headers: Set(String), 26 + max_age: Option(Int), 27 + allow_credentials: Option(Bool), 28 + allow_methods: Set(Method), 29 + allow_headers: Set(String), 30 + ) 31 + } 32 + 33 + pub fn new() -> Cors { 34 + Cors( 35 + allow_origin: None, 36 + expose_headers: set.new(), 37 + max_age: None, 38 + allow_credentials: None, 39 + allow_methods: set.new(), 40 + allow_headers: set.new(), 41 + ) 42 + } 43 + 44 + /// Be extremely careful, you should not use this function in production! 45 + /// Allowing all origins can easily be a huge security flaw! 46 + /// Allow only the origins you need, and use this function only locally, in dev mode. 47 + pub fn allow_all_origins(cors: Cors) { 48 + let allow_origin = Some(Wildcard) 49 + Cors(..cors, allow_origin: allow_origin) 50 + } 51 + 52 + pub fn allow_origin(cors: Cors, origin: String) { 53 + let allow_origin = case cors.allow_origin { 54 + Some(Wildcard) -> Some(Wildcard) 55 + Some(Origin(content)) -> Some(Origin(set.insert(content, origin))) 56 + None -> Some(Origin(set.from_list([origin]))) 57 + } 58 + Cors(..cors, allow_origin: allow_origin) 59 + } 60 + 61 + pub fn expose_headers(cors: Cors, header: String) { 62 + let expose_headers = set.insert(cors.expose_headers, header) 63 + Cors(..cors, expose_headers: expose_headers) 64 + } 65 + 66 + pub fn max_age(cors: Cors, age: Int) { 67 + let max_age = Some(age) 68 + Cors(..cors, max_age: max_age) 69 + } 70 + 71 + pub fn allow_credentials(cors: Cors) { 72 + let allow_credentials = Some(True) 73 + Cors(..cors, allow_credentials: allow_credentials) 74 + } 75 + 76 + pub fn allow_method(cors: Cors, method: Method) { 77 + let allow_methods = set.insert(cors.allow_methods, method) 78 + Cors(..cors, allow_methods: allow_methods) 79 + } 80 + 81 + pub fn allow_header(cors: Cors, header: String) { 82 + let allow_headers = set.insert(cors.allow_headers, header) 83 + Cors(..cors, allow_headers: allow_headers) 84 + } 85 + 86 + fn set_allowed_origin(cors: Cors, origin: String) { 87 + let hd = "Access-Control-Allow-Origin" 88 + case cors.allow_origin { 89 + None -> function.identity 90 + Some(Wildcard) -> set_header(_, hd, "*") 91 + Some(Origin(origins)) -> { 92 + let origins = set.to_list(origins) 93 + case origins { 94 + [o] -> set_header(_, hd, o) 95 + _ -> { 96 + let not_origin = !list.contains(origins, origin) 97 + use <- bool.guard(when: not_origin, return: function.identity) 98 + fn(res) { 99 + res 100 + |> set_header(hd, origin) 101 + |> set_header("Vary", "Origin") 102 + } 103 + } 104 + } 105 + } 106 + } 107 + } 108 + 109 + fn set_expose_headers(res: Response(body), cors: Cors) { 110 + let hd = "Access-Control-Expose-Headers" 111 + cors.expose_headers 112 + |> set.to_list() 113 + |> string.join(",") 114 + |> set_header(res, hd, _) 115 + } 116 + 117 + fn set_max_age(res: Response(body), cors: Cors) { 118 + let hd = "Access-Control-Max-Age" 119 + cors.max_age 120 + |> option.map(fn(a) { set_header(res, hd, int.to_string(a)) }) 121 + |> option.unwrap(res) 122 + } 123 + 124 + fn set_allow_credentials(res: Response(body), cors: Cors) { 125 + let hd = "Access-Control-Allow-Credentials" 126 + cors.allow_credentials 127 + |> option.map(fn(_) { set_header(res, hd, "true") }) 128 + |> option.unwrap(res) 129 + } 130 + 131 + fn method_to_string(method: Method) { 132 + case method { 133 + http.Get -> "GET" 134 + http.Post -> "POST" 135 + http.Head -> "HEAD" 136 + http.Put -> "PUT" 137 + http.Delete -> "DELETE" 138 + http.Trace -> "TRACE" 139 + http.Connect -> "CONNECT" 140 + http.Options -> "OPTIONS" 141 + http.Patch -> "PATCH" 142 + http.Other(content) -> content 143 + } 144 + } 145 + 146 + fn set_allow_methods(res: Response(body), cors: Cors) { 147 + let hd = "Access-Control-Allow-Methods" 148 + let methods = set.to_list(cors.allow_methods) 149 + case list.is_empty(methods) { 150 + True -> res 151 + False -> 152 + methods 153 + |> list.map(method_to_string) 154 + |> string.join(",") 155 + |> set_header(res, hd, _) 156 + } 157 + } 158 + 159 + fn set_allow_headers(res: Response(body), cors: Cors) { 160 + let hd = "Access-Control-Allow-Headers" 161 + let headers = set.to_list(cors.allow_headers) 162 + case list.is_empty(headers) { 163 + True -> res 164 + False -> 165 + headers 166 + |> string.join(",") 167 + |> set_header(res, hd, _) 168 + } 169 + } 170 + 171 + fn set_response(res: Response(body), cors: Cors, origin: Option(String)) { 172 + res 173 + |> set_allowed_origin(cors, option.unwrap(origin, "")) 174 + |> set_expose_headers(cors) 175 + |> set_max_age(cors) 176 + |> set_allow_credentials(cors) 177 + |> set_allow_methods(cors) 178 + |> set_allow_headers(cors) 179 + } 180 + 181 + pub fn set_cors(res: Response(response), cors: Cors) { 182 + set_response(res, cors, None) 183 + } 184 + 185 + pub fn set_cors_origin(res: Response(response), cors: Cors, origin: String) { 186 + set_response(res, cors, Some(origin)) 187 + } 188 + 189 + fn find_origin(req: Request(connection)) { 190 + req.headers 191 + |> list.find(fn(h) { pair.first(h) == "Origin" }) 192 + |> result.map(pair.second) 193 + |> result.unwrap("") 194 + } 195 + 196 + fn middleware( 197 + empty: resdata, 198 + req: Request(connection), 199 + cors: Cors, 200 + handler: fn(Request(connection)) -> Response(resdata), 201 + ) { 202 + let res = case req.method { 203 + http.Options -> response.set_body(response.new(204), empty) 204 + _ -> handler(req) 205 + } 206 + req 207 + |> find_origin() 208 + |> set_cors_origin(res, cors, _) 209 + } 210 + 211 + pub fn mist_handle( 212 + req: Request(mist.Connection), 213 + cors: Cors, 214 + handler: fn(Request(mist.Connection)) -> Response(mist.ResponseData), 215 + ) { 216 + bytes_builder.new() 217 + |> mist.Bytes() 218 + |> middleware(req, cors, handler) 219 + } 220 + 221 + pub fn wisp_handle( 222 + req: wisp.Request, 223 + cors: Cors, 224 + handler: fn(wisp.Request) -> wisp.Response, 225 + ) { 226 + middleware(wisp.Empty, req, cors, handler) 227 + }
+5
test/cors_test.gleam
··· 1 + import gleeunit 2 + 3 + pub fn main() { 4 + gleeunit.main() 5 + }