diff --git a/README.md b/README.md index 33b8cc1..a879b68 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,33 @@ -# Simple CORS +# CORS Builder -## Middlewares +> Before diving in CORS, [make sure you're aware of security advices](#) and see +> if you can't just use a simple proxy to avoid CORS! It's a better and more +> secure way to manage CORS! + +Manipulating CORS is often a pain for developers, and always a little blurry, to +understand what should be done, how it should be configured, etc. CORS Builder +abstract the complexity while trying to remains simple, and friendly warns you +when something is wrong. + +CORS Builder is compatible with every servers, as long as you're using the +[`gleam_http`](https://hexdocs.pm/gleam_http) `Response` as a foundation. +However, to simplify your development, two middlewares are provided +out-of-the-box: +[`wisp_handle`](https://hexdocs.pm/cors_builder/cors_builder#wisp_handle) and +[`mist_handle`](https://hexdocs.pm/cors_builder/cors_builder#mist_handle) to +integrate nicely in `wisp` and `mist`. You should never have to worry about CORS +again! Use the package, configure your CORS, and everything should work +smoothly! + +## Quickstart + +You can interchange `wisp_handle` with `mist_handle` if you're using `wisp` or +`mist`. ```gleam +import cors_builder as cors import gleam/http import mist -import simple_cors as cors import wisp.{type Request, type Response} fn cors() { @@ -30,7 +52,47 @@ fn main() { } ``` -## What is CORS? +## More details & notes about security + +CORS are often badly understood, however they're full parts of the web stack +when working with browsers, and they're part of security measures, to avoid +users' browsers behaving badly. + +CORS intervene when browsers have to manage with cross-origin requests. A +cross-origin request is a request coming from a different domain than the domain +you're currently on. Imagine you're browsing your favorite website, like +[packages.gleam.run](https://packages.gleam.run), and suddenlly, your browser +want to query [google.com](https://google.com) in an async way. Because you're +not on Google, the browser will identify your request as a cross-origin request. +Some more security measures have to be taken to make sure the request is valid. +That's where CORS comes into play. CORS stands for Cross-Origin Resource +Sharing. It means it's a way to authorize cross-origin requests, to allow +outside clients to access the desired resources. + +This mechanism is a way to prevent browsers to ask for data on behalf of a user, +in an undesired way. It's up to you, when developping your server, to make sure +only authentified, regular users can access your service. **It is a bad idea to +let everyone access your data directly from a browser.** You should identify who +can access your service, and how, that's what CORS are made for. Most of the +time, you want your frontend to access your backend, and nothing else. You can +simply identify those domains, and add them in your CORS configuration. Let's +imagine your frontend is hosted on `https://frontend.app` and your backend on +`https://backend.app`. You can configure your CORS to _only_ accept +`https://frontend.app`. That way, every request coming from another domain will +be rejected, and only your users will be accepted. + +Keep in mind that CORS will never trigger as long as your frontend query the +same domain where it resides. When your frontend queries +`https://frontend.app/api/path` for example, because your frontend resides on +`https://frontend.app`, no cross-origin request is identified, so CORS won't +comes into play. So always think about this, and see if you can just host your +frontend at the same address as your backend. This can be achieved using a +proxy, and this should be soon available in lustre dev tools, and is already +available if you're using +[Vite](https://vitejs.dev/config/server-options#server-proxy) or +[Webpack](https://webpack.js.org/configuration/dev-server/#devserverproxy)! + +## How are CORS working? Browsers apply a simple rules for every HTTP request: when the request originates from a different origin than the target server URL — and if it's not @@ -106,3 +168,9 @@ headers: talking with the server. - `Access-Control-Request-Header` contains the desired headers that the request want to have. + +## Contributing + +You love the package and want to improve it? You have a shiny new framework and +want to provide an integration with CORS in this package? Every contribution is +welcome! Feel free to open a Pull Request, and let's discuss about it! diff --git a/e2e/mist_test/.github/workflows/test.yml b/e2e/mist_test/.github/workflows/test.yml deleted file mode 100644 index 12b9772..0000000 --- a/e2e/mist_test/.github/workflows/test.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: test - -on: - push: - branches: - - master - - main - pull_request: - -jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: erlef/setup-beam@v1 - with: - otp-version: "26.0.2" - gleam-version: "1.1.0" - rebar3-version: "3" - # elixir-version: "1.15.4" - - run: gleam deps download - - run: gleam test - - run: gleam format --check src test diff --git a/e2e/mist_test/.gitignore b/e2e/mist_test/.gitignore deleted file mode 100644 index 599be4e..0000000 --- a/e2e/mist_test/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -*.beam -*.ez -/build -erl_crash.dump diff --git a/e2e/mist_test/README.md b/e2e/mist_test/README.md deleted file mode 100644 index e1fd789..0000000 --- a/e2e/mist_test/README.md +++ /dev/null @@ -1,25 +0,0 @@ -# mist_test - -[![Package Version](https://img.shields.io/hexpm/v/mist_test)](https://hex.pm/packages/mist_test) -[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/mist_test/) - -```sh -gleam add mist_test -``` -```gleam -import mist_test - -pub fn main() { - // TODO: An example of the project in use -} -``` - -Further documentation can be found at . - -## Development - -```sh -gleam run # Run the project -gleam test # Run the tests -gleam shell # Run an Erlang shell -``` diff --git a/e2e/mist_test/gleam.toml b/e2e/mist_test/gleam.toml deleted file mode 100644 index 8bb544a..0000000 --- a/e2e/mist_test/gleam.toml +++ /dev/null @@ -1,23 +0,0 @@ -name = "mist_test" -version = "1.0.0" - -# Fill out these fields if you intend to generate HTML documentation or publish -# your project to the Hex package manager. -# -# description = "" -# licences = ["Apache-2.0"] -# repository = { type = "github", user = "username", repo = "project" } -# links = [{ title = "Website", href = "https://gleam.run" }] -# -# For a full reference of all the available options, you can have a look at -# https://gleam.run/writing-gleam/gleam-toml/. - -[dependencies] -gleam_http = ">= 3.6.0 and < 4.0.0" -gleam_stdlib = ">= 0.34.0 and < 2.0.0" -mist = ">= 1.0.0 and < 2.0.0" -simple_cors = {path = "../.."} -gleam_erlang = ">= 0.25.0 and < 1.0.0" - -[dev-dependencies] -gleeunit = ">= 1.0.0 and < 2.0.0" diff --git a/e2e/mist_test/manifest.toml b/e2e/mist_test/manifest.toml deleted file mode 100644 index 1ff5314..0000000 --- a/e2e/mist_test/manifest.toml +++ /dev/null @@ -1,33 +0,0 @@ -# This file was generated by Gleam -# You typically do not need to edit this file - -packages = [ - { name = "birl", version = "1.6.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "976CFF85D34D50F7775896615A71745FBE0C325E50399787088F941B539A0497" }, - { name = "exception", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "F5580D584F16A20B7FCDCABF9E9BE9A2C1F6AC4F9176FA6DD0B63E3B20D450AA" }, - { name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" }, - { name = "gleam_crypto", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "ADD058DEDE8F0341F1ADE3AAC492A224F15700829D9A3A3F9ADF370F875C51B7" }, - { name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" }, - { name = "gleam_http", version = "3.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8C07DF9DF8CC7F054C650839A51C30A7D3C26482AC241C899C1CEA86B22DBE51" }, - { name = "gleam_json", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "8B197DD5D578EA6AC2C0D4BDC634C71A5BCA8E7DB5F47091C263ECB411A60DF3" }, - { name = "gleam_otp", version = "0.10.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "0B04FE915ACECE539B317F9652CAADBBC0F000184D586AAAF2D94C100945D72B" }, - { name = "gleam_stdlib", version = "0.37.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "5398BD6C2ABA17338F676F42F404B9B7BABE1C8DC7380031ACB05BBE1BCF3742" }, - { name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" }, - { name = "glisten", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "glisten", source = "hex", outer_checksum = "CF3A9383E9BA4A8CBAF2F7B799716290D02F2AC34E7A77556B49376B662B9314" }, - { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" }, - { name = "logging", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "82C112ED9B6C30C1772A6FE2613B94B13F62EA35F5869A2630D13948D297BD39" }, - { name = "marceau", version = "1.1.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "1AAD727A30BE0F95562C3403BB9B27C823797AD90037714255EEBF617B1CDA81" }, - { 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" }, - { name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" }, - { name = "simple_cors", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_stdlib", "mist", "wisp"], source = "local", path = "../.." }, - { name = "simplifile", version = "1.7.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "1D5DFA3A2F9319EC85825F6ED88B8E449F381B0D55A62F5E61424E748E7DDEB0" }, - { name = "thoas", version = "0.4.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "4918D50026C073C4AB1388437132C77A6F6F7C8AC43C60C13758CC0ADCE2134E" }, - { 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" }, -] - -[requirements] -gleam_erlang = { version = ">= 0.25.0 and < 1.0.0" } -gleam_http = { version = ">= 3.6.0 and < 4.0.0" } -gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" } -gleeunit = { version = ">= 1.0.0 and < 2.0.0" } -mist = { version = ">= 1.0.0 and < 2.0.0" } -simple_cors = { path = "../.." } diff --git a/e2e/mist_test/src/mist_test.gleam b/e2e/mist_test/src/mist_test.gleam deleted file mode 100644 index b12f0b9..0000000 --- a/e2e/mist_test/src/mist_test.gleam +++ /dev/null @@ -1,30 +0,0 @@ -import gleam/bytes_builder -import gleam/erlang/process -import gleam/http -import gleam/http/request.{type Request} -import gleam/http/response.{type Response} -import mist.{type Connection, type ResponseData} -import simple_cors as cors - -fn cors() { - cors.new() - |> cors.allow_origin("http://localhost:3000") - |> cors.allow_method(http.Get) - |> cors.allow_method(http.Post) -} - -fn main_handler(req: Request(Connection)) -> Response(ResponseData) { - use _req <- cors.mist_handle(req, cors()) - let empty = mist.Bytes(bytes_builder.new()) - response.new(200) - |> response.set_body(empty) -} - -pub fn main() { - let assert Ok(_) = - main_handler - |> mist.new() - |> mist.port(8080) - |> mist.start_http() - process.sleep(5000) -} diff --git a/e2e/mist_test/test/mist_test_test.gleam b/e2e/mist_test/test/mist_test_test.gleam deleted file mode 100644 index 3831e7a..0000000 --- a/e2e/mist_test/test/mist_test_test.gleam +++ /dev/null @@ -1,12 +0,0 @@ -import gleeunit -import gleeunit/should - -pub fn main() { - gleeunit.main() -} - -// gleeunit test functions end in `_test` -pub fn hello_world_test() { - 1 - |> should.equal(1) -} diff --git a/e2e/wisp_test/.github/workflows/test.yml b/e2e/wisp_test/.github/workflows/test.yml deleted file mode 100644 index 12b9772..0000000 --- a/e2e/wisp_test/.github/workflows/test.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: test - -on: - push: - branches: - - master - - main - pull_request: - -jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: erlef/setup-beam@v1 - with: - otp-version: "26.0.2" - gleam-version: "1.1.0" - rebar3-version: "3" - # elixir-version: "1.15.4" - - run: gleam deps download - - run: gleam test - - run: gleam format --check src test diff --git a/e2e/wisp_test/.gitignore b/e2e/wisp_test/.gitignore deleted file mode 100644 index 599be4e..0000000 --- a/e2e/wisp_test/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -*.beam -*.ez -/build -erl_crash.dump diff --git a/e2e/wisp_test/README.md b/e2e/wisp_test/README.md deleted file mode 100644 index 83ae9e1..0000000 --- a/e2e/wisp_test/README.md +++ /dev/null @@ -1,25 +0,0 @@ -# wisp_test - -[![Package Version](https://img.shields.io/hexpm/v/wisp_test)](https://hex.pm/packages/wisp_test) -[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/wisp_test/) - -```sh -gleam add wisp_test -``` -```gleam -import wisp_test - -pub fn main() { - // TODO: An example of the project in use -} -``` - -Further documentation can be found at . - -## Development - -```sh -gleam run # Run the project -gleam test # Run the tests -gleam shell # Run an Erlang shell -``` diff --git a/e2e/wisp_test/gleam.toml b/e2e/wisp_test/gleam.toml deleted file mode 100644 index b82e674..0000000 --- a/e2e/wisp_test/gleam.toml +++ /dev/null @@ -1,24 +0,0 @@ -name = "wisp_test" -version = "1.0.0" - -# Fill out these fields if you intend to generate HTML documentation or publish -# your project to the Hex package manager. -# -# description = "" -# licences = ["Apache-2.0"] -# repository = { type = "github", user = "username", repo = "project" } -# links = [{ title = "Website", href = "https://gleam.run" }] -# -# For a full reference of all the available options, you can have a look at -# https://gleam.run/writing-gleam/gleam-toml/. - -[dependencies] -gleam_http = ">= 3.6.0 and < 4.0.0" -gleam_stdlib = ">= 0.34.0 and < 2.0.0" -mist = ">= 1.0.0 and < 2.0.0" -simple_cors = {path = "../.."} -wisp = ">= 0.14.0 and < 1.0.0" -gleam_erlang = ">= 0.25.0 and < 1.0.0" - -[dev-dependencies] -gleeunit = ">= 1.0.0 and < 2.0.0" diff --git a/e2e/wisp_test/manifest.toml b/e2e/wisp_test/manifest.toml deleted file mode 100644 index 6f4dfd3..0000000 --- a/e2e/wisp_test/manifest.toml +++ /dev/null @@ -1,34 +0,0 @@ -# This file was generated by Gleam -# You typically do not need to edit this file - -packages = [ - { name = "birl", version = "1.6.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "976CFF85D34D50F7775896615A71745FBE0C325E50399787088F941B539A0497" }, - { name = "exception", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "F5580D584F16A20B7FCDCABF9E9BE9A2C1F6AC4F9176FA6DD0B63E3B20D450AA" }, - { name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" }, - { name = "gleam_crypto", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "ADD058DEDE8F0341F1ADE3AAC492A224F15700829D9A3A3F9ADF370F875C51B7" }, - { name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" }, - { name = "gleam_http", version = "3.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8C07DF9DF8CC7F054C650839A51C30A7D3C26482AC241C899C1CEA86B22DBE51" }, - { name = "gleam_json", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "8B197DD5D578EA6AC2C0D4BDC634C71A5BCA8E7DB5F47091C263ECB411A60DF3" }, - { name = "gleam_otp", version = "0.10.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "0B04FE915ACECE539B317F9652CAADBBC0F000184D586AAAF2D94C100945D72B" }, - { name = "gleam_stdlib", version = "0.37.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "5398BD6C2ABA17338F676F42F404B9B7BABE1C8DC7380031ACB05BBE1BCF3742" }, - { name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" }, - { name = "glisten", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "glisten", source = "hex", outer_checksum = "CF3A9383E9BA4A8CBAF2F7B799716290D02F2AC34E7A77556B49376B662B9314" }, - { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" }, - { name = "logging", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "82C112ED9B6C30C1772A6FE2613B94B13F62EA35F5869A2630D13948D297BD39" }, - { name = "marceau", version = "1.1.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "1AAD727A30BE0F95562C3403BB9B27C823797AD90037714255EEBF617B1CDA81" }, - { 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" }, - { name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" }, - { name = "simple_cors", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_stdlib", "mist", "wisp"], source = "local", path = "../.." }, - { name = "simplifile", version = "1.7.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "1D5DFA3A2F9319EC85825F6ED88B8E449F381B0D55A62F5E61424E748E7DDEB0" }, - { name = "thoas", version = "0.4.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "4918D50026C073C4AB1388437132C77A6F6F7C8AC43C60C13758CC0ADCE2134E" }, - { 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" }, -] - -[requirements] -gleam_erlang = { version = ">= 0.25.0 and < 1.0.0"} -gleam_http = { version = ">= 3.6.0 and < 4.0.0" } -gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" } -gleeunit = { version = ">= 1.0.0 and < 2.0.0" } -mist = { version = ">= 1.0.0 and < 2.0.0" } -simple_cors = { path = "../.." } -wisp = { version = ">= 0.14.0 and < 1.0.0" } diff --git a/e2e/wisp_test/src/wisp_test.gleam b/e2e/wisp_test/src/wisp_test.gleam deleted file mode 100644 index d183b5c..0000000 --- a/e2e/wisp_test/src/wisp_test.gleam +++ /dev/null @@ -1,29 +0,0 @@ -import gleam/erlang/process -import gleam/http -import mist -import simple_cors as cors -import wisp.{type Request, type Response} - -fn cors() { - cors.new() - |> cors.allow_origin("http://localhost:3000") - |> cors.allow_origin("http://localhost:4000") - |> cors.allow_method(http.Get) - |> cors.allow_method(http.Post) -} - -fn main_handler(req: Request) -> Response { - use _req <- cors.wisp_handle(req, cors()) - wisp.ok() -} - -pub fn main() { - let secret_key = wisp.random_string(64) - let assert Ok(_) = - main_handler - |> wisp.mist_handler(secret_key) - |> mist.new() - |> mist.port(8080) - |> mist.start_http() - process.sleep(5000) -} diff --git a/e2e/wisp_test/test/wisp_test_test.gleam b/e2e/wisp_test/test/wisp_test_test.gleam deleted file mode 100644 index 3831e7a..0000000 --- a/e2e/wisp_test/test/wisp_test_test.gleam +++ /dev/null @@ -1,12 +0,0 @@ -import gleeunit -import gleeunit/should - -pub fn main() { - gleeunit.main() -} - -// gleeunit test functions end in `_test` -pub fn hello_world_test() { - 1 - |> should.equal(1) -} diff --git a/gleam.toml b/gleam.toml index 77517f1..e427419 100644 --- a/gleam.toml +++ b/gleam.toml @@ -1,4 +1,4 @@ -name = "simple_cors" +name = "cors_builder" version = "0.1.0" # Fill out these fields if you intend to generate HTML documentation or publish diff --git a/src/cors_builder.gleam b/src/cors_builder.gleam new file mode 100644 index 0000000..46bb4aa --- /dev/null +++ b/src/cors_builder.gleam @@ -0,0 +1,378 @@ +//// `cors_builder` provides an easy way to build and inject your CORS configuration +//// in your server. The package tries to remains as simplest as possible, while +//// guaranteeing type-safety and correctness of the CORS configuration. +//// +//// ## Quickstart +//// +//// Import the `cors_builder` package, and configure your CORS. Finally, use the +//// correct corresponding middleware for your server, and you're done! +//// +//// ``` +//// import cors_builder as cors +//// import gleam/http +//// import mist +//// import wisp.{type Request, type Response} +//// +//// // Dummy example. +//// fn cors() { +//// cors.new() +//// |> cors.allow_origin("http://localhost:3000") +//// |> cors.allow_origin("http://localhost:4000") +//// |> cors.allow_method(http.Get) +//// |> cors.allow_method(http.Post) +//// } +//// +//// fn handler(req: Request) -> Response { +//// use req <- cors.wisp_handle(req, cors()) +//// wisp.ok() +//// } +//// +//// fn main() { +//// handler +//// |> wisp.mist_handler(secret_key) +//// |> mist.new() +//// |> mist.port(3000) +//// |> mist.start_http() +//// } +//// ``` +//// +//// ## Low-level functions +//// +//// If you're building your framework or you know what you're doing, you should +//// take a look at [`set_cors`](#set_cors) and +//// [`set_cors_multiple_origin`](#set_cors_multiple_origin). They allow to +//// inject the CORS in your response, and it allows you to create your +//// middleware to use with the bare CORS data. +//// +//// If you're not building your framework, you should _probably_ heads to [`wisp`](https://hexdocs.pm/wisp) +//// to get you started. It's better to familiarize with the ecosystem before +//// jumping right in your custom code. + +import gleam/bool +import gleam/bytes_builder +import gleam/function +import gleam/http.{type Method} +import gleam/http/request.{type Request} +import gleam/http/response.{type Response, set_header} +import gleam/int +import gleam/io +import gleam/list +import gleam/option.{type Option, None, Some} +import gleam/pair +import gleam/result +import gleam/set.{type Set} +import gleam/string +import mist +import wisp + +/// Indicates the origin for CORS. Could be any origin (wildcard `"*"`) or a +/// list of domains. In case it's a list of domains, one domain will be returned +/// every time, and the `vary` header will be filled with `origin`. +/// If you only have one domain, the domain will always be filled. +pub opaque type Origin { + Wildcard + Origin(Set(String)) +} + +/// CORS builder. Use it in your program to generate the good CORS for your +/// responses. +pub opaque type Cors { + Cors( + allow_origin: Option(Origin), + expose_headers: Set(String), + max_age: Option(Int), + allow_credentials: Option(Bool), + allow_methods: Set(Method), + allow_headers: Set(String), + ) +} + +/// Creates an empty CORS object. It will not contains anything by default. +/// If you're using it directly, no headers will be added to the response. +pub fn new() -> Cors { + Cors( + allow_origin: None, + expose_headers: set.new(), + max_age: None, + allow_credentials: None, + allow_methods: set.new(), + allow_headers: set.new(), + ) +} + +/// Allow all domains to access your server. +/// Be extremely careful, you should not use this function in production! +/// Allowing all origins can easily be a huge security flaw! +/// Allow only the origins you need, and use this function only locally, +/// in dev mode. +pub fn allow_all_origins(cors: Cors) { + let allow_origin = Some(Wildcard) + Cors(..cors, allow_origin: allow_origin) +} + +/// Allow a specific domain to access your server. +/// You can specify multiple domains to access your server. In this case, call +/// the function multiple times on `Cors` data. +/// ``` +/// fn cors() { +/// cors.new() +/// |> cors.allow_origin("domain") +/// |> cors.allow_origin("domain2") +/// } +pub fn allow_origin(cors: Cors, origin: String) { + let allow_origin = case cors.allow_origin { + Some(Wildcard) -> Some(Wildcard) + Some(Origin(content)) -> Some(Origin(set.insert(content, origin))) + None -> Some(Origin(set.from_list([origin]))) + } + Cors(..cors, allow_origin: allow_origin) +} + +/// Expose headers in the resulting request. +/// You can specify multiple headers to access your server. In this case, call +/// the function multiple times on `Cors` data. +/// ``` +/// fn cors() { +/// cors.new() +/// |> cors.expose_header("content-type") +/// |> cors.expose_header("vary") +/// } +/// ``` +pub fn expose_header(cors: Cors, header: String) { + let expose_headers = set.insert(cors.expose_headers, header) + Cors(..cors, expose_headers: expose_headers) +} + +/// Set an amount of milliseconds during which CORS requests can be cached. +/// When using `max_age`, the browser will issue one request `OPTIONS` at first, +/// and will reuse the result of that request for the specified amount of time. +/// Once the cache expired, a new `OPTIONS` request will be made. +pub fn max_age(cors: Cors, age: Int) { + let max_age = Some(age) + Cors(..cors, max_age: max_age) +} + +/// Allow credentials to be sent in the request. Credentials take form of +/// username and password, stored in cookies most of the time. +/// Be extremely careful with this header, and consider it with caution, mainly +/// for legacy systems relying on cookies or for systems aware of the danger of +/// cookies, because of [CSRF attacks](https://developer.mozilla.org/en-US/docs/Glossary/CSRF). +/// You probably don't really need it if you use lustre or any modern framework +/// you'll find in the gleam ecosystem! +/// +/// When you can, prefer using some modern system, like OAuth2 or rely on a +/// framework doing the authentication for you. A simple and secured way to +/// authenticate your users is to use the `authorization` header, with a `Bearer` +/// token. +pub fn allow_credentials(cors: Cors) { + let allow_credentials = Some(True) + Cors(..cors, allow_credentials: allow_credentials) +} + +/// Allow methods to be used in subsequent CORS requests. +/// You can specify multiple allowed methods. In this case, call the function +/// multiple times on `Cors` data. +/// ``` +/// import gleam/http +/// +/// fn cors() { +/// cors.new() +/// |> cors.allow_method(http.Get) +/// |> cors.allow_method(http.Post) +/// } +/// ``` +pub fn allow_method(cors: Cors, method: Method) { + let allow_methods = set.insert(cors.allow_methods, method) + Cors(..cors, allow_methods: allow_methods) +} + +/// All header to be sent to the server. +/// You can specify multiple headers to send to your server. In this case, call +/// the function multiple times on `Cors` data. +/// ``` +/// fn cors() { +/// cors.new() +/// |> cors.allow_header("content-type") +/// |> cors.allow_header("origin") +/// } +/// ``` +pub fn allow_header(cors: Cors, header: String) { + let allow_headers = set.insert(cors.allow_headers, header) + Cors(..cors, allow_headers: allow_headers) +} + +// Set functions +// Used internally to simplify the CORS apply. + +fn warn_if_origin_empty(origin: String) { + case origin { + "" -> + io.println( + "origin is empty, but you have multiple allowed domains in your CORS configuration. Are you sure you're calling set_cors_multiple_origin and not set_cors?", + ) + _ -> Nil + } +} + +fn set_allowed_origin(cors: Cors, origin: String) { + let hd = "access-control-allow-origin" + case cors.allow_origin { + None -> function.identity + Some(Wildcard) -> set_header(_, hd, "*") + Some(Origin(origins)) -> { + let origins = set.to_list(origins) + case origins { + [o] -> set_header(_, hd, o) + _ -> { + warn_if_origin_empty(origin) + let not_origin = !list.contains(origins, origin) + use <- bool.guard(when: not_origin, return: function.identity) + fn(res) { + res + |> set_header(hd, origin) + |> set_header("vary", "origin") + } + } + } + } + } +} + +fn set_expose_headers(res: Response(body), cors: Cors) { + let hd = "access-control-expose-headers" + let ls = set.to_list(cors.expose_headers) + use <- bool.guard(when: list.is_empty(ls), return: res) + ls + |> string.join(",") + |> set_header(res, hd, _) +} + +fn set_max_age(res: Response(body), cors: Cors) { + let hd = "access-control-max-age" + cors.max_age + |> option.map(fn(a) { set_header(res, hd, int.to_string(a)) }) + |> option.unwrap(res) +} + +fn set_allow_credentials(res: Response(body), cors: Cors) { + let hd = "access-control-allow-credentials" + cors.allow_credentials + |> option.map(fn(_) { set_header(res, hd, "true") }) + |> option.unwrap(res) +} + +fn method_to_string(method: Method) { + case method { + http.Get -> "GET" + http.Post -> "POST" + http.Head -> "HEAD" + http.Put -> "PUT" + http.Delete -> "DELETE" + http.Trace -> "TRACE" + http.Connect -> "CONNECT" + http.Options -> "OPTIONS" + http.Patch -> "PATCH" + http.Other(content) -> content + } +} + +fn set_allow_methods(res: Response(body), cors: Cors) { + let hd = "access-control-allow-methods" + let methods = set.to_list(cors.allow_methods) + use <- bool.guard(when: list.is_empty(methods), return: res) + methods + |> list.map(method_to_string) + |> string.join(",") + |> set_header(res, hd, _) +} + +fn set_allow_headers(res: Response(body), cors: Cors) { + let hd = "access-control-allow-headers" + let headers = set.to_list(cors.allow_headers) + case list.is_empty(headers) { + True -> res + False -> + headers + |> string.join(",") + |> set_header(res, hd, _) + } +} + +fn set_response(res: Response(body), cors: Cors, origin: Option(String)) { + res + |> set_allowed_origin(cors, option.unwrap(origin, "")) + |> set_expose_headers(cors) + |> set_max_age(cors) + |> set_allow_credentials(cors) + |> set_allow_methods(cors) + |> set_allow_headers(cors) +} + +// Request methods + +/// Set CORS headers on a response. Should be used in your handler. +/// In case you're using a framework, it probably already implements it. +/// If you're using mist or wisp, use the corresponding provided middlewares, +/// ([mist_handle](#mist_handle)) and ([wisp_handle](#wisp_handle)) and do not +/// use this "low-level" function. +pub fn set_cors(res: Response(response), cors: Cors) { + set_response(res, cors, None) +} + +/// Set CORS headers on a response. Should be used when you have multiple +/// allowed domains. Should be used in your handler. +/// In case you're using a framework, it probably already implements it. +/// If you're using mist or wisp, use the corresponding provided middlewares, +/// ([mist_handle](#mist_handle)) and ([wisp_handle](#wisp_handle)) and do not +/// use this "low-level" function. +pub fn set_cors_multiple_origin( + res: Response(response), + cors: Cors, + origin: String, +) { + set_response(res, cors, Some(origin)) +} + +fn find_origin(req: Request(connection)) { + req.headers + |> list.find(fn(h) { pair.first(h) == "origin" }) + |> result.map(pair.second) + |> result.unwrap("") +} + +fn middleware( + empty: resdata, + req: Request(connection), + cors: Cors, + handler: fn(Request(connection)) -> Response(resdata), +) { + let res = case req.method { + http.Options -> response.set_body(response.new(204), empty) + _ -> handler(req) + } + req + |> find_origin() + |> set_cors_multiple_origin(res, cors, _) +} + +/// Intercepts the request for mist and handles CORS directly without worrying +/// about it. Provide your CORS configuration, and you're good to go! +pub fn mist_handle( + req: Request(mist.Connection), + cors: Cors, + handler: fn(Request(mist.Connection)) -> Response(mist.ResponseData), +) { + bytes_builder.new() + |> mist.Bytes() + |> middleware(req, cors, handler) +} + +/// Intercepts the request for wisp and handles CORS directly without worrying +/// about it. Provide your CORS configuration and you're good to go! +pub fn wisp_handle( + req: wisp.Request, + cors: Cors, + handler: fn(wisp.Request) -> wisp.Response, +) { + middleware(wisp.Empty, req, cors, handler) +} diff --git a/src/simple_cors.gleam b/src/simple_cors.gleam deleted file mode 100644 index af3ec5e..0000000 --- a/src/simple_cors.gleam +++ /dev/null @@ -1,225 +0,0 @@ -import gleam/bool -import gleam/bytes_builder -import gleam/function -import gleam/http.{type Method} -import gleam/http/request.{type Request} -import gleam/http/response.{type Response, set_header} -import gleam/int -import gleam/list -import gleam/option.{type Option, None, Some} -import gleam/pair -import gleam/result -import gleam/set.{type Set} -import gleam/string -import mist -import wisp - -pub opaque type Origin { - Wildcard - Origin(Set(String)) -} - -pub opaque type Cors { - Cors( - allow_origin: Option(Origin), - expose_headers: Set(String), - max_age: Option(Int), - allow_credentials: Option(Bool), - allow_methods: Set(Method), - allow_headers: Set(String), - ) -} - -pub fn new() -> Cors { - Cors( - allow_origin: None, - expose_headers: set.new(), - max_age: None, - allow_credentials: None, - allow_methods: set.new(), - allow_headers: set.new(), - ) -} - -/// Be extremely careful, you should not use this function in production! -/// Allowing all origins can easily be a huge security flaw! -/// Allow only the origins you need, and use this function only locally, in dev mode. -pub fn allow_all_origins(cors: Cors) { - let allow_origin = Some(Wildcard) - Cors(..cors, allow_origin: allow_origin) -} - -pub fn allow_origin(cors: Cors, origin: String) { - let allow_origin = case cors.allow_origin { - Some(Wildcard) -> Some(Wildcard) - Some(Origin(content)) -> Some(Origin(set.insert(content, origin))) - None -> Some(Origin(set.from_list([origin]))) - } - Cors(..cors, allow_origin: allow_origin) -} - -pub fn expose_headers(cors: Cors, header: String) { - let expose_headers = set.insert(cors.expose_headers, header) - Cors(..cors, expose_headers: expose_headers) -} - -pub fn max_age(cors: Cors, age: Int) { - let max_age = Some(age) - Cors(..cors, max_age: max_age) -} - -pub fn allow_credentials(cors: Cors) { - let allow_credentials = Some(True) - Cors(..cors, allow_credentials: allow_credentials) -} - -pub fn allow_method(cors: Cors, method: Method) { - let allow_methods = set.insert(cors.allow_methods, method) - Cors(..cors, allow_methods: allow_methods) -} - -pub fn allow_header(cors: Cors, header: String) { - let allow_headers = set.insert(cors.allow_headers, header) - Cors(..cors, allow_headers: allow_headers) -} - -fn set_allowed_origin(cors: Cors, origin: String) { - let hd = "access-control-allow-origin" - case cors.allow_origin { - None -> function.identity - Some(Wildcard) -> set_header(_, hd, "*") - Some(Origin(origins)) -> { - let origins = set.to_list(origins) - case origins { - [o] -> set_header(_, hd, o) - _ -> { - let not_origin = !list.contains(origins, origin) - use <- bool.guard(when: not_origin, return: function.identity) - fn(res) { - res - |> set_header(hd, origin) - |> set_header("vary", "origin") - } - } - } - } - } -} - -fn set_expose_headers(res: Response(body), cors: Cors) { - let hd = "access-control-expose-headers" - let ls = set.to_list(cors.expose_headers) - use <- bool.guard(when: list.is_empty(ls), return: res) - ls - |> string.join(",") - |> set_header(res, hd, _) -} - -fn set_max_age(res: Response(body), cors: Cors) { - let hd = "access-control-max-age" - cors.max_age - |> option.map(fn(a) { set_header(res, hd, int.to_string(a)) }) - |> option.unwrap(res) -} - -fn set_allow_credentials(res: Response(body), cors: Cors) { - let hd = "access-control-allow-credentials" - cors.allow_credentials - |> option.map(fn(_) { set_header(res, hd, "true") }) - |> option.unwrap(res) -} - -fn method_to_string(method: Method) { - case method { - http.Get -> "GET" - http.Post -> "POST" - http.Head -> "HEAD" - http.Put -> "PUT" - http.Delete -> "DELETE" - http.Trace -> "TRACE" - http.Connect -> "CONNECT" - http.Options -> "OPTIONS" - http.Patch -> "PATCH" - http.Other(content) -> content - } -} - -fn set_allow_methods(res: Response(body), cors: Cors) { - let hd = "access-control-allow-methods" - let methods = set.to_list(cors.allow_methods) - use <- bool.guard(when: list.is_empty(methods), return: res) - methods - |> list.map(method_to_string) - |> string.join(",") - |> set_header(res, hd, _) -} - -fn set_allow_headers(res: Response(body), cors: Cors) { - let hd = "access-control-allow-headers" - let headers = set.to_list(cors.allow_headers) - case list.is_empty(headers) { - True -> res - False -> - headers - |> string.join(",") - |> set_header(res, hd, _) - } -} - -fn set_response(res: Response(body), cors: Cors, origin: Option(String)) { - res - |> set_allowed_origin(cors, option.unwrap(origin, "")) - |> set_expose_headers(cors) - |> set_max_age(cors) - |> set_allow_credentials(cors) - |> set_allow_methods(cors) - |> set_allow_headers(cors) -} - -pub fn set_cors(res: Response(response), cors: Cors) { - set_response(res, cors, None) -} - -pub fn set_cors_origin(res: Response(response), cors: Cors, origin: String) { - set_response(res, cors, Some(origin)) -} - -fn find_origin(req: Request(connection)) { - req.headers - |> list.find(fn(h) { pair.first(h) == "origin" }) - |> result.map(pair.second) - |> result.unwrap("") -} - -fn middleware( - empty: resdata, - req: Request(connection), - cors: Cors, - handler: fn(Request(connection)) -> Response(resdata), -) { - let res = case req.method { - http.Options -> response.set_body(response.new(204), empty) - _ -> handler(req) - } - req - |> find_origin() - |> set_cors_origin(res, cors, _) -} - -pub fn mist_handle( - req: Request(mist.Connection), - cors: Cors, - handler: fn(Request(mist.Connection)) -> Response(mist.ResponseData), -) { - bytes_builder.new() - |> mist.Bytes() - |> middleware(req, cors, handler) -} - -pub fn wisp_handle( - req: wisp.Request, - cors: Cors, - handler: fn(wisp.Request) -> wisp.Response, -) { - middleware(wisp.Empty, req, cors, handler) -} diff --git a/test/simple_cors_test.gleam b/test/cors_builder_test.gleam similarity index 100% rename from test/simple_cors_test.gleam rename to test/cors_builder_test.gleam diff --git a/test/servers/full.gleam b/test/servers/full.gleam index c66b62f..48deead 100644 --- a/test/servers/full.gleam +++ b/test/servers/full.gleam @@ -1,9 +1,9 @@ +import cors_builder as cors import gleam/bytes_builder import gleam/http import gleam/http/request.{type Request} import gleam/http/response import mist.{type Connection} -import simple_cors as cors fn cors() { cors.new() @@ -11,7 +11,7 @@ fn cors() { |> cors.allow_origin("http://localhost:4000") |> cors.allow_method(http.Get) |> cors.allow_method(http.Post) - |> cors.expose_headers("content-type") + |> cors.expose_header("content-type") |> cors.max_age(200) |> cors.allow_credentials() |> cors.allow_header("content-type") diff --git a/test/servers/none.gleam b/test/servers/none.gleam index 38173c5..3ae0289 100644 --- a/test/servers/none.gleam +++ b/test/servers/none.gleam @@ -1,8 +1,8 @@ +import cors_builder as cors import gleam/bytes_builder import gleam/http/request.{type Request} import gleam/http/response.{type Response} import mist.{type Connection, type ResponseData} -import simple_cors as cors pub fn run(req: Request(Connection)) -> Response(ResponseData) { use _req <- cors.mist_handle(req, cors.new()) diff --git a/test/servers/partial.gleam b/test/servers/partial.gleam index b0b13df..84a546a 100644 --- a/test/servers/partial.gleam +++ b/test/servers/partial.gleam @@ -1,9 +1,9 @@ +import cors_builder as cors import gleam/bytes_builder import gleam/http import gleam/http/request.{type Request} import gleam/http/response.{type Response} import mist.{type Connection, type ResponseData} -import simple_cors as cors fn cors() { cors.new()