mirror of
https://github.com/h44z/wg-portal.git
synced 2026-07-14 12:38:21 +00:00
Compare commits
44 Commits
oauth_pkce
...
stable
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27d1648699 | ||
|
|
c800e7dd34 | ||
|
|
012013c188 | ||
|
|
1a541d7bd4 | ||
|
|
9dc87dda4f | ||
|
|
ea3742c193 | ||
|
|
de2f7c6835 | ||
|
|
d8da5ff95a | ||
|
|
316f389f11 | ||
|
|
e3dc31a133 | ||
|
|
dea358c8cf | ||
|
|
72cfd1d8a9 | ||
|
|
1517041363 | ||
|
|
a5bb2042aa | ||
|
|
8fd2721345 | ||
|
|
8fe50bf7dd | ||
|
|
1c3eacb08d | ||
|
|
cc472216b4 | ||
|
|
95394628d3 | ||
|
|
b553375c43 | ||
|
|
0a8ec71b3f | ||
|
|
fe4485037a | ||
|
|
6e47d8c3e9 | ||
|
|
eb28492539 | ||
|
|
d1a4ddde10 | ||
|
|
b1637b0c4e | ||
|
|
0cc7ebb83e | ||
|
|
eb6a787cfc | ||
|
|
b546eec4ed | ||
|
|
9be2133220 | ||
|
|
b05837b2d9 | ||
|
|
08c8f8eac0 | ||
|
|
d864e24145 | ||
|
|
5b56e58fe9 | ||
|
|
930ef7b573 | ||
|
|
18296673d7 | ||
|
|
4ccc59c109 | ||
|
|
e6b01a9903 | ||
|
|
2f79dd04c0 | ||
|
|
e5ed9736b3 | ||
|
|
c8353b85ae | ||
|
|
6142031387 | ||
|
|
dd86d0ff49 | ||
|
|
bdd426a679 |
@@ -135,7 +135,8 @@ func main() {
|
|||||||
apiV0EndpointPeers := handlersV0.NewPeerEndpoint(cfg, apiV0Auth, validatorManager, apiV0BackendPeers)
|
apiV0EndpointPeers := handlersV0.NewPeerEndpoint(cfg, apiV0Auth, validatorManager, apiV0BackendPeers)
|
||||||
apiV0EndpointConfig := handlersV0.NewConfigEndpoint(cfg, apiV0Auth, wireGuard)
|
apiV0EndpointConfig := handlersV0.NewConfigEndpoint(cfg, apiV0Auth, wireGuard)
|
||||||
apiV0EndpointTest := handlersV0.NewTestEndpoint(apiV0Auth)
|
apiV0EndpointTest := handlersV0.NewTestEndpoint(apiV0Auth)
|
||||||
apiV0EndpointWebsocket := handlersV0.NewWebsocketEndpoint(cfg, apiV0Auth, eventBus)
|
apiV0EndpointWebsocket := handlersV0.NewWebsocketEndpoint(cfg, apiV0Auth, eventBus, apiV0BackendPeers)
|
||||||
|
apiV0EndpointWebsocket.StartBackgroundJobs(ctx)
|
||||||
|
|
||||||
apiFrontend := handlersV0.NewRestApi(apiV0Session,
|
apiFrontend := handlersV0.NewRestApi(apiV0Session,
|
||||||
apiV0EndpointAuth,
|
apiV0EndpointAuth,
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ auth:
|
|||||||
extra_scopes:
|
extra_scopes:
|
||||||
- https://www.googleapis.com/auth/userinfo.email
|
- https://www.googleapis.com/auth/userinfo.email
|
||||||
- https://www.googleapis.com/auth/userinfo.profile
|
- https://www.googleapis.com/auth/userinfo.profile
|
||||||
|
use_pkce: true
|
||||||
|
pkce_method: S256
|
||||||
registration_enabled: true
|
registration_enabled: true
|
||||||
logout_idp_session: true
|
logout_idp_session: true
|
||||||
- id: oidc2
|
- id: oidc2
|
||||||
@@ -79,6 +81,7 @@ auth:
|
|||||||
user_identifier: sub
|
user_identifier: sub
|
||||||
is_admin: this-attribute-must-be-true
|
is_admin: this-attribute-must-be-true
|
||||||
registration_enabled: true
|
registration_enabled: true
|
||||||
|
use_pkce: false
|
||||||
- id: google_plain_oauth_with_groups
|
- id: google_plain_oauth_with_groups
|
||||||
provider_name: google4
|
provider_name: google4
|
||||||
display_name: Login with</br>Google4
|
display_name: Login with</br>Google4
|
||||||
|
|||||||
@@ -552,6 +552,7 @@ Below are the properties for each OIDC provider entry inside `auth.oidc`:
|
|||||||
#### `provider_name`
|
#### `provider_name`
|
||||||
- **Default:** *(empty)*
|
- **Default:** *(empty)*
|
||||||
- **Description:** A **unique** name for this provider. Must not conflict with other providers.
|
- **Description:** A **unique** name for this provider. Must not conflict with other providers.
|
||||||
|
This name is used to derive the callback URL for the OIDC provider: `<external_url>/api/v0/auth/login/<provider_name>/callback`.
|
||||||
|
|
||||||
#### `display_name`
|
#### `display_name`
|
||||||
- **Default:** *(empty)*
|
- **Default:** *(empty)*
|
||||||
@@ -617,6 +618,14 @@ Below are the properties for each OIDC provider entry inside `auth.oidc`:
|
|||||||
- **Description:** If `true`, sensitive OIDC user data, such as tokens and raw responses, will be logged at the trace level upon login (for debugging).
|
- **Description:** If `true`, sensitive OIDC user data, such as tokens and raw responses, will be logged at the trace level upon login (for debugging).
|
||||||
- **Important:** Keep this setting disabled in production environments! Remove logs once you finished debugging authentication issues.
|
- **Important:** Keep this setting disabled in production environments! Remove logs once you finished debugging authentication issues.
|
||||||
|
|
||||||
|
#### `use_pkce`
|
||||||
|
- **Default:** `true`
|
||||||
|
- **Description:** If `true`, Proof Key for Code Exchange (PKCE) is used for the OIDC authorization code flow. A fresh `code_verifier` is generated per login request, the matching `code_challenge` is sent with the authorization request, and the `code_verifier` is included in the token exchange. Set to `false` only for providers that do not support PKCE.
|
||||||
|
|
||||||
|
#### `pkce_method`
|
||||||
|
- **Default:** `S256`
|
||||||
|
- **Description:** PKCE challenge method to use when `use_pkce` is enabled. Supported values are `S256` and `plain`. `S256` is recommended; use `plain` only for providers that explicitly require it.
|
||||||
|
|
||||||
#### `logout_idp_session`
|
#### `logout_idp_session`
|
||||||
- **Default:** `true`
|
- **Default:** `true`
|
||||||
- **Description:** If `true` (default), WireGuard Portal will redirect the user to the OIDC provider's `end_session_endpoint` after local logout, terminating the session at the IdP as well. Set to `false` to only invalidate the local WireGuard Portal session without touching the IdP session.
|
- **Description:** If `true` (default), WireGuard Portal will redirect the user to the OIDC provider's `end_session_endpoint` after local logout, terminating the session at the IdP as well. Set to `false` to only invalidate the local WireGuard Portal session without touching the IdP session.
|
||||||
@@ -631,6 +640,7 @@ Below are the properties for each OAuth provider entry inside `auth.oauth`:
|
|||||||
#### `provider_name`
|
#### `provider_name`
|
||||||
- **Default:** *(empty)*
|
- **Default:** *(empty)*
|
||||||
- **Description:** A **unique** name for this provider. Must not conflict with other providers.
|
- **Description:** A **unique** name for this provider. Must not conflict with other providers.
|
||||||
|
This name is used to derive the callback URL for the OAuth provider: `<external_url>/api/v0/auth/login/<provider_name>/callback`.
|
||||||
|
|
||||||
#### `display_name`
|
#### `display_name`
|
||||||
- **Default:** *(empty)*
|
- **Default:** *(empty)*
|
||||||
@@ -703,6 +713,14 @@ Below are the properties for each OAuth provider entry inside `auth.oauth`:
|
|||||||
- **Description:** If `true`, sensitive OIDC user data, such as tokens and raw responses, will be logged at the trace level upon login (for debugging).
|
- **Description:** If `true`, sensitive OIDC user data, such as tokens and raw responses, will be logged at the trace level upon login (for debugging).
|
||||||
- **Important:** Keep this setting disabled in production environments! Remove logs once you finished debugging authentication issues.
|
- **Important:** Keep this setting disabled in production environments! Remove logs once you finished debugging authentication issues.
|
||||||
|
|
||||||
|
#### `use_pkce`
|
||||||
|
- **Default:** `true`
|
||||||
|
- **Description:** If `true`, Proof Key for Code Exchange (PKCE) is used for the OIDC authorization code flow. A fresh `code_verifier` is generated per login request, the matching `code_challenge` is sent with the authorization request, and the `code_verifier` is included in the token exchange. Set to `false` only for providers that do not support PKCE.
|
||||||
|
|
||||||
|
#### `pkce_method`
|
||||||
|
- **Default:** `S256`
|
||||||
|
- **Description:** PKCE challenge method to use when `use_pkce` is enabled. Supported values are `S256` and `plain`. `S256` is recommended; use `plain` only for providers that explicitly require it.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### LDAP
|
### LDAP
|
||||||
|
|||||||
@@ -51,13 +51,31 @@ sudo install wg-portal /opt/wg-portal/
|
|||||||
To handle tasks such as restarting the service or configuring automatic startup, it is recommended to use a process manager like [systemd](https://systemd.io/).
|
To handle tasks such as restarting the service or configuring automatic startup, it is recommended to use a process manager like [systemd](https://systemd.io/).
|
||||||
Refer to [Systemd Service Setup](#systemd-service-setup) for instructions.
|
Refer to [Systemd Service Setup](#systemd-service-setup) for instructions.
|
||||||
|
|
||||||
## Systemd Service Setup
|
## Systemd Integration
|
||||||
|
|
||||||
> **Note:** To run WireGuard Portal as systemd service, you need to download the binary for your architecture beforehand.
|
> **Note:** To run WireGuard Portal as systemd service, you need to download the binary for your architecture beforehand.
|
||||||
>
|
>
|
||||||
> The following examples assume that you downloaded the binary to `/opt/wg-portal/wg-portal`.
|
> The following examples assume that you downloaded the binary to `/opt/wg-portal/wg-portal`.
|
||||||
> The configuration file is expected to be located at `/opt/wg-portal/config.yml`.
|
> The configuration file is expected to be located at `/opt/wg-portal/config.yml`.
|
||||||
|
|
||||||
|
### Limit Systemd-Networkd Management Scope
|
||||||
|
|
||||||
|
If you are using `systemd-networkd` to manage the rest of your network
|
||||||
|
configuration, you will need to ensure it doesn't remove routing policy
|
||||||
|
created by `wg-portal` when it restarts:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
sudo mkdir --parents /etc/systemd/networkd.conf.d/
|
||||||
|
sudo tee --append /etc/systemd/networkd.conf.d/foreign-routing.conf <<EOF
|
||||||
|
[Network]
|
||||||
|
ManageForeignRoutingPolicyRules=no
|
||||||
|
EOF
|
||||||
|
sudo systemctl restart systemd-networkd.service
|
||||||
|
sudo systemctl status systemd-networkd.service
|
||||||
|
```
|
||||||
|
|
||||||
|
### Wireguard Portal Service Setup
|
||||||
|
|
||||||
To run WireGuard Portal as a systemd service, you can create a service unit file. The easiest way to do this is by using `systemctl edit`:
|
To run WireGuard Portal as a systemd service, you can create a service unit file. The easiest way to do this is by using `systemctl edit`:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
|
|||||||
@@ -51,6 +51,15 @@ To add OIDC or OAuth2 authentication to WireGuard Portal, create a Client-ID and
|
|||||||
configure a new authentication provider in the [`auth`](../configuration/overview.md#auth) section of the configuration file.
|
configure a new authentication provider in the [`auth`](../configuration/overview.md#auth) section of the configuration file.
|
||||||
Make sure that each configured provider has a unique `provider_name` property set. Samples can be seen [here](../configuration/examples.md).
|
Make sure that each configured provider has a unique `provider_name` property set. Samples can be seen [here](../configuration/examples.md).
|
||||||
|
|
||||||
|
When registering the OAuth2 or OIDC application with your provider, configure the callback/redirect URL as follows:
|
||||||
|
|
||||||
|
```text
|
||||||
|
<external_url>/api/v0/auth/login/<provider_name>/callback
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace `<external_url>` with the value configured in [`external_url`](../configuration/overview.md#external_url) and
|
||||||
|
`<provider_name>` with the exact `provider_name` from the matching OAuth2 or OIDC provider configuration.
|
||||||
|
|
||||||
#### Limiting Login to Specific Domains
|
#### Limiting Login to Specific Domains
|
||||||
|
|
||||||
You can limit the login to specific domains by setting the `allowed_domains` property for OAuth2 or OIDC providers.
|
You can limit the login to specific domains by setting the `allowed_domains` property for OAuth2 or OIDC providers.
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ backend:
|
|||||||
|
|
||||||
> :warning: The pfSense backend is currently **alpha**. Only basic interface and peer CRUD are supported. Traffic statistics (rx/tx, last handshake) are not exposed by the pfSense REST API and will show as empty.
|
> :warning: The pfSense backend is currently **alpha**. Only basic interface and peer CRUD are supported. Traffic statistics (rx/tx, last handshake) are not exposed by the pfSense REST API and will show as empty.
|
||||||
|
|
||||||
The pfSense backend talks to the pfSense REST API (pfSense Plus / CE with the REST API package installed). Point the backend at the appliance hostname without appending `/api/v2` — the portal appends `/api/v2` automatically.
|
The pfSense backend talks to the pfSense REST API (pfSense Plus / CE with the REST API package installed). Point the backend at the appliance hostname without appending `/api/v2` — the portal appends `/api/v2` automatically. wg-portal is developed for and tested against REST API v2.8.0.
|
||||||
|
|
||||||
### Prerequisites on pfSense:
|
### Prerequisites on pfSense:
|
||||||
- pfSense with the REST API package enabled (`System -> API`) and WireGuard configured.
|
- pfSense with the REST API package enabled (`System -> API`) and WireGuard configured.
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build-dev": "vite build --mode development --base=/app/",
|
"build-dev": "vite build --mode development",
|
||||||
"build": "vite build --base=/app/",
|
"build": "vite build",
|
||||||
"preview": "vite preview --port 5050"
|
"preview": "vite preview --port 5050"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import vue from '@vitejs/plugin-vue'
|
|||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
base: './',
|
||||||
plugins: [vue()],
|
plugins: [vue()],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
|
|||||||
18
go.mod
18
go.mod
@@ -9,8 +9,8 @@ require (
|
|||||||
github.com/glebarez/sqlite v1.11.0
|
github.com/glebarez/sqlite v1.11.0
|
||||||
github.com/go-ldap/ldap/v3 v3.4.13
|
github.com/go-ldap/ldap/v3 v3.4.13
|
||||||
github.com/go-pkgz/routegroup v1.6.0
|
github.com/go-pkgz/routegroup v1.6.0
|
||||||
github.com/go-playground/validator/v10 v10.30.2
|
github.com/go-playground/validator/v10 v10.30.3
|
||||||
github.com/go-webauthn/webauthn v0.17.3
|
github.com/go-webauthn/webauthn v0.17.4
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/gorilla/websocket v1.5.3
|
github.com/gorilla/websocket v1.5.3
|
||||||
github.com/prometheus-community/pro-bing v0.8.0
|
github.com/prometheus-community/pro-bing v0.8.0
|
||||||
@@ -22,17 +22,17 @@ require (
|
|||||||
github.com/xhit/go-simple-mail/v2 v2.16.0
|
github.com/xhit/go-simple-mail/v2 v2.16.0
|
||||||
github.com/yeqown/go-qrcode/v2 v2.2.5
|
github.com/yeqown/go-qrcode/v2 v2.2.5
|
||||||
github.com/yeqown/go-qrcode/writer/compressed v1.0.1
|
github.com/yeqown/go-qrcode/writer/compressed v1.0.1
|
||||||
golang.org/x/crypto v0.51.0
|
golang.org/x/crypto v0.53.0
|
||||||
golang.org/x/oauth2 v0.36.0
|
golang.org/x/oauth2 v0.36.0
|
||||||
golang.org/x/sys v0.44.0
|
golang.org/x/sys v0.46.0
|
||||||
golang.org/x/text v0.37.0
|
golang.org/x/text v0.38.0
|
||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
gorm.io/driver/mysql v1.6.0
|
gorm.io/driver/mysql v1.6.0
|
||||||
gorm.io/driver/postgres v1.6.0
|
gorm.io/driver/postgres v1.6.0
|
||||||
gorm.io/driver/sqlserver v1.6.3
|
gorm.io/driver/sqlserver v1.6.3
|
||||||
gorm.io/gorm v1.31.1
|
gorm.io/gorm v1.31.1
|
||||||
pgregory.net/rapid v1.2.0
|
pgregory.net/rapid v1.3.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -63,7 +63,7 @@ require (
|
|||||||
github.com/go-sql-driver/mysql v1.10.0 // indirect
|
github.com/go-sql-driver/mysql v1.10.0 // indirect
|
||||||
github.com/go-test/deep v1.1.1 // indirect
|
github.com/go-test/deep v1.1.1 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||||
github.com/go-webauthn/x v0.2.5 // indirect
|
github.com/go-webauthn/x v0.2.6 // indirect
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
|
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
|
||||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
||||||
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
||||||
@@ -98,8 +98,8 @@ require (
|
|||||||
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
||||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
golang.org/x/mod v0.36.0 // indirect
|
golang.org/x/mod v0.36.0 // indirect
|
||||||
golang.org/x/net v0.54.0 // indirect
|
golang.org/x/net v0.55.0 // indirect
|
||||||
golang.org/x/sync v0.20.0 // indirect
|
golang.org/x/sync v0.21.0 // indirect
|
||||||
golang.org/x/tools v0.45.0 // indirect
|
golang.org/x/tools v0.45.0 // indirect
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
|
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
|
||||||
google.golang.org/protobuf v1.36.11 // indirect
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
|
|||||||
87
go.sum
87
go.sum
@@ -3,9 +3,8 @@ filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q
|
|||||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo=
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28=
|
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA=
|
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1 h1:jHb/wfvRikGdxMXYV3QG/SzUOPYN9KEUUuC0Yd0/vC0=
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1 h1:jHb/wfvRikGdxMXYV3QG/SzUOPYN9KEUUuC0Yd0/vC0=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1/go.mod h1:pzBXCYn05zvYIrwLgtK8Ap8QcjRg+0i76tMQdWN6wOk=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.1/go.mod h1:uE9zaUfEQT/nbQjVi2IblCG9iaLtZsuYZ8ne+PuQ02M=
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.1/go.mod h1:uE9zaUfEQT/nbQjVi2IblCG9iaLtZsuYZ8ne+PuQ02M=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg=
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=
|
||||||
@@ -13,17 +12,14 @@ github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc
|
|||||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM=
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2/go.mod h1:yInRyqWXAuaPrgI7p70+lDDgh3mlBohis29jGMISnmc=
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2/go.mod h1:yInRyqWXAuaPrgI7p70+lDDgh3mlBohis29jGMISnmc=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0/go.mod h1:4OG6tQ9EOP/MT0NMjDlRzWoVFxfu9rN9B2X+tlSVktg=
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0/go.mod h1:4OG6tQ9EOP/MT0NMjDlRzWoVFxfu9rN9B2X+tlSVktg=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=
|
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI=
|
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 h1:fhqpLE3UEXi9lPaBRpQ6XuRW0nU7hgg4zlmZZa+a9q4=
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 h1:fhqpLE3UEXi9lPaBRpQ6XuRW0nU7hgg4zlmZZa+a9q4=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0/go.mod h1:7dCRMLwisfRH3dBupKeNCioWYUZ4SS09Z14H+7i8ZoY=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1/go.mod h1:GpPjLhVR9dnUoJMyHWSPy71xY9/lcmpzIPZXmF0FCVY=
|
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1/go.mod h1:GpPjLhVR9dnUoJMyHWSPy71xY9/lcmpzIPZXmF0FCVY=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0 h1:E4MgwLBGeVB5f2MdcIVD3ELVAWpr+WD6MUe1i+tM/PA=
|
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0 h1:E4MgwLBGeVB5f2MdcIVD3ELVAWpr+WD6MUe1i+tM/PA=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0/go.mod h1:Y2b/1clN4zsAoUd/pgNAQHjLDnTis/6ROkUfyob6psM=
|
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0/go.mod h1:Y2b/1clN4zsAoUd/pgNAQHjLDnTis/6ROkUfyob6psM=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI=
|
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 h1:nCYfgcSyHZXJI8J0IWE5MsCGlb2xp9fJiXyxWgmOFg4=
|
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 h1:nCYfgcSyHZXJI8J0IWE5MsCGlb2xp9fJiXyxWgmOFg4=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0/go.mod h1:ucUjca2JtSZboY8IoUqyQyuuXvwbMBVwFOm0vdQPNhA=
|
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0/go.mod h1:ucUjca2JtSZboY8IoUqyQyuuXvwbMBVwFOm0vdQPNhA=
|
||||||
github.com/Azure/go-ntlmssp v0.1.0 h1:DjFo6YtWzNqNvQdrwEyr/e4nhU3vRiwenz5QX7sFz+A=
|
|
||||||
github.com/Azure/go-ntlmssp v0.1.0/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
|
|
||||||
github.com/Azure/go-ntlmssp v0.1.1 h1:l+FM/EEMb0U9QZE7mKNEDw5Mu3mFiaa2GKOoTSsNDPw=
|
github.com/Azure/go-ntlmssp v0.1.1 h1:l+FM/EEMb0U9QZE7mKNEDw5Mu3mFiaa2GKOoTSsNDPw=
|
||||||
github.com/Azure/go-ntlmssp v0.1.1/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
|
github.com/Azure/go-ntlmssp v0.1.1/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
|
||||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
||||||
@@ -52,8 +48,6 @@ github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/
|
|||||||
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
|
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ=
|
|
||||||
github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
|
||||||
github.com/fxamacker/cbor/v2 v2.9.2 h1:X4Ksno9+x3cz0TZv69ec1hxP/+tymuR8PXQJyDwfh78=
|
github.com/fxamacker/cbor/v2 v2.9.2 h1:X4Ksno9+x3cz0TZv69ec1hxP/+tymuR8PXQJyDwfh78=
|
||||||
github.com/fxamacker/cbor/v2 v2.9.2/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
github.com/fxamacker/cbor/v2 v2.9.2/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
||||||
@@ -68,8 +62,6 @@ github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz
|
|||||||
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||||
github.com/go-ldap/ldap/v3 v3.4.13 h1:+x1nG9h+MZN7h/lUi5Q3UZ0fJ1GyDQYbPvbuH38baDQ=
|
github.com/go-ldap/ldap/v3 v3.4.13 h1:+x1nG9h+MZN7h/lUi5Q3UZ0fJ1GyDQYbPvbuH38baDQ=
|
||||||
github.com/go-ldap/ldap/v3 v3.4.13/go.mod h1:LxsGZV6vbaK0sIvYfsv47rfh4ca0JXokCoKjZxsszv0=
|
github.com/go-ldap/ldap/v3 v3.4.13/go.mod h1:LxsGZV6vbaK0sIvYfsv47rfh4ca0JXokCoKjZxsszv0=
|
||||||
github.com/go-openapi/jsonpointer v0.23.0 h1:c25HFTJ6uWGmoe5BQI6p72p4o7KnlWYsy1MeFlAumsw=
|
|
||||||
github.com/go-openapi/jsonpointer v0.23.0/go.mod h1:iWRmZTrGn7XwYhtPt/fvdSFj1OfNBngqRT2UG3BxSqY=
|
|
||||||
github.com/go-openapi/jsonpointer v0.23.1 h1:1HBACs7XIwR2RcmItfdSFlALhGbe6S92p0ry4d1GWg4=
|
github.com/go-openapi/jsonpointer v0.23.1 h1:1HBACs7XIwR2RcmItfdSFlALhGbe6S92p0ry4d1GWg4=
|
||||||
github.com/go-openapi/jsonpointer v0.23.1/go.mod h1:iWRmZTrGn7XwYhtPt/fvdSFj1OfNBngqRT2UG3BxSqY=
|
github.com/go-openapi/jsonpointer v0.23.1/go.mod h1:iWRmZTrGn7XwYhtPt/fvdSFj1OfNBngqRT2UG3BxSqY=
|
||||||
github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE=
|
github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE=
|
||||||
@@ -105,24 +97,18 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
|
|||||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-playground/validator/v10 v10.30.2 h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK2xqPNk8vgvu5JQ=
|
github.com/go-playground/validator/v10 v10.30.3 h1:4MU6YkEwx7GbcPJOZxrtbu+QfF3pJLJuaYTeAH0DYy8=
|
||||||
github.com/go-playground/validator/v10 v10.30.2/go.mod h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc=
|
github.com/go-playground/validator/v10 v10.30.3/go.mod h1:4Axh7oCNGcoGkqLoE4YWt6n20mcEIsPRlB7vPk3lpyc=
|
||||||
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
|
||||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
|
||||||
github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw=
|
github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw=
|
||||||
github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk=
|
github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk=
|
||||||
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
|
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
|
||||||
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||||
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||||
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||||
github.com/go-webauthn/webauthn v0.16.4 h1:R9jqR/cYZa7hRquFF7Za/8qoH/K/TIs1/Q/4CyGN+1Q=
|
github.com/go-webauthn/webauthn v0.17.4 h1:KFTSz3R2RYDiUn/0cDi3XTJgFenSG74eKTTHlqWhlxk=
|
||||||
github.com/go-webauthn/webauthn v0.16.4/go.mod h1:SU2ljAgToTV/YLPI0C05QS4qn+e04WpB5g1RMfcZfS4=
|
github.com/go-webauthn/webauthn v0.17.4/go.mod h1:pZk63EE/BdztlmyS4Yc+9H5g4a8blNlbtGmdHQHbZX8=
|
||||||
github.com/go-webauthn/webauthn v0.17.3 h1:XHZ0TXV7k8vChcE4TFgPitOPJ5cb7h1dpAeFDS0cjCo=
|
github.com/go-webauthn/x v0.2.6 h1:TEyDuQAIiEgYpx60nKiBJIX/5nSUC8LxNbH+uf5U9uk=
|
||||||
github.com/go-webauthn/webauthn v0.17.3/go.mod h1:PlkMgmuL9McCT7dvgBj/Sz/fgs3V6ZID6/KnFkEcPvQ=
|
github.com/go-webauthn/x v0.2.6/go.mod h1:45bA7YEqyQhRcQJ/TiBb46Ww8yqHBGvgEhQ3WWF0aDo=
|
||||||
github.com/go-webauthn/x v0.2.3 h1:8oArS+Rc1SWFLXhE17KZNx258Z4kUSyaDgsSncCO5RA=
|
|
||||||
github.com/go-webauthn/x v0.2.3/go.mod h1:tM04GF3V6VYq79AZMl7vbj4q6pz9r7L2criWRzbWhPk=
|
|
||||||
github.com/go-webauthn/x v0.2.5 h1:wEVTfU04XFyPTXGQbKOQwMKhcDWfDAkdsDDBsDaG9yY=
|
|
||||||
github.com/go-webauthn/x v0.2.5/go.mod h1:Qna/yJz9rV6lRzwl5BfYbmTJpVGxcBIds3gJtw2tlGg=
|
|
||||||
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
@@ -157,8 +143,6 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
|
|||||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
github.com/jackc/pgx/v5 v5.9.1 h1:uwrxJXBnx76nyISkhr33kQLlUqjv7et7b9FjCen/tdc=
|
|
||||||
github.com/jackc/pgx/v5 v5.9.1/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
|
||||||
github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
|
github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
|
||||||
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||||
@@ -192,21 +176,15 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
|
|||||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=
|
|
||||||
github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
|
||||||
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
|
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
|
||||||
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||||
github.com/mdlayher/genetlink v1.4.0 h1:f/Xs7Y2T+GyX9b3dbiUhnLE9InGs5F9RxJ2JwBMl71o=
|
github.com/mdlayher/genetlink v1.4.0 h1:f/Xs7Y2T+GyX9b3dbiUhnLE9InGs5F9RxJ2JwBMl71o=
|
||||||
github.com/mdlayher/genetlink v1.4.0/go.mod h1:d1hrKr8fwZU2JkcAtQUAzeTrI7nbgQSl+5k1cC0biSA=
|
github.com/mdlayher/genetlink v1.4.0/go.mod h1:d1hrKr8fwZU2JkcAtQUAzeTrI7nbgQSl+5k1cC0biSA=
|
||||||
github.com/mdlayher/netlink v1.11.0 h1:Cot7ixQZL6P/pxRFB4z3jRdGPYeZosFT+WHS3sMXy8Y=
|
|
||||||
github.com/mdlayher/netlink v1.11.0/go.mod h1:rMwDzh42W85uW3yTtiTRZFX9uway98aDQ5i+D8Jq4g4=
|
|
||||||
github.com/mdlayher/netlink v1.11.2 h1:HKh2jqe+omdSWcQ88nrT7INE61B0NXfiSPFdgL4YbNI=
|
github.com/mdlayher/netlink v1.11.2 h1:HKh2jqe+omdSWcQ88nrT7INE61B0NXfiSPFdgL4YbNI=
|
||||||
github.com/mdlayher/netlink v1.11.2/go.mod h1:uT2Yc/QLaZubzDpZIBi9d4GoeLwtp3x1AMeqSRrK2sA=
|
github.com/mdlayher/netlink v1.11.2/go.mod h1:uT2Yc/QLaZubzDpZIBi9d4GoeLwtp3x1AMeqSRrK2sA=
|
||||||
github.com/mdlayher/socket v0.6.0 h1:ScZPaAGyO1icQnbFrhPM8mnXyMu9qukC1K4ZoM2IQKU=
|
github.com/mdlayher/socket v0.6.0 h1:ScZPaAGyO1icQnbFrhPM8mnXyMu9qukC1K4ZoM2IQKU=
|
||||||
github.com/mdlayher/socket v0.6.0/go.mod h1:q7vozUAnxSqnjHc12Fik5yUKIzfZ8ITCfMkhOtE9z18=
|
github.com/mdlayher/socket v0.6.0/go.mod h1:q7vozUAnxSqnjHc12Fik5yUKIzfZ8ITCfMkhOtE9z18=
|
||||||
github.com/microsoft/go-mssqldb v1.8.2/go.mod h1:vp38dT33FGfVotRiTmDo3bFyaHq+p3LektQrjTULowo=
|
github.com/microsoft/go-mssqldb v1.8.2/go.mod h1:vp38dT33FGfVotRiTmDo3bFyaHq+p3LektQrjTULowo=
|
||||||
github.com/microsoft/go-mssqldb v1.9.8 h1:d4IFMvF/o+HdpXUqbBfzHvn/NlFA75YGcfHUUvDFJEM=
|
|
||||||
github.com/microsoft/go-mssqldb v1.9.8/go.mod h1:eGSRSGAW4hKMy5YcAenhCDjIRm2rhqIdmmwgciMzLus=
|
|
||||||
github.com/microsoft/go-mssqldb v1.10.0 h1:pHEt+Qz6YFPWqREq10mqSE524QQo+/QremwTCQht7TY=
|
github.com/microsoft/go-mssqldb v1.10.0 h1:pHEt+Qz6YFPWqREq10mqSE524QQo+/QremwTCQht7TY=
|
||||||
github.com/microsoft/go-mssqldb v1.10.0/go.mod h1:mnG7lGa9iYJbzJqGCXyuQCegStKMr3kogDLD6+bmggg=
|
github.com/microsoft/go-mssqldb v1.10.0/go.mod h1:mnG7lGa9iYJbzJqGCXyuQCegStKMr3kogDLD6+bmggg=
|
||||||
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
|
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
|
||||||
@@ -300,18 +278,14 @@ golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOM
|
|||||||
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
||||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
||||||
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
||||||
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
||||||
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
|
|
||||||
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
|
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
|
|
||||||
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
|
|
||||||
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
|
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
|
||||||
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
|
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
@@ -331,10 +305,8 @@ golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
|||||||
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
||||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||||
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
||||||
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
||||||
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
|
|
||||||
golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
|
|
||||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@@ -344,8 +316,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
|||||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
@@ -366,10 +338,8 @@ golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|||||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
|
||||||
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
|
||||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
@@ -398,18 +368,14 @@ golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
|||||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||||
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
||||||
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
||||||
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
||||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
|
||||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
|
|
||||||
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
|
|
||||||
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
|
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
|
||||||
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
|
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
@@ -438,12 +404,10 @@ gorm.io/driver/sqlserver v1.6.3/go.mod h1:VZeNn7hqX1aXoN5TPAFGWvxWG90xtA8erGn2gQ
|
|||||||
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
||||||
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
||||||
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||||
modernc.org/cc/v4 v4.27.3 h1:uNCgn37E5U09mTv1XgskEVUJ8ADKpmFMPxzGJ0TSo+U=
|
|
||||||
modernc.org/cc/v4 v4.27.3/go.mod h1:3YjcbCqhoTTHPycJDRl2WZKKFj0nwcOIPBfEZK0Hdk8=
|
|
||||||
modernc.org/cc/v4 v4.28.2 h1:3tQ0lf2ADtoby2EtSP+J7IE2SHwEJdP8ioR59wx7XpY=
|
modernc.org/cc/v4 v4.28.2 h1:3tQ0lf2ADtoby2EtSP+J7IE2SHwEJdP8ioR59wx7XpY=
|
||||||
modernc.org/ccgo/v4 v4.32.4 h1:L5OB8rpEX4ZsXEQwGozRfJyJSFHbbNVOoQ59DU9/KuU=
|
modernc.org/cc/v4 v4.28.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
|
||||||
modernc.org/ccgo/v4 v4.32.4/go.mod h1:lY7f+fiTDHfcv6YlRgSkxYfhs+UvOEEzj49jAn2TOx0=
|
|
||||||
modernc.org/ccgo/v4 v4.34.0 h1:yRLPFZieg532OT4rp4JFNIVcquwalMX26G95WQDqwCQ=
|
modernc.org/ccgo/v4 v4.34.0 h1:yRLPFZieg532OT4rp4JFNIVcquwalMX26G95WQDqwCQ=
|
||||||
|
modernc.org/ccgo/v4 v4.34.0/go.mod h1:AS5WYMyBakQ+fhsHhtP8mWB82KTGPkNNJDGfGQCe0/A=
|
||||||
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
||||||
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
||||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||||
@@ -452,28 +416,23 @@ modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
|
|||||||
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||||
modernc.org/libc v1.72.0 h1:IEu559v9a0XWjw0DPoVKtXpO2qt5NVLAnFaBbjq+n8c=
|
|
||||||
modernc.org/libc v1.72.0/go.mod h1:tTU8DL8A+XLVkEY3x5E/tO7s2Q/q42EtnNWda/L5QhQ=
|
|
||||||
modernc.org/libc v1.72.3 h1:ZnDF4tXn4NBXFutMMQC4vtbTFSXhhKzR73fv0beZEAU=
|
modernc.org/libc v1.72.3 h1:ZnDF4tXn4NBXFutMMQC4vtbTFSXhhKzR73fv0beZEAU=
|
||||||
modernc.org/libc v1.72.3/go.mod h1:dn0dZNnnn1clLyvRxLxYExxiKRZIRENOfqQ8XEeg4Qs=
|
modernc.org/libc v1.72.3/go.mod h1:dn0dZNnnn1clLyvRxLxYExxiKRZIRENOfqQ8XEeg4Qs=
|
||||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
|
||||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
|
||||||
modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
|
modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
|
||||||
|
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||||
modernc.org/sqlite v1.48.2 h1:5CnW4uP8joZtA0LedVqLbZV5GD7F/0x91AXeSyjoh5c=
|
|
||||||
modernc.org/sqlite v1.48.2/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig=
|
|
||||||
modernc.org/sqlite v1.50.1 h1:l+cQvn0sd0zJJtfygGHuQJ5AjlrwXmWPw4KP3ZMwr9w=
|
modernc.org/sqlite v1.50.1 h1:l+cQvn0sd0zJJtfygGHuQJ5AjlrwXmWPw4KP3ZMwr9w=
|
||||||
modernc.org/sqlite v1.50.1/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM=
|
modernc.org/sqlite v1.50.1/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM=
|
||||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||||
pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
|
pgregory.net/rapid v1.3.0 h1:vBvO0VSqti75J1jjYqpgPNBLKMd1+gxa9fYo7vk/Exc=
|
||||||
pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
|
pgregory.net/rapid v1.3.0/go.mod h1:dPlE4OBBxgXPqkP79flB6sJL1dx5azpI7HQ9MY9Z7uk=
|
||||||
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
|
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
|
||||||
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
|
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
|
||||||
|
|||||||
@@ -617,18 +617,22 @@ func (c *PfsenseController) SaveInterface(
|
|||||||
mutex.Lock()
|
mutex.Lock()
|
||||||
defer mutex.Unlock()
|
defer mutex.Unlock()
|
||||||
|
|
||||||
physicalInterface, err := c.getOrCreateInterface(ctx, id)
|
physicalInterface, err := c.getInterface(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
deviceId := ""
|
if physicalInterface == nil {
|
||||||
if physicalInterface.GetExtras() != nil {
|
physicalInterface = &domain.PhysicalInterface{
|
||||||
if extras, ok := physicalInterface.GetExtras().(domain.PfsenseInterfaceExtras); ok {
|
Identifier: id,
|
||||||
deviceId = extras.Id
|
ImportSource: domain.ControllerTypePfsense,
|
||||||
|
DeviceType: domain.ControllerTypePfsense,
|
||||||
}
|
}
|
||||||
|
physicalInterface.SetExtras(domain.PfsenseInterfaceExtras{})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deviceId := physicalInterface.GetExtras().(domain.PfsenseInterfaceExtras).Id
|
||||||
|
|
||||||
if updateFunc != nil {
|
if updateFunc != nil {
|
||||||
physicalInterface, err = updateFunc(physicalInterface)
|
physicalInterface, err = updateFunc(physicalInterface)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -643,14 +647,14 @@ func (c *PfsenseController) SaveInterface(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.updateInterface(ctx, physicalInterface); err != nil {
|
if err := c.createOrUpdateInterface(ctx, physicalInterface); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *PfsenseController) getOrCreateInterface(
|
func (c *PfsenseController) getInterface(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
id domain.InterfaceIdentifier,
|
id domain.InterfaceIdentifier,
|
||||||
) (*domain.PhysicalInterface, error) {
|
) (*domain.PhysicalInterface, error) {
|
||||||
@@ -659,50 +663,84 @@ func (c *PfsenseController) getOrCreateInterface(
|
|||||||
"name": string(id),
|
"name": string(id),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if wgReply.Status == lowlevel.PfsenseApiStatusOk && len(wgReply.Data) > 0 {
|
if wgReply.Status != lowlevel.PfsenseApiStatusOk {
|
||||||
return c.loadInterfaceData(ctx, wgReply.Data[0])
|
return nil, fmt.Errorf("failed to query interface %s: %v", id, wgReply.Error)
|
||||||
}
|
}
|
||||||
|
if len(wgReply.Data) == 0 {
|
||||||
// create a new tunnel if it does not exist
|
return nil, nil
|
||||||
// Actual endpoint: POST /api/v2/vpn/wireguard/tunnel (singular)
|
|
||||||
createReply := c.client.Create(ctx, "/api/v2/vpn/wireguard/tunnel", lowlevel.GenericJsonObject{
|
|
||||||
"name": string(id),
|
|
||||||
})
|
|
||||||
if createReply.Status == lowlevel.PfsenseApiStatusOk {
|
|
||||||
return c.loadInterfaceData(ctx, createReply.Data)
|
|
||||||
}
|
}
|
||||||
|
return c.loadInterfaceData(ctx, wgReply.Data[0])
|
||||||
return nil, fmt.Errorf("failed to create interface %s: %v", id, createReply.Error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *PfsenseController) updateInterface(ctx context.Context, pi *domain.PhysicalInterface) error {
|
type pfsenseWireGuardAddress struct {
|
||||||
|
Address string `json:"address"`
|
||||||
|
Mask int `json:"mask"`
|
||||||
|
Descr string `json:"descr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func cidrToPfsense(cidr *domain.Cidr) pfsenseWireGuardAddress {
|
||||||
|
return pfsenseWireGuardAddress{
|
||||||
|
Address: cidr.Addr,
|
||||||
|
Mask: cidr.NetLength,
|
||||||
|
// supported in pfsense, but not in wg-portal GUI
|
||||||
|
Descr: "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PfsenseController) createOrUpdateInterface(ctx context.Context, pi *domain.PhysicalInterface) error {
|
||||||
extras := pi.GetExtras().(domain.PfsenseInterfaceExtras)
|
extras := pi.GetExtras().(domain.PfsenseInterfaceExtras)
|
||||||
interfaceId := extras.Id
|
interfaceId := extras.Id
|
||||||
|
|
||||||
payload := lowlevel.GenericJsonObject{
|
payload := lowlevel.GenericJsonObject{
|
||||||
"name": string(pi.Identifier),
|
"name": string(pi.Identifier),
|
||||||
"description": extras.Comment,
|
"descr": extras.Comment,
|
||||||
"mtu": strconv.Itoa(pi.Mtu),
|
"mtu": pi.Mtu,
|
||||||
"listenport": strconv.Itoa(pi.ListenPort),
|
"listenport": strconv.Itoa(pi.ListenPort),
|
||||||
"privatekey": pi.KeyPair.PrivateKey,
|
"privatekey": pi.KeyPair.PrivateKey,
|
||||||
"disabled": strconv.FormatBool(!pi.DeviceUp),
|
"disabled": strconv.FormatBool(!pi.DeviceUp),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add addresses if present
|
addresses := make([]pfsenseWireGuardAddress, 0, len(pi.Addresses))
|
||||||
if len(pi.Addresses) > 0 {
|
for _, addr := range pi.Addresses {
|
||||||
addresses := make([]string, 0, len(pi.Addresses))
|
addresses = append(addresses, cidrToPfsense(&addr))
|
||||||
for _, addr := range pi.Addresses {
|
}
|
||||||
addresses = append(addresses, addr.String())
|
payload["addresses"] = addresses
|
||||||
|
|
||||||
|
if interfaceId == "" {
|
||||||
|
// Actual endpoint: POST /api/v2/vpn/wireguard/tunnel (singular)
|
||||||
|
createReply := c.client.Create(ctx, "/api/v2/vpn/wireguard/tunnel", payload)
|
||||||
|
if createReply.Status != lowlevel.PfsenseApiStatusOk {
|
||||||
|
return fmt.Errorf("failed to create interface %s: %v", pi.Identifier, createReply.Error)
|
||||||
}
|
}
|
||||||
payload["addresses"] = strings.Join(addresses, ",")
|
// Capture the newly-assigned ID so callers see it
|
||||||
|
if newId := createReply.Data.GetString("id"); newId != "" {
|
||||||
|
extras.Id = newId
|
||||||
|
pi.SetExtras(extras)
|
||||||
|
}
|
||||||
|
if applyReply := c.client.Create(ctx, "/api/v2/vpn/wireguard/apply", lowlevel.GenericJsonObject{}); applyReply.Status != lowlevel.PfsenseApiStatusOk {
|
||||||
|
return fmt.Errorf("failed to apply WireGuard changes after creating interface %s: %v",
|
||||||
|
pi.Identifier, applyReply.Error)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Actual endpoint: PATCH /api/v2/vpn/wireguard/tunnel?id={id}
|
interfaceIdInt, err := strconv.Atoi(interfaceId)
|
||||||
wgReply := c.client.Update(ctx, "/api/v2/vpn/wireguard/tunnel?id="+interfaceId, payload)
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid pfSense interface id %q for %s: %w", interfaceId, pi.Identifier, err)
|
||||||
|
}
|
||||||
|
payload["id"] = interfaceIdInt
|
||||||
|
|
||||||
|
// Actual endpoint: PATCH /api/v2/vpn/wireguard/tunnel
|
||||||
|
wgReply := c.client.Update(ctx, "/api/v2/vpn/wireguard/tunnel", payload)
|
||||||
if wgReply.Status != lowlevel.PfsenseApiStatusOk {
|
if wgReply.Status != lowlevel.PfsenseApiStatusOk {
|
||||||
return fmt.Errorf("failed to update interface %s: %v", pi.Identifier, wgReply.Error)
|
return fmt.Errorf("failed to update interface %s: %v", pi.Identifier, wgReply.Error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if applyReply := c.client.Create(ctx, "/api/v2/vpn/wireguard/apply", lowlevel.GenericJsonObject{}); applyReply.Status != lowlevel.PfsenseApiStatusOk {
|
||||||
|
return fmt.Errorf("failed to apply WireGuard changes after updating interface %s: %v",
|
||||||
|
pi.Identifier, applyReply.Error)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -726,8 +764,10 @@ func (c *PfsenseController) DeleteInterface(ctx context.Context, id domain.Inter
|
|||||||
}
|
}
|
||||||
|
|
||||||
interfaceId := wgReply.Data[0].GetString("id")
|
interfaceId := wgReply.Data[0].GetString("id")
|
||||||
// Actual endpoint: DELETE /api/v2/vpn/wireguard/tunnel?id={id}
|
// Actual endpoint: DELETE /api/v2/vpn/wireguard/tunnel?id={id}&apply=true
|
||||||
deleteReply := c.client.Delete(ctx, "/api/v2/vpn/wireguard/tunnel?id="+interfaceId)
|
deleteReply := c.client.Delete(ctx, "/api/v2/vpn/wireguard/tunnel", &lowlevel.PfsenseRequestOptions{
|
||||||
|
Filters: map[string]string{"id": interfaceId, "apply": "true"},
|
||||||
|
})
|
||||||
if deleteReply.Status != lowlevel.PfsenseApiStatusOk {
|
if deleteReply.Status != lowlevel.PfsenseApiStatusOk {
|
||||||
return fmt.Errorf("failed to delete WireGuard interface %s: %v", id, deleteReply.Error)
|
return fmt.Errorf("failed to delete WireGuard interface %s: %v", id, deleteReply.Error)
|
||||||
}
|
}
|
||||||
@@ -746,18 +786,22 @@ func (c *PfsenseController) SavePeer(
|
|||||||
mutex.Lock()
|
mutex.Lock()
|
||||||
defer mutex.Unlock()
|
defer mutex.Unlock()
|
||||||
|
|
||||||
physicalPeer, err := c.getOrCreatePeer(ctx, deviceId, id)
|
physicalPeer, err := c.getPeer(ctx, deviceId, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
peerId := ""
|
if physicalPeer == nil {
|
||||||
if physicalPeer.GetExtras() != nil {
|
physicalPeer = &domain.PhysicalPeer{
|
||||||
if extras, ok := physicalPeer.GetExtras().(domain.PfsensePeerExtras); ok {
|
Identifier: id,
|
||||||
peerId = extras.Id
|
KeyPair: domain.KeyPair{PublicKey: string(id)},
|
||||||
|
ImportSource: domain.ControllerTypePfsense,
|
||||||
}
|
}
|
||||||
|
physicalPeer.SetExtras(domain.PfsensePeerExtras{})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
peerId := physicalPeer.GetExtras().(domain.PfsensePeerExtras).Id
|
||||||
|
|
||||||
physicalPeer, err = updateFunc(physicalPeer)
|
physicalPeer, err = updateFunc(physicalPeer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -770,14 +814,14 @@ func (c *PfsenseController) SavePeer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.updatePeer(ctx, deviceId, physicalPeer); err != nil {
|
if err := c.createOrUpdatePeer(ctx, deviceId, physicalPeer); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *PfsenseController) getOrCreatePeer(
|
func (c *PfsenseController) getPeer(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
deviceId domain.InterfaceIdentifier,
|
deviceId domain.InterfaceIdentifier,
|
||||||
id domain.PeerIdentifier,
|
id domain.PeerIdentifier,
|
||||||
@@ -787,40 +831,25 @@ func (c *PfsenseController) getOrCreatePeer(
|
|||||||
wgReply := c.client.Query(ctx, "/api/v2/vpn/wireguard/peers", &lowlevel.PfsenseRequestOptions{
|
wgReply := c.client.Query(ctx, "/api/v2/vpn/wireguard/peers", &lowlevel.PfsenseRequestOptions{
|
||||||
Filters: map[string]string{
|
Filters: map[string]string{
|
||||||
"publickey": string(id),
|
"publickey": string(id),
|
||||||
"tun": string(deviceId), // Use "tun" field name as that's what the API uses
|
"tun": string(deviceId), // Use "tun" field name as that's what the API uses
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if wgReply.Status == lowlevel.PfsenseApiStatusOk && len(wgReply.Data) > 0 {
|
if wgReply.Status != lowlevel.PfsenseApiStatusOk {
|
||||||
slog.Debug("found existing pfSense peer", "peer", id, "interface", deviceId)
|
return nil, fmt.Errorf("failed to query peer %s for interface %s: %v", id, deviceId, wgReply.Error)
|
||||||
existingPeer, err := c.convertWireGuardPeer(wgReply.Data[0])
|
}
|
||||||
if err != nil {
|
if len(wgReply.Data) == 0 {
|
||||||
return nil, err
|
return nil, nil
|
||||||
}
|
|
||||||
return &existingPeer, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// create a new peer if it does not exist
|
slog.Debug("found existing pfSense peer", "peer", id, "interface", deviceId)
|
||||||
// Actual endpoint: POST /api/v2/vpn/wireguard/peer (singular)
|
existingPeer, err := c.convertWireGuardPeer(wgReply.Data[0])
|
||||||
slog.Debug("creating new pfSense peer", "peer", id, "interface", deviceId)
|
if err != nil {
|
||||||
createReply := c.client.Create(ctx, "/api/v2/vpn/wireguard/peer", lowlevel.GenericJsonObject{
|
return nil, err
|
||||||
"name": fmt.Sprintf("wg-%s", id[0:8]),
|
|
||||||
"interface": string(deviceId),
|
|
||||||
"publickey": string(id),
|
|
||||||
"allowedips": "0.0.0.0/0", // Use 0.0.0.0/0 as default, will be updated by updatePeer
|
|
||||||
})
|
|
||||||
if createReply.Status == lowlevel.PfsenseApiStatusOk {
|
|
||||||
newPeer, err := c.convertWireGuardPeer(createReply.Data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
slog.Debug("successfully created pfSense peer", "peer", id, "interface", deviceId)
|
|
||||||
return &newPeer, nil
|
|
||||||
}
|
}
|
||||||
|
return &existingPeer, nil
|
||||||
return nil, fmt.Errorf("failed to create peer %s for interface %s: %v", id, deviceId, createReply.Error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *PfsenseController) updatePeer(
|
func (c *PfsenseController) createOrUpdatePeer(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
deviceId domain.InterfaceIdentifier,
|
deviceId domain.InterfaceIdentifier,
|
||||||
pp *domain.PhysicalPeer,
|
pp *domain.PhysicalPeer,
|
||||||
@@ -828,36 +857,74 @@ func (c *PfsenseController) updatePeer(
|
|||||||
extras := pp.GetExtras().(domain.PfsensePeerExtras)
|
extras := pp.GetExtras().(domain.PfsensePeerExtras)
|
||||||
peerId := extras.Id
|
peerId := extras.Id
|
||||||
|
|
||||||
allowedIPsStr := domain.CidrsToString(pp.AllowedIPs)
|
|
||||||
|
|
||||||
slog.Debug("updating pfSense peer",
|
|
||||||
"peer", pp.Identifier,
|
|
||||||
"interface", deviceId,
|
|
||||||
"allowed-ips", allowedIPsStr,
|
|
||||||
"allowed-ips-count", len(pp.AllowedIPs),
|
|
||||||
"disabled", extras.Disabled)
|
|
||||||
|
|
||||||
payload := lowlevel.GenericJsonObject{
|
payload := lowlevel.GenericJsonObject{
|
||||||
"name": extras.Name,
|
"tun": string(deviceId),
|
||||||
"description": extras.Comment,
|
"descr": extras.Name,
|
||||||
"presharedkey": string(pp.PresharedKey),
|
"presharedkey": string(pp.PresharedKey),
|
||||||
"publickey": pp.KeyPair.PublicKey,
|
"publickey": pp.KeyPair.PublicKey,
|
||||||
"privatekey": pp.KeyPair.PrivateKey,
|
"persistentkeepalive": pp.PersistentKeepalive,
|
||||||
"persistentkeepalive": strconv.Itoa(pp.PersistentKeepalive),
|
"enabled": !extras.Disabled,
|
||||||
"disabled": strconv.FormatBool(extras.Disabled),
|
|
||||||
"allowedips": allowedIPsStr,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if pp.Endpoint != "" {
|
if pp.Endpoint != "" {
|
||||||
payload["endpoint"] = pp.Endpoint
|
payload["endpoint"] = pp.Endpoint
|
||||||
}
|
}
|
||||||
|
|
||||||
// Actual endpoint: PATCH /api/v2/vpn/wireguard/peer?id={id}
|
allowedIps := make([]pfsenseWireGuardAddress, 0, len(pp.AllowedIPs))
|
||||||
wgReply := c.client.Update(ctx, "/api/v2/vpn/wireguard/peer?id="+peerId, payload)
|
for _, addr := range pp.AllowedIPs {
|
||||||
|
allowedIps = append(allowedIps, cidrToPfsense(&addr))
|
||||||
|
}
|
||||||
|
payload["allowedips"] = allowedIps
|
||||||
|
|
||||||
|
if peerId == "" {
|
||||||
|
slog.Debug("creating new pfSense peer",
|
||||||
|
"peer", pp.Identifier,
|
||||||
|
"interface", deviceId,
|
||||||
|
"allowed-ips", domain.CidrsToString(pp.AllowedIPs),
|
||||||
|
"disabled", extras.Disabled)
|
||||||
|
|
||||||
|
// Actual endpoint: POST /api/v2/vpn/wireguard/peer (singular)
|
||||||
|
createReply := c.client.Create(ctx, "/api/v2/vpn/wireguard/peer", payload)
|
||||||
|
if createReply.Status != lowlevel.PfsenseApiStatusOk {
|
||||||
|
return fmt.Errorf("failed to create peer %s for interface %s: %v",
|
||||||
|
pp.Identifier, deviceId, createReply.Error)
|
||||||
|
}
|
||||||
|
if newId := createReply.Data.GetString("id"); newId != "" {
|
||||||
|
extras.Id = newId
|
||||||
|
pp.SetExtras(extras)
|
||||||
|
}
|
||||||
|
if applyReply := c.client.Create(ctx, "/api/v2/vpn/wireguard/apply", lowlevel.GenericJsonObject{}); applyReply.Status != lowlevel.PfsenseApiStatusOk {
|
||||||
|
return fmt.Errorf("failed to apply WireGuard changes after creating peer %s on interface %s: %v",
|
||||||
|
pp.Identifier, deviceId, applyReply.Error)
|
||||||
|
}
|
||||||
|
slog.Debug("successfully created pfSense peer", "peer", pp.Identifier, "interface", deviceId)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Debug("updating pfSense peer",
|
||||||
|
"peer", pp.Identifier,
|
||||||
|
"interface", deviceId,
|
||||||
|
"allowed-ips", domain.CidrsToString(pp.AllowedIPs),
|
||||||
|
"allowed-ips-count", len(pp.AllowedIPs),
|
||||||
|
"disabled", extras.Disabled)
|
||||||
|
|
||||||
|
peerIdInt, err := strconv.Atoi(peerId)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid pfSense peer id %q for %s: %w", peerId, pp.Identifier, err)
|
||||||
|
}
|
||||||
|
payload["id"] = peerIdInt
|
||||||
|
|
||||||
|
// Actual endpoint: PATCH /api/v2/vpn/wireguard/peer
|
||||||
|
wgReply := c.client.Update(ctx, "/api/v2/vpn/wireguard/peer", payload)
|
||||||
if wgReply.Status != lowlevel.PfsenseApiStatusOk {
|
if wgReply.Status != lowlevel.PfsenseApiStatusOk {
|
||||||
return fmt.Errorf("failed to update peer %s on interface %s: %v", pp.Identifier, deviceId, wgReply.Error)
|
return fmt.Errorf("failed to update peer %s on interface %s: %v", pp.Identifier, deviceId, wgReply.Error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if applyReply := c.client.Create(ctx, "/api/v2/vpn/wireguard/apply", lowlevel.GenericJsonObject{}); applyReply.Status != lowlevel.PfsenseApiStatusOk {
|
||||||
|
return fmt.Errorf("failed to apply WireGuard changes after updating peer %s on interface %s: %v",
|
||||||
|
pp.Identifier, deviceId, applyReply.Error)
|
||||||
|
}
|
||||||
|
|
||||||
if extras.Disabled {
|
if extras.Disabled {
|
||||||
slog.Debug("successfully disabled pfSense peer", "peer", pp.Identifier, "interface", deviceId)
|
slog.Debug("successfully disabled pfSense peer", "peer", pp.Identifier, "interface", deviceId)
|
||||||
} else {
|
} else {
|
||||||
@@ -882,7 +949,7 @@ func (c *PfsenseController) DeletePeer(
|
|||||||
wgReply := c.client.Query(ctx, "/api/v2/vpn/wireguard/peers", &lowlevel.PfsenseRequestOptions{
|
wgReply := c.client.Query(ctx, "/api/v2/vpn/wireguard/peers", &lowlevel.PfsenseRequestOptions{
|
||||||
Filters: map[string]string{
|
Filters: map[string]string{
|
||||||
"publickey": string(id),
|
"publickey": string(id),
|
||||||
"tun": string(deviceId), // Use "tun" field name as that's what the API uses
|
"tun": string(deviceId), // Use "tun" field name as that's what the API uses
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if wgReply.Status != lowlevel.PfsenseApiStatusOk {
|
if wgReply.Status != lowlevel.PfsenseApiStatusOk {
|
||||||
@@ -893,8 +960,10 @@ func (c *PfsenseController) DeletePeer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
peerId := wgReply.Data[0].GetString("id")
|
peerId := wgReply.Data[0].GetString("id")
|
||||||
// Actual endpoint: DELETE /api/v2/vpn/wireguard/peer?id={id}
|
// Actual endpoint: DELETE /api/v2/vpn/wireguard/peer?id={id}&apply=true
|
||||||
deleteReply := c.client.Delete(ctx, "/api/v2/vpn/wireguard/peer?id="+peerId)
|
deleteReply := c.client.Delete(ctx, "/api/v2/vpn/wireguard/peer", &lowlevel.PfsenseRequestOptions{
|
||||||
|
Filters: map[string]string{"id": peerId, "apply": "true"},
|
||||||
|
})
|
||||||
if deleteReply.Status != lowlevel.PfsenseApiStatusOk {
|
if deleteReply.Status != lowlevel.PfsenseApiStatusOk {
|
||||||
return fmt.Errorf("failed to delete WireGuard peer %s for interface %s: %v", id, deviceId, deleteReply.Error)
|
return fmt.Errorf("failed to delete WireGuard peer %s for interface %s: %v", id, deviceId, deleteReply.Error)
|
||||||
}
|
}
|
||||||
@@ -976,4 +1045,3 @@ func (c *PfsenseController) PingAddresses(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// endregion statistics-related
|
// endregion statistics-related
|
||||||
|
|
||||||
|
|||||||
@@ -283,10 +283,13 @@ func (s *Server) updateBasePathInFrontend(useEmbeddedFrontend bool) ([]byte, []b
|
|||||||
} else {
|
} else {
|
||||||
customIndexFile, _ = os.ReadFile(filepath.Join(s.cfg.Web.FrontendFilePath, "index.html"))
|
customIndexFile, _ = os.ReadFile(filepath.Join(s.cfg.Web.FrontendFilePath, "index.html"))
|
||||||
}
|
}
|
||||||
newIndexStr := strings.ReplaceAll(string(customIndexFile), "src=\"/", "src=\""+s.cfg.Web.BasePath+"/")
|
// rewrite relative paths
|
||||||
newIndexStr = strings.ReplaceAll(newIndexStr, "href=\"/", "href=\""+s.cfg.Web.BasePath+"/")
|
newIndexStr := strings.ReplaceAll(string(customIndexFile), "src=\"./", "src=\""+s.cfg.Web.BasePath+"/app/")
|
||||||
|
newIndexStr = strings.ReplaceAll(newIndexStr, "href=\"./", "href=\""+s.cfg.Web.BasePath+"/app/")
|
||||||
|
// rewrite absolute paths (api calls)
|
||||||
|
newIndexStr = strings.ReplaceAll(newIndexStr, "src=\"/api/", "src=\""+s.cfg.Web.BasePath+"/api/")
|
||||||
|
|
||||||
re := regexp.MustCompile(`/app/assets/(index-.+.css)`)
|
re := regexp.MustCompile(`assets/(index-.+.css)`)
|
||||||
match := re.FindStringSubmatch(newIndexStr)
|
match := re.FindStringSubmatch(newIndexStr)
|
||||||
cssFileName := match[1]
|
cssFileName := match[1]
|
||||||
|
|
||||||
@@ -296,7 +299,7 @@ func (s *Server) updateBasePathInFrontend(useEmbeddedFrontend bool) ([]byte, []b
|
|||||||
} else {
|
} else {
|
||||||
customCssFile, _ = os.ReadFile(filepath.Join(s.cfg.Web.FrontendFilePath, "/assets/", cssFileName))
|
customCssFile, _ = os.ReadFile(filepath.Join(s.cfg.Web.FrontendFilePath, "/assets/", cssFileName))
|
||||||
}
|
}
|
||||||
newCssStr := strings.ReplaceAll(string(customCssFile), "/app/assets/", s.cfg.Web.BasePath+"/app/assets/")
|
newCssStr := strings.ReplaceAll(string(customCssFile), "./assets/", s.cfg.Web.BasePath+"/app/assets/")
|
||||||
|
|
||||||
return []byte(newIndexStr), []byte(newCssStr), cssFileName
|
return []byte(newIndexStr), []byte(newCssStr), cssFileName
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,9 +24,9 @@ type AuthenticationService interface {
|
|||||||
// PlainLogin authenticates a user with a username and password.
|
// PlainLogin authenticates a user with a username and password.
|
||||||
PlainLogin(ctx context.Context, username, password string) (*domain.User, error)
|
PlainLogin(ctx context.Context, username, password string) (*domain.User, error)
|
||||||
// OauthLoginStep1 initiates the OAuth login flow.
|
// OauthLoginStep1 initiates the OAuth login flow.
|
||||||
OauthLoginStep1(_ context.Context, providerId string) (authCodeUrl, state, nonce string, err error)
|
OauthLoginStep1(_ context.Context, providerId string) (authCodeUrl, state, nonce, codeVerifier string, err error)
|
||||||
// OauthLoginStep2 completes the OAuth login flow and logins the user in.
|
// OauthLoginStep2 completes the OAuth login flow and logins the user in.
|
||||||
OauthLoginStep2(ctx context.Context, providerId, nonce, code string) (*domain.User, string, error)
|
OauthLoginStep2(ctx context.Context, providerId, nonce, code, codeVerifier string) (*domain.User, string, error)
|
||||||
// OauthProviderLogoutUrl returns an IdP logout URL for the given provider if supported.
|
// OauthProviderLogoutUrl returns an IdP logout URL for the given provider if supported.
|
||||||
OauthProviderLogoutUrl(providerId, idTokenHint, postLogoutRedirectUri string) (string, bool)
|
OauthProviderLogoutUrl(providerId, idTokenHint, postLogoutRedirectUri string) (string, bool)
|
||||||
}
|
}
|
||||||
@@ -231,7 +231,7 @@ func (e AuthEndpoint) handleOauthInitiateGet() http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
authCodeUrl, state, nonce, err := e.authService.OauthLoginStep1(context.Background(), provider)
|
authCodeUrl, state, nonce, codeVerifier, err := e.authService.OauthLoginStep1(context.Background(), provider)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Debug("failed to create oauth auth code URL",
|
slog.Debug("failed to create oauth auth code URL",
|
||||||
"provider", provider, "error", err)
|
"provider", provider, "error", err)
|
||||||
@@ -247,6 +247,7 @@ func (e AuthEndpoint) handleOauthInitiateGet() http.HandlerFunc {
|
|||||||
authSession := e.session.GetData(r.Context())
|
authSession := e.session.GetData(r.Context())
|
||||||
authSession.OauthState = state
|
authSession.OauthState = state
|
||||||
authSession.OauthNonce = nonce
|
authSession.OauthNonce = nonce
|
||||||
|
authSession.OauthCodeVerifier = codeVerifier
|
||||||
authSession.OauthProvider = provider
|
authSession.OauthProvider = provider
|
||||||
authSession.OauthReturnTo = returnTo
|
authSession.OauthReturnTo = returnTo
|
||||||
e.session.SetData(r.Context(), authSession)
|
e.session.SetData(r.Context(), authSession)
|
||||||
@@ -323,7 +324,7 @@ func (e AuthEndpoint) handleOauthCallbackGet() http.HandlerFunc {
|
|||||||
|
|
||||||
loginCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) // avoid long waits
|
loginCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) // avoid long waits
|
||||||
user, idTokenHint, err := e.authService.OauthLoginStep2(loginCtx, provider, currentSession.OauthNonce,
|
user, idTokenHint, err := e.authService.OauthLoginStep2(loginCtx, provider, currentSession.OauthNonce,
|
||||||
oauthCode)
|
oauthCode, currentSession.OauthCodeVerifier)
|
||||||
cancel()
|
cancel()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Debug("failed to process oauth code",
|
slog.Debug("failed to process oauth code",
|
||||||
@@ -362,6 +363,7 @@ func (e AuthEndpoint) setAuthenticatedUser(r *http.Request, user *domain.User, o
|
|||||||
|
|
||||||
currentSession.OauthState = ""
|
currentSession.OauthState = ""
|
||||||
currentSession.OauthNonce = ""
|
currentSession.OauthNonce = ""
|
||||||
|
currentSession.OauthCodeVerifier = ""
|
||||||
currentSession.OauthProvider = oauthProvider
|
currentSession.OauthProvider = oauthProvider
|
||||||
currentSession.OauthReturnTo = ""
|
currentSession.OauthReturnTo = ""
|
||||||
currentSession.OauthIdToken = idTokenHint
|
currentSession.OauthIdToken = idTokenHint
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ package handlers
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/go-pkgz/routegroup"
|
"github.com/go-pkgz/routegroup"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
@@ -14,49 +16,83 @@ import (
|
|||||||
"github.com/h44z/wg-portal/internal/domain"
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
websocketPeerUserIdentifierCacheTTL = 90 * time.Second
|
||||||
|
websocketPeerUserIdentifierCacheCleanupInterval = websocketPeerUserIdentifierCacheTTL * 2
|
||||||
|
)
|
||||||
|
|
||||||
type WebsocketEventBus interface {
|
type WebsocketEventBus interface {
|
||||||
Subscribe(topic string, fn any) error
|
Subscribe(topic string, fn any) error
|
||||||
Unsubscribe(topic string, fn any) error
|
Unsubscribe(topic string, fn any) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type WebsocketPeerService interface {
|
||||||
|
GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error)
|
||||||
|
}
|
||||||
|
|
||||||
type WebsocketEndpoint struct {
|
type WebsocketEndpoint struct {
|
||||||
authenticator Authenticator
|
authenticator Authenticator
|
||||||
bus WebsocketEventBus
|
bus WebsocketEventBus
|
||||||
|
peerService WebsocketPeerService
|
||||||
|
|
||||||
upgrader websocket.Upgrader
|
upgrader websocket.Upgrader
|
||||||
|
|
||||||
|
ownershipCache map[domain.PeerIdentifier]peerUserIdentifierCacheEntry
|
||||||
|
ownershipCacheMux sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewWebsocketEndpoint(cfg *config.Config, auth Authenticator, bus WebsocketEventBus) *WebsocketEndpoint {
|
func NewWebsocketEndpoint(
|
||||||
|
cfg *config.Config,
|
||||||
|
auth Authenticator,
|
||||||
|
bus WebsocketEventBus,
|
||||||
|
peerService WebsocketPeerService,
|
||||||
|
) *WebsocketEndpoint {
|
||||||
return &WebsocketEndpoint{
|
return &WebsocketEndpoint{
|
||||||
authenticator: auth,
|
authenticator: auth,
|
||||||
bus: bus,
|
bus: bus,
|
||||||
|
peerService: peerService,
|
||||||
upgrader: websocket.Upgrader{
|
upgrader: websocket.Upgrader{
|
||||||
ReadBufferSize: 1024,
|
ReadBufferSize: 1024,
|
||||||
WriteBufferSize: 1024,
|
WriteBufferSize: 1024,
|
||||||
CheckOrigin: func(r *http.Request) bool {
|
CheckOrigin: func(r *http.Request) bool {
|
||||||
origin := r.Header.Get("Origin")
|
return matchOrigin(cfg.Web.ExternalUrl, r.Header.Get("Origin"))
|
||||||
return strings.HasPrefix(origin, cfg.Web.ExternalUrl)
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
ownershipCache: make(map[domain.PeerIdentifier]peerUserIdentifierCacheEntry),
|
||||||
|
ownershipCacheMux: sync.Mutex{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e WebsocketEndpoint) GetName() string {
|
func (e *WebsocketEndpoint) GetName() string {
|
||||||
return "WebsocketEndpoint"
|
return "WebsocketEndpoint"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e WebsocketEndpoint) RegisterRoutes(g *routegroup.Bundle) {
|
func (e *WebsocketEndpoint) RegisterRoutes(g *routegroup.Bundle) {
|
||||||
g.With(e.authenticator.LoggedIn()).HandleFunc("GET /ws", e.handleWebsocket())
|
g.With(e.authenticator.LoggedIn()).HandleFunc("GET /ws", e.handleWebsocket())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StartBackgroundJobs starts background jobs like the expired peers check.
|
||||||
|
// This method is non-blocking.
|
||||||
|
func (e *WebsocketEndpoint) StartBackgroundJobs(ctx context.Context) {
|
||||||
|
go e.startOwnerCacheCleanup(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
// wsMessage represents a message sent over websocket to the frontend
|
// wsMessage represents a message sent over websocket to the frontend
|
||||||
type wsMessage struct {
|
type wsMessage struct {
|
||||||
Type string `json:"type"` // either "peer_stats" or "interface_stats"
|
Type string `json:"type"` // either "peer_stats" or "interface_stats"
|
||||||
Data any `json:"data"` // domain.TrafficDelta
|
Data any `json:"data"` // domain.TrafficDelta
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e WebsocketEndpoint) handleWebsocket() http.HandlerFunc {
|
// peerUserIdentifierCacheEntry is a cache entry object that reduces database load when checking peer ownership.
|
||||||
|
type peerUserIdentifierCacheEntry struct {
|
||||||
|
userIdentifier domain.UserIdentifier
|
||||||
|
expiresAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *WebsocketEndpoint) handleWebsocket() http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
userInfo := domain.GetUserInfo(r.Context())
|
||||||
|
|
||||||
conn, err := e.upgrader.Upgrade(w, r, nil)
|
conn, err := e.upgrader.Upgrade(w, r, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
@@ -74,9 +110,29 @@ func (e WebsocketEndpoint) handleWebsocket() http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
peerStatsHandler := func(status domain.TrafficDelta) {
|
peerStatsHandler := func(status domain.TrafficDelta) {
|
||||||
|
if !userInfo.IsAdmin {
|
||||||
|
// lookup peer user-info to validate ownership
|
||||||
|
peerUserIdentifier, err := e.getPeerUserIdentifier(ctx, domain.PeerIdentifier(status.EntityId))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if peerUserIdentifier == "" {
|
||||||
|
return // if peer is not assigned to any user, dont send stats
|
||||||
|
}
|
||||||
|
|
||||||
|
if peerUserIdentifier != userInfo.Id {
|
||||||
|
return // only expose stats for own peers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_ = writeJSON(wsMessage{Type: "peer_stats", Data: status})
|
_ = writeJSON(wsMessage{Type: "peer_stats", Data: status})
|
||||||
}
|
}
|
||||||
interfaceStatsHandler := func(status domain.TrafficDelta) {
|
interfaceStatsHandler := func(status domain.TrafficDelta) {
|
||||||
|
if !userInfo.IsAdmin {
|
||||||
|
return // interface stats will only be exposed to admins
|
||||||
|
}
|
||||||
|
|
||||||
_ = writeJSON(wsMessage{Type: "interface_stats", Data: status})
|
_ = writeJSON(wsMessage{Type: "interface_stats", Data: status})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,3 +154,72 @@ func (e WebsocketEndpoint) handleWebsocket() http.HandlerFunc {
|
|||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *WebsocketEndpoint) getPeerUserIdentifier(
|
||||||
|
ctx context.Context,
|
||||||
|
peerIdentifier domain.PeerIdentifier,
|
||||||
|
) (domain.UserIdentifier, error) {
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
e.ownershipCacheMux.Lock()
|
||||||
|
entry, ok := e.ownershipCache[peerIdentifier]
|
||||||
|
if ok && now.Before(entry.expiresAt) {
|
||||||
|
e.ownershipCacheMux.Unlock()
|
||||||
|
return entry.userIdentifier, nil
|
||||||
|
}
|
||||||
|
e.ownershipCacheMux.Unlock()
|
||||||
|
|
||||||
|
peer, err := e.peerService.GetPeer(ctx, peerIdentifier)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
e.ownershipCacheMux.Lock()
|
||||||
|
defer e.ownershipCacheMux.Unlock()
|
||||||
|
e.ownershipCache[peerIdentifier] = peerUserIdentifierCacheEntry{
|
||||||
|
userIdentifier: peer.UserIdentifier,
|
||||||
|
expiresAt: now.Add(websocketPeerUserIdentifierCacheTTL),
|
||||||
|
}
|
||||||
|
|
||||||
|
return peer.UserIdentifier, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *WebsocketEndpoint) startOwnerCacheCleanup(ctx context.Context) {
|
||||||
|
ticker := time.NewTicker(websocketPeerUserIdentifierCacheCleanupInterval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case now := <-ticker.C:
|
||||||
|
e.cleanupOwnerCache(now)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *WebsocketEndpoint) cleanupOwnerCache(now time.Time) {
|
||||||
|
e.ownershipCacheMux.Lock()
|
||||||
|
defer e.ownershipCacheMux.Unlock()
|
||||||
|
|
||||||
|
for peerIdentifier, entry := range e.ownershipCache {
|
||||||
|
if !now.Before(entry.expiresAt) {
|
||||||
|
delete(e.ownershipCache, peerIdentifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchOrigin(externalBaseUrl, origin string) bool {
|
||||||
|
originURL, err := url.Parse(origin)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
externalURL, err := url.Parse(externalBaseUrl)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return originURL.Scheme == externalURL.Scheme &&
|
||||||
|
strings.EqualFold(originURL.Host, externalURL.Host)
|
||||||
|
}
|
||||||
|
|||||||
249
internal/app/api/v0/handlers/endpoint_websocket_test.go
Normal file
249
internal/app/api/v0/handlers/endpoint_websocket_test.go
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
evbus "github.com/vardius/message-bus"
|
||||||
|
|
||||||
|
"github.com/h44z/wg-portal/internal/app"
|
||||||
|
"github.com/h44z/wg-portal/internal/config"
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
// region test-helper
|
||||||
|
|
||||||
|
type websocketTestPeerService struct {
|
||||||
|
peers map[domain.PeerIdentifier]*domain.Peer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s websocketTestPeerService) GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error) {
|
||||||
|
peer, ok := s.peers[id]
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("peer not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return peer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTestWebsocketConnection(
|
||||||
|
t *testing.T,
|
||||||
|
bus evbus.MessageBus,
|
||||||
|
userInfo *domain.ContextUserInfo,
|
||||||
|
peers map[domain.PeerIdentifier]*domain.Peer,
|
||||||
|
) (*websocket.Conn, func()) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
cfg := &config.Config{}
|
||||||
|
endpoint := NewWebsocketEndpoint(cfg, nil, bus, websocketTestPeerService{peers: peers})
|
||||||
|
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r = r.WithContext(domain.SetUserInfo(r.Context(), userInfo))
|
||||||
|
endpoint.handleWebsocket()(w, r)
|
||||||
|
}))
|
||||||
|
cfg.Web.ExternalUrl = server.URL
|
||||||
|
|
||||||
|
wsURL := "ws" + server.URL[len("http"):]
|
||||||
|
conn, _, err := websocket.DefaultDialer.Dial(wsURL, http.Header{"Origin": []string{server.URL}})
|
||||||
|
if err != nil {
|
||||||
|
server.Close()
|
||||||
|
t.Fatalf("failed to dial websocket: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup := func() {
|
||||||
|
conn.Close()
|
||||||
|
server.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return conn, cleanup
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertWebsocketMessage(t *testing.T, conn *websocket.Conn, messageType string, entityId string) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
if err := conn.SetReadDeadline(time.Now().Add(time.Second)); err != nil {
|
||||||
|
t.Fatalf("failed to set read deadline: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var message wsMessage
|
||||||
|
if err := conn.ReadJSON(&message); err != nil {
|
||||||
|
t.Fatalf("failed to read websocket message: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if message.Type != messageType {
|
||||||
|
t.Fatalf("unexpected message type: got %q, want %q", message.Type, messageType)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, ok := message.Data.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("unexpected message data type: %T", message.Data)
|
||||||
|
}
|
||||||
|
if data["EntityId"] != entityId {
|
||||||
|
t.Fatalf("unexpected entity id: got %v, want %q", data["EntityId"], entityId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertNoWebsocketMessage(t *testing.T, conn *websocket.Conn) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
if err := conn.SetReadDeadline(time.Now().Add(100 * time.Millisecond)); err != nil {
|
||||||
|
t.Fatalf("failed to set read deadline: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var message wsMessage
|
||||||
|
if err := conn.ReadJSON(&message); err == nil {
|
||||||
|
t.Fatalf("unexpected websocket message: %+v", message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion test-helper
|
||||||
|
|
||||||
|
func TestWebsocketEndpointAllowsOwnPeerStatsForNonAdmin(t *testing.T) {
|
||||||
|
bus := evbus.New(10)
|
||||||
|
conn, cleanup := newTestWebsocketConnection(t, bus, &domain.ContextUserInfo{Id: "user-a"},
|
||||||
|
map[domain.PeerIdentifier]*domain.Peer{
|
||||||
|
"own-peer": {Identifier: "own-peer", UserIdentifier: "user-a"},
|
||||||
|
})
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
bus.Publish(app.TopicPeerStatsUpdated, domain.TrafficDelta{EntityId: "own-peer", BytesReceivedPerSecond: 1})
|
||||||
|
assertWebsocketMessage(t, conn, "peer_stats", "own-peer")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWebsocketEndpointCleansExpiredPeerUserIdentifierCache(t *testing.T) {
|
||||||
|
now := time.Now()
|
||||||
|
endpoint := &WebsocketEndpoint{
|
||||||
|
ownershipCache: map[domain.PeerIdentifier]peerUserIdentifierCacheEntry{
|
||||||
|
"expired-peer": {
|
||||||
|
userIdentifier: "user-a",
|
||||||
|
expiresAt: now.Add(-time.Second),
|
||||||
|
},
|
||||||
|
"active-peer": {
|
||||||
|
userIdentifier: "user-b",
|
||||||
|
expiresAt: now.Add(time.Second),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint.cleanupOwnerCache(now)
|
||||||
|
|
||||||
|
if _, ok := endpoint.ownershipCache["expired-peer"]; ok {
|
||||||
|
t.Fatal("expired peer cache entry was not removed")
|
||||||
|
}
|
||||||
|
if _, ok := endpoint.ownershipCache["active-peer"]; !ok {
|
||||||
|
t.Fatal("active peer cache entry was removed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWebsocketEndpointFiltersOtherPeerStatsForNonAdmin(t *testing.T) {
|
||||||
|
bus := evbus.New(10)
|
||||||
|
conn, cleanup := newTestWebsocketConnection(t, bus, &domain.ContextUserInfo{Id: "user-a"},
|
||||||
|
map[domain.PeerIdentifier]*domain.Peer{
|
||||||
|
"other-peer": {Identifier: "other-peer", UserIdentifier: "user-b"},
|
||||||
|
})
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
bus.Publish(app.TopicPeerStatsUpdated, domain.TrafficDelta{EntityId: "other-peer", BytesReceivedPerSecond: 1})
|
||||||
|
assertNoWebsocketMessage(t, conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWebsocketEndpointFiltersUnknownPeerStatsForNonAdmin(t *testing.T) {
|
||||||
|
bus := evbus.New(10)
|
||||||
|
conn, cleanup := newTestWebsocketConnection(t, bus, &domain.ContextUserInfo{Id: "user-a"},
|
||||||
|
map[domain.PeerIdentifier]*domain.Peer{
|
||||||
|
"other-peer": {Identifier: "other-peer", UserIdentifier: ""},
|
||||||
|
})
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
bus.Publish(app.TopicPeerStatsUpdated, domain.TrafficDelta{EntityId: "other-peer", BytesReceivedPerSecond: 1})
|
||||||
|
assertNoWebsocketMessage(t, conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWebsocketEndpointFiltersUnknownPeerStatsForNonAdmin2(t *testing.T) {
|
||||||
|
bus := evbus.New(10)
|
||||||
|
conn, cleanup := newTestWebsocketConnection(t, bus, &domain.ContextUserInfo{Id: "user-a"}, nil)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
bus.Publish(app.TopicPeerStatsUpdated, domain.TrafficDelta{EntityId: "unknown-peer", BytesReceivedPerSecond: 1})
|
||||||
|
assertNoWebsocketMessage(t, conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWebsocketEndpointFiltersInterfaceStatsForNonAdmin(t *testing.T) {
|
||||||
|
bus := evbus.New(10)
|
||||||
|
conn, cleanup := newTestWebsocketConnection(t, bus, &domain.ContextUserInfo{Id: "user-a"}, nil)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
bus.Publish(app.TopicInterfaceStatsUpdated, domain.TrafficDelta{EntityId: "wg0", BytesReceivedPerSecond: 1})
|
||||||
|
assertNoWebsocketMessage(t, conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWebsocketEndpointAllowsAllStatsForAdmin(t *testing.T) {
|
||||||
|
bus := evbus.New(10)
|
||||||
|
conn, cleanup := newTestWebsocketConnection(t, bus, &domain.ContextUserInfo{Id: "admin", IsAdmin: true}, nil)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
bus.Publish(app.TopicPeerStatsUpdated, domain.TrafficDelta{EntityId: "other-peer", BytesReceivedPerSecond: 1})
|
||||||
|
assertWebsocketMessage(t, conn, "peer_stats", "other-peer")
|
||||||
|
|
||||||
|
bus.Publish(app.TopicInterfaceStatsUpdated, domain.TrafficDelta{EntityId: "wg0", BytesReceivedPerSecond: 1})
|
||||||
|
assertWebsocketMessage(t, conn, "interface_stats", "wg0")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_matchOrigin(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
externalBaseUrl string
|
||||||
|
origin string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "matching origin",
|
||||||
|
externalBaseUrl: "https://example.com",
|
||||||
|
origin: "https://example.com",
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "matching origin with path",
|
||||||
|
externalBaseUrl: "https://example.com/app1",
|
||||||
|
origin: "https://example.com/app2",
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-matching origin with different host",
|
||||||
|
externalBaseUrl: "https://example.com",
|
||||||
|
origin: "https://example.com.malicious.com",
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-matching origin with different scheme",
|
||||||
|
externalBaseUrl: "https://example.com",
|
||||||
|
origin: "http://example.com",
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid origin URL",
|
||||||
|
externalBaseUrl: "https://example.com",
|
||||||
|
origin: "://invalid-url",
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid externalBaseUrl",
|
||||||
|
externalBaseUrl: "://invalid-url",
|
||||||
|
origin: "https://example.com",
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := matchOrigin(tt.externalBaseUrl, tt.origin)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("matchOrigin() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,11 +26,12 @@ type SessionData struct {
|
|||||||
Lastname string
|
Lastname string
|
||||||
Email string
|
Email string
|
||||||
|
|
||||||
OauthState string
|
OauthState string
|
||||||
OauthNonce string
|
OauthNonce string
|
||||||
OauthProvider string
|
OauthCodeVerifier string
|
||||||
OauthReturnTo string
|
OauthProvider string
|
||||||
OauthIdToken string
|
OauthReturnTo string
|
||||||
|
OauthIdToken string
|
||||||
|
|
||||||
WebAuthnData string
|
WebAuthnData string
|
||||||
|
|
||||||
@@ -80,16 +81,17 @@ func (s *SessionWrapper) DestroyData(ctx context.Context) {
|
|||||||
|
|
||||||
func (s *SessionWrapper) defaultSessionData() SessionData {
|
func (s *SessionWrapper) defaultSessionData() SessionData {
|
||||||
return SessionData{
|
return SessionData{
|
||||||
LoggedIn: false,
|
LoggedIn: false,
|
||||||
IsAdmin: false,
|
IsAdmin: false,
|
||||||
UserIdentifier: "",
|
UserIdentifier: "",
|
||||||
Firstname: "",
|
Firstname: "",
|
||||||
Lastname: "",
|
Lastname: "",
|
||||||
Email: "",
|
Email: "",
|
||||||
OauthState: "",
|
OauthState: "",
|
||||||
OauthNonce: "",
|
OauthNonce: "",
|
||||||
OauthProvider: "",
|
OauthCodeVerifier: "",
|
||||||
OauthReturnTo: "",
|
OauthProvider: "",
|
||||||
OauthIdToken: "",
|
OauthReturnTo: "",
|
||||||
|
OauthIdToken: "",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,11 @@ const (
|
|||||||
AuthenticatorTypeOidc AuthenticatorType = "oidc"
|
AuthenticatorTypeOidc AuthenticatorType = "oidc"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
pkceMethodS256 = "S256" // SHA-256 hashing
|
||||||
|
pkceMethodPlain = "plain" // plain text
|
||||||
|
)
|
||||||
|
|
||||||
// AuthenticatorOauth is the interface for all OAuth authenticators.
|
// AuthenticatorOauth is the interface for all OAuth authenticators.
|
||||||
type AuthenticatorOauth interface {
|
type AuthenticatorOauth interface {
|
||||||
// GetName returns the name of the authenticator.
|
// GetName returns the name of the authenticator.
|
||||||
@@ -70,6 +75,10 @@ type AuthenticatorOauth interface {
|
|||||||
GetAllowedUserGroups() []string
|
GetAllowedUserGroups() []string
|
||||||
// GetLogoutUrl returns an IdP logout URL if supported by the provider.
|
// GetLogoutUrl returns an IdP logout URL if supported by the provider.
|
||||||
GetLogoutUrl(idTokenHint, postLogoutRedirectUri string) (string, bool)
|
GetLogoutUrl(idTokenHint, postLogoutRedirectUri string) (string, bool)
|
||||||
|
// PKCEAuthCodeOptions returns PKCE options for the authorization request and the verifier for the token exchange.
|
||||||
|
PKCEAuthCodeOptions() ([]oauth2.AuthCodeOption, string)
|
||||||
|
// PKCETokenOptions returns PKCE options for the token exchange.
|
||||||
|
PKCETokenOptions(verifier string) []oauth2.AuthCodeOption
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuthenticatorLdap is the interface for all LDAP authenticators.
|
// AuthenticatorLdap is the interface for all LDAP authenticators.
|
||||||
@@ -448,30 +457,34 @@ func (a *Authenticator) passwordAuthentication(
|
|||||||
|
|
||||||
// OauthLoginStep1 starts the oauth authentication flow by returning the authentication URL, state and nonce.
|
// OauthLoginStep1 starts the oauth authentication flow by returning the authentication URL, state and nonce.
|
||||||
func (a *Authenticator) OauthLoginStep1(_ context.Context, providerId string) (
|
func (a *Authenticator) OauthLoginStep1(_ context.Context, providerId string) (
|
||||||
authCodeUrl, state, nonce string,
|
authCodeUrl, state, nonce, codeVerifier string,
|
||||||
err error,
|
err error,
|
||||||
) {
|
) {
|
||||||
oauthProvider, ok := a.oauthAuthenticators[providerId]
|
oauthProvider, ok := a.oauthAuthenticators[providerId]
|
||||||
if !ok {
|
if !ok {
|
||||||
return "", "", "", fmt.Errorf("missing oauth provider %s", providerId)
|
return "", "", "", "", fmt.Errorf("missing oauth provider %s", providerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare authentication flow, set state cookies
|
// Prepare authentication flow, set state cookies
|
||||||
state, err = a.randString(16)
|
state, err = a.randString(16)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", "", fmt.Errorf("failed to generate state: %w", err)
|
return "", "", "", "", fmt.Errorf("failed to generate state: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate PKCE code verifier and challenge if enabled. Otherwise, options will be empty.
|
||||||
|
authCodeOptions, codeVerifier := oauthProvider.PKCEAuthCodeOptions()
|
||||||
|
|
||||||
switch oauthProvider.GetType() {
|
switch oauthProvider.GetType() {
|
||||||
case AuthenticatorTypeOAuth:
|
case AuthenticatorTypeOAuth:
|
||||||
authCodeUrl = oauthProvider.AuthCodeURL(state)
|
authCodeUrl = oauthProvider.AuthCodeURL(state, authCodeOptions...)
|
||||||
case AuthenticatorTypeOidc:
|
case AuthenticatorTypeOidc:
|
||||||
nonce, err = a.randString(16)
|
nonce, err = a.randString(16)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", "", fmt.Errorf("failed to generate nonce: %w", err)
|
return "", "", "", "", fmt.Errorf("failed to generate nonce: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
authCodeUrl = oauthProvider.AuthCodeURL(state, oidc.Nonce(nonce))
|
authCodeOptions = append(authCodeOptions, oidc.Nonce(nonce))
|
||||||
|
authCodeUrl = oauthProvider.AuthCodeURL(state, authCodeOptions...)
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -531,13 +544,16 @@ func isAnyAllowedUserGroup(userGroups, allowedUserGroups []string) bool {
|
|||||||
|
|
||||||
// OauthLoginStep2 finishes the oauth authentication flow by exchanging the code for an access token and
|
// OauthLoginStep2 finishes the oauth authentication flow by exchanging the code for an access token and
|
||||||
// fetching the user information.
|
// fetching the user information.
|
||||||
func (a *Authenticator) OauthLoginStep2(ctx context.Context, providerId, nonce, code string) (*domain.User, string, error) {
|
func (a *Authenticator) OauthLoginStep2(
|
||||||
|
ctx context.Context,
|
||||||
|
providerId, nonce, code, codeVerifier string,
|
||||||
|
) (*domain.User, string, error) {
|
||||||
oauthProvider, ok := a.oauthAuthenticators[providerId]
|
oauthProvider, ok := a.oauthAuthenticators[providerId]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, "", fmt.Errorf("missing oauth provider %s", providerId)
|
return nil, "", fmt.Errorf("missing oauth provider %s", providerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
oauth2Token, err := oauthProvider.Exchange(ctx, code)
|
oauth2Token, err := oauthProvider.Exchange(ctx, code, oauthProvider.PKCETokenOptions(codeVerifier)...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", fmt.Errorf("unable to exchange code: %w", err)
|
return nil, "", fmt.Errorf("unable to exchange code: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ type PlainOauthAuthenticator struct {
|
|||||||
sensitiveInfoLogging bool
|
sensitiveInfoLogging bool
|
||||||
allowedDomains []string
|
allowedDomains []string
|
||||||
allowedUserGroups []string
|
allowedUserGroups []string
|
||||||
|
usePKCE bool
|
||||||
|
pkceMethod string
|
||||||
}
|
}
|
||||||
|
|
||||||
func newPlainOauthAuthenticator(
|
func newPlainOauthAuthenticator(
|
||||||
@@ -62,6 +64,14 @@ func newPlainOauthAuthenticator(
|
|||||||
provider.sensitiveInfoLogging = cfg.LogSensitiveInfo
|
provider.sensitiveInfoLogging = cfg.LogSensitiveInfo
|
||||||
provider.allowedDomains = cfg.AllowedDomains
|
provider.allowedDomains = cfg.AllowedDomains
|
||||||
provider.allowedUserGroups = cfg.AllowedUserGroups
|
provider.allowedUserGroups = cfg.AllowedUserGroups
|
||||||
|
provider.usePKCE = cfg.UsePKCE == nil || *cfg.UsePKCE
|
||||||
|
provider.pkceMethod = cfg.PKCEMethod
|
||||||
|
if provider.pkceMethod == "" {
|
||||||
|
provider.pkceMethod = pkceMethodS256
|
||||||
|
}
|
||||||
|
if provider.usePKCE && provider.pkceMethod != pkceMethodS256 && provider.pkceMethod != pkceMethodPlain {
|
||||||
|
return nil, fmt.Errorf("unsupported PKCE method %q, allowed: S256, plain", provider.pkceMethod)
|
||||||
|
}
|
||||||
|
|
||||||
return provider, nil
|
return provider, nil
|
||||||
}
|
}
|
||||||
@@ -83,6 +93,32 @@ func (p PlainOauthAuthenticator) GetLogoutUrl(_, _ string) (string, bool) {
|
|||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PKCEAuthCodeOptions returns PKCE options for the authorization request and the verifier for the token exchange.
|
||||||
|
func (p PlainOauthAuthenticator) PKCEAuthCodeOptions() ([]oauth2.AuthCodeOption, string) {
|
||||||
|
if !p.usePKCE {
|
||||||
|
return nil, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
verifier := oauth2.GenerateVerifier()
|
||||||
|
if p.pkceMethod == pkceMethodPlain {
|
||||||
|
return []oauth2.AuthCodeOption{
|
||||||
|
oauth2.SetAuthURLParam("code_challenge", verifier),
|
||||||
|
oauth2.SetAuthURLParam("code_challenge_method", pkceMethodPlain),
|
||||||
|
}, verifier
|
||||||
|
}
|
||||||
|
|
||||||
|
return []oauth2.AuthCodeOption{oauth2.S256ChallengeOption(verifier)}, verifier
|
||||||
|
}
|
||||||
|
|
||||||
|
// PKCETokenOptions returns PKCE options for the token exchange.
|
||||||
|
func (p PlainOauthAuthenticator) PKCETokenOptions(verifier string) []oauth2.AuthCodeOption {
|
||||||
|
if !p.usePKCE || verifier == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return []oauth2.AuthCodeOption{oauth2.VerifierOption(verifier)}
|
||||||
|
}
|
||||||
|
|
||||||
// RegistrationEnabled returns whether registration is enabled for the OAuth authenticator.
|
// RegistrationEnabled returns whether registration is enabled for the OAuth authenticator.
|
||||||
func (p PlainOauthAuthenticator) RegistrationEnabled() bool {
|
func (p PlainOauthAuthenticator) RegistrationEnabled() bool {
|
||||||
return p.registrationEnabled
|
return p.registrationEnabled
|
||||||
|
|||||||
61
internal/app/auth/auth_oauth_test.go
Normal file
61
internal/app/auth/auth_oauth_test.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPlainOauthAuthenticatorPKCES256Options(t *testing.T) {
|
||||||
|
authenticator := PlainOauthAuthenticator{usePKCE: true, pkceMethod: "S256"}
|
||||||
|
|
||||||
|
options, verifier := authenticator.PKCEAuthCodeOptions()
|
||||||
|
if verifier == "" {
|
||||||
|
t.Fatal("expected verifier")
|
||||||
|
}
|
||||||
|
|
||||||
|
values := authCodeValues(t, options)
|
||||||
|
|
||||||
|
if values.Get("code_challenge") == "" {
|
||||||
|
t.Fatal("expected code_challenge")
|
||||||
|
}
|
||||||
|
if values.Get("code_challenge_method") != "S256" {
|
||||||
|
t.Fatalf("expected S256 challenge method, got %q", values.Get("code_challenge_method"))
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenOptions := authenticator.PKCETokenOptions(verifier)
|
||||||
|
if len(tokenOptions) != 1 {
|
||||||
|
t.Fatalf("expected one token option, got %d", len(tokenOptions))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPlainOauthAuthenticatorPKCEPlainOptions(t *testing.T) {
|
||||||
|
authenticator := PlainOauthAuthenticator{usePKCE: true, pkceMethod: "plain"}
|
||||||
|
|
||||||
|
options, verifier := authenticator.PKCEAuthCodeOptions()
|
||||||
|
values := authCodeValues(t, options)
|
||||||
|
|
||||||
|
if values.Get("code_challenge") != verifier {
|
||||||
|
t.Fatalf("expected plain challenge %q, got %q", verifier, values.Get("code_challenge"))
|
||||||
|
}
|
||||||
|
if values.Get("code_challenge_method") != "plain" {
|
||||||
|
t.Fatalf("expected plain challenge method, got %q", values.Get("code_challenge_method"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPlainOauthAuthenticatorPKCEDisabled(t *testing.T) {
|
||||||
|
authenticator := PlainOauthAuthenticator{usePKCE: false, pkceMethod: "S256"}
|
||||||
|
|
||||||
|
options, verifier := authenticator.PKCEAuthCodeOptions()
|
||||||
|
if len(options) != 0 {
|
||||||
|
t.Fatalf("expected no auth code options, got %d", len(options))
|
||||||
|
}
|
||||||
|
if verifier != "" {
|
||||||
|
t.Fatalf("expected empty verifier, got %q", verifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenOptions := authenticator.PKCETokenOptions(oauth2.GenerateVerifier())
|
||||||
|
if len(tokenOptions) != 0 {
|
||||||
|
t.Fatalf("expected no token options, got %d", len(tokenOptions))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,6 +30,8 @@ type OidcAuthenticator struct {
|
|||||||
allowedUserGroups []string
|
allowedUserGroups []string
|
||||||
endSessionEndpoint string
|
endSessionEndpoint string
|
||||||
logoutIdpSession bool
|
logoutIdpSession bool
|
||||||
|
usePKCE bool
|
||||||
|
pkceMethod string
|
||||||
}
|
}
|
||||||
|
|
||||||
func newOidcAuthenticator(
|
func newOidcAuthenticator(
|
||||||
@@ -67,6 +69,14 @@ func newOidcAuthenticator(
|
|||||||
provider.allowedDomains = cfg.AllowedDomains
|
provider.allowedDomains = cfg.AllowedDomains
|
||||||
provider.allowedUserGroups = cfg.AllowedUserGroups
|
provider.allowedUserGroups = cfg.AllowedUserGroups
|
||||||
provider.logoutIdpSession = cfg.LogoutIdpSession == nil || *cfg.LogoutIdpSession
|
provider.logoutIdpSession = cfg.LogoutIdpSession == nil || *cfg.LogoutIdpSession
|
||||||
|
provider.usePKCE = cfg.UsePKCE == nil || *cfg.UsePKCE
|
||||||
|
provider.pkceMethod = cfg.PKCEMethod
|
||||||
|
if provider.pkceMethod == "" {
|
||||||
|
provider.pkceMethod = pkceMethodS256
|
||||||
|
}
|
||||||
|
if provider.usePKCE && provider.pkceMethod != pkceMethodS256 && provider.pkceMethod != pkceMethodPlain {
|
||||||
|
return nil, fmt.Errorf("unsupported PKCE method %q, allowed: S256, plain", provider.pkceMethod)
|
||||||
|
}
|
||||||
|
|
||||||
var providerMetadata struct {
|
var providerMetadata struct {
|
||||||
EndSessionEndpoint string `json:"end_session_endpoint"`
|
EndSessionEndpoint string `json:"end_session_endpoint"`
|
||||||
@@ -121,6 +131,32 @@ func (o OidcAuthenticator) GetLogoutUrl(idTokenHint, postLogoutRedirectUri strin
|
|||||||
return logoutUrl.String(), true
|
return logoutUrl.String(), true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PKCEAuthCodeOptions returns PKCE options for the authorization request and the verifier for the token exchange.
|
||||||
|
func (o OidcAuthenticator) PKCEAuthCodeOptions() ([]oauth2.AuthCodeOption, string) {
|
||||||
|
if !o.usePKCE {
|
||||||
|
return nil, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
verifier := oauth2.GenerateVerifier()
|
||||||
|
if o.pkceMethod == pkceMethodPlain {
|
||||||
|
return []oauth2.AuthCodeOption{
|
||||||
|
oauth2.SetAuthURLParam("code_challenge", verifier),
|
||||||
|
oauth2.SetAuthURLParam("code_challenge_method", pkceMethodPlain),
|
||||||
|
}, verifier
|
||||||
|
}
|
||||||
|
|
||||||
|
return []oauth2.AuthCodeOption{oauth2.S256ChallengeOption(verifier)}, verifier
|
||||||
|
}
|
||||||
|
|
||||||
|
// PKCETokenOptions returns PKCE options for the token exchange.
|
||||||
|
func (o OidcAuthenticator) PKCETokenOptions(verifier string) []oauth2.AuthCodeOption {
|
||||||
|
if !o.usePKCE || verifier == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return []oauth2.AuthCodeOption{oauth2.VerifierOption(verifier)}
|
||||||
|
}
|
||||||
|
|
||||||
// RegistrationEnabled returns whether registration is enabled for this authenticator.
|
// RegistrationEnabled returns whether registration is enabled for this authenticator.
|
||||||
func (o OidcAuthenticator) RegistrationEnabled() bool {
|
func (o OidcAuthenticator) RegistrationEnabled() bool {
|
||||||
return o.registrationEnabled
|
return o.registrationEnabled
|
||||||
@@ -144,7 +180,7 @@ func (o OidcAuthenticator) Exchange(ctx context.Context, code string, opts ...oa
|
|||||||
return o.cfg.Exchange(ctx, code, opts...)
|
return o.cfg.Exchange(ctx, code, opts...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserInfo retrieves the user info from the token.
|
// GetUserInfo retrieves the user info from the token and the userinfo endpoint.
|
||||||
func (o OidcAuthenticator) GetUserInfo(ctx context.Context, token *oauth2.Token, nonce string) (
|
func (o OidcAuthenticator) GetUserInfo(ctx context.Context, token *oauth2.Token, nonce string) (
|
||||||
map[string]any,
|
map[string]any,
|
||||||
error,
|
error,
|
||||||
@@ -182,6 +218,41 @@ func (o OidcAuthenticator) GetUserInfo(ctx context.Context, token *oauth2.Token,
|
|||||||
return nil, fmt.Errorf("failed to parse extra claims: %w", err)
|
return nil, fmt.Errorf("failed to parse extra claims: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch additional user information from the userinfo endpoint
|
||||||
|
userInfo, err := o.provider.UserInfo(ctx, oauth2.StaticTokenSource(token))
|
||||||
|
if err != nil {
|
||||||
|
if o.sensitiveInfoLogging {
|
||||||
|
slog.Debug("OIDC: failed to fetch user info from endpoint", "provider", o.name, "error", err)
|
||||||
|
}
|
||||||
|
// Don't fail the entire flow if userinfo endpoint is unavailable;
|
||||||
|
// ID token claims may be sufficient
|
||||||
|
slog.Debug("OIDC: proceeding with ID token claims only", "provider", o.name)
|
||||||
|
} else {
|
||||||
|
// Parse claims from userinfo endpoint response
|
||||||
|
var userInfoFields map[string]any
|
||||||
|
if err = userInfo.Claims(&userInfoFields); err != nil {
|
||||||
|
if o.sensitiveInfoLogging {
|
||||||
|
slog.Debug("OIDC: failed to parse userinfo claims", "provider", o.name, "error", err)
|
||||||
|
}
|
||||||
|
// Don't fail if we can't parse userinfo; continue with ID token claims
|
||||||
|
slog.Debug("OIDC: proceeding with ID token claims only", "provider", o.name)
|
||||||
|
} else {
|
||||||
|
// Merge userinfo fields into tokenFields, preferring ID token claims
|
||||||
|
for key, value := range userInfoFields {
|
||||||
|
if _, exists := tokenFields[key]; !exists {
|
||||||
|
tokenFields[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.userInfoLogging {
|
||||||
|
contents, _ := json.Marshal(userInfoFields)
|
||||||
|
slog.Debug("OIDC: user info from endpoint",
|
||||||
|
"source", o.name,
|
||||||
|
"info", string(contents))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if o.userInfoLogging {
|
if o.userInfoLogging {
|
||||||
contents, _ := json.Marshal(tokenFields)
|
contents, _ := json.Marshal(tokenFields)
|
||||||
slog.Debug("OIDC: user info debug",
|
slog.Debug("OIDC: user info debug",
|
||||||
|
|||||||
79
internal/app/auth/auth_oidc_test.go
Normal file
79
internal/app/auth/auth_oidc_test.go
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func authCodeValues(t *testing.T, options []oauth2.AuthCodeOption) url.Values {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
config := oauth2.Config{
|
||||||
|
ClientID: "client-id",
|
||||||
|
Endpoint: oauth2.Endpoint{AuthURL: "https://example.com/auth"},
|
||||||
|
RedirectURL: "https://wg.example.com/callback",
|
||||||
|
}
|
||||||
|
authCodeURL, err := url.Parse(config.AuthCodeURL("state", options...))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to parse auth code URL: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return authCodeURL.Query()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOidcAuthenticatorPKCES256Options(t *testing.T) {
|
||||||
|
authenticator := OidcAuthenticator{usePKCE: true, pkceMethod: "S256"}
|
||||||
|
|
||||||
|
options, verifier := authenticator.PKCEAuthCodeOptions()
|
||||||
|
if verifier == "" {
|
||||||
|
t.Fatal("expected verifier")
|
||||||
|
}
|
||||||
|
|
||||||
|
values := authCodeValues(t, options)
|
||||||
|
|
||||||
|
if values.Get("code_challenge") == "" {
|
||||||
|
t.Fatal("expected code_challenge")
|
||||||
|
}
|
||||||
|
if values.Get("code_challenge_method") != "S256" {
|
||||||
|
t.Fatalf("expected S256 challenge method, got %q", values.Get("code_challenge_method"))
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenOptions := authenticator.PKCETokenOptions(verifier)
|
||||||
|
if len(tokenOptions) != 1 {
|
||||||
|
t.Fatalf("expected one token option, got %d", len(tokenOptions))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOidcAuthenticatorPKCEPlainOptions(t *testing.T) {
|
||||||
|
authenticator := OidcAuthenticator{usePKCE: true, pkceMethod: "plain"}
|
||||||
|
|
||||||
|
options, verifier := authenticator.PKCEAuthCodeOptions()
|
||||||
|
values := authCodeValues(t, options)
|
||||||
|
|
||||||
|
if values.Get("code_challenge") != verifier {
|
||||||
|
t.Fatalf("expected plain challenge %q, got %q", verifier, values.Get("code_challenge"))
|
||||||
|
}
|
||||||
|
if values.Get("code_challenge_method") != "plain" {
|
||||||
|
t.Fatalf("expected plain challenge method, got %q", values.Get("code_challenge_method"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOidcAuthenticatorPKCEDisabled(t *testing.T) {
|
||||||
|
authenticator := OidcAuthenticator{usePKCE: false, pkceMethod: "S256"}
|
||||||
|
|
||||||
|
options, verifier := authenticator.PKCEAuthCodeOptions()
|
||||||
|
if len(options) != 0 {
|
||||||
|
t.Fatalf("expected no auth code options, got %d", len(options))
|
||||||
|
}
|
||||||
|
if verifier != "" {
|
||||||
|
t.Fatalf("expected empty verifier, got %q", verifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenOptions := authenticator.PKCETokenOptions(oauth2.GenerateVerifier())
|
||||||
|
if len(tokenOptions) != 0 {
|
||||||
|
t.Fatalf("expected no token options, got %d", len(tokenOptions))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -893,16 +893,7 @@ func (m Manager) importInterface(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// try to predict the interface type based on the number of peers
|
iface.Type = inferImportedInterfaceType(iface, peers)
|
||||||
switch len(peers) {
|
|
||||||
case 0:
|
|
||||||
iface.Type = domain.InterfaceTypeAny // no peers means this is an unknown interface
|
|
||||||
case 1:
|
|
||||||
iface.Type = domain.InterfaceTypeClient // one peer means this is a client interface
|
|
||||||
default: // multiple peers means this is a server interface
|
|
||||||
|
|
||||||
iface.Type = domain.InterfaceTypeServer
|
|
||||||
}
|
|
||||||
|
|
||||||
existingInterface, err := m.db.GetInterface(ctx, iface.Identifier)
|
existingInterface, err := m.db.GetInterface(ctx, iface.Identifier)
|
||||||
if err != nil && !errors.Is(err, domain.ErrNotFound) {
|
if err != nil && !errors.Is(err, domain.ErrNotFound) {
|
||||||
@@ -930,6 +921,20 @@ func (m Manager) importInterface(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func inferImportedInterfaceType(iface *domain.Interface, peers []domain.PhysicalPeer) domain.InterfaceType {
|
||||||
|
switch len(peers) {
|
||||||
|
case 0:
|
||||||
|
return domain.InterfaceTypeAny // no peers means this is an unknown interface
|
||||||
|
case 1:
|
||||||
|
if iface.ListenPort > 0 {
|
||||||
|
return domain.InterfaceTypeServer // a listening interface with one peer is commonly a site-to-site server
|
||||||
|
}
|
||||||
|
return domain.InterfaceTypeClient
|
||||||
|
default: // multiple peers means this is a server interface
|
||||||
|
return domain.InterfaceTypeServer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// extractPfsenseDefaultsFromPeers extracts common endpoint and DNS information from peers
|
// extractPfsenseDefaultsFromPeers extracts common endpoint and DNS information from peers
|
||||||
// For server interfaces, peers typically have endpoints pointing to the server, so we use the most common one
|
// For server interfaces, peers typically have endpoints pointing to the server, so we use the most common one
|
||||||
func extractPfsenseDefaultsFromPeers(peers []domain.PhysicalPeer, listenPort int) (endpoint, dns string) {
|
func extractPfsenseDefaultsFromPeers(peers []domain.PhysicalPeer, listenPort int) (endpoint, dns string) {
|
||||||
|
|||||||
@@ -10,6 +10,49 @@ import (
|
|||||||
"github.com/h44z/wg-portal/internal/domain"
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestInferImportedInterfaceType(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
listenPort int
|
||||||
|
peerCount int
|
||||||
|
expected domain.InterfaceType
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no peers stays unknown",
|
||||||
|
listenPort: 51820,
|
||||||
|
peerCount: 0,
|
||||||
|
expected: domain.InterfaceTypeAny,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "single peer with listen port is server",
|
||||||
|
listenPort: 51820,
|
||||||
|
peerCount: 1,
|
||||||
|
expected: domain.InterfaceTypeServer,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "single peer without listen port stays client",
|
||||||
|
listenPort: 0,
|
||||||
|
peerCount: 1,
|
||||||
|
expected: domain.InterfaceTypeClient,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple peers is server",
|
||||||
|
listenPort: 0,
|
||||||
|
peerCount: 2,
|
||||||
|
expected: domain.InterfaceTypeServer,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
iface := &domain.Interface{ListenPort: tt.listenPort}
|
||||||
|
peers := make([]domain.PhysicalPeer, tt.peerCount)
|
||||||
|
|
||||||
|
assert.Equal(t, tt.expected, inferImportedInterfaceType(iface, peers))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestImportPeer_AddressMapping(t *testing.T) {
|
func TestImportPeer_AddressMapping(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
@@ -279,6 +279,14 @@ type OpenIDConnectProvider struct {
|
|||||||
// This also includes OAuth tokens! Keep this disabled in production!
|
// This also includes OAuth tokens! Keep this disabled in production!
|
||||||
LogSensitiveInfo bool `yaml:"log_sensitive_info"`
|
LogSensitiveInfo bool `yaml:"log_sensitive_info"`
|
||||||
|
|
||||||
|
// UsePKCE controls whether Proof Key for Code Exchange is used during the authorization code flow.
|
||||||
|
// If unset, PKCE is enabled by default.
|
||||||
|
UsePKCE *bool `yaml:"use_pkce"`
|
||||||
|
|
||||||
|
// PKCEMethod controls which PKCE challenge method is used. Supported values are "S256" and "plain".
|
||||||
|
// If empty, "S256" is used.
|
||||||
|
PKCEMethod string `yaml:"pkce_method"`
|
||||||
|
|
||||||
// LogoutIdpSession controls whether the user's session at the OIDC provider is terminated on logout.
|
// LogoutIdpSession controls whether the user's session at the OIDC provider is terminated on logout.
|
||||||
// If set to true (default), the user will be redirected to the IdP's end_session_endpoint after local logout.
|
// If set to true (default), the user will be redirected to the IdP's end_session_endpoint after local logout.
|
||||||
// If set to false, only the local wg-portal session is invalidated.
|
// If set to false, only the local wg-portal session is invalidated.
|
||||||
@@ -332,6 +340,14 @@ type OAuthProvider struct {
|
|||||||
// If LogSensitiveInfo is set to true, sensitive information retrieved from the OAuth provider will be logged in trace level.
|
// If LogSensitiveInfo is set to true, sensitive information retrieved from the OAuth provider will be logged in trace level.
|
||||||
// This also includes OAuth tokens! Keep this disabled in production!
|
// This also includes OAuth tokens! Keep this disabled in production!
|
||||||
LogSensitiveInfo bool `yaml:"log_sensitive_info"`
|
LogSensitiveInfo bool `yaml:"log_sensitive_info"`
|
||||||
|
|
||||||
|
// UsePKCE controls whether Proof Key for Code Exchange is used during the authorization code flow.
|
||||||
|
// If unset, PKCE is enabled by default.
|
||||||
|
UsePKCE *bool `yaml:"use_pkce"`
|
||||||
|
|
||||||
|
// PKCEMethod controls which PKCE challenge method is used. Supported values are "S256" and "plain".
|
||||||
|
// If empty, "S256" is used.
|
||||||
|
PKCEMethod string `yaml:"pkce_method"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebauthnConfig contains the configuration for the WebAuthn authenticator.
|
// WebauthnConfig contains the configuration for the WebAuthn authenticator.
|
||||||
|
|||||||
@@ -57,6 +57,9 @@ type EmptyResponse struct{}
|
|||||||
|
|
||||||
func (JsonObject GenericJsonObject) GetString(key string) string {
|
func (JsonObject GenericJsonObject) GetString(key string) string {
|
||||||
if value, ok := JsonObject[key]; ok {
|
if value, ok := JsonObject[key]; ok {
|
||||||
|
if value == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
if strValue, ok := value.(string); ok {
|
if strValue, ok := value.(string); ok {
|
||||||
return strValue
|
return strValue
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import (
|
|||||||
// region models
|
// region models
|
||||||
|
|
||||||
const (
|
const (
|
||||||
PfsenseApiStatusOk = "ok" // pfSense REST API uses "ok" in response
|
PfsenseApiStatusOk = "ok" // pfSense REST API uses "ok" in response
|
||||||
PfsenseApiStatusError = "error"
|
PfsenseApiStatusError = "error"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -37,8 +37,8 @@ const (
|
|||||||
type PfsenseApiResponse[T any] struct {
|
type PfsenseApiResponse[T any] struct {
|
||||||
Status string
|
Status string
|
||||||
Code int
|
Code int
|
||||||
Data T `json:"data,omitempty"`
|
Data T `json:"data,omitempty"`
|
||||||
Error *PfsenseApiError `json:"error,omitempty"`
|
Error *PfsenseApiError `json:"error,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PfsenseApiError struct {
|
type PfsenseApiError struct {
|
||||||
@@ -193,6 +193,7 @@ func (p *PfsenseApiClient) preparePayloadRequest(
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to marshal payload: %w", err)
|
return nil, fmt.Errorf("failed to marshal payload: %w", err)
|
||||||
}
|
}
|
||||||
|
p.debugLog("Prepared payload", "payload", string(payloadBytes))
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, method, fullUrl, bytes.NewReader(payloadBytes))
|
req, err := http.NewRequestWithContext(ctx, method, fullUrl, bytes.NewReader(payloadBytes))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -405,11 +406,12 @@ func (p *PfsenseApiClient) Update(
|
|||||||
func (p *PfsenseApiClient) Delete(
|
func (p *PfsenseApiClient) Delete(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
command string,
|
command string,
|
||||||
|
opts *PfsenseRequestOptions,
|
||||||
) PfsenseApiResponse[EmptyResponse] {
|
) PfsenseApiResponse[EmptyResponse] {
|
||||||
apiCtx, cancel := context.WithTimeout(ctx, p.cfg.GetApiTimeout())
|
apiCtx, cancel := context.WithTimeout(ctx, p.cfg.GetApiTimeout())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
fullUrl := p.getFullPath(command)
|
fullUrl := opts.GetPath(p.getFullPath(command))
|
||||||
|
|
||||||
req, err := p.prepareDeleteRequest(apiCtx, fullUrl)
|
req, err := p.prepareDeleteRequest(apiCtx, fullUrl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -425,4 +427,3 @@ func (p *PfsenseApiClient) Delete(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// endregion API-client
|
// endregion API-client
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user