Compare commits

...

114 commits
v0.4.0 ... main

Author SHA1 Message Date
3fb227f5d9 chore(deps): update dependency mimic to v1.10.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-09-02 22:26:28 +12:00
d74c8e3cb3 chore(deps): update dependency spark to v2.2.23
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-08-30 10:22:43 +12:00
fad27f142e chore: release version v0.6.1
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2024-08-27 03:54:31 +00:00
b96516405e
refactor(WebSocketProxy): Correctly traverse incoming messages to set the response.
All checks were successful
continuous-integration/drone/push Build is passing
2024-08-27 15:53:17 +12:00
a191077cc4
fix(Proxy): don't crash for responses with no body. 2024-08-27 15:53:17 +12:00
de29cb2b51 chore(deps): update dependency telemetry to v1.3.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-08-23 04:35:50 +12:00
d9fe59958e chore: release version v0.6.0
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2024-08-20 04:45:52 +00:00
07c41153f5 feat: Add request telemetry (#114)
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #114
Co-authored-by: James Harton <james@harton.nz>
Co-committed-by: James Harton <james@harton.nz>
2024-08-20 16:44:08 +12:00
dfe52b6e74 chore(deps): update dependency spark to v2.2.22
Some checks failed
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is failing
2024-08-20 14:38:38 +12:00
c5a337ca4a
docs: Update readme.
All checks were successful
continuous-integration/drone/push Build is passing
2024-08-18 09:58:18 +12:00
c440a88bdc chore: release version v0.5.0
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2024-08-17 06:54:11 +00:00
4ad256acdd
feat: Add support for proxying websockets.
All checks were successful
continuous-integration/drone/push Build is passing
2024-08-17 18:50:43 +12:00
de160bd70c chore(deps): update dependency spark to v2.2.21
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-08-15 06:19:21 +12:00
11b8444b96
chore: update cheatsheet.
All checks were successful
continuous-integration/drone/push Build is passing
2024-08-14 15:44:07 +12:00
60664ff5cc chore(deps): update dependency spark to v2.2.20
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-08-14 14:21:51 +12:00
d8e7e0c3a0 chore(deps): update dependency spark to v2.2.19
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is failing
2024-08-14 11:20:45 +12:00
e43c31997e chore(deps): update dependency spark to v2.2.18
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-08-14 08:18:53 +12:00
f739d4b7e1 chore(deps): update dependency spark to v2.2.17
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-08-14 05:21:13 +12:00
cd489acd63 chore(deps): update dependency spark to v2.2.16
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-08-14 01:17:31 +12:00
09433f8e26 chore(deps): update dependency spark to v2.2.14
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is passing
2024-08-13 14:17:38 +12:00
adc2453081 chore(deps): update dependency spark to v2.2.12
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is passing
2024-08-13 13:16:28 +12:00
cac6f82746 chore(deps): update dependency spark to v2.2.11
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-08-02 18:36:41 +12:00
8cf2027f28 chore(deps): update dependency bandit to v1.5.7
Some checks failed
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is failing
2024-08-02 18:28:38 +12:00
638badbba8 chore(deps): update dependency bandit to v1.5.5 (#102)
All checks were successful
continuous-integration/drone/push Build is passing
This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [bandit](https://hex.pm/packages/bandit) | patch | `1.5.3` -> `1.5.5` |

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled because a matching PR was automerged previously.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC43LjEiLCJ1cGRhdGVkSW5WZXIiOiIzOC44LjEiLCJ0YXJnZXRCcmFuY2giOiJtYWluIiwibGFiZWxzIjpbInJlbm92YXRlIl19-->

Reviewed-on: #102
Co-authored-by: Renovate Bot <bot@harton.nz>
Co-committed-by: Renovate Bot <bot@harton.nz>
2024-07-27 21:07:06 +12:00
a701a200b0
chore: remove unused dependencies.
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-07-27 15:31:09 +12:00
b11b37078e chore(deps): update dependency spark to v2.2.10
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2024-07-27 06:38:13 +12:00
7ffec1bed6 chore(deps): update dependency mimic to v1.9.0
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-07-18 20:32:31 +12:00
19f4a9b387 chore(deps): update dependency mix_audit to v2.1.4
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-07-17 02:34:26 +12:00
c016c5a8ae chore(deps): update dependency earmark to v1.4.47
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-07-13 06:42:06 +12:00
59ae3a59cf chore(deps): update dependency erlang to v27.0.1
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is passing
2024-07-11 01:42:20 +12:00
331827d6b4 chore(deps): update dependency ex_doc to v0.34.2
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is passing
2024-07-09 03:46:20 +12:00
c6b7c98930 chore(deps): update dependency elixir to v1.17.2
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-07-07 09:40:47 +12:00
08976af06c chore(deps): update dependency spark to v2.2.7
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-07-03 10:37:42 +12:00
69a32accd4 chore(deps): update dependency castore to v1.0.8
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-07-02 19:21:25 +12:00
0dd15ad75e chore(deps): update dependency mint to v1.6.2
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-07-02 04:12:11 +12:00
6bf61d35f1 chore(deps): update dependency spark to v2.2.6
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-29 10:41:44 +12:00
ec239d1e57 chore(deps): update dependency spark to v2.2.5
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-06-26 07:39:37 +12:00
bbc6f05d20 chore(deps): update dependency mimic to v1.8.2
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-23 22:40:48 +12:00
82e4b461eb chore: release version v0.4.1
Some checks reported errors
continuous-integration/drone/push Build encountered an error
continuous-integration/drone/tag Build encountered an error
2024-06-23 08:44:45 +00:00
384257a33f
fix: incorrect handling of Mint stream failure.
All checks were successful
continuous-integration/drone Build is passing
continuous-integration/drone/push Build is passing
2024-06-23 16:00:51 +12:00
d4d38b7b34 chore(deps): update dependency elixir to v1.17.1
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2024-06-23 01:40:13 +12:00
57e0f63ee8 chore(deps): update dependency mimic to v1.8.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-23 00:42:06 +12:00
6eb6fd6a1f chore(deps): update dependency mimic to v1.8.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-22 22:42:33 +12:00
90c60759ca
chore: ignore a dialyzer warning I just can't wrap my head around.
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-21 19:28:37 +12:00
886e032c05 chore(deps): update dependency spark to v2.2.4
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2024-06-21 06:22:28 +12:00
3545d01262 chore(deps): update dependency ex_doc to v0.34.1
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-21 02:42:13 +12:00
e1e796e234 chore(deps): update dependency bandit to v1.5.5
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-21 02:24:07 +12:00
d7d16fd925 chore(deps): update dependency spark to v2.2.1
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-15 13:33:20 +12:00
0c9137cfe9 chore(deps): update dependency credo to v1.7.7
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-15 12:55:44 +12:00
204dd61258 chore(deps): update dependency spark to v2.1.24
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-11 03:21:52 +12:00
51d100898c chore(deps): update dependency bandit to v1.5.3
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-08 06:20:41 +12:00
b6e2337dab chore(deps): update dependency spark to v2.1.23
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-06-08 02:23:58 +12:00
304fd0bcd4 chore(deps): update dependency ex_doc to v0.34.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-31 01:26:40 +12:00
1008da98a4 chore(deps): update dependency nimble_options to v1.1.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-25 23:17:11 +12:00
5d8be9ae42 chore(deps): update dependency ex_doc to v0.33.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-22 01:22:56 +12:00
2be599db1b chore(deps): update dependency elixir to v1.16.3
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-21 11:23:57 +12:00
e32b2a868d chore(deps): update dependency erlang to v27
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-20 21:45:26 +12:00
6009a19705 chore(deps): update dependency plug to v1.16.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-18 21:45:45 +12:00
055b0ca2d5 chore(deps): update dependency spark to v2.1.22
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-15 04:19:50 +12:00
b8b5623a2f chore(deps): update dependency spark to v2.1.21
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-11 11:35:59 +12:00
4766d48dcb chore(deps): update dependency git_ops to v2.6.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-11 10:42:14 +12:00
db3abee0fb chore(deps): update dependency bandit to v1.5.2
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-11 05:45:31 +12:00
99aaa5d57d chore(deps): update dependency bandit to v1.5.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-11 03:36:03 +12:00
4ce0d89652 chore(deps): update dependency ex_doc to v0.32.2
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-10 23:40:33 +12:00
d78dca56f6 chore(deps): update dependency credo to v1.7.6
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-10 00:00:52 +12:00
989d48c46d chore(deps): update dependency erlang to v26.2.5
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-03 03:37:37 +12:00
27858f1474 chore(deps): update dependency spark to v2.1.20
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-04-22 05:32:32 +12:00
7093ae3517 chore(deps): update dependency spark to v2.1.19
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-04-22 03:35:27 +12:00
6e63fa3961 chore(deps): update dependency bandit to v1.5.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-04-22 01:32:24 +12:00
fe61ed4e1a chore(deps): update dependency castore to v1.0.7
Some checks failed
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is failing
2024-04-20 17:32:43 +12:00
7e5ec8c11c chore(deps): update dependency ex_doc to v0.32.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-04-13 08:36:52 +12:00
ac3440d512 chore(deps): update dependency spark to v2.1.18
Some checks failed
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is failing
2024-04-13 06:33:15 +12:00
bbf213825b chore(deps): update dependency erlang to v26.2.4
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-04-13 00:37:35 +12:00
5a411f54ee chore(deps): update dependency spark to v2.1.17
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-04-12 05:31:37 +12:00
f3c49db9db chore(deps): update dependency spark to v2.1.15
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-04-11 04:59:18 +12:00
c6f84bb1c5 chore(deps): update dependency spark to v2.1.14
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-04-11 02:31:26 +12:00
5c70adaaff chore(deps): update dependency ex_doc to v0.32.0
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-04-10 21:34:21 +12:00
35dce07e77 chore(deps): update dependency spark to v2.1.13 (#50)
All checks were successful
continuous-integration/drone/push Build is passing
This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [spark](https://hex.pm/packages/spark) | patch | `2.1.11` -> `2.1.13` |

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIwLjAuMC1zZW1hbnRpYy1yZWxlYXNlIiwidXBkYXRlZEluVmVyIjoiMC4wLjAtc2VtYW50aWMtcmVsZWFzZSIsInRhcmdldEJyYW5jaCI6Im1haW4ifQ==-->

Reviewed-on: #50
Co-authored-by: Renovate Bot <bot@harton.nz>
Co-committed-by: Renovate Bot <bot@harton.nz>
2024-04-06 12:48:47 +13:00
4747c50673 chore(deps): update dependency bandit to v1.4.2
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-04-03 06:25:01 +13:00
b35d678050 chore(deps): update dependency spark to v2.1.11
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-03-30 10:30:14 +13:00
8791e1228d chore(deps): update dependency spark to v2.1.10
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-03-30 08:00:51 +13:00
5d491362d2 chore(deps): update dependency spark to v2.1.9
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-03-29 11:41:51 +13:00
2e9f41c00d chore(deps): update dependency spark to v2.1.8
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-03-28 12:16:44 +13:00
3ed6b64e80 chore(deps): update dependency bandit to v1.4.1 (#45)
All checks were successful
continuous-integration/drone/push Build is passing
This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [bandit](https://hex.pm/packages/bandit) | minor | `1.3.0` -> `1.4.1` |

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIwLjAuMC1zZW1hbnRpYy1yZWxlYXNlIiwidXBkYXRlZEluVmVyIjoiMC4wLjAtc2VtYW50aWMtcmVsZWFzZSIsInRhcmdldEJyYW5jaCI6Im1haW4ifQ==-->

Reviewed-on: #45
Co-authored-by: Renovate Bot <bot@harton.nz>
Co-committed-by: Renovate Bot <bot@harton.nz>
2024-03-28 11:37:01 +13:00
2b79e3cb10 chore(deps): update dependency spark to v2.1.4
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-03-26 11:15:52 +13:00
25fb114808 chore(deps): update dependency spark to v2.1.3
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-03-21 02:25:50 +13:00
c672e2c4f4 chore(deps): update dependency mix_audit to v2.1.3
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-03-19 16:10:11 +13:00
7e2f0f2182 chore(deps): update dependency spark to v2.1.2
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-03-19 07:24:15 +13:00
41ac07b8f1 chore(deps): update dependency spark to v2.1.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-03-16 05:33:49 +13:00
0d186e834a
chore: fix docs release.
All checks were successful
continuous-integration/drone/push Build is passing
2024-03-14 19:30:17 +13:00
306a0d7935 chore(deps): update dependency ip to v2.0.3
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-03-14 17:19:59 +13:00
2736fdbbd1 chore(deps): update dependency ex_doc to v0.31.2
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-03-14 16:07:40 +13:00
e48f26dd85 chore(deps): update dependency bandit to v1.3.0
All checks were successful
continuous-integration/drone/push Build is passing
2024-03-14 16:01:40 +13:00
b59af873e8 chore(deps): update dependency mix_audit to v2.1.2
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-03-14 15:25:04 +13:00
5cfb9a4a7d chore(deps): update dependency ip to v2.0.2
All checks were successful
continuous-integration/drone/push Build is passing
2024-03-14 15:05:11 +13:00
52632fe88e chore(deps): update dependency credo to v1.7.5
All checks were successful
continuous-integration/drone/push Build is passing
2024-03-14 15:04:38 +13:00
d95e23d727 chore(deps): update dependency dialyxir to v1.4.3
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-03-14 00:54:22 +00:00
25c9003193 chore(deps): update dependency plug to v1.15.3
All checks were successful
continuous-integration/drone/push Build is passing
2024-03-14 13:33:30 +13:00
ccdfce9850 chore(deps): update dependency castore to v1.0.6
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-03-13 21:20:24 +00:00
d57e6a0b7f chore(deps): update dependency elixir to v1.16.2
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-03-11 00:37:49 +13:00
5ec7cf267a
chore: fix typo in readme
All checks were successful
continuous-integration/drone/push Build is passing
2024-03-08 14:48:42 +13:00
48bc11159a chore(deps): update dependency erlang to v26.2.3
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-03-07 22:33:49 +13:00
3383898512
docs: Update Readme.
All checks were successful
continuous-integration/drone/push Build is passing
2024-03-06 20:26:35 +13:00
ca1a344057
docs(README): Add GitHub mirror link.
All checks were successful
continuous-integration/drone/push Build is passing
2024-03-06 18:05:41 +13:00
3312d02642
chore: Update hex package links.
All checks were successful
continuous-integration/drone/push Build is passing
2024-03-06 18:03:25 +13:00
e4224a0daf
chore(deps): update dependency spark to v2
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-03-03 20:13:16 +13:00
49c30c4dde chore(deps): update dependency ex_check to ~> 0.16
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-03-01 23:30:58 +13:00
3ebb9843da chore(deps): update dependency faker to ~> 0.18
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-02-29 19:45:15 +13:00
f192b9fd5f chore(deps): update dependency erlang to v26.2.2
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-02-09 10:48:53 +13:00
76f2a299f8 chore: Update forgejo hostname.
All checks were successful
continuous-integration/drone/push Build is passing
2024-02-05 14:58:36 +13:00
37a064c507 chore: make more tests synchronous. 2024-02-05 14:54:27 +13:00
8358d2de28 chore(deps): update dependency elixir to v1.16.1
Some checks failed
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is failing
2024-01-31 23:22:35 +13:00
c142bdbc1a chore(deps): update dependency elixir to v1.16.0 (#13)
All checks were successful
continuous-integration/drone/push Build is passing
This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [elixir](https://elixir-lang.org/) ([source](https://github.com/elixir-lang/elixir)) | minor | `1.15.7` -> `1.16.0` |

---

### Release Notes

<details>
<summary>elixir-lang/elixir (elixir)</summary>

### [`v1.16.0`](https://github.com/elixir-lang/elixir/releases/tag/v1.16.0)

[Compare Source](https://github.com/elixir-lang/elixir/compare/v1.15.7...v1.16.0)

Official announcement: https://elixir-lang.org/blog/2023/12/22/elixir-v1-16-0-released/

##### 1. Enhancements

##### EEx

-   \[EEx] Include relative file information in diagnostics

##### Elixir

-   \[Code] Add `:emit_warnings` for `Code.string_to_quoted/2`
-   \[Code] Automatically include columns in parsing options
-   \[Code] Introduce `MismatchedDelimiterError` for handling mismatched delimiter exceptions
-   \[Code.Fragment] Handle anonymous calls in fragments
-   \[Code.Formatter] Trim trailing whitespace on heredocs with `\r\n`
-   \[File] Add `:offset` option to `File.stream!/2`
-   \[Kernel] Auto infer size of matched variable in bitstrings
-   \[Kernel] Preserve column information when translating typespecs
-   \[Kernel] Suggest module names based on suffix and casing errors when the module does not exist in `UndefinedFunctionError`
-   \[Kernel.ParallelCompiler] Introduce `Kernel.ParallelCompiler.pmap/2` to compile multiple additional entries in parallel
-   \[Kernel.SpecialForms] Warn if `True`/`False`/`Nil` are used as aliases and there is no such alias
-   \[Macro] Add `Macro.compile_apply/4`
-   \[Module] Add support for `@nifs` annotation from Erlang/OTP 25
-   \[Module] Add support for missing `@dialyzer` configuration
-   \[String] Update to Unicode 15.1.0
-   \[String] Add `String.replace_invalid/2`
-   \[Task] Add `:limit` option to `Task.yield_many/2`

##### Logger

-   \[Logger] Add `Logger.levels/0`

##### Mix

-   \[mix] Add `MIX_PROFILE` to profile a list of comma separated tasks
-   \[mix archive.install] Support `--sparse` option
-   \[mix compile.app] Warn if both `:applications` and `:extra_applications` are used
-   \[mix compile.elixir] Pass original exception down to diagnostic `:details` when possible
-   \[mix compile.elixir] Optimize scenario where there are thousands of files in `lib/` and one of them is changed
-   \[mix deps.clean] Emit a warning instead of crashing when a dependency cannot be removed
-   \[mix escript.install] Support `--sparse` option
-   \[mix release] Include `include/` directory in releases
-   \[mix test] Allow testing multiple file:line at once, such as `mix test test/foo_test.exs:13 test/bar_test.exs:27`

##### 2. Bug fixes

##### Elixir

-   \[Code] Keep quotes for atom keys in formatter
-   \[Code.Fragment] Fix crash in `Code.Fragment.surround_context/2` when matching on `->`
-   \[IO] Raise when using `IO.binwrite/2` on terminated device (mirroring `IO.write/2`)
-   \[Kernel] Do not expand aliases recursively (the alias stored in Macro.Env is already expanded)
-   \[Kernel] Ensure `dbg` module is a compile-time dependency
-   \[Kernel] Warn when a private function or macro uses `unquote/1` and the function/macro itself is unused
-   \[Kernel] Re-enabled compiler optimizations for top level functions in scripts (disabled in v1.14.0 but shouldn't impact most programs)
-   \[Kernel] Do not define an alias for nested modules starting with `Elixir.` in their definition
-   \[Kernel.ParallelCompiler] Consider a module has been defined in `@after_compile` callbacks to avoid deadlocks
-   \[Macro] Address exception on `Macro.to_string/1` for certain ASTs
-   \[Path] Lazily evaluate `File.cwd!/0` in `Path.expand/1` and `Path.absname/1`
-   \[Path] Ensure `Path.relative_to/2` returns a relative path when the given argument does not share a common prefix with `cwd`

##### ExUnit

-   \[ExUnit] Raise on incorrectly dedented doctests

##### IEx

-   \[IEx.Pry] Fix prying functions with only literals in their body

##### Mix

-   \[mix archive.install] Restore code paths after `mix archive.install`
-   \[mix compile] Ensure files with duplicate modules are recompiled whenever any of the files change
-   \[mix compile] Update Mix compiler diagnostics documentation and typespecs to match the Elixir compiler behaviour where both lines and columns start from one (before it inaccurately said that columns started from zero)
-   \[mix escript.install] Restore code paths after `mix escript.install`

##### 3. Soft deprecations (no warnings emitted)

##### Elixir

-   \[File] Deprecate `File.stream!(file, options, line_or_bytes)` in favor of keeping the options as last argument, as in `File.stream!(file, line_or_bytes, options)`
-   \[Kernel.ParallelCompiler] Deprecate `Kernel.ParallelCompiler.async/1` in favor of `Kernel.ParallelCompiler.pmap/2`
-   \[Path] Deprecate `Path.safe_relative_to/2` in favor of `Path.safe_relative/2`

##### Mix

-   \[mix compile] Returning a four-element tuple as a position in `Mix.Task.Compiler.Diagnostic`

##### 4. Hard deprecations

##### Elixir

-   \[Date] Deprecate inferring a range with negative step, call `Date.range/3` with a negative step instead
-   \[Enum] Deprecate passing a range with negative step on `Enum.slice/2`, give `first..last//1` instead
-   \[Kernel] `~R/.../` is deprecated in favor of `~r/.../`. This is because `~R/.../` still allowed escape codes, which did not fit the definition of uppercase sigils
-   \[String] Deprecate passing a range with negative step on `String.slice/2`, give `first..last//1` instead

##### ExUnit

-   \[ExUnit.Formatter] Deprecate `format_time/2`, use `format_times/1` instead

##### Mix

-   \[mix compile.leex] Require `:leex` to be added as a compiler to run the `leex` compiler
-   \[mix compile.yecc] Require `:yecc` to be added as a compiler to run the `yecc` compiler

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4xMDcuMCIsInVwZGF0ZWRJblZlciI6IjM3LjEyNy4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiJ9-->

Reviewed-on: https://code.harton.nz/james/wayfarer/pulls/13
Co-authored-by: Renovate Bot <bot@harton.nz>
Co-committed-by: Renovate Bot <bot@harton.nz>
2024-01-15 10:37:57 +13:00
545384b149 chore(deps): update dependency erlang to v26.2.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-12-19 04:10:27 +13:00
33 changed files with 2964 additions and 2411 deletions

5
.dialyzer_ignore.exs Normal file
View file

@ -0,0 +1,5 @@
[
{"lib/wayfarer/server/proxy.ex", :unknown_function},
{"lib/wayfarer/target/check.ex", :unknown_function},
{"test/support/http_request.ex", :unknown_function}
]

View file

@ -49,7 +49,7 @@ steps:
- .rebar3
- name: install dependencies
image: code.harton.nz/james/asdf_container:latest
image: harton.dev/james/asdf_container:latest
pull: "always"
environment:
MIX_ENV: test
@ -122,7 +122,7 @@ steps:
- .rebar3
- name: mix compile
image: code.harton.nz/james/asdf_container:latest
image: harton.dev/james/asdf_container:latest
environment:
MIX_ENV: test
HEX_HOME: /drone/src/.hex
@ -135,7 +135,7 @@ steps:
- asdf mix compile --warnings-as-errors
- name: mix test
image: code.harton.nz/james/asdf_container:latest
image: harton.dev/james/asdf_container:latest
environment:
MIX_ENV: test
HEX_HOME: /drone/src/.hex
@ -148,7 +148,7 @@ steps:
- asdf mix test
- name: mix credo
image: code.harton.nz/james/asdf_container:latest
image: harton.dev/james/asdf_container:latest
environment:
MIX_ENV: test
HEX_HOME: /drone/src/.hex
@ -161,7 +161,7 @@ steps:
- asdf mix credo --strict
- name: mix hex.audit
image: code.harton.nz/james/asdf_container:latest
image: harton.dev/james/asdf_container:latest
environment:
MIX_ENV: test
HEX_HOME: /drone/src/.hex
@ -174,7 +174,7 @@ steps:
- asdf mix hex.audit
- name: mix format
image: code.harton.nz/james/asdf_container:latest
image: harton.dev/james/asdf_container:latest
environment:
MIX_ENV: test
HEX_HOME: /drone/src/.hex
@ -187,7 +187,7 @@ steps:
- asdf mix format --check-formatted
- name: mix deps.unlock
image: code.harton.nz/james/asdf_container:latest
image: harton.dev/james/asdf_container:latest
environment:
MIX_ENV: test
HEX_HOME: /drone/src/.hex
@ -200,7 +200,7 @@ steps:
- asdf mix deps.unlock --check-unused
- name: mix doctor
image: code.harton.nz/james/asdf_container:latest
image: harton.dev/james/asdf_container:latest
environment:
MIX_ENV: test
HEX_HOME: /drone/src/.hex
@ -213,7 +213,7 @@ steps:
- asdf mix doctor --full
- name: mix dialyzer
image: code.harton.nz/james/asdf_container:latest
image: harton.dev/james/asdf_container:latest
environment:
MIX_ENV: test
HEX_HOME: /drone/src/.hex
@ -226,7 +226,7 @@ steps:
- asdf mix dialyzer
- name: mix git_ops.check_message
image: code.harton.nz/james/asdf_container:latest
image: harton.dev/james/asdf_container:latest
environment:
MIX_ENV: test
HEX_HOME: /drone/src/.hex
@ -240,7 +240,7 @@ steps:
- asdf mix git_ops.check_message .last_commit_message
- name: mix git_ops.release
image: code.harton.nz/james/asdf_container:latest
image: harton.dev/james/asdf_container:latest
when:
branch:
- main
@ -280,7 +280,7 @@ steps:
- fi
- name: build artifacts
image: code.harton.nz/james/asdf_container:latest
image: harton.dev/james/asdf_container:latest
when:
event:
- tag
@ -327,7 +327,7 @@ steps:
settings:
api_key:
from_secret: DRONE_TOKEN
base_url: https://code.harton.nz
base_url: https://harton.dev
files: artifacts/*.tar.gz
checksum: sha256
@ -351,12 +351,11 @@ steps:
commands:
- mc alias set store $${S3_ENDPOINT} $${ACCESS_KEY} $${SECRET_KEY}
- mc mb -p store/docs.harton.nz
- mc anonymous set download store/docs.harton.nz
- mc mirror --overwrite doc/ store/docs.harton.nz/$${DRONE_REPO}/$${DRONE_TAG}
- mc mirror --overwrite doc/ store/docs.harton.nz/$${DRONE_REPO}
- name: hex release
image: code.harton.nz/james/asdf_container:latest
image: harton.dev/james/asdf_container:latest
when:
event:
- tag

View file

@ -38,7 +38,12 @@ spark_locals_without_parens = [
targets: 1,
thousand_island_options: 1,
threshold: 1,
websocket_options: 1
transport: 1,
websocket_options: 1,
ws: 2,
ws: 3,
wss: 2,
wss: 3
]
[

View file

@ -1,3 +1,3 @@
erlang 26.1.2
elixir 1.15.7
erlang 27.0.1
elixir 1.17.2
hey 0.1.4

3
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,3 @@
{
"cSpell.words": ["ntoa"]
}

View file

@ -5,7 +5,43 @@ See [Conventional Commits](Https://conventionalcommits.org) for commit guideline
<!-- changelog -->
## [v0.4.0](https://code.harton.nz/james/wayfarer/compare/v0.3.0...v0.4.0) (2023-11-19)
## [v0.6.1](https://harton.dev/james/wayfarer/compare/v0.6.0...v0.6.1) (2024-08-27)
### Bug Fixes:
* Proxy: don't crash for responses with no body.
## [v0.6.0](https://harton.dev/james/wayfarer/compare/v0.5.0...v0.6.0) (2024-08-20)
### Features:
* Add request telemetry (#114)
## [v0.5.0](https://harton.dev/james/wayfarer/compare/v0.4.0...v0.5.0) (2024-08-17)
### Features:
* Add support for proxying websockets.
## [v0.4.1](https://harton.dev/james/wayfarer/compare/v0.4.0...v0.4.1) (2024-06-23)
### Bug Fixes:
* incorrect handling of Mint stream failure.
## [v0.4.0](https://harton.dev/james/wayfarer/compare/v0.3.0...v0.4.0) (2023-11-19)
@ -14,7 +50,7 @@ See [Conventional Commits](Https://conventionalcommits.org) for commit guideline
* add proxying. (#7)
## [v0.3.0](https://code.harton.nz/james/wayfarer/compare/v0.2.0...v0.3.0) (2023-10-14)
## [v0.3.0](https://harton.dev/james/wayfarer/compare/v0.2.0...v0.3.0) (2023-10-14)
@ -27,7 +63,7 @@ See [Conventional Commits](Https://conventionalcommits.org) for commit guideline
* Listener: Register listeners with scheme, address and port.
## [v0.2.0](https://code.harton.nz/james/wayfarer/compare/v0.1.0...v0.2.0) (2023-10-14)
## [v0.2.0](https://harton.dev/james/wayfarer/compare/v0.1.0...v0.2.0) (2023-10-14)
@ -36,7 +72,7 @@ See [Conventional Commits](Https://conventionalcommits.org) for commit guideline
* Add ability to start and stop HTTP listeners. (#1)
## [v0.1.0](https://code.harton.nz/james/wayfarer/compare/v0.1.0...v0.1.0) (2023-10-13)
## [v0.1.0](https://harton.dev/james/wayfarer/compare/v0.1.0...v0.1.0) (2023-10-13)

View file

@ -1,6 +1,6 @@
# Wayfarer
[![Build Status](https://drone.harton.nz/api/badges/james/wayfarer/status.svg?ref=refs/heads/main)](https://drone.harton.nz/james/wayfarer)
[![Build Status](https://drone.harton.dev/api/badges/james/wayfarer/status.svg?ref=refs/heads/main)](https://drone.harton.dev/james/wayfarer)
[![Hippocratic License HL3-FULL](https://img.shields.io/static/v1?label=Hippocratic%20License&message=HL3-FULL&labelColor=5e2751&color=bc8c3d)](https://firstdonoharm.dev/version/3/0/full.html)
Wayfarer is a runtime-configurable HTTP reverse proxy using
@ -9,22 +9,31 @@ Wayfarer is a runtime-configurable HTTP reverse proxy using
## Status
Wayfarer is yet to handle it's first HTTP request. Please hold.
Wayfarer is able to proxy HTTP/1, HTTP/2 and WebSocket requests. There are
probably still edge cases and bugs, but it can be used.
## Installation
Wayfarer is not yet available on Hex, so you will need to add it as a Git
dependency in your app:
Wayfarer is [available in Hex](https://hex.pm/packages/wayfarer), the package
can be installed by adding `wayfarer` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:wayfarer, git: "https://code.harton.nz/james/wayfarer.git", tag: "v0.1.0"}
{:wayfarer, "~> 0.4.1"}
]
end
```
Documentation for `main` is always available on [my docs site](https://docs.harton.nz/james/wayfarer/Wayfarer.html).
Documentation for the latest release can be found on
[HexDocs](https://hexdocs.pm/wayfarer) and for the `main` branch on
[docs.harton.nz](https://docs.harton.nz/james/wayfarer).
## Github Mirror
This repository is mirrored [on Github](https://github.com/jimsynz/wayfarer)
from it's primary location [on my Forgejo instance](https://harton.dev/james/wayfarer).
Feel free to raise issues and open PRs on Github.
## License

View file

@ -3,7 +3,7 @@ import Config
config :git_ops,
mix_project: Mix.Project.get!(),
changelog_file: "CHANGELOG.md",
repository_url: "https://code.harton.nz/james/wayfarer",
repository_url: "https://harton.dev/james/wayfarer",
manage_mix_version?: true,
version_tag_prefix: "v",
manage_readme_version: "README.md"

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -85,7 +85,7 @@ defmodule Wayfarer.Dsl.HealthCheck do
doc: "Path"
],
success_codes: [
type: {:wrap_list, {:struct, Range}},
type: {:wrap_list, {:or, [{:struct, Range}, {:in, 100..500}]}},
required: false,
default: @defaults[:success_codes],
doc: "HTTP status codes which are considered successful."
@ -125,22 +125,34 @@ defmodule Wayfarer.Dsl.HealthCheck do
@doc false
@spec transform(t) :: {:ok, t} | {:error, any}
def transform(check) do
with :ok <- verify_success_codes(check.success_codes) do
maybe_set_name(check)
with {:ok, success_codes} <- transform_success_codes(check.success_codes) do
maybe_set_name(%{check | success_codes: success_codes})
end
end
defguardp is_valid_status_code?(code) when is_integer(code) and code >= 100 and code <= 599
defguardp is_valid_range?(range)
when is_struct(range, Range) and is_integer(range.first) and range.first >= 100 and
is_integer(range.last) and range.last <= 500
when is_struct(range, Range) and is_valid_status_code?(range.first) and
is_valid_status_code?(range.last)
defp verify_success_codes(range) when is_valid_range?(range), do: :ok
defp verify_success_codes([]), do: :ok
defp transform_success_codes(range) when is_valid_range?(range), do: {:ok, [range]}
defp transform_success_codes([]), do: {:ok, []}
defp verify_success_codes([head | tail]) when is_valid_range?(head),
do: verify_success_codes(tail)
defp transform_success_codes([head | tail]) when is_valid_range?(head) do
with {:ok, tail} <- transform_success_codes(tail) do
{:ok, [head | tail]}
end
end
defp verify_success_codes([range | _]),
defp transform_success_codes([head | tail])
when is_integer(head) and is_valid_status_code?(head) do
with {:ok, tail} <- transform_success_codes(tail) do
{:ok, [head..head | tail]}
end
end
defp transform_success_codes([range | _]),
do: {:error, "Value `#{inspect(range)}` is not valid. Must be a range between 100..599"}
defp maybe_set_name(check) when is_binary(check.name), do: {:ok, check}

View file

@ -3,7 +3,7 @@ defmodule Wayfarer.Dsl.Target do
A struct for storing a target generated by the DSL.
"""
alias Spark.{Dsl.Entity, OptionsHelpers}
alias Spark.{Dsl.Entity, Options}
alias Wayfarer.Dsl.{HealthCheck, HealthChecks}
alias Wayfarer.Utils
@ -13,6 +13,7 @@ defmodule Wayfarer.Dsl.Target do
name: nil,
port: nil,
scheme: :http,
transport: :auto,
uri: nil
@type t :: %__MODULE__{
@ -21,7 +22,8 @@ defmodule Wayfarer.Dsl.Target do
module: nil | module,
name: nil | String.t(),
port: :inet.port_number(),
scheme: :http | :https | :plug,
scheme: :http | :https | :plug | :ws | :wss,
transport: :http1 | :http2 | :auto,
uri: URI.t()
}
@ -40,6 +42,12 @@ defmodule Wayfarer.Dsl.Target do
type: :pos_integer,
required: true,
doc: "The TCP port on which to listen for incoming connections."
],
transport: [
type: {:in, [:http1, :http2, :auto]},
required: false,
default: :auto,
doc: "Which HTTP protocol to use."
]
]
@ -81,6 +89,28 @@ defmodule Wayfarer.Dsl.Target do
],
auto_set_fields: [scheme: :plug],
args: [:module]
},
%Entity{
name: :ws,
target: __MODULE__,
schema: @shared_schema,
auto_set_fields: [scheme: :ws],
args: [:address, :port],
imports: [IP.Sigil],
transform: {__MODULE__, :transform, []},
entities: [health_checks: HealthChecks.entities()],
singleton_entity_keys: [:health_checks]
},
%Entity{
name: :wss,
target: __MODULE__,
schema: @shared_schema,
auto_set_fields: [scheme: :wss],
args: [:address, :port],
imports: [IP.Sigil],
transform: {__MODULE__, :transform, []},
entities: [health_checks: HealthChecks.entities()],
singleton_entity_keys: [:health_checks]
}
]
end
@ -98,13 +128,13 @@ defmodule Wayfarer.Dsl.Target do
@doc false
def schema do
@shared_schema
|> OptionsHelpers.make_optional!(:address)
|> OptionsHelpers.make_optional!(:port)
|> Options.Helpers.make_optional!(:address)
|> Options.Helpers.make_optional!(:port)
|> Keyword.merge(
scheme: [
type: {:in, [:http, :https, :plug]},
type: {:in, [:http, :https, :plug, :ws, :wss]},
required: true,
doc: "The protocol used to talk to the target."
doc: "The connection type for the target."
],
plug: [
type: {:behaviour, Plug},

View file

@ -98,7 +98,7 @@ defmodule Wayfarer.Dsl.Transformer do
{target.scheme, target.module}
target ->
{target.scheme, IP.Address.to_tuple(target.address), target.port}
{target.scheme, IP.Address.to_tuple(target.address), target.port, target.transport}
end)
end
@ -113,7 +113,7 @@ defmodule Wayfarer.Dsl.Transformer do
|> Map.drop([:address, :health_checks, :port, :uri])
|> Enum.to_list()
target when target.scheme in [:http, :https] ->
target when target.scheme in [:http, :https, :ws, :wss] ->
health_checks =
target.health_checks.health_checks
|> Enum.map(&HealthCheck.to_options/1)

View file

@ -3,7 +3,7 @@ defmodule Wayfarer.Listener do
use GenServer, restart: :transient
require Logger
alias Spark.OptionsHelpers
alias Spark.Options
import Wayfarer.Utils
@options_schema [
@ -90,7 +90,7 @@ defmodule Wayfarer.Listener do
## Options
#{OptionsHelpers.docs(@options_schema)}
#{Options.docs(@options_schema)}
"""
@doc false
@ -126,14 +126,14 @@ defmodule Wayfarer.Listener do
{:ok, :https} ->
schema =
@options_schema
|> OptionsHelpers.make_required!(:keyfile)
|> OptionsHelpers.make_required!(:certfile)
|> OptionsHelpers.make_required!(:cipher_suite)
|> Options.Helpers.make_required!(:keyfile)
|> Options.Helpers.make_required!(:certfile)
|> Options.Helpers.make_required!(:cipher_suite)
OptionsHelpers.validate(options, schema)
Options.validate(options, schema)
_ ->
OptionsHelpers.validate(options, @options_schema)
Options.validate(options, @options_schema)
end
end

View file

@ -51,7 +51,17 @@ defmodule Wayfarer.Router do
local `Plug`.
"""
@type target ::
{scheme, :inet.ip_address(), :socket.port_number()}
{:http | :https | :ws | :wss, :inet.ip_address(), :socket.port_number(),
:http1 | :http2 | :auto}
| {:plug, module}
| {:plug, {module, any}}
@typedoc """
Like `t:target/0` except that it can contain user input for the address portion.
"""
@type target_input ::
{:http | :https | :ws | :wss, Wayfarer.Utils.address_input(), :socket.port_number(),
:http1 | :http2 | :auto}
| {:plug, module}
| {:plug, {module, any}}
@ -85,7 +95,8 @@ defmodule Wayfarer.Router do
This should only ever be called by `Wayfarer.Server` directly.
"""
@spec add_route(:ets.tid(), listener, target, [host_name], algorithm) :: :ok | {:error, any}
@spec add_route(:ets.tid(), listener, target_input, [host_name], algorithm) ::
:ok | {:error, any}
def add_route(table, listener, target, host_names, algorithm) do
with {:ok, entries} <- route_to_entries(table, listener, target, host_names, algorithm) do
:ets.insert(table, entries)
@ -96,9 +107,9 @@ defmodule Wayfarer.Router do
end
@doc """
Add a number of router into the routing table.
Add a number of routes into the routing table.
"""
@spec import_routes(:ets.tid(), [{listener, target, [host_name], algorithm}]) :: :ok
@spec import_routes(:ets.tid(), [{listener, target_input, [host_name], algorithm}]) :: :ok
def import_routes(table, routes) do
with {:ok, entries} <- routes_to_entries(table, routes) do
:ets.insert(table, entries)
@ -142,15 +153,15 @@ defmodule Wayfarer.Router do
Change a target's health state.
"""
@spec update_target_health_status(:ets.tid(), target, health) :: :ok
def update_target_health_status(table, {scheme, address, port}, status) do
def update_target_health_status(table, {scheme, address, port, transport}, status) do
# Match spec generated using:
# :ets.fun2ms(fn {listener, host_pattern, {:http, {192, 168, 4, 26}, 80}, algorithm, _} ->
# {listener, host_pattern, {:http, {192, 168, 4, 26}, 80}, algorithm, :healthy}
# :ets.fun2ms(fn {listener, host_pattern, {:http, {192, 168, 4, 26}, 80, transport}, algorithm, _} ->
# {listener, host_pattern, {:http, {192, 168, 4, 26}, 80, transport}, algorithm, :healthy}
# end)
match_spec = [
{{:"$1", :"$2", {scheme, address, port}, :"$3", :_}, [],
[{{:"$1", :"$2", {{scheme, {address}, port}}, :"$3", status}}]}
{{:"$1", :"$2", {scheme, address, port, transport}, :"$3", :_}, [],
[{{:"$1", :"$2", {{scheme, {address}, port, transport}}, :"$3", status}}]}
]
:ets.select_replace(table, match_spec)
@ -223,12 +234,12 @@ defmodule Wayfarer.Router do
message: "Value `#{inspect(algorithm)}` is not a valid load balancing algorithm."
)}
defp current_health_state(table, {scheme, address, port}) do
defp current_health_state(table, {scheme, address, port, transport}) do
# Generated using
# :ets.fun2ms(fn {_, _, :target, :_, health} -> health end)
match_spec = [
{{:_, :_, {scheme, address, port}, :_, :"$1"}, [], [:"$1"]}
{{:_, :_, {scheme, address, port, transport}, :_, :"$1"}, [], [:"$1"]}
]
case :ets.select(table, match_spec, 1) do
@ -277,6 +288,14 @@ defmodule Wayfarer.Router do
defp sanitise_listener(listener),
do: {:error, ArgumentError.exception(message: "Not a valid listener: `#{inspect(listener)}")}
defp sanitise_transport(transport) when transport in [:http1, :http2, :auto],
do: {:ok, transport}
defp sanitise_transport(transport),
do:
{:error,
ArgumentError.exception(message: "Not a valid target transport: `#{inspect(transport)}`")}
defp sanitise_target({:plug, module}), do: sanitise_target({:plug, module, []})
defp sanitise_target({:plug, module, _}) do
@ -290,7 +309,14 @@ defmodule Wayfarer.Router do
end
end
defp sanitise_target({scheme, address, port}), do: sanitise_listener({scheme, address, port})
defp sanitise_target({scheme, address, port, transport}) do
with {:ok, scheme} <- sanitise_scheme(scheme),
{:ok, address} <- sanitise_ip_address(address),
{:ok, port} <- sanitise_port(port),
{:ok, transport} <- sanitise_transport(transport) do
{:ok, {scheme, address, port, transport}}
end
end
defp sanitise_target(target),
do: {:error, ArgumentError.exception(message: "Not a valid target: `#{inspect(target)}")}

View file

@ -1,5 +1,5 @@
defmodule Wayfarer.Server do
alias Spark.OptionsHelpers
alias Spark.Options
alias Wayfarer.{Dsl, Listener, Router, Server, Target}
use GenServer
require Logger
@ -7,8 +7,9 @@ defmodule Wayfarer.Server do
@callback child_spec(keyword()) :: Supervisor.child_spec()
@callback start_link(keyword()) :: GenServer.on_start()
@scheme_type {:in, [:http, :https]}
@scheme_type {:in, [:http, :https, :ws, :wss]}
@port_type {:in, 0..0xFFFF}
@transport_type {:in, [:http1, :http2, :auto]}
@ip_type {:or,
[
{:tuple, [{:in, 0..0xFF}, {:in, 0..0xFF}, {:in, 0..0xFF}, {:in, 0..0xFF}]},
@ -50,7 +51,7 @@ defmodule Wayfarer.Server do
{:tuple,
[
{:tuple, [@scheme_type, @ip_type, @port_type]},
{:tuple, [@scheme_type, @ip_type, @port_type]},
{:tuple, [@scheme_type, @ip_type, @port_type, @transport_type]},
{:list, :string},
{:in, [:round_robin, :sticky, :random, :least_connections]}
]}},
@ -78,7 +79,7 @@ defmodule Wayfarer.Server do
## Options
#{OptionsHelpers.docs(@options_schema)}
#{Options.docs(@options_schema)}
"""
@type options :: keyword
@ -117,13 +118,14 @@ defmodule Wayfarer.Server do
@doc false
@spec target_status_change(
{module, :http | :https, IP.Address.t(), :socket.port_number()},
{module, :http | :https, IP.Address.t(), :socket.port_number(),
:http1 | :http2 | :auto},
Router.health()
) :: :ok
def target_status_change({module, scheme, address, port}, status) do
def target_status_change({module, scheme, address, port, transport}, status) do
GenServer.cast(
{:via, Registry, {Wayfarer.Server.Registry, module}},
{:target_status_change, scheme, address, port, status}
{:target_status_change, scheme, address, port, transport, status}
)
end
@ -145,7 +147,7 @@ defmodule Wayfarer.Server do
@impl true
@spec init(options) :: {:ok, map} | {:stop, any}
def init(options) do
with {:ok, options} <- OptionsHelpers.validate(options, @options_schema),
with {:ok, options} <- Options.validate(options, @options_schema),
{:ok, module} <- assert_is_server(options[:module]),
listeners <- Keyword.get(options, :listeners, []),
targets <- Keyword.get(options, :targets, []),
@ -165,10 +167,10 @@ defmodule Wayfarer.Server do
@doc false
@impl true
@spec handle_cast(any, map) :: {:noreply, map}
def handle_cast({:target_status_change, scheme, address, port, status}, state) do
def handle_cast({:target_status_change, scheme, address, port, transport, status}, state) do
Router.update_target_health_status(
state.routing_table,
{scheme, IP.Address.to_tuple(address), port},
{scheme, IP.Address.to_tuple(address), port, transport},
status
)

View file

@ -3,7 +3,7 @@ defmodule Wayfarer.Server.Plug do
Plug pipeline to handle inbound HTTP connections.
"""
alias Wayfarer.{Router, Server.Proxy, Target.Selector}
alias Wayfarer.{Router, Server.Proxy, Target.Selector, Telemetry}
require Logger
import Plug.Conn
@ -17,22 +17,57 @@ defmodule Wayfarer.Server.Plug do
@doc false
@impl true
@spec call(Plug.Conn.t(), map) :: Plug.Conn.t()
def call(conn, config) when is_atom(config.module) do
conn = put_private(conn, :wayfarer, %{listener: config})
def call(conn, config) do
transport = get_transport(conn)
conn
|> put_private(:wayfarer, %{listener: config, transport: transport})
|> Telemetry.request_start()
|> do_call(config)
end
defp do_call(conn, config) when is_atom(config.module) do
listener = {config.scheme, config.address, config.port}
with {:ok, targets} <- Router.find_healthy_targets(config.module, listener, conn.host),
{:ok, targets, algorithm} <- split_targets_and_algorithms(targets),
{:ok, target} <- Selector.choose(conn, targets, algorithm) do
do_proxy(conn, target)
conn
|> Telemetry.request_routed(target, algorithm)
|> do_proxy(target)
else
:error -> bad_gateway(conn)
{:error, reason} -> internal_error(conn, reason)
:error ->
conn
|> Telemetry.request_exception(:error, :target_not_found)
|> bad_gateway()
{:error, reason} ->
conn
|> Telemetry.request_exception(:error, reason)
|> internal_error(reason)
end
rescue
exception ->
conn
|> Telemetry.request_exception(:exception, exception, __STACKTRACE__)
|> internal_error(exception)
catch
reason ->
conn
|> Telemetry.request_exception(:throw, reason)
|> internal_error(reason)
kind, reason ->
conn
|> Telemetry.request_exception(kind, reason)
|> internal_error(reason)
end
def call(conn, _config), do: bad_gateway(conn)
defp do_call(conn, _config) do
conn
|> Telemetry.request_exception(:error, :unrecognised_request)
|> bad_gateway()
end
defp internal_error(conn, reason) do
Logger.error("Internal error when routing proxy request: #{inspect(reason)}")
@ -64,4 +99,12 @@ defmodule Wayfarer.Server.Plug do
do: split_targets_and_algorithms(tail, [target | targets], algorithm)
defp split_targets_and_algorithms([], targets, algorithm), do: {:ok, targets, algorithm}
defp get_transport(%{adapter: {Bandit.Adapter, %{transport: %Bandit.HTTP1.Socket{}}}}),
do: :http1
defp get_transport(%{adapter: {Bandit.Adapter, %{transport: %Bandit.HTTP2.Stream{}}}}),
do: :http2
defp get_transport(_), do: :unknown
end

View file

@ -4,9 +4,9 @@ defmodule Wayfarer.Server.Proxy do
specific target.
"""
alias Mint.HTTP
alias Mint.{HTTP, HTTP1, HTTP2}
alias Plug.Conn
alias Wayfarer.{Router, Target.ActiveConnections, Target.TotalConnections}
alias Wayfarer.{Router, Target.ActiveConnections, Target.TotalConnections, Telemetry}
require Logger
@connect_timeout 5_000
@ -16,27 +16,67 @@ defmodule Wayfarer.Server.Proxy do
Convert the request conn into an HTTP request to the specified target.
"""
@spec request(Conn.t(), Router.target()) :: Conn.t()
def request(conn, {scheme, address, port} = target) do
with {:ok, mint} <-
HTTP.connect(scheme, address, port, hostname: conn.host, timeout: @connect_timeout),
def request(conn, target) do
with {:ok, mint} <- connect(conn, target),
:ok <- ActiveConnections.connect(target),
:ok <- TotalConnections.proxy_connect(target),
{:ok, body, conn} <- read_request_body(conn),
{:ok, mint, req} <- send_request(conn, mint, body),
{:ok, conn, _mint} <- proxy_responses(conn, mint, req) do
conn
:ok <- TotalConnections.proxy_connect(target) do
handle_request(mint, conn, target)
else
error -> handle_error(error, conn, target)
error ->
conn
|> Telemetry.request_exception(:error, error)
|> handle_error(error, target)
end
end
defp handle_error({:error, _, reason}, conn, target),
do: handle_error({:error, reason}, conn, target)
defp handle_request(mint, conn, {proto, _, _, _}) do
if http1?(conn) && connection_wants_upgrade?(conn) && upgrade_is_websocket?(conn) do
handle_websocket_request(mint, conn, proto)
else
handle_http_request(mint, conn)
end
end
defp handle_error({:error, reason}, conn, {scheme, address, port}) do
Logger.error(
"Proxy error [phase=#{connection_phase(conn)},ip=#{:inet.ntoa(address)},port=#{port},proto=#{scheme}]: #{message(reason)}"
)
defp handle_http_request(mint, conn) do
with {:ok, mint, req} <- send_request(conn, mint),
{:ok, mint, conn} <- stream_request_body(conn, mint, req),
{:ok, conn, _mint} <- proxy_responses(conn, mint, req) do
conn
|> Telemetry.request_stop()
end
end
defp handle_websocket_request(mint, conn, proto) do
conn
|> WebSockAdapter.upgrade(Wayfarer.Server.WebSocketProxy, {mint, conn, proto}, compress: true)
|> Telemetry.request_upgraded()
end
defp connect(conn, {:ws, address, port, transport}),
do: connect(conn, {:http, address, port, transport})
defp connect(conn, {:wss, address, port, transport}),
do: connect(conn, {:https, address, port, transport})
defp connect(conn, {scheme, address, port, :http1}) when is_tuple(address),
do: HTTP1.connect(scheme, address, port, hostname: conn.host, timeout: @connect_timeout)
defp connect(conn, {scheme, address, port, :http2}) when is_tuple(address),
do: HTTP2.connect(scheme, address, port, hostname: conn.host, timeout: @connect_timeout)
defp connect(conn, {scheme, address, port, :auto}) when is_tuple(address),
do: HTTP.connect(scheme, address, port, hostname: conn.host, timeout: @connect_timeout)
defp handle_error(conn, {:error, _, reason}, target),
do: handle_error(conn, {:error, reason}, target)
defp handle_error(conn, {:error, reason}, {scheme, address, port, transport}) do
Logger.error(fn ->
phase = connection_phase(conn)
ip = :inet.ntoa(address)
"Proxy error [phase=#{phase},ip=#{ip},port=#{port},proto=#{scheme},trans=#{transport}]: #{message(reason)}"
end)
if conn.halted || conn.state in [:sent, :chunked, :upgraded] do
# Sadly there's not much more we can do here.
@ -50,7 +90,54 @@ defmodule Wayfarer.Server.Proxy do
end
end
defp connection_phase(nil), do: "connect"
defp http1?(%{private: %{wayfarer: %{transport: :http1}}}), do: true
defp http1?(_), do: false
defp connection_wants_upgrade?(conn) do
case Conn.get_req_header(conn, "connection") do
["Upgrade"] ->
true
["upgrade"] ->
true
[] ->
false
maybe ->
maybe
|> Enum.flat_map(&String.split(&1, ~r/,\s*/))
|> Enum.map(fn chunk ->
chunk
|> String.trim()
|> String.downcase()
end)
|> Enum.member?("upgrade")
end
end
defp upgrade_is_websocket?(conn) do
case Conn.get_req_header(conn, "upgrade") do
["websocket"] ->
true
[] ->
false
maybe ->
maybe
|> Enum.flat_map(&String.split(&1, ~r/,\s*/))
|> Enum.map(fn chunk ->
chunk
|> String.trim()
|> String.split("/")
|> hd()
|> String.downcase()
end)
|> Enum.member?("websocket")
end
end
defp connection_phase(conn) when conn.state in [:chunked, :upgraded], do: "stream"
defp connection_phase(conn) when conn.halted, do: "done"
defp connection_phase(conn) when conn.state == :sent, do: "done"
@ -72,92 +159,163 @@ defmodule Wayfarer.Server.Proxy do
receive do
message ->
case HTTP.stream(mint, message) do
:unknown -> proxy_responses(conn, mint, req)
{:ok, mint, responses} -> handle_responses(responses, conn, mint, req)
{:error, _, reason, _} -> {:error, reason}
:unknown ->
proxy_responses(conn, mint, req)
{:ok, mint, responses} ->
handle_responses(conn, responses, mint, req)
{:error, _, reason, _} ->
{:error, reason}
end
after
@idle_timeout -> {:error, conn, :idle_timeout}
@idle_timeout -> {:error, :idle_timeout}
end
end
defp handle_responses([], conn, mint, req), do: proxy_responses(conn, mint, req)
defp handle_responses(conn, [], mint, req), do: proxy_responses(conn, mint, req)
defp handle_responses([{:status, req, status} | responses], conn, mint, req),
do: handle_responses(responses, Conn.put_status(conn, status), mint, req)
defp handle_responses([{:headers, req, headers} | responses], conn, mint, req) do
conn =
headers
|> Enum.reduce(conn, &Conn.put_resp_header(&2, elem(&1, 0), elem(&1, 1)))
handle_responses(responses, conn, mint, req)
defp handle_responses(conn, [{:status, req, status} | responses], mint, req) do
conn
|> Conn.put_status(status)
|> Telemetry.request_received_status(status)
|> handle_responses(responses, mint, req)
end
defp handle_responses([{:data, req, body} | responses], conn, mint, req)
defp handle_responses(conn, [{:headers, req, headers} | responses], mint, req) do
headers
|> Enum.reduce(conn, fn {header_name, header_value}, conn ->
conn
|> Conn.put_resp_header(header_name, header_value)
end)
|> handle_responses(responses, mint, req)
end
defp handle_responses(conn, [{:data, req, body} | responses], mint, req)
when conn.state == :chunked do
case Conn.chunk(conn, body) do
{:ok, conn} -> handle_responses(responses, conn, mint, req)
{:error, reason} -> {:error, conn, reason}
{:ok, conn} ->
body_size = byte_size(body)
conn
|> Telemetry.increment_metrics(%{resp_body_bytes: body_size})
|> Telemetry.request_resp_body_chunk(body_size)
|> handle_responses(responses, mint, req)
{:error, reason} ->
{:error, conn, reason}
end
end
defp handle_responses([{:data, req, body} | responses], conn, mint, req) do
defp handle_responses(conn, [{:data, req, body} | responses], mint, req) do
# We need to check here for a content-length or transfer encoding header and
# deal with it. This should be refactored out into a proxy state rather
# than using the conn as our state.
body_size = byte_size(body)
case Conn.get_resp_header(conn, "content-length") do
[] ->
conn = Conn.send_chunked(conn, conn.status)
handle_responses([{:data, req, body} | responses], conn, mint, req)
conn
|> Conn.send_chunked(conn.status)
|> Telemetry.request_resp_started()
|> handle_responses([{:data, req, body} | responses], mint, req)
[length] ->
if String.to_integer(length) == byte_size(body) do
if String.to_integer(length) == body_size do
conn =
conn
|> Conn.delete_resp_header("content-length")
|> Conn.send_resp(conn.status, body)
|> Telemetry.request_resp_started()
|> Telemetry.increment_metrics(%{resp_body_bytes: body_size})
|> Telemetry.request_resp_body_chunk(body_size)
|> Conn.halt()
{:ok, conn, mint}
else
conn =
conn
|> Conn.delete_resp_header("content-length")
|> Telemetry.increment_metrics(%{resp_body_bytes: body_size})
|> Telemetry.request_resp_body_chunk(body_size)
|> Conn.send_chunked(conn.status)
handle_responses([{:data, req, body} | responses], conn, mint, req)
handle_responses(conn, [{:data, req, body} | responses], mint, req)
end
end
end
defp handle_responses([{:done, req} | _], conn, mint, req) do
{:ok, Conn.halt(conn), mint}
# If the connection is done without sending any body content, then we need to
# send the respond and halt the conn.
defp handle_responses(conn, [{:done, req} | _], mint, req) when conn.state == :unset do
conn =
conn
|> Conn.send_resp(conn.status, "")
|> Conn.halt()
{:ok, conn, mint}
end
defp handle_responses([{:error, req, reason} | _], conn, _mint, req), do: {:error, conn, reason}
defp handle_responses(conn, [{:done, req} | _], mint, req) when conn.state == :chunked,
do: {:ok, Conn.halt(conn), mint}
# This is bad - we need to figure out how to stream the body, but it's fine
# for now.
defp read_request_body(conn, body \\ <<>>) do
case Conn.read_body(conn) do
{:ok, chunk, conn} -> {:ok, body <> chunk, conn}
{:more, chunk, conn} -> read_request_body(conn, body <> chunk)
{:error, reason} -> {:error, conn, reason}
end
end
defp handle_responses(conn, [{:error, req, reason} | _], _mint, req), do: {:error, conn, reason}
defp send_request(conn, mint) do
request_path =
case {conn.request_path, conn.query_string} do
{path, nil} -> path
{path, ""} -> path
{path, query} -> path <> "?" <> query
end
defp send_request(conn, mint, body) do
HTTP.request(
mint,
conn.method,
conn.request_path,
request_path,
proxy_headers(conn),
body
:stream
)
end
defp stream_request_body(conn, mint, req) do
case Conn.read_body(conn) do
{:ok, <<>>, conn} ->
with {:ok, mint} <- HTTP.stream_request_body(mint, req, :eof) do
conn =
conn
|> Telemetry.set_metrics(%{req_body_bytes: 0, req_body_chunks: 0})
{:ok, mint, conn}
end
{:ok, chunk, conn} ->
with {:ok, mint} <- HTTP.stream_request_body(mint, req, chunk),
{:ok, mint} <- HTTP.stream_request_body(mint, req, :eof) do
chunk_size = byte_size(chunk)
conn =
conn
|> Telemetry.increment_metrics(%{req_body_bytes: chunk_size, req_body_chunks: 1})
|> Telemetry.request_req_body_chunk(chunk_size)
{:ok, mint, conn}
end
{:more, chunk, conn} ->
with {:ok, mint} <- HTTP.stream_request_body(mint, req, chunk) do
chunk_size = byte_size(chunk)
conn
|> Telemetry.increment_metrics(%{req_body_bytes: chunk_size, req_body_chunks: 1})
|> Telemetry.request_req_body_chunk(chunk_size)
|> stream_request_body(mint, req)
end
{:error, reason} ->
{:error, conn, reason}
end
end
defp proxy_headers(conn) do
listener =
conn.private.wayfarer.listener

View file

@ -0,0 +1,256 @@
defmodule Wayfarer.Server.WebSocketProxy do
@moduledoc """
When a connection is upgraded to a websocket, we switch from handing via
`Plug` to `WebSock` via `WebSockAdapter`.
The outgoing connection is made using `Mint.WebSocket`.
"""
@behaviour WebSock
alias Mint.WebSocket
alias Plug.Conn
alias Wayfarer.Telemetry
require Logger
@default_opts [extensions: [WebSocket.PerMessageDeflate]]
@doc false
@impl true
def init({mint, conn, proto}) when proto in [:ws, :wss] do
request_path =
case {conn.request_path, conn.query_string} do
{path, nil} -> path
{path, ""} -> path
{path, query} -> path <> "?" <> query
end
case WebSocket.upgrade(proto, mint, request_path, proxy_headers(conn), @default_opts) do
{:ok, mint, ref} -> {:ok, %{mint: mint, ref: ref, status: :init, buffer: [], conn: conn}}
{:error, _mint, reason} -> {:error, reason}
end
end
def init({mint, conn, :https}), do: init({mint, conn, :wss})
def init({mint, conn, :http}), do: init({mint, conn, :ws})
@doc false
@impl true
def handle_control({frame, [{:opcode, :ping}]}, state) do
with {:ok, websocket, data} <- WebSocket.encode(state.websocket, {:ping, frame}),
{:ok, mint} <- WebSocket.stream_request_body(state.mint, state.ref, data) do
conn = request_client_frame(state.conn, {:ping, frame})
{:ok, %{state | websocket: websocket, mint: mint, conn: conn}}
else
error -> handle_error(error, state)
end
end
def handle_control({frame, [{:opcode, frame_type}]}, state) do
conn = request_client_frame(state.conn, {frame_type, frame})
{:ok, %{state | conn: conn}}
end
@doc false
@impl true
def handle_in({payload, [{:opcode, frame_type}]}, state) when state.status == :init do
frame = {frame_type, payload}
buffer = [frame | state.buffer]
conn = request_client_frame(state.conn, frame)
{:ok, %{state | buffer: buffer, conn: conn}}
end
def handle_in({payload, [{:opcode, frame_type}]}, state) do
with {:ok, websocket, data} <- WebSocket.encode(state.websocket, {frame_type, payload}),
{:ok, mint} <- WebSocket.stream_request_body(state.mint, state.ref, data) do
conn = request_client_frame(state.conn, {frame_type, payload})
{:ok, %{state | websocket: websocket, mint: mint, conn: conn}}
else
error -> handle_error(error, state)
end
end
@doc false
@impl true
def handle_info(msg, state) when state.status == :init do
with {:ok, mint, result} <- WebSocket.stream(state.mint, msg),
{:ok, result} <- handle_upgrade_response(result, state.ref),
{:ok, mint, websocket} <- WebSocket.new(mint, state.ref, result.status, result.headers),
state <- Map.merge(state, %{status: :connected, websocket: websocket, mint: mint}),
{:ok, state} <- empty_buffer(state),
{:ok, messages, state} <- decode_frames(result.data, state) do
response_for_messages(messages, state)
else
error -> handle_error(error, state)
end
end
def handle_info(msg, state) when state.status == :connected do
with {:ok, mint, result} <- WebSocket.stream(state.mint, msg),
{:ok, frames} <- handle_websocket_data(result, state.ref),
{:ok, messages, state} <- decode_frames(frames, %{state | mint: mint}) do
response_for_messages(messages, state)
else
error -> handle_error(error, state)
end
end
@doc false
@impl true
def terminate(_reason, _state), do: :ok
defp handle_error({:error, _, %{reason: reason}, _}, state),
do: handle_error({:error, reason}, state)
defp handle_error({:error, reason, state}, _state),
do: handle_error({:error, reason}, state)
defp handle_error({:error, reason}, state) do
Logger.debug(fn ->
"Dropping WebSocket connection for reason: #{inspect(reason)}"
end)
{:stop, :normal, state}
end
defp proxy_headers(conn) do
listener =
conn.private.wayfarer.listener
|> case do
%{address: address, port: port} when tuple_size(address) == 8 ->
"[#{:inet.ntoa(address)}]:#{port}"
%{address: address, port: port} ->
"#{:inet.ntoa(address)}:#{port}"
end
client =
conn
|> Conn.get_peer_data()
|> case do
%{address: address, port: port} when tuple_size(address) == 8 ->
"[#{:inet.ntoa(address)}]:#{port}"
%{address: address, port: port} ->
"#{:inet.ntoa(address)}:#{port}"
end
req_headers =
conn.req_headers
|> Enum.reject(
&(elem(&1, 0) in [
"connection",
"upgrade",
"sec-websocket-extensions",
"sec-websocket-key",
"sec-websocket-version"
])
)
[
{"forwarded", "by=#{listener};for=#{client};host=#{conn.host};proto=#{conn.scheme}"}
| req_headers
]
end
defp empty_buffer(state) when state.buffer == [], do: {:ok, state}
defp empty_buffer(state), do: do_empty_buffer(Enum.reverse(state.buffer), %{state | buffer: []})
defp do_empty_buffer([], state), do: {:ok, state}
defp do_empty_buffer([head | tail], state) do
with {:ok, websocket, data} <- WebSocket.encode(state.websocket, head),
{:ok, mint} <- WebSocket.stream_request_body(state.mint, state.ref, data) do
conn = request_client_frame(state.conn, head)
do_empty_buffer(tail, %{state | websocket: websocket, mint: mint, conn: conn})
end
end
defp handle_upgrade_response(result, ref), do: handle_upgrade_response(result, ref, %{data: []})
defp handle_upgrade_response([{:done, ref}], ref, result), do: {:ok, result}
defp handle_upgrade_response([{:status, ref, status} | tail], ref, result) do
handle_upgrade_response(tail, ref, Map.put(result, :status, status))
end
defp handle_upgrade_response([{:headers, ref, headers} | tail], ref, result) do
handle_upgrade_response(tail, ref, Map.put(result, :headers, headers))
end
defp handle_upgrade_response([{:data, ref, data} | tail], ref, result) do
result = Map.update!(result, :data, &[data | &1])
handle_upgrade_response(tail, ref, result)
end
defp handle_websocket_data(data, ref),
do: handle_websocket_data(data, ref, [])
defp handle_websocket_data([], _ref, messages), do: {:ok, Enum.reverse(messages)}
defp handle_websocket_data([{:data, ref, data} | tail], ref, messages),
do: handle_websocket_data(tail, ref, [data | messages])
defp decode_frames(frames, state) do
frames
|> Enum.reduce_while({:ok, [], state}, fn frame, {:ok, messages, state} ->
case decode_frame(state, frame) do
{:ok, new_messages, state} -> {:cont, {:ok, [new_messages, messages], state}}
{:error, reason, state} -> {:halt, {:error, reason, state}}
end
end)
|> case do
{:ok, messages, state} -> {:ok, List.flatten(messages), state}
{:error, reason, state} -> {:error, reason, state}
end
end
defp decode_frame(state, frame) do
case WebSocket.decode(state.websocket, frame) do
{:ok, websocket, frames} when is_list(frames) ->
conn = Enum.reduce(frames, state.conn, &request_server_frame(&2, &1))
{:ok, frames, %{state | websocket: websocket, conn: conn}}
{:error, websocket, reason} ->
{:error, reason, %{state | websocket: websocket}}
end
end
# Handle all the frames coming from the target and decide how to respond to
# Bandit/WebSock. In the case of encountering a close frame, we terminate the
# client websocket with the same code, otherwise we just copy the frames over.
defp response_for_messages(messages, state, response \\ [])
defp response_for_messages([], state, []), do: {:ok, state}
defp response_for_messages([], state, response), do: {:push, Enum.reverse(response), state}
defp response_for_messages([{:close, code, _} | _], state, response),
do: {:stop, :normal, code, Enum.reverse(response), state}
defp response_for_messages([message | messages], state, response),
do: response_for_messages(messages, state, [message | response])
defp request_client_frame(conn, {frame_type, frame}) do
frame_size = byte_size(frame)
conn
|> Telemetry.increment_metrics(%{
client_frame_bytes: frame_size,
client_frame_count: 1
})
|> Telemetry.request_client_frame(frame_size, frame_type)
end
defp request_server_frame(conn, {frame_type, frame}) do
frame_size = byte_size(frame)
conn
|> Telemetry.increment_metrics(%{
server_frame_bytes: frame_size,
server_frame_count: 1
})
|> Telemetry.request_server_frame(frame_size, frame_type)
end
end

View file

@ -3,14 +3,14 @@ defmodule Wayfarer.Target do
use GenServer, restart: :transient
require Logger
alias Spark.OptionsHelpers
alias Spark.Options
alias Wayfarer.{Dsl.HealthCheck, Router, Server, Target}
import Wayfarer.Utils
@options_schema [
scheme: [
type: {:in, [:http, :https]},
doc: "The connection protocol.",
type: {:in, [:http, :https, :ws, :wss]},
doc: "The connection scheme.",
required: true
],
port: [
@ -33,6 +33,12 @@ defmodule Wayfarer.Target do
doc: "An optional name for the target.",
required: false
],
transport: [
type: {:in, [:http1, :http2, :auto]},
required: false,
default: :auto,
doc: "The connection protocol."
],
health_checks: [
type: {:list, {:keyword_list, HealthCheck.schema()}},
required: false,
@ -61,7 +67,7 @@ defmodule Wayfarer.Target do
## Options
#{OptionsHelpers.docs(@options_schema)}
#{Options.docs(@options_schema)}
"""
@type key :: {module, :http | :https, IP.Address.t(), :socket.port_number()}
@ -90,9 +96,9 @@ defmodule Wayfarer.Target do
@doc false
@impl true
def init(options) do
with {:ok, options} <- OptionsHelpers.validate(options, @options_schema),
with {:ok, options} <- Options.validate(options, @options_schema),
{:ok, uri} <- to_uri(options[:scheme], options[:address], options[:port]) do
target = options |> Keyword.take(~w[scheme address port]a) |> Map.new()
target = options |> Keyword.take(~w[scheme address port transport]a) |> Map.new()
module = options[:module]
key = {module, target.scheme, target.address, target.port}
@ -117,6 +123,7 @@ defmodule Wayfarer.Target do
method: check[:method] |> to_string() |> String.upcase(),
headers: @default_headers,
hostname: check[:hostname] || uri.host,
transport: target.transport,
passes: 0
})
@ -170,7 +177,8 @@ defmodule Wayfarer.Target do
end)
Server.target_status_change(
{state.module, state.target.scheme, state.target.address, state.target.port},
{state.module, state.target.scheme, state.target.address, state.target.port,
state.target.transport},
:unhealthy
)
@ -205,7 +213,8 @@ defmodule Wayfarer.Target do
if target_became_healthy? do
Server.target_status_change(
{state.module, state.target.scheme, state.target.address, state.target.port},
{state.module, state.target.scheme, state.target.address, state.target.port,
state.target.transport},
:healthy
)

View file

@ -36,8 +36,8 @@ defmodule Wayfarer.Target.ActiveConnections do
@impl true
@spec handle_info(:tick | {:DOWN, any, :process, any, pid, any}, state) :: {:noreply, state}
def handle_info(:tick, state) do
size = :ets.info(state.table, :size)
Logger.debug("Active connections: #{size}")
# size = :ets.info(state.table, :size)
# Logger.debug("Active connections: #{size}")
{:noreply, state}
end

View file

@ -4,23 +4,48 @@ defmodule Wayfarer.Target.Check do
"""
use GenServer, restart: :transient
alias Mint.HTTP
alias Wayfarer.{Target, Target.TotalConnections}
alias Mint.{HTTP, HTTP1, HTTP2, WebSocket}
alias Wayfarer.{Target, Target.TotalConnections, Telemetry}
require Logger
@type state :: %{
conn: struct(),
req: reference(),
scheme: :http | :https | :ws | :wss,
address: :inet.ip_address(),
port: :socket.port_number(),
uri: URI.t(),
ref: any,
method: String.t(),
headers: [{String.t(), String.t()}],
hostname: String.t(),
transport: :http1 | :http2 | :auto,
span: map
}
@doc false
@impl true
def init(state), do: {:ok, state, {:continue, :start_check}}
@doc false
@impl true
@spec handle_continue(:start_check, state) :: {:noreply, state, timeout} | {:stop, :normal, nil}
def handle_continue(:start_check, state) do
with {:ok, conn} <- connect(state),
{:ok, conn, req} <- request(Map.put(state, :conn, conn)) do
state =
state
|> Map.merge(%{conn: conn, req: req})
state =
state
|> Map.put(:span, %{
metadata: %{
target: %{scheme: state.scheme, address: state.address, port: state.port},
method: state.method,
uri: state.uri,
hostname: state.hostname,
telemetry_span_context: make_ref()
}
})
|> Telemetry.health_check_start()
with {:ok, state} <- connect(state),
{:ok, state} <- request(state) do
{:noreply, state, state.response_timeout}
else
{:error, reason} ->
@ -33,59 +58,121 @@ defmodule Wayfarer.Target.Check do
@doc false
@impl true
def handle_info(:timeout, state), do: check_failed(state, "request timeout expired.")
def handle_info(:timeout, state), do: check_failed(state, :timeout)
def handle_info(message, state) do
with {:ok, conn, responses} <- Mint.HTTP.stream(state.conn, message),
:ok <- TotalConnections.health_check_connect({state.scheme, state.address, state.port}),
with {:ok, conn, responses} <- WebSocket.stream(state.conn, message),
:ok <-
TotalConnections.health_check_connect(
{state.scheme, state.address, state.port, state.transport}
),
{:ok, status} <- get_status_response(conn, responses) do
if Enum.any?(state.success_codes, &Enum.member?(&1, status)) do
Target.check_passed(state.ref)
Telemetry.health_check_pass(state, status)
{:stop, :normal, nil}
else
check_failed(state, "received #{status} status code")
end
else
{:continue, conn} ->
{:noreply, %{state | conn: conn}}
{:noreply, Map.put(state, :conn, conn)}
:unknown ->
check_failed(state, "Received unknown message: `#{inspect(message)}`")
{:error, reason} ->
check_failed(state, reason)
{:error, _conn, error, _responses} ->
check_failed(state, error)
end
end
defp connect(state),
do:
HTTP.connect(state.scheme, state.address, state.port,
timeout: state.connect_timeout,
hostname: state.hostname
)
defp connect(state) when state.scheme == :ws,
do: connect(%{state | scheme: :http})
defp request(state),
do: HTTP.request(state.conn, state.method, state.path, state.headers, nil)
defp connect(state) when state.scheme == :wss,
do: connect(%{state | scheme: :https})
defp connect(state) when state.transport == :http1 do
with {:ok, conn} <-
HTTP1.connect(state.scheme, state.address, state.port,
timeout: state.connect_timeout,
hostname: state.hostname
) do
{:ok, Telemetry.health_check_connect(Map.put(state, :conn, conn), :http1)}
end
end
defp connect(state) when state.transport == :http2 do
with {:ok, conn} <-
HTTP2.connect(state.scheme, state.address, state.port,
timeout: state.connect_timeout,
hostname: state.hostname
) do
{:ok, Telemetry.health_check_connect(Map.put(state, :conn, conn), :http2)}
end
end
defp connect(state) do
with {:ok, conn} <-
HTTP.connect(state.scheme, state.address, state.port,
timeout: state.connect_timeout,
hostname: state.hostname
) do
transport =
case conn do
%Mint.HTTP1{} -> :http1
%Mint.HTTP2{} -> :http2
end
{:ok, Telemetry.health_check_connect(Map.put(state, :conn, conn), transport)}
end
end
defp request(state) when state.scheme in [:ws, :wss] do
with {:ok, conn, req} <-
WebSocket.upgrade(state.scheme, state.conn, state.path, state.headers, []) do
state = Map.merge(state, %{conn: conn, req: req})
{:ok, Telemetry.health_check_request(state)}
end
end
defp request(state) do
with {:ok, conn, req} <-
HTTP.request(state.conn, state.method, state.path, state.headers, nil) do
state = Map.merge(state, %{conn: conn, req: req})
{:ok, Telemetry.health_check_request(state)}
end
end
defp check_failed(state, reason) when is_binary(reason) do
Target.check_failed(state.ref)
Logger.warning("Health check failed for #{state.method} #{state.uri}: #{reason}.")
Telemetry.health_check_fail(state, reason)
Logger.warning(fn -> "Health check failed for #{state.method} #{state.uri}: #{reason}." end)
{:stop, :normal, nil}
end
defp check_failed(state, exception) when is_exception(exception) do
Target.check_failed(state.ref)
Telemetry.health_check_fail(state, exception)
Logger.warning(
Logger.warning(fn ->
"Health check failed for #{state.method} #{state.uri}: #{Exception.message(exception)}"
)
end)
{:stop, :normal, nil}
end
defp check_failed(state, reason) do
Target.check_failed(state.ref)
Logger.warning("Health check failed for #{state.method} #{state.uri}: `#{inspect(reason)}`")
Telemetry.health_check_fail(state, reason)
Logger.warning(fn ->
"Health check failed for #{state.method} #{state.uri}: `#{inspect(reason)}`"
end)
{:stop, :normal, nil}
end

764
lib/wayfarer/telemetry.ex Normal file
View file

@ -0,0 +1,764 @@
defmodule Wayfarer.Telemetry do
@moduledoc """
Wayfarer emits a number of telemetry events and spans on top of the excellent
telemetry events emitted by `Bandit.Telemetry`.
"""
alias Plug.Conn
alias Wayfarer.{Router, Target.Check, Target.Selector}
@typedoc """
Information about the target a request has been routed to.
"""
@type target ::
%{
scheme: :http | :https | :ws | :wss,
address: :inet.ip_address(),
port: :socket.port_number()
}
| %{scheme: :plug, module: module, options: any}
@typedoc """
Information about the listener that received the request.
"""
@type listener ::
%{
scheme: :http | :https,
module: module(),
port: :socket.port_number(),
address: :inet.ip_address()
}
@typedoc """
The time that the event was emitted, in `:native` time units.
This is sources from `System.monotonic_time/0` which has some caveats but in
general is better for calculating durations as it should never go backwards.
"""
@type monotonic_time :: integer()
@typedoc """
The time passed since the beginning of the span, in `:native` time units.
The difference between the current `monotonic_time` and the first
`monotonic_time` at the start of the span.
"""
@type duration :: integer()
@typedoc "The HTTP protocol version of the request"
@type transport :: :http1 | :http2
@typedoc "A unique identifier for the span"
@type telemetry_span_context :: reference()
@typedoc "A convenience type for describing telemetry events"
@opaque event(measurements, metadata) :: {measurements, metadata}
@typedoc """
The `[:wayfarer, :request, :start]` event.
This event signals the start of a request span tracking a client request to
completion.
You can use the `telemetry_span_context` metadata value to correlate
subsequent events within the same span.
"""
@type request_start ::
event(
%{
required(:monotonic_time) => monotonic_time(),
required(:wallclock_time) => DateTime.t()
},
%{
required(:conn) => Conn.t(),
required(:listener) => listener(),
required(:transport) => transport(),
required(:telemetry_span_context) => telemetry_span_context()
}
)
@typedoc """
The `[:wayfarer, :request, :routed]` event.
This event signals that the routing process has completed and a target has
been chosen to serve the request.
"""
@type request_routed ::
event(
%{
required(:monotonic_time) => monotonic_time(),
required(:duration) => duration(),
required(:wallclock_time) => DateTime.t()
},
%{
required(:conn) => Conn.t(),
required(:listener) => listener(),
required(:transport) => transport(),
required(:telemetry_span_context) => telemetry_span_context(),
required(:target) => target(),
required(:algorithm) => Selector.algorithm()
}
)
@typedoc """
The `[:wayfarer, :request, :exception]` event.
This event signals that something went wrong while processing the event. You
will likely still receive other events (eg `:stop`) for this span however.
"""
@type request_exception ::
event(
%{
required(:monotonic_time) => monotonic_time(),
required(:duration) => duration(),
required(:wallclock_time) => DateTime.t()
},
%{
required(:conn) => Conn.t(),
required(:listener) => listener(),
required(:transport) => transport(),
required(:telemetry_span_context) => telemetry_span_context(),
optional(:target) => target(),
optional(:algorithm) => Selector.algorithm(),
required(:kind) => :throw | :exit | :error | :exception,
required(:reason) => any,
required(:stacktrace) => Exception.stacktrace()
}
)
@typedoc """
The `[:wayfarer, :request, :stop]` event.
This event signals that the request has completed.
The measurements will contain any incrementing counters accumulated during the
course of the request.
"""
@type request_stop ::
event(
%{
required(:monotonic_time) => monotonic_time(),
required(:duration) => duration(),
required(:wallclock_time) => DateTime.t(),
required(:status) => nil | 100..599,
optional(:req_body_bytes) => non_neg_integer(),
optional(:resp_body_bytes) => non_neg_integer(),
optional(:client_frame_bytes) => non_neg_integer(),
optional(:client_frame_count) => non_neg_integer(),
optional(:server_frame_bytes) => non_neg_integer(),
optional(:server_frame_count) => non_neg_integer()
},
%{
required(:conn) => Conn.t(),
required(:listener) => listener(),
required(:transport) => transport(),
required(:telemetry_span_context) => telemetry_span_context(),
optional(:target) => target(),
optional(:algorithm) => Selector.algorithm(),
optional(:status) => nil | 100..599,
optional(:kind) => :throw | :exit | :error | :exception,
optional(:reason) => any,
optional(:stacktrace) => Exception.stacktrace()
}
)
@typedoc """
The `[:wayfarer, :request, :received_status]` event.
This event signals that an HTTP status code has been received from the
upstream target.
"""
@type request_received_status ::
event(
%{
required(:monotonic_time) => monotonic_time(),
required(:duration) => duration(),
required(:wallclock_time) => DateTime.t(),
required(:status) => nil | 100..599,
optional(:req_body_bytes) => non_neg_integer()
},
%{
required(:conn) => Conn.t(),
required(:listener) => listener(),
required(:transport) => transport(),
required(:telemetry_span_context) => telemetry_span_context(),
required(:target) => target(),
required(:algorithm) => Selector.algorithm(),
required(:status) => nil | 100..599
}
)
@typedoc """
The `[:wayfarer, :request, :req_body_chunk]` event.
This event is emitted while streaming the request body from the client to the
target. Under the hood `Plug.Conn.read_body/2` is being called with the
default options, meaning that each chunk is likely to be up to 8MB in size.
If there is no request body then this event will not be emitted and the
`req_body_bytes` and `req_body_chunks` counters will both be set to zero for
this request.
"""
@type request_req_body_chunk ::
event(
%{
required(:monotonic_time) => monotonic_time(),
required(:duration) => duration(),
required(:wallclock_time) => DateTime.t(),
required(:status) => nil | 100..599,
required(:req_body_bytes) => non_neg_integer(),
required(:req_body_chunks) => non_neg_integer(),
required(:chunk_bytes) => non_neg_integer()
},
%{
required(:conn) => Conn.t(),
required(:listener) => listener(),
required(:transport) => transport(),
required(:telemetry_span_context) => telemetry_span_context(),
required(:target) => target(),
required(:algorithm) => Selector.algorithm(),
required(:status) => nil | 100..599
}
)
@typedoc """
The `[:wayfarer, :request, :resp_started]` event.
This event indicates that the HTTP status and headers have been received from
the target and the response will now start being sent to the target.
"""
@type request_resp_started ::
event(
%{
required(:monotonic_time) => monotonic_time(),
required(:duration) => duration(),
required(:wallclock_time) => DateTime.t(),
required(:status) => nil | 100..599,
optional(:req_body_bytes) => non_neg_integer(),
optional(:req_body_chunks) => non_neg_integer()
},
%{
required(:conn) => Conn.t(),
required(:listener) => listener(),
required(:transport) => transport(),
required(:telemetry_span_context) => telemetry_span_context(),
required(:target) => target(),
required(:algorithm) => Selector.algorithm(),
required(:status) => nil | 100..599
}
)
@typedoc """
The `[:wayfarer, :request, :resp_body_chunk]` event.
This event is emitted every time a chunk of response body is received from the
target for streaming to the client. Under the hood, these are emitted every
time `Mint.HTTP.stream/2` returns a data frame.
"""
@type request_resp_body_chunk ::
event(
%{
required(:monotonic_time) => monotonic_time(),
required(:duration) => duration(),
required(:wallclock_time) => DateTime.t(),
optional(:req_body_bytes) => non_neg_integer(),
optional(:req_body_chunks) => non_neg_integer(),
required(:resp_body_bytes) => non_neg_integer(),
required(:resp_body_chunks) => non_neg_integer(),
required(:chunk_bytes) => non_neg_integer()
},
%{
required(:conn) => Conn.t(),
required(:listener) => listener(),
required(:transport) => transport(),
required(:telemetry_span_context) => telemetry_span_context(),
required(:target) => target(),
required(:algorithm) => Selector.algorithm(),
required(:status) => nil | 100..599
}
)
@typedoc """
The `[:wayfarer, :request, :upgraded]` event.
This event is emitted when a client connection is upgraded to a WebSocket
connection.
"""
@type request_upgraded ::
event(
%{
required(:monotonic_time) => monotonic_time(),
required(:duration) => duration(),
required(:wallclock_time) => DateTime.t()
},
%{
required(:conn) => Conn.t(),
required(:listener) => listener(),
required(:transport) => transport(),
required(:telemetry_span_context) => telemetry_span_context(),
required(:target) => target(),
required(:algorithm) => Selector.algorithm(),
required(:status) => nil | 100..599
}
)
@typedoc """
The `[:wayfarer, :request, :client_frame]` event.
This event is emitted any time a WebSocket frame is received from the client
for transmission to the target.
"""
@type request_client_frame ::
event(
%{
required(:monotonic_time) => monotonic_time(),
required(:duration) => duration(),
required(:wallclock_time) => DateTime.t(),
required(:frame_size) => non_neg_integer(),
required(:client_frame_bytes) => non_neg_integer(),
required(:client_frame_count) => non_neg_integer(),
optional(:server_frame_bytes) => non_neg_integer(),
optional(:server_frame_count) => non_neg_integer()
},
%{
required(:conn) => Conn.t(),
required(:listener) => listener(),
required(:transport) => transport(),
required(:telemetry_span_context) => telemetry_span_context(),
required(:target) => target(),
required(:algorithm) => Selector.algorithm(),
required(:status) => nil | 100..599,
required(:opcode) => :text | :binary | :ping | :pong | :close
}
)
@typedoc """
The `[:wayfarer, :request, :server_frame]` event.
This event is emitted any time a WebSocket frame is received from the target
for transmission to the client.
"""
@type request_server_frame ::
event(
%{
required(:monotonic_time) => monotonic_time(),
required(:duration) => duration(),
required(:wallclock_time) => DateTime.t(),
required(:frame_size) => non_neg_integer(),
required(:server_frame_bytes) => non_neg_integer(),
required(:server_frame_count) => non_neg_integer(),
optional(:client_frame_bytes) => non_neg_integer(),
optional(:client_frame_count) => non_neg_integer()
},
%{
required(:conn) => Conn.t(),
required(:listener) => listener(),
required(:transport) => transport(),
required(:telemetry_span_context) => telemetry_span_context(),
required(:target) => target(),
required(:algorithm) => Selector.algorithm(),
required(:status) => nil | 100..599
}
)
@typedoc """
All the event types that make up the `[:wayfarer, :request, :*]` span.
"""
@type request_span ::
request_start
| request_routed
| request_exception
| request_stop
| request_received_status
| request_req_body_chunk
| request_resp_started
| request_resp_body_chunk
| request_upgraded
| request_client_frame
| request_server_frame
@typedoc """
The `[:wayfarer, :health_check, :start]` event.
This event signals the start of a health check span.
You can use the `telemetry_span_context` metadata value to correlate
subsequent events within the same span.
"""
@type health_check_start ::
event(
%{
required(:monotonic_time) => monotonic_time(),
required(:wallclock_time) => DateTime.t()
},
%{
required(:telemetry_span_context) => telemetry_span_context(),
required(:hostname) => String.t(),
required(:uri) => URI.t(),
required(:target) => target(),
required(:method) => String.t()
}
)
@typedoc """
The `[:wayfarer, :health_check, :connect]` event.
This event signals that the outgoing TCP connection has been made to the
target.
"""
@type health_check_connect ::
event(
%{
required(:monotonic_time) => monotonic_time(),
required(:wallclock_time) => DateTime.t(),
required(:duration) => duration()
},
%{
required(:telemetry_span_context) => telemetry_span_context(),
required(:hostname) => String.t(),
required(:uri) => URI.t(),
required(:target) => target(),
required(:method) => String.t(),
required(:transport) => transport()
}
)
@typedoc """
The `[:wayfarer, :health_check, :request]` event.
This event signals that the HTTP or WebSocket request has been sent.
"""
@type health_check_request ::
event(
%{
required(:monotonic_time) => monotonic_time(),
required(:wallclock_time) => DateTime.t(),
required(:duration) => duration()
},
%{
required(:telemetry_span_context) => telemetry_span_context(),
required(:hostname) => String.t(),
required(:uri) => URI.t(),
required(:target) => target(),
required(:method) => String.t(),
required(:transport) => transport()
}
)
@typedoc """
The `[:wayfarer, :health_check, :pass]` event.
This event signals that the HTTP status code returned by the target matches
one of the configured success codes.
It also signals the end of the span.
"""
@type health_check_pass ::
event(
%{
required(:monotonic_time) => monotonic_time(),
required(:wallclock_time) => DateTime.t(),
required(:duration) => duration(),
required(:status) => 100..599
},
%{
required(:telemetry_span_context) => telemetry_span_context(),
required(:hostname) => String.t(),
required(:uri) => URI.t(),
required(:target) => target(),
required(:method) => String.t(),
required(:transport) => transport()
}
)
@typedoc """
The `[:wayfarer, :health_check, :fail]` event.
This event signals that the HTTP status code returned by the target did not
match any of the configured success codes.
It also signals the end of the span.
"""
@type health_check_fail ::
event(
%{
required(:monotonic_time) => monotonic_time(),
required(:wallclock_time) => DateTime.t(),
required(:duration) => duration(),
required(:reason) => any
},
%{
required(:telemetry_span_context) => telemetry_span_context(),
required(:hostname) => String.t(),
required(:uri) => URI.t(),
required(:target) => target(),
required(:method) => String.t(),
required(:transport) => transport()
}
)
@typedoc """
All the event types that make up the `[:wayfarer, :health_check, :*]` span.
"""
@type health_check_span ::
health_check_start | health_check_request | health_check_pass | health_check_fail
@typedoc """
All the spans that can be emitted by Wayfarer.
"""
@type spans :: request_span | health_check_span
@doc false
@spec request_start(Conn.t()) :: Conn.t()
def request_start(conn) do
telemetry_span_context = make_ref()
metadata =
conn
|> Map.get(:private, %{})
|> Map.get(:wayfarer, %{})
|> Map.merge(%{telemetry_span_context: telemetry_span_context, conn: conn})
conn
|> execute_request_span_event(:start, %{}, metadata)
end
@doc false
@spec request_routed(Conn.t(), Router.target(), Router.algorithm()) :: Conn.t()
def request_routed(conn, target, algorithm) do
target =
case target do
{scheme, address, port, _transport} -> %{scheme: scheme, address: address, port: port}
{:plug, {module, opts}} -> %{scheme: :plug, module: module, options: opts}
{:plug, module} -> %{scheme: :plug, module: module, options: []}
end
conn
|> execute_request_span_event(:routed, %{}, %{
target: target,
algorithm: algorithm
})
end
@doc false
@spec request_exception(Conn.t(), kind :: any, reason :: any, stacktrace :: list | nil) ::
Conn.t()
def request_exception(conn, kind, reason, stacktrace \\ nil) do
conn
|> execute_request_span_event(:exception, %{}, %{
kind: kind,
reason: reason,
stacktrace: stacktrace || Process.info(self(), :current_stacktrace)
})
end
@doc false
def request_stop(conn) do
conn
|> execute_request_span_event(:stop, %{status: conn.status}, %{})
end
@doc false
@spec request_received_status(Conn.t(), non_neg_integer()) :: Conn.t()
def request_received_status(conn, status) do
conn
|> execute_request_span_event(:received_status, %{status: status}, %{status: status})
end
@doc false
@spec request_req_body_chunk(Conn.t(), non_neg_integer()) :: Conn.t()
def request_req_body_chunk(conn, chunk_bytes) do
conn
|> execute_request_span_event(:req_body_chunk, %{chunk_bytes: chunk_bytes}, %{})
end
@doc false
@spec request_resp_started(Conn.t()) :: Conn.t()
def request_resp_started(conn) do
metric = %{status: conn.status}
conn
|> execute_request_span_event(:resp_started, metric, metric)
end
@doc false
@spec request_resp_body_chunk(Conn.t(), non_neg_integer()) :: Conn.t()
def request_resp_body_chunk(conn, chunk_bytes) do
conn
|> execute_request_span_event(:resp_body_chunk, %{chunk_bytes: chunk_bytes}, %{})
end
@doc false
@spec request_upgraded(Conn.t()) :: Conn.t()
def request_upgraded(conn) do
conn
|> execute_request_span_event(:upgraded, %{}, %{})
end
@doc false
@spec request_client_frame(Conn.t(), non_neg_integer(), atom) :: Conn.t()
def request_client_frame(conn, bytes, opcode) do
conn
|> execute_request_span_event(:client_frame, %{frame_size: bytes}, %{opcode: opcode})
end
@doc false
@spec request_server_frame(Conn.t(), non_neg_integer(), atom) :: Conn.t()
def request_server_frame(conn, bytes, opcode) do
conn
|> execute_request_span_event(:server_frame, %{frame_size: bytes}, %{opcode: opcode})
end
@doc false
@spec set_metrics(Conn.t(), %{atom => number}) :: Conn.t()
def set_metrics(conn, metrics) do
update_metrics(conn, &Map.merge(&1, metrics))
end
@doc false
@spec increment_metrics(Conn.t(), %{atom => number}) :: Conn.t()
def increment_metrics(conn, to_increment) do
update_metrics(conn, fn metrics ->
Enum.reduce(to_increment, metrics, fn {metric_name, increment_by}, metrics ->
Map.update(metrics, metric_name, increment_by, &(&1 + increment_by))
end)
end)
end
@doc false
@spec health_check_start(Check.state()) :: Check.state()
def health_check_start(check) do
execute_check_span_event(check, :start, %{}, %{})
end
@doc false
@spec health_check_connect(Check.state(), atom) :: Check.state()
def health_check_connect(check, transport) do
execute_check_span_event(check, :connect, %{}, %{transport: transport})
end
@doc false
@spec health_check_request(Check.state()) :: Check.state()
def health_check_request(check) do
execute_check_span_event(check, :request, %{}, %{})
end
@doc false
@spec health_check_fail(Check.state(), any) :: Check.state()
def health_check_fail(check, reason) do
execute_check_span_event(check, :fail, %{}, %{reason: reason})
end
@doc false
@spec health_check_pass(Check.state(), 100..599) :: Check.state()
def health_check_pass(check, status) do
execute_check_span_event(check, :pass, %{}, %{status: status})
end
defp update_metrics(conn, callback) do
private =
conn
|> Map.get(:private, %{})
|> Map.get(:wayfarer, %{})
metrics =
private
|> Map.get(:metrics, %{})
|> callback.()
private = Map.put(private, :metrics, metrics)
conn
|> Conn.put_private(:wayfarer, private)
end
defp execute_request_span_event(conn, event, measurements, metadata) do
monotonic_time = System.monotonic_time()
now = DateTime.utc_now()
private =
conn
|> Map.get(:private, %{})
|> Map.get(:wayfarer, %{})
span_info =
private
|> Map.get(:request_span, %{})
metrics =
private
|> Map.get(:metrics, %{})
measurements =
if Map.has_key?(span_info, :start_time) do
%{
monotonic_time: monotonic_time,
duration: monotonic_time - span_info.start_time,
wallclock_time: now
}
else
%{
monotonic_time: monotonic_time,
wallclock_time: now
}
end
|> Map.merge(metrics)
|> Map.merge(measurements)
metadata =
span_info
|> Map.get(:metadata, %{})
|> Map.merge(metadata)
:telemetry.execute([:wayfarer, :request, event], measurements, Map.put(metadata, :conn, conn))
span_info =
span_info
|> Map.put(:metadata, metadata)
|> Map.put_new(:start_time, monotonic_time)
private =
private
|> Map.put(:request_span, span_info)
conn
|> Conn.put_private(:wayfarer, private)
end
defp execute_check_span_event(check, event, measurements, metadata) do
monotonic_time = System.monotonic_time()
now = DateTime.utc_now()
span_info = Map.get(check, :span, %{})
metrics = Map.get(span_info, :metrics, %{})
measurements =
if Map.has_key?(span_info, :start_time) do
%{
monotonic_time: monotonic_time,
duration: monotonic_time - span_info.start_time,
wallclock_time: now
}
else
%{
monotonic_time: monotonic_time,
wallclock_time: now
}
end
|> Map.merge(metrics)
|> Map.merge(measurements)
metadata =
span_info
|> Map.get(:metadata, %{})
|> Map.merge(metadata)
:telemetry.execute([:wayfarer, :health_check, event], measurements, metadata)
span_info =
span_info
|> Map.put(:metadata, metadata)
|> Map.put_new(:start_time, monotonic_time)
Map.put(check, :span, span_info)
end
end

View file

@ -5,7 +5,8 @@ defmodule Wayfarer.Utils do
@type address_input :: IP.Address.t() | String.t() | :inet.ip_address()
@type port_number :: 1..0xFFFF
@type scheme :: :http | :https
@type scheme :: :http | :https | :ws | :wss
@type transport :: :http1 | :http2 | :auto
@doc """
Verify an IP address and convert it into a tuple.
@ -46,7 +47,7 @@ defmodule Wayfarer.Utils do
Verify a scheme.
"""
@spec sanitise_scheme(scheme) :: {:ok, scheme} | {:error, any}
def sanitise_scheme(scheme) when scheme in [:http, :https], do: {:ok, scheme}
def sanitise_scheme(scheme) when scheme in [:http, :https, :ws, :wss], do: {:ok, scheme}
def sanitise_scheme(scheme),
do:
@ -58,7 +59,7 @@ defmodule Wayfarer.Utils do
@doc """
Convert a scheme, address, port tuple into a `URI`.
"""
@spec to_uri(:http | :https, address_input, port_number) :: {:ok, URI.t()} | {:error, any}
@spec to_uri(scheme, address_input, port_number) :: {:ok, URI.t()} | {:error, any}
def to_uri(scheme, address, port) do
with {:ok, scheme} <- sanitise_scheme(scheme),
{:ok, address} <- sanitise_ip_address(address),
@ -76,7 +77,7 @@ defmodule Wayfarer.Utils do
@doc """
Convert a list of targets into a match spec guard.
"""
@spec targets_to_ms_guard(atom, [{:http | :https, address_input, port_number}]) :: [
@spec targets_to_ms_guard(atom, [{scheme, :inet.ip_address(), port_number, transport}]) :: [
{atom, any, any}
]
def targets_to_ms_guard(_var, []), do: []
@ -92,11 +93,8 @@ defmodule Wayfarer.Utils do
@doc """
Convert a target tuple into a tuple safe for injection into a match spec.
"""
@spec target_to_ms({:http | :https, address_input, port_number}) ::
{{:http | :https, address_input, port_number}}
| {{:http | :https, {:inet.ip_address()}, port_number}}
def target_to_ms({scheme, address, port}) when is_tuple(address),
do: {{scheme, {address}, port}}
def target_to_ms({scheme, address, port}), do: {{scheme, address, port}}
@spec target_to_ms({scheme, :inet.ip_address(), port_number, transport}) ::
{{scheme, {:inet.ip_address()}, port_number, transport}}
def target_to_ms({scheme, address, port, transport}) when is_tuple(address),
do: {{scheme, {address}, port, transport}}
end

26
mix.exs
View file

@ -5,7 +5,7 @@ defmodule Wayfarer.MixProject do
A runtime-configurable HTTP reverse proxy based on Bandit.
"""
@version "0.4.0"
@version "0.6.1"
def project do
[
@ -18,17 +18,16 @@ defmodule Wayfarer.MixProject do
deps: deps(),
description: @moduledoc,
package: package(),
source_url: "https://code.harton.nz/james/wayfarer",
homepage_url: "https://code.harton.nz/james/wayfarer",
source_url: "https://harton.dev/james/wayfarer",
homepage_url: "https://harton.dev/james/wayfarer",
aliases: aliases(),
dialyzer: [plt_add_apps: []],
dialyzer: [plt_ignore_apps: [:mint]],
docs: [
main: "Wayfarer",
formatters: ["html"],
extra_section: "GUIDES",
filter_modules: ~r/^Elixir.Wayfarer/,
source_url_pattern:
"https://code.harton.nz/james/wayfarer/src/branch/main/%{path}#L%{line}",
source_url_pattern: "https://harton.dev/james/wayfarer/src/branch/main/%{path}#L%{line}",
spark: [
extensions: [
%{
@ -71,9 +70,12 @@ defmodule Wayfarer.MixProject do
maintainers: ["James Harton <james@harton.nz>"],
licenses: ["HL3-FULL"],
links: %{
"Source" => "https://code.harton.nz/james/wayfarer"
"Source" => "https://harton.dev/james/wayfarer",
"GitHub" => "https://github.com/jimsynz/wayfarer",
"Changelog" => "https://docs.harton.nz/james/wayfarer/changelog.html",
"Sponsor" => "https://github.com/sponsors/jimsynz"
},
source_url: "https://code.harton.nz/james/wayfarer"
source_url: "https://harton.dev/james/wayfarer"
]
end
@ -94,20 +96,22 @@ defmodule Wayfarer.MixProject do
{:castore, "~> 1.0"},
{:ip, "~> 2.0"},
{:mint, "~> 1.5"},
{:mint_web_socket, "~> 1.0"},
{:nimble_options, "~> 1.0"},
{:plug, "~> 1.15"},
{:spark, "~> 1.1"},
{:spark, "~> 2.0"},
{:telemetry, "~> 1.2"},
{:websock, "~> 0.5"},
{:websock_adapter, "~> 0.5"},
# Dev/test
{:credo, "~> 1.7", opts},
{:dialyxir, "~> 1.3", opts},
{:doctor, "~> 0.21", opts},
{:earmark, ">= 0.0.0", opts},
{:ex_check, "~> 0.15", opts},
{:ex_check, "~> 0.16", opts},
{:ex_doc, ">= 0.0.0", opts},
{:faker, "~> 0.17", opts},
{:faker, "~> 0.18", opts},
{:git_ops, "~> 2.6", opts},
{:mimic, "~> 1.7", Keyword.delete(opts, :runtime)},
{:mix_audit, "~> 2.1", opts}

View file

@ -1,39 +1,49 @@
%{
"bandit": {:hex, :bandit, "1.1.0", "1414e65916229d4ee0914f6d4e7f8ec16c6f2d90e01ad5174d89e90baa577625", [:mix], [{:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "4891fb2f48a83445da70a4e949f649a9b4032310f1f640f4a8a372bc91cece18"},
"bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"},
"castore": {:hex, :castore, "1.0.4", "ff4d0fb2e6411c0479b1d965a814ea6d00e51eb2f58697446e9c41a97d940b28", [:mix], [], "hexpm", "9418c1b8144e11656f0be99943db4caf04612e3eaecefb5dae9a2a87565584f8"},
"credo": {:hex, :credo, "1.7.1", "6e26bbcc9e22eefbff7e43188e69924e78818e2fe6282487d0703652bc20fd62", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "e9871c6095a4c0381c89b6aa98bc6260a8ba6addccf7f6a53da8849c748a58a2"},
"bandit": {:hex, :bandit, "1.5.7", "6856b1e1df4f2b0cb3df1377eab7891bec2da6a7fd69dc78594ad3e152363a50", [:mix], [{:hpax, "~> 1.0.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "f2dd92ae87d2cbea2fa9aa1652db157b6cba6c405cb44d4f6dd87abba41371cd"},
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
"castore": {:hex, :castore, "1.0.8", "dedcf20ea746694647f883590b82d9e96014057aff1d44d03ec90f36a5c0dc6e", [:mix], [], "hexpm", "0b2b66d2ee742cb1d9cb8c8be3b43c3a70ee8651f37b75a8b982e036752983f1"},
"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"},
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
"dialyxir": {:hex, :dialyxir, "1.4.1", "a22ed1e7bd3a3e3f197b68d806ef66acb61ee8f57b3ac85fc5d57354c5482a93", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "84b795d6d7796297cca5a3118444b80c7d94f7ce247d49886e7c291e1ae49801"},
"dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"},
"doctor": {:hex, :doctor, "0.21.0", "20ef89355c67778e206225fe74913e96141c4d001cb04efdeba1a2a9704f1ab5", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "a227831daa79784eb24cdeedfa403c46a4cb7d0eab0e31232ec654314447e4e0"},
"earmark": {:hex, :earmark, "1.4.46", "8c7287bd3137e99d26ae4643e5b7ef2129a260e3dcf41f251750cb4563c8fb81", [:mix], [], "hexpm", "798d86db3d79964e759ddc0c077d5eb254968ed426399fbf5a62de2b5ff8910a"},
"earmark_parser": {:hex, :earmark_parser, "1.4.37", "2ad73550e27c8946648b06905a57e4d454e4d7229c2dafa72a0348c99d8be5f7", [:mix], [], "hexpm", "6b19783f2802f039806f375610faa22da130b8edc21209d0bff47918bb48360e"},
"earmark": {:hex, :earmark, "1.4.47", "7e7596b84fe4ebeb8751e14cbaeaf4d7a0237708f2ce43630cfd9065551f94ca", [:mix], [], "hexpm", "3e96bebea2c2d95f3b346a7ff22285bc68a99fbabdad9b655aa9c6be06c698f8"},
"earmark_parser": {:hex, :earmark_parser, "1.4.40", "f3534689f6b58f48aa3a9ac850d4f05832654fe257bf0549c08cc290035f70d5", [:mix], [], "hexpm", "cdb34f35892a45325bad21735fadb88033bcb7c4c296a999bde769783f53e46a"},
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
"ex_check": {:hex, :ex_check, "0.15.0", "074b94c02de11c37bba1ca82ae5cc4926e6ccee862e57a485b6ba60fca2d8dc1", [:mix], [], "hexpm", "33848031a0c7e4209c3b4369ce154019788b5219956220c35ca5474299fb6a0e"},
"ex_doc": {:hex, :ex_doc, "0.30.6", "5f8b54854b240a2b55c9734c4b1d0dd7bdd41f71a095d42a70445c03cf05a281", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "bd48f2ddacf4e482c727f9293d9498e0881597eae6ddc3d9562bd7923375109f"},
"faker": {:hex, :faker, "0.17.0", "671019d0652f63aefd8723b72167ecdb284baf7d47ad3a82a15e9b8a6df5d1fa", [:mix], [], "hexpm", "a7d4ad84a93fd25c5f5303510753789fc2433ff241bf3b4144d3f6f291658a6a"},
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
"ex_check": {:hex, :ex_check, "0.16.0", "07615bef493c5b8d12d5119de3914274277299c6483989e52b0f6b8358a26b5f", [:mix], [], "hexpm", "4d809b72a18d405514dda4809257d8e665ae7cf37a7aee3be6b74a34dec310f5"},
"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"},
"faker": {:hex, :faker, "0.18.0", "943e479319a22ea4e8e39e8e076b81c02827d9302f3d32726c5bf82f430e6e14", [:mix], [], "hexpm", "bfbdd83958d78e2788e99ec9317c4816e651ad05e24cfd1196ce5db5b3e81797"},
"file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"},
"git_cli": {:hex, :git_cli, "0.3.0", "a5422f9b95c99483385b976f5d43f7e8233283a47cda13533d7c16131cb14df5", [:mix], [], "hexpm", "78cb952f4c86a41f4d3511f1d3ecb28edb268e3a7df278de2faa1bd4672eaf9b"},
"git_ops": {:hex, :git_ops, "2.6.0", "e0791ee1cf5db03f2c61b7ebd70e2e95cba2bb9b9793011f26609f22c0900087", [:mix], [{:git_cli, "~> 0.2", [hex: :git_cli, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "b98fca849b18aaf490f4ac7d1dd8c6c469b0cc3e6632562d366cab095e666ffe"},
"hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"},
"ip": {:hex, :ip, "2.0.1", "c0b77da86e62cb28ae85132e84881bdb6e067be4a74de74a95db6637482b03e9", [:mix], [], "hexpm", "431d9cca05c4f835d592c3b1e59b44910e28437698e4f965119f50b78347b4fd"},
"jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},
"makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"},
"makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [: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", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"},
"makeup_erlang": {:hex, :makeup_erlang, "0.1.2", "ad87296a092a46e03b7e9b0be7631ddcf64c790fa68a9ef5323b6cbb36affc72", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f3f5a1ca93ce6e092d92b6d9c049bcda58a3b617a8d888f8e7231c85630e8108"},
"mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
"mimic": {:hex, :mimic, "1.7.4", "cd2772ffbc9edefe964bc668bfd4059487fa639a5b7f1cbdf4fd22946505aa4f", [:mix], [], "hexpm", "437c61041ecf8a7fae35763ce89859e4973bb0666e6ce76d75efc789204447c3"},
"mint": {:hex, :mint, "1.5.1", "8db5239e56738552d85af398798c80648db0e90f343c8469f6c6d8898944fb6f", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "4a63e1e76a7c3956abd2c72f370a0d0aecddc3976dea5c27eccbecfa5e7d5b1e"},
"mix_audit": {:hex, :mix_audit, "2.1.1", "653aa6d8f291fc4b017aa82bdb79a4017903902ebba57960ef199cbbc8c008a1", [:make, :mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.9", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "541990c3ab3a7bb8c4aaa2ce2732a4ae160ad6237e5dcd5ad1564f4f85354db1"},
"nimble_options": {:hex, :nimble_options, "1.0.2", "92098a74df0072ff37d0c12ace58574d26880e522c22801437151a159392270e", [:mix], [], "hexpm", "fd12a8db2021036ce12a309f26f564ec367373265b53e25403f0ee697380f1b8"},
"nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"},
"plug": {:hex, :plug, "1.15.1", "b7efd81c1a1286f13efb3f769de343236bd8b7d23b4a9f40d3002fc39ad8f74c", [: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", "459497bd94d041d98d948054ec6c0b76feacd28eec38b219ca04c0de13c79d30"},
"plug_crypto": {:hex, :plug_crypto, "2.0.0", "77515cc10af06645abbfb5e6ad7a3e9714f805ae118fa1a70205f80d2d70fe73", [:mix], [], "hexpm", "53695bae57cc4e54566d993eb01074e4d894b65a3766f1c43e2c61a1b0f45ea9"},
"sourceror": {:hex, :sourceror, "0.14.0", "b6b8552d0240400d66b6f107c1bab7ac1726e998efc797f178b7b517e928e314", [:mix], [], "hexpm", "809c71270ad48092d40bbe251a133e49ae229433ce103f762a2373b7a10a8d8b"},
"spark": {:hex, :spark, "1.1.48", "64b804711818526e371d12ea3acc886365b14239565e361001aad801a38bad85", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.5 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:sourceror, "~> 0.1", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "3215a8b1bb1dc93945ce9a0f68430d7265ea596c6b911f7bd6dba77b65cee370"},
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
"thousand_island": {:hex, :thousand_island, "1.1.0", "dcc115650adc61c5e7de12619f0cb94b2b8f050326e7f21ffbf6fdeb3d291e4c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7745cf71520d74e119827ff32c2da6307e822cf835bebed3b2c459cc57f32d21"},
"git_ops": {:hex, :git_ops, "2.6.1", "cc7799a68c26cf814d6d1a5121415b4f5bf813de200908f930b27a2f1fe9dad5", [:mix], [{:git_cli, "~> 0.2", [hex: :git_cli, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "ce62d07e41fe993ec22c35d5edb11cf333a21ddaead6f5d9868fcb607d42039e"},
"glob_ex": {:hex, :glob_ex, "0.1.8", "f7ef872877ca2ae7a792ab1f9ff73d9c16bf46ecb028603a8a3c5283016adc07", [:mix], [], "hexpm", "9e39d01729419a60a937c9260a43981440c43aa4cadd1fa6672fecd58241c464"},
"ham": {:hex, :ham, "0.3.0", "7cd031b4a55fba219c11553e7b13ba73bd86eab4034518445eff1e038cb9a44d", [:mix], [], "hexpm", "7d6c6b73d7a6a83233876cc1b06a4d9b5de05562b228effda4532f9a49852bf6"},
"hpax": {:hex, :hpax, "1.0.0", "28dcf54509fe2152a3d040e4e3df5b265dcb6cb532029ecbacf4ce52caea3fd2", [:mix], [], "hexpm", "7f1314731d711e2ca5fdc7fd361296593fc2542570b3105595bb0bc6d0fad601"},
"igniter": {:hex, :igniter, "0.3.24", "791a91650ffab9d66b9a3011c66491f767577ad55c363f820cc188554207ee6f", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:inflex, "~> 2.0", [hex: :inflex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:owl, "~> 0.9", [hex: :owl, repo: "hexpm", optional: false]}, {:rewrite, "~> 0.9", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}, {:ucwidth, "~> 0.2", [hex: :ucwidth, repo: "hexpm", optional: false]}], "hexpm", "2e1d336534c6129bae0db043fae650303b96974c0488c290191d6d4c61ec9a9f"},
"inflex": {:hex, :inflex, "2.1.0", "a365cf0821a9dacb65067abd95008ca1b0bb7dcdd85ae59965deef2aa062924c", [:mix], [], "hexpm", "14c17d05db4ee9b6d319b0bff1bdf22aa389a25398d1952c7a0b5f3d93162dd8"},
"ip": {:hex, :ip, "2.0.3", "290d71c05b79ad62c99d8fe175e86130dc120489d119b8c2819cec16bad3c77c", [:mix], [], "hexpm", "19fa2f9c6f5cb288ca2192499888bd96f88af3564eaa7bbcfc1231ffdc5df8c2"},
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
"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"},
"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"},
"makeup_erlang": {:hex, :makeup_erlang, "1.0.0", "6f0eff9c9c489f26b69b61440bf1b238d95badae49adac77973cbacae87e3c2e", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "ea7a9307de9d1548d2a72d299058d1fd2339e3d398560a0e46c27dab4891e4d2"},
"mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"},
"mimic": {:hex, :mimic, "1.10.0", "58ee13aa46addcadbb033ce311bb5ed8b0a825c2dec6e1d55ca767138a6374c8", [:mix], [{:ham, "~> 0.2", [hex: :ham, repo: "hexpm", optional: false]}], "hexpm", "ea639e48f6a5bd043218297b80c3a52e227541aafa3dc8a299cc0c01991523a5"},
"mint": {:hex, :mint, "1.6.2", "af6d97a4051eee4f05b5500671d47c3a67dac7386045d87a904126fd4bbcea2e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5ee441dffc1892f1ae59127f74afe8fd82fda6587794278d924e4d90ea3d63f9"},
"mint_web_socket": {:hex, :mint_web_socket, "1.0.4", "0b539116dbb3d3f861cdf5e15e269a933cb501c113a14db7001a3157d96ffafd", [:mix], [{:mint, ">= 1.4.1 and < 2.0.0-0", [hex: :mint, repo: "hexpm", optional: false]}], "hexpm", "027d4c5529c45a4ba0ce27a01c0f35f284a5468519c045ca15f43decb360a991"},
"mix_audit": {:hex, :mix_audit, "2.1.4", "0a23d5b07350cdd69001c13882a4f5fb9f90fbd4cbf2ebc190a2ee0d187ea3e9", [:make, :mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "fd807653cc8c1cada2911129c7eb9e985e3cc76ebf26f4dd628bb25bbcaa7099"},
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
"nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
"owl": {:hex, :owl, "0.11.0", "2cd46185d330aa2400f1c8c3cddf8d2ff6320baeff23321d1810e58127082cae", [:mix], [{:ucwidth, "~> 0.2", [hex: :ucwidth, repo: "hexpm", optional: true]}], "hexpm", "73f5783f0e963cc04a061be717a0dbb3e49ae0c4bfd55fb4b78ece8d33a65efe"},
"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"},
"plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"},
"rewrite": {:hex, :rewrite, "0.10.5", "6afadeae0b9d843b27ac6225e88e165884875e0aed333ef4ad3bf36f9c101bed", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "51cc347a4269ad3a1e7a2c4122dbac9198302b082f5615964358b4635ebf3d4f"},
"sourceror": {:hex, :sourceror, "1.6.0", "9907884e1449a4bd7dbaabe95088ed4d9a09c3c791fb0103964e6316bc9448a7", [:mix], [], "hexpm", "e90aef8c82dacf32c89c8ef83d1416fc343cd3e5556773eeffd2c1e3f991f699"},
"spark": {:hex, :spark, "2.2.23", "78f0a1b0b713a91ad556fe9dc19ec92d977aaa0803cce2e255d90e58b9045c2a", [:mix], [{:igniter, ">= 0.2.6 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.2", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "a354b5cd7c3f021e3cd1da5a033b7643fe7b3c71c96b96d9f500a742f40c94db"},
"spitfire": {:hex, :spitfire, "0.1.3", "7ea0f544005dfbe48e615ed90250c9a271bfe126914012023fd5e4b6b82b7ec7", [:mix], [], "hexpm", "d53b5107bcff526a05c5bb54c95e77b36834550affd5830c9f58760e8c543657"},
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
"thousand_island": {:hex, :thousand_island, "1.3.5", "6022b6338f1635b3d32406ff98d68b843ba73b3aa95cfc27154223244f3a6ca5", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2be6954916fdfe4756af3239fb6b6d75d0b8063b5df03ba76fd8a4c87849e180"},
"ucwidth": {:hex, :ucwidth, "0.2.0", "1f0a440f541d895dff142275b96355f7e91e15bca525d4a0cc788ea51f0e3441", [:mix], [], "hexpm", "c1efd1798b8eeb11fb2bec3cafa3dd9c0c3647bee020543f0340b996177355bf"},
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
"websock_adapter": {:hex, :websock_adapter, "0.5.7", "65fa74042530064ef0570b75b43f5c49bb8b235d6515671b3d250022cb8a1f9e", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "d0f478ee64deddfec64b800673fd6e0c8888b079d9f3444dd96d2a98383bdbd1"},
"yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"},
"yaml_elixir": {:hex, :yaml_elixir, "2.9.0", "9a256da867b37b8d2c1ffd5d9de373a4fda77a32a45b452f1708508ba7bbcb53", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "0cb0e7d4c56f5e99a6253ed1a670ed0e39c13fc45a6da054033928607ac08dfc"},
"yaml_elixir": {:hex, :yaml_elixir, "2.11.0", "9e9ccd134e861c66b84825a3542a1c22ba33f338d82c07282f4f1f52d847bd50", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "53cc28357ee7eb952344995787f4bb8cc3cecbf189652236e9b163e8ce1bc242"},
}

View file

@ -4,25 +4,22 @@ defmodule Support.Example do
config "Example" do
listeners do
# http "127.0.0.1", 8080
http "0.0.0.0", 8080
http "0.0.0.0", 8000
end
targets do
http "127.0.0.1", 8082
http "192.168.4.26", 80
http "127.0.0.1", 4000
end
health_checks do
check do
interval :timer.seconds(5)
success_codes 200..399
end
end
host_patterns do
pattern "*.example.com"
pattern "example.com"
pattern "localhost"
end
end
end

View file

@ -48,7 +48,7 @@ defmodule Wayfarer.RouterTest do
{:ok, table} = Router.init(Support.Example)
listener = {:http, {127, 0, 0, 1}, random_port()}
target = {:http, {127, 0, 0, 1}, random_port()}
target = {:http, {127, 0, 0, 1}, random_port(), :auto}
assert :ok = Router.add_route(table, listener, target, ["*.example.com"], :round_robin)
@ -61,7 +61,7 @@ defmodule Wayfarer.RouterTest do
{:ok, table} = Router.init(Support.Example)
listener = {:http, {127, 0, 0, 1}, random_port()}
target = {:http, {127, 0, 0, 1}, random_port()}
target = {:http, {127, 0, 0, 1}, random_port(), :auto}
assert :ok = Router.add_route(table, listener, target, ["www.example.com"], :round_robin)
@ -74,7 +74,7 @@ defmodule Wayfarer.RouterTest do
{:ok, table} = Router.init(Support.Example)
listener = {:http, {127, 0, 0, 1}, random_port()}
target = {:http, {127, 0, 0, 1}, random_port()}
target = {:http, {127, 0, 0, 1}, random_port(), :auto}
assert :ok =
Router.add_route(
@ -114,9 +114,9 @@ defmodule Wayfarer.RouterTest do
{:ok, table} = Router.init(Support.Example)
listener0 = {:http, {127, 0, 0, 1}, random_port()}
target0 = {:http, {127, 0, 0, 1}, random_port()}
target0 = {:http, {127, 0, 0, 1}, random_port(), :auto}
listener1 = {:http, {127, 0, 0, 1}, random_port()}
target1 = {:http, {127, 0, 0, 1}, random_port()}
target1 = {:http, {127, 0, 0, 1}, random_port(), :auto}
assert :ok =
Router.import_routes(table, [
@ -142,9 +142,9 @@ defmodule Wayfarer.RouterTest do
{:ok, table} = Router.init(Support.Example)
listener0 = {:http, {127, 0, 0, 1}, random_port()}
target0 = {:http, {127, 0, 0, 1}, random_port()}
target0 = {:http, {127, 0, 0, 1}, random_port(), :auto}
listener1 = {:http, {127, 0, 0, 1}, random_port()}
target1 = {:http, {127, 0, 0, 1}, random_port()}
target1 = {:http, {127, 0, 0, 1}, random_port(), :auto}
Router.import_routes(table, [
{listener0, target0, ["0.example.com"], :round_robin},
@ -164,9 +164,9 @@ defmodule Wayfarer.RouterTest do
{:ok, table} = Router.init(Support.Example)
listener0 = {:http, {127, 0, 0, 1}, random_port()}
target0 = {:http, {127, 0, 0, 1}, random_port()}
target0 = {:http, {127, 0, 0, 1}, random_port(), :auto}
listener1 = {:http, {127, 0, 0, 1}, random_port()}
target1 = {:http, {127, 0, 0, 1}, random_port()}
target1 = {:http, {127, 0, 0, 1}, random_port(), :auto}
Router.import_routes(table, [
{listener0, target0, ["0.example.com"], :round_robin},
@ -186,9 +186,9 @@ defmodule Wayfarer.RouterTest do
{:ok, table} = Router.init(Support.Example)
listener0 = {:http, {127, 0, 0, 1}, random_port()}
target0 = {:http, {127, 0, 0, 1}, random_port()}
target0 = {:http, {127, 0, 0, 1}, random_port(), :auto}
listener1 = {:http, {127, 0, 0, 1}, random_port()}
target1 = {:http, {127, 0, 0, 1}, random_port()}
target1 = {:http, {127, 0, 0, 1}, random_port(), :auto}
Router.import_routes(table, [
{listener0, target0, ["0.example.com"], :round_robin},
@ -215,8 +215,8 @@ defmodule Wayfarer.RouterTest do
{:ok, table} = Router.init(Support.Example)
listener = {:http, {127, 0, 0, 1}, random_port()}
target0 = {:http, {127, 0, 0, 1}, random_port()}
target1 = {:http, {127, 0, 0, 1}, random_port()}
target0 = {:http, {127, 0, 0, 1}, random_port(), :auto}
target1 = {:http, {127, 0, 0, 1}, random_port(), :auto}
:ok =
Router.import_routes(table, [

View file

@ -1,6 +1,6 @@
defmodule Wayfarer.Server.PlugTest do
@moduledoc false
use ExUnit.Case, async: true
use ExUnit.Case, async: false
use Mimic
use Plug.Test
use Support.PortTracker
@ -52,7 +52,7 @@ defmodule Wayfarer.Server.PlugTest do
test "it looks for healthy targets in the router" do
listener = {:http, {127, 0, 0, 1}, random_port()}
target = {:http, {127, 0, 0, 1}, random_port()}
target = {:http, {127, 0, 0, 1}, random_port(), :auto}
{:ok, _} = HttpServer.start_link(elem(target, 2), 200, "OK")
@ -76,7 +76,7 @@ defmodule Wayfarer.Server.PlugTest do
test "it selects a target to proxy to" do
listener = {:http, {127, 0, 0, 1}, random_port()}
target = {:http, {127, 0, 0, 1}, random_port()}
target = {:http, {127, 0, 0, 1}, random_port(), :auto}
{:ok, _} = HttpServer.start_link(elem(target, 2), 200, "OK")

View file

@ -49,17 +49,18 @@ defmodule Wayfarer.Server.ProxyTest do
HTTP
|> stub(:connect, fn _, _, _, _ -> {:ok, :fake_conn} end)
|> stub(:stream_request_body, fn mint, _, _ -> {:ok, mint} end)
|> stub(:request, fn mint, _, _, _, _ ->
send(self(), :ignore)
{:ok, mint, req}
end)
|> stub(:stream, fn mint, :ignore -> {:ok, mint, responses} end)
|> stub(:stream, fn mint, _ -> {:ok, mint, [{:done, req}]} end)
|> stub(:stream, fn mint, _ -> {:ok, mint, [{:status, req, 200}, {:done, req}]} end)
end
describe "request/2" do
test "it opens an HTTP connection to the target and sends the request", %{conn: conn} do
target = {:http, {127, 0, 0, 1}, random_port()}
target = {:http, {127, 0, 0, 1}, random_port(), :auto}
req = make_ref()
HTTP
@ -72,12 +73,13 @@ defmodule Wayfarer.Server.ProxyTest do
{:ok, :fake_conn}
end)
|> expect(:stream_request_body, fn mint, _, _ -> {:ok, mint} end)
|> expect(:request, fn mint, _, _, _, _ ->
send(self(), :ignore)
{:ok, mint, req}
end)
|> expect(:stream, fn mint, :ignore ->
{:ok, mint, [{:done, req}]}
{:ok, mint, [{:status, req, 200}, {:done, req}]}
end)
assert conn = Proxy.request(conn, target)
@ -85,7 +87,7 @@ defmodule Wayfarer.Server.ProxyTest do
end
test "it records the outgoing connection", %{conn: conn} do
target = {:http, {127, 0, 0, 1}, random_port()}
target = {:http, {127, 0, 0, 1}, random_port(), :auto}
ActiveConnections
|> expect(:connect, fn ^target -> :ok end)
@ -103,7 +105,7 @@ defmodule Wayfarer.Server.ProxyTest do
{:error, %Mint.TransportError{reason: :protocol_not_negotiated}}
end)
target = {:http, {127, 0, 0, 1}, random_port()}
target = {:http, {127, 0, 0, 1}, random_port(), :auto}
assert conn = Proxy.request(conn, target)
assert conn.status == 502
end
@ -116,7 +118,7 @@ defmodule Wayfarer.Server.ProxyTest do
{:error, %Mint.TransportError{reason: :timeout}}
end)
target = {:http, {127, 0, 0, 1}, random_port()}
target = {:http, {127, 0, 0, 1}, random_port(), :auto}
assert conn = Proxy.request(conn, target)
assert conn.status == 504
end
@ -141,7 +143,7 @@ defmodule Wayfarer.Server.ProxyTest do
{:error, :ignore}
end)
target = {:http, {127, 0, 0, 1}, random_port()}
target = {:http, {127, 0, 0, 1}, random_port(), :auto}
Proxy.request(conn, target)
end
@ -176,7 +178,7 @@ defmodule Wayfarer.Server.ProxyTest do
{:error, :ignore}
end)
target = {:http, {127, 0, 0, 1}, random_port()}
target = {:http, {127, 0, 0, 1}, random_port(), :auto}
Proxy.request(conn, target)
end
@ -205,7 +207,7 @@ defmodule Wayfarer.Server.ProxyTest do
{:error, :ignore}
end)
target = {:http, {127, 0, 0, 1}, random_port()}
target = {:http, {127, 0, 0, 1}, random_port(), :auto}
Proxy.request(conn, target)
end
end

View file

@ -54,7 +54,7 @@ defmodule Wayfarer.ServerTest do
routing_table: [
{
{:http, ~i"127.0.0.1", listen_port},
{:http, ~i"127.0.0.1", target_port},
{:http, ~i"127.0.0.1", target_port, :auto},
["example.com"],
:round_robin
}
@ -63,7 +63,7 @@ defmodule Wayfarer.ServerTest do
assert :ets.tab2list(Support.Example) == [
{{:http, {127, 0, 0, 1}, listen_port}, {"example", "com"},
{:http, {127, 0, 0, 1}, target_port}, :round_robin, :initial}
{:http, {127, 0, 0, 1}, target_port, :auto}, :round_robin, :initial}
]
end
end

View file

@ -1,6 +1,6 @@
defmodule Wayfarer.Target.SelectorTest do
@moduledoc false
use ExUnit.Case, async: true
use ExUnit.Case, async: false
use Plug.Test
use Mimic
use Support.PortTracker

View file

@ -51,9 +51,9 @@ defmodule WayfarerTest do
]
],
routing_table: [
{{:http, ~i"127.0.0.1", listener_port}, {:http, ~i"127.0.0.1", target0_port},
{{:http, ~i"127.0.0.1", listener_port}, {:http, ~i"127.0.0.1", target0_port, :auto},
["www.example.com"], :round_robin},
{{:http, ~i"127.0.0.1", listener_port}, {:http, ~i"127.0.0.1", target1_port},
{{:http, ~i"127.0.0.1", listener_port}, {:http, ~i"127.0.0.1", target1_port, :auto},
["www.example.com"], :round_robin}
]
)
@ -69,8 +69,8 @@ defmodule WayfarerTest do
assert [1, 1] =
[
{:http, {127, 0, 0, 1}, target0_port},
{:http, {127, 0, 0, 1}, target1_port}
{:http, {127, 0, 0, 1}, target0_port, :auto},
{:http, {127, 0, 0, 1}, target1_port, :auto}
]
|> TotalConnections.proxy_count()
|> Enum.map(&elem(&1, 1))
@ -85,8 +85,8 @@ defmodule WayfarerTest do
assert [11, 11] =
[
{:http, {127, 0, 0, 1}, target0_port},
{:http, {127, 0, 0, 1}, target1_port}
{:http, {127, 0, 0, 1}, target0_port, :auto},
{:http, {127, 0, 0, 1}, target1_port, :auto}
]
|> TotalConnections.proxy_count()
|> Enum.map(&elem(&1, 1))