mirror of
https://github.com/h44z/wg-portal.git
synced 2026-07-14 20:48:33 +00:00
Compare commits
1 Commits
stable
...
oauth_pkce
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c986cc74c |
@@ -135,8 +135,7 @@ 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, apiV0BackendPeers)
|
apiV0EndpointWebsocket := handlersV0.NewWebsocketEndpoint(cfg, apiV0Auth, eventBus)
|
||||||
apiV0EndpointWebsocket.StartBackgroundJobs(ctx)
|
|
||||||
|
|
||||||
apiFrontend := handlersV0.NewRestApi(apiV0Session,
|
apiFrontend := handlersV0.NewRestApi(apiV0Session,
|
||||||
apiV0EndpointAuth,
|
apiV0EndpointAuth,
|
||||||
|
|||||||
@@ -552,7 +552,6 @@ 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)*
|
||||||
@@ -640,7 +639,6 @@ 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)*
|
||||||
|
|||||||
@@ -51,31 +51,13 @@ 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 Integration
|
## Systemd Service Setup
|
||||||
|
|
||||||
> **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,15 +51,6 @@ 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. wg-portal is developed for and tested against REST API v2.8.0.
|
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.
|
||||||
|
|
||||||
### 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",
|
"build-dev": "vite build --mode development --base=/app/",
|
||||||
"build": "vite build",
|
"build": "vite build --base=/app/",
|
||||||
"preview": "vite preview --port 5050"
|
"preview": "vite preview --port 5050"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ 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.3
|
github.com/go-playground/validator/v10 v10.30.2
|
||||||
github.com/go-webauthn/webauthn v0.17.4
|
github.com/go-webauthn/webauthn v0.17.3
|
||||||
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.53.0
|
golang.org/x/crypto v0.51.0
|
||||||
golang.org/x/oauth2 v0.36.0
|
golang.org/x/oauth2 v0.36.0
|
||||||
golang.org/x/sys v0.46.0
|
golang.org/x/sys v0.44.0
|
||||||
golang.org/x/text v0.38.0
|
golang.org/x/text v0.37.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.3.0
|
pgregory.net/rapid v1.2.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.6 // indirect
|
github.com/go-webauthn/x v0.2.5 // 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.55.0 // indirect
|
golang.org/x/net v0.54.0 // indirect
|
||||||
golang.org/x/sync v0.21.0 // indirect
|
golang.org/x/sync v0.20.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,8 +3,9 @@ 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=
|
||||||
@@ -12,14 +13,17 @@ 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=
|
||||||
@@ -48,6 +52,8 @@ 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=
|
||||||
@@ -62,6 +68,8 @@ 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=
|
||||||
@@ -97,18 +105,24 @@ 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.3 h1:4MU6YkEwx7GbcPJOZxrtbu+QfF3pJLJuaYTeAH0DYy8=
|
github.com/go-playground/validator/v10 v10.30.2 h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK2xqPNk8vgvu5JQ=
|
||||||
github.com/go-playground/validator/v10 v10.30.3/go.mod h1:4Axh7oCNGcoGkqLoE4YWt6n20mcEIsPRlB7vPk3lpyc=
|
github.com/go-playground/validator/v10 v10.30.2/go.mod h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc=
|
||||||
|
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.17.4 h1:KFTSz3R2RYDiUn/0cDi3XTJgFenSG74eKTTHlqWhlxk=
|
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/go.mod h1:pZk63EE/BdztlmyS4Yc+9H5g4a8blNlbtGmdHQHbZX8=
|
github.com/go-webauthn/webauthn v0.16.4/go.mod h1:SU2ljAgToTV/YLPI0C05QS4qn+e04WpB5g1RMfcZfS4=
|
||||||
github.com/go-webauthn/x v0.2.6 h1:TEyDuQAIiEgYpx60nKiBJIX/5nSUC8LxNbH+uf5U9uk=
|
github.com/go-webauthn/webauthn v0.17.3 h1:XHZ0TXV7k8vChcE4TFgPitOPJ5cb7h1dpAeFDS0cjCo=
|
||||||
github.com/go-webauthn/x v0.2.6/go.mod h1:45bA7YEqyQhRcQJ/TiBb46Ww8yqHBGvgEhQ3WWF0aDo=
|
github.com/go-webauthn/webauthn v0.17.3/go.mod h1:PlkMgmuL9McCT7dvgBj/Sz/fgs3V6ZID6/KnFkEcPvQ=
|
||||||
|
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=
|
||||||
@@ -143,6 +157,8 @@ 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=
|
||||||
@@ -176,15 +192,21 @@ 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=
|
||||||
@@ -278,14 +300,18 @@ 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.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||||
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||||
|
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=
|
||||||
@@ -305,8 +331,10 @@ 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.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
||||||
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||||
|
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=
|
||||||
@@ -316,8 +344,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.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
golang.org/x/sync v0.20.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=
|
||||||
@@ -338,8 +366,10 @@ 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.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||||
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.43.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=
|
||||||
@@ -368,14 +398,18 @@ 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.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||||
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||||
|
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=
|
||||||
@@ -404,10 +438,12 @@ 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/cc/v4 v4.28.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
|
modernc.org/ccgo/v4 v4.32.4 h1:L5OB8rpEX4ZsXEQwGozRfJyJSFHbbNVOoQ59DU9/KuU=
|
||||||
|
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=
|
||||||
@@ -416,23 +452,28 @@ 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.3.0 h1:vBvO0VSqti75J1jjYqpgPNBLKMd1+gxa9fYo7vk/Exc=
|
pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
|
||||||
pgregory.net/rapid v1.3.0/go.mod h1:dPlE4OBBxgXPqkP79flB6sJL1dx5azpI7HQ9MY9Z7uk=
|
pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
|
||||||
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=
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ func (c *PfsenseController) GetInterface(ctx context.Context, id domain.Interfac
|
|||||||
}
|
}
|
||||||
|
|
||||||
tunnelId := wgReply.Data[0].GetString("id")
|
tunnelId := wgReply.Data[0].GetString("id")
|
||||||
|
|
||||||
// Query the specific tunnel endpoint to get full details including addresses
|
// Query the specific tunnel endpoint to get full details including addresses
|
||||||
// Endpoint: GET /api/v2/vpn/wireguard/tunnel?id={id}
|
// Endpoint: GET /api/v2/vpn/wireguard/tunnel?id={id}
|
||||||
if tunnelId != "" {
|
if tunnelId != "" {
|
||||||
@@ -227,7 +227,7 @@ func (c *PfsenseController) extractAddresses(
|
|||||||
if addrObj, ok := addrItem.(map[string]any); ok {
|
if addrObj, ok := addrItem.(map[string]any); ok {
|
||||||
address := ""
|
address := ""
|
||||||
mask := 0
|
mask := 0
|
||||||
|
|
||||||
// Extract address
|
// Extract address
|
||||||
if addrVal, ok := addrObj["address"]; ok {
|
if addrVal, ok := addrObj["address"]; ok {
|
||||||
if addrStr, ok := addrVal.(string); ok {
|
if addrStr, ok := addrVal.(string); ok {
|
||||||
@@ -236,7 +236,7 @@ func (c *PfsenseController) extractAddresses(
|
|||||||
address = fmt.Sprintf("%v", addrVal)
|
address = fmt.Sprintf("%v", addrVal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract mask
|
// Extract mask
|
||||||
if maskVal, ok := addrObj["mask"]; ok {
|
if maskVal, ok := addrObj["mask"]; ok {
|
||||||
if maskInt, ok := maskVal.(int); ok {
|
if maskInt, ok := maskVal.(int); ok {
|
||||||
@@ -249,7 +249,7 @@ func (c *PfsenseController) extractAddresses(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to CIDR format
|
// Convert to CIDR format
|
||||||
if address != "" && mask > 0 {
|
if address != "" && mask > 0 {
|
||||||
cidrStr := fmt.Sprintf("%s/%d", address, mask)
|
cidrStr := fmt.Sprintf("%s/%d", address, mask)
|
||||||
@@ -286,11 +286,11 @@ func (c *PfsenseController) extractAddresses(
|
|||||||
// Each object has "address" and "mask" fields (similar to allowedips structure)
|
// Each object has "address" and "mask" fields (similar to allowedips structure)
|
||||||
func (c *PfsenseController) parseAddressArray(addressArray []lowlevel.GenericJsonObject) []domain.Cidr {
|
func (c *PfsenseController) parseAddressArray(addressArray []lowlevel.GenericJsonObject) []domain.Cidr {
|
||||||
addresses := make([]domain.Cidr, 0, len(addressArray))
|
addresses := make([]domain.Cidr, 0, len(addressArray))
|
||||||
|
|
||||||
for _, addrObj := range addressArray {
|
for _, addrObj := range addressArray {
|
||||||
address := addrObj.GetString("address")
|
address := addrObj.GetString("address")
|
||||||
mask := addrObj.GetInt("mask")
|
mask := addrObj.GetInt("mask")
|
||||||
|
|
||||||
if address != "" && mask > 0 {
|
if address != "" && mask > 0 {
|
||||||
cidrStr := fmt.Sprintf("%s/%d", address, mask)
|
cidrStr := fmt.Sprintf("%s/%d", address, mask)
|
||||||
if cidr, err := domain.CidrFromString(cidrStr); err == nil {
|
if cidr, err := domain.CidrFromString(cidrStr); err == nil {
|
||||||
@@ -303,7 +303,7 @@ func (c *PfsenseController) parseAddressArray(addressArray []lowlevel.GenericJso
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return addresses
|
return addresses
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -405,7 +405,7 @@ func (c *PfsenseController) GetPeers(ctx context.Context, deviceId domain.Interf
|
|||||||
peerTun = peer.GetString("tunnel")
|
peerTun = peer.GetString("tunnel")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only include peers that match the requested interface name
|
// Only include peers that match the requested interface name
|
||||||
if peerTun != string(deviceId) {
|
if peerTun != string(deviceId) {
|
||||||
if c.cfg.Debug {
|
if c.cfg.Debug {
|
||||||
@@ -425,7 +425,7 @@ func (c *PfsenseController) GetPeers(ctx context.Context, deviceId domain.Interf
|
|||||||
}
|
}
|
||||||
peers = append(peers, peerModel)
|
peers = append(peers, peerModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.cfg.Debug {
|
if c.cfg.Debug {
|
||||||
slog.Debug("filtered peers for interface",
|
slog.Debug("filtered peers for interface",
|
||||||
"interface", deviceId,
|
"interface", deviceId,
|
||||||
@@ -465,7 +465,7 @@ func (c *PfsenseController) convertWireGuardPeer(peer lowlevel.GenericJsonObject
|
|||||||
if itemObj, ok := item.(map[string]any); ok {
|
if itemObj, ok := item.(map[string]any); ok {
|
||||||
address := ""
|
address := ""
|
||||||
mask := 0
|
mask := 0
|
||||||
|
|
||||||
// Extract address
|
// Extract address
|
||||||
if addrVal, ok := itemObj["address"]; ok {
|
if addrVal, ok := itemObj["address"]; ok {
|
||||||
if addrStr, ok := addrVal.(string); ok {
|
if addrStr, ok := addrVal.(string); ok {
|
||||||
@@ -474,7 +474,7 @@ func (c *PfsenseController) convertWireGuardPeer(peer lowlevel.GenericJsonObject
|
|||||||
address = fmt.Sprintf("%v", addrVal)
|
address = fmt.Sprintf("%v", addrVal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract mask
|
// Extract mask
|
||||||
if maskVal, ok := itemObj["mask"]; ok {
|
if maskVal, ok := itemObj["mask"]; ok {
|
||||||
if maskInt, ok := maskVal.(int); ok {
|
if maskInt, ok := maskVal.(int); ok {
|
||||||
@@ -487,7 +487,7 @@ func (c *PfsenseController) convertWireGuardPeer(peer lowlevel.GenericJsonObject
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to CIDR format (e.g., "10.1.2.3/32")
|
// Convert to CIDR format (e.g., "10.1.2.3/32")
|
||||||
if address != "" && mask > 0 {
|
if address != "" && mask > 0 {
|
||||||
cidrStr := fmt.Sprintf("%s/%d", address, mask)
|
cidrStr := fmt.Sprintf("%s/%d", address, mask)
|
||||||
@@ -502,7 +502,7 @@ func (c *PfsenseController) convertWireGuardPeer(peer lowlevel.GenericJsonObject
|
|||||||
allowedAddresses, _ = domain.CidrsFromString(allowedIPsStr)
|
allowedAddresses, _ = domain.CidrsFromString(allowedIPsStr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to string parsing if array parsing didn't work
|
// Fallback to string parsing if array parsing didn't work
|
||||||
if len(allowedAddresses) == 0 {
|
if len(allowedAddresses) == 0 {
|
||||||
allowedIPsStr := peer.GetString("allowedips")
|
allowedIPsStr := peer.GetString("allowedips")
|
||||||
@@ -516,7 +516,7 @@ func (c *PfsenseController) convertWireGuardPeer(peer lowlevel.GenericJsonObject
|
|||||||
|
|
||||||
endpoint := peer.GetString("endpoint")
|
endpoint := peer.GetString("endpoint")
|
||||||
port := peer.GetString("port")
|
port := peer.GetString("port")
|
||||||
|
|
||||||
// Combine endpoint and port if both are available
|
// Combine endpoint and port if both are available
|
||||||
if endpoint != "" && port != "" {
|
if endpoint != "" && port != "" {
|
||||||
// Check if endpoint already contains a port
|
// Check if endpoint already contains a port
|
||||||
@@ -617,22 +617,18 @@ func (c *PfsenseController) SaveInterface(
|
|||||||
mutex.Lock()
|
mutex.Lock()
|
||||||
defer mutex.Unlock()
|
defer mutex.Unlock()
|
||||||
|
|
||||||
physicalInterface, err := c.getInterface(ctx, id)
|
physicalInterface, err := c.getOrCreateInterface(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if physicalInterface == nil {
|
deviceId := ""
|
||||||
physicalInterface = &domain.PhysicalInterface{
|
if physicalInterface.GetExtras() != nil {
|
||||||
Identifier: id,
|
if extras, ok := physicalInterface.GetExtras().(domain.PfsenseInterfaceExtras); ok {
|
||||||
ImportSource: domain.ControllerTypePfsense,
|
deviceId = extras.Id
|
||||||
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 {
|
||||||
@@ -647,14 +643,14 @@ func (c *PfsenseController) SaveInterface(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.createOrUpdateInterface(ctx, physicalInterface); err != nil {
|
if err := c.updateInterface(ctx, physicalInterface); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *PfsenseController) getInterface(
|
func (c *PfsenseController) getOrCreateInterface(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
id domain.InterfaceIdentifier,
|
id domain.InterfaceIdentifier,
|
||||||
) (*domain.PhysicalInterface, error) {
|
) (*domain.PhysicalInterface, error) {
|
||||||
@@ -663,84 +659,50 @@ func (c *PfsenseController) getInterface(
|
|||||||
"name": string(id),
|
"name": string(id),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if wgReply.Status != lowlevel.PfsenseApiStatusOk {
|
if wgReply.Status == lowlevel.PfsenseApiStatusOk && len(wgReply.Data) > 0 {
|
||||||
return nil, fmt.Errorf("failed to query interface %s: %v", id, wgReply.Error)
|
return c.loadInterfaceData(ctx, wgReply.Data[0])
|
||||||
}
|
}
|
||||||
if len(wgReply.Data) == 0 {
|
|
||||||
return nil, nil
|
// create a new tunnel if it does not exist
|
||||||
|
// 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
type pfsenseWireGuardAddress struct {
|
func (c *PfsenseController) updateInterface(ctx context.Context, pi *domain.PhysicalInterface) error {
|
||||||
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),
|
||||||
"descr": extras.Comment,
|
"description": extras.Comment,
|
||||||
"mtu": pi.Mtu,
|
"mtu": strconv.Itoa(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),
|
||||||
}
|
}
|
||||||
|
|
||||||
addresses := make([]pfsenseWireGuardAddress, 0, len(pi.Addresses))
|
// Add addresses if present
|
||||||
for _, addr := range pi.Addresses {
|
if len(pi.Addresses) > 0 {
|
||||||
addresses = append(addresses, cidrToPfsense(&addr))
|
addresses := make([]string, 0, len(pi.Addresses))
|
||||||
}
|
for _, addr := range pi.Addresses {
|
||||||
payload["addresses"] = addresses
|
addresses = append(addresses, addr.String())
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
// Capture the newly-assigned ID so callers see it
|
payload["addresses"] = strings.Join(addresses, ",")
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interfaceIdInt, err := strconv.Atoi(interfaceId)
|
// Actual endpoint: PATCH /api/v2/vpn/wireguard/tunnel?id={id}
|
||||||
if err != nil {
|
wgReply := c.client.Update(ctx, "/api/v2/vpn/wireguard/tunnel?id="+interfaceId, payload)
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -764,10 +726,8 @@ 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}&apply=true
|
// Actual endpoint: DELETE /api/v2/vpn/wireguard/tunnel?id={id}
|
||||||
deleteReply := c.client.Delete(ctx, "/api/v2/vpn/wireguard/tunnel", &lowlevel.PfsenseRequestOptions{
|
deleteReply := c.client.Delete(ctx, "/api/v2/vpn/wireguard/tunnel?id="+interfaceId)
|
||||||
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)
|
||||||
}
|
}
|
||||||
@@ -786,22 +746,18 @@ func (c *PfsenseController) SavePeer(
|
|||||||
mutex.Lock()
|
mutex.Lock()
|
||||||
defer mutex.Unlock()
|
defer mutex.Unlock()
|
||||||
|
|
||||||
physicalPeer, err := c.getPeer(ctx, deviceId, id)
|
physicalPeer, err := c.getOrCreatePeer(ctx, deviceId, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if physicalPeer == nil {
|
peerId := ""
|
||||||
physicalPeer = &domain.PhysicalPeer{
|
if physicalPeer.GetExtras() != nil {
|
||||||
Identifier: id,
|
if extras, ok := physicalPeer.GetExtras().(domain.PfsensePeerExtras); ok {
|
||||||
KeyPair: domain.KeyPair{PublicKey: string(id)},
|
peerId = extras.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
|
||||||
@@ -814,14 +770,14 @@ func (c *PfsenseController) SavePeer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.createOrUpdatePeer(ctx, deviceId, physicalPeer); err != nil {
|
if err := c.updatePeer(ctx, deviceId, physicalPeer); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *PfsenseController) getPeer(
|
func (c *PfsenseController) getOrCreatePeer(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
deviceId domain.InterfaceIdentifier,
|
deviceId domain.InterfaceIdentifier,
|
||||||
id domain.PeerIdentifier,
|
id domain.PeerIdentifier,
|
||||||
@@ -831,25 +787,40 @@ func (c *PfsenseController) getPeer(
|
|||||||
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 && len(wgReply.Data) > 0 {
|
||||||
return nil, fmt.Errorf("failed to query peer %s for interface %s: %v", id, deviceId, wgReply.Error)
|
slog.Debug("found existing pfSense peer", "peer", id, "interface", deviceId)
|
||||||
}
|
existingPeer, err := c.convertWireGuardPeer(wgReply.Data[0])
|
||||||
if len(wgReply.Data) == 0 {
|
if err != nil {
|
||||||
return nil, nil
|
return nil, err
|
||||||
|
}
|
||||||
|
return &existingPeer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
slog.Debug("found existing pfSense peer", "peer", id, "interface", deviceId)
|
// create a new peer if it does not exist
|
||||||
existingPeer, err := c.convertWireGuardPeer(wgReply.Data[0])
|
// Actual endpoint: POST /api/v2/vpn/wireguard/peer (singular)
|
||||||
if err != nil {
|
slog.Debug("creating new pfSense peer", "peer", id, "interface", deviceId)
|
||||||
return nil, err
|
createReply := c.client.Create(ctx, "/api/v2/vpn/wireguard/peer", lowlevel.GenericJsonObject{
|
||||||
|
"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) createOrUpdatePeer(
|
func (c *PfsenseController) updatePeer(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
deviceId domain.InterfaceIdentifier,
|
deviceId domain.InterfaceIdentifier,
|
||||||
pp *domain.PhysicalPeer,
|
pp *domain.PhysicalPeer,
|
||||||
@@ -857,74 +828,36 @@ func (c *PfsenseController) createOrUpdatePeer(
|
|||||||
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{
|
||||||
"tun": string(deviceId),
|
"name": extras.Name,
|
||||||
"descr": extras.Name,
|
"description": extras.Comment,
|
||||||
"presharedkey": string(pp.PresharedKey),
|
"presharedkey": string(pp.PresharedKey),
|
||||||
"publickey": pp.KeyPair.PublicKey,
|
"publickey": pp.KeyPair.PublicKey,
|
||||||
"persistentkeepalive": pp.PersistentKeepalive,
|
"privatekey": pp.KeyPair.PrivateKey,
|
||||||
"enabled": !extras.Disabled,
|
"persistentkeepalive": strconv.Itoa(pp.PersistentKeepalive),
|
||||||
|
"disabled": strconv.FormatBool(extras.Disabled),
|
||||||
|
"allowedips": allowedIPsStr,
|
||||||
}
|
}
|
||||||
|
|
||||||
if pp.Endpoint != "" {
|
if pp.Endpoint != "" {
|
||||||
payload["endpoint"] = pp.Endpoint
|
payload["endpoint"] = pp.Endpoint
|
||||||
}
|
}
|
||||||
|
|
||||||
allowedIps := make([]pfsenseWireGuardAddress, 0, len(pp.AllowedIPs))
|
// Actual endpoint: PATCH /api/v2/vpn/wireguard/peer?id={id}
|
||||||
for _, addr := range pp.AllowedIPs {
|
wgReply := c.client.Update(ctx, "/api/v2/vpn/wireguard/peer?id="+peerId, payload)
|
||||||
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 {
|
||||||
@@ -949,7 +882,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 {
|
||||||
@@ -960,10 +893,8 @@ 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}&apply=true
|
// Actual endpoint: DELETE /api/v2/vpn/wireguard/peer?id={id}
|
||||||
deleteReply := c.client.Delete(ctx, "/api/v2/vpn/wireguard/peer", &lowlevel.PfsenseRequestOptions{
|
deleteReply := c.client.Delete(ctx, "/api/v2/vpn/wireguard/peer?id="+peerId)
|
||||||
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)
|
||||||
}
|
}
|
||||||
@@ -1045,3 +976,4 @@ func (c *PfsenseController) PingAddresses(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// endregion statistics-related
|
// endregion statistics-related
|
||||||
|
|
||||||
|
|||||||
@@ -283,13 +283,10 @@ 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"))
|
||||||
}
|
}
|
||||||
// rewrite relative paths
|
newIndexStr := strings.ReplaceAll(string(customIndexFile), "src=\"/", "src=\""+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+"/")
|
||||||
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(`assets/(index-.+.css)`)
|
re := regexp.MustCompile(`/app/assets/(index-.+.css)`)
|
||||||
match := re.FindStringSubmatch(newIndexStr)
|
match := re.FindStringSubmatch(newIndexStr)
|
||||||
cssFileName := match[1]
|
cssFileName := match[1]
|
||||||
|
|
||||||
@@ -299,7 +296,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), "./assets/", s.cfg.Web.BasePath+"/app/assets/")
|
newCssStr := strings.ReplaceAll(string(customCssFile), "/app/assets/", s.cfg.Web.BasePath+"/app/assets/")
|
||||||
|
|
||||||
return []byte(newIndexStr), []byte(newCssStr), cssFileName
|
return []byte(newIndexStr), []byte(newCssStr), cssFileName
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,8 @@ 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"
|
||||||
@@ -16,83 +14,49 @@ 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(
|
func NewWebsocketEndpoint(cfg *config.Config, auth Authenticator, bus WebsocketEventBus) *WebsocketEndpoint {
|
||||||
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 {
|
||||||
return matchOrigin(cfg.Web.ExternalUrl, r.Header.Get("Origin"))
|
origin := 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
|
||||||
}
|
}
|
||||||
|
|
||||||
// peerUserIdentifierCacheEntry is a cache entry object that reduces database load when checking peer ownership.
|
func (e WebsocketEndpoint) handleWebsocket() http.HandlerFunc {
|
||||||
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
|
||||||
@@ -110,29 +74,9 @@ 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})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,72 +98,3 @@ 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)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,249 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -180,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 and the userinfo endpoint.
|
// GetUserInfo retrieves the user info from the token.
|
||||||
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,
|
||||||
@@ -218,41 +218,6 @@ 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",
|
||||||
|
|||||||
@@ -893,7 +893,16 @@ func (m Manager) importInterface(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
iface.Type = inferImportedInterfaceType(iface, peers)
|
// try to predict the interface type based on the number of 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) {
|
||||||
@@ -921,20 +930,6 @@ 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,49 +10,6 @@ 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
|
||||||
|
|||||||
@@ -57,9 +57,6 @@ 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,7 +193,6 @@ 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 {
|
||||||
@@ -244,7 +243,7 @@ func parsePfsenseHttpResponse[T any](resp *http.Response, err error) PfsenseApiR
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return errToPfsenseApiResponse[T](PfsenseApiErrorCodeResponseDecodeFailed, "failed to read response body", err)
|
return errToPfsenseApiResponse[T](PfsenseApiErrorCodeResponseDecodeFailed, "failed to read response body", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close the body after reading
|
// Close the body after reading
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := resp.Body.Close(); err != nil {
|
if err := resp.Body.Close(); err != nil {
|
||||||
@@ -274,7 +273,7 @@ func parsePfsenseHttpResponse[T any](resp *http.Response, err error) PfsenseApiR
|
|||||||
"method", resp.Request.Method,
|
"method", resp.Request.Method,
|
||||||
"body_preview", bodyPreview,
|
"body_preview", bodyPreview,
|
||||||
"error", err)
|
"error", err)
|
||||||
return errToPfsenseApiResponse[T](PfsenseApiErrorCodeResponseDecodeFailed,
|
return errToPfsenseApiResponse[T](PfsenseApiErrorCodeResponseDecodeFailed,
|
||||||
fmt.Sprintf("failed to decode response (status %d, content-type: %s): %v", resp.StatusCode, contentType, err), err)
|
fmt.Sprintf("failed to decode response (status %d, content-type: %s): %v", resp.StatusCode, contentType, err), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -406,12 +405,11 @@ 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 := opts.GetPath(p.getFullPath(command))
|
fullUrl := p.getFullPath(command)
|
||||||
|
|
||||||
req, err := p.prepareDeleteRequest(apiCtx, fullUrl)
|
req, err := p.prepareDeleteRequest(apiCtx, fullUrl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -427,3 +425,4 @@ func (p *PfsenseApiClient) Delete(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// endregion API-client
|
// endregion API-client
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user