Skip to content

Commit 2d2939e

Browse files
committed
initial working implementation with tests from RFC example
1 parent d9eb20f commit 2d2939e

18 files changed

+737
-0
lines changed

.formatter.exs

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Used by "mix format"
2+
[
3+
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
4+
]

.github/workflows/ci.yml

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
name: CI
2+
3+
on:
4+
workflow_dispatch:
5+
pull_request:
6+
push:
7+
branches:
8+
- main
9+
10+
jobs:
11+
tests:
12+
runs-on: ubuntu-latest
13+
14+
steps:
15+
- uses: actions/checkout@v4
16+
- uses: erlef/setup-beam@v1
17+
with:
18+
otp-version: "27.0.1"
19+
elixir-version: "1.17.2"
20+
21+
- uses: actions/cache@v4
22+
id: cache-elixir-build-deps
23+
with:
24+
path: |
25+
_build
26+
deps
27+
key: ${{ runner.os }}-mix-deps-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }}
28+
29+
- name: compile elixir deps
30+
if: steps.cache-elixir-build-deps.outputs.cache-hit != 'true'
31+
env:
32+
MIX_ENV: test
33+
run: mix do deps.get, deps.compile
34+
35+
- name: test
36+
run: mix test
37+
38+
- name: credo
39+
env:
40+
MIX_ENV: test
41+
run: mix credo --strict
42+
43+
- name: check format
44+
env:
45+
MIX_ENV: test
46+
run: mix format --check-formatted
47+

.gitignore

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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-*.tar
24+
25+
# Temporary files, for example, from tests.
26+
/tmp/

CHANGELOG.md

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
## 0.1.0
2+
3+
* initial release
4+
* basic aes128gcm functionality
5+
* supports browsers, desktop and mobile:
6+
* apple safari
7+
* google chrome
8+
* mozilla firefox

README.md

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# WebPush
2+
3+
This library implements RFC 8291 Message Encryption for Web Push.
4+
5+
It generates request details but does not make the HTTP POST request itself. The
6+
generated details should be enough to feed to your HTTP client of choice.
7+
8+
## Installation
9+
10+
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
11+
by adding `web_push` to your list of dependencies in `mix.exs`:
12+
13+
```elixir
14+
def deps do
15+
[
16+
{:web_push, "~> 0.1.0"}
17+
]
18+
end
19+
```
20+
21+
## Configuration
22+
23+
`WebPush` needs VAPID keys to work. Run `mix web_push.vapid` and adapt the
24+
generated config to your needs:
25+
26+
```
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:
35+
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()`:
49+
50+
```javascript
51+
pushManager.subscribe({
52+
userVisibleOnly: true,
53+
applicationServerKey: vapidPublicKey,
54+
}).then(...);
55+
```
56+
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.
59+
60+
```elixir
61+
# create the struct from the subscription JSON data
62+
subscription =
63+
WebPush.Subscription.from_json("""
64+
{"endpoint":"https://push.example.com/123","keys":{"p256dh":"user_agent_public_key","auth":"auth_secret"}}
65+
""")
66+
67+
# structured message, see example serviceWorker.js linked below
68+
message = %{title: "Notification Title", body: "lorem ipsum etc"}
69+
70+
# generate request details
71+
%WebPush.Request{} = request = WebPush.request(subscription, :json.encode(message))
72+
73+
request.endpoint
74+
# => "https://push.example.com/123"
75+
76+
request.body
77+
# => binary data
78+
79+
request.headers
80+
# => %{
81+
# "Authorization" => "vapid t=..., k=...",
82+
# "Content-Encoding" => "aes128gcm",
83+
# "Content-Length" => "42",
84+
# "Content-Type" => "application/octet-stream",
85+
# "TTL" => "43200",
86+
# "Urgency" => "normal"
87+
# }
88+
89+
# send web push notification via http client e.g. tesla
90+
Tesla.post(request.endpoint, request.body, headers: Map.to_list(request.headers))
91+
```
92+
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/
114+
115+
* https://developer.apple.com/documentation/usernotifications/sending-web-push-notifications-in-web-apps-and-browsers

config/config.exs

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import Config
2+
3+
import_config "#{config_env()}.exs"

config/dev.exs

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import Config
2+
3+
# use OTP :json for development
4+
config :jose, :json_module, WebPush.JOSEjson
5+
6+
config :web_push, :vapid,
7+
public_key: "",
8+
private_key: "",
9+
subject: "[email protected]"

config/test.exs

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import Config
2+
3+
# use OTP :json for testing
4+
config :jose, :json_module, WebPush.JOSEjson
5+
6+
config :web_push, :vapid,
7+
public_key: "",
8+
private_key: "",
9+
subject: "[email protected]"

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

0 commit comments

Comments
 (0)