Compare commits
No commits in common. "master" and "multiple_oidc" have entirely different histories.
master
...
multiple_o
13
.github/workflows/codeql-analysis.yml
vendored
13
.github/workflows/codeql-analysis.yml
vendored
@ -41,16 +41,11 @@ jobs:
|
|||||||
run: sudo apt-get -y install libpam-dev
|
run: sudo apt-get -y install libpam-dev
|
||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Install Go
|
|
||||||
uses: actions/setup-go@v4
|
|
||||||
with:
|
|
||||||
go-version-file: go.mod
|
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v3
|
uses: github/codeql-action/init@v2
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
@ -64,7 +59,7 @@ jobs:
|
|||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v3
|
uses: github/codeql-action/autobuild@v2
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||||
@ -77,4 +72,4 @@ jobs:
|
|||||||
# ./location_of_script_within_repo/buildscript.sh
|
# ./location_of_script_within_repo/buildscript.sh
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v3
|
uses: github/codeql-action/analyze@v2
|
||||||
|
|||||||
20
.github/workflows/docker-image.yml
vendored
20
.github/workflows/docker-image.yml
vendored
@ -4,6 +4,8 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches: [ "master" ]
|
branches: [ "master" ]
|
||||||
tags: [ "v*" ]
|
tags: [ "v*" ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ "master" ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
@ -13,17 +15,17 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v3
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v2
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v2
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USER }}
|
username: ${{ secrets.DOCKER_USER }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
- name: Build and push - latest
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v3
|
uses: docker/build-push-action@v3
|
||||||
with:
|
with:
|
||||||
context: ./dev/docker
|
context: ./dev/docker
|
||||||
@ -31,14 +33,6 @@ jobs:
|
|||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ github.repository_owner }}/rdpgw:latest
|
tags: ${{ github.repository_owner }}/rdpgw:latest
|
||||||
- name: Build and push - latest
|
|
||||||
uses: docker/build-push-action@v3
|
|
||||||
with:
|
|
||||||
context: ./dev/docker
|
|
||||||
file: ./dev/docker/Dockerfile
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
push: true
|
|
||||||
tags: ${{ github.repository_owner }}/rdpgw:${{ github.ref_name }}
|
|
||||||
- name: Update Docker Hub Description
|
- name: Update Docker Hub Description
|
||||||
uses: peter-evans/dockerhub-description@v3
|
uses: peter-evans/dockerhub-description@v3
|
||||||
with:
|
with:
|
||||||
|
|||||||
2
.github/workflows/go.yml
vendored
2
.github/workflows/go.yml
vendored
@ -16,7 +16,7 @@ jobs:
|
|||||||
- name: Set up Go 1.x
|
- name: Set up Go 1.x
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v2
|
||||||
with:
|
with:
|
||||||
go-version: ^1.22
|
go-version: ^1.19
|
||||||
id: go
|
id: go
|
||||||
|
|
||||||
- name: Install pam-devel
|
- name: Install pam-devel
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,3 +1 @@
|
|||||||
go.sum
|
go.sum
|
||||||
node_modules/
|
|
||||||
package-lock.json
|
|
||||||
9
Makefile
9
Makefile
@ -26,7 +26,7 @@ ifneq ($(GIT_TAG),)
|
|||||||
endif
|
endif
|
||||||
|
|
||||||
.PHONY: all
|
.PHONY: all
|
||||||
all: mod build deb
|
all: mod build
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# build
|
# build
|
||||||
@ -38,10 +38,6 @@ $(BINDIR)/$(BINNAME): $(SRC)
|
|||||||
go build $(GOFLAGS) -trimpath -tags '$(TAGS)' -ldflags '$(LDFLAGS)' -o '$(BINDIR)'/$(BINNAME) ./cmd/rdpgw
|
go build $(GOFLAGS) -trimpath -tags '$(TAGS)' -ldflags '$(LDFLAGS)' -o '$(BINDIR)'/$(BINNAME) ./cmd/rdpgw
|
||||||
go build $(GOFLAGS) -trimpath -tags '$(TAGS)' -ldflags '$(LDFLAGS)' -o '$(BINDIR)'/$(BINNAME2) ./cmd/auth
|
go build $(GOFLAGS) -trimpath -tags '$(TAGS)' -ldflags '$(LDFLAGS)' -o '$(BINDIR)'/$(BINNAME2) ./cmd/auth
|
||||||
|
|
||||||
.PHONY: deb
|
|
||||||
deb:clean mod build
|
|
||||||
dpkg-buildpackage -b -us -uc
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# install
|
# install
|
||||||
|
|
||||||
@ -54,7 +50,7 @@ install: build
|
|||||||
|
|
||||||
.PHONY: mod
|
.PHONY: mod
|
||||||
mod:
|
mod:
|
||||||
go mod tidy -compat=1.23
|
go mod tidy -compat=1.19
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# test
|
# test
|
||||||
@ -68,7 +64,6 @@ test:
|
|||||||
.PHONY: clean
|
.PHONY: clean
|
||||||
clean:
|
clean:
|
||||||
@rm -rf '$(BINDIR)' ./_dist
|
@rm -rf '$(BINDIR)' ./_dist
|
||||||
dh_clean
|
|
||||||
|
|
||||||
.PHONY: info
|
.PHONY: info
|
||||||
info:
|
info:
|
||||||
|
|||||||
460
README.md
460
README.md
@ -14,258 +14,67 @@ This allows you to connect with the official Microsoft clients to remote desktop
|
|||||||
These desktops could be, for example, [XRDP](http://www.xrdp.org) desktops running in containers
|
These desktops could be, for example, [XRDP](http://www.xrdp.org) desktops running in containers
|
||||||
on Kubernetes.
|
on Kubernetes.
|
||||||
|
|
||||||
# AIM
|
## AIM
|
||||||
RDPGW aims to provide a full open source replacement for MS Remote Desktop Gateway,
|
RDPGW aims to provide a full open source replacement for MS Remote Desktop Gateway,
|
||||||
including access policies.
|
including access policies.
|
||||||
|
|
||||||
# Security requirements
|
## Multi Factor Authentication (MFA)
|
||||||
|
RDPGW provides multi factor authentication out of the box with OpenID Connect integration. Thus
|
||||||
|
you can integrate your remote desktops with Keycloak, Okta, Google, Azure, Apple or Facebook
|
||||||
|
if you want.
|
||||||
|
|
||||||
Several security requirements are stipulated by the client that is connecting to it and some are
|
## Security
|
||||||
enforced by the gateway. The client requires that the server's TLS certificate is valid and that
|
|
||||||
it is signed by a trusted authority. In addition, the common name in the certificate needs to
|
|
||||||
match the DNS hostname of the gateway. If these requirements are not met the client will refuse
|
|
||||||
to connect.
|
|
||||||
|
|
||||||
The gateway has several security phases. In the authentication phase the client's credentials are
|
__NOTE__: rdogw now supports PAM authentication as well if you configure it to use 'local' authentication. Further documentation pending.
|
||||||
verified. Depending the authentication mechanism used, the client's credentials are verified against
|
|
||||||
an OpenID Connect provider, Kerberos, a local PAM service or a local database.
|
|
||||||
|
|
||||||
If OpenID Connect is used the user will
|
RDPGW wants to be secure when you set it up from the beginning. It does this by having OpenID
|
||||||
need to connect to a webpage provided by the gateway to authenticate, which in turn will redirect
|
Connect integration enabled by default. Cookies are encrypted and signed on the client side relying
|
||||||
the user to the OpenID Connect provider. If the authentication is successful the browser will download
|
|
||||||
a RDP file with temporary credentials that allow the user to connect to the gateway by using a remote
|
|
||||||
desktop client.
|
|
||||||
|
|
||||||
If Kerberos is used the client will need to have a valid ticket granting ticket (TGT). The gateway
|
|
||||||
will proxy the TGT request to the KDC. Therefore, the gateway needs to be able to connect to the KDC
|
|
||||||
and a krb5.conf file needs to be provided. The proxy works without the need for an RDP file and thus
|
|
||||||
the client can connect directly to the gateway.
|
|
||||||
|
|
||||||
If local authentication is used the client will need to provide a username and password that is verified
|
|
||||||
against PAM. This requires, to ensure privilege separation, that ```rdpgw-auth``` is also running and a
|
|
||||||
valid PAM configuration is provided per typical configuration.
|
|
||||||
|
|
||||||
If NTLM authentication is used, the allowed user credentials for the gateway should be configured in the
|
|
||||||
configuration file of `rdpgw-auth`.
|
|
||||||
|
|
||||||
Finally, RDP hosts that the client wants to connect to are verified against what was provided by / allowed by
|
|
||||||
the server. Next to that the client's ip address needs to match the one it obtained the gateway token with if
|
|
||||||
using OpenID Connect. Due to proxies and NAT this is not always possible and thus can be disabled. However, this
|
|
||||||
is a security risk.
|
|
||||||
|
|
||||||
# Configuration
|
|
||||||
The configuration is done through a YAML file. The configuration file is read from `rdpgw.yaml` by default.
|
|
||||||
At the bottom of this README is an example configuration file. In these sections you will find the most important
|
|
||||||
settings.
|
|
||||||
|
|
||||||
## Authentication
|
|
||||||
|
|
||||||
RDPGW wants to be secure when you set it up from the start. It supports several authentication
|
|
||||||
mechanisms such as OpenID Connect, Kerberos, PAM or NTLM.
|
|
||||||
|
|
||||||
Technically, cookies are encrypted and signed on the client side relying
|
|
||||||
on [Gorilla Sessions](https://www.gorillatoolkit.org/pkg/sessions). PAA tokens (gateway access tokens)
|
on [Gorilla Sessions](https://www.gorillatoolkit.org/pkg/sessions). PAA tokens (gateway access tokens)
|
||||||
are generated and signed according to the JWT spec by using [jwt-go](https://github.com/dgrijalva/jwt-go)
|
are generated and signed according to the JWT spec by using [jwt-go](https://github.com/dgrijalva/jwt-go)
|
||||||
signed with a 256 bit HMAC.
|
signed with a 256 bit HMAC. Hosts provided by the user are verified against what was provided by
|
||||||
|
the server. Finally, the client's ip address needs to match the one it obtained the token with.
|
||||||
|
|
||||||
### Multi Factor Authentication (MFA)
|
## How to build & install
|
||||||
RDPGW provides multi-factor authentication out of the box with OpenID Connect integration. Thus
|
|
||||||
you can integrate your remote desktops with Keycloak, Okta, Google, Azure, Apple or Facebook
|
|
||||||
if you want.
|
|
||||||
|
|
||||||
### Mixing authentication mechanisms
|
__NOTE__: a docker image is available on docker hub, which removes the need for building and installing go.
|
||||||
|
|
||||||
It is technically possible to mix authentication mechanisms. Currently, you can mix local with Kerberos or NTLM. If you enable
|
Ensure that you have `make` (comes with standard build tools, like `build-essential` on Debian), `go` (version 1.19 or above), and development files for PAM (`libpam0g-dev` on Debian) installed.
|
||||||
OpenID Connect it is not possible to mix it with local or Kerberos at the moment.
|
|
||||||
|
|
||||||
### Open ID Connect
|
Then clone the repo and issues the following.
|
||||||

|
|
||||||
|
|
||||||
To use OpenID Connect make sure you have properly configured your OpenID Connect provider, and you have a client id
|
|
||||||
and secret. The client id and secret are used to authenticate the gateway to the OpenID Connect provider. The provider
|
|
||||||
will then authenticate the user and provide the gateway with a token. The gateway will then use this token to generate
|
|
||||||
a PAA token that is used to connect to the RDP host.
|
|
||||||
|
|
||||||
To enable OpenID Connect make sure to set the following variables in the configuration file.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
Server:
|
|
||||||
Authentication:
|
|
||||||
- openid
|
|
||||||
OpenId:
|
|
||||||
ProviderUrl: http://<provider_url>
|
|
||||||
ClientId: <your client id>
|
|
||||||
ClientSecret: <your-secret>
|
|
||||||
Caps:
|
|
||||||
TokenAuth: true
|
|
||||||
```
|
|
||||||
|
|
||||||
As you can see in the flow diagram when using OpenID Connect the user will use a browser to connect to the gateway first at
|
|
||||||
https://your-gateway/connect. If authentication is successful the browser will download a RDP file with temporary credentials
|
|
||||||
that allow the user to connect to the gateway by using a remote desktop client.
|
|
||||||
|
|
||||||
### Kerberos
|
|
||||||

|
|
||||||
|
|
||||||
__NOTE__: Kerberos is heavily reliant on DNS (forward and reverse). Make sure that your DNS is properly configured.
|
|
||||||
Next to that, its errors are not always very descriptive. It is beyond the scope of this project to provide a full
|
|
||||||
Kerberos tutorial.
|
|
||||||
|
|
||||||
To use Kerberos make sure you have a keytab and krb5.conf file. The keytab is used to authenticate the gateway to the KDC
|
|
||||||
and the krb5.conf file is used to configure the KDC. The keytab needs to contain a valid principal for the gateway.
|
|
||||||
|
|
||||||
Use `ktutil` or a similar tool provided by your Kerberos server to create a keytab file for the newly created service principal.
|
|
||||||
Place this keytab file in a secure location on the server and make sure that the file is only readable by the user that runs
|
|
||||||
the gateway.
|
|
||||||
|
|
||||||
```plaintext
|
|
||||||
ktutil
|
|
||||||
addent -password -p HTTP/rdpgw.example.com@YOUR.REALM -k 1 -e aes256-cts-hmac-sha1-96
|
|
||||||
wkt rdpgw.keytab
|
|
||||||
```
|
|
||||||
|
|
||||||
Then set the following in the configuration file.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
Server:
|
|
||||||
Authentication:
|
|
||||||
- kerberos
|
|
||||||
Kerberos:
|
|
||||||
Keytab: /etc/keytabs/rdpgw.keytab
|
|
||||||
Krb5conf: /etc/krb5.conf
|
|
||||||
Caps:
|
|
||||||
TokenAuth: false
|
|
||||||
```
|
|
||||||
|
|
||||||
The client can then connect directly to the gateway without the need for a RDP file.
|
|
||||||
|
|
||||||
|
|
||||||
### PAM / Local (Basic Auth)
|
|
||||||

|
|
||||||
|
|
||||||
The gateway can also support authentication against PAM. Sometimes this is referred to as local or passwd authentication,
|
|
||||||
but it also supports LDAP authentication or even Active Directory if you have the correct modules installed. Typically
|
|
||||||
(for passwd), PAM requires that it is accessed as root. Therefore, the gateway comes with a small helper program called
|
|
||||||
`rdpgw-auth` that is used to authenticate the user. This program needs to be run as root or setuid.
|
|
||||||
|
|
||||||
__NOTE__: The default windows client ``mstsc`` does not support basic auth. You will need to use a different client or
|
|
||||||
switch to OpenID Connect, Kerberos or NTLM authentication.
|
|
||||||
|
|
||||||
__NOTE__: Using PAM for passwd (i.e. LDAP is fine) within a container is not recommended. It is better to use OpenID
|
|
||||||
Connect or Kerberos. If you do want to use it within a container you can choose to run the helper program outside the
|
|
||||||
container and have the socket available within. Alternatively, you can mount all what is needed into the container but
|
|
||||||
PAM is quite sensitive to the environment.
|
|
||||||
|
|
||||||
Ensure you have a PAM service file for the gateway, `/etc/pam.d/rdpgw`. For authentication against local accounts on the
|
|
||||||
host located in `/etc/passwd` and `/etc/shadow` you can use the following.
|
|
||||||
|
|
||||||
```plaintext
|
|
||||||
auth required pam_unix.so
|
|
||||||
account required pam_unix.so
|
|
||||||
```
|
|
||||||
|
|
||||||
Then set the following in the configuration file.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
Server:
|
|
||||||
Authentication:
|
|
||||||
- local
|
|
||||||
AuthSocket: /tmp/rdpgw-auth.sock
|
|
||||||
Caps:
|
|
||||||
TokenAuth: false
|
|
||||||
```
|
|
||||||
|
|
||||||
Make sure to run both the gateway and `rdpgw-auth`. The gateway will connect to the socket to authenticate the user.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# ./rdpgw-auth -n rdpgw -s /tmp/rdpgw-auth.sock
|
cd rdpgw
|
||||||
|
make
|
||||||
|
make install
|
||||||
```
|
```
|
||||||
|
|
||||||
The client can then connect to the gateway directly by using a remote desktop client.
|
## Configuration
|
||||||
|
By default the configuration is read from `rdpgw.yaml`. Below is a
|
||||||
### NTLM
|
template.
|
||||||
|
|
||||||
The gateway can also support NTLM authentication.
|
|
||||||
Currently, only the configuration file is supported as a database for credential lookup.
|
|
||||||
In the future, support for real databases (e.g. sqlite) may be added.
|
|
||||||
|
|
||||||
NTLM authentication has the advantage that it is easy to setup, especially in case the gateway is used for a limited number of users.
|
|
||||||
Unlike PAM / local, NTLM authentication supports the default windows client ``mstsc``.
|
|
||||||
|
|
||||||
__WARNING__: The password is currently saved in plain text. So, you should keep the config file as secure as possible and avoid
|
|
||||||
reusing the same password for other applications. The password is stored in plain text to support the NTLM authentication protocol.
|
|
||||||
|
|
||||||
To enable NTLM authentication make sure to set the following variables in the configuration file.
|
|
||||||
|
|
||||||
Configuration file for `rdpgw`:
|
|
||||||
```yaml
|
|
||||||
Server:
|
|
||||||
Authentication:
|
|
||||||
- ntlm
|
|
||||||
Caps:
|
|
||||||
TokenAuth: false
|
|
||||||
```
|
|
||||||
|
|
||||||
Configuration file for `rdpgw-auth`:
|
|
||||||
````yaml
|
|
||||||
Users:
|
|
||||||
- {Username: "my_username", Password: "my_secure_password"} # Modify this password!
|
|
||||||
````
|
|
||||||
|
|
||||||
The client can then connect to the gateway directly by using a remote desktop client using the gateway credentials
|
|
||||||
configured in the YAML configuration file.
|
|
||||||
|
|
||||||
## TLS
|
|
||||||
|
|
||||||
The gateway requires a valid TLS certificate. This means a certificate that is signed by a valid CA that is in the store
|
|
||||||
of your clients. If this is not the case particularly Windows clients will fail to connect. You can either provide a
|
|
||||||
certificate and key file or let the gateway obtain a certificate from letsencrypt. If you want to use letsencrypt make
|
|
||||||
sure that the host is reachable on port 80 from the letsencrypt servers.
|
|
||||||
|
|
||||||
For letsencrypt:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
Tls: auto
|
|
||||||
```
|
|
||||||
|
|
||||||
for your own certificate:
|
|
||||||
```yaml
|
|
||||||
Tls: enable
|
|
||||||
CertFile: server.pem
|
|
||||||
KeyFile: key.pem
|
|
||||||
```
|
|
||||||
|
|
||||||
__NOTE__: You can disable TLS on the gateway, but you will then need to make sure a proxy is run in front of it that does
|
|
||||||
TLS termination.
|
|
||||||
|
|
||||||
|
|
||||||
## Example configuration file for Open ID Connect
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# web server configuration.
|
# web server configuration.
|
||||||
Server:
|
Server:
|
||||||
# can be set to openid, kerberos, local and ntlm. If openid is used rdpgw expects
|
# can be set to openid, kerberos and local. If openid is used rdpgw expects
|
||||||
# a configured openid provider, make sure to set caps.tokenauth to true. If local
|
# a configured openid provider, make sure to set caps.tokenauth to true. If local
|
||||||
# rdpgw connects to rdpgw-auth over a socket to verify users and password. Note:
|
# rdpgw connects to rdpgw-auth over a socket to verify users and password. Note:
|
||||||
# rdpgw-auth needs to be run as root or setuid in order to work. If kerberos is
|
# rdpgw-auth needs to be run as root or setuid in order to work. If kerberos is
|
||||||
# used a keytab and krb5conf need to be supplied. local can be stacked with
|
# used a keytab and krb5conf need to be supplied. local and kerberos authentication
|
||||||
# kerberos or ntlm authentication, so that the clients selects what it wants.
|
# can be stacked, so that the clients selects what it wants.
|
||||||
Authentication:
|
Authentication:
|
||||||
# - kerberos
|
|
||||||
# - local
|
|
||||||
- openid
|
- openid
|
||||||
# - ntlm
|
|
||||||
# The socket to connect to if using local auth. Ensure rdpgw auth is configured to
|
# The socket to connect to if using local auth. Ensure rdpgw auth is configured to
|
||||||
# use the same socket.
|
# use the same socket.
|
||||||
# AuthSocket: /tmp/rdpgw-auth.sock
|
AuthSocket: /tmp/rdpgw-auth.sock
|
||||||
# Basic auth timeout (in seconds). Useful if you're planning on waiting for MFA
|
|
||||||
BasicAuthTimeout: 5
|
|
||||||
# The default option 'auto' uses a certificate file if provided and found otherwise
|
# The default option 'auto' uses a certificate file if provided and found otherwise
|
||||||
# it uses letsencrypt to obtain a certificate, the latter requires that the host is reachable
|
# it uses letsencrypt to obtain a certificate, the latter requires that the host is reachable
|
||||||
# from letsencrypt servers. If TLS termination happens somewhere else (e.g. a load balancer)
|
# from letsencrypt servers. If TLS termination happens somewhere else (e.g. a load balancer)
|
||||||
# set this option to 'disable'. This is mutually exclusive with 'authentication: local'
|
# set this option to 'disable'. This is mutually exclusive with 'authentication: local'
|
||||||
# Note: rdp connections over a gateway require TLS
|
# Note: rdp connections over a gateway require TLS
|
||||||
Tls: auto
|
Tls: auto
|
||||||
|
# TLS certificate files
|
||||||
|
CertFile: server.pem
|
||||||
|
KeyFile: key.pem
|
||||||
# gateway address advertised in the rdp files and browser
|
# gateway address advertised in the rdp files and browser
|
||||||
GatewayAddress: localhost
|
GatewayAddress: localhost
|
||||||
# port to listen on (change to 80 or equivalent if not using TLS)
|
# port to listen on (change to 80 or equivalent if not using TLS)
|
||||||
@ -299,13 +108,12 @@ OpenId:
|
|||||||
ProviderUrl: http://keycloak/auth/realms/test
|
ProviderUrl: http://keycloak/auth/realms/test
|
||||||
ClientId: rdpgw
|
ClientId: rdpgw
|
||||||
ClientSecret: your-secret
|
ClientSecret: your-secret
|
||||||
# Kerberos:
|
Kerberos:
|
||||||
# Keytab: /etc/keytabs/rdpgw.keytab
|
Keytab: /etc/keytabs/rdpgw.keytab
|
||||||
# Krb5conf: /etc/krb5.conf
|
Krb5conf: /etc/krb5.conf
|
||||||
# enabled / disabled capabilities
|
# enabled / disabled capabilities
|
||||||
Caps:
|
Caps:
|
||||||
SmartCardAuth: false
|
SmartCardAuth: false
|
||||||
# required for openid connect
|
|
||||||
TokenAuth: true
|
TokenAuth: true
|
||||||
# connection timeout in minutes, 0 is limitless
|
# connection timeout in minutes, 0 is limitless
|
||||||
IdleTimeout: 10
|
IdleTimeout: 10
|
||||||
@ -315,19 +123,18 @@ Caps:
|
|||||||
EnableDrive: true
|
EnableDrive: true
|
||||||
EnableClipboard: true
|
EnableClipboard: true
|
||||||
Client:
|
Client:
|
||||||
# template rdp file to use for clients
|
|
||||||
# rdp file settings and their defaults see here:
|
|
||||||
# https://docs.microsoft.com/en-us/windows-server/remote/remote-desktop-services/clients/rdp-files
|
|
||||||
defaults: /etc/rdpgw/default.rdp
|
|
||||||
# this is a go string templated with {{ username }} and {{ token }}
|
# this is a go string templated with {{ username }} and {{ token }}
|
||||||
# the example below uses the ASCII field separator to distinguish
|
# the example below uses the ASCII field separator to distinguish
|
||||||
# between user and token
|
# between user and token
|
||||||
UsernameTemplate: "{{ username }}@bla.com\x1f{{ token }}"
|
UsernameTemplate: "{{ username }}@bla.com\x1f{{ token }}"
|
||||||
|
# rdp file settings see:
|
||||||
|
# https://docs.microsoft.com/en-us/windows-server/remote/remote-desktop-services/clients/rdp-files
|
||||||
|
NetworkAutoDetect: 0
|
||||||
|
BandwidthAutoDetect: 1
|
||||||
|
ConnectionType: 6
|
||||||
# If true puts splits "user@domain.com" into the user and domain component so that
|
# If true puts splits "user@domain.com" into the user and domain component so that
|
||||||
# domain gets set in the rdp file and the domain name is stripped from the username
|
# domain gets set in the rdp file and the domain name is stripped from the username
|
||||||
SplitUserDomain: false
|
SplitUserDomain: false
|
||||||
# If true, removes "username" (and "domain" if SplitUserDomain is true) from RDP file.
|
|
||||||
# NoUsername: true
|
|
||||||
Security:
|
Security:
|
||||||
# a random string of 32 characters to secure cookies on the client
|
# a random string of 32 characters to secure cookies on the client
|
||||||
# make sure to share this amongst different pods
|
# make sure to share this amongst different pods
|
||||||
@ -335,8 +142,6 @@ Security:
|
|||||||
# PAATokenEncryptionKey: thisisasessionkeyreplacethisjetzt
|
# PAATokenEncryptionKey: thisisasessionkeyreplacethisjetzt
|
||||||
# a random string of 32 characters to secure cookies on the client
|
# a random string of 32 characters to secure cookies on the client
|
||||||
UserTokenEncryptionKey: thisisasessionkeyreplacethisjetzt
|
UserTokenEncryptionKey: thisisasessionkeyreplacethisjetzt
|
||||||
# Signing makes the token bigger and we are limited to 511 characters
|
|
||||||
# UserTokenSigningKey: thisisasessionkeyreplacethisjetzt
|
|
||||||
# if you want to enable token generation for the user
|
# if you want to enable token generation for the user
|
||||||
# if true the username will be set to a jwt with the username embedded into it
|
# if true the username will be set to a jwt with the username embedded into it
|
||||||
EnableUserToken: true
|
EnableUserToken: true
|
||||||
@ -344,49 +149,17 @@ Security:
|
|||||||
# connection is opened.
|
# connection is opened.
|
||||||
VerifyClientIp: true
|
VerifyClientIp: true
|
||||||
```
|
```
|
||||||
|
|
||||||
## How to build & install
|
|
||||||
|
|
||||||
__NOTE__: a [docker image](https://hub.docker.com/r/bolkedebruin/rdpgw/) is available on docker hub, which removes the need for building and installing go.
|
|
||||||
|
|
||||||
Ensure that you have `make` (comes with standard build tools, like `build-essential` on Debian), `go` (version 1.19 or above), and development files for PAM (`libpam0g-dev` on Debian) installed.
|
|
||||||
|
|
||||||
Then clone the repo and issues the following.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd rdpgw
|
|
||||||
make
|
|
||||||
make install
|
|
||||||
```
|
|
||||||
|
|
||||||
## Testing locally
|
## Testing locally
|
||||||
A convenience docker-compose allows you to test the RDPGW locally. It uses [Keycloak](http://www.keycloak.org)
|
A convenience docker-compose allows you to test the RDPGW locally. It uses [Keycloak](http://www.keycloak.org)
|
||||||
and [xrdp](http://www.xrdp.org) and exposes it services on port 9443. You will need to allow your browser
|
and [xrdp](http://www.xrdp.org) and exposes it services on port 443. You will need to allow your browser
|
||||||
to connect to localhost with and self signed security certificate. For chrome set `chrome://flags/#allow-insecure-localhost`.
|
to connect to localhost with and self signed security certificate. For chrome set `chrome://flags/#allow-insecure-localhost`.
|
||||||
The username to login to both Keycloak and xrdp is `admin` as is the password.
|
The username to login to both Keycloak and xrdp is `admin` as is the password.
|
||||||
|
|
||||||
__NOTE__: The redirecting relies on DNS. Make sure to add ``127.0.0.1 keycloak`` to your `/etc/hosts` file to ensure
|
|
||||||
that the redirect works.
|
|
||||||
|
|
||||||
__NOTE__: The local testing environment uses a self signed certificate. This works for MAC clients, but not for Windows.
|
|
||||||
If you want to test it on Windows you will need to provide a valid certificate.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# with open id
|
|
||||||
cd dev/docker
|
cd dev/docker
|
||||||
docker-compose -f docker-compose.yml up
|
docker-compose build
|
||||||
|
docker-compose up
|
||||||
# or for arm64 with open id
|
|
||||||
docker-compose -f docker-compose-arm64.yml up
|
|
||||||
|
|
||||||
# or for local or pam
|
|
||||||
docker-compose -f docker-compose-local.yml up
|
|
||||||
```
|
```
|
||||||
|
|
||||||
You can then connect to the gateway at `https://localhost:9443/connect` for the OpenID connect flavors which will start
|
|
||||||
the authentication flow. Or you can connect directly with the gateway set and the host set to ``xrdp`` if using the ``local``
|
|
||||||
flavor. You can login with 'admin/admin'. The RDP file will download and you can open it with a remote
|
|
||||||
desktop client. Also for logging in 'admin/admin' will work.
|
|
||||||
|
|
||||||
## Use
|
## Use
|
||||||
Point your browser to `https://your-gateway/connect`. After authentication
|
Point your browser to `https://your-gateway/connect`. After authentication
|
||||||
@ -402,154 +175,11 @@ It will return 200 OK with the decrypted token.
|
|||||||
|
|
||||||
In this way you can integrate, for example, it with [pam-jwt](https://github.com/bolkedebruin/pam-jwt).
|
In this way you can integrate, for example, it with [pam-jwt](https://github.com/bolkedebruin/pam-jwt).
|
||||||
|
|
||||||
## Client Caveats
|
|
||||||
The several clients that Microsoft provides come with their own caveats.
|
|
||||||
The most important one is that the default client on Windows ``mstsc`` does
|
|
||||||
not support basic authentication. This means you need to use either OpenID Connect,
|
|
||||||
Kerberos or ntlm authentication.
|
|
||||||
|
|
||||||
In addition to that, ``mstsc``, when configuring a gateway directly in the client requires
|
|
||||||
you to either:
|
|
||||||
* "save the credentials" for the gateway
|
|
||||||
* or specify a (random) domain name in the username field (e.g. ``.\username``) when prompted for the gateway credentials,
|
|
||||||
|
|
||||||
otherwise the client will not connect at all (it won't send any packages to the gateway) and it will keep on asking for new credentials.
|
|
||||||
|
|
||||||
Finally, ``mstsc`` requires a valid certificate on the gateway.
|
|
||||||
|
|
||||||
The Microsoft Remote Desktop Client from the Microsoft Store does not have these issues,
|
|
||||||
but it requires that the username and password used for authentication are the same for
|
|
||||||
both the gateway and the RDP host.
|
|
||||||
|
|
||||||
The Microsoft Remote Desktop Client for Mac does not have these issues and is the most flexible.
|
|
||||||
It supports basic authentication, OpenID Connect and Kerberos and can use different credentials
|
|
||||||
|
|
||||||
The official Microsoft IOS and Android clients seem also more flexible.
|
|
||||||
|
|
||||||
Third party clients like [FreeRDP](https://www.freerdp.com) might also provide more
|
|
||||||
flexibility.
|
|
||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
|
* Integrate Open Policy Agent
|
||||||
|
* Integrate GOKRB5
|
||||||
|
* Integrate uber-go/zap
|
||||||
|
* Research: TLS defragmentation
|
||||||
* Improve Web Interface
|
* Improve Web Interface
|
||||||
|
|
||||||
# Acknowledgements
|
|
||||||
* This product includes software developed by the Thomson Reuters Global Resources. ([go-ntlm](https://github.com/m7913d/go-ntlm) - BSD-4 License)
|
|
||||||
|
|
||||||
# RDPGW 认证 API 服务器
|
|
||||||
|
|
||||||
这是一个用于 RDPGW(远程桌面网关)的认证 API 服务器,提供用户验证和密码获取功能。
|
|
||||||
|
|
||||||
## 功能特性
|
|
||||||
|
|
||||||
- 支持用户名和密码验证(verify 模式)
|
|
||||||
- 支持密码检索功能(getpassword 模式),用于 NTLM 认证
|
|
||||||
- 支持 GET 和 POST 请求方式
|
|
||||||
- 可通过配置文件自定义设置
|
|
||||||
|
|
||||||
## 安装
|
|
||||||
|
|
||||||
确保已安装 Node.js v10 或更高版本,然后执行:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install express
|
|
||||||
```
|
|
||||||
|
|
||||||
## 配置
|
|
||||||
|
|
||||||
配置文件为 `config.json`,包含以下选项:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"port": 3000,
|
|
||||||
"apiPath": "/api/checkperm",
|
|
||||||
"users": {
|
|
||||||
"testuser": "testpassword",
|
|
||||||
"admin": "adminpass"
|
|
||||||
},
|
|
||||||
"logger": {
|
|
||||||
"level": "info",
|
|
||||||
"logToFile": false,
|
|
||||||
"logFile": "server.log"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 配置选项说明
|
|
||||||
|
|
||||||
- `port`: 服务器监听端口
|
|
||||||
- `apiPath`: API 路径
|
|
||||||
- `users`: 用户名和密码字典
|
|
||||||
- `logger`: 日志配置
|
|
||||||
|
|
||||||
## 使用方法
|
|
||||||
|
|
||||||
### 启动服务器
|
|
||||||
|
|
||||||
```bash
|
|
||||||
node index.js
|
|
||||||
```
|
|
||||||
|
|
||||||
### API 请求示例
|
|
||||||
|
|
||||||
#### 验证模式 (verify)
|
|
||||||
|
|
||||||
**GET 请求**:
|
|
||||||
```
|
|
||||||
http://localhost:3000/api/checkperm?username=testuser&password=testpassword&mode=verify
|
|
||||||
```
|
|
||||||
|
|
||||||
**POST 请求**:
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:3000/api/checkperm \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"username":"testuser", "password":"testpassword", "mode":"verify"}'
|
|
||||||
```
|
|
||||||
|
|
||||||
**响应**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"status": "success",
|
|
||||||
"user": "testuser"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 密码获取模式 (getpassword)
|
|
||||||
|
|
||||||
**GET 请求**:
|
|
||||||
```
|
|
||||||
http://localhost:3000/api/checkperm?username=testuser&mode=getpassword
|
|
||||||
```
|
|
||||||
|
|
||||||
**POST 请求**:
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:3000/api/checkperm \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"username":"testuser", "mode":"getpassword"}'
|
|
||||||
```
|
|
||||||
|
|
||||||
**响应**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"status": "success",
|
|
||||||
"password": "testpassword"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## RDPGW 集成
|
|
||||||
|
|
||||||
在 RDPGW 配置中添加以下内容:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
ntlm_api:
|
|
||||||
enable: true
|
|
||||||
server: http://localhost:3000
|
|
||||||
path: /api/checkperm
|
|
||||||
mode: getpassword
|
|
||||||
```
|
|
||||||
|
|
||||||
## 安全建议
|
|
||||||
|
|
||||||
- 在生产环境中,请使用 HTTPS 而非 HTTP
|
|
||||||
- 限制 API 服务器的访问权限
|
|
||||||
- 定期更换密码并审查访问日志
|
|
||||||
- 考虑实现 IP 访问限制和请求频率限制
|
|
||||||
|
|||||||
15
UPGRADING.md
15
UPGRADING.md
@ -1,15 +0,0 @@
|
|||||||
# Upgrading from 1.X to 2.0
|
|
||||||
|
|
||||||
In 2.0 the options for configuring client side RDP settings have been removed in favor of template file.
|
|
||||||
The template file is a RDP file that is used as a template for the connection. The template file is parsed
|
|
||||||
and a few settings are replaced to ensure the client can connect to the server and the correct domain is used.
|
|
||||||
|
|
||||||
The format of the template file is as follows:
|
|
||||||
|
|
||||||
```
|
|
||||||
# <setting>:<type i or s>:<value>
|
|
||||||
domain:s:testdomain
|
|
||||||
connection type:i:2
|
|
||||||
```
|
|
||||||
|
|
||||||
The filename is set under `client > defaults`.
|
|
||||||
@ -3,12 +3,8 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"github.com/bolkedebruin/rdpgw/cmd/auth/config"
|
|
||||||
"github.com/bolkedebruin/rdpgw/cmd/auth/database"
|
|
||||||
"github.com/bolkedebruin/rdpgw/cmd/auth/ntlm"
|
|
||||||
"github.com/bolkedebruin/rdpgw/shared/auth"
|
"github.com/bolkedebruin/rdpgw/shared/auth"
|
||||||
"github.com/msteinert/pam/v2"
|
"github.com/msteinert/pam"
|
||||||
"github.com/thought-machine/go-flags"
|
"github.com/thought-machine/go-flags"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"log"
|
"log"
|
||||||
@ -24,24 +20,16 @@ const (
|
|||||||
var opts struct {
|
var opts struct {
|
||||||
ServiceName string `short:"n" long:"name" default:"rdpgw" description:"the PAM service name to use"`
|
ServiceName string `short:"n" long:"name" default:"rdpgw" description:"the PAM service name to use"`
|
||||||
SocketAddr string `short:"s" long:"socket" default:"/tmp/rdpgw-auth.sock" description:"the location of the socket"`
|
SocketAddr string `short:"s" long:"socket" default:"/tmp/rdpgw-auth.sock" description:"the location of the socket"`
|
||||||
ConfigFile string `short:"c" long:"conf" default:"rdpgw-auth.yaml" description:"users config file for NTLM (yaml)"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthServiceImpl struct {
|
type AuthServiceImpl struct {
|
||||||
auth.UnimplementedAuthenticateServer
|
|
||||||
|
|
||||||
serviceName string
|
serviceName string
|
||||||
ntlm *ntlm.NTLMAuth
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var conf config.Configuration
|
|
||||||
var _ auth.AuthenticateServer = (*AuthServiceImpl)(nil)
|
var _ auth.AuthenticateServer = (*AuthServiceImpl)(nil)
|
||||||
|
|
||||||
func NewAuthService(serviceName string, database database.Database) auth.AuthenticateServer {
|
func NewAuthService(serviceName string) auth.AuthenticateServer {
|
||||||
s := &AuthServiceImpl{
|
s := &AuthServiceImpl{serviceName: serviceName}
|
||||||
serviceName: serviceName,
|
|
||||||
ntlm: ntlm.NewNTLMAuth(database),
|
|
||||||
}
|
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,13 +52,7 @@ func (s *AuthServiceImpl) Authenticate(ctx context.Context, message *auth.UserPa
|
|||||||
r.Error = err.Error()
|
r.Error = err.Error()
|
||||||
return r, err
|
return r, err
|
||||||
}
|
}
|
||||||
defer func() {
|
|
||||||
err := t.End()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "end: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
if err = t.Authenticate(0); err != nil {
|
if err = t.Authenticate(0); err != nil {
|
||||||
log.Printf("Authentication for user: %s failed due to: %s", message.Username, err)
|
log.Printf("Authentication for user: %s failed due to: %s", message.Username, err)
|
||||||
r.Error = err.Error()
|
r.Error = err.Error()
|
||||||
@ -88,35 +70,12 @@ func (s *AuthServiceImpl) Authenticate(ctx context.Context, message *auth.UserPa
|
|||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AuthServiceImpl) NTLM(ctx context.Context, message *auth.NtlmRequest) (*auth.NtlmResponse, error) {
|
|
||||||
r, err := s.ntlm.Authenticate(message)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("[%s] NTLM failed: %s", message.Session, err)
|
|
||||||
} else if r.Authenticated {
|
|
||||||
log.Printf("[%s] User: %s authenticated using NTLM", message.Session, r.Username)
|
|
||||||
} else if r.NtlmMessage != "" {
|
|
||||||
log.Printf("[%s] Sending NTLM challenge", message.Session)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
_, err := flags.Parse(&opts)
|
_, err := flags.Parse(&opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var fErr *flags.Error
|
panic(err)
|
||||||
if errors.As(err, &fErr) {
|
|
||||||
if fErr.Type == flags.ErrHelp {
|
|
||||||
fmt.Printf("Acknowledgements:\n")
|
|
||||||
fmt.Printf(" - This product includes software developed by the Thomson Reuters Global Resources. (go-ntlm - https://github.com/m7913d/go-ntlm - BSD-4 License)\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
conf = config.Load(opts.ConfigFile)
|
|
||||||
|
|
||||||
log.Printf("Starting auth server on %s", opts.SocketAddr)
|
log.Printf("Starting auth server on %s", opts.SocketAddr)
|
||||||
cleanup := func() {
|
cleanup := func() {
|
||||||
if _, err := os.Stat(opts.SocketAddr); err == nil {
|
if _, err := os.Stat(opts.SocketAddr); err == nil {
|
||||||
@ -133,20 +92,8 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
server := grpc.NewServer()
|
server := grpc.NewServer()
|
||||||
|
service := NewAuthService(opts.ServiceName)
|
||||||
// 根据配置选择使用API认证或本地配置认证
|
|
||||||
var db database.Database
|
|
||||||
if conf.PXVDI.Enabled && conf.PXVDI.ApiUrl != "" {
|
|
||||||
log.Printf("Using API authentication, API URL: %s", conf.PXVDI.ApiUrl)
|
|
||||||
db = database.NewApiDb(conf.PXVDI.ApiUrl, conf.PXVDI.ApiKey)
|
|
||||||
} else {
|
|
||||||
log.Printf("Using local configuration file authentication")
|
|
||||||
db = database.NewConfig(conf.Users)
|
|
||||||
}
|
|
||||||
|
|
||||||
service := NewAuthService(opts.ServiceName, db)
|
|
||||||
auth.RegisterAuthenticateServer(server, service)
|
auth.RegisterAuthenticateServer(server, service)
|
||||||
server.Serve(listener)
|
server.Serve(listener)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,50 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/knadh/koanf/parsers/yaml"
|
|
||||||
"github.com/knadh/koanf/providers/confmap"
|
|
||||||
"github.com/knadh/koanf/providers/file"
|
|
||||||
"github.com/knadh/koanf/v2"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Configuration struct {
|
|
||||||
Users []UserConfig `koanf:"users"`
|
|
||||||
PXVDI PXVDIConfig `koanf:"pxvdi"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PXVDIConfig struct {
|
|
||||||
Enabled bool `koanf:"enabled"`
|
|
||||||
ApiUrl string `koanf:"apiurl"`
|
|
||||||
ApiKey string `koanf:"apikey"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserConfig struct {
|
|
||||||
Username string `koanf:"username"`
|
|
||||||
Password string `koanf:"password"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var Conf Configuration
|
|
||||||
|
|
||||||
func Load(configFile string) Configuration {
|
|
||||||
|
|
||||||
var k = koanf.New(".")
|
|
||||||
|
|
||||||
k.Load(confmap.Provider(map[string]interface{}{}, "."), nil)
|
|
||||||
|
|
||||||
if _, err := os.Stat(configFile); os.IsNotExist(err) {
|
|
||||||
log.Printf("Config file %s not found, skipping config file", configFile)
|
|
||||||
} else {
|
|
||||||
if err := k.Load(file.Provider(configFile), yaml.Parser()); err != nil {
|
|
||||||
log.Fatalf("Error loading config from file: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
koanfTag := koanf.UnmarshalConf{Tag: "koanf"}
|
|
||||||
k.UnmarshalWithConf("Users", &Conf.Users, koanfTag)
|
|
||||||
k.UnmarshalWithConf("PXVDI", &Conf.PXVDI, koanfTag)
|
|
||||||
|
|
||||||
return Conf
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,181 +0,0 @@
|
|||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/tls"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ApiDb 结构实现Database接口,通过API验证用户凭据
|
|
||||||
type ApiDb struct {
|
|
||||||
ApiUrl string // API URL模板,使用{username}和{password}作为占位符
|
|
||||||
ApiKey string // API密钥
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewApiDb 创建一个新的ApiDb实例
|
|
||||||
func NewApiDb(apiUrl string, apiKey string) *ApiDb {
|
|
||||||
return &ApiDb{
|
|
||||||
ApiUrl: apiUrl,
|
|
||||||
ApiKey: apiKey,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPassword 从API获取用户密码
|
|
||||||
// 这个方法会调用API来获取用户的实际密码,用于NTLM认证
|
|
||||||
func (a *ApiDb) GetPassword(username string) string {
|
|
||||||
log.Printf("Getpassword: %s", username)
|
|
||||||
|
|
||||||
// 如果用户名为空,直接返回失败
|
|
||||||
if username == "" {
|
|
||||||
log.Printf("API password retrieval failed: empty username")
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构建API URL
|
|
||||||
fullUrl := fmt.Sprintf("%s/api/custom/public/ntlmcheck", a.ApiUrl)
|
|
||||||
|
|
||||||
// 构建POST请求体
|
|
||||||
requestData := map[string]string{
|
|
||||||
"user": username,
|
|
||||||
"apikey": a.ApiKey,
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonData, err := json.Marshal(requestData)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Failed to marshal request data: %v", err)
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("Sending API password POST request to: %s", fullUrl)
|
|
||||||
|
|
||||||
// 创建自定义HTTP客户端,跳过SSL证书验证
|
|
||||||
tr := &http.Transport{
|
|
||||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
|
||||||
}
|
|
||||||
client := &http.Client{Transport: tr}
|
|
||||||
|
|
||||||
// 发送POST请求到API,使用不验证SSL证书的客户端
|
|
||||||
resp, err := client.Post(fullUrl, "application/json", bytes.NewBuffer(jsonData))
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("API password retrieval error: %v", err)
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
// 检查HTTP状态码
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
log.Printf("API password retrieval failed with status: %d", resp.StatusCode)
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// 读取响应内容
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Failed to read API response: %v", err)
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
log.Printf("API password response received")
|
|
||||||
|
|
||||||
// 解析响应 - 适应新的响应格式
|
|
||||||
var result struct {
|
|
||||||
Success bool `json:"success"`
|
|
||||||
Data struct {
|
|
||||||
Pass string `json:"pass"`
|
|
||||||
} `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.NewDecoder(bytes.NewReader(body)).Decode(&result)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Failed to parse API response: %v", err)
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查响应内容
|
|
||||||
if !result.Success || result.Data.Pass == "" {
|
|
||||||
log.Printf("API did not return a valid password for user: %s", username)
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("API password retrieval successful for user: %s", username)
|
|
||||||
return result.Data.Pass
|
|
||||||
}
|
|
||||||
|
|
||||||
// VerifyCredentials 验证用户凭据
|
|
||||||
func (a *ApiDb) VerifyCredentials(username, password string) bool {
|
|
||||||
// 如果用户名为空,直接返回失败
|
|
||||||
if username == "" {
|
|
||||||
log.Printf("API verification failed: empty username")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构建API URL,替换占位符
|
|
||||||
apiUrl := a.ApiUrl
|
|
||||||
|
|
||||||
// 构建完整的URL,包括查询参数
|
|
||||||
var fullUrl string
|
|
||||||
if password == "" {
|
|
||||||
// NTLM场景下直接将用户名传递给API
|
|
||||||
// 这种情况下,后端API应当能够独立验证用户
|
|
||||||
fullUrl = fmt.Sprintf("%s?username=%s&mode=verify",
|
|
||||||
apiUrl, url.QueryEscape(username))
|
|
||||||
log.Printf("Verifying NTLM user via API: %s", username)
|
|
||||||
} else {
|
|
||||||
// 常规场景
|
|
||||||
fullUrl = fmt.Sprintf("%s?username=%s&password=%s&mode=verify",
|
|
||||||
apiUrl, url.QueryEscape(username), url.QueryEscape(password))
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("Sending API verification request to: %s", fullUrl)
|
|
||||||
|
|
||||||
// 创建自定义HTTP客户端,跳过SSL证书验证
|
|
||||||
tr := &http.Transport{
|
|
||||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
|
||||||
}
|
|
||||||
client := &http.Client{Transport: tr}
|
|
||||||
|
|
||||||
// 发送请求到API,使用不验证SSL证书的客户端
|
|
||||||
resp, err := client.Get(fullUrl)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("API verification error: %v", err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
// 检查HTTP状态码
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
log.Printf("API verification failed with status: %d", resp.StatusCode)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 读取响应内容
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Failed to read API response: %v", err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
log.Printf("API response: %s", string(body))
|
|
||||||
|
|
||||||
// 解析响应
|
|
||||||
var result map[string]interface{}
|
|
||||||
err = json.NewDecoder(bytes.NewReader(body)).Decode(&result)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Failed to parse API response: %v", err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查响应内容
|
|
||||||
// 如果响应包含"success",则验证成功
|
|
||||||
if status, ok := result["status"].(string); ok && status == "success" {
|
|
||||||
log.Printf("API verification successful for user: %s", username)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果没有找到预期的成功标识,则验证失败
|
|
||||||
log.Printf("API verification failed for user: %s", username)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/bolkedebruin/rdpgw/cmd/auth/config"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
users map[string]config.UserConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewConfig(users []config.UserConfig) *Config {
|
|
||||||
usersMap := map[string]config.UserConfig{}
|
|
||||||
|
|
||||||
for _, user := range users {
|
|
||||||
usersMap[user.Username] = user
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Config{
|
|
||||||
users: usersMap,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Config) GetPassword (username string) string {
|
|
||||||
return c.users[username].Password
|
|
||||||
}
|
|
||||||
@ -1,43 +0,0 @@
|
|||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/bolkedebruin/rdpgw/cmd/auth/config"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func createTestDatabase () (Database) {
|
|
||||||
var users = []config.UserConfig{}
|
|
||||||
|
|
||||||
user1 := config.UserConfig{}
|
|
||||||
user1.Username = "my_username"
|
|
||||||
user1.Password = "my_password"
|
|
||||||
users = append(users, user1)
|
|
||||||
|
|
||||||
user2 := config.UserConfig{}
|
|
||||||
user2.Username = "my_username2"
|
|
||||||
user2.Password = "my_password2"
|
|
||||||
users = append(users, user2)
|
|
||||||
|
|
||||||
config := NewConfig(users)
|
|
||||||
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDatabaseConfigValidUsername(t *testing.T) {
|
|
||||||
database := createTestDatabase()
|
|
||||||
|
|
||||||
if database.GetPassword("my_username") != "my_password" {
|
|
||||||
t.Fatalf("Wrong password returned")
|
|
||||||
}
|
|
||||||
if database.GetPassword("my_username2") != "my_password2" {
|
|
||||||
t.Fatalf("Wrong password returned")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDatabaseInvalidUsername(t *testing.T) {
|
|
||||||
database := createTestDatabase()
|
|
||||||
|
|
||||||
if database.GetPassword("my_invalid_username") != "" {
|
|
||||||
t.Fatalf("Non empty password returned for invalid username")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
package database
|
|
||||||
|
|
||||||
type Database interface {
|
|
||||||
GetPassword (username string) string
|
|
||||||
}
|
|
||||||
@ -1,164 +0,0 @@
|
|||||||
package ntlm
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/base64"
|
|
||||||
"errors"
|
|
||||||
"github.com/bolkedebruin/rdpgw/cmd/auth/database"
|
|
||||||
"github.com/bolkedebruin/rdpgw/shared/auth"
|
|
||||||
"github.com/patrickmn/go-cache"
|
|
||||||
"github.com/m7913d/go-ntlm/ntlm"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
cacheExpiration = time.Minute
|
|
||||||
cleanupInterval = time.Minute * 5
|
|
||||||
)
|
|
||||||
|
|
||||||
type NTLMAuth struct {
|
|
||||||
contextCache *cache.Cache
|
|
||||||
|
|
||||||
// Information about the server, returned to the client during authentication
|
|
||||||
ServerName string // e.g. EXAMPLE1
|
|
||||||
DomainName string // e.g. EXAMPLE
|
|
||||||
DnsServerName string // e.g. example1.example.com
|
|
||||||
DnsDomainName string // e.g. example.com
|
|
||||||
DnsTreeName string // e.g. example.com
|
|
||||||
|
|
||||||
Database database.Database
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewNTLMAuth (database database.Database) (*NTLMAuth) {
|
|
||||||
return &NTLMAuth{
|
|
||||||
contextCache: cache.New(cacheExpiration, cleanupInterval),
|
|
||||||
Database: database,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *NTLMAuth) Authenticate(message *auth.NtlmRequest) (*auth.NtlmResponse, error) {
|
|
||||||
r := &auth.NtlmResponse{}
|
|
||||||
r.Authenticated = false
|
|
||||||
|
|
||||||
if message.Session == "" {
|
|
||||||
return r, errors.New("Invalid (empty) session specified")
|
|
||||||
}
|
|
||||||
|
|
||||||
if message.NtlmMessage == "" {
|
|
||||||
return r, errors.New("Empty NTLM message specified")
|
|
||||||
}
|
|
||||||
|
|
||||||
c := h.getContext(message.Session)
|
|
||||||
err := c.Authenticate(message.NtlmMessage, r)
|
|
||||||
|
|
||||||
if err != nil || r.Authenticated {
|
|
||||||
h.removeContext(message.Session)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *NTLMAuth) getContext (session string) (*ntlmContext) {
|
|
||||||
if c_, found := h.contextCache.Get(session); found {
|
|
||||||
if c, ok := c_.(*ntlmContext); ok {
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
}
|
|
||||||
c := new(ntlmContext)
|
|
||||||
c.h = h
|
|
||||||
h.contextCache.Set(session, c, cache.DefaultExpiration)
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *NTLMAuth) removeContext (session string) {
|
|
||||||
h.contextCache.Delete(session)
|
|
||||||
}
|
|
||||||
|
|
||||||
type ntlmContext struct {
|
|
||||||
session ntlm.ServerSession
|
|
||||||
h *NTLMAuth
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ntlmContext) Authenticate(authorisationEncoded string, r *auth.NtlmResponse) (error) {
|
|
||||||
authorisation, err := base64.StdEncoding.DecodeString(authorisationEncoded)
|
|
||||||
if err != nil {
|
|
||||||
return errors.New(fmt.Sprintf("Failed to decode NTLM Authorisation header: %s", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
nm, err := ntlm.ParseNegotiateMessage(authorisation)
|
|
||||||
if err == nil {
|
|
||||||
return c.negotiate(nm, r)
|
|
||||||
}
|
|
||||||
if (nm != nil && nm.MessageType == 1) {
|
|
||||||
return errors.New(fmt.Sprintf("Failed to parse NTLM Authorisation header: %s", err))
|
|
||||||
} else if c.session == nil {
|
|
||||||
return errors.New(fmt.Sprintf("New NTLM auth sequence should start with negotioate request"))
|
|
||||||
}
|
|
||||||
|
|
||||||
am, err := ntlm.ParseAuthenticateMessage(authorisation, 2)
|
|
||||||
if err == nil {
|
|
||||||
return c.authenticate(am, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.New(fmt.Sprintf("Failed to parse NTLM Authorisation header: %s", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ntlmContext) negotiate(nm *ntlm.NegotiateMessage, r *auth.NtlmResponse) (error) {
|
|
||||||
session, err := ntlm.CreateServerSession(ntlm.Version2, ntlm.ConnectionOrientedMode)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
c.session = nil;
|
|
||||||
return errors.New(fmt.Sprintf("Failed to create NTLM server session: %s", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
c.session = session
|
|
||||||
c.session.SetRequireNtHash(true)
|
|
||||||
c.session.SetDomainName(c.h.DomainName)
|
|
||||||
c.session.SetComputerName(c.h.ServerName)
|
|
||||||
c.session.SetDnsDomainName(c.h.DnsDomainName)
|
|
||||||
c.session.SetDnsComputerName(c.h.DnsServerName)
|
|
||||||
c.session.SetDnsTreeName(c.h.DnsTreeName)
|
|
||||||
|
|
||||||
err = c.session.ProcessNegotiateMessage(nm)
|
|
||||||
if err != nil {
|
|
||||||
return errors.New(fmt.Sprintf("Failed to process NTLM negotiate message: %s", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
cm, err := c.session.GenerateChallengeMessage()
|
|
||||||
if err != nil {
|
|
||||||
return errors.New(fmt.Sprintf("Failed to generate NTLM challenge message: %s", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
r.NtlmMessage = base64.StdEncoding.EncodeToString(cm.Bytes())
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ntlmContext) authenticate(am *ntlm.AuthenticateMessage, r *auth.NtlmResponse) (error) {
|
|
||||||
if c.session == nil {
|
|
||||||
return errors.New(fmt.Sprintf("NTLM Authenticate requires active session: first call negotioate"))
|
|
||||||
}
|
|
||||||
|
|
||||||
username := am.UserName.String()
|
|
||||||
log.Printf("NTLM: Trying to validate user: %s", username)
|
|
||||||
|
|
||||||
password := c.h.Database.GetPassword(username)
|
|
||||||
if password == "" {
|
|
||||||
log.Printf("NTLM: unknown username specified: %s", username)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("NTLM: Successfully retrieved password for user: %s", username)
|
|
||||||
c.session.SetUserInfo(username, password, "")
|
|
||||||
|
|
||||||
err := c.session.ProcessAuthenticateMessage(am)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Failed to process NTLM authenticate message: %s", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
r.Authenticated = true
|
|
||||||
r.Username = username
|
|
||||||
log.Printf("NTLM: User %s authenticated successfully", username)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@ -1,168 +0,0 @@
|
|||||||
package ntlm
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/base64"
|
|
||||||
"github.com/bolkedebruin/rdpgw/cmd/auth/config"
|
|
||||||
"github.com/bolkedebruin/rdpgw/cmd/auth/database"
|
|
||||||
"github.com/bolkedebruin/rdpgw/shared/auth"
|
|
||||||
"github.com/m7913d/go-ntlm/ntlm"
|
|
||||||
"testing"
|
|
||||||
"log"
|
|
||||||
)
|
|
||||||
|
|
||||||
func createTestDatabase () (database.Database) {
|
|
||||||
user := config.UserConfig{}
|
|
||||||
user.Username = "my_username"
|
|
||||||
user.Password = "my_password"
|
|
||||||
|
|
||||||
var users = []config.UserConfig{}
|
|
||||||
users = append(users, user)
|
|
||||||
|
|
||||||
config := database.NewConfig(users)
|
|
||||||
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNtlmValidCredentials(t *testing.T) {
|
|
||||||
client := ntlm.V2ClientSession{}
|
|
||||||
client.SetUserInfo("my_username", "my_password", "")
|
|
||||||
|
|
||||||
authenticateResponse := authenticate(t, &client)
|
|
||||||
if !authenticateResponse.Authenticated {
|
|
||||||
t.Errorf("Failed to authenticate")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if authenticateResponse.Username != "my_username" {
|
|
||||||
t.Errorf("Wrong username returned")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNtlmInvalidPassword(t *testing.T) {
|
|
||||||
client := ntlm.V2ClientSession{}
|
|
||||||
client.SetUserInfo("my_username", "my_invalid_password", "")
|
|
||||||
|
|
||||||
authenticateResponse := authenticate(t, &client)
|
|
||||||
if authenticateResponse.Authenticated {
|
|
||||||
t.Errorf("Authenticated with wrong password")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if authenticateResponse.Username != "" {
|
|
||||||
t.Errorf("If authentication failed, no username should be returned")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNtlmInvalidUsername(t *testing.T) {
|
|
||||||
client := ntlm.V2ClientSession{}
|
|
||||||
client.SetUserInfo("my_invalid_username", "my_password", "")
|
|
||||||
|
|
||||||
authenticateResponse := authenticate(t, &client)
|
|
||||||
if authenticateResponse.Authenticated {
|
|
||||||
t.Errorf("Authenticated with wrong password")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if authenticateResponse.Username != "" {
|
|
||||||
t.Errorf("If authentication failed, no username should be returned")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func authenticate(t *testing.T, client *ntlm.V2ClientSession) (*auth.NtlmResponse) {
|
|
||||||
session := "X"
|
|
||||||
database := createTestDatabase()
|
|
||||||
|
|
||||||
server := NewNTLMAuth(database)
|
|
||||||
|
|
||||||
negotiate, err := client.GenerateNegotiateMessage()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Could not generate negotiate message: %s", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
negotiateRequest := &auth.NtlmRequest{}
|
|
||||||
negotiateRequest.Session = session
|
|
||||||
negotiateRequest.NtlmMessage = base64.StdEncoding.EncodeToString(negotiate.Bytes())
|
|
||||||
negotiateResponse, err := server.Authenticate(negotiateRequest)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Could not generate challenge message: %s", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if negotiateResponse.Authenticated {
|
|
||||||
t.Errorf("User should not be authenticated by after negotiate message")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if negotiateResponse.NtlmMessage == "" {
|
|
||||||
t.Errorf("Could not generate challenge message")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
decodedChallenge, err := base64.StdEncoding.DecodeString(negotiateResponse.NtlmMessage)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Challenge should be base64 encoded: %s", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
challenge, err := ntlm.ParseChallengeMessage(decodedChallenge)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Invalid challenge message generated: %s", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
client.ProcessChallengeMessage(challenge)
|
|
||||||
authenticate, err := client.GenerateAuthenticateMessage()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Could not generate authenticate message: %s", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
authenticateRequest := &auth.NtlmRequest{}
|
|
||||||
authenticateRequest.Session = session
|
|
||||||
authenticateRequest.NtlmMessage = base64.StdEncoding.EncodeToString(authenticate.Bytes())
|
|
||||||
authenticateResponse, err := server.Authenticate(authenticateRequest)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Could not parse authenticate message: %s", err)
|
|
||||||
return authenticateResponse
|
|
||||||
}
|
|
||||||
if authenticateResponse.NtlmMessage != "" {
|
|
||||||
t.Errorf("Authenticate request should not generate a new NTLM message")
|
|
||||||
return authenticateResponse
|
|
||||||
}
|
|
||||||
return authenticateResponse
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestInvalidBase64 (t *testing.T) {
|
|
||||||
testInvalidDataBase(t, "X", "X") // not valid base64
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestInvalidData (t *testing.T) {
|
|
||||||
testInvalidDataBase(t, "X", "XXXX") // valid base64
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestInvalidDataEmptyMessage (t *testing.T) {
|
|
||||||
testInvalidDataBase(t, "X", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEmptySession (t *testing.T) {
|
|
||||||
testInvalidDataBase(t, "", "XXXX")
|
|
||||||
}
|
|
||||||
|
|
||||||
func testInvalidDataBase (t *testing.T, session string, data string) {
|
|
||||||
database := createTestDatabase()
|
|
||||||
server := NewNTLMAuth(database)
|
|
||||||
|
|
||||||
request := &auth.NtlmRequest{}
|
|
||||||
request.Session = session
|
|
||||||
request.NtlmMessage = data
|
|
||||||
response, err := server.Authenticate(request)
|
|
||||||
log.Printf("%s",err)
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("Invalid request should return an error")
|
|
||||||
}
|
|
||||||
if response.Authenticated {
|
|
||||||
t.Errorf("User should not be authenticated using invalid data")
|
|
||||||
}
|
|
||||||
if response.NtlmMessage != "" {
|
|
||||||
t.Errorf("No NTLM message should be generated for invalid data")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -2,13 +2,12 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/security"
|
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/security"
|
||||||
|
"github.com/knadh/koanf"
|
||||||
"github.com/knadh/koanf/parsers/yaml"
|
"github.com/knadh/koanf/parsers/yaml"
|
||||||
"github.com/knadh/koanf/providers/confmap"
|
"github.com/knadh/koanf/providers/confmap"
|
||||||
"github.com/knadh/koanf/providers/env"
|
"github.com/knadh/koanf/providers/env"
|
||||||
"github.com/knadh/koanf/providers/file"
|
"github.com/knadh/koanf/providers/file"
|
||||||
"github.com/knadh/koanf/v2"
|
|
||||||
"log"
|
"log"
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -34,13 +33,6 @@ type Configuration struct {
|
|||||||
Caps RDGCapsConfig `koanf:"caps"`
|
Caps RDGCapsConfig `koanf:"caps"`
|
||||||
Security SecurityConfig `koanf:"security"`
|
Security SecurityConfig `koanf:"security"`
|
||||||
Client ClientConfig `koanf:"client"`
|
Client ClientConfig `koanf:"client"`
|
||||||
PXVDI PXVDIConfig `koanf:"pxvdi"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PXVDIConfig struct {
|
|
||||||
Enabled bool `koanf:"enabled"`
|
|
||||||
ApiUrl string `koanf:"apiurl"`
|
|
||||||
ApiKey string `koanf:"apikey"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ServerConfig struct {
|
type ServerConfig struct {
|
||||||
@ -59,7 +51,6 @@ type ServerConfig struct {
|
|||||||
Tls string `koanf:"tls"`
|
Tls string `koanf:"tls"`
|
||||||
Authentication []string `koanf:"authentication"`
|
Authentication []string `koanf:"authentication"`
|
||||||
AuthSocket string `koanf:"authsocket"`
|
AuthSocket string `koanf:"authsocket"`
|
||||||
BasicAuthTimeout int `koanf:"basicauthtimeout"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type KerberosConfig struct {
|
type KerberosConfig struct {
|
||||||
@ -98,11 +89,12 @@ type SecurityConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ClientConfig struct {
|
type ClientConfig struct {
|
||||||
Defaults string `koanf:"defaults"`
|
NetworkAutoDetect int `koanf:"networkautodetect"`
|
||||||
// kept for backwards compatibility
|
BandwidthAutoDetect int `koanf:"bandwidthautodetect"`
|
||||||
UsernameTemplate string `koanf:"usernametemplate"`
|
ConnectionType int `koanf:"connectiontype"`
|
||||||
SplitUserDomain bool `koanf:"splituserdomain"`
|
UsernameTemplate string `koanf:"usernametemplate"`
|
||||||
NoUsername bool `koanf:"nousername"`
|
SplitUserDomain bool `koanf:"splituserdomain"`
|
||||||
|
DefaultDomain string `koanf:"defaultdomain"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToCamel(s string) string {
|
func ToCamel(s string) string {
|
||||||
@ -153,35 +145,22 @@ func Load(configFile string) Configuration {
|
|||||||
"Server.HostSelection": "roundrobin",
|
"Server.HostSelection": "roundrobin",
|
||||||
"Server.Authentication": "openid",
|
"Server.Authentication": "openid",
|
||||||
"Server.AuthSocket": "/tmp/rdpgw-auth.sock",
|
"Server.AuthSocket": "/tmp/rdpgw-auth.sock",
|
||||||
"Server.BasicAuthTimeout": 5,
|
|
||||||
"Client.NetworkAutoDetect": 1,
|
"Client.NetworkAutoDetect": 1,
|
||||||
"Client.BandwidthAutoDetect": 1,
|
"Client.BandwidthAutoDetect": 1,
|
||||||
"Security.VerifyClientIp": true,
|
"Security.VerifyClientIp": true,
|
||||||
"Caps.TokenAuth": true,
|
"Caps.TokenAuth": true,
|
||||||
}, "."), nil)
|
}, "."), nil)
|
||||||
|
|
||||||
if _, err := os.Stat(configFile); os.IsNotExist(err) {
|
if err := k.Load(file.Provider(configFile), yaml.Parser()); err != nil {
|
||||||
log.Printf("Config file %s not found, using defaults and environment", configFile)
|
log.Fatalf("Error loading config from file: %v", err)
|
||||||
} else {
|
|
||||||
if err := k.Load(file.Provider(configFile), yaml.Parser()); err != nil {
|
|
||||||
log.Fatalf("Error loading config from file: %v", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := k.Load(env.ProviderWithValue("RDPGW_", ".", func(s string, v string) (string, interface{}) {
|
if err := k.Load(env.ProviderWithValue("RDPGW_", ".", func(s string, v string) (string, interface{}) {
|
||||||
key := strings.Replace(strings.ToLower(strings.TrimPrefix(s, "RDPGW_")), "__", ".", -1)
|
key := strings.Replace(strings.ToLower(strings.TrimPrefix(s, "RDPGW_")), "__", ".", -1)
|
||||||
key = ToCamel(key)
|
key = ToCamel(key)
|
||||||
|
|
||||||
v = strings.Trim(v, " ")
|
|
||||||
|
|
||||||
// handle lists
|
|
||||||
if strings.Contains(v, " ") {
|
|
||||||
return key, strings.Split(v, " ")
|
|
||||||
}
|
|
||||||
return key, v
|
return key, v
|
||||||
|
|
||||||
}), nil); err != nil {
|
}), nil); err != nil {
|
||||||
log.Fatalf("Error loading config from environment: %v", err)
|
log.Fatalf("Error loading config from file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
koanfTag := koanf.UnmarshalConf{Tag: "koanf"}
|
koanfTag := koanf.UnmarshalConf{Tag: "koanf"}
|
||||||
@ -191,7 +170,6 @@ func Load(configFile string) Configuration {
|
|||||||
k.UnmarshalWithConf("Security", &Conf.Security, koanfTag)
|
k.UnmarshalWithConf("Security", &Conf.Security, koanfTag)
|
||||||
k.UnmarshalWithConf("Client", &Conf.Client, koanfTag)
|
k.UnmarshalWithConf("Client", &Conf.Client, koanfTag)
|
||||||
k.UnmarshalWithConf("Kerberos", &Conf.Kerberos, koanfTag)
|
k.UnmarshalWithConf("Kerberos", &Conf.Kerberos, koanfTag)
|
||||||
k.UnmarshalWithConf("PXVDI", &Conf.PXVDI, koanfTag)
|
|
||||||
|
|
||||||
if len(Conf.Security.PAATokenEncryptionKey) != 32 {
|
if len(Conf.Security.PAATokenEncryptionKey) != 32 {
|
||||||
Conf.Security.PAATokenEncryptionKey, _ = security.GenerateRandomString(32)
|
Conf.Security.PAATokenEncryptionKey, _ = security.GenerateRandomString(32)
|
||||||
@ -232,10 +210,6 @@ func Load(configFile string) Configuration {
|
|||||||
if Conf.Server.BasicAuthEnabled() && Conf.Server.Tls == "disable" {
|
if Conf.Server.BasicAuthEnabled() && Conf.Server.Tls == "disable" {
|
||||||
log.Fatalf("basicauth=local and tls=disable are mutually exclusive")
|
log.Fatalf("basicauth=local and tls=disable are mutually exclusive")
|
||||||
}
|
}
|
||||||
|
|
||||||
if Conf.Server.NtlmEnabled() && Conf.Server.KerberosEnabled() {
|
|
||||||
log.Fatalf("ntlm and kerberos authentication are not stackable")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !Conf.Caps.TokenAuth && Conf.Server.OpenIDEnabled() {
|
if !Conf.Caps.TokenAuth && Conf.Server.OpenIDEnabled() {
|
||||||
log.Fatalf("openid is configured but tokenauth disabled")
|
log.Fatalf("openid is configured but tokenauth disabled")
|
||||||
@ -266,10 +240,6 @@ func (s *ServerConfig) BasicAuthEnabled() bool {
|
|||||||
return s.matchAuth("local") || s.matchAuth("basic")
|
return s.matchAuth("local") || s.matchAuth("basic")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ServerConfig) NtlmEnabled() bool {
|
|
||||||
return s.matchAuth("ntlm")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ServerConfig) matchAuth(needle string) bool {
|
func (s *ServerConfig) matchAuth(needle string) bool {
|
||||||
for _, q := range s.Authentication {
|
for _, q := range s.Authentication {
|
||||||
if q == needle {
|
if q == needle {
|
||||||
|
|||||||
@ -23,13 +23,6 @@ type KdcProxyMsg struct {
|
|||||||
Flags int `asn1:"tag:2,optional"`
|
Flags int `asn1:"tag:2,optional"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Kdc struct {
|
|
||||||
Realm string
|
|
||||||
Host string
|
|
||||||
Proto string
|
|
||||||
Conn net.Conn
|
|
||||||
}
|
|
||||||
|
|
||||||
type KerberosProxy struct {
|
type KerberosProxy struct {
|
||||||
krb5Config *krbconfig.Config
|
krb5Config *krbconfig.Config
|
||||||
}
|
}
|
||||||
@ -104,71 +97,39 @@ func (k *KerberosProxy) forward(realm string, data []byte) (resp []byte, err err
|
|||||||
}
|
}
|
||||||
|
|
||||||
// load udp first as is the default for kerberos
|
// load udp first as is the default for kerberos
|
||||||
udpCnt, udpKdcs, err := k.krb5Config.GetKDCs(realm, false)
|
c, kdcs, err := k.krb5Config.GetKDCs(realm, false)
|
||||||
if err != nil {
|
if err != nil || c < 1 {
|
||||||
return nil, fmt.Errorf("cannot get udp kdc for realm %s due to %s", realm, err)
|
return nil, fmt.Errorf("cannot get kdc for realm %s due to %s", realm, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// load tcp
|
|
||||||
tcpCnt, tcpKdcs, err := k.krb5Config.GetKDCs(realm, true)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot get tcp kdc for realm %s due to %s", realm, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if tcpCnt+udpCnt == 0 {
|
|
||||||
return nil, fmt.Errorf("cannot get any kdcs (tcp or udp) for realm %s", realm)
|
|
||||||
}
|
|
||||||
|
|
||||||
// merge the kdcs
|
|
||||||
kdcs := make([]Kdc, tcpCnt+udpCnt)
|
|
||||||
for i := range udpKdcs {
|
|
||||||
kdcs[i] = Kdc{Realm: realm, Host: udpKdcs[i], Proto: "udp"}
|
|
||||||
}
|
|
||||||
for i := range tcpKdcs {
|
|
||||||
kdcs[i+udpCnt] = Kdc{Realm: realm, Host: tcpKdcs[i], Proto: "tcp"}
|
|
||||||
}
|
|
||||||
|
|
||||||
replies := make(chan []byte, len(kdcs))
|
|
||||||
for i := range kdcs {
|
for i := range kdcs {
|
||||||
conn, err := net.Dial(kdcs[i].Proto, kdcs[i].Host)
|
conn, err := net.Dial("tcp", kdcs[i])
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("error connecting to %s due to %s, trying next if available", kdcs[i], err)
|
log.Printf("error connecting to %s due to %s, trying next if available", kdcs[i], err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
conn.SetDeadline(time.Now().Add(timeout))
|
conn.SetDeadline(time.Now().Add(timeout))
|
||||||
|
|
||||||
// if we proxy over UDP remove the length prefix
|
_, err = conn.Write(data)
|
||||||
if kdcs[i].Proto == "tcp" {
|
|
||||||
_, err = conn.Write(data)
|
|
||||||
} else {
|
|
||||||
_, err = conn.Write(data[4:])
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("cannot write packet data to %s due to %s, trying next if available", kdcs[i], err)
|
log.Printf("cannot write packet data to %s due to %s, trying next if available", kdcs[i], err)
|
||||||
conn.Close()
|
conn.Close()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
kdcs[i].Conn = conn
|
// todo check header
|
||||||
go awaitReply(conn, kdcs[i].Proto == "udp", replies)
|
resp, err = io.ReadAll(conn)
|
||||||
}
|
if err != nil {
|
||||||
|
log.Printf("error reading from kdc %s due to %s, trying next if available", kdcs[i], err)
|
||||||
reply := <-replies
|
conn.Close()
|
||||||
|
continue
|
||||||
// close all the connections and return the first reply
|
|
||||||
for kdc := range kdcs {
|
|
||||||
if kdcs[kdc].Conn != nil {
|
|
||||||
kdcs[kdc].Conn.Close()
|
|
||||||
}
|
}
|
||||||
<-replies
|
conn.Close()
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if reply != nil {
|
return nil, fmt.Errorf("no kdcs found for realm %s", realm)
|
||||||
return reply, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, fmt.Errorf("no replies received from kdcs for realm %s", realm)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func decode(data []byte) (msg *KdcProxyMsg, err error) {
|
func decode(data []byte) (msg *KdcProxyMsg, err error) {
|
||||||
@ -194,17 +155,3 @@ func encode(krb5data []byte) (r []byte, err error) {
|
|||||||
}
|
}
|
||||||
return enc, nil
|
return enc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func awaitReply(conn net.Conn, isUdp bool, reply chan<- []byte) {
|
|
||||||
resp, err := io.ReadAll(conn)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("error reading from kdc due to %s", err)
|
|
||||||
reply <- nil
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if isUdp {
|
|
||||||
// udp will be missing the length prefix so add it
|
|
||||||
resp = append([]byte{byte(len(resp))}, resp...)
|
|
||||||
}
|
|
||||||
reply <- resp
|
|
||||||
}
|
|
||||||
|
|||||||
@ -3,7 +3,6 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/bolkedebruin/gokrb5/v8/keytab"
|
"github.com/bolkedebruin/gokrb5/v8/keytab"
|
||||||
"github.com/bolkedebruin/gokrb5/v8/service"
|
"github.com/bolkedebruin/gokrb5/v8/service"
|
||||||
@ -24,7 +23,6 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -67,27 +65,6 @@ func initOIDC(callbackUrl *url.URL) *web.OIDC {
|
|||||||
return o.New()
|
return o.New()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 定时记录连接用户信息
|
|
||||||
func startConnectionLogger(interval time.Duration) {
|
|
||||||
ticker := time.NewTicker(interval)
|
|
||||||
go func() {
|
|
||||||
for range ticker.C {
|
|
||||||
connections := protocol.GetActiveConnections()
|
|
||||||
if len(connections) > 0 {
|
|
||||||
connData, err := json.Marshal(connections)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("连接信息序列化失败: %v", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
log.Printf("当前活跃连接数: %d, 连接详情: %s", len(connections), string(connData))
|
|
||||||
} else {
|
|
||||||
log.Printf("当前无活跃连接")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
log.Printf("启动连接信息记录器,间隔时间: %v", interval)
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// load config
|
// load config
|
||||||
_, err := flags.Parse(&opts)
|
_, err := flags.Parse(&opts)
|
||||||
@ -131,12 +108,14 @@ func main() {
|
|||||||
Hosts: conf.Server.Hosts,
|
Hosts: conf.Server.Hosts,
|
||||||
HostSelection: conf.Server.HostSelection,
|
HostSelection: conf.Server.HostSelection,
|
||||||
RdpOpts: web.RdpOpts{
|
RdpOpts: web.RdpOpts{
|
||||||
UsernameTemplate: conf.Client.UsernameTemplate,
|
UsernameTemplate: conf.Client.UsernameTemplate,
|
||||||
SplitUserDomain: conf.Client.SplitUserDomain,
|
SplitUserDomain: conf.Client.SplitUserDomain,
|
||||||
NoUsername: conf.Client.NoUsername,
|
DefaultDomain: conf.Client.DefaultDomain,
|
||||||
|
NetworkAutoDetect: conf.Client.NetworkAutoDetect,
|
||||||
|
BandwidthAutoDetect: conf.Client.BandwidthAutoDetect,
|
||||||
|
ConnectionType: conf.Client.ConnectionType,
|
||||||
},
|
},
|
||||||
GatewayAddress: url,
|
GatewayAddress: url,
|
||||||
TemplateFile: conf.Client.Defaults,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if conf.Caps.TokenAuth {
|
if conf.Caps.TokenAuth {
|
||||||
@ -148,10 +127,6 @@ func main() {
|
|||||||
h := w.NewHandler()
|
h := w.NewHandler()
|
||||||
|
|
||||||
log.Printf("Starting remote desktop gateway server")
|
log.Printf("Starting remote desktop gateway server")
|
||||||
|
|
||||||
// 启动连接信息记录器
|
|
||||||
startConnectionLogger(10 * time.Second)
|
|
||||||
|
|
||||||
cfg := &tls.Config{}
|
cfg := &tls.Config{}
|
||||||
|
|
||||||
// configure tls security
|
// configure tls security
|
||||||
@ -248,7 +223,7 @@ func main() {
|
|||||||
r.HandleFunc("/callback", o.HandleCallback)
|
r.HandleFunc("/callback", o.HandleCallback)
|
||||||
|
|
||||||
// only enable un-auth endpoint for openid only config
|
// only enable un-auth endpoint for openid only config
|
||||||
if !conf.Server.KerberosEnabled() && !conf.Server.BasicAuthEnabled() && !conf.Server.NtlmEnabled() {
|
if !conf.Server.KerberosEnabled() || !conf.Server.BasicAuthEnabled() {
|
||||||
rdp.Name("gw").HandlerFunc(gw.HandleGatewayProtocol)
|
rdp.Name("gw").HandlerFunc(gw.HandleGatewayProtocol)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -256,21 +231,11 @@ func main() {
|
|||||||
// for stacking of authentication
|
// for stacking of authentication
|
||||||
auth := web.NewAuthMux()
|
auth := web.NewAuthMux()
|
||||||
rdp.MatcherFunc(web.NoAuthz).HandlerFunc(auth.SetAuthenticate)
|
rdp.MatcherFunc(web.NoAuthz).HandlerFunc(auth.SetAuthenticate)
|
||||||
|
|
||||||
// ntlm
|
|
||||||
if conf.Server.NtlmEnabled() {
|
|
||||||
log.Printf("enabling NTLM authentication")
|
|
||||||
ntlm := web.NTLMAuthHandler{SocketAddress: conf.Server.AuthSocket, Timeout: conf.Server.BasicAuthTimeout}
|
|
||||||
rdp.NewRoute().HeadersRegexp("Authorization", "NTLM").HandlerFunc(ntlm.NTLMAuth(gw.HandleGatewayProtocol))
|
|
||||||
rdp.NewRoute().HeadersRegexp("Authorization", "Negotiate").HandlerFunc(ntlm.NTLMAuth(gw.HandleGatewayProtocol))
|
|
||||||
auth.Register(`NTLM`)
|
|
||||||
auth.Register(`Negotiate`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// basic auth
|
// basic auth
|
||||||
if conf.Server.BasicAuthEnabled() {
|
if conf.Server.BasicAuthEnabled() {
|
||||||
log.Printf("enabling basic authentication")
|
log.Printf("enabling basic authentication")
|
||||||
q := web.BasicAuthHandler{SocketAddress: conf.Server.AuthSocket, Timeout: conf.Server.BasicAuthTimeout}
|
q := web.BasicAuthHandler{SocketAddress: conf.Server.AuthSocket}
|
||||||
rdp.NewRoute().HeadersRegexp("Authorization", "Basic").HandlerFunc(q.BasicAuth(gw.HandleGatewayProtocol))
|
rdp.NewRoute().HeadersRegexp("Authorization", "Basic").HandlerFunc(q.BasicAuth(gw.HandleGatewayProtocol))
|
||||||
auth.Register(`Basic realm="restricted", charset="UTF-8"`)
|
auth.Register(`Basic realm="restricted", charset="UTF-8"`)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,6 @@
|
|||||||
package protocol
|
package protocol
|
||||||
|
|
||||||
import (
|
import "fmt"
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
var Connections map[string]*Monitor
|
var Connections map[string]*Monitor
|
||||||
|
|
||||||
@ -44,40 +41,6 @@ func Disconnect(id string) error {
|
|||||||
return fmt.Errorf("%s connection does not exist", id)
|
return fmt.Errorf("%s connection does not exist", id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetActiveConnections 返回所有当前活跃的连接信息
|
|
||||||
func GetActiveConnections() []map[string]interface{} {
|
|
||||||
connections := []map[string]interface{}{}
|
|
||||||
|
|
||||||
for id, monitor := range Connections {
|
|
||||||
tunnel := monitor.Tunnel
|
|
||||||
if tunnel == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算连接持续时间
|
|
||||||
duration := time.Since(tunnel.ConnectedOn)
|
|
||||||
|
|
||||||
// 收集每个连接的关键信息
|
|
||||||
connInfo := map[string]interface{}{
|
|
||||||
"id": id,
|
|
||||||
"rdgId": tunnel.RDGId,
|
|
||||||
"targetServer": tunnel.TargetServer,
|
|
||||||
"remoteAddr": tunnel.RemoteAddr,
|
|
||||||
"userName": tunnel.User.UserName(),
|
|
||||||
"domain": tunnel.User.Domain(),
|
|
||||||
"connectedOn": tunnel.ConnectedOn,
|
|
||||||
"lastSeen": tunnel.LastSeen,
|
|
||||||
"bytesSent": tunnel.BytesSent,
|
|
||||||
"bytesReceived": tunnel.BytesReceived,
|
|
||||||
"durationSecs": int(duration.Seconds()),
|
|
||||||
}
|
|
||||||
|
|
||||||
connections = append(connections, connInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
return connections
|
|
||||||
}
|
|
||||||
|
|
||||||
// CalculateSpeedPerSecond calculate moving average.
|
// CalculateSpeedPerSecond calculate moving average.
|
||||||
/*
|
/*
|
||||||
func CalculateSpeedPerSecond(connId string) (in int, out int) {
|
func CalculateSpeedPerSecond(connId string) (in int, out int) {
|
||||||
|
|||||||
@ -1,85 +0,0 @@
|
|||||||
package rdp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"sort"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type RDP struct{}
|
|
||||||
|
|
||||||
func Parser() *RDP {
|
|
||||||
return &RDP{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *RDP) Unmarshal(b []byte) (map[string]interface{}, error) {
|
|
||||||
r := bytes.NewReader(b)
|
|
||||||
scanner := bufio.NewScanner(r)
|
|
||||||
mp := make(map[string]interface{})
|
|
||||||
|
|
||||||
c := 0
|
|
||||||
for scanner.Scan() {
|
|
||||||
c++
|
|
||||||
line := strings.TrimSpace(scanner.Text())
|
|
||||||
if line == "" || strings.HasPrefix(line, "#") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
fields := strings.SplitN(line, ":", 3)
|
|
||||||
if len(fields) != 3 {
|
|
||||||
return nil, fmt.Errorf("malformed line %d: %q", c, line)
|
|
||||||
}
|
|
||||||
|
|
||||||
key := strings.TrimSpace(fields[0])
|
|
||||||
t := strings.TrimSpace(fields[1])
|
|
||||||
val := strings.TrimSpace(fields[2])
|
|
||||||
|
|
||||||
switch t {
|
|
||||||
case "i":
|
|
||||||
intValue, err := strconv.Atoi(val)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot parse integer at line %d: %s", c, line)
|
|
||||||
}
|
|
||||||
mp[key] = intValue
|
|
||||||
case "s":
|
|
||||||
mp[key] = val
|
|
||||||
case "b":
|
|
||||||
mp[key] = val
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("malformed line %d: %s", c, line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return mp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *RDP) Marshal(o map[string]interface{}) ([]byte, error) {
|
|
||||||
var b bytes.Buffer
|
|
||||||
|
|
||||||
keys := make([]string, 0, len(o))
|
|
||||||
for k := range o {
|
|
||||||
keys = append(keys, k)
|
|
||||||
}
|
|
||||||
sort.Strings(keys)
|
|
||||||
|
|
||||||
for _, key := range keys {
|
|
||||||
v := o[key]
|
|
||||||
switch v.(type) {
|
|
||||||
case bool:
|
|
||||||
if v == true {
|
|
||||||
fmt.Fprintf(&b, "%s:i:1", key)
|
|
||||||
} else {
|
|
||||||
fmt.Fprintf(&b, "%s:i:0", key)
|
|
||||||
}
|
|
||||||
case int:
|
|
||||||
fmt.Fprintf(&b, "%s:i:%d", key, v)
|
|
||||||
case string:
|
|
||||||
fmt.Fprintf(&b, "%s:s:%s", key, v)
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("error marshalling")
|
|
||||||
}
|
|
||||||
fmt.Fprint(&b, "\r\n")
|
|
||||||
}
|
|
||||||
return b.Bytes(), nil
|
|
||||||
}
|
|
||||||
@ -1,85 +0,0 @@
|
|||||||
package rdp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestUnmarshalRDPFile(t *testing.T) {
|
|
||||||
rdp := Parser()
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
cfg []byte
|
|
||||||
expOutput map[string]interface{}
|
|
||||||
err error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "empty",
|
|
||||||
expOutput: map[string]interface{}{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "string",
|
|
||||||
cfg: []byte(`username:s:user1`),
|
|
||||||
expOutput: map[string]interface{}{
|
|
||||||
"username": "user1",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "integer",
|
|
||||||
cfg: []byte(`session bpp:i:32`),
|
|
||||||
expOutput: map[string]interface{}{
|
|
||||||
"session bpp": 32,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "multi",
|
|
||||||
cfg: []byte("compression:i:1\r\nusername:s:user2\r\n"),
|
|
||||||
expOutput: map[string]interface{}{
|
|
||||||
"compression": 1,
|
|
||||||
"username": "user2",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
outMap, err := rdp.Unmarshal(tc.cfg)
|
|
||||||
assert.Equal(t, tc.err, err)
|
|
||||||
assert.Equal(t, tc.expOutput, outMap)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRDP_Marshal(t *testing.T) {
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
input map[string]interface{}
|
|
||||||
output []byte
|
|
||||||
err error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Empty RDP",
|
|
||||||
input: map[string]interface{}{},
|
|
||||||
output: []byte(nil),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Valid RDP all types",
|
|
||||||
input: map[string]interface{}{
|
|
||||||
"compression": 1,
|
|
||||||
"session bpp": 32,
|
|
||||||
"username": "user1",
|
|
||||||
},
|
|
||||||
output: []byte("compression:i:1\r\nsession bpp:i:32\r\nusername:s:user1\r\n"),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
rdp := Parser()
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
out, err := rdp.Marshal(tc.input)
|
|
||||||
assert.Equal(t, tc.output, out)
|
|
||||||
assert.Equal(t, tc.err, err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,253 +0,0 @@
|
|||||||
package rdp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/rdp/koanf/parsers/rdp"
|
|
||||||
"github.com/fatih/structs"
|
|
||||||
"github.com/go-viper/mapstructure/v2"
|
|
||||||
"github.com/knadh/koanf/providers/file"
|
|
||||||
"github.com/knadh/koanf/v2"
|
|
||||||
"log"
|
|
||||||
"reflect"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
CRLF = "\r\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
SourceNTLM int = iota
|
|
||||||
SourceSmartCard
|
|
||||||
SourceCurrent
|
|
||||||
SourceBasic
|
|
||||||
SourceUserSelect
|
|
||||||
SourceCookie
|
|
||||||
)
|
|
||||||
|
|
||||||
type RdpSettings struct {
|
|
||||||
AllowFontSmoothing bool `rdp:"allow font smoothing" default:"0"`
|
|
||||||
AllowDesktopComposition bool `rdp:"allow desktop composition" default:"0"`
|
|
||||||
DisableFullWindowDrag bool `rdp:"disable full window drag" default:"0"`
|
|
||||||
DisableMenuAnims bool `rdp:"disable menu anims" default:"0"`
|
|
||||||
DisableThemes bool `rdp:"disable themes" default:"0"`
|
|
||||||
DisableCursorSetting bool `rdp:"disable cursor setting" default:"0"`
|
|
||||||
GatewayHostname string `rdp:"gatewayhostname"`
|
|
||||||
FullAddress string `rdp:"full address"`
|
|
||||||
AlternateFullAddress string `rdp:"alternate full address"`
|
|
||||||
Username string `rdp:"username"`
|
|
||||||
Domain string `rdp:"domain"`
|
|
||||||
GatewayCredentialsSource int `rdp:"gatewaycredentialssource" default:"0"`
|
|
||||||
GatewayCredentialMethod int `rdp:"gatewayprofileusagemethod" default:"0"`
|
|
||||||
GatewayUsageMethod int `rdp:"gatewayusagemethod" default:"0"`
|
|
||||||
GatewayAccessToken string `rdp:"gatewayaccesstoken"`
|
|
||||||
PromptCredentialsOnce bool `rdp:"promptcredentialonce" default:"true"`
|
|
||||||
AuthenticationLevel int `rdp:"authentication level" default:"3"`
|
|
||||||
EnableCredSSPSupport bool `rdp:"enablecredsspsupport" default:"true"`
|
|
||||||
EnableRdsAasAuth bool `rdp:"enablerdsaadauth" default:"false"`
|
|
||||||
DisableConnectionSharing bool `rdp:"disableconnectionsharing" default:"false"`
|
|
||||||
AlternateShell string `rdp:"alternate shell"`
|
|
||||||
AutoReconnectionEnabled bool `rdp:"autoreconnectionenabled" default:"true"`
|
|
||||||
BandwidthAutodetect bool `rdp:"bandwidthautodetect" default:"true"`
|
|
||||||
NetworkAutodetect bool `rdp:"networkautodetect" default:"true"`
|
|
||||||
Compression bool `rdp:"compression" default:"true"`
|
|
||||||
VideoPlaybackMode bool `rdp:"videoplaybackmode" default:"true"`
|
|
||||||
ConnectionType int `rdp:"connection type" default:"2"`
|
|
||||||
AudioCaptureMode bool `rdp:"audiocapturemode" default:"false"`
|
|
||||||
EncodeRedirectedVideoCapture bool `rdp:"encode redirected video capture" default:"true"`
|
|
||||||
RedirectedVideoCaptureEncodingQuality int `rdp:"redirected video capture encoding quality" default:"0"`
|
|
||||||
AudioMode int `rdp:"audiomode" default:"0"`
|
|
||||||
CameraStoreRedirect string `rdp:"camerastoredirect" default:"false"`
|
|
||||||
DeviceStoreRedirect string `rdp:"devicestoredirect" default:"false"`
|
|
||||||
DriveStoreRedirect string `rdp:"drivestoredirect" default:"false"`
|
|
||||||
KeyboardHook int `rdp:"keyboardhook" default:"2"`
|
|
||||||
RedirectClipboard bool `rdp:"redirectclipboard" default:"true"`
|
|
||||||
RedirectComPorts bool `rdp:"redirectcomports" default:"false"`
|
|
||||||
RedirectLocation bool `rdp:"redirectlocation" default:"false"`
|
|
||||||
RedirectPrinters bool `rdp:"redirectprinters" default:"true"`
|
|
||||||
RedirectSmartcards bool `rdp:"redirectsmartcards" default:"true"`
|
|
||||||
RedirectWebAuthn bool `rdp:"redirectwebauthn" default:"true"`
|
|
||||||
UsbDeviceStoRedirect string `rdp:"usbdevicestoredirect"`
|
|
||||||
UseMultimon bool `rdp:"use multimon" default:"false"`
|
|
||||||
SelectedMonitors string `rdp:"selectedmonitors"`
|
|
||||||
MaximizeToCurrentDisplays bool `rdp:"maximizetocurrentdisplays" default:"false"`
|
|
||||||
SingleMonInWindowedMode bool `rdp:"singlemoninwindowedmode" default:"0"`
|
|
||||||
ScreenModeId int `rdp:"screen mode id" default:"2"`
|
|
||||||
SmartSizing bool `rdp:"smart sizing" default:"false"`
|
|
||||||
DynamicResolution bool `rdp:"dynamic resolution" default:"true"`
|
|
||||||
DesktopSizeId int `rdp:"desktop size id"`
|
|
||||||
DesktopHeight int `rdp:"desktopheight"`
|
|
||||||
DesktopWidth int `rdp:"desktopwidth"`
|
|
||||||
DesktopScaleFactor int `rdp:"desktopscalefactor"`
|
|
||||||
BitmapCacheSize int `rdp:"bitmapcachesize" default:"1500"`
|
|
||||||
BitmapCachePersistEnable bool `rdp:"bitmapcachepersistenable" default:"true"`
|
|
||||||
RemoteApplicationCmdLine string `rdp:"remoteapplicationcmdline"`
|
|
||||||
RemoteAppExpandWorkingDir bool `rdp:"remoteapplicationexpandworkingdir" default:"true"`
|
|
||||||
RemoteApplicationFile string `rdp:"remoteapplicationfile" default:"true"`
|
|
||||||
RemoteApplicationIcon string `rdp:"remoteapplicationicon"`
|
|
||||||
RemoteApplicationMode bool `rdp:"remoteapplicationmode" default:"false"`
|
|
||||||
RemoteApplicationName string `rdp:"remoteapplicationname"`
|
|
||||||
RemoteApplicationProgram string `rdp:"remoteapplicationprogram"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Builder struct {
|
|
||||||
Settings RdpSettings
|
|
||||||
Metadata mapstructure.Metadata
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewBuilder() *Builder {
|
|
||||||
c := RdpSettings{}
|
|
||||||
|
|
||||||
initStruct(&c)
|
|
||||||
|
|
||||||
return &Builder{
|
|
||||||
Settings: c,
|
|
||||||
Metadata: mapstructure.Metadata{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewBuilderFromFile(filename string) (*Builder, error) {
|
|
||||||
c := RdpSettings{}
|
|
||||||
initStruct(&c)
|
|
||||||
metadata := mapstructure.Metadata{}
|
|
||||||
|
|
||||||
decoderConfig := &mapstructure.DecoderConfig{
|
|
||||||
Result: &c,
|
|
||||||
Metadata: &metadata,
|
|
||||||
WeaklyTypedInput: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
var k = koanf.New(".")
|
|
||||||
if err := k.Load(file.Provider(filename), rdp.Parser()); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
t := koanf.UnmarshalConf{Tag: "rdp", DecoderConfig: decoderConfig}
|
|
||||||
|
|
||||||
if err := k.UnmarshalWithConf("", &c, t); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &Builder{
|
|
||||||
Settings: c,
|
|
||||||
Metadata: metadata,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rb *Builder) String() string {
|
|
||||||
var sb strings.Builder
|
|
||||||
|
|
||||||
addStructToString(rb.Settings, rb.Metadata, &sb)
|
|
||||||
|
|
||||||
return sb.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func addStructToString(st interface{}, metadata mapstructure.Metadata, sb *strings.Builder) {
|
|
||||||
s := structs.New(st)
|
|
||||||
for _, f := range s.Fields() {
|
|
||||||
if isZero(f) && !isSet(f, metadata) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
sb.WriteString(f.Tag("rdp"))
|
|
||||||
sb.WriteString(":")
|
|
||||||
|
|
||||||
switch f.Kind() {
|
|
||||||
case reflect.String:
|
|
||||||
sb.WriteString("s:")
|
|
||||||
sb.WriteString(f.Value().(string))
|
|
||||||
case reflect.Int:
|
|
||||||
sb.WriteString("i:")
|
|
||||||
fmt.Fprintf(sb, "%d", f.Value())
|
|
||||||
case reflect.Bool:
|
|
||||||
sb.WriteString("i:")
|
|
||||||
if f.Value().(bool) {
|
|
||||||
sb.WriteString("1")
|
|
||||||
} else {
|
|
||||||
sb.WriteString("0")
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sb.WriteString(CRLF)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func isZero(f *structs.Field) bool {
|
|
||||||
t := f.Tag("default")
|
|
||||||
if t == "" {
|
|
||||||
return f.IsZero()
|
|
||||||
}
|
|
||||||
|
|
||||||
switch f.Kind() {
|
|
||||||
case reflect.String:
|
|
||||||
if f.Value().(string) != t {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
case reflect.Int:
|
|
||||||
i, err := strconv.Atoi(t)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("runtime error: default %s is not an integer", t)
|
|
||||||
}
|
|
||||||
if f.Value().(int) != i {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
case reflect.Bool:
|
|
||||||
b := false
|
|
||||||
if t == "true" || t == "1" {
|
|
||||||
b = true
|
|
||||||
}
|
|
||||||
if f.Value().(bool) != b {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return f.IsZero()
|
|
||||||
}
|
|
||||||
|
|
||||||
func isSet(f *structs.Field, metadata mapstructure.Metadata) bool {
|
|
||||||
for _, v := range metadata.Unset {
|
|
||||||
if v == f.Name() {
|
|
||||||
log.Printf("field %s is unset", f.Name())
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func initStruct(st interface{}) {
|
|
||||||
s := structs.New(st)
|
|
||||||
for _, f := range s.Fields() {
|
|
||||||
t := f.Tag("default")
|
|
||||||
if t == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
err := setVariable(f, t)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("cannot init rdp struct: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func setVariable(f *structs.Field, v string) error {
|
|
||||||
switch f.Kind() {
|
|
||||||
case reflect.String:
|
|
||||||
return f.Set(v)
|
|
||||||
case reflect.Int:
|
|
||||||
i, err := strconv.Atoi(v)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return f.Set(i)
|
|
||||||
case reflect.Bool:
|
|
||||||
b := false
|
|
||||||
if v == "true" || v == "1" {
|
|
||||||
b = true
|
|
||||||
}
|
|
||||||
return f.Set(b)
|
|
||||||
default:
|
|
||||||
return errors.New("invalid field type")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,47 +0,0 @@
|
|||||||
package rdp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
GatewayHostName = "my.yahoo.com"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestRdpBuilder(t *testing.T) {
|
|
||||||
builder := NewBuilder()
|
|
||||||
builder.Settings.GatewayHostname = "my.yahoo.com"
|
|
||||||
builder.Settings.AutoReconnectionEnabled = true
|
|
||||||
builder.Settings.SmartSizing = true
|
|
||||||
|
|
||||||
s := builder.String()
|
|
||||||
if !strings.Contains(s, "gatewayhostname:s:"+GatewayHostName+CRLF) {
|
|
||||||
t.Fatalf("%s does not contain `gatewayhostname:s:%s", s, GatewayHostName)
|
|
||||||
}
|
|
||||||
if strings.Contains(s, "autoreconnectionenabled") {
|
|
||||||
t.Fatalf("autoreconnectionenabled is in %s, but it's default value", s)
|
|
||||||
}
|
|
||||||
if !strings.Contains(s, "smart sizing:i:1"+CRLF) {
|
|
||||||
t.Fatalf("%s does not contain smart sizing:i:1", s)
|
|
||||||
|
|
||||||
}
|
|
||||||
log.Printf(builder.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestInitStruct(t *testing.T) {
|
|
||||||
conn := RdpSettings{}
|
|
||||||
initStruct(&conn)
|
|
||||||
|
|
||||||
if conn.PromptCredentialsOnce != true {
|
|
||||||
t.Fatalf("conn.PromptCredentialsOnce != true")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLoadFile(t *testing.T) {
|
|
||||||
_, err := NewBuilderFromFile("rdp_test_file.rdp")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("LoadFile failed: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
Password:b:0200000000000000000000000000000000000000000000000800000072006400700000000E660000100000001000000031A2D4A21767565E3A268420A9397C4400000000048000001000000010000000A56C359BBBA13EC284391427E6A107BD20000000333E6F6DA024E1B6B4CC7DDF57BFC1783ED02F212B8FBD39997C888F9D4B438914000000A80D19234BA4CC5CE2695A34EF0B9B92D5D777A6
|
|
||||||
ColorDepthID:i:1
|
|
||||||
ScreenStyle:i:0
|
|
||||||
DesktopWidth:i:640
|
|
||||||
DesktopHeight:i:480
|
|
||||||
UserName:s:rdesktop
|
|
||||||
SavePassword:i:1
|
|
||||||
Keyboard Layout:s:00000409
|
|
||||||
BitmapPersistCacheSize:i:1
|
|
||||||
BitmapCacheSize:i:21
|
|
||||||
KeyboardFunctionKey:i:12
|
|
||||||
KeyboardSubType:i:0
|
|
||||||
KeyboardType:i:4
|
|
||||||
KeyboardLayoutString:s:0xE0010409
|
|
||||||
Disable Themes:i:0
|
|
||||||
Disable Menu Anims:i:1
|
|
||||||
Disable Full Window Drag:i:1
|
|
||||||
Disable Wallpaper:i:1
|
|
||||||
MaxReconnectAttempts:i:20
|
|
||||||
KeyboardHookMode:i:0
|
|
||||||
Compress:i:1
|
|
||||||
BBarShowPinBtn:i:0
|
|
||||||
BitmapPersistenceEnabled:i:0
|
|
||||||
AudioRedirectionMode:i:2
|
|
||||||
EnablePortRedirection:i:0
|
|
||||||
EnableDriveRedirection:i:0
|
|
||||||
AutoReconnectEnabled:i:1
|
|
||||||
EnableSCardRedirection:i:1
|
|
||||||
EnablePrinterRedirection:i:0
|
|
||||||
BBarEnabled:i:0
|
|
||||||
DisableFileAccess:i:0
|
|
||||||
MinutesToIdleTimeout:i:5
|
|
||||||
GrabFocusOnConnect:i:0
|
|
||||||
StartFullScreen:i:1
|
|
||||||
Domain:s:GE3SDT8KLRL4J
|
|
||||||
enablecredsspsupport:i:0
|
|
||||||
use multimon:i:1
|
|
||||||
@ -7,8 +7,8 @@ import (
|
|||||||
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/identity"
|
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/identity"
|
||||||
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/protocol"
|
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/protocol"
|
||||||
"github.com/coreos/go-oidc/v3/oidc"
|
"github.com/coreos/go-oidc/v3/oidc"
|
||||||
"github.com/go-jose/go-jose/v4"
|
"github.com/go-jose/go-jose/v3"
|
||||||
"github.com/go-jose/go-jose/v4/jwt"
|
"github.com/go-jose/go-jose/v3/jwt"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
@ -62,9 +62,9 @@ func CheckPAACookie(ctx context.Context, tokenString string) (bool, error) {
|
|||||||
return false, errors.New("no token to parse")
|
return false, errors.New("no token to parse")
|
||||||
}
|
}
|
||||||
|
|
||||||
token, err := jwt.ParseSigned(tokenString, []jose.SignatureAlgorithm{jose.HS256})
|
token, err := jwt.ParseSigned(tokenString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("cannot parse token due to: %t", err)
|
log.Printf("cannot parse token due to: %tunnel", err)
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,7 +136,7 @@ func GeneratePAAToken(ctx context.Context, username string, server string) (stri
|
|||||||
AccessToken: id.GetAttribute(identity.AttrAccessToken).(string),
|
AccessToken: id.GetAttribute(identity.AttrAccessToken).(string),
|
||||||
}
|
}
|
||||||
|
|
||||||
if token, err := jwt.Signed(sig).Claims(standard).Claims(private).Serialize(); err != nil {
|
if token, err := jwt.Signed(sig).Claims(standard).Claims(private).CompactSerialize(); err != nil {
|
||||||
log.Printf("Cannot sign PAA token %s", err)
|
log.Printf("Cannot sign PAA token %s", err)
|
||||||
return "", err
|
return "", err
|
||||||
} else {
|
} else {
|
||||||
@ -157,10 +157,7 @@ func GenerateUserToken(ctx context.Context, userName string) (string, error) {
|
|||||||
|
|
||||||
enc, err := jose.NewEncrypter(
|
enc, err := jose.NewEncrypter(
|
||||||
jose.A128CBC_HS256,
|
jose.A128CBC_HS256,
|
||||||
jose.Recipient{
|
jose.Recipient{Algorithm: jose.DIRECT, Key: UserEncryptionKey},
|
||||||
Algorithm: jose.DIRECT,
|
|
||||||
Key: UserEncryptionKey,
|
|
||||||
},
|
|
||||||
(&jose.EncrypterOptions{Compression: jose.DEFLATE}).WithContentType("JWT"),
|
(&jose.EncrypterOptions{Compression: jose.DEFLATE}).WithContentType("JWT"),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -170,29 +167,16 @@ func GenerateUserToken(ctx context.Context, userName string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// this makes the token bigger and we deal with a limited space of 511 characters
|
// this makes the token bigger and we deal with a limited space of 511 characters
|
||||||
if len(UserSigningKey) > 0 {
|
// sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.HS256, Key: SigningKey}, nil)
|
||||||
sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.HS256, Key: UserSigningKey}, nil)
|
// token, err := jwt.SignedAndEncrypted(sig, enc).Claims(claims).CompactSerialize()
|
||||||
token, err := jwt.SignedAndEncrypted(sig, enc).Claims(claims).Serialize()
|
token, err := jwt.Encrypted(enc).Claims(claims).CompactSerialize()
|
||||||
if len(token) > 511 {
|
|
||||||
log.Printf("WARNING: token too long: len %d > 511", len(token))
|
|
||||||
}
|
|
||||||
return token, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// no signature
|
|
||||||
token, err := jwt.Encrypted(enc).Claims(claims).Serialize()
|
|
||||||
return token, err
|
return token, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func UserInfo(ctx context.Context, token string) (jwt.Claims, error) {
|
func UserInfo(ctx context.Context, token string) (jwt.Claims, error) {
|
||||||
standard := jwt.Claims{}
|
standard := jwt.Claims{}
|
||||||
if len(UserEncryptionKey) > 0 && len(UserSigningKey) > 0 {
|
if len(UserEncryptionKey) > 0 && len(UserSigningKey) > 0 {
|
||||||
enc, err := jwt.ParseSignedAndEncrypted(
|
enc, err := jwt.ParseSignedAndEncrypted(token)
|
||||||
token,
|
|
||||||
[]jose.KeyAlgorithm{jose.DIRECT},
|
|
||||||
[]jose.ContentEncryption{jose.A128CBC_HS256},
|
|
||||||
[]jose.SignatureAlgorithm{jose.HS256},
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Cannot get token %s", err)
|
log.Printf("Cannot get token %s", err)
|
||||||
return standard, errors.New("cannot get token")
|
return standard, errors.New("cannot get token")
|
||||||
@ -202,12 +186,16 @@ func UserInfo(ctx context.Context, token string) (jwt.Claims, error) {
|
|||||||
log.Printf("Cannot decrypt token %s", err)
|
log.Printf("Cannot decrypt token %s", err)
|
||||||
return standard, errors.New("cannot decrypt token")
|
return standard, errors.New("cannot decrypt token")
|
||||||
}
|
}
|
||||||
|
if _, err := verifyAlg(token.Headers, string(jose.HS256)); err != nil {
|
||||||
|
log.Printf("signature validation failure: %s", err)
|
||||||
|
return standard, errors.New("signature validation failure")
|
||||||
|
}
|
||||||
if err = token.Claims(UserSigningKey, &standard); err != nil {
|
if err = token.Claims(UserSigningKey, &standard); err != nil {
|
||||||
log.Printf("cannot verify signature %s", err)
|
log.Printf("cannot verify signature %s", err)
|
||||||
return standard, errors.New("cannot verify signature")
|
return standard, errors.New("cannot verify signature")
|
||||||
}
|
}
|
||||||
} else if len(UserSigningKey) == 0 {
|
} else if len(UserSigningKey) == 0 {
|
||||||
token, err := jwt.ParseEncrypted(token, []jose.KeyAlgorithm{jose.DIRECT}, []jose.ContentEncryption{jose.A128CBC_HS256})
|
token, err := jwt.ParseEncrypted(token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Cannot get token %s", err)
|
log.Printf("Cannot get token %s", err)
|
||||||
return standard, errors.New("cannot get token")
|
return standard, errors.New("cannot get token")
|
||||||
@ -217,6 +205,21 @@ func UserInfo(ctx context.Context, token string) (jwt.Claims, error) {
|
|||||||
log.Printf("Cannot decrypt token %s", err)
|
log.Printf("Cannot decrypt token %s", err)
|
||||||
return standard, errors.New("cannot decrypt token")
|
return standard, errors.New("cannot decrypt token")
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
token, err := jwt.ParseSigned(token)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Cannot get token %s", err)
|
||||||
|
return standard, errors.New("cannot get token")
|
||||||
|
}
|
||||||
|
if _, err := verifyAlg(token.Headers, string(jose.HS256)); err != nil {
|
||||||
|
log.Printf("signature validation failure: %s", err)
|
||||||
|
return standard, errors.New("signature validation failure")
|
||||||
|
}
|
||||||
|
err = token.Claims(UserSigningKey, &standard)
|
||||||
|
if err = token.Claims(UserSigningKey, &standard); err != nil {
|
||||||
|
log.Printf("cannot verify signature %s", err)
|
||||||
|
return standard, errors.New("cannot verify signature")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// go-jose doesnt verify the expiry
|
// go-jose doesnt verify the expiry
|
||||||
@ -235,11 +238,15 @@ func UserInfo(ctx context.Context, token string) (jwt.Claims, error) {
|
|||||||
|
|
||||||
func QueryInfo(ctx context.Context, tokenString string, issuer string) (string, error) {
|
func QueryInfo(ctx context.Context, tokenString string, issuer string) (string, error) {
|
||||||
standard := jwt.Claims{}
|
standard := jwt.Claims{}
|
||||||
token, err := jwt.ParseSigned(tokenString, []jose.SignatureAlgorithm{jose.HS256})
|
token, err := jwt.ParseSigned(tokenString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Cannot get token %s", err)
|
log.Printf("Cannot get token %s", err)
|
||||||
return "", errors.New("cannot get token")
|
return "", errors.New("cannot get token")
|
||||||
}
|
}
|
||||||
|
if _, err := verifyAlg(token.Headers, string(jose.HS256)); err != nil {
|
||||||
|
log.Printf("signature validation failure: %s", err)
|
||||||
|
return "", errors.New("signature validation failure")
|
||||||
|
}
|
||||||
err = token.Claims(QuerySigningKey, &standard)
|
err = token.Claims(QuerySigningKey, &standard)
|
||||||
if err = token.Claims(QuerySigningKey, &standard); err != nil {
|
if err = token.Claims(QuerySigningKey, &standard); err != nil {
|
||||||
log.Printf("cannot verify signature %s", err)
|
log.Printf("cannot verify signature %s", err)
|
||||||
@ -280,7 +287,7 @@ func GenerateQueryToken(ctx context.Context, query string, issuer string) (strin
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
token, err := jwt.Signed(sig).Claims(claims).Serialize()
|
token, err := jwt.Signed(sig).Claims(claims).CompactSerialize()
|
||||||
return token, err
|
return token, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -292,3 +299,12 @@ func getTunnel(ctx context.Context) *protocol.Tunnel {
|
|||||||
}
|
}
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func verifyAlg(headers []jose.Header, alg string) (bool, error) {
|
||||||
|
for _, header := range headers {
|
||||||
|
if header.Algorithm != alg {
|
||||||
|
return false, fmt.Errorf("invalid signing method %s", header.Algorithm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|||||||
@ -1,76 +0,0 @@
|
|||||||
package security
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/identity"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestGenerateUserToken(t *testing.T) {
|
|
||||||
cases := []struct {
|
|
||||||
SigningKey []byte
|
|
||||||
EncryptionKey []byte
|
|
||||||
name string
|
|
||||||
username string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
SigningKey: []byte("5aa3a1568fe8421cd7e127d5ace28d2d"),
|
|
||||||
EncryptionKey: []byte("d3ecd7e565e56e37e2f2e95b584d8c0c"),
|
|
||||||
name: "sign_and_encrypt",
|
|
||||||
username: "test_sign_and_encrypt",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
SigningKey: nil,
|
|
||||||
EncryptionKey: []byte("d3ecd7e565e56e37e2f2e95b584d8c0c"),
|
|
||||||
name: "encrypt_only",
|
|
||||||
username: "test_encrypt_only",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tc := range cases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
SigningKey = tc.SigningKey
|
|
||||||
UserEncryptionKey = tc.EncryptionKey
|
|
||||||
token, err := GenerateUserToken(context.Background(), tc.username)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("GenerateUserToken failed: %s", err)
|
|
||||||
}
|
|
||||||
claims, err := UserInfo(context.Background(), token)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("UserInfo failed: %s", err)
|
|
||||||
}
|
|
||||||
if claims.Subject != tc.username {
|
|
||||||
t.Fatalf("Expected %s, got %s", tc.username, claims.Subject)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPAACookie(t *testing.T) {
|
|
||||||
SigningKey = []byte("5aa3a1568fe8421cd7e127d5ace28d2d")
|
|
||||||
EncryptionKey = []byte("d3ecd7e565e56e37e2f2e95b584d8c0c")
|
|
||||||
|
|
||||||
username := "test_paa_cookie"
|
|
||||||
attr_client_ip := "127.0.0.1"
|
|
||||||
attr_access_token := "aabbcc"
|
|
||||||
|
|
||||||
id := identity.NewUser()
|
|
||||||
id.SetUserName(username)
|
|
||||||
id.SetAttribute(identity.AttrClientIp, attr_client_ip)
|
|
||||||
id.SetAttribute(identity.AttrAccessToken, attr_access_token)
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
ctx = context.WithValue(ctx, identity.CTXKey, id)
|
|
||||||
|
|
||||||
_, err := GeneratePAAToken(ctx, "test_paa_cookie", "host.does.not.exist")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("GeneratePAAToken failed: %s", err)
|
|
||||||
}
|
|
||||||
/*ok, err := CheckPAACookie(ctx, token)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("CheckPAACookie failed: %s", err)
|
|
||||||
}
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("CheckPAACookie failed")
|
|
||||||
}*/
|
|
||||||
}
|
|
||||||
@ -2,9 +2,9 @@ package transport
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"crypto/rand"
|
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
@ -12,14 +12,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
crlf = "\r\n"
|
crlf = "\r\n"
|
||||||
HttpOK = "HTTP/1.1 200 OK\r\n"
|
HttpOK = "HTTP/1.1 200 OK\r\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
type LegacyPKT struct {
|
type LegacyPKT struct {
|
||||||
Conn net.Conn
|
Conn net.Conn
|
||||||
ChunkedReader io.Reader
|
ChunkedReader io.Reader
|
||||||
Writer *bufio.Writer
|
Writer *bufio.Writer
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewLegacy(w http.ResponseWriter) (*LegacyPKT, error) {
|
func NewLegacy(w http.ResponseWriter) (*LegacyPKT, error) {
|
||||||
@ -27,9 +27,9 @@ func NewLegacy(w http.ResponseWriter) (*LegacyPKT, error) {
|
|||||||
if ok {
|
if ok {
|
||||||
conn, rw, err := hj.Hijack()
|
conn, rw, err := hj.Hijack()
|
||||||
l := &LegacyPKT{
|
l := &LegacyPKT{
|
||||||
Conn: conn,
|
Conn: conn,
|
||||||
ChunkedReader: httputil.NewChunkedReader(rw.Reader),
|
ChunkedReader: httputil.NewChunkedReader(rw.Reader),
|
||||||
Writer: rw.Writer,
|
Writer: rw.Writer,
|
||||||
}
|
}
|
||||||
return l, err
|
return l, err
|
||||||
}
|
}
|
||||||
@ -37,7 +37,7 @@ func NewLegacy(w http.ResponseWriter) (*LegacyPKT, error) {
|
|||||||
return nil, errors.New("cannot hijack connection")
|
return nil, errors.New("cannot hijack connection")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *LegacyPKT) ReadPacket() (n int, p []byte, err error) {
|
func (t *LegacyPKT) ReadPacket() (n int, p []byte, err error){
|
||||||
buf := make([]byte, 4096) // bufio.defaultBufSize
|
buf := make([]byte, 4096) // bufio.defaultBufSize
|
||||||
n, err = t.ChunkedReader.Read(buf)
|
n, err = t.ChunkedReader.Read(buf)
|
||||||
p = make([]byte, n)
|
p = make([]byte, n)
|
||||||
|
|||||||
@ -18,16 +18,38 @@ const (
|
|||||||
|
|
||||||
type BasicAuthHandler struct {
|
type BasicAuthHandler struct {
|
||||||
SocketAddress string
|
SocketAddress string
|
||||||
Timeout int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *BasicAuthHandler) BasicAuth(next http.HandlerFunc) http.HandlerFunc {
|
func (h *BasicAuthHandler) BasicAuth(next http.HandlerFunc) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
username, password, ok := r.BasicAuth()
|
username, password, ok := r.BasicAuth()
|
||||||
if ok {
|
if ok {
|
||||||
authenticated := h.authenticate(w, r, username, password)
|
ctx := r.Context()
|
||||||
|
|
||||||
if !authenticated {
|
conn, err := grpc.Dial(h.SocketAddress, grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||||
|
grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) {
|
||||||
|
return net.Dial(protocolGrpc, addr)
|
||||||
|
}))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Cannot reach authentication provider: %s", err)
|
||||||
|
http.Error(w, "Server error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
c := auth.NewAuthenticateClient(conn)
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
req := &auth.UserPass{Username: username, Password: password}
|
||||||
|
res, err := c.Authenticate(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error talking to authentication provider: %s", err)
|
||||||
|
http.Error(w, "Server error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !res.Authenticated {
|
||||||
log.Printf("User %s is not authenticated for this service", username)
|
log.Printf("User %s is not authenticated for this service", username)
|
||||||
} else {
|
} else {
|
||||||
log.Printf("User %s authenticated", username)
|
log.Printf("User %s authenticated", username)
|
||||||
@ -38,6 +60,7 @@ func (h *BasicAuthHandler) BasicAuth(next http.HandlerFunc) http.HandlerFunc {
|
|||||||
next.ServeHTTP(w, identity.AddToRequestCtx(id, r))
|
next.ServeHTTP(w, identity.AddToRequestCtx(id, r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
// If the Authentication header is not present, is invalid, or the
|
// If the Authentication header is not present, is invalid, or the
|
||||||
// username or password is wrong, then set a WWW-Authenticate
|
// username or password is wrong, then set a WWW-Authenticate
|
||||||
@ -47,36 +70,3 @@ func (h *BasicAuthHandler) BasicAuth(next http.HandlerFunc) http.HandlerFunc {
|
|||||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *BasicAuthHandler) authenticate(w http.ResponseWriter, r *http.Request, username string, password string) (authenticated bool) {
|
|
||||||
if h.SocketAddress == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := r.Context()
|
|
||||||
|
|
||||||
conn, err := grpc.Dial(h.SocketAddress, grpc.WithTransportCredentials(insecure.NewCredentials()),
|
|
||||||
grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) {
|
|
||||||
return net.Dial(protocolGrpc, addr)
|
|
||||||
}))
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Cannot reach authentication provider: %s", err)
|
|
||||||
http.Error(w, "Server error", http.StatusInternalServerError)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
c := auth.NewAuthenticateClient(conn)
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(h.Timeout))
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
req := &auth.UserPass{Username: username, Password: password}
|
|
||||||
res, err := c.Authenticate(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error talking to authentication provider: %s", err)
|
|
||||||
http.Error(w, "Server error", http.StatusInternalServerError)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.Authenticated
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,120 +0,0 @@
|
|||||||
package web
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/identity"
|
|
||||||
"github.com/bolkedebruin/rdpgw/shared/auth"
|
|
||||||
"google.golang.org/grpc"
|
|
||||||
"google.golang.org/grpc/credentials/insecure"
|
|
||||||
"log"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ntlmAuthMode uint32
|
|
||||||
const (
|
|
||||||
authNone ntlmAuthMode = iota
|
|
||||||
authNTLM
|
|
||||||
authNegotiate
|
|
||||||
)
|
|
||||||
|
|
||||||
type NTLMAuthHandler struct {
|
|
||||||
SocketAddress string
|
|
||||||
Timeout int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *NTLMAuthHandler) NTLMAuth(next http.HandlerFunc) http.HandlerFunc {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
authPayload, authMode, err := h.getAuthPayload(r)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Failed parsing auth header: %s", err)
|
|
||||||
h.requestAuthenticate(w)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
authenticated, username := h.authenticate(w, r, authPayload, authMode)
|
|
||||||
|
|
||||||
if authenticated {
|
|
||||||
log.Printf("NTLM: User %s authenticated", username)
|
|
||||||
id := identity.FromRequestCtx(r)
|
|
||||||
id.SetUserName(username)
|
|
||||||
id.SetAuthenticated(true)
|
|
||||||
id.SetAuthTime(time.Now())
|
|
||||||
next.ServeHTTP(w, identity.AddToRequestCtx(id, r))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *NTLMAuthHandler) getAuthPayload (r *http.Request) (payload string, authMode ntlmAuthMode, err error) {
|
|
||||||
authorisationEncoded := r.Header.Get("Authorization")
|
|
||||||
if authorisationEncoded[0:5] == "NTLM " {
|
|
||||||
return authorisationEncoded[5:], authNTLM, nil
|
|
||||||
}
|
|
||||||
if authorisationEncoded[0:10] == "Negotiate " {
|
|
||||||
return authorisationEncoded[10:], authNegotiate, nil
|
|
||||||
}
|
|
||||||
return "", authNone, errors.New("Invalid NTLM Authorisation header")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *NTLMAuthHandler) requestAuthenticate (w http.ResponseWriter) {
|
|
||||||
w.Header().Add("WWW-Authenticate", `NTLM`)
|
|
||||||
w.Header().Add("WWW-Authenticate", `Negotiate`)
|
|
||||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *NTLMAuthHandler) getAuthPrefix (authMode ntlmAuthMode) (prefix string) {
|
|
||||||
if authMode == authNTLM {
|
|
||||||
return "NTLM "
|
|
||||||
}
|
|
||||||
if authMode == authNegotiate {
|
|
||||||
return "Negotiate "
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *NTLMAuthHandler) authenticate(w http.ResponseWriter, r *http.Request, authorisationEncoded string, authMode ntlmAuthMode) (authenticated bool, username string) {
|
|
||||||
if h.SocketAddress == "" {
|
|
||||||
return false, ""
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := r.Context()
|
|
||||||
|
|
||||||
conn, err := grpc.Dial(h.SocketAddress, grpc.WithTransportCredentials(insecure.NewCredentials()),
|
|
||||||
grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) {
|
|
||||||
return net.Dial(protocolGrpc, addr)
|
|
||||||
}))
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Cannot reach authentication provider: %s", err)
|
|
||||||
http.Error(w, "Server error", http.StatusInternalServerError)
|
|
||||||
return false, ""
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
c := auth.NewAuthenticateClient(conn)
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(h.Timeout))
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
req := &auth.NtlmRequest{Session: r.RemoteAddr, NtlmMessage: authorisationEncoded}
|
|
||||||
res, err := c.NTLM(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error talking to authentication provider: %s", err)
|
|
||||||
http.Error(w, "Server error", http.StatusInternalServerError)
|
|
||||||
return false, ""
|
|
||||||
}
|
|
||||||
|
|
||||||
if res.NtlmMessage != "" {
|
|
||||||
log.Printf("Sending NTLM challenge")
|
|
||||||
w.Header().Add("WWW-Authenticate", h.getAuthPrefix(authMode)+res.NtlmMessage)
|
|
||||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
|
||||||
return false, ""
|
|
||||||
}
|
|
||||||
|
|
||||||
if !res.Authenticated {
|
|
||||||
h.requestAuthenticate(w)
|
|
||||||
return false, ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.Authenticated, res.Username
|
|
||||||
}
|
|
||||||
@ -1,13 +1,13 @@
|
|||||||
package web
|
package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/identity"
|
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/identity"
|
||||||
"github.com/coreos/go-oidc/v3/oidc"
|
"github.com/coreos/go-oidc/v3/oidc"
|
||||||
"github.com/patrickmn/go-cache"
|
"github.com/patrickmn/go-cache"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@ -99,7 +99,7 @@ func (h *OIDC) HandleCallback(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func findUsernameInClaims(data map[string]interface{}) string {
|
func findUsernameInClaims(data map[string]interface{}) string {
|
||||||
candidates := []string{"preferred_username", "unique_name", "upn", "username"}
|
candidates := []string{"preferred_username", "unique_name", "upn"}
|
||||||
for _, claim := range candidates {
|
for _, claim := range candidates {
|
||||||
userName, found := data[claim].(string)
|
userName, found := data[claim].(string)
|
||||||
if found {
|
if found {
|
||||||
@ -116,11 +116,7 @@ func (h *OIDC) Authenticated(next http.Handler) http.Handler {
|
|||||||
|
|
||||||
if !id.Authenticated() {
|
if !id.Authenticated() {
|
||||||
seed := make([]byte, 16)
|
seed := make([]byte, 16)
|
||||||
_, err := rand.Read(seed)
|
rand.Read(seed)
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
state := hex.EncodeToString(seed)
|
state := hex.EncodeToString(seed)
|
||||||
h.stateStore.Set(state, r.RequestURI, cache.DefaultExpiration)
|
h.stateStore.Set(state, r.RequestURI, cache.DefaultExpiration)
|
||||||
http.Redirect(w, r, h.oAuth2Config.AuthCodeURL(state), http.StatusFound)
|
http.Redirect(w, r, h.oAuth2Config.AuthCodeURL(state), http.StatusFound)
|
||||||
|
|||||||
229
cmd/rdpgw/web/rdp.go
Normal file
229
cmd/rdpgw/web/rdp.go
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/fatih/structs"
|
||||||
|
"log"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
crlf = "\r\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
SourceNTLM int = iota
|
||||||
|
SourceSmartCard
|
||||||
|
SourceCurrent
|
||||||
|
SourceBasic
|
||||||
|
SourceUserSelect
|
||||||
|
SourceCookie
|
||||||
|
)
|
||||||
|
|
||||||
|
type RdpConnection struct {
|
||||||
|
GatewayHostname string `rdp:"gatewayhostname"`
|
||||||
|
FullAddress string `rdp:"full address"`
|
||||||
|
AlternateFullAddress string `rdp:"alternate full address"`
|
||||||
|
Username string `rdp:"username"`
|
||||||
|
Domain string `rdp:"domain"`
|
||||||
|
GatewayCredentialsSource int `rdp:"gatewaycredentialssource" default:"0"`
|
||||||
|
GatewayCredentialMethod int `rdp:"gatewayprofileusagemethod" default:"0"`
|
||||||
|
GatewayUsageMethod int `rdp:"gatewayusagemethod" default:"0"`
|
||||||
|
GatewayAccessToken string `rdp:"gatewayaccesstoken"`
|
||||||
|
PromptCredentialsOnce bool `rdp:"promptcredentialonce" default:"true"`
|
||||||
|
AuthenticationLevel int `rdp:"authentication level" default:"3"`
|
||||||
|
EnableCredSSPSupport bool `rdp:"enablecredsspsupport" default:"true"`
|
||||||
|
EnableRdsAasAuth bool `rdp:"enablerdsaadauth" default:"false"`
|
||||||
|
DisableConnectionSharing bool `rdp:"disableconnectionsharing" default:"false"`
|
||||||
|
AlternateShell string `rdp:"alternate shell"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RdpSession struct {
|
||||||
|
AutoReconnectionEnabled bool `rdp:"autoreconnectionenabled" default:"true"`
|
||||||
|
BandwidthAutodetect bool `rdp:"bandwidthautodetect" default:"true"`
|
||||||
|
NetworkAutodetect bool `rdp:"networkautodetect" default:"true"`
|
||||||
|
Compression bool `rdp:"compression" default:"true"`
|
||||||
|
VideoPlaybackMode bool `rdp:"videoplaybackmode" default:"true"`
|
||||||
|
ConnectionType int `rdp:"connection type" default:"2"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RdpDeviceRedirect struct {
|
||||||
|
AudioCaptureMode bool `rdp:"audiocapturemode" default:"false"`
|
||||||
|
EncodeRedirectedVideoCapture bool `rdp:"encode redirected video capture" default:"true"`
|
||||||
|
RedirectedVideoCaptureEncodingQuality int `rdp:"redirected video capture encoding quality" default:"0"`
|
||||||
|
AudioMode int `rdp:"audiomode" default:"0"`
|
||||||
|
CameraStoreRedirect string `rdp:"camerastoredirect" default:"false"`
|
||||||
|
DeviceStoreRedirect string `rdp:"devicestoredirect" default:"false"`
|
||||||
|
DriveStoreRedirect string `rdp:"drivestoredirect" default:"false"`
|
||||||
|
KeyboardHook int `rdp:"keyboardhook" default:"2"`
|
||||||
|
RedirectClipboard bool `rdp:"redirectclipboard" default:"true"`
|
||||||
|
RedirectComPorts bool `rdp:"redirectcomports" default:"false"`
|
||||||
|
RedirectLocation bool `rdp:"redirectlocation" default:"false"`
|
||||||
|
RedirectPrinters bool `rdp:"redirectprinters" default:"true"`
|
||||||
|
RedirectSmartcards bool `rdp:"redirectsmartcards" default:"true"`
|
||||||
|
RedirectWebAuthn bool `rdp:"redirectwebauthn" default:"true"`
|
||||||
|
UsbDeviceStoRedirect string `rdp:"usbdevicestoredirect"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RdpDisplay struct {
|
||||||
|
UseMultimon bool `rdp:"use multimon" default:"false"`
|
||||||
|
SelectedMonitors string `rdp:"selectedmonitors"`
|
||||||
|
MaximizeToCurrentDisplays bool `rdp:"maximizetocurrentdisplays" default:"false"`
|
||||||
|
SingleMonInWindowedMode bool `rdp:"singlemoninwindowedmode" default:"0"`
|
||||||
|
ScreenModeId int `rdp:"screen mode id" default:"2"`
|
||||||
|
SmartSizing bool `rdp:"smart sizing" default:"false"`
|
||||||
|
DynamicResolution bool `rdp:"dynamic resolution" default:"true"`
|
||||||
|
DesktopSizeId int `rdp:"desktop size id"`
|
||||||
|
DesktopHeight int `rdp:"desktopheight"`
|
||||||
|
DesktopWidth int `rdp:"desktopwidth"`
|
||||||
|
DesktopScaleFactor int `rdp:"desktopscalefactor"`
|
||||||
|
BitmapCacheSize int `rdp:"bitmapcachesize" default:"1500"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RdpRemoteApp struct {
|
||||||
|
RemoteApplicationCmdLine string `rdp:"remoteapplicationcmdline"`
|
||||||
|
RemoteAppExpandWorkingDir bool `rdp:"remoteapplicationexpandworkingdir" default:"true"`
|
||||||
|
RemoteApplicationFile string `rdp:"remoteapplicationfile" default:"true"`
|
||||||
|
RemoteApplicationIcon string `rdp:"remoteapplicationicon"`
|
||||||
|
RemoteApplicationMode bool `rdp:"remoteapplicationmode" default:"true"`
|
||||||
|
RemoteApplicationName string `rdp:"remoteapplicationname"`
|
||||||
|
RemoteApplicationProgram string `rdp:"remoteapplicationprogram"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RdpBuilder struct {
|
||||||
|
Connection RdpConnection
|
||||||
|
Session RdpSession
|
||||||
|
DeviceRedirect RdpDeviceRedirect
|
||||||
|
Display RdpDisplay
|
||||||
|
RemoteApp RdpRemoteApp
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRdp() *RdpBuilder {
|
||||||
|
c := RdpConnection{}
|
||||||
|
s := RdpSession{}
|
||||||
|
dr := RdpDeviceRedirect{}
|
||||||
|
disp := RdpDisplay{}
|
||||||
|
ra := RdpRemoteApp{}
|
||||||
|
|
||||||
|
initStruct(&c)
|
||||||
|
initStruct(&s)
|
||||||
|
initStruct(&dr)
|
||||||
|
initStruct(&disp)
|
||||||
|
initStruct(&ra)
|
||||||
|
|
||||||
|
return &RdpBuilder{
|
||||||
|
Connection: c,
|
||||||
|
Session: s,
|
||||||
|
DeviceRedirect: dr,
|
||||||
|
Display: disp,
|
||||||
|
RemoteApp: ra,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rb *RdpBuilder) String() string {
|
||||||
|
var sb strings.Builder
|
||||||
|
|
||||||
|
addStructToString(rb.Connection, &sb)
|
||||||
|
addStructToString(rb.Session, &sb)
|
||||||
|
addStructToString(rb.DeviceRedirect, &sb)
|
||||||
|
addStructToString(rb.Display, &sb)
|
||||||
|
addStructToString(rb.RemoteApp, &sb)
|
||||||
|
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func addStructToString(st interface{}, sb *strings.Builder) {
|
||||||
|
s := structs.New(st)
|
||||||
|
for _, f := range s.Fields() {
|
||||||
|
if isZero(f) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
sb.WriteString(f.Tag("rdp"))
|
||||||
|
sb.WriteString(":")
|
||||||
|
|
||||||
|
switch f.Kind() {
|
||||||
|
case reflect.String:
|
||||||
|
sb.WriteString("s:")
|
||||||
|
sb.WriteString(f.Value().(string))
|
||||||
|
case reflect.Int:
|
||||||
|
sb.WriteString("i:")
|
||||||
|
fmt.Fprintf(sb, "%d", f.Value())
|
||||||
|
case reflect.Bool:
|
||||||
|
sb.WriteString("i:")
|
||||||
|
if f.Value().(bool) {
|
||||||
|
sb.WriteString("1")
|
||||||
|
} else {
|
||||||
|
sb.WriteString("0")
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sb.WriteString(crlf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isZero(f *structs.Field) bool {
|
||||||
|
t := f.Tag("default")
|
||||||
|
if t == "" {
|
||||||
|
return f.IsZero()
|
||||||
|
}
|
||||||
|
|
||||||
|
switch f.Kind() {
|
||||||
|
case reflect.String:
|
||||||
|
if f.Value().(string) != t {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
case reflect.Int:
|
||||||
|
i, err := strconv.Atoi(t)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("runtime error: default %s is not an integer", t)
|
||||||
|
}
|
||||||
|
if f.Value().(int) != i {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
case reflect.Bool:
|
||||||
|
b := false
|
||||||
|
if t == "true" || t == "1" {
|
||||||
|
b = true
|
||||||
|
}
|
||||||
|
if f.Value().(bool) != b {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return f.IsZero()
|
||||||
|
}
|
||||||
|
|
||||||
|
func initStruct(st interface{}) {
|
||||||
|
s := structs.New(st)
|
||||||
|
for _, f := range s.Fields() {
|
||||||
|
t := f.Tag("default")
|
||||||
|
if t == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch f.Kind() {
|
||||||
|
case reflect.String:
|
||||||
|
f.Set(t)
|
||||||
|
case reflect.Int:
|
||||||
|
i, err := strconv.Atoi(t)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("runtime error: default %s is not an integer", t)
|
||||||
|
}
|
||||||
|
f.Set(i)
|
||||||
|
case reflect.Bool:
|
||||||
|
b := false
|
||||||
|
if t == "true" || t == "1" {
|
||||||
|
b = true
|
||||||
|
}
|
||||||
|
err := f.Set(b)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Cannot set bool field")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
40
cmd/rdpgw/web/rdp_test.go
Normal file
40
cmd/rdpgw/web/rdp_test.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
GatewayHostName = "my.yahoo.com"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRdpBuilder(t *testing.T) {
|
||||||
|
builder := NewRdp()
|
||||||
|
builder.Connection.GatewayHostname = "my.yahoo.com"
|
||||||
|
builder.Session.AutoReconnectionEnabled = true
|
||||||
|
builder.Display.SmartSizing = true
|
||||||
|
|
||||||
|
s := builder.String()
|
||||||
|
if !strings.Contains(s, "gatewayhostname:s:"+GatewayHostName+crlf) {
|
||||||
|
t.Fatalf("%s does not contain `gatewayhostname:s:%s", s, GatewayHostName)
|
||||||
|
}
|
||||||
|
if strings.Contains(s, "autoreconnectionenabled") {
|
||||||
|
t.Fatalf("autoreconnectionenabled is in %s, but is default value", s)
|
||||||
|
}
|
||||||
|
if !strings.Contains(s, "smart sizing:i:1"+crlf) {
|
||||||
|
t.Fatalf("%s does not contain smart sizing:i:1", s)
|
||||||
|
|
||||||
|
}
|
||||||
|
log.Printf(builder.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInitStruct(t *testing.T) {
|
||||||
|
conn := RdpConnection{}
|
||||||
|
initStruct(&conn)
|
||||||
|
|
||||||
|
if conn.PromptCredentialsOnce != true {
|
||||||
|
t.Fatalf("conn.PromptCredentialsOnce != true")
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,15 +2,12 @@ package web
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/identity"
|
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/identity"
|
||||||
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/rdp"
|
|
||||||
"hash/maphash"
|
|
||||||
"log"
|
"log"
|
||||||
rnd "math/rand"
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
@ -31,13 +28,15 @@ type Config struct {
|
|||||||
HostSelection string
|
HostSelection string
|
||||||
GatewayAddress *url.URL
|
GatewayAddress *url.URL
|
||||||
RdpOpts RdpOpts
|
RdpOpts RdpOpts
|
||||||
TemplateFile string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type RdpOpts struct {
|
type RdpOpts struct {
|
||||||
UsernameTemplate string
|
UsernameTemplate string
|
||||||
SplitUserDomain bool
|
SplitUserDomain bool
|
||||||
NoUsername bool
|
DefaultDomain string
|
||||||
|
NetworkAutoDetect int
|
||||||
|
BandwidthAutoDetect int
|
||||||
|
ConnectionType int
|
||||||
}
|
}
|
||||||
|
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
@ -50,14 +49,12 @@ type Handler struct {
|
|||||||
hosts []string
|
hosts []string
|
||||||
hostSelection string
|
hostSelection string
|
||||||
rdpOpts RdpOpts
|
rdpOpts RdpOpts
|
||||||
rdpDefaults string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) NewHandler() *Handler {
|
func (c *Config) NewHandler() *Handler {
|
||||||
if len(c.Hosts) < 1 {
|
if len(c.Hosts) < 1 {
|
||||||
log.Fatal("Not enough hosts to connect to specified")
|
log.Fatal("Not enough hosts to connect to specified")
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Handler{
|
return &Handler{
|
||||||
paaTokenGenerator: c.PAATokenGenerator,
|
paaTokenGenerator: c.PAATokenGenerator,
|
||||||
enableUserToken: c.EnableUserToken,
|
enableUserToken: c.EnableUserToken,
|
||||||
@ -68,13 +65,12 @@ func (c *Config) NewHandler() *Handler {
|
|||||||
hosts: c.Hosts,
|
hosts: c.Hosts,
|
||||||
hostSelection: c.HostSelection,
|
hostSelection: c.HostSelection,
|
||||||
rdpOpts: c.RdpOpts,
|
rdpOpts: c.RdpOpts,
|
||||||
rdpDefaults: c.TemplateFile,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) selectRandomHost() string {
|
func (h *Handler) selectRandomHost() string {
|
||||||
r := rnd.New(rnd.NewSource(int64(new(maphash.Hash).Sum64())))
|
rand.Seed(time.Now().Unix())
|
||||||
host := h.hosts[r.Intn(len(h.hosts))]
|
host := h.hosts[rand.Intn(len(h.hosts))]
|
||||||
return host
|
return host
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,7 +145,7 @@ func (h *Handler) HandleDownload(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// split the username into user and domain
|
// split the username into user and domain
|
||||||
var user = id.UserName()
|
var user = id.UserName()
|
||||||
var domain = ""
|
var domain = opts.DefaultDomain
|
||||||
if opts.SplitUserDomain {
|
if opts.SplitUserDomain {
|
||||||
creds := strings.SplitN(id.UserName(), "@", 2)
|
creds := strings.SplitN(id.UserName(), "@", 2)
|
||||||
user = creds[0]
|
user = creds[0]
|
||||||
@ -173,7 +169,6 @@ func (h *Handler) HandleDownload(w http.ResponseWriter, r *http.Request) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Cannot generate PAA token for user %s due to %s", user, err)
|
log.Printf("Cannot generate PAA token for user %s due to %s", user, err)
|
||||||
http.Error(w, errors.New("unable to generate gateway credentials").Error(), http.StatusInternalServerError)
|
http.Error(w, errors.New("unable to generate gateway credentials").Error(), http.StatusInternalServerError)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if h.enableUserToken {
|
if h.enableUserToken {
|
||||||
@ -181,48 +176,32 @@ func (h *Handler) HandleDownload(w http.ResponseWriter, r *http.Request) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Cannot generate token for user %s due to %s", user, err)
|
log.Printf("Cannot generate token for user %s due to %s", user, err)
|
||||||
http.Error(w, errors.New("unable to generate gateway credentials").Error(), http.StatusInternalServerError)
|
http.Error(w, errors.New("unable to generate gateway credentials").Error(), http.StatusInternalServerError)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
render = strings.Replace(render, "{{ token }}", userToken, 1)
|
render = strings.Replace(render, "{{ token }}", userToken, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// authenticated
|
// authenticated
|
||||||
seed := make([]byte, 16)
|
seed := make([]byte, 16)
|
||||||
_, err = rand.Read(seed)
|
rand.Read(seed)
|
||||||
if err != nil {
|
|
||||||
log.Printf("Cannot generate random seed due to %s", err)
|
|
||||||
http.Error(w, errors.New("unable to generate random sequence").Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fn := hex.EncodeToString(seed) + ".rdp"
|
fn := hex.EncodeToString(seed) + ".rdp"
|
||||||
|
|
||||||
w.Header().Set("Content-Disposition", "attachment; filename="+fn)
|
w.Header().Set("Content-Disposition", "attachment; filename="+fn)
|
||||||
w.Header().Set("Content-Type", "application/x-rdp")
|
w.Header().Set("Content-Type", "application/x-rdp")
|
||||||
|
|
||||||
var d *rdp.Builder
|
rdp := NewRdp()
|
||||||
if h.rdpDefaults == "" {
|
rdp.Connection.Username = render
|
||||||
d = rdp.NewBuilder()
|
rdp.Connection.Domain = domain
|
||||||
} else {
|
rdp.Connection.FullAddress = host
|
||||||
d, err = rdp.NewBuilderFromFile(h.rdpDefaults)
|
rdp.Connection.GatewayHostname = h.gatewayAddress.Host
|
||||||
if err != nil {
|
rdp.Connection.GatewayCredentialsSource = SourceCookie
|
||||||
log.Printf("Cannot load RDP template file %s due to %s", h.rdpDefaults, err)
|
rdp.Connection.GatewayAccessToken = token
|
||||||
http.Error(w, errors.New("unable to load RDP template").Error(), http.StatusInternalServerError)
|
rdp.Connection.GatewayCredentialMethod = 1
|
||||||
return
|
rdp.Connection.GatewayUsageMethod = 1
|
||||||
}
|
rdp.Session.NetworkAutodetect = opts.NetworkAutoDetect != 0
|
||||||
}
|
rdp.Session.BandwidthAutodetect = opts.BandwidthAutoDetect != 0
|
||||||
|
rdp.Session.ConnectionType = opts.ConnectionType
|
||||||
|
rdp.Display.SmartSizing = true
|
||||||
|
rdp.Display.BitmapCacheSize = 32000
|
||||||
|
|
||||||
if !h.rdpOpts.NoUsername {
|
http.ServeContent(w, r, fn, time.Now(), strings.NewReader(rdp.String()))
|
||||||
d.Settings.Username = render
|
|
||||||
if domain != "" {
|
|
||||||
d.Settings.Domain = domain
|
|
||||||
}
|
|
||||||
}
|
|
||||||
d.Settings.FullAddress = host
|
|
||||||
d.Settings.GatewayHostname = h.gatewayAddress.Host
|
|
||||||
d.Settings.GatewayCredentialsSource = rdp.SourceCookie
|
|
||||||
d.Settings.GatewayAccessToken = token
|
|
||||||
d.Settings.GatewayCredentialMethod = 1
|
|
||||||
d.Settings.GatewayUsageMethod = 1
|
|
||||||
|
|
||||||
http.ServeContent(w, r, fn, time.Now(), strings.NewReader(d.String()))
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,12 +3,10 @@ package web
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/identity"
|
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/identity"
|
||||||
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/rdp"
|
|
||||||
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/security"
|
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/security"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@ -151,7 +149,7 @@ func TestHandler_HandleDownload(t *testing.T) {
|
|||||||
t.Errorf("content disposition is nil")
|
t.Errorf("content disposition is nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
data := rdpToMap(strings.Split(rr.Body.String(), rdp.CRLF))
|
data := rdpToMap(strings.Split(rr.Body.String(), crlf))
|
||||||
if data["username"] != testuser {
|
if data["username"] != testuser {
|
||||||
t.Errorf("username key in rdp does not match: got %v want %v", data["username"], testuser)
|
t.Errorf("username key in rdp does not match: got %v want %v", data["username"], testuser)
|
||||||
}
|
}
|
||||||
@ -172,51 +170,6 @@ func TestHandler_HandleDownload(t *testing.T) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHandler_HandleDownloadWithRdpTemplate(t *testing.T) {
|
|
||||||
f, err := os.CreateTemp("", "rdp")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer os.Remove(f.Name())
|
|
||||||
|
|
||||||
err = os.WriteFile(f.Name(), []byte("domain:s:testdomain\r\n"), 0644)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", "/connect", nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
rr := httptest.NewRecorder()
|
|
||||||
id := identity.NewUser()
|
|
||||||
|
|
||||||
id.SetUserName(testuser)
|
|
||||||
id.SetAuthenticated(true)
|
|
||||||
|
|
||||||
req = identity.AddToRequestCtx(id, req)
|
|
||||||
|
|
||||||
u, _ := url.Parse(gateway)
|
|
||||||
c := Config{
|
|
||||||
HostSelection: "roundrobin",
|
|
||||||
Hosts: hosts,
|
|
||||||
PAATokenGenerator: paaTokenMock,
|
|
||||||
GatewayAddress: u,
|
|
||||||
RdpOpts: RdpOpts{SplitUserDomain: true},
|
|
||||||
TemplateFile: f.Name(),
|
|
||||||
}
|
|
||||||
h := c.NewHandler()
|
|
||||||
|
|
||||||
hh := http.HandlerFunc(h.HandleDownload)
|
|
||||||
hh.ServeHTTP(rr, req)
|
|
||||||
|
|
||||||
data := rdpToMap(strings.Split(rr.Body.String(), rdp.CRLF))
|
|
||||||
if data["domain"] != "testdomain" {
|
|
||||||
t.Errorf("domain key in rdp does not match: got %v want %v", data["domain"], "testdomain")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func paaTokenMock(ctx context.Context, username string, host string) (string, error) {
|
func paaTokenMock(ctx context.Context, username string, host string) (string, error) {
|
||||||
return username + "_" + host, nil
|
return username + "_" + host, nil
|
||||||
}
|
}
|
||||||
|
|||||||
11
debian/changelog
vendored
11
debian/changelog
vendored
@ -1,11 +0,0 @@
|
|||||||
rdpgw (2.0.3) UNRELEASED; urgency=medium
|
|
||||||
|
|
||||||
* add Dynamic Gateway
|
|
||||||
|
|
||||||
-- Lierfang Support Team <itsupport@lierfang.com> Fri, 13 Jun 2025 15:56:06 +0800
|
|
||||||
|
|
||||||
rdpgw (2.0.2) UNSTABLE; urgency=medium
|
|
||||||
|
|
||||||
* init
|
|
||||||
|
|
||||||
-- Jiangcuo <Jiangcuo@lierfang.com> Mon, 03 Feb 2025 13:56:49 +0800
|
|
||||||
21
debian/control
vendored
21
debian/control
vendored
@ -1,21 +0,0 @@
|
|||||||
Source: rdpgw
|
|
||||||
Section: admin
|
|
||||||
Priority: optional
|
|
||||||
Maintainer: Lierfang <it_support@lierfang.com>
|
|
||||||
Homepage: https://github.com/bolkedebruin/rdpgw
|
|
||||||
Build-Depends: debhelper-compat (= 12),
|
|
||||||
golang ( >= 1.23.5-1~bpo12+1 ),
|
|
||||||
build-essential,
|
|
||||||
libpam0g-dev,
|
|
||||||
dh-golang
|
|
||||||
|
|
||||||
Package: rdpgw
|
|
||||||
Architecture: any
|
|
||||||
Depends: dbus,
|
|
||||||
openssl,
|
|
||||||
${misc:Depends},
|
|
||||||
${shlibs:Depends},
|
|
||||||
Description: rdpgw
|
|
||||||
RDPGW is an implementation of the Remote Desktop Gateway protocol.
|
|
||||||
This allows you to connect with the official Microsoft clients to remote desktops over HTTPS.
|
|
||||||
These desktops could be, for example, XRDP desktops running in containers on Kubernetes.
|
|
||||||
11
debian/copyright
vendored
11
debian/copyright
vendored
@ -1,11 +0,0 @@
|
|||||||
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
|
||||||
Source: https://github.com/bolkedebruin/rdpgw
|
|
||||||
|
|
||||||
|
|
||||||
Files: *
|
|
||||||
Copyright: bolkedebruin
|
|
||||||
License: Apache-2.0
|
|
||||||
|
|
||||||
Files: debian/*
|
|
||||||
Copyright: Lierfang <service@lierfang.com>
|
|
||||||
License: Apache-2.0
|
|
||||||
5
debian/install
vendored
5
debian/install
vendored
@ -1,5 +0,0 @@
|
|||||||
bin/rdpgw usr/bin/
|
|
||||||
bin/rdpgw-auth usr/sbin/
|
|
||||||
debian/rdpgw.yaml etc/rdpgw/
|
|
||||||
debian/rdpgw-auth.yaml etc/rdpgw/
|
|
||||||
debian/rdpgw-auth.service lib/systemd/system/
|
|
||||||
21
debian/postinst
vendored
21
debian/postinst
vendored
@ -1,21 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
#DEBHELPER#
|
|
||||||
|
|
||||||
case "$1" in
|
|
||||||
configure)
|
|
||||||
if [ ! -f "/etc/rdpgw/server.pem" ]; then
|
|
||||||
random=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1)
|
|
||||||
openssl genrsa -des3 -passout pass:$random -out /tmp/server.pass.key 2048
|
|
||||||
openssl rsa -passin pass:$random -in /tmp/server.pass.key -out /etc/rdpgw/key.pem
|
|
||||||
rm /tmp/server.pass.key
|
|
||||||
openssl req -new -sha256 -key /etc/rdpgw/key.pem -out /etc/rdpgw/server.csr -subj "/C=US/ST=VA/L=SomeCity/O=MyCompany/OU=MyDivision/CN=rdpgw"
|
|
||||||
openssl x509 -req -days 365 -in /etc/rdpgw/server.csr -signkey /etc/rdpgw/key.pem -out /etc/rdpgw/server.pem
|
|
||||||
fi
|
|
||||||
deb-systemd-invoke reload-or-try-restart rdpgw-auth.service || true
|
|
||||||
deb-systemd-invoke reload-or-try-restart rdpgw.service || true
|
|
||||||
;;
|
|
||||||
|
|
||||||
esac
|
|
||||||
16
debian/rdpgw-auth.service
vendored
16
debian/rdpgw-auth.service
vendored
@ -1,16 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=RDP Gateway Auth Service
|
|
||||||
After=network.target
|
|
||||||
StartLimitBurst=5
|
|
||||||
StartLimitInterval=10s
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
User=root
|
|
||||||
ExecStart=/usr/sbin/rdpgw-auth -c /etc/rdpgw/rdpgw-auth.yaml -s /run/rdpgw-auth.sock
|
|
||||||
Restart=on-failure
|
|
||||||
Restart=on-failure
|
|
||||||
RestartSec=5
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
6
debian/rdpgw-auth.yaml
vendored
6
debian/rdpgw-auth.yaml
vendored
@ -1,6 +0,0 @@
|
|||||||
PXVDI:
|
|
||||||
Enabled: true
|
|
||||||
apiUrl: "https://10.13.16.164:3002"
|
|
||||||
apiKey: "dasdasdasdas"
|
|
||||||
Users:
|
|
||||||
- {Username: "debian-rdpgw-start", Password: "debian-rdpgw-password"}
|
|
||||||
16
debian/rdpgw.service
vendored
16
debian/rdpgw.service
vendored
@ -1,16 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=RDP Gateway Service
|
|
||||||
After=network.target
|
|
||||||
StartLimitBurst=5
|
|
||||||
StartLimitInterval=10s
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
User=root
|
|
||||||
ExecStart=/usr/bin/rdpgw -c /etc/rdpgw/rdpgw.yaml
|
|
||||||
Restart=on-failure
|
|
||||||
Restart=on-failure
|
|
||||||
RestartSec=5
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
30
debian/rdpgw.yaml
vendored
30
debian/rdpgw.yaml
vendored
@ -1,30 +0,0 @@
|
|||||||
Server:
|
|
||||||
Authentication:
|
|
||||||
- ntlm
|
|
||||||
BasicAuthTimeout: 5
|
|
||||||
AuthSocket: /run/rdpgw-auth.sock
|
|
||||||
GatewayAddress: localhost
|
|
||||||
Port: 443
|
|
||||||
Hosts:
|
|
||||||
- localhost:3389
|
|
||||||
HostSelection: any
|
|
||||||
Tls: enable
|
|
||||||
CertFile: /etc/rdpgw/server.pem
|
|
||||||
KeyFile: /etc/rdpgw/key.pem
|
|
||||||
Caps:
|
|
||||||
SmartCardAuth: false
|
|
||||||
TokenAuth: false
|
|
||||||
IdleTimeout: 10
|
|
||||||
EnablePrinter: true
|
|
||||||
EnablePort: true
|
|
||||||
EnablePnp: true
|
|
||||||
EnableDrive: true
|
|
||||||
EnableClipboard: true
|
|
||||||
Client:
|
|
||||||
UsernameTemplate: "{{ username }}"
|
|
||||||
SplitUserDomain: false
|
|
||||||
Security:
|
|
||||||
PAATokenSigningKey: thisisasessionkeyreplacethisjetzt
|
|
||||||
UserTokenEncryptionKey: thisisasessionkeyreplacethisjetzt
|
|
||||||
EnableUserToken: false
|
|
||||||
VerifyClientIp: trufalsee
|
|
||||||
19
debian/rules
vendored
19
debian/rules
vendored
@ -1,19 +0,0 @@
|
|||||||
#!/usr/bin/make -f
|
|
||||||
|
|
||||||
# Uncomment this to turn on verbose mode.
|
|
||||||
#export DH_VERBOSE=1
|
|
||||||
|
|
||||||
include /usr/share/dpkg/pkg-info.mk
|
|
||||||
|
|
||||||
%:
|
|
||||||
dh $@
|
|
||||||
|
|
||||||
override_dh_auto_build:
|
|
||||||
|
|
||||||
override_dh_auto_test:
|
|
||||||
|
|
||||||
override_dh_auto_install:
|
|
||||||
|
|
||||||
override_dh_auto_clean:
|
|
||||||
|
|
||||||
override_dh_dwz:
|
|
||||||
@ -1,8 +1,7 @@
|
|||||||
# builder stage
|
# builder stage
|
||||||
FROM golang:1.22-alpine as builder
|
FROM golang as builder
|
||||||
|
|
||||||
#RUN apt-get update && apt-get install -y libpam-dev
|
RUN apt-get update && apt-get install -y libpam-dev
|
||||||
RUN apk --no-cache add git gcc musl-dev linux-pam-dev openssl
|
|
||||||
|
|
||||||
# add user
|
# add user
|
||||||
RUN adduser --disabled-password --gecos "" --home /opt/rdpgw --uid 1001 rdpgw
|
RUN adduser --disabled-password --gecos "" --home /opt/rdpgw --uid 1001 rdpgw
|
||||||
@ -28,22 +27,18 @@ RUN git clone https://github.com/bolkedebruin/rdpgw.git /app && \
|
|||||||
chmod +x /opt/rdpgw/rdpgw-auth && \
|
chmod +x /opt/rdpgw/rdpgw-auth && \
|
||||||
chmod u+s /opt/rdpgw/rdpgw-auth
|
chmod u+s /opt/rdpgw/rdpgw-auth
|
||||||
|
|
||||||
FROM alpine:latest
|
FROM scratch
|
||||||
|
|
||||||
RUN apk --no-cache add linux-pam musl
|
|
||||||
|
|
||||||
# make tempdir in case filestore is used
|
# make tempdir in case filestore is used
|
||||||
ADD tmp.tar /
|
ADD tmp.tar /
|
||||||
|
|
||||||
COPY --chown=0 rdpgw-pam /etc/pam.d/rdpgw
|
|
||||||
|
|
||||||
USER 1001
|
USER 1001
|
||||||
COPY --chown=1001 run.sh run.sh
|
|
||||||
COPY --chown=1001 --from=builder /opt/rdpgw /opt/rdpgw
|
COPY --chown=1001 --from=builder /opt/rdpgw /opt/rdpgw
|
||||||
COPY --chown=1001 --from=builder /etc/passwd /etc/passwd
|
COPY --chown=1001 --from=builder /etc/passwd /etc/passwd
|
||||||
COPY --chown=1001 --from=builder /etc/ssl/certs /etc/ssl/certs
|
COPY --chown=1001 --from=builder /etc/ssl/certs /etc/ssl/certs
|
||||||
|
|
||||||
USER 0
|
COPY --chown=1001 rdpgw.yaml /opt/rdpgw/rdpgw.yaml
|
||||||
|
|
||||||
WORKDIR /opt/rdpgw
|
WORKDIR /opt/rdpgw
|
||||||
ENTRYPOINT ["/bin/sh", "/run.sh"]
|
ENTRYPOINT ["/opt/rdpgw/rdpgw"]
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
FROM rattydave/docker-ubuntu-xrdp-mate-custom:latest
|
FROM rattydave/docker-ubuntu-xrdp-mate-custom:20.04
|
||||||
|
|
||||||
RUN cd /etc/xrdp/ && \
|
RUN cd /etc/xrdp/ && \
|
||||||
openssl req -x509 -newkey rsa:2048 -nodes -keyout key.pem -out cert.pem -days 3650 \
|
openssl req -x509 -newkey rsa:2048 -nodes -keyout key.pem -out cert.pem -days 3650 \
|
||||||
|
|||||||
@ -21,14 +21,14 @@ services:
|
|||||||
restart: on-failure
|
restart: on-failure
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:8080/auth"]
|
test: ["CMD", "curl", "-f", "http://localhost:8080/auth"]
|
||||||
interval: 10s
|
interval: 30s
|
||||||
timeout: 3s
|
timeout: 3s
|
||||||
retries: 10
|
retries: 10
|
||||||
start_period: 5s
|
start_period: 5s
|
||||||
xrdp:
|
xrdp:
|
||||||
container_name: xrdp
|
container_name: xrdp
|
||||||
hostname: xrdp
|
hostname: xrdp
|
||||||
image: bolkedebruin/docker-ubuntu-xrdp-mate-rdpgw:latest
|
image: bolkedebruin/docker-ubuntu-xrdp-mate-rdpgw:20.04
|
||||||
ports:
|
ports:
|
||||||
- 3389:3389
|
- 3389:3389
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
@ -38,30 +38,16 @@ services:
|
|||||||
TZ: "Europe/Amsterdam"
|
TZ: "Europe/Amsterdam"
|
||||||
rdpgw:
|
rdpgw:
|
||||||
container_name: rdpgw
|
container_name: rdpgw
|
||||||
hostname: rdpgw
|
|
||||||
image: bolkedebruin/rdpgw:latest
|
|
||||||
build: .
|
build: .
|
||||||
ports:
|
ports:
|
||||||
- 9443:9443
|
- 9443:9443
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
depends_on:
|
depends_on:
|
||||||
keycloak:
|
- keycloak
|
||||||
condition: service_healthy
|
|
||||||
environment:
|
environment:
|
||||||
RDPGW_SERVER__SESSION_STORE: file
|
RDPGW_SERVER__SESSION_STORE: file
|
||||||
RDPGW_SERVER__CERT_FILE: /opt/rdpgw/server.pem
|
|
||||||
RDPGW_SERVER__KEY_FILE: /opt/rdpgw/key.pem
|
|
||||||
RDPGW_SERVER__GATEWAY_ADDRESS: localhost:9443
|
|
||||||
RDPGW_SERVER__PORT: 9443
|
|
||||||
RDPGW_SERVER__HOSTS: xrdp:3389
|
|
||||||
RDPGW_SERVER__ROUND_ROBIN: "false"
|
|
||||||
RDPGW_OPEN_ID__PROVIDER_URL: "http://keycloak:8080/auth/realms/rdpgw"
|
|
||||||
RDPGW_OPEN_ID__CLIENT_ID: rdpgw
|
|
||||||
RDPGW_OPEN_ID__CLIENT_SECRET: 01cd304c-6f43-4480-9479-618eb6fd578f
|
|
||||||
RDPGW_CLIENT__USERNAME_TEMPLATE: "{{ username }}"
|
|
||||||
RDPGW_CAPS__TOKEN_AUTH: "true"
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://keycloak:8080"]
|
test: ["CMD", "curl", "-f", "http://keycloak:8080"]
|
||||||
interval: 10s
|
interval: 30s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 10
|
retries: 10
|
||||||
|
|||||||
@ -1,39 +0,0 @@
|
|||||||
version: '3.4'
|
|
||||||
|
|
||||||
services:
|
|
||||||
xrdp:
|
|
||||||
container_name: xrdp
|
|
||||||
hostname: xrdp
|
|
||||||
image: bolkedebruin/docker-ubuntu-xrdp-mate-rdpgw:latest
|
|
||||||
ports:
|
|
||||||
- 3389:3389
|
|
||||||
restart: on-failure
|
|
||||||
volumes:
|
|
||||||
- ${PWD}/xrdp_users.txt:/root/createusers.txt
|
|
||||||
environment:
|
|
||||||
TZ: "Europe/Amsterdam"
|
|
||||||
rdpgw:
|
|
||||||
container_name: rdpgw
|
|
||||||
hostname: rdpgw
|
|
||||||
image: bolkedebruin/rdpgw:latest
|
|
||||||
build: .
|
|
||||||
ports:
|
|
||||||
- 9443:9443
|
|
||||||
restart: on-failure
|
|
||||||
volumes:
|
|
||||||
- ${PWD}/xrdp_users.txt:/root/createusers.txt
|
|
||||||
environment:
|
|
||||||
RDPGW_SERVER__SESSION_STORE: file
|
|
||||||
RDPGW_SERVER__CERT_FILE: /opt/rdpgw/server.pem
|
|
||||||
RDPGW_SERVER__KEY_FILE: /opt/rdpgw/key.pem
|
|
||||||
RDPGW_SERVER__GATEWAY_ADDRESS: localhost:9443
|
|
||||||
RDPGW_SERVER__PORT: 9443
|
|
||||||
RDPGW_SERVER__HOSTS: xrdp:3389
|
|
||||||
RDPGW_SERVER__ROUND_ROBIN: "false"
|
|
||||||
RDPGW_SERVER__AUTHENTICATION: local
|
|
||||||
RDPGW_CAPS__TOKEN_AUTH: "false"
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:9443/"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 10
|
|
||||||
@ -21,19 +21,17 @@ services:
|
|||||||
- 8080:8080
|
- 8080:8080
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
command:
|
command:
|
||||||
- start-dev
|
- start-dev --import-realm --http-relative-path=/auth
|
||||||
- --import-realm
|
|
||||||
- --http-relative-path=/auth
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:8080/auth"]
|
test: ["CMD", "curl", "-f", "http://localhost:8080/auth"]
|
||||||
interval: 10s
|
interval: 30s
|
||||||
timeout: 3s
|
timeout: 3s
|
||||||
retries: 10
|
retries: 10
|
||||||
start_period: 5s
|
start_period: 5s
|
||||||
xrdp:
|
xrdp:
|
||||||
container_name: xrdp
|
container_name: xrdp
|
||||||
hostname: xrdp
|
hostname: xrdp
|
||||||
image: bolkedebruin/docker-ubuntu-xrdp-mate-rdpgw:latest
|
image: bolkedebruin/docker-ubuntu-xrdp-mate-rdpgw:20.04
|
||||||
ports:
|
ports:
|
||||||
- 3389:3389
|
- 3389:3389
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
@ -47,23 +45,9 @@ services:
|
|||||||
- 9443:9443
|
- 9443:9443
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
depends_on:
|
depends_on:
|
||||||
keycloak:
|
- keycloak
|
||||||
condition: service_healthy
|
|
||||||
environment:
|
|
||||||
RDPGW_SERVER__SESSION_STORE: file
|
|
||||||
RDPGW_SERVER__CERT_FILE: /opt/rdpgw/server.pem
|
|
||||||
RDPGW_SERVER__KEY_FILE: /opt/rdpgw/key.pem
|
|
||||||
RDPGW_SERVER__GATEWAY_ADDRESS: localhost:9443
|
|
||||||
RDPGW_SERVER__PORT: 9443
|
|
||||||
RDPGW_SERVER__HOSTS: xrdp:3389
|
|
||||||
RDPGW_SERVER__ROUND_ROBIN: "false"
|
|
||||||
RDPGW_OPEN_ID__PROVIDER_URL: "http://keycloak:8080/auth/realms/rdpgw"
|
|
||||||
RDPGW_OPEN_ID__CLIENT_ID: rdpgw
|
|
||||||
RDPGW_OPEN_ID__CLIENT_SECRET: 01cd304c-6f43-4480-9479-618eb6fd578f
|
|
||||||
RDPGW_CLIENT__USERNAME_TEMPLATE: "{{ username }}"
|
|
||||||
RDPGW_CAPS__TOKEN_AUTH: "true"
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://keycloak:8080"]
|
test: ["CMD", "curl", "-f", "http://keycloak:8080"]
|
||||||
interval: 10s
|
interval: 30s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 10
|
retries: 10
|
||||||
|
|||||||
@ -22,14 +22,14 @@ across the different instances if this is not what you want.
|
|||||||
## Configuration through environment variables
|
## Configuration through environment variables
|
||||||
```bash
|
```bash
|
||||||
docker --run name rdpgw bolkedebruin/rdpgw:latest \
|
docker --run name rdpgw bolkedebruin/rdpgw:latest \
|
||||||
-e RDPGW_SERVER__CERT_FILE=/etc/rdpgw/cert.pem
|
-e RDPGW_SERVER__SSL_CERT_FILE=/etc/rdpgw/cert.pem
|
||||||
-e RDPGW_SERVER__KEY_FILE=/etc/rdpgw.cert.pem
|
-e RDPGW_SERVER__SSL_KEY_FILE=/etc/rdpgw.cert.pem
|
||||||
-e RDPGW_SERVER__GATEWAY_ADDRESS=https://localhost:443
|
-e RDPGW_SERVER__GATEWAY_ADDRESS=https://localhost:443
|
||||||
-e RDPGW_SERVER__SESSION_KEY=thisisasessionkeyreplacethisjetz # 32 characters
|
-e RDPGW_SERVER__SESSION_KEY=thisisasessionkeyreplacethisjetz # 32 characters
|
||||||
-e RDPGW_SERVER__SESSION_ENCRYPTION_KEY=thisisasessionkeyreplacethisnunu # 32 characters
|
-e RDPGW_SERVER__SESSION_ENCRYPTION_KEY=thisisasessionkeyreplacethisnunu # 32 characters
|
||||||
-e RDPGW_OPEN_ID__PROVIDER_URL=http://keycloak:8080/auth/realms/rdpgw
|
-e RDPGW_OPENID__PROVIDER_URL=http://keycloak:8080/auth/realms/rdpgw
|
||||||
-e RDPGW_OPEN_ID__CLIENT_ID=rdpgw
|
-e RDPGW_OPENID__CLIENT_ID=rdpgw
|
||||||
-e RDPGW_OPEN_ID__CLIENT_SECRET=01cd304c-6f43-4480-9479-618eb6fd578f
|
-e RDPGW_OPENID__CLIENT_SECRET=01cd304c-6f43-4480-9479-618eb6fd578f
|
||||||
-e RDPGW_SECURITY__SECURITY_PAA_TOKEN_SIGNING_KEY=prettypleasereplacemeinproductio # 32 characters
|
-e RDPGW_SECURITY__SECURITY_PAA_TOKEN_SIGNING_KEY=prettypleasereplacemeinproductio # 32 characters
|
||||||
-v conf:/etc/rdpgw
|
-v conf:/etc/rdpgw
|
||||||
```
|
```
|
||||||
@ -1,3 +0,0 @@
|
|||||||
# basic PAM configuration for rdpgw on Alpine
|
|
||||||
auth include base-auth
|
|
||||||
auth include base-account
|
|
||||||
@ -14,6 +14,9 @@ OpenId:
|
|||||||
ClientSecret: 01cd304c-6f43-4480-9479-618eb6fd578f
|
ClientSecret: 01cd304c-6f43-4480-9479-618eb6fd578f
|
||||||
Client:
|
Client:
|
||||||
UsernameTemplate: "{{ username }}"
|
UsernameTemplate: "{{ username }}"
|
||||||
|
NetworkAutoDetect: 0
|
||||||
|
BandwidthAutoDetect: 1
|
||||||
|
ConnectionType: 6
|
||||||
Security:
|
Security:
|
||||||
PAATokenSigningKey: prettypleasereplacemeinproductio
|
PAATokenSigningKey: prettypleasereplacemeinproductio
|
||||||
Caps:
|
Caps:
|
||||||
|
|||||||
@ -1,34 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
USER=rdpgw
|
|
||||||
|
|
||||||
file="/root/createusers.txt"
|
|
||||||
if [ -f $file ]
|
|
||||||
then
|
|
||||||
while IFS=: read -r username password is_sudo
|
|
||||||
do
|
|
||||||
echo "Username: $username, Password: **** , Sudo: $is_sudo"
|
|
||||||
|
|
||||||
if getent passwd "$username" > /dev/null 2>&1
|
|
||||||
then
|
|
||||||
echo "User Exists"
|
|
||||||
else
|
|
||||||
adduser -s /sbin/nologin "$username"
|
|
||||||
echo "$username:$password" | chpasswd
|
|
||||||
fi
|
|
||||||
done <"$file"
|
|
||||||
fi
|
|
||||||
|
|
||||||
cd /opt/rdpgw || exit 1
|
|
||||||
|
|
||||||
if [ -n "${RDPGW_SERVER__AUTHENTICATION}" ]; then
|
|
||||||
if [ "${RDPGW_SERVER__AUTHENTICATION}" = "local" ]; then
|
|
||||||
echo "Starting rdpgw-auth"
|
|
||||||
/opt/rdpgw/rdpgw-auth &
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# drop privileges and run the application
|
|
||||||
su -c /opt/rdpgw/rdpgw "${USER}" -- "$@" &
|
|
||||||
wait
|
|
||||||
exit $?
|
|
||||||
@ -1,84 +0,0 @@
|
|||||||
# NTLM认证API集成
|
|
||||||
|
|
||||||
RDPGW支持通过API集成NTLM认证,允许使用您自己的用户管理系统进行认证。
|
|
||||||
|
|
||||||
## API模式
|
|
||||||
|
|
||||||
API支持两种模式:
|
|
||||||
|
|
||||||
1. **验证模式(verify)**:验证用户凭据是否有效
|
|
||||||
2. **密码获取模式(getpassword)**:获取用户的明文密码,用于NTLM挑战-响应计算
|
|
||||||
|
|
||||||
## API要求
|
|
||||||
|
|
||||||
### 1. 验证模式
|
|
||||||
|
|
||||||
用于验证用户凭据是否有效。
|
|
||||||
|
|
||||||
**请求格式**:
|
|
||||||
```
|
|
||||||
GET https://your-api-server/api/checkperm/?username=<用户名>&password=<密码>&mode=verify
|
|
||||||
```
|
|
||||||
|
|
||||||
**成功响应**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"status": "success"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 密码获取模式
|
|
||||||
|
|
||||||
用于获取用户的明文密码,这是NTLM认证所必需的。
|
|
||||||
|
|
||||||
**请求格式**:
|
|
||||||
```
|
|
||||||
GET https://your-api-server/api/checkperm/?username=<用户名>&mode=getpassword
|
|
||||||
```
|
|
||||||
|
|
||||||
**成功响应**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"status": "success",
|
|
||||||
"password": "用户的明文密码"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 配置
|
|
||||||
|
|
||||||
在`rdpgw-auth.yaml`中配置API认证:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
apiauth:
|
|
||||||
enabled: true
|
|
||||||
apiurl: "https://your-api-server/api/checkperm/"
|
|
||||||
```
|
|
||||||
|
|
||||||
## 安全考虑
|
|
||||||
|
|
||||||
1. **密码安全**:API必须通过HTTPS提供,并且在内部网络中运行以确保安全性
|
|
||||||
|
|
||||||
2. **密码存储**:您的API服务必须能够以某种形式获取或计算用户密码,这需要安全的密码存储机制
|
|
||||||
|
|
||||||
3. **替代方案**:如果不想暴露明文密码,请考虑使用其他认证方式,如OpenID Connect或基本认证
|
|
||||||
|
|
||||||
## NTLM认证流程
|
|
||||||
|
|
||||||
1. 客户端(如FreeRDP)发送NTLM协商消息到RDPGW
|
|
||||||
2. RDPGW生成挑战并发送回客户端
|
|
||||||
3. 客户端计算响应并发送认证消息
|
|
||||||
4. RDPGW调用API以`getpassword`模式获取用户密码
|
|
||||||
5. RDPGW使用获取的密码计算期望的响应并与客户端响应比较
|
|
||||||
6. 如果匹配,认证成功;否则,认证失败
|
|
||||||
|
|
||||||
## 优势
|
|
||||||
|
|
||||||
- 保持标准NTLM认证流程,客户端无需修改
|
|
||||||
- 与您的用户管理系统集成
|
|
||||||
- 支持所有NTLM客户端,包括标准Windows远程桌面客户端
|
|
||||||
|
|
||||||
## 注意事项
|
|
||||||
|
|
||||||
- 您的API必须能够安全地存储和提供用户密码
|
|
||||||
- 明文密码传输存在固有的安全风险,即使在加密通道中也是如此
|
|
||||||
- 确保API服务器仅对RDPGW服务器可访问,并考虑实施IP限制或类似措施
|
|
||||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 60 KiB |
@ -1,232 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
width="500"
|
|
||||||
height="350"
|
|
||||||
version="1.1"
|
|
||||||
id="svg12"
|
|
||||||
sodipodi:docname="flow-kerberos.svg"
|
|
||||||
xml:space="preserve"
|
|
||||||
inkscape:version="1.3.2 (091e20e, 2023-11-25)"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
|
||||||
id="defs12"><rect
|
|
||||||
x="170.78426"
|
|
||||||
y="222.01954"
|
|
||||||
width="83.594403"
|
|
||||||
height="14.381833"
|
|
||||||
id="rect25" /><rect
|
|
||||||
x="151.90811"
|
|
||||||
y="181.57064"
|
|
||||||
width="73.706893"
|
|
||||||
height="19.77502"
|
|
||||||
id="rect24" /><rect
|
|
||||||
x="161.79562"
|
|
||||||
y="151.00924"
|
|
||||||
width="124.94217"
|
|
||||||
height="19.77502"
|
|
||||||
id="rect23" /><rect
|
|
||||||
x="62.920519"
|
|
||||||
y="159.99789"
|
|
||||||
width="170.78426"
|
|
||||||
height="27.864801"
|
|
||||||
id="rect22" /><rect
|
|
||||||
x="154.6047"
|
|
||||||
y="70.111435"
|
|
||||||
width="114.1558"
|
|
||||||
height="14.381833"
|
|
||||||
id="rect20" /><rect
|
|
||||||
x="133.93082"
|
|
||||||
y="257.97412"
|
|
||||||
width="213.0309"
|
|
||||||
height="26.067072"
|
|
||||||
id="rect18" /><rect
|
|
||||||
x="346.96173"
|
|
||||||
y="155.50357"
|
|
||||||
width="102.47056"
|
|
||||||
height="28.763666"
|
|
||||||
id="rect17" /><rect
|
|
||||||
x="200.44679"
|
|
||||||
y="197.7502"
|
|
||||||
width="212.13203"
|
|
||||||
height="20.673885"
|
|
||||||
id="rect16" /><rect
|
|
||||||
x="81.796677"
|
|
||||||
y="164.4922"
|
|
||||||
width="157.3013"
|
|
||||||
height="16.179562"
|
|
||||||
id="rect15" /><rect
|
|
||||||
x="200.44679"
|
|
||||||
y="108.76261"
|
|
||||||
width="95.27964"
|
|
||||||
height="19.775021"
|
|
||||||
id="rect14" /><rect
|
|
||||||
x="200.44679"
|
|
||||||
y="197.7502"
|
|
||||||
width="212.13203"
|
|
||||||
height="20.673885"
|
|
||||||
id="rect16-2" /><rect
|
|
||||||
x="81.796677"
|
|
||||||
y="164.4922"
|
|
||||||
width="157.3013"
|
|
||||||
height="16.179562"
|
|
||||||
id="rect15-6" /></defs><sodipodi:namedview
|
|
||||||
id="namedview12"
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#000000"
|
|
||||||
borderopacity="0.25"
|
|
||||||
inkscape:showpageshadow="2"
|
|
||||||
inkscape:pageopacity="0.0"
|
|
||||||
inkscape:pagecheckerboard="0"
|
|
||||||
inkscape:deskcolor="#d1d1d1"
|
|
||||||
inkscape:zoom="1.1125147"
|
|
||||||
inkscape:cx="521.34143"
|
|
||||||
inkscape:cy="166.73937"
|
|
||||||
inkscape:window-width="2400"
|
|
||||||
inkscape:window-height="1274"
|
|
||||||
inkscape:window-x="0"
|
|
||||||
inkscape:window-y="25"
|
|
||||||
inkscape:window-maximized="0"
|
|
||||||
inkscape:current-layer="svg12" /><!-- Rectangles --><!-- Text --><text
|
|
||||||
x="61.016945"
|
|
||||||
y="43.05085"
|
|
||||||
font-family="Arial"
|
|
||||||
font-size="16px"
|
|
||||||
fill="#000000"
|
|
||||||
id="text5"><tspan
|
|
||||||
sodipodi:role="line"
|
|
||||||
id="tspan20"
|
|
||||||
x="61.016945"
|
|
||||||
y="43.05085">Kerberos</tspan><tspan
|
|
||||||
sodipodi:role="line"
|
|
||||||
x="61.016945"
|
|
||||||
y="63.05085"
|
|
||||||
id="tspan22" /></text><!-- Lines --><line
|
|
||||||
x1="134.4955"
|
|
||||||
y1="87.286629"
|
|
||||||
x2="288.42737"
|
|
||||||
y2="87.286629"
|
|
||||||
stroke="#000000"
|
|
||||||
stroke-width="2.48139"
|
|
||||||
id="line9" /><line
|
|
||||||
x1="132.88857"
|
|
||||||
y1="146.08278"
|
|
||||||
x2="286.8204"
|
|
||||||
y2="146.08278"
|
|
||||||
stroke="#000000"
|
|
||||||
stroke-width="2.48139"
|
|
||||||
id="line9-49" /><line
|
|
||||||
x1="132.17955"
|
|
||||||
y1="216.35213"
|
|
||||||
x2="286.11139"
|
|
||||||
y2="216.35213"
|
|
||||||
stroke="#000000"
|
|
||||||
stroke-width="2.48139"
|
|
||||||
id="line9-49-5" /><line
|
|
||||||
x1="134.74902"
|
|
||||||
y1="144.4328"
|
|
||||||
x2="287.49759"
|
|
||||||
y2="98.587257"
|
|
||||||
stroke="#000000"
|
|
||||||
stroke-width="2.37506"
|
|
||||||
id="line9-4" /><line
|
|
||||||
x1="134.23099"
|
|
||||||
y1="215.71254"
|
|
||||||
x2="286.97955"
|
|
||||||
y2="169.86702"
|
|
||||||
stroke="#000000"
|
|
||||||
stroke-width="2.37506"
|
|
||||||
id="line9-4-7" /><g
|
|
||||||
style="overflow:hidden;fill:currentColor"
|
|
||||||
id="g12"
|
|
||||||
transform="matrix(0.06600417,0,0,0.05178799,19.223463,60.951852)"><path
|
|
||||||
d="M 843.28296,870.11556 C 834.84444,729.6 738.98667,612.69333 609.37481,572.96593 687.88148,536.27259 742.4,456.53333 742.4,364.08889 c 0,-127.24148 -103.15852,-230.4 -230.4,-230.4 -127.24148,0 -230.4,103.15852 -230.4,230.4 0,92.44444 54.51852,172.1837 133.12,208.87704 C 285.10815,612.69333 189.25037,729.6 180.81185,870.11556 c -0.6637,10.9037 7.96445,20.19555 18.96297,20.19555 v 0 c 9.95555,0 18.29925,-7.77481 18.96296,-17.73037 C 227.74518,718.50667 355.65037,596.38518 512,596.38518 c 156.34963,0 284.25481,122.12149 293.35704,276.19556 0.56889,9.95556 8.91259,17.73037 18.96296,17.73037 10.99852,0 19.62667,-9.29185 18.96296,-20.19555 z M 319.52593,364.08889 c 0,-106.28741 86.18666,-192.47408 192.47407,-192.47408 106.28741,0 192.47407,86.18667 192.47407,192.47408 0,106.28741 -86.18666,192.47407 -192.47407,192.47407 -106.28741,0 -192.47407,-86.18666 -192.47407,-192.47407 z"
|
|
||||||
id="path1" /></g><g
|
|
||||||
style="overflow:hidden;fill:currentColor"
|
|
||||||
id="g14"
|
|
||||||
transform="matrix(0.04275091,0,0,0.04222869,292.71414,66.391967)"><path
|
|
||||||
d="M 665.6,509.952 H 347.648 c -12.8,0 -21.504,8.704 -21.504,21.504 v 204.8 c 0,12.8 8.704,21.504 21.504,21.504 h 315.904 c 12.8,0 21.504,-8.704 21.504,-21.504 v -204.8 c 2.048,-12.8 -8.704,-21.504 -19.456,-21.504 z M 533.504,661.504 c 0,0 0,2.048 0,0 0,12.8 -8.704,23.552 -21.504,23.552 -12.8,0 -21.504,-8.704 -21.504,-21.504 v -2.048 c -12.8,-6.144 -21.504,-21.504 -21.504,-36.352 0,-23.552 19.456,-42.496 42.496,-42.496 23.04,0 42.496,19.456 42.496,42.496 0.512,14.848 -7.68,29.696 -20.48,36.352 z"
|
|
||||||
fill="#ff6a00"
|
|
||||||
id="path1-3" /><path
|
|
||||||
d="M 981.504,492.544 C 970.752,243.2 763.904,44.544 512,44.544 c -251.904,0 -458.752,198.656 -469.504,448 v 31.744 C 48.64,778.24 256,983.04 512,983.04 c 256,0 462.848,-204.8 469.504,-458.752 z M 810.496,272.896 c -42.496,34.304 -91.648,51.2 -130.048,61.952 -23.552,-87.552 -64,-159.744 -108.544,-198.144 95.744,14.848 179.2,64 238.592,136.192 z M 452.096,136.704 C 409.6,175.104 369.152,247.296 345.6,332.8 307.2,322.048 260.096,305.152 217.6,270.848 275.456,198.656 358.4,151.552 452.096,136.704 Z M 825.344,733.696 C 808.448,718.848 786.944,706.048 765.44,693.248 735.744,678.4 720.384,708.096 748.544,720.896 768,729.6 786.944,742.4 805.888,757.248 743.936,832 656.384,881.152 556.032,891.904 c 21.504,-14.848 45.056,-40.448 64,-72.704 6.656,-16.896 -21.504,-27.648 -36.352,-2.048 -25.6,36.352 -51.2,57.344 -74.752,57.344 -21.504,0 -49.152,-21.504 -72.704,-55.296 -21.504,-31.744 -42.496,-14.848 -38.4,0 19.456,29.696 40.448,53.248 61.952,70.656 -98.304,-8.704 -183.296,-57.344 -243.2,-130.048 19.456,-14.848 38.4,-27.648 57.344,-36.352 27.648,-14.848 10.752,-42.496 -16.896,-27.648 -19.456,10.752 -40.448,23.552 -59.904,38.4 C 154.624,674.304 131.072,602.112 129.024,525.312 H 261.12 c 8.704,0 16.896,-6.656 16.896,-16.896 0,-10.24 -6.144,-16.896 -16.896,-16.896 H 130.048 c 4.096,-72.704 27.648,-140.8 68.096,-198.144 38.4,34.304 91.648,55.296 138.752,68.096 -4.096,21.504 -8.704,42.496 -10.752,66.048 0,19.456 32.256,19.456 32.256,0 16.896,-149.504 96.256,-283.648 153.6,-283.648 57.344,0 136.704,136.704 155.648,288.256 2.048,19.456 34.304,16.896 31.744,0 -2.048,-21.504 -6.656,-42.496 -10.752,-64 49.152,-12.8 102.4,-34.304 140.8,-68.096 38.4,55.296 61.952,123.904 66.048,194.048 H 763.392 c -8.704,0 -16.896,6.144 -16.896,16.896 0,10.752 6.144,16.896 16.896,16.896 H 896 c -2.048,75.776 -27.648,146.432 -70.656,205.824 z"
|
|
||||||
fill="#ff6a00"
|
|
||||||
id="path2-2" /><path
|
|
||||||
d="m 512,317.952 c -59.904,0 -106.496,47.104 -106.496,106.496 v 31.744 H 448 v -31.744 c 0,-34.304 27.648,-64 64,-64 36.352,0 64,27.648 64,64 v 149.504 h 42.496 V 424.448 C 618.496,364.544 571.904,317.952 512,317.952 Z"
|
|
||||||
fill="#ff6a00"
|
|
||||||
id="path3-6" /></g><g
|
|
||||||
style="overflow:hidden;fill:currentColor"
|
|
||||||
id="g14-8"
|
|
||||||
transform="matrix(0.04275091,0,0,0.04222869,291.65593,116.90534)"><path
|
|
||||||
d="M 665.6,509.952 H 347.648 c -12.8,0 -21.504,8.704 -21.504,21.504 v 204.8 c 0,12.8 8.704,21.504 21.504,21.504 h 315.904 c 12.8,0 21.504,-8.704 21.504,-21.504 v -204.8 c 2.048,-12.8 -8.704,-21.504 -19.456,-21.504 z M 533.504,661.504 c 0,0 0,2.048 0,0 0,12.8 -8.704,23.552 -21.504,23.552 -12.8,0 -21.504,-8.704 -21.504,-21.504 v -2.048 c -12.8,-6.144 -21.504,-21.504 -21.504,-36.352 0,-23.552 19.456,-42.496 42.496,-42.496 23.04,0 42.496,19.456 42.496,42.496 0.512,14.848 -7.68,29.696 -20.48,36.352 z"
|
|
||||||
fill="#ff6a00"
|
|
||||||
id="path1-3-7" /><path
|
|
||||||
d="M 981.504,492.544 C 970.752,243.2 763.904,44.544 512,44.544 c -251.904,0 -458.752,198.656 -469.504,448 v 31.744 C 48.64,778.24 256,983.04 512,983.04 c 256,0 462.848,-204.8 469.504,-458.752 z M 810.496,272.896 c -42.496,34.304 -91.648,51.2 -130.048,61.952 -23.552,-87.552 -64,-159.744 -108.544,-198.144 95.744,14.848 179.2,64 238.592,136.192 z M 452.096,136.704 C 409.6,175.104 369.152,247.296 345.6,332.8 307.2,322.048 260.096,305.152 217.6,270.848 275.456,198.656 358.4,151.552 452.096,136.704 Z M 825.344,733.696 C 808.448,718.848 786.944,706.048 765.44,693.248 735.744,678.4 720.384,708.096 748.544,720.896 768,729.6 786.944,742.4 805.888,757.248 743.936,832 656.384,881.152 556.032,891.904 c 21.504,-14.848 45.056,-40.448 64,-72.704 6.656,-16.896 -21.504,-27.648 -36.352,-2.048 -25.6,36.352 -51.2,57.344 -74.752,57.344 -21.504,0 -49.152,-21.504 -72.704,-55.296 -21.504,-31.744 -42.496,-14.848 -38.4,0 19.456,29.696 40.448,53.248 61.952,70.656 -98.304,-8.704 -183.296,-57.344 -243.2,-130.048 19.456,-14.848 38.4,-27.648 57.344,-36.352 27.648,-14.848 10.752,-42.496 -16.896,-27.648 -19.456,10.752 -40.448,23.552 -59.904,38.4 C 154.624,674.304 131.072,602.112 129.024,525.312 H 261.12 c 8.704,0 16.896,-6.656 16.896,-16.896 0,-10.24 -6.144,-16.896 -16.896,-16.896 H 130.048 c 4.096,-72.704 27.648,-140.8 68.096,-198.144 38.4,34.304 91.648,55.296 138.752,68.096 -4.096,21.504 -8.704,42.496 -10.752,66.048 0,19.456 32.256,19.456 32.256,0 16.896,-149.504 96.256,-283.648 153.6,-283.648 57.344,0 136.704,136.704 155.648,288.256 2.048,19.456 34.304,16.896 31.744,0 -2.048,-21.504 -6.656,-42.496 -10.752,-64 49.152,-12.8 102.4,-34.304 140.8,-68.096 38.4,55.296 61.952,123.904 66.048,194.048 H 763.392 c -8.704,0 -16.896,6.144 -16.896,16.896 0,10.752 6.144,16.896 16.896,16.896 H 896 c -2.048,75.776 -27.648,146.432 -70.656,205.824 z"
|
|
||||||
fill="#ff6a00"
|
|
||||||
id="path2-2-5" /><path
|
|
||||||
d="m 512,317.952 c -59.904,0 -106.496,47.104 -106.496,106.496 v 31.744 H 448 v -31.744 c 0,-34.304 27.648,-64 64,-64 36.352,0 64,27.648 64,64 v 149.504 h 42.496 V 424.448 C 618.496,364.544 571.904,317.952 512,317.952 Z"
|
|
||||||
fill="#ff6a00"
|
|
||||||
id="path3-6-9" /></g><g
|
|
||||||
style="overflow:hidden;fill:currentColor"
|
|
||||||
id="g14-0-7"
|
|
||||||
transform="matrix(0.04275091,0,0,0.04222869,290.08317,192.55758)"><path
|
|
||||||
d="M 665.6,509.952 H 347.648 c -12.8,0 -21.504,8.704 -21.504,21.504 v 204.8 c 0,12.8 8.704,21.504 21.504,21.504 h 315.904 c 12.8,0 21.504,-8.704 21.504,-21.504 v -204.8 c 2.048,-12.8 -8.704,-21.504 -19.456,-21.504 z M 533.504,661.504 c 0,0 0,2.048 0,0 0,12.8 -8.704,23.552 -21.504,23.552 -12.8,0 -21.504,-8.704 -21.504,-21.504 v -2.048 c -12.8,-6.144 -21.504,-21.504 -21.504,-36.352 0,-23.552 19.456,-42.496 42.496,-42.496 23.04,0 42.496,19.456 42.496,42.496 0.512,14.848 -7.68,29.696 -20.48,36.352 z"
|
|
||||||
fill="#ff6a00"
|
|
||||||
id="path1-3-0-1" /><path
|
|
||||||
d="M 981.504,492.544 C 970.752,243.2 763.904,44.544 512,44.544 c -251.904,0 -458.752,198.656 -469.504,448 v 31.744 C 48.64,778.24 256,983.04 512,983.04 c 256,0 462.848,-204.8 469.504,-458.752 z M 810.496,272.896 c -42.496,34.304 -91.648,51.2 -130.048,61.952 -23.552,-87.552 -64,-159.744 -108.544,-198.144 95.744,14.848 179.2,64 238.592,136.192 z M 452.096,136.704 C 409.6,175.104 369.152,247.296 345.6,332.8 307.2,322.048 260.096,305.152 217.6,270.848 275.456,198.656 358.4,151.552 452.096,136.704 Z M 825.344,733.696 C 808.448,718.848 786.944,706.048 765.44,693.248 735.744,678.4 720.384,708.096 748.544,720.896 768,729.6 786.944,742.4 805.888,757.248 743.936,832 656.384,881.152 556.032,891.904 c 21.504,-14.848 45.056,-40.448 64,-72.704 6.656,-16.896 -21.504,-27.648 -36.352,-2.048 -25.6,36.352 -51.2,57.344 -74.752,57.344 -21.504,0 -49.152,-21.504 -72.704,-55.296 -21.504,-31.744 -42.496,-14.848 -38.4,0 19.456,29.696 40.448,53.248 61.952,70.656 -98.304,-8.704 -183.296,-57.344 -243.2,-130.048 19.456,-14.848 38.4,-27.648 57.344,-36.352 27.648,-14.848 10.752,-42.496 -16.896,-27.648 -19.456,10.752 -40.448,23.552 -59.904,38.4 C 154.624,674.304 131.072,602.112 129.024,525.312 H 261.12 c 8.704,0 16.896,-6.656 16.896,-16.896 0,-10.24 -6.144,-16.896 -16.896,-16.896 H 130.048 c 4.096,-72.704 27.648,-140.8 68.096,-198.144 38.4,34.304 91.648,55.296 138.752,68.096 -4.096,21.504 -8.704,42.496 -10.752,66.048 0,19.456 32.256,19.456 32.256,0 16.896,-149.504 96.256,-283.648 153.6,-283.648 57.344,0 136.704,136.704 155.648,288.256 2.048,19.456 34.304,16.896 31.744,0 -2.048,-21.504 -6.656,-42.496 -10.752,-64 49.152,-12.8 102.4,-34.304 140.8,-68.096 38.4,55.296 61.952,123.904 66.048,194.048 H 763.392 c -8.704,0 -16.896,6.144 -16.896,16.896 0,10.752 6.144,16.896 16.896,16.896 H 896 c -2.048,75.776 -27.648,146.432 -70.656,205.824 z"
|
|
||||||
fill="#ff6a00"
|
|
||||||
id="path2-2-9-5" /><path
|
|
||||||
d="m 512,317.952 c -59.904,0 -106.496,47.104 -106.496,106.496 v 31.744 H 448 v -31.744 c 0,-34.304 27.648,-64 64,-64 36.352,0 64,27.648 64,64 v 149.504 h 42.496 V 424.448 C 618.496,364.544 571.904,317.952 512,317.952 Z"
|
|
||||||
fill="#ff6a00"
|
|
||||||
id="path3-6-5-5" /></g><g
|
|
||||||
style="overflow:hidden;fill:currentColor"
|
|
||||||
id="g18"
|
|
||||||
transform="matrix(0.02516607,0,0,0.02459152,94.079836,77.295599)"><path
|
|
||||||
d="m 128,85.333333 c -46.933333,0 -85.333333,38.399997 -85.333333,85.333337 v 512 A 85.333333,85.333333 0 0 0 128,768 h 298.66667 v 85.33333 h -85.33334 v 85.33334 H 682.66667 V 853.33333 H 597.33333 V 768 H 896 c 46.93333,0 85.33333,-38.4 85.33333,-85.33333 v -512 c 0,-46.93334 -38.4,-85.333336 -85.33333,-85.333337 M 128,170.66667 h 768 v 512 H 128 M 640,213.33333 490.66667,362.66667 640,512 l 59.73333,-59.73333 -89.6,-89.6 89.6,-89.6 M 384,341.33333 l -59.73333,59.73334 89.6,89.6 -89.6,89.6 L 384,640 533.33333,490.66667"
|
|
||||||
id="path1-8" /></g><g
|
|
||||||
style="overflow:hidden;fill:currentColor"
|
|
||||||
id="g19"
|
|
||||||
transform="matrix(0.03266725,0,0,0.03617844,341.02251,197.20412)"><path
|
|
||||||
d="M 0,139.392 409.42933,81.92 409.6,489.13067 0.384,491.52 Z M 409.30133,535.21067 409.6,942.08 0,884.18133 V 532.48 Z M 450.56,81.024 1024,0 V 487.12533 L 450.56,491.52 Z M 1024,533.33333 1023.872,1024 451.37067,944.72533 450.56,532.48 1024,533.376 Z"
|
|
||||||
fill="#0078d7"
|
|
||||||
id="path1-5" /></g><text
|
|
||||||
xml:space="preserve"
|
|
||||||
id="text20"
|
|
||||||
style="white-space:pre;shape-inside:url(#rect20);display:inline;fill:#000000"
|
|
||||||
transform="translate(17.078426,1.7977291)"><tspan
|
|
||||||
x="154.60547"
|
|
||||||
y="81.059292"
|
|
||||||
id="tspan6">Authentication</tspan></text><path
|
|
||||||
id="path5289"
|
|
||||||
style="fill:#000000;stroke-width:0.110144"
|
|
||||||
d="m 327.94236,132.02299 c 0.0606,0.0171 -3.71191,0.11032 -3.45946,5.43921 -1.81339,-0.4647 -4.67039,0.14458 -6.6981,2.42505 -2.05764,2.31413 -5.53002,3.06567 -7.03568,4.20321 0,0 0.82169,1.59402 1.5939,2.02394 0.20057,0.11165 0.4123,0.17508 0.62946,0.20238 l 0.67006,1.21071 0.0736,-1.17458 0.45687,0.8204 0.0634,-1.03363 c 0.0247,-0.009 0.0488,-0.0128 0.0736,-0.0216 l 0.39342,0.70113 0.0532,-0.88184 c 1.07968,-0.47773 2.17818,-1.2374 3.07114,-1.41671 1.59806,-0.32091 2.65787,0.33129 2.25636,4.24291 -3.06578,-2.0673 -5.34393,-1.5927 -7.31738,-0.40477 0,0 -0.50812,1.23953 0.3325,1.76369 3.50763,-1.83664 3.52516,0.94253 6.15491,1.1312 0.0221,-0.0241 0.0443,-0.0479 0.0661,-0.0723 1.97991,-2.22673 4.6186,-2.92005 6.54581,-2.62379 0.0456,-2.42769 0.95349,-3.87017 1.87063,-4.57175 0.75561,-0.56763 1.3864,-0.85491 2.0711,-0.78426 0.15219,0.041 0.2391,0.32129 0.32742,0.5132 0,1e-5 -0.0217,0.0573 -0.0279,0.0759 l 0.0279,0.003 c -0.0387,1.27274 -0.26338,2.51089 -0.27918,3.59232 -0.004,0.23655 0.002,0.4618 0.0178,0.67944 1.11291,-1.67183 2.29902,-3.14124 3.46712,-4.12725 -1.5352,-2.33582 -3.20478,-4.29403 -4.69809,-5.08499 -1.61033,-1.86095 -0.77262,-4.46043 -0.70051,-6.83055 z m 7.47734,4.76338 c 0.0721,2.37025 0.90985,4.96959 -0.70051,6.83054 -1.50705,0.79824 -3.19456,2.78545 -4.74119,5.15006 0.1149,0.47877 0.32471,0.92106 0.68528,1.3481 2.01177,1.11647 4.1666,4.00763 5.9824,7.15229 1.47575,-1.6174 3.49397,-2.45042 6.01534,-1.37701 4.15829,0.92235 3.54345,-3.12836 7.59909,-1.00471 0.84062,-0.52413 0.33503,-1.76369 0.33503,-1.76369 -1.97355,-1.18787 -4.25159,-1.6625 -7.31737,0.40476 -0.40149,-3.91156 0.65833,-4.56018 2.25636,-4.23935 0.89295,0.17931 1.99137,0.93536 3.07113,1.41316 l 0.0534,0.88183 0.39339,-0.70113 c 0.0248,0.009 0.0489,0.0132 0.0736,0.0217 l 0.0634,1.03362 0.45687,-0.81677 0.0736,1.17457 0.67006,-1.21071 c 0.21716,-0.0273 0.42889,-0.0944 0.62946,-0.20601 0.77224,-0.42989 1.5939,-2.02393 1.5939,-2.02393 -1.50566,-1.13745 -4.98052,-1.8854 -7.03815,-4.19953 -2.02773,-2.28049 -4.88223,-2.89336 -6.69553,-2.42861 0.25243,-5.32881 -3.5201,-5.42212 -3.45946,-5.43921 z m -12.94929,2.50812 0.0787,0.51681 c 0,0 -0.78651,0.48492 -1.35538,0.72281 0.0624,0.13017 0.0982,0.28584 0.0965,0.45538 -0.005,0.43746 -0.25624,0.78708 -0.56345,0.78064 -0.30723,-0.006 -0.55277,-0.36487 -0.54824,-0.80232 8.2e-4,-0.0748 0.0111,-0.1485 0.0253,-0.21683 -0.48125,0.0481 -0.94926,0.0542 -0.94926,0.0542 l -0.0787,-0.51681 c 0,0 1.14374,-0.0181 1.69544,-0.18432 0.5517,-0.16625 1.59898,-0.80955 1.59898,-0.80955 z m 7.26154,4.47422 c 0.0606,0.0171 -3.71441,0.11033 -3.46205,5.43921 -1.81339,-0.46469 -4.6678,0.14821 -6.69552,2.42861 -2.05765,2.31413 -5.53251,3.06212 -7.03817,4.19954 0,0 0.82169,1.59401 1.5939,2.02393 0.20058,0.11165 0.41229,0.17869 0.62947,0.206 l 0.67005,1.2107 0.0761,-1.17457 0.45687,0.81677 0.0634,-1.03362 c 0.0247,-0.009 0.0488,-0.0128 0.0736,-0.0216 l 0.39087,0.70112 0.0532,-0.88183 c 1.07967,-0.47773 2.17819,-1.23378 3.07113,-1.41316 1.59806,-0.3209 2.65789,0.33129 2.25637,4.2429 -3.06577,-2.0673 -5.34392,-1.59625 -7.31738,-0.40838 0,0 -0.50558,1.23953 0.33503,1.76369 4.05565,-2.12369 3.4409,1.92706 7.59909,1.00471 6.11014,-2.60131 9.27192,5.99647 8.89356,9.81945 l 8.83772,-4.79229 c 0.69424,-2.21832 -5.33784,-14.94256 -9.78973,-17.30046 -1.61033,-1.86096 -0.7701,-4.46042 -0.69799,-6.83054 z m 11.16043,0.28913 c 0,0 1.04729,0.64692 1.59899,0.81316 0.55171,0.16625 1.69294,0.18433 1.69294,0.18433 l -0.0762,0.51318 c 0,0 -0.46801,-0.006 -0.94925,-0.0542 0.0143,0.0683 0.0221,0.142 0.0229,0.21684 0.004,0.43746 -0.24102,0.79586 -0.54823,0.80231 -0.30722,0.007 -0.55638,-0.34317 -0.56093,-0.78063 -0.002,-0.16954 0.0341,-0.32521 0.0965,-0.45538 -0.56883,-0.23789 -1.35539,-0.72281 -1.35539,-0.72281 l 0.0787,-0.51681 z m -16.63497,6.98236 0.0787,0.5168 c 0,0 -0.784,0.48492 -1.35281,0.72281 0.0624,0.13018 0.0982,0.28584 0.0965,0.45538 -0.005,0.43746 -0.25623,0.78708 -0.56345,0.78063 -0.30722,-0.006 -0.55276,-0.36485 -0.54823,-0.80232 8.2e-4,-0.0748 0.009,-0.14849 0.0228,-0.21684 -0.48124,0.0481 -0.94677,0.0542 -0.94677,0.0542 l -0.0787,-0.5132 c 0,0 1.14373,-0.0181 1.69542,-0.18432 0.55171,-0.16625 1.59649,-0.81315 1.59649,-0.81315 z" /><text
|
|
||||||
xml:space="preserve"
|
|
||||||
id="text22"
|
|
||||||
style="white-space:pre;shape-inside:url(#rect22);fill:#000000"
|
|
||||||
transform="rotate(-14.400077,-58.773649,-265.80494)"><tspan
|
|
||||||
x="62.919922"
|
|
||||||
y="170.94601"
|
|
||||||
id="tspan7">Auth: Negotiate</tspan></text><text
|
|
||||||
xml:space="preserve"
|
|
||||||
id="text23"
|
|
||||||
style="white-space:pre;shape-inside:url(#rect23);fill:#000000"
|
|
||||||
transform="translate(-1.7977291,-1.7977291)"><tspan
|
|
||||||
x="161.79492"
|
|
||||||
y="161.95773"
|
|
||||||
id="tspan8">Get TGT over proxy</tspan></text><text
|
|
||||||
xml:space="preserve"
|
|
||||||
id="text24"
|
|
||||||
style="white-space:pre;shape-inside:url(#rect24);fill:#000000"
|
|
||||||
transform="rotate(-15.585876,175.24729,55.905131)"
|
|
||||||
inkscape:transform-center-x="33.257988"
|
|
||||||
inkscape:transform-center-y="3.5954582"><tspan
|
|
||||||
x="151.9082"
|
|
||||||
y="192.51828"
|
|
||||||
id="tspan9">TGT</tspan></text><text
|
|
||||||
xml:space="preserve"
|
|
||||||
id="text25"
|
|
||||||
style="white-space:pre;shape-inside:url(#rect25);fill:#000000"
|
|
||||||
transform="translate(22.471614,-2.6965937)"><tspan
|
|
||||||
x="170.78516"
|
|
||||||
y="232.9675"
|
|
||||||
id="tspan10">Connect</tspan></text></svg>
|
|
||||||
|
Before Width: | Height: | Size: 20 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 60 KiB |
@ -1,218 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
width="500"
|
|
||||||
height="250"
|
|
||||||
version="1.1"
|
|
||||||
id="svg12"
|
|
||||||
sodipodi:docname="flow-pam.svg"
|
|
||||||
xml:space="preserve"
|
|
||||||
inkscape:version="1.3.2 (091e20e, 2023-11-25)"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
|
||||||
id="defs12"><rect
|
|
||||||
x="168.98654"
|
|
||||||
y="172.58199"
|
|
||||||
width="111.4592"
|
|
||||||
height="26.067072"
|
|
||||||
id="rect29" /><rect
|
|
||||||
x="341.56854"
|
|
||||||
y="66.515976"
|
|
||||||
width="62.021652"
|
|
||||||
height="17.078426"
|
|
||||||
id="rect28" /><rect
|
|
||||||
x="376.62424"
|
|
||||||
y="32.359123"
|
|
||||||
width="96.178505"
|
|
||||||
height="19.775021"
|
|
||||||
id="rect27" /><rect
|
|
||||||
x="170.78426"
|
|
||||||
y="222.01955"
|
|
||||||
width="83.594406"
|
|
||||||
height="14.381833"
|
|
||||||
id="rect25" /><rect
|
|
||||||
x="151.90811"
|
|
||||||
y="181.57063"
|
|
||||||
width="73.706894"
|
|
||||||
height="19.775021"
|
|
||||||
id="rect24" /><rect
|
|
||||||
x="161.79562"
|
|
||||||
y="151.00925"
|
|
||||||
width="124.94217"
|
|
||||||
height="19.775021"
|
|
||||||
id="rect23" /><rect
|
|
||||||
x="62.920521"
|
|
||||||
y="159.99789"
|
|
||||||
width="170.78426"
|
|
||||||
height="27.864801"
|
|
||||||
id="rect22" /><rect
|
|
||||||
x="154.60471"
|
|
||||||
y="70.111435"
|
|
||||||
width="114.1558"
|
|
||||||
height="14.381833"
|
|
||||||
id="rect20" /><rect
|
|
||||||
x="133.93082"
|
|
||||||
y="257.97412"
|
|
||||||
width="213.0309"
|
|
||||||
height="26.067072"
|
|
||||||
id="rect18" /><rect
|
|
||||||
x="346.96173"
|
|
||||||
y="155.50357"
|
|
||||||
width="102.47056"
|
|
||||||
height="28.763666"
|
|
||||||
id="rect17" /><rect
|
|
||||||
x="200.44679"
|
|
||||||
y="197.7502"
|
|
||||||
width="212.13203"
|
|
||||||
height="20.673885"
|
|
||||||
id="rect16" /><rect
|
|
||||||
x="81.796677"
|
|
||||||
y="164.4922"
|
|
||||||
width="157.3013"
|
|
||||||
height="16.179562"
|
|
||||||
id="rect15" /><rect
|
|
||||||
x="200.44679"
|
|
||||||
y="108.76261"
|
|
||||||
width="95.27964"
|
|
||||||
height="19.775021"
|
|
||||||
id="rect14" /><rect
|
|
||||||
x="200.44679"
|
|
||||||
y="197.7502"
|
|
||||||
width="212.13203"
|
|
||||||
height="20.673885"
|
|
||||||
id="rect16-2" /><rect
|
|
||||||
x="81.796677"
|
|
||||||
y="164.4922"
|
|
||||||
width="157.3013"
|
|
||||||
height="16.179562"
|
|
||||||
id="rect15-6" /></defs><sodipodi:namedview
|
|
||||||
id="namedview12"
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#000000"
|
|
||||||
borderopacity="0.25"
|
|
||||||
inkscape:showpageshadow="2"
|
|
||||||
inkscape:pageopacity="0.0"
|
|
||||||
inkscape:pagecheckerboard="0"
|
|
||||||
inkscape:deskcolor="#d1d1d1"
|
|
||||||
inkscape:zoom="1.1125147"
|
|
||||||
inkscape:cx="521.34143"
|
|
||||||
inkscape:cy="166.73937"
|
|
||||||
inkscape:window-width="2400"
|
|
||||||
inkscape:window-height="1274"
|
|
||||||
inkscape:window-x="0"
|
|
||||||
inkscape:window-y="25"
|
|
||||||
inkscape:window-maximized="0"
|
|
||||||
inkscape:current-layer="svg12" /><!-- Rectangles --><!-- Text --><text
|
|
||||||
x="61.016945"
|
|
||||||
y="43.05085"
|
|
||||||
font-family="Arial"
|
|
||||||
font-size="16px"
|
|
||||||
fill="#000000"
|
|
||||||
id="text5"><tspan
|
|
||||||
sodipodi:role="line"
|
|
||||||
x="61.016945"
|
|
||||||
y="43.05085"
|
|
||||||
id="tspan22">PAM(Basic/Local)</tspan></text><!-- Lines --><line
|
|
||||||
x1="134.4955"
|
|
||||||
y1="103.28663"
|
|
||||||
x2="288.42737"
|
|
||||||
y2="103.28663"
|
|
||||||
stroke="#000000"
|
|
||||||
stroke-width="2.48139"
|
|
||||||
id="line9" /><line
|
|
||||||
x1="342.65012"
|
|
||||||
y1="104.55545"
|
|
||||||
x2="403.99893"
|
|
||||||
y2="104.55545"
|
|
||||||
stroke="#000000"
|
|
||||||
stroke-width="1.56651"
|
|
||||||
id="line9-1" /><line
|
|
||||||
x1="132.88857"
|
|
||||||
y1="162.08278"
|
|
||||||
x2="286.8204"
|
|
||||||
y2="162.08278"
|
|
||||||
stroke="#000000"
|
|
||||||
stroke-width="2.48139"
|
|
||||||
id="line9-49" /><line
|
|
||||||
x1="134.74902"
|
|
||||||
y1="160.4328"
|
|
||||||
x2="287.49759"
|
|
||||||
y2="114.58726"
|
|
||||||
stroke="#000000"
|
|
||||||
stroke-width="2.37506"
|
|
||||||
id="line9-4" /><g
|
|
||||||
style="overflow:hidden;fill:currentColor"
|
|
||||||
id="g12"
|
|
||||||
transform="matrix(0.06600417,0,0,0.05178799,19.223463,76.951852)"><path
|
|
||||||
d="M 843.28296,870.11556 C 834.84444,729.6 738.98667,612.69333 609.37481,572.96593 687.88148,536.27259 742.4,456.53333 742.4,364.08889 c 0,-127.24148 -103.15852,-230.4 -230.4,-230.4 -127.24148,0 -230.4,103.15852 -230.4,230.4 0,92.44444 54.51852,172.1837 133.12,208.87704 C 285.10815,612.69333 189.25037,729.6 180.81185,870.11556 c -0.6637,10.9037 7.96445,20.19555 18.96297,20.19555 v 0 c 9.95555,0 18.29925,-7.77481 18.96296,-17.73037 C 227.74518,718.50667 355.65037,596.38518 512,596.38518 c 156.34963,0 284.25481,122.12149 293.35704,276.19556 0.56889,9.95556 8.91259,17.73037 18.96296,17.73037 10.99852,0 19.62667,-9.29185 18.96296,-20.19555 z M 319.52593,364.08889 c 0,-106.28741 86.18666,-192.47408 192.47407,-192.47408 106.28741,0 192.47407,86.18667 192.47407,192.47408 0,106.28741 -86.18666,192.47407 -192.47407,192.47407 -106.28741,0 -192.47407,-86.18666 -192.47407,-192.47407 z"
|
|
||||||
id="path1" /></g><g
|
|
||||||
style="overflow:hidden;fill:currentColor"
|
|
||||||
id="g14"
|
|
||||||
transform="matrix(0.04275091,0,0,0.04222869,292.71414,82.391967)"><path
|
|
||||||
d="M 665.6,509.952 H 347.648 c -12.8,0 -21.504,8.704 -21.504,21.504 v 204.8 c 0,12.8 8.704,21.504 21.504,21.504 h 315.904 c 12.8,0 21.504,-8.704 21.504,-21.504 v -204.8 c 2.048,-12.8 -8.704,-21.504 -19.456,-21.504 z M 533.504,661.504 c 0,0 0,2.048 0,0 0,12.8 -8.704,23.552 -21.504,23.552 -12.8,0 -21.504,-8.704 -21.504,-21.504 v -2.048 c -12.8,-6.144 -21.504,-21.504 -21.504,-36.352 0,-23.552 19.456,-42.496 42.496,-42.496 23.04,0 42.496,19.456 42.496,42.496 0.512,14.848 -7.68,29.696 -20.48,36.352 z"
|
|
||||||
fill="#ff6a00"
|
|
||||||
id="path1-3" /><path
|
|
||||||
d="M 981.504,492.544 C 970.752,243.2 763.904,44.544 512,44.544 c -251.904,0 -458.752,198.656 -469.504,448 v 31.744 C 48.64,778.24 256,983.04 512,983.04 c 256,0 462.848,-204.8 469.504,-458.752 z M 810.496,272.896 c -42.496,34.304 -91.648,51.2 -130.048,61.952 -23.552,-87.552 -64,-159.744 -108.544,-198.144 95.744,14.848 179.2,64 238.592,136.192 z M 452.096,136.704 C 409.6,175.104 369.152,247.296 345.6,332.8 307.2,322.048 260.096,305.152 217.6,270.848 275.456,198.656 358.4,151.552 452.096,136.704 Z M 825.344,733.696 C 808.448,718.848 786.944,706.048 765.44,693.248 735.744,678.4 720.384,708.096 748.544,720.896 768,729.6 786.944,742.4 805.888,757.248 743.936,832 656.384,881.152 556.032,891.904 c 21.504,-14.848 45.056,-40.448 64,-72.704 6.656,-16.896 -21.504,-27.648 -36.352,-2.048 -25.6,36.352 -51.2,57.344 -74.752,57.344 -21.504,0 -49.152,-21.504 -72.704,-55.296 -21.504,-31.744 -42.496,-14.848 -38.4,0 19.456,29.696 40.448,53.248 61.952,70.656 -98.304,-8.704 -183.296,-57.344 -243.2,-130.048 19.456,-14.848 38.4,-27.648 57.344,-36.352 27.648,-14.848 10.752,-42.496 -16.896,-27.648 -19.456,10.752 -40.448,23.552 -59.904,38.4 C 154.624,674.304 131.072,602.112 129.024,525.312 H 261.12 c 8.704,0 16.896,-6.656 16.896,-16.896 0,-10.24 -6.144,-16.896 -16.896,-16.896 H 130.048 c 4.096,-72.704 27.648,-140.8 68.096,-198.144 38.4,34.304 91.648,55.296 138.752,68.096 -4.096,21.504 -8.704,42.496 -10.752,66.048 0,19.456 32.256,19.456 32.256,0 16.896,-149.504 96.256,-283.648 153.6,-283.648 57.344,0 136.704,136.704 155.648,288.256 2.048,19.456 34.304,16.896 31.744,0 -2.048,-21.504 -6.656,-42.496 -10.752,-64 49.152,-12.8 102.4,-34.304 140.8,-68.096 38.4,55.296 61.952,123.904 66.048,194.048 H 763.392 c -8.704,0 -16.896,6.144 -16.896,16.896 0,10.752 6.144,16.896 16.896,16.896 H 896 c -2.048,75.776 -27.648,146.432 -70.656,205.824 z"
|
|
||||||
fill="#ff6a00"
|
|
||||||
id="path2-2" /><path
|
|
||||||
d="m 512,317.952 c -59.904,0 -106.496,47.104 -106.496,106.496 v 31.744 H 448 v -31.744 c 0,-34.304 27.648,-64 64,-64 36.352,0 64,27.648 64,64 v 149.504 h 42.496 V 424.448 C 618.496,364.544 571.904,317.952 512,317.952 Z"
|
|
||||||
fill="#ff6a00"
|
|
||||||
id="path3-6" /></g><g
|
|
||||||
style="overflow:hidden;fill:currentColor"
|
|
||||||
id="g14-8"
|
|
||||||
transform="matrix(0.04275091,0,0,0.04222869,292.55479,139.19739)"><path
|
|
||||||
d="M 665.6,509.952 H 347.648 c -12.8,0 -21.504,8.704 -21.504,21.504 v 204.8 c 0,12.8 8.704,21.504 21.504,21.504 h 315.904 c 12.8,0 21.504,-8.704 21.504,-21.504 v -204.8 c 2.048,-12.8 -8.704,-21.504 -19.456,-21.504 z M 533.504,661.504 c 0,0 0,2.048 0,0 0,12.8 -8.704,23.552 -21.504,23.552 -12.8,0 -21.504,-8.704 -21.504,-21.504 v -2.048 c -12.8,-6.144 -21.504,-21.504 -21.504,-36.352 0,-23.552 19.456,-42.496 42.496,-42.496 23.04,0 42.496,19.456 42.496,42.496 0.512,14.848 -7.68,29.696 -20.48,36.352 z"
|
|
||||||
fill="#ff6a00"
|
|
||||||
id="path1-3-7" /><path
|
|
||||||
d="M 981.504,492.544 C 970.752,243.2 763.904,44.544 512,44.544 c -251.904,0 -458.752,198.656 -469.504,448 v 31.744 C 48.64,778.24 256,983.04 512,983.04 c 256,0 462.848,-204.8 469.504,-458.752 z M 810.496,272.896 c -42.496,34.304 -91.648,51.2 -130.048,61.952 -23.552,-87.552 -64,-159.744 -108.544,-198.144 95.744,14.848 179.2,64 238.592,136.192 z M 452.096,136.704 C 409.6,175.104 369.152,247.296 345.6,332.8 307.2,322.048 260.096,305.152 217.6,270.848 275.456,198.656 358.4,151.552 452.096,136.704 Z M 825.344,733.696 C 808.448,718.848 786.944,706.048 765.44,693.248 735.744,678.4 720.384,708.096 748.544,720.896 768,729.6 786.944,742.4 805.888,757.248 743.936,832 656.384,881.152 556.032,891.904 c 21.504,-14.848 45.056,-40.448 64,-72.704 6.656,-16.896 -21.504,-27.648 -36.352,-2.048 -25.6,36.352 -51.2,57.344 -74.752,57.344 -21.504,0 -49.152,-21.504 -72.704,-55.296 -21.504,-31.744 -42.496,-14.848 -38.4,0 19.456,29.696 40.448,53.248 61.952,70.656 -98.304,-8.704 -183.296,-57.344 -243.2,-130.048 19.456,-14.848 38.4,-27.648 57.344,-36.352 27.648,-14.848 10.752,-42.496 -16.896,-27.648 -19.456,10.752 -40.448,23.552 -59.904,38.4 C 154.624,674.304 131.072,602.112 129.024,525.312 H 261.12 c 8.704,0 16.896,-6.656 16.896,-16.896 0,-10.24 -6.144,-16.896 -16.896,-16.896 H 130.048 c 4.096,-72.704 27.648,-140.8 68.096,-198.144 38.4,34.304 91.648,55.296 138.752,68.096 -4.096,21.504 -8.704,42.496 -10.752,66.048 0,19.456 32.256,19.456 32.256,0 16.896,-149.504 96.256,-283.648 153.6,-283.648 57.344,0 136.704,136.704 155.648,288.256 2.048,19.456 34.304,16.896 31.744,0 -2.048,-21.504 -6.656,-42.496 -10.752,-64 49.152,-12.8 102.4,-34.304 140.8,-68.096 38.4,55.296 61.952,123.904 66.048,194.048 H 763.392 c -8.704,0 -16.896,6.144 -16.896,16.896 0,10.752 6.144,16.896 16.896,16.896 H 896 c -2.048,75.776 -27.648,146.432 -70.656,205.824 z"
|
|
||||||
fill="#ff6a00"
|
|
||||||
id="path2-2-5" /><path
|
|
||||||
d="m 512,317.952 c -59.904,0 -106.496,47.104 -106.496,106.496 v 31.744 H 448 v -31.744 c 0,-34.304 27.648,-64 64,-64 36.352,0 64,27.648 64,64 v 149.504 h 42.496 V 424.448 C 618.496,364.544 571.904,317.952 512,317.952 Z"
|
|
||||||
fill="#ff6a00"
|
|
||||||
id="path3-6-9" /></g><g
|
|
||||||
style="overflow:hidden;fill:currentColor"
|
|
||||||
id="g18"
|
|
||||||
transform="matrix(0.02516607,0,0,0.02459152,94.079836,93.295599)"><path
|
|
||||||
d="m 128,85.333333 c -46.933333,0 -85.333333,38.399997 -85.333333,85.333337 v 512 A 85.333333,85.333333 0 0 0 128,768 h 298.66667 v 85.33333 h -85.33334 v 85.33334 H 682.66667 V 853.33333 H 597.33333 V 768 H 896 c 46.93333,0 85.33333,-38.4 85.33333,-85.33333 v -512 c 0,-46.93334 -38.4,-85.333336 -85.33333,-85.333337 M 128,170.66667 h 768 v 512 H 128 M 640,213.33333 490.66667,362.66667 640,512 l 59.73333,-59.73333 -89.6,-89.6 89.6,-89.6 M 384,341.33333 l -59.73333,59.73334 89.6,89.6 -89.6,89.6 L 384,640 533.33333,490.66667"
|
|
||||||
id="path1-8" /></g><g
|
|
||||||
style="overflow:hidden;fill:currentColor"
|
|
||||||
id="g19"
|
|
||||||
transform="matrix(0.03266725,0,0,0.03617844,345.51683,142.19382)"><path
|
|
||||||
d="M 0,139.392 409.42933,81.92 409.6,489.13067 0.384,491.52 Z M 409.30133,535.21067 409.6,942.08 0,884.18133 V 532.48 Z M 450.56,81.024 1024,0 V 487.12533 L 450.56,491.52 Z M 1024,533.33333 1023.872,1024 451.37067,944.72533 450.56,532.48 1024,533.376 Z"
|
|
||||||
fill="#0078d7"
|
|
||||||
id="path1-5" /></g><text
|
|
||||||
xml:space="preserve"
|
|
||||||
id="text20"
|
|
||||||
style="white-space:pre;shape-inside:url(#rect20);display:inline;fill:#000000"
|
|
||||||
transform="translate(17.078426,17.797729)"><tspan
|
|
||||||
x="154.60547"
|
|
||||||
y="81.059292"
|
|
||||||
id="tspan5">Authentication</tspan></text><g
|
|
||||||
style="overflow:hidden;fill:currentColor"
|
|
||||||
id="g27"
|
|
||||||
transform="matrix(0.04222519,0,0,0.03933851,410.28976,84.846267)"><path
|
|
||||||
d="M 916.48,242.88 907.52,172.16 512,32 116.48,172.16 l -8.96,70.4 c -3.2,23.68 -68.48,578.88 365.12,736 L 512,992 551.04,977.92 C 983.36,822.4 919.68,266.56 916.48,242.88 Z m -154.88,147.2 -211.52,339.2 -2.88,4.48 A 86.4,86.4 0 0 1 428.48,761.28 87.68,87.68 0 0 1 405.12,739.84 L 268.48,557.76 A 64.03559,64.03559 0 0 1 359.04,467.2 L 459.2,544 659.2,315.2 a 64,64 0 0 1 102.72,76.16 z"
|
|
||||||
fill="#231f20"
|
|
||||||
id="path1-4" /></g><text
|
|
||||||
xml:space="preserve"
|
|
||||||
id="text27"
|
|
||||||
style="white-space:pre;shape-inside:url(#rect27);display:inline;fill:#000000"
|
|
||||||
transform="translate(21.572749,35.77502)"><tspan
|
|
||||||
x="376.625"
|
|
||||||
y="43.307339"
|
|
||||||
id="tspan6">rdpgw-auth</tspan></text><text
|
|
||||||
xml:space="preserve"
|
|
||||||
id="text28"
|
|
||||||
style="white-space:pre;shape-inside:url(#rect28);display:inline;fill:#000000"
|
|
||||||
transform="translate(11.685239,21.393187)"><tspan
|
|
||||||
x="341.56836"
|
|
||||||
y="77.463589"
|
|
||||||
id="tspan7">socket</tspan></text><text
|
|
||||||
xml:space="preserve"
|
|
||||||
id="text29"
|
|
||||||
style="white-space:pre;shape-inside:url(#rect29);display:inline;fill:#000000"
|
|
||||||
transform="translate(23.370478,-8.089781)"><tspan
|
|
||||||
x="168.98633"
|
|
||||||
y="183.53"
|
|
||||||
id="tspan8">connect</tspan></text></svg>
|
|
||||||
|
Before Width: | Height: | Size: 13 KiB |
@ -1,22 +0,0 @@
|
|||||||
<svg width="500" height="300" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<!-- Rectangles -->
|
|
||||||
<rect x="50" y="50" width="150" height="50" fill="lightblue" stroke="black" stroke-width="2"/>
|
|
||||||
<rect x="200" y="50" width="150" height="50" fill="lightblue" stroke="black" stroke-width="2"/>
|
|
||||||
<rect x="50" y="150" width="150" height="50" fill="lightblue" stroke="black" stroke-width="2"/>
|
|
||||||
<rect x="200" y="150" width="150" height="50" fill="lightblue" stroke="black" stroke-width="2"/>
|
|
||||||
<rect x="350" y="150" width="150" height="50" fill="lightblue" stroke="black" stroke-width="2"/>
|
|
||||||
|
|
||||||
<!-- Text -->
|
|
||||||
<text x="75" y="85" font-family="Arial" font-size="16" fill="black">Client</text>
|
|
||||||
<text x="235" y="85" font-family="Arial" font-size="16" fill="black">RDP Gateway</text>
|
|
||||||
<text x="65" y="185" font-family="Arial" font-size="16" fill="black">RDP GW Auth</text>
|
|
||||||
<text x="215" y="185" font-family="Arial" font-size="16" fill="black">PAM</text>
|
|
||||||
<text x="365" y="185" font-family="Arial" font-size="16" fill="black">Passwd or LDAP</text>
|
|
||||||
|
|
||||||
<!-- Lines -->
|
|
||||||
<line x1="100" y1="75" x2="200" y2="75" stroke="black" stroke-width="2"/>
|
|
||||||
<line x1="200" y1="100" x2="100" y2="175" stroke="black" stroke-width="2"/>
|
|
||||||
<line x1="100" y1="175" x2="200" y2="175" stroke="black" stroke-width="2"/>
|
|
||||||
<line x1="200" y1="200" x2="350" y2="200" stroke="black" stroke-width="2"/>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 1.4 KiB |
@ -1,23 +0,0 @@
|
|||||||
# RDPGW认证配置示例
|
|
||||||
# 将此文件保存为rdpgw-auth.yaml并根据您的环境调整设置
|
|
||||||
|
|
||||||
# API认证配置
|
|
||||||
ApiAuth:
|
|
||||||
# 启用API认证
|
|
||||||
enabled: true
|
|
||||||
# API URL - 验证用户凭据的端点
|
|
||||||
# NTLM认证需要API提供明文密码
|
|
||||||
# 此API需要支持以下两种模式:
|
|
||||||
# 1. ?username=user&mode=getpassword - 返回用户的明文密码
|
|
||||||
# 2. ?username=user&password=pass&mode=verify - 验证凭据是否正确
|
|
||||||
apiurl: "https://your-api-server/api/checkperm/"
|
|
||||||
|
|
||||||
# 用户配置
|
|
||||||
# 当使用API认证时,此部分可以为空
|
|
||||||
# 如果为特定用户提供密码,将绕过API并使用本地密码
|
|
||||||
Users: []
|
|
||||||
|
|
||||||
# 或者,您也可以同时支持API认证和本地用户认证
|
|
||||||
# 在这种情况下,请提供本地用户凭据
|
|
||||||
# users:
|
|
||||||
# - {Username: "local_user", Password: "local_password"}
|
|
||||||
70
go.mod
70
go.mod
@ -1,60 +1,52 @@
|
|||||||
module github.com/bolkedebruin/rdpgw
|
module github.com/bolkedebruin/rdpgw
|
||||||
|
|
||||||
go 1.22
|
go 1.19
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/bolkedebruin/gokrb5/v8 v8.5.0
|
github.com/bolkedebruin/gokrb5/v8 v8.5.0
|
||||||
github.com/coreos/go-oidc/v3 v3.9.0
|
github.com/coreos/go-oidc/v3 v3.2.0
|
||||||
github.com/fatih/structs v1.1.0
|
github.com/fatih/structs v1.1.0
|
||||||
github.com/go-jose/go-jose/v4 v4.0.1
|
github.com/go-jose/go-jose/v3 v3.0.0
|
||||||
github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1
|
github.com/google/uuid v1.1.2
|
||||||
github.com/google/uuid v1.6.0
|
github.com/gorilla/mux v1.8.0
|
||||||
github.com/gorilla/mux v1.8.1
|
github.com/gorilla/sessions v1.2.1
|
||||||
github.com/gorilla/sessions v1.2.2
|
github.com/gorilla/websocket v1.5.0
|
||||||
github.com/gorilla/websocket v1.5.1
|
|
||||||
github.com/jcmturner/gofork v1.7.6
|
github.com/jcmturner/gofork v1.7.6
|
||||||
github.com/jcmturner/goidentity/v6 v6.0.1
|
github.com/jcmturner/goidentity/v6 v6.0.1
|
||||||
github.com/knadh/koanf/parsers/yaml v0.1.0
|
github.com/knadh/koanf v1.4.2
|
||||||
github.com/knadh/koanf/providers/confmap v0.1.0
|
github.com/msteinert/pam v1.0.0
|
||||||
github.com/knadh/koanf/providers/env v0.1.0
|
|
||||||
github.com/knadh/koanf/providers/file v0.1.0
|
|
||||||
github.com/knadh/koanf/v2 v2.1.0
|
|
||||||
github.com/m7913d/go-ntlm v0.0.1
|
|
||||||
github.com/msteinert/pam/v2 v2.0.0
|
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||||
github.com/prometheus/client_golang v1.19.0
|
github.com/prometheus/client_golang v1.12.1
|
||||||
github.com/stretchr/testify v1.9.0
|
github.com/thought-machine/go-flags v1.6.1
|
||||||
github.com/thought-machine/go-flags v1.6.3
|
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa
|
||||||
golang.org/x/crypto v0.31.0
|
golang.org/x/oauth2 v0.0.0-20220722155238-128564f6959c
|
||||||
golang.org/x/oauth2 v0.18.0
|
google.golang.org/grpc v1.49.0
|
||||||
google.golang.org/grpc v1.62.1
|
google.golang.org/protobuf v1.28.1
|
||||||
google.golang.org/protobuf v1.33.0
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/fsnotify/fsnotify v1.5.4 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
github.com/golang/protobuf v1.5.2 // indirect
|
||||||
github.com/go-jose/go-jose/v3 v3.0.1 // indirect
|
github.com/gorilla/securecookie v1.1.1 // indirect
|
||||||
github.com/golang/protobuf v1.5.4 // indirect
|
|
||||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
|
||||||
github.com/hashicorp/go-uuid v1.0.3 // indirect
|
github.com/hashicorp/go-uuid v1.0.3 // indirect
|
||||||
github.com/jcmturner/aescts/v2 v2.0.0 // indirect
|
github.com/jcmturner/aescts/v2 v2.0.0 // indirect
|
||||||
github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect
|
github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect
|
||||||
github.com/jcmturner/rpc/v2 v2.0.3 // indirect
|
github.com/jcmturner/rpc/v2 v2.0.3 // indirect
|
||||||
github.com/knadh/koanf/maps v0.1.1 // indirect
|
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||||
github.com/kr/text v0.2.0 // indirect
|
|
||||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||||
github.com/prometheus/client_model v0.6.0 // indirect
|
github.com/prometheus/client_model v0.2.0 // indirect
|
||||||
github.com/prometheus/common v0.50.0 // indirect
|
github.com/prometheus/common v0.32.1 // indirect
|
||||||
github.com/prometheus/procfs v0.13.0 // indirect
|
github.com/prometheus/procfs v0.7.3 // indirect
|
||||||
golang.org/x/net v0.23.0 // indirect
|
golang.org/x/net v0.0.0-20220725212005-46097bf591d3 // indirect
|
||||||
golang.org/x/sys v0.28.0 // indirect
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect
|
||||||
golang.org/x/text v0.21.0 // indirect
|
golang.org/x/text v0.3.7 // indirect
|
||||||
google.golang.org/appengine v1.6.8 // indirect
|
google.golang.org/appengine v1.6.7 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c // indirect
|
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987 // indirect
|
||||||
|
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
132
index.js
132
index.js
@ -1,132 +0,0 @@
|
|||||||
const express = require('express');
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
// 配置信息
|
|
||||||
const config = {
|
|
||||||
port: 3000,
|
|
||||||
// 用户信息,可以通过配置文件覆盖
|
|
||||||
users: {
|
|
||||||
'testuser': 'testpassword',
|
|
||||||
'admin': 'adminpass',
|
|
||||||
'user1': 'password1'
|
|
||||||
},
|
|
||||||
// API路径
|
|
||||||
apiPath: '/api/checkperm'
|
|
||||||
};
|
|
||||||
|
|
||||||
// 尝试加载配置文件
|
|
||||||
try {
|
|
||||||
const configPath = path.join(__dirname, 'config.json');
|
|
||||||
if (fs.existsSync(configPath)) {
|
|
||||||
const fileConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
||||||
// 合并配置
|
|
||||||
Object.assign(config, fileConfig);
|
|
||||||
console.log('已加载配置文件');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log('加载配置文件失败,使用默认配置:', error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
const app = express();
|
|
||||||
|
|
||||||
// 添加中间件解析JSON和表单数据
|
|
||||||
app.use(express.json());
|
|
||||||
app.use(express.urlencoded({ extended: true }));
|
|
||||||
|
|
||||||
// 处理认证逻辑的函数
|
|
||||||
function handleAuth(username, password, mode) {
|
|
||||||
// 验证参数是否存在
|
|
||||||
if (!username) {
|
|
||||||
console.log('缺少必要参数: username');
|
|
||||||
return { status: 400, response: { status: 'error', message: '缺少必要参数: username' } };
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查用户是否存在
|
|
||||||
if (!config.users[username]) {
|
|
||||||
console.log(`用户不存在: ${username}`);
|
|
||||||
return { status: 401, response: { status: 'error', message: '用户不存在' } };
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据模式处理请求
|
|
||||||
if (mode === 'getpassword') {
|
|
||||||
// 密码获取模式 - 用于NTLM认证
|
|
||||||
console.log(`返回用户 ${username} 的密码`);
|
|
||||||
return {
|
|
||||||
status: 200,
|
|
||||||
response: {
|
|
||||||
status: 'success',
|
|
||||||
password: config.users[username]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
// 默认为verify模式 - 验证用户名和密码
|
|
||||||
if (!password) {
|
|
||||||
console.log('缺少必要参数: password');
|
|
||||||
return { status: 400, response: { status: 'error', message: '缺少必要参数: password' } };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.users[username] === password) {
|
|
||||||
console.log('认证成功');
|
|
||||||
return {
|
|
||||||
status: 200,
|
|
||||||
response: {
|
|
||||||
status: 'success',
|
|
||||||
user: username
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
console.log('认证失败: 密码不正确');
|
|
||||||
return { status: 401, response: { status: 'error', message: '认证失败' } };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 认证API端点 - GET
|
|
||||||
app.get(config.apiPath, (req, res) => {
|
|
||||||
const { username, password, mode } = req.query;
|
|
||||||
|
|
||||||
console.log('收到GET认证请求:');
|
|
||||||
console.log(`username: ${username}`);
|
|
||||||
console.log(`mode: ${mode || 'verify'}`);
|
|
||||||
if (password) {
|
|
||||||
console.log(`password: ${'*'.repeat(password ? password.length : 0)}`); // 为安全起见不打印实际密码
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = handleAuth(username, password, mode);
|
|
||||||
return res.status(result.status).json(result.response);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 认证API端点 - POST
|
|
||||||
app.post(config.apiPath, (req, res) => {
|
|
||||||
const { username, password, mode } = req.body;
|
|
||||||
|
|
||||||
console.log('收到POST认证请求:');
|
|
||||||
console.log(`username: ${username}`);
|
|
||||||
console.log(`mode: ${mode || 'verify'}`);
|
|
||||||
if (password) {
|
|
||||||
console.log(`password: ${'*'.repeat(password ? password.length : 0)}`); // 为安全起见不打印实际密码
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = handleAuth(username, password, mode);
|
|
||||||
return res.status(result.status).json(result.response);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 根路径返回服务信息
|
|
||||||
app.get('/', (req, res) => {
|
|
||||||
res.send('RDPGW远程认证测试服务已启动');
|
|
||||||
});
|
|
||||||
|
|
||||||
// 启动服务器
|
|
||||||
app.listen(config.port, () => {
|
|
||||||
console.log(`认证服务器已启动,监听端口: ${config.port}`);
|
|
||||||
console.log('当前配置:');
|
|
||||||
console.log(`- 端口: ${config.port}`);
|
|
||||||
console.log(`- API路径: ${config.apiPath}`);
|
|
||||||
console.log(`- 已配置用户数: ${Object.keys(config.users).length}`);
|
|
||||||
console.log('\n支持的模式:');
|
|
||||||
console.log(`1. 验证模式 (GET): http://localhost:${config.port}${config.apiPath}?username=testuser&password=testpassword&mode=verify`);
|
|
||||||
console.log(`2. 密码获取模式 (GET): http://localhost:${config.port}${config.apiPath}?username=testuser&mode=getpassword`);
|
|
||||||
console.log('---');
|
|
||||||
console.log('POST请求也支持,可以通过请求体发送参数');
|
|
||||||
});
|
|
||||||
@ -14,18 +14,6 @@ message AuthResponse {
|
|||||||
string error = 2;
|
string error = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message NtlmRequest {
|
|
||||||
string session = 1;
|
|
||||||
string ntlmMessage = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
message NtlmResponse {
|
|
||||||
bool authenticated = 1;
|
|
||||||
string username = 2;
|
|
||||||
string ntlmMessage = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
service Authenticate {
|
service Authenticate {
|
||||||
rpc Authenticate (UserPass) returns (AuthResponse) {}
|
rpc Authenticate (UserPass) returns (AuthResponse) {}
|
||||||
rpc NTLM (NtlmRequest) returns (NtlmResponse) {}
|
}
|
||||||
}
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// protoc-gen-go v1.25.0-devel
|
// protoc-gen-go v1.28.1
|
||||||
// protoc v3.14.0
|
// protoc v3.21.5
|
||||||
// source: auth.proto
|
// source: auth.proto
|
||||||
|
|
||||||
package auth
|
package auth
|
||||||
@ -130,132 +130,6 @@ func (x *AuthResponse) GetError() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
type NtlmRequest struct {
|
|
||||||
state protoimpl.MessageState
|
|
||||||
sizeCache protoimpl.SizeCache
|
|
||||||
unknownFields protoimpl.UnknownFields
|
|
||||||
|
|
||||||
Session string `protobuf:"bytes,1,opt,name=session,proto3" json:"session,omitempty"`
|
|
||||||
NtlmMessage string `protobuf:"bytes,2,opt,name=ntlmMessage,proto3" json:"ntlmMessage,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *NtlmRequest) Reset() {
|
|
||||||
*x = NtlmRequest{}
|
|
||||||
if protoimpl.UnsafeEnabled {
|
|
||||||
mi := &file_auth_proto_msgTypes[2]
|
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
|
||||||
ms.StoreMessageInfo(mi)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *NtlmRequest) String() string {
|
|
||||||
return protoimpl.X.MessageStringOf(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*NtlmRequest) ProtoMessage() {}
|
|
||||||
|
|
||||||
func (x *NtlmRequest) ProtoReflect() protoreflect.Message {
|
|
||||||
mi := &file_auth_proto_msgTypes[2]
|
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
|
||||||
if ms.LoadMessageInfo() == nil {
|
|
||||||
ms.StoreMessageInfo(mi)
|
|
||||||
}
|
|
||||||
return ms
|
|
||||||
}
|
|
||||||
return mi.MessageOf(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deprecated: Use NtlmRequest.ProtoReflect.Descriptor instead.
|
|
||||||
func (*NtlmRequest) Descriptor() ([]byte, []int) {
|
|
||||||
return file_auth_proto_rawDescGZIP(), []int{2}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *NtlmRequest) GetSession() string {
|
|
||||||
if x != nil {
|
|
||||||
return x.Session
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *NtlmRequest) GetNtlmMessage() string {
|
|
||||||
if x != nil {
|
|
||||||
return x.NtlmMessage
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
type NtlmResponse struct {
|
|
||||||
state protoimpl.MessageState
|
|
||||||
sizeCache protoimpl.SizeCache
|
|
||||||
unknownFields protoimpl.UnknownFields
|
|
||||||
|
|
||||||
Authenticated bool `protobuf:"varint,1,opt,name=authenticated,proto3" json:"authenticated,omitempty"`
|
|
||||||
Username string `protobuf:"bytes,2,opt,name=username,proto3" json:"username,omitempty"`
|
|
||||||
NtlmMessage string `protobuf:"bytes,3,opt,name=ntlmMessage,proto3" json:"ntlmMessage,omitempty"`
|
|
||||||
Error string `protobuf:"bytes,4,opt,name=error,proto3" json:"error,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *NtlmResponse) Reset() {
|
|
||||||
*x = NtlmResponse{}
|
|
||||||
if protoimpl.UnsafeEnabled {
|
|
||||||
mi := &file_auth_proto_msgTypes[3]
|
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
|
||||||
ms.StoreMessageInfo(mi)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *NtlmResponse) String() string {
|
|
||||||
return protoimpl.X.MessageStringOf(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*NtlmResponse) ProtoMessage() {}
|
|
||||||
|
|
||||||
func (x *NtlmResponse) ProtoReflect() protoreflect.Message {
|
|
||||||
mi := &file_auth_proto_msgTypes[3]
|
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
|
||||||
if ms.LoadMessageInfo() == nil {
|
|
||||||
ms.StoreMessageInfo(mi)
|
|
||||||
}
|
|
||||||
return ms
|
|
||||||
}
|
|
||||||
return mi.MessageOf(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deprecated: Use NtlmResponse.ProtoReflect.Descriptor instead.
|
|
||||||
func (*NtlmResponse) Descriptor() ([]byte, []int) {
|
|
||||||
return file_auth_proto_rawDescGZIP(), []int{3}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *NtlmResponse) GetAuthenticated() bool {
|
|
||||||
if x != nil {
|
|
||||||
return x.Authenticated
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *NtlmResponse) GetUsername() string {
|
|
||||||
if x != nil {
|
|
||||||
return x.Username
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *NtlmResponse) GetNtlmMessage() string {
|
|
||||||
if x != nil {
|
|
||||||
return x.NtlmMessage
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *NtlmResponse) GetError() string {
|
|
||||||
if x != nil {
|
|
||||||
return x.Error
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
var File_auth_proto protoreflect.FileDescriptor
|
var File_auth_proto protoreflect.FileDescriptor
|
||||||
|
|
||||||
var file_auth_proto_rawDesc = []byte{
|
var file_auth_proto_rawDesc = []byte{
|
||||||
@ -269,29 +143,12 @@ var file_auth_proto_rawDesc = []byte{
|
|||||||
0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x61,
|
0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x61,
|
||||||
0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05,
|
0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05,
|
||||||
0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72,
|
0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72,
|
||||||
0x6f, 0x72, 0x22, 0x49, 0x0a, 0x0b, 0x4e, 0x74, 0x6c, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
|
0x6f, 0x72, 0x32, 0x44, 0x0a, 0x0c, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61,
|
||||||
0x74, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01,
|
0x74, 0x65, 0x12, 0x34, 0x0a, 0x0c, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61,
|
||||||
0x28, 0x09, 0x52, 0x07, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x0a, 0x0b, 0x6e,
|
0x74, 0x65, 0x12, 0x0e, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x50, 0x61,
|
||||||
0x74, 0x6c, 0x6d, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
|
0x73, 0x73, 0x1a, 0x12, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x52, 0x65,
|
||||||
0x52, 0x0b, 0x6e, 0x74, 0x6c, 0x6d, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x88, 0x01,
|
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2e, 0x2f, 0x61, 0x75,
|
||||||
0x0a, 0x0c, 0x4e, 0x74, 0x6c, 0x6d, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24,
|
0x74, 0x68, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||||
0x0a, 0x0d, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x18,
|
|
||||||
0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63,
|
|
||||||
0x61, 0x74, 0x65, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65,
|
|
||||||
0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65,
|
|
||||||
0x12, 0x20, 0x0a, 0x0b, 0x6e, 0x74, 0x6c, 0x6d, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18,
|
|
||||||
0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6e, 0x74, 0x6c, 0x6d, 0x4d, 0x65, 0x73, 0x73, 0x61,
|
|
||||||
0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28,
|
|
||||||
0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x32, 0x75, 0x0a, 0x0c, 0x41, 0x75, 0x74, 0x68,
|
|
||||||
0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x34, 0x0a, 0x0c, 0x41, 0x75, 0x74, 0x68,
|
|
||||||
0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x2e,
|
|
||||||
0x55, 0x73, 0x65, 0x72, 0x50, 0x61, 0x73, 0x73, 0x1a, 0x12, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x2e,
|
|
||||||
0x41, 0x75, 0x74, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x2f,
|
|
||||||
0x0a, 0x04, 0x4e, 0x54, 0x4c, 0x4d, 0x12, 0x11, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x4e, 0x74,
|
|
||||||
0x6c, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x61, 0x75, 0x74, 0x68,
|
|
||||||
0x2e, 0x4e, 0x74, 0x6c, 0x6d, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42,
|
|
||||||
0x08, 0x5a, 0x06, 0x2e, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f,
|
|
||||||
0x33,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -306,20 +163,16 @@ func file_auth_proto_rawDescGZIP() []byte {
|
|||||||
return file_auth_proto_rawDescData
|
return file_auth_proto_rawDescData
|
||||||
}
|
}
|
||||||
|
|
||||||
var file_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
|
var file_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
|
||||||
var file_auth_proto_goTypes = []interface{}{
|
var file_auth_proto_goTypes = []interface{}{
|
||||||
(*UserPass)(nil), // 0: auth.UserPass
|
(*UserPass)(nil), // 0: auth.UserPass
|
||||||
(*AuthResponse)(nil), // 1: auth.AuthResponse
|
(*AuthResponse)(nil), // 1: auth.AuthResponse
|
||||||
(*NtlmRequest)(nil), // 2: auth.NtlmRequest
|
|
||||||
(*NtlmResponse)(nil), // 3: auth.NtlmResponse
|
|
||||||
}
|
}
|
||||||
var file_auth_proto_depIdxs = []int32{
|
var file_auth_proto_depIdxs = []int32{
|
||||||
0, // 0: auth.Authenticate.Authenticate:input_type -> auth.UserPass
|
0, // 0: auth.Authenticate.Authenticate:input_type -> auth.UserPass
|
||||||
2, // 1: auth.Authenticate.NTLM:input_type -> auth.NtlmRequest
|
1, // 1: auth.Authenticate.Authenticate:output_type -> auth.AuthResponse
|
||||||
1, // 2: auth.Authenticate.Authenticate:output_type -> auth.AuthResponse
|
1, // [1:2] is the sub-list for method output_type
|
||||||
3, // 3: auth.Authenticate.NTLM:output_type -> auth.NtlmResponse
|
0, // [0:1] is the sub-list for method input_type
|
||||||
2, // [2:4] is the sub-list for method output_type
|
|
||||||
0, // [0:2] is the sub-list for method input_type
|
|
||||||
0, // [0:0] is the sub-list for extension type_name
|
0, // [0:0] is the sub-list for extension type_name
|
||||||
0, // [0:0] is the sub-list for extension extendee
|
0, // [0:0] is the sub-list for extension extendee
|
||||||
0, // [0:0] is the sub-list for field type_name
|
0, // [0:0] is the sub-list for field type_name
|
||||||
@ -355,30 +208,6 @@ func file_auth_proto_init() {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_auth_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
|
|
||||||
switch v := v.(*NtlmRequest); i {
|
|
||||||
case 0:
|
|
||||||
return &v.state
|
|
||||||
case 1:
|
|
||||||
return &v.sizeCache
|
|
||||||
case 2:
|
|
||||||
return &v.unknownFields
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
file_auth_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
|
|
||||||
switch v := v.(*NtlmResponse); i {
|
|
||||||
case 0:
|
|
||||||
return &v.state
|
|
||||||
case 1:
|
|
||||||
return &v.sizeCache
|
|
||||||
case 2:
|
|
||||||
return &v.unknownFields
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
type x struct{}
|
type x struct{}
|
||||||
out := protoimpl.TypeBuilder{
|
out := protoimpl.TypeBuilder{
|
||||||
@ -386,7 +215,7 @@ func file_auth_proto_init() {
|
|||||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
RawDescriptor: file_auth_proto_rawDesc,
|
RawDescriptor: file_auth_proto_rawDesc,
|
||||||
NumEnums: 0,
|
NumEnums: 0,
|
||||||
NumMessages: 4,
|
NumMessages: 2,
|
||||||
NumExtensions: 0,
|
NumExtensions: 0,
|
||||||
NumServices: 1,
|
NumServices: 1,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,4 +1,8 @@
|
|||||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// - protoc-gen-go-grpc v1.2.0
|
||||||
|
// - protoc v3.21.5
|
||||||
|
// source: auth.proto
|
||||||
|
|
||||||
package auth
|
package auth
|
||||||
|
|
||||||
@ -19,7 +23,6 @@ const _ = grpc.SupportPackageIsVersion7
|
|||||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||||
type AuthenticateClient interface {
|
type AuthenticateClient interface {
|
||||||
Authenticate(ctx context.Context, in *UserPass, opts ...grpc.CallOption) (*AuthResponse, error)
|
Authenticate(ctx context.Context, in *UserPass, opts ...grpc.CallOption) (*AuthResponse, error)
|
||||||
NTLM(ctx context.Context, in *NtlmRequest, opts ...grpc.CallOption) (*NtlmResponse, error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type authenticateClient struct {
|
type authenticateClient struct {
|
||||||
@ -39,35 +42,20 @@ func (c *authenticateClient) Authenticate(ctx context.Context, in *UserPass, opt
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *authenticateClient) NTLM(ctx context.Context, in *NtlmRequest, opts ...grpc.CallOption) (*NtlmResponse, error) {
|
|
||||||
out := new(NtlmResponse)
|
|
||||||
err := c.cc.Invoke(ctx, "/auth.Authenticate/NTLM", in, out, opts...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// AuthenticateServer is the server API for Authenticate service.
|
// AuthenticateServer is the server API for Authenticate service.
|
||||||
// All implementations must embed UnimplementedAuthenticateServer
|
// All implementations should embed UnimplementedAuthenticateServer
|
||||||
// for forward compatibility
|
// for forward compatibility
|
||||||
type AuthenticateServer interface {
|
type AuthenticateServer interface {
|
||||||
Authenticate(context.Context, *UserPass) (*AuthResponse, error)
|
Authenticate(context.Context, *UserPass) (*AuthResponse, error)
|
||||||
NTLM(context.Context, *NtlmRequest) (*NtlmResponse, error)
|
|
||||||
mustEmbedUnimplementedAuthenticateServer()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnimplementedAuthenticateServer must be embedded to have forward compatible implementations.
|
// UnimplementedAuthenticateServer should be embedded to have forward compatible implementations.
|
||||||
type UnimplementedAuthenticateServer struct {
|
type UnimplementedAuthenticateServer struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (UnimplementedAuthenticateServer) Authenticate(context.Context, *UserPass) (*AuthResponse, error) {
|
func (UnimplementedAuthenticateServer) Authenticate(context.Context, *UserPass) (*AuthResponse, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method Authenticate not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method Authenticate not implemented")
|
||||||
}
|
}
|
||||||
func (UnimplementedAuthenticateServer) NTLM(context.Context, *NtlmRequest) (*NtlmResponse, error) {
|
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method NTLM not implemented")
|
|
||||||
}
|
|
||||||
func (UnimplementedAuthenticateServer) mustEmbedUnimplementedAuthenticateServer() {}
|
|
||||||
|
|
||||||
// UnsafeAuthenticateServer may be embedded to opt out of forward compatibility for this service.
|
// UnsafeAuthenticateServer may be embedded to opt out of forward compatibility for this service.
|
||||||
// Use of this interface is not recommended, as added methods to AuthenticateServer will
|
// Use of this interface is not recommended, as added methods to AuthenticateServer will
|
||||||
@ -98,24 +86,6 @@ func _Authenticate_Authenticate_Handler(srv interface{}, ctx context.Context, de
|
|||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
func _Authenticate_NTLM_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
|
||||||
in := new(NtlmRequest)
|
|
||||||
if err := dec(in); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if interceptor == nil {
|
|
||||||
return srv.(AuthenticateServer).NTLM(ctx, in)
|
|
||||||
}
|
|
||||||
info := &grpc.UnaryServerInfo{
|
|
||||||
Server: srv,
|
|
||||||
FullMethod: "/auth.Authenticate/NTLM",
|
|
||||||
}
|
|
||||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
|
||||||
return srv.(AuthenticateServer).NTLM(ctx, req.(*NtlmRequest))
|
|
||||||
}
|
|
||||||
return interceptor(ctx, in, info, handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Authenticate_ServiceDesc is the grpc.ServiceDesc for Authenticate service.
|
// Authenticate_ServiceDesc is the grpc.ServiceDesc for Authenticate service.
|
||||||
// It's only intended for direct use with grpc.RegisterService,
|
// It's only intended for direct use with grpc.RegisterService,
|
||||||
// and not to be introspected or modified (even as a copy)
|
// and not to be introspected or modified (even as a copy)
|
||||||
@ -127,10 +97,6 @@ var Authenticate_ServiceDesc = grpc.ServiceDesc{
|
|||||||
MethodName: "Authenticate",
|
MethodName: "Authenticate",
|
||||||
Handler: _Authenticate_Authenticate_Handler,
|
Handler: _Authenticate_Authenticate_Handler,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
MethodName: "NTLM",
|
|
||||||
Handler: _Authenticate_NTLM_Handler,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
Streams: []grpc.StreamDesc{},
|
Streams: []grpc.StreamDesc{},
|
||||||
Metadata: "auth.proto",
|
Metadata: "auth.proto",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user