├── .formatter.exs
├── .github
├── FUNDING.yml
├── dependabot.yml
└── workflows
│ ├── static.yml
│ └── test.yml
├── .gitignore
├── LICENSE
├── README.md
├── config
├── config.exs
└── test.exs
├── example
├── app.webmanifest
├── favicon.ico
├── index.html
├── main.js
└── service-worker.js
├── lib
├── mix
│ └── tasks
│ │ └── generate_vapid_keys.ex
├── web_push_elixir.ex
└── web_push_elixir
│ ├── application.ex
│ └── mock_server.ex
├── mix.exs
├── mix.lock
└── test
├── generate_vapid_keys_test.exs
├── test_helper.exs
└── web_push_elixir_test.exs
/.formatter.exs:
--------------------------------------------------------------------------------
1 | # Used by "mix format"
2 | [
3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
4 | ]
5 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: trueChazza
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: mix
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | time: "10:00"
8 | open-pull-requests-limit: 10
--------------------------------------------------------------------------------
/.github/workflows/static.yml:
--------------------------------------------------------------------------------
1 | # Simple workflow for deploying static content to GitHub Pages
2 | name: Deploy static content to Pages
3 |
4 | on:
5 | # Runs on pushes targeting the default branch
6 | push:
7 | branches: ["main"]
8 |
9 | # Allows you to run this workflow manually from the Actions tab
10 | workflow_dispatch:
11 |
12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
13 | permissions:
14 | contents: read
15 | pages: write
16 | id-token: write
17 |
18 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
19 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
20 | concurrency:
21 | group: "pages"
22 | cancel-in-progress: false
23 |
24 | jobs:
25 | # Single deploy job since we're just deploying
26 | deploy:
27 | environment:
28 | name: github-pages
29 | url: ${{ steps.deployment.outputs.page_url }}
30 | runs-on: ubuntu-latest
31 | steps:
32 | - name: Checkout
33 | uses: actions/checkout@v3
34 | - name: Setup Pages
35 | uses: actions/configure-pages@v3
36 | - name: Upload artifact
37 | uses: actions/upload-pages-artifact@v2
38 | with:
39 | # Upload entire repository
40 | path: './example'
41 | - name: Deploy to GitHub Pages
42 | id: deployment
43 | uses: actions/deploy-pages@v2
44 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 |
3 | on:
4 | push:
5 | branches:
6 | - '**'
7 |
8 | env:
9 | MIX_ENV: test
10 |
11 | jobs:
12 |
13 | test:
14 | name: OTP ${{matrix.otp}} / Elixir ${{matrix.elixir}}
15 | runs-on: ubuntu-latest
16 | strategy:
17 | matrix:
18 | otp: [ '26', '25', '24' ]
19 | elixir: [ '1.15' ]
20 | steps:
21 | - uses: actions/checkout@v3
22 | - uses: erlef/setup-beam@v1
23 | with:
24 | otp-version: ${{matrix.otp}}
25 | elixir-version: ${{matrix.elixir}}
26 | - run: mix deps.get
27 | - run: mix test
28 | - run: mix coveralls.json
29 | - uses: codecov/codecov-action@v3
30 | env:
31 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # The directory Mix will write compiled artifacts to.
2 | /_build/
3 |
4 | # If you run "mix test --cover", coverage assets end up here.
5 | /cover/
6 |
7 | # The directory Mix downloads your dependencies sources to.
8 | /deps/
9 |
10 | # Where third-party dependencies like ExDoc output generated docs.
11 | /doc/
12 |
13 | # Ignore .fetch files in case you like to edit your project deps locally.
14 | /.fetch
15 |
16 | # If the VM crashes, it generates a dump, let's ignore it too.
17 | erl_crash.dump
18 |
19 | # Also ignore archive artifacts (built via "mix archive.build").
20 | *.ez
21 |
22 | # Ignore package tarball (built via "mix hex.build").
23 | web_push_elixir-*.tar
24 |
25 | # Temporary files, for example, from tests.
26 | /tmp/
27 |
28 | /.idea/
29 | *.iml
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Midarr Labs
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # Web Push Elixir
3 |
4 | Simple web push library for Elixir
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | ## Prerequisities
21 |
22 | * Elixir 1.15
23 | * OTP 24 / 25 / 26
24 |
25 | ## Installation
26 |
27 | 1. Add `web_push_elixir` to your list of dependencies in `mix.exs`:
28 |
29 | ```elixir
30 | def deps do
31 | [
32 | {:web_push_elixir, "~> 0.4.0"}
33 | ]
34 | end
35 | ```
36 |
37 | 2. Run mix command to generate your Vapid public and private keys:
38 |
39 | ```commandline
40 | mix generate.vapid.keys
41 | ```
42 |
43 | 3. Set config for your generated keys:
44 |
45 | ```elixir
46 | config :web_push_elixir,
47 | vapid_public_key: "someVapidPublicKey",
48 | vapid_private_key: "someVapidPrivateKey",
49 | vapid_subject: "mailto:admin@email.com"
50 | ```
51 |
52 | ## Usage
53 |
54 | `WebPushElixir` provides a simple `send_notification/2` that takes 2 arguments:
55 |
56 | * `subscription`: the subscription information received from the client - [example demo](https://midarrlabs.github.io/web-push-elixir/)
57 | * `message`: the message string.
58 |
59 | ```elixir
60 | subscription = '{"endpoint":"https://some-push-service","keys":{"p256dh":"BNcRdreALRFXTkOOUHK1EtK2wtaz5Ry4YfYCA_0QTpQtUbVlUls0VJXg7A8u-Ts1XbjhazAkj7I99e8QcYP7DkM=","auth":"tBHItJI5svbpez7KI4CCXg=="}}'
61 | message = "Some message"
62 |
63 | WebPushElixir.send_notification(subscription, message)
64 | ```
65 |
66 | For more information on how to subscribe a client, permission UX and more - take a look at [https://web.dev/notifications/](https://web.dev/notifications/)
67 |
68 | ## Run tests
69 |
70 | ```commandline
71 | mix test
72 | ```
73 |
74 | ## License
75 |
76 | Web Push Elixir is open-sourced software licensed under the [MIT license](LICENSE).
77 |
78 |
79 | ## Credits
80 |
81 | Heavily inspired by [elixir-web-push-encryption](https://github.com/danhper/elixir-web-push-encryption)
82 |
--------------------------------------------------------------------------------
/config/config.exs:
--------------------------------------------------------------------------------
1 | import Config
2 |
3 | if Mix.env() == :test do
4 | import_config "test.exs"
5 | end
--------------------------------------------------------------------------------
/config/test.exs:
--------------------------------------------------------------------------------
1 | import Config
2 |
3 | config :web_push_elixir,
4 | vapid_public_key: "someVapidPublicKey",
5 | vapid_private_key: "someVapidPrivateKey",
6 | vapid_subject: "mailto:admin@email.com"
--------------------------------------------------------------------------------
/example/app.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Web Push Elixir",
3 | "display": "standalone"
4 | }
--------------------------------------------------------------------------------
/example/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/midarrlabs/web-push-elixir/092ac293b5f98781d28b74a1e6c8afe9dcab82d1/example/favicon.ico
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Web Push Elixir
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
Web Push Elixir
15 |
Client Subscription
16 |
Example client subscription for push notifications
17 |
18 |
19 |
20 |
21 |
22 |
23 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/example/main.js:
--------------------------------------------------------------------------------
1 | const message = document.getElementById('message')
2 |
3 | navigator.serviceWorker
4 | .register(`${ window.location.pathname }service-worker.js`)
5 | .then(registration => {
6 | message.innerHTML += 'Service worker successfully registered
'
7 | })
8 | .catch(err => {
9 | message.innerHTML += `Unable to register service worker - ${err}
`
10 | })
11 |
12 | const request = document.getElementById('request')
13 | const subscribe = document.getElementById('subscribe')
14 |
15 | request.addEventListener('click', event => {
16 | Notification.requestPermission()
17 | .then(permission => {
18 | message.innerHTML += `Permission ${permission}
`
19 | })
20 | });
21 |
22 | subscribe.addEventListener('click', event => {
23 | navigator.serviceWorker.ready
24 | .then(registration => {
25 |
26 | registration.pushManager.subscribe({
27 | userVisibleOnly: true,
28 | applicationServerKey: 'BDntLA3k5K1tsrFOXXAuS_9Ey30jxy-R2CAosC2DOQnTs8LpQGxpTEx3AcPXinVYFFpJI6tT_RJC8pHgUsdbhOk'
29 | })
30 | .then(pushSubscription => {
31 | message.innerHTML += `Received PushSubscription:
`
32 | message.innerHTML += `${JSON.stringify(pushSubscription)}
`
33 | })
34 | .catch(err => {
35 | registration.pushManager.getSubscription().then(subscription => {
36 | if (subscription !== null) {
37 | subscription
38 | .unsubscribe()
39 | .then(successful => {
40 | message.innerHTML += 'Unsubscribed from existing subscription, please subscribe again
'
41 | })
42 | .catch(err => {
43 | message.innerHTML += `Failed to unsubscribe from existing subscription - ${err}
`
44 | })
45 | } else {
46 | message.innerHTML += 'No subscription found
'
47 | }
48 | })
49 | })
50 | })
51 | })
--------------------------------------------------------------------------------
/example/service-worker.js:
--------------------------------------------------------------------------------
1 | self.addEventListener('push', event => {
2 | if (event.data) {
3 | const promiseChain = self.registration.showNotification(event.data.text());
4 |
5 | event.waitUntil(promiseChain);
6 | } else {
7 | console.log('This push event has no data.');
8 | }
9 | });
--------------------------------------------------------------------------------
/lib/mix/tasks/generate_vapid_keys.ex:
--------------------------------------------------------------------------------
1 | defmodule Mix.Tasks.Generate.Vapid.Keys do
2 | @moduledoc "The mix task: `mix help generate.vapid.keys`"
3 | use Mix.Task
4 |
5 | @shortdoc "Generate vapid keys"
6 | def run(_args) do
7 | {public_key, private_key} = :crypto.generate_key(:ecdh, :prime256v1)
8 |
9 | %{
10 | vapid_public_key: Base.url_encode64(public_key, padding: false),
11 | vapid_private_key: Base.url_encode64(private_key, padding: false),
12 | vapid_subject: "mailto:admin@email.com"
13 | }
14 | |> IO.inspect
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/lib/web_push_elixir.ex:
--------------------------------------------------------------------------------
1 | defmodule WebPushElixir do
2 | @moduledoc """
3 | Module to send web push notifications with an encrypted payload.
4 | """
5 |
6 | defp url_encode(string) do
7 | Base.url_encode64(string, padding: false)
8 | end
9 |
10 | defp url_decode(string) do
11 | Base.url_decode64!(string, padding: false)
12 | end
13 |
14 | defp hmac_based_key_derivation_function(salt, initial_keying_material, info, length) do
15 | pseudo_random_key =
16 | :crypto.mac_init(:hmac, :sha256, salt)
17 | |> :crypto.mac_update(initial_keying_material)
18 | |> :crypto.mac_final()
19 |
20 | :crypto.mac_init(:hmac, :sha256, pseudo_random_key)
21 | |> :crypto.mac_update(info)
22 | |> :crypto.mac_update(<<1>>)
23 | |> :crypto.mac_final()
24 | |> :binary.part(0, length)
25 | end
26 |
27 | defp encrypt_payload(message, p256dh, auth) do
28 | client_public_key = url_decode(p256dh)
29 | client_auth_secret = url_decode(auth)
30 |
31 | salt = :crypto.strong_rand_bytes(16)
32 |
33 | {local_public_key, local_private_key} = :crypto.generate_key(:ecdh, :prime256v1)
34 |
35 | shared_secret = :crypto.compute_key(:ecdh, client_public_key, local_private_key, :prime256v1)
36 |
37 | pseudo_random_key =
38 | hmac_based_key_derivation_function(
39 | client_auth_secret,
40 | shared_secret,
41 | "Content-Encoding: auth" <> <<0>>,
42 | 32
43 | )
44 |
45 | context =
46 | <<0, byte_size(client_public_key)::unsigned-big-integer-size(16)>> <>
47 | client_public_key <>
48 | <> <> local_public_key
49 |
50 | content_encryption_key_info = "Content-Encoding: aesgcm" <> <<0>> <> "P-256" <> context
51 |
52 | content_encryption_key =
53 | hmac_based_key_derivation_function(salt, pseudo_random_key, content_encryption_key_info, 16)
54 |
55 | nonce =
56 | hmac_based_key_derivation_function(
57 | salt,
58 | pseudo_random_key,
59 | "Content-Encoding: nonce" <> <<0>> <> "P-256" <> context,
60 | 12
61 | )
62 |
63 | padded_message = <<0::unsigned-big-integer-size(16)>> <> :binary.copy(<<0>>, 0) <> message
64 |
65 | {cipher_text, cipher_tag} =
66 | :crypto.crypto_one_time_aead(
67 | :aes_128_gcm,
68 | content_encryption_key,
69 | nonce,
70 | padded_message,
71 | <<>>,
72 | true
73 | )
74 |
75 | %{ciphertext: cipher_text <> cipher_tag, salt: salt, local_public_key: local_public_key}
76 | end
77 |
78 | defp sign_json_web_token(endpoint, vapid_public_key, vapid_private_key) do
79 | json_web_token =
80 | JOSE.JWT.from_map(%{
81 | aud: URI.parse(endpoint).scheme <> "://" <> URI.parse(endpoint).host,
82 | exp: DateTime.to_unix(DateTime.utc_now()) + 12 * 3600,
83 | sub: Application.get_env(:web_push_elixir, :vapid_subject)
84 | })
85 |
86 | json_web_key =
87 | JOSE.JWK.from_key(
88 | {:ECPrivateKey, 1, vapid_private_key, {:namedCurve, {1, 2, 840, 10045, 3, 1, 7}},
89 | vapid_public_key, nil}
90 | )
91 |
92 | {%{alg: :jose_jws_alg_ecdsa}, signed_json_web_token} =
93 | JOSE.JWS.compact(JOSE.JWT.sign(json_web_key, %{"alg" => "ES256"}, json_web_token))
94 |
95 | signed_json_web_token
96 | end
97 |
98 | @doc """
99 | Sends a web push notification with an encrypted payload.
100 |
101 | ## Arguments
102 |
103 | * `subscription` the subscription information received from the client. Accepted example: `'{"endpoint":"https://some-push-service","keys":{"p256dh":"BNcRdreALRFXTkOOUHK1EtK2wtaz5Ry4YfYCA_0QTpQtUbVlUls0VJXg7A8u-Ts1XbjhazAkj7I99e8QcYP7DkM=","auth":"tBHItJI5svbpez7KI4CCXg=="}}'`
104 | * `message` the message string.
105 |
106 | ## Return value
107 |
108 | Returns the result of `HTTPoison.post`
109 | """
110 | def send_notification(subscription, message) do
111 | vapid_public_key = url_decode(Application.get_env(:web_push_elixir, :vapid_public_key))
112 | vapid_private_key = url_decode(Application.get_env(:web_push_elixir, :vapid_private_key))
113 |
114 | %{"endpoint" => endpoint, "keys" => %{"p256dh" => p256dh, "auth" => auth}} =
115 | Jason.decode!(subscription)
116 |
117 | encrypted_payload = encrypt_payload(message, p256dh, auth)
118 |
119 | signed_json_web_token =
120 | sign_json_web_token(endpoint, vapid_public_key, vapid_private_key)
121 |
122 | HTTPoison.post(endpoint, encrypted_payload.ciphertext, %{
123 | "Authorization" => "WebPush #{signed_json_web_token}",
124 | "Content-Encoding" => "aesgcm",
125 | "Content-Length" => "#{byte_size(encrypted_payload.ciphertext)}",
126 | "Content-Type" => "application/octet-stream",
127 | "Crypto-Key" =>
128 | "dh=#{url_encode(encrypted_payload.local_public_key)};p256ecdsa=#{url_encode(vapid_public_key)}",
129 | "Encryption" => "salt=#{url_encode(encrypted_payload.salt)}",
130 | "TTL" => "60"
131 | })
132 | end
133 | end
134 |
--------------------------------------------------------------------------------
/lib/web_push_elixir/application.ex:
--------------------------------------------------------------------------------
1 | defmodule WebPushElixir.Application do
2 | use Application
3 |
4 | def start(_type, _args) do
5 | children = [
6 | {DynamicSupervisor, name: WebPushElixir.DynamicSupervisor}
7 | ]
8 |
9 | opts = [strategy: :one_for_one, name: WebPushElixir.Supervisor]
10 | Supervisor.start_link(children, opts)
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/lib/web_push_elixir/mock_server.ex:
--------------------------------------------------------------------------------
1 | defmodule WebPushElixir.MockServer do
2 | use Plug.Router
3 |
4 | plug(:match)
5 | plug(:dispatch)
6 |
7 | post "/some-push-service" do
8 | conn
9 | |> Plug.Conn.send_resp(200, "ok")
10 | end
11 |
12 | get "/" do
13 | conn
14 | |> put_resp_header("content-type", "text/html; charset=utf-8")
15 | |> Plug.Conn.send_file(200, "./example/index.html")
16 | end
17 |
18 | get "/app.webmanifest" do
19 | conn
20 | |> put_resp_header("content-type", "application/manifest+json")
21 | |> Plug.Conn.send_file(200, "./example/app.webmanifest")
22 | end
23 |
24 | get "/main.js" do
25 | conn
26 | |> put_resp_header("content-type", "application/x-javascript")
27 | |> Plug.Conn.send_file(200, "./example/main.js")
28 | end
29 |
30 | get "/service-worker.js" do
31 | conn
32 | |> put_resp_header("content-type", "application/x-javascript")
33 | |> Plug.Conn.send_file(200, "./example/service-worker.js")
34 | end
35 |
36 | get "/web-push-elixir/service-worker.js" do
37 | conn
38 | |> put_resp_header("content-type", "application/x-javascript")
39 | |> Plug.Conn.send_file(200, "./example/service-worker.js")
40 | end
41 |
42 | get "/favicon.ico" do
43 | conn
44 | |> put_resp_header("content-type", "image/x-icon")
45 | |> Plug.Conn.send_file(200, "./example/favicon.ico")
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule WebPushElixir.MixProject do
2 | use Mix.Project
3 |
4 | def project do
5 | [
6 | app: :web_push_elixir,
7 | version: "0.4.0",
8 | elixir: "~> 1.15",
9 | start_permanent: Mix.env() == :prod,
10 | deps: deps(),
11 | description: "Simple web push for Elixir",
12 | package: [
13 | licenses: ["MIT"],
14 | links: %{"GitHub" => "https://github.com/midarrlabs/web-push-elixir"}
15 | ],
16 | test_coverage: [tool: ExCoveralls]
17 | ]
18 | end
19 |
20 | # Run "mix help compile.app" to learn about applications.
21 | def application do
22 | [
23 | mod: {WebPushElixir.Application, []},
24 | extra_applications: [:logger]
25 | ]
26 | end
27 |
28 | # Run "mix help deps" to learn about dependencies.
29 | defp deps do
30 | [
31 | {:ex_doc, "~> 0.27", only: :dev, runtime: false},
32 | {:excoveralls, "~> 0.10", only: :test},
33 | {:jose, "~> 1.11"},
34 | {:jason, "~> 1.4"},
35 | {:plug, "~> 1.14"},
36 | {:plug_cowboy, "~> 2.0"},
37 | {:httpoison, "~> 2.0"}
38 | ]
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/mix.lock:
--------------------------------------------------------------------------------
1 | %{
2 | "certifi": {:hex, :certifi, "2.14.0", "ed3bef654e69cde5e6c022df8070a579a79e8ba2368a00acf3d75b82d9aceeed", [:rebar3], [], "hexpm", "ea59d87ef89da429b8e905264fdec3419f84f2215bb3d81e07a18aac919026c3"},
3 | "cowboy": {:hex, :cowboy, "2.13.0", "09d770dd5f6a22cc60c071f432cd7cb87776164527f205c5a6b0f24ff6b38990", [:make, :rebar3], [{:cowlib, ">= 2.14.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "e724d3a70995025d654c1992c7b11dbfea95205c047d86ff9bf1cda92ddc5614"},
4 | "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
5 | "cowlib": {:hex, :cowlib, "2.14.0", "623791c56c1cc9df54a71a9c55147a401549917f00a2e48a6ae12b812c586ced", [:make, :rebar3], [], "hexpm", "0af652d1550c8411c3b58eed7a035a7fb088c0b86aff6bc504b0bc3b7f791aa2"},
6 | "earmark_parser": {:hex, :earmark_parser, "1.4.42", "f23d856f41919f17cd06a493923a722d87a2d684f143a1e663c04a2b93100682", [:mix], [], "hexpm", "6915b6ca369b5f7346636a2f41c6a6d78b5af419d61a611079189233358b8b8b"},
7 | "ex_doc": {:hex, :ex_doc, "0.36.1", "4197d034f93e0b89ec79fac56e226107824adcce8d2dd0a26f5ed3a95efc36b1", [:mix], [{:earmark_parser, "~> 1.4.42", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "d7d26a7cf965dacadcd48f9fa7b5953d7d0cfa3b44fa7a65514427da44eafd89"},
8 | "excoveralls": {:hex, :excoveralls, "0.18.5", "e229d0a65982613332ec30f07940038fe451a2e5b29bce2a5022165f0c9b157e", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "523fe8a15603f86d64852aab2abe8ddbd78e68579c8525ae765facc5eae01562"},
9 | "hackney": {:hex, :hackney, "1.23.0", "55cc09077112bcb4a69e54be46ed9bc55537763a96cd4a80a221663a7eafd767", [:rebar3], [{:certifi, "~> 2.14.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "6cd1c04cd15c81e5a493f167b226a15f0938a84fc8f0736ebe4ddcab65c0b44e"},
10 | "httpoison": {:hex, :httpoison, "2.2.3", "a599d4b34004cc60678999445da53b5e653630651d4da3d14675fedc9dd34bd6", [:mix], [{:hackney, "~> 1.21", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "fa0f2e3646d3762fdc73edb532104c8619c7636a6997d20af4003da6cfc53e53"},
11 | "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
12 | "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
13 | "jose": {:hex, :jose, "1.11.10", "a903f5227417bd2a08c8a00a0cbcc458118be84480955e8d251297a425723f83", [:mix, :rebar3], [], "hexpm", "0d6cd36ff8ba174db29148fc112b5842186b68a90ce9fc2b3ec3afe76593e614"},
14 | "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"},
15 | "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"},
16 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"},
17 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
18 | "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"},
19 | "mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"},
20 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
21 | "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
22 | "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"},
23 | "plug_cowboy": {:hex, :plug_cowboy, "2.7.3", "1304d36752e8bdde213cea59ef424ca932910a91a07ef9f3874be709c4ddb94b", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "77c95524b2aa5364b247fa17089029e73b951ebc1adeef429361eab0bb55819d"},
24 | "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"},
25 | "ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"},
26 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
27 | "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
28 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
29 | }
30 |
--------------------------------------------------------------------------------
/test/generate_vapid_keys_test.exs:
--------------------------------------------------------------------------------
1 | defmodule GenerateVapidKeysTest do
2 | use ExUnit.Case
3 |
4 | test "it should generate" do
5 | assert %{
6 | vapid_public_key: <<_vapid_public_key::binary>>,
7 | vapid_private_key: <<_vapid_private_key::binary>>,
8 | vapid_subject: "mailto:admin@email.com"
9 | } = Mix.Tasks.Generate.Vapid.Keys.run([])
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | ExUnit.start()
2 |
3 | DynamicSupervisor.start_child(WebPushElixir.DynamicSupervisor,{Plug.Cowboy, scheme: :http, plug: WebPushElixir.MockServer, options: [port: 4040]})
4 |
--------------------------------------------------------------------------------
/test/web_push_elixir_test.exs:
--------------------------------------------------------------------------------
1 | defmodule WebPushElixirTest do
2 | use ExUnit.Case
3 |
4 | @subscription '{"endpoint":"http://localhost:4040/some-push-service","keys":{"p256dh":"BNcRdreALRFXTkOOUHK1EtK2wtaz5Ry4YfYCA_0QTpQtUbVlUls0VJXg7A8u-Ts1XbjhazAkj7I99e8QcYP7DkM=","auth":"tBHItJI5svbpez7KI4CCXg=="}}'
5 |
6 | test "it should send notification" do
7 | %{
8 | vapid_public_key: vapid_public_key,
9 | vapid_private_key: vapid_private_key,
10 | vapid_subject: vapid_subject
11 | } = Mix.Tasks.Generate.Vapid.Keys.run([])
12 |
13 | Application.put_env(:web_push_elixir, :vapid_public_key, vapid_public_key)
14 |
15 | Application.put_env(:web_push_elixir, :vapid_private_key, vapid_private_key)
16 |
17 | Application.put_env(:web_push_elixir, :vapid_subject, vapid_subject)
18 |
19 | {:ok, response} = WebPushElixir.send_notification(@subscription, "some message")
20 |
21 | assert [
22 | {"Authorization", "WebPush " <> <<_jwt::binary>>},
23 | {"Content-Encoding", "aesgcm"},
24 | {"Content-Length", "30"},
25 | {"Content-Type", "application/octet-stream"},
26 | {"Crypto-Key", <<_crypto_keys::binary>>},
27 | {"Encryption", "salt=" <> <<_salt::binary>>},
28 | {"TTL", "60"}
29 | ] = response.request.headers
30 |
31 | assert <<_body::binary>> = response.request.body
32 | end
33 |
34 | test "it should have index headers" do
35 | {:ok, response} = HTTPoison.get(~c"http://localhost:4040")
36 |
37 | assert [
38 | {"cache-control", "max-age=0, private, must-revalidate"},
39 | {"content-length", "1578"},
40 | {"content-type", "text/html; charset=utf-8"},
41 | {"date", <<_date::binary>>},
42 | {"server", "Cowboy"}
43 | ] = response.headers
44 | end
45 |
46 | test "it should have mainfest headers" do
47 | {:ok, response} = HTTPoison.get(~c"http://localhost:4040/app.webmanifest")
48 |
49 | assert [
50 | {"cache-control", "max-age=0, private, must-revalidate"},
51 | {"content-length", "58"},
52 | {"content-type", "application/manifest+json"},
53 | {"date", <<_date::binary>>},
54 | {"server", "Cowboy"}
55 | ] = response.headers
56 | end
57 |
58 | test "it should have main js headers" do
59 | {:ok, response} = HTTPoison.get(~c"http://localhost:4040/main.js")
60 |
61 | assert [
62 | {"cache-control", "max-age=0, private, must-revalidate"},
63 | {"content-length", "1911"},
64 | {"content-type", "application/x-javascript"},
65 | {"date", <<_date::binary>>},
66 | {"server", "Cowboy"}
67 | ] = response.headers
68 | end
69 |
70 | test "it should have service worker headers" do
71 | {:ok, response} = HTTPoison.get(~c"http://localhost:4040/service-worker.js")
72 |
73 | assert [
74 | {"cache-control", "max-age=0, private, must-revalidate"},
75 | {"content-length", "262"},
76 | {"content-type", "application/x-javascript"},
77 | {"date", <<_date::binary>>},
78 | {"server", "Cowboy"}
79 | ] = response.headers
80 | end
81 |
82 | test "it should have static service worker headers" do
83 | {:ok, response} = HTTPoison.get(~c"http://localhost:4040/web-push-elixir/service-worker.js")
84 |
85 | assert [
86 | {"cache-control", "max-age=0, private, must-revalidate"},
87 | {"content-length", "262"},
88 | {"content-type", "application/x-javascript"},
89 | {"date", <<_date::binary>>},
90 | {"server", "Cowboy"}
91 | ] = response.headers
92 | end
93 |
94 | test "it should have favicon headers" do
95 | {:ok, response} = HTTPoison.get(~c"http://localhost:4040/favicon.ico")
96 |
97 | assert [
98 | {"cache-control", "max-age=0, private, must-revalidate"},
99 | {"content-length", "1150"},
100 | {"content-type", "image/x-icon"},
101 | {"date", <<_date::binary>>},
102 | {"server", "Cowboy"}
103 | ] = response.headers
104 | end
105 | end
106 |
--------------------------------------------------------------------------------