Skip to content

Commit 923eef6

Browse files
committed
update readme, rename/update vapid task, add ex_doc
1 parent 4bae9ac commit 923eef6

File tree

5 files changed

+111
-68
lines changed

5 files changed

+111
-68
lines changed

README.md

+53-57
Original file line numberDiff line numberDiff line change
@@ -18,67 +18,44 @@ def deps do
1818
end
1919
```
2020

21-
## Usage
21+
## Configuration
2222

23-
`serviceWorker.js` example (abridged):
23+
`WebPush` needs VAPID keys to work. Run `mix web_push.vapid` and adapt the
24+
generated config to your needs:
2425

25-
```javascript
26-
self.addEventListener('push', function(event) {
27-
event.waitUntil(
28-
self.clients.matchAll().then(function(_clientList) {
29-
const payload = event.data.json();
30-
return self.registration.showNotification(payload.title, { body: payload.body, });
31-
})
32-
);
33-
});
3426
```
27+
$ mix web_push.vapid
28+
# in config/config.exs:
29+
30+
config :web_push, :vapid,
31+
public_key: "<base64 encoded public key>"
32+
subject: "[email protected]"
33+
34+
# in config/runtime.exs:
3535
36-
`notify.js` example:
36+
config :web_push, :vapid,
37+
private_key: System.fetch_env!("WEB_PUSH_VAPID_PRIVATE_KEY")
38+
39+
# in your environment:
40+
41+
export WEB_PUSH_VAPID_PRIVATE_KEY=<base64 encoded private key>
42+
```
43+
44+
## Usage
45+
46+
Your application should handle getting and persisting subscription data from
47+
browsers. The implementation is up to you but the VAPID public key is required
48+
to be presented when calling `pushManager.subscribe()`:
3749

3850
```javascript
39-
function postSubscription(subscription) {
40-
fetch("https://app.example.com/subscriptions", {
41-
body: JSON.stringify({ subscription }),
42-
method: "POST",
43-
headers: {
44-
"content-type": "application/json",
45-
},
46-
});
47-
}
48-
49-
document.getElementById("#request-permission").addEventListener("click", (_event) => {
50-
Notification.requestPermission()
51-
.then((permission) => {
52-
if (permission === "granted") {
53-
navigator.serviceWorker
54-
.register("https://app.example.com/serviceWorker.js", {scope: "https://app.example.com"})
55-
.then((registration) => {
56-
registration.pushManager.getSubscription()
57-
.then((subscription) => {
58-
if (subscription) {
59-
postSubscription(subscription);
60-
} else {
61-
document.getElementById("#subscribe").addEventListener("click", (_event) => {
62-
const subscribeOpts = {
63-
userVisibleOnly: true,
64-
applicationServerKey: "VAPID public key",
65-
};
66-
67-
registration.pushManager.subscribe(subscribeOpts)
68-
.then(postSubscription);
69-
});
70-
}
71-
});
72-
.catch(console.error);
73-
74-
})
75-
.catch(console.error);
76-
}
77-
})
78-
.catch(console.error);
79-
});
51+
pushManager.subscribe({
52+
userVisibleOnly: true,
53+
applicationServerKey: vapidPublicKey,
54+
}).then(...);
8055
```
8156

57+
Once you have your subscription data, you may construct a `WebPush.Request` and
58+
use it to make the push notification via the HTTP client of your choice.
8259

8360
```elixir
8461
# create the struct from the subscription JSON data
@@ -87,7 +64,7 @@ subscription =
8764
{"endpoint":"https://push.example.com/123","keys":{"p256dh":"user_agent_public_key","auth":"auth_secret"}}
8865
""")
8966

90-
# structured message, see example serviceWorker.js above
67+
# structured message, see example serviceWorker.js linked below
9168
message = %{title: "Notification Title", body: "lorem ipsum etc"}
9269

9370
# generate request details
@@ -113,7 +90,26 @@ request.headers
11390
Tesla.post(request.endpoint, request.body, headers: Map.to_list(request.headers))
11491
```
11592

116-
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
117-
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
118-
be found at <https://hexdocs.pm/web_push>.
93+
## tl()
94+
95+
#### Motivation && Inspiration
96+
97+
* [web-push-elixir](https://github.com/midarrlabs/web-push-elixir)
98+
* [web-push-encryption](https://github.com/tuvistavie/elixir-web-push-encryption)
99+
* [web-push](https://github.com/web-push-libs/web-push)
100+
* [erl_web_push](https://github.com/truqu/erl_web_push)
101+
102+
#### Useful Links
103+
104+
* https://datatracker.ietf.org/doc/html/rfc8291/
105+
* https://datatracker.ietf.org/doc/html/rfc8188/
106+
* https://datatracker.ietf.org/doc/html/rfc3279/
107+
108+
* https://mozilla-services.github.io/WebPushDataTestPage/
109+
* https://developer.mozilla.org/en-US/docs/Web/API/Push_API
110+
* https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers
111+
* https://github.com/mdn/serviceworker-cookbook/tree/master/push-payload
112+
* https://blog.mozilla.org/services/2016/08/23/sending-vapid-identified-webpush-notifications-via-mozillas-push-service/
113+
* https://hacks.mozilla.org/2017/05/debugging-web-push-in-mozilla-firefox/
119114

115+
* https://developer.apple.com/documentation/usernotifications/sending-web-push-notifications-in-web-apps-and-browsers

lib/mix/tasks/web_push/vapid.ex

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
defmodule Mix.Tasks.WebPush.Vapid do
2+
@moduledoc """
3+
Generate VAPID keys for config.
4+
5+
Example:
6+
7+
$ mix web_push.vapid
8+
9+
"""
10+
11+
@shortdoc "Generate VAPID keys for config"
12+
13+
use Mix.Task
14+
15+
@impl true
16+
def run(_args) do
17+
:ecdh
18+
|> :crypto.generate_key(:prime256v1)
19+
|> encode()
20+
|> build_config_message()
21+
|> IO.puts()
22+
end
23+
24+
defp encode({public_key, private_key}), do: {encode(public_key), encode(private_key)}
25+
26+
defp encode(key), do: Base.url_encode64(key, padding: false)
27+
28+
defp build_config_message({public_key, private_key}) do
29+
"""
30+
# in config/config.exs:
31+
32+
config :web_push, :vapid,
33+
public_key: "#{public_key}",
34+
subject: "[email protected]"
35+
36+
# in config/runtime.exs:
37+
38+
config :web_push, :vapid,
39+
private_key: System.fetch_env!("WEB_PUSH_VAPID_PRIVATE_KEY")
40+
41+
# in your environment:
42+
43+
export WEB_PUSH_VAPID_PRIVATE_KEY=#{private_key}
44+
"""
45+
end
46+
end

lib/web_push.ex

+5-11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
defmodule WebPush do
22
@moduledoc """
3-
Web Push notifications.
3+
Web Push notifications via `aes128gcm`.
44
55
Implementation of RFC 8291, 8188, & 5689.
66
"""
@@ -142,7 +142,7 @@ defmodule WebPush do
142142
# number of zero octets. The last record uses a padding delimiter
143143
# octet set to the value 2, all other records have a padding delimiter
144144
# octet value of 1.
145-
payload <> <<2>>,
145+
<<payload::binary, 2>>,
146146
# The additional data passed to each invocation of AEAD_AES_128_GCM is
147147
# a zero-length octet sequence.
148148
<<>>,
@@ -166,18 +166,12 @@ defmodule WebPush do
166166

167167
@spec hkdf_extract(salt(), input_keying_material()) :: psuedo_random_key()
168168
defp hkdf_extract(salt, input_keying_material) do
169-
:crypto.mac_init(:hmac, :sha256, salt)
170-
|> :crypto.mac_update(input_keying_material)
171-
|> :crypto.mac_final()
169+
:crypto.mac(:hmac, :sha256, salt, input_keying_material)
172170
end
173171

174172
@spec hkdf_expand(psuedo_random_key(), info(), length()) :: key()
175-
defp hkdf_expand(prk, info, length) do
176-
:crypto.mac_init(:hmac, :sha256, prk)
177-
|> :crypto.mac_update(info)
178-
|> :crypto.mac_update(<<1>>)
179-
|> :crypto.mac_final()
180-
|> :binary.part(0, length)
173+
defp hkdf_expand(psuedo_random_key, info, length) do
174+
:crypto.macN(:hmac, :sha256, psuedo_random_key, <<info::binary, 1>>, length)
181175
end
182176

183177
@spec fetch_vapid!(vapid_key(), boolean()) :: String.t()

mix.exs

+1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ defmodule WebPush.MixProject do
4141
[
4242
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
4343
{:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false},
44+
{:ex_doc, "~> 0.34", only: :dev, runtime: false},
4445
{:jose, "~> 1.11"}
4546
]
4647
end

mix.lock

+6
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,14 @@
22
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
33
"credo": {:hex, :credo, "1.7.7", "771445037228f763f9b2afd612b6aa2fd8e28432a95dbbc60d8e03ce71ba4446", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8bc87496c9aaacdc3f90f01b7b0582467b69b4bd2441fe8aae3109d843cc2f2e"},
44
"dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"},
5+
"earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"},
56
"erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"},
7+
"ex_doc": {:hex, :ex_doc, "0.34.2", "13eedf3844ccdce25cfd837b99bea9ad92c4e511233199440488d217c92571e8", [:mix], [{:earmark_parser, "~> 1.4.39", [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", "5ce5f16b41208a50106afed3de6a2ed34f4acfd65715b82a0b84b49d995f95c1"},
68
"file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"},
79
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
810
"jose": {:hex, :jose, "1.11.10", "a903f5227417bd2a08c8a00a0cbcc458118be84480955e8d251297a425723f83", [:mix, :rebar3], [], "hexpm", "0d6cd36ff8ba174db29148fc112b5842186b68a90ce9fc2b3ec3afe76593e614"},
11+
"makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"},
12+
"makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [: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", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"},
13+
"makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"},
14+
"nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
915
}

0 commit comments

Comments
 (0)