This is a new authentication system that covers all the features exposed by the server, including playback, API, metrics and PPROF, improves internal authentication by adding permissions, improves HTTP-based authentication by adding the ability to exclude certain actions from being authenticated, adds an additional method (JWT-based authentication).
This commit is contained in:
parent
765d29fc8e
commit
9c6ba7e2c7
|
@ -15,11 +15,11 @@ jobs:
|
|||
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: "1.19"
|
||||
go-version: "1.21"
|
||||
|
||||
- run: touch internal/servers/hls/hls.min.js
|
||||
|
||||
- uses: golangci/golangci-lint-action@v3
|
||||
- uses: golangci/golangci-lint-action@v4
|
||||
with:
|
||||
version: v1.55.0
|
||||
|
||||
|
|
181
README.md
181
README.md
|
@ -57,7 +57,7 @@ And can be recorded and played back with:
|
|||
* Serve multiple streams at once in separate paths
|
||||
* Record streams to disk
|
||||
* Playback recorded streams
|
||||
* Authenticate users; use internal or external authentication
|
||||
* Authenticate users
|
||||
* Redirect readers to other RTSP servers (load balancing)
|
||||
* Control the server through the Control API
|
||||
* Reload the configuration without disconnecting existing clients (hot reloading)
|
||||
|
@ -113,6 +113,9 @@ _rtsp-simple-server_ has been rebranded as _MediaMTX_. The reason is pretty obvi
|
|||
* [Other features](#other-features)
|
||||
* [Configuration](#configuration)
|
||||
* [Authentication](#authentication)
|
||||
* [Internal](#internal)
|
||||
* [HTTP-based](#http-based)
|
||||
* [JWT-based](#jwt-based)
|
||||
* [Encrypt the configuration](#encrypt-the-configuration)
|
||||
* [Remuxing, re-encoding, compression](#remuxing-re-encoding-compression)
|
||||
* [Record streams to disk](#record-streams-to-disk)
|
||||
|
@ -1028,31 +1031,44 @@ There are 3 ways to change the configuration:
|
|||
|
||||
### Authentication
|
||||
|
||||
Edit `mediamtx.yml` and set `publishUser` and `publishPass`:
|
||||
#### Internal
|
||||
|
||||
The server provides three way to authenticate users:
|
||||
* Internal: users are stored in the configuration file
|
||||
* HTTP-based: an external HTTP URL is contacted to perform authentication
|
||||
* JWT: an external identity server provides authentication through JWTs
|
||||
|
||||
The internal authentication method is the default one. Users are stored inside the configuration file, in this format:
|
||||
|
||||
```yml
|
||||
pathDefaults:
|
||||
publishUser: myuser
|
||||
publishPass: mypass
|
||||
authInternalUsers:
|
||||
# Username. 'any' means any user, including anonymous ones.
|
||||
- user: any
|
||||
# Password. Not used in case of 'any' user.
|
||||
pass:
|
||||
# IPs or networks allowed to use this user. An empty list means any IP.
|
||||
ips: []
|
||||
# List of permissions.
|
||||
permissions:
|
||||
# Available actions are: publish, read, playback, api, metrics, pprof.
|
||||
- action: publish
|
||||
# Paths can be set to further restrict access to a specific path.
|
||||
# An empty path means any path.
|
||||
# Regular expressions can be used by using a tilde as prefix.
|
||||
path:
|
||||
- action: read
|
||||
path:
|
||||
- action: playback
|
||||
path:
|
||||
```
|
||||
|
||||
Only publishers that provide both username and password will be able to proceed:
|
||||
Only clients that provide username and passwords will be able to perform a given action:
|
||||
|
||||
```
|
||||
ffmpeg -re -stream_loop -1 -i file.ts -c copy -f rtsp rtsp://myuser:mypass@localhost:8554/mystream
|
||||
```
|
||||
|
||||
It's possible to setup authentication for readers too:
|
||||
|
||||
```yml
|
||||
pathDefaults:
|
||||
readUser: myuser
|
||||
readPass: mypass
|
||||
```
|
||||
|
||||
If storing plain credentials in the configuration file is a security problem, username and passwords can be stored as hashed strings. The Argon2 and SHA256 hashing algorithms are supported.
|
||||
|
||||
To use Argon2, the string must be hashed using Argon2id (recommended) or Argon2i:
|
||||
If storing plain credentials in the configuration file is a security problem, username and passwords can be stored as hashed strings. The Argon2 and SHA256 hashing algorithms are supported. To use Argon2, the string must be hashed using Argon2id (recommended) or Argon2i:
|
||||
|
||||
```
|
||||
echo -n "mypass" | argon2 saltItWithSalt -id -l 32 -e
|
||||
|
@ -1061,9 +1077,11 @@ echo -n "mypass" | argon2 saltItWithSalt -id -l 32 -e
|
|||
Then stored with the `argon2:` prefix:
|
||||
|
||||
```yml
|
||||
pathDefaults:
|
||||
readUser: argon2:$argon2id$v=19$m=4096,t=3,p=1$MTIzNDU2Nzg$OGGO0eCMN0ievb4YGSzvS/H+Vajx1pcbUmtLp2tRqRU
|
||||
readPass: argon2:$argon2i$v=19$m=4096,t=3,p=1$MTIzNDU2Nzg$oct3kOiFywTdDdt19kT07hdvmsPTvt9zxAUho2DLqZw
|
||||
authInternalUsers:
|
||||
- user: argon2:$argon2id$v=19$m=4096,t=3,p=1$MTIzNDU2Nzg$OGGO0eCMN0ievb4YGSzvS/H+Vajx1pcbUmtLp2tRqRU
|
||||
pass: argon2:$argon2i$v=19$m=4096,t=3,p=1$MTIzNDU2Nzg$oct3kOiFywTdDdt19kT07hdvmsPTvt9zxAUho2DLqZw
|
||||
permissions:
|
||||
- action: publish
|
||||
```
|
||||
|
||||
To use SHA256, the string must be hashed with SHA256 and encoded with base64:
|
||||
|
@ -1075,16 +1093,21 @@ echo -n "mypass" | openssl dgst -binary -sha256 | openssl base64
|
|||
Then stored with the `sha256:` prefix:
|
||||
|
||||
```yml
|
||||
pathDefaults:
|
||||
readUser: sha256:j1tsRqDEw9xvq/D7/9tMx6Jh/jMhk3UfjwIB2f1zgMo=
|
||||
readPass: sha256:BdSWkrdV+ZxFBLUQQY7+7uv9RmiSVA8nrPmjGjJtZQQ=
|
||||
authInternalUsers:
|
||||
- user: sha256:j1tsRqDEw9xvq/D7/9tMx6Jh/jMhk3UfjwIB2f1zgMo=
|
||||
pass: sha256:BdSWkrdV+ZxFBLUQQY7+7uv9RmiSVA8nrPmjGjJtZQQ=
|
||||
permissions:
|
||||
- action: publish
|
||||
```
|
||||
|
||||
**WARNING**: enable encryption or use a VPN to ensure that no one is intercepting the credentials in transit.
|
||||
|
||||
#### HTTP-based
|
||||
|
||||
Authentication can be delegated to an external HTTP server:
|
||||
|
||||
```yml
|
||||
authMethod: http
|
||||
externalAuthenticationURL: http://myauthserver/auth
|
||||
```
|
||||
|
||||
|
@ -1092,20 +1115,18 @@ Each time a user needs to be authenticated, the specified URL will be requested
|
|||
|
||||
```json
|
||||
{
|
||||
"ip": "ip",
|
||||
"user": "user",
|
||||
"password": "password",
|
||||
"ip": "ip",
|
||||
"action": "publish|read|playback|api|metrics|pprof",
|
||||
"path": "path",
|
||||
"protocol": "rtsp|rtmp|hls|webrtc",
|
||||
"protocol": "rtsp|rtmp|hls|webrtc|srt",
|
||||
"id": "id",
|
||||
"action": "read|publish",
|
||||
"query": "query"
|
||||
}
|
||||
```
|
||||
|
||||
If the URL returns a status code that begins with `20` (i.e. `200`), authentication is successful, otherwise it fails.
|
||||
|
||||
Please be aware that it's perfectly normal for the authentication server to receive requests with empty users and passwords, i.e.:
|
||||
If the URL returns a status code that begins with `20` (i.e. `200`), authentication is successful, otherwise it fails. Be aware that it's perfectly normal for the authentication server to receive requests with empty users and passwords, i.e.:
|
||||
|
||||
```json
|
||||
{
|
||||
|
@ -1114,7 +1135,107 @@ Please be aware that it's perfectly normal for the authentication server to rece
|
|||
}
|
||||
```
|
||||
|
||||
This happens because a RTSP client doesn't provide credentials until it is asked to. In order to receive the credentials, the authentication server must reply with status code `401`, then the client will send credentials.
|
||||
This happens because RTSP clients don't provide credentials until they are asked to. In order to receive the credentials, the authentication server must reply with status code `401`, then the client will send credentials.
|
||||
|
||||
Some actions can be excluded from the process:
|
||||
|
||||
```yml
|
||||
# Actions to exclude from HTTP-based authentication.
|
||||
# Format is the same as the one of user permissions.
|
||||
authHTTPExclude:
|
||||
- action: api
|
||||
- action: metrics
|
||||
- action: pprof
|
||||
```
|
||||
|
||||
#### JWT-based
|
||||
|
||||
Authentication can be delegated to an external identity server, that is capable of generating JWTs and provides a JWKS endpoint. With respect to the HTTP-based method, this has the advantage that the external server is contacted just once, and not for every request, greatly improving performance. In order to use the JWT-based authentication method, set `authMethod` and `authJWTJWKS`:
|
||||
|
||||
```yml
|
||||
authMethod: jwt
|
||||
authJWTJWKS: http://my_identity_server/jwks_endpoint
|
||||
```
|
||||
|
||||
The JWT is expected to contain the `mediamtx_permissions` scope, with a list of permissions in the same format as the one of user permissions:
|
||||
|
||||
```json
|
||||
{
|
||||
"mediamtx_permissions": [
|
||||
{
|
||||
"action": "publish",
|
||||
"path": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Clients are expected to pass the JWT in query parameters, for instance:
|
||||
|
||||
```
|
||||
ffmpeg -re -stream_loop -1 -i file.ts -c copy -f rtsp rtsp://localhost:8554/mystream?jwt=MY_JWT
|
||||
```
|
||||
|
||||
Here's a tutorial on how to setup the [Keycloak identity server](https://www.keycloak.org/) in order to provide such JWTs:
|
||||
|
||||
1. Start Keycloak:
|
||||
|
||||
```
|
||||
docker run --name=keycloak -p 8080:8080 -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:23.0.7 start-dev
|
||||
```
|
||||
|
||||
2. Open the Keycloak administration console on http://localhost:8080, click on _master_ in the top left corner, _create realm_, set realm name to `mediamtx`, Save
|
||||
|
||||
3. Open page _Client scopes_, _create client scope_, set name to `mediamtx`, Save
|
||||
|
||||
4. Open tab _Mappers_, _Configure a new Mapper_, _User Attribute_
|
||||
|
||||
* Name: `mediamtx_permissions`
|
||||
* User Attribute: `mediamtx_permissions`
|
||||
* Token Claim Name: `mediamtx_permissions`
|
||||
* Claim JSON Type: `JSON`
|
||||
* Multivalued: `On`
|
||||
|
||||
Save
|
||||
|
||||
5. Open page _Clients_, _Create client_, set Client ID to `mediamtx`, Next, Client authentication `On`, Next, Save
|
||||
|
||||
6. Open tab _Credentials_, copy client secret somewhere
|
||||
|
||||
7. Open tab _Client scopes_, _Add client scope_, Select `mediamtx`, Add, Default
|
||||
|
||||
8. Open page _Users_, _Create user_, Username `testuser`, Tab credentials, _Set password_, pick a password, Save
|
||||
|
||||
9. Open tab _Attributes_, _Add an attribute_
|
||||
|
||||
* Key: `mediamtx_permissions`
|
||||
* Value: `{"action":"publish", "paths": "all"}`
|
||||
|
||||
You can add as many attributes with key `mediamtx_permissions` as you want, each with a single permission in it
|
||||
|
||||
10. In MediaMTX, use the following URL:
|
||||
|
||||
```yml
|
||||
authJWTJWKS: http://localhost:8080/realms/mediamtx/protocol/openid-connect/certs
|
||||
```
|
||||
|
||||
11. Perform authentication on Keycloak:
|
||||
|
||||
```
|
||||
curl \
|
||||
-d "client_id=mediamtx" \
|
||||
-d "client_secret=$CLIENT_SECRET" \
|
||||
-d "username=$USER" \
|
||||
-d "password=$PASS" \
|
||||
-d "grant_type=password" \
|
||||
http://localhost:8080/realms/mediamtx/protocol/openid-connect/token
|
||||
```
|
||||
|
||||
The JWT is inside the `access_token` key of the response:
|
||||
|
||||
```json
|
||||
{"access_token":"eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIyNzVjX3ptOVlOdHQ0TkhwWVk4Und6ZndUclVGSzRBRmQwY3lsM2wtY3pzIn0.eyJleHAiOjE3MDk1NTUwOTIsImlhdCI6MTcwOTU1NDc5MiwianRpIjoiMzE3ZTQ1NGUtNzczMi00OTM1LWExNzAtOTNhYzQ2ODhhYWIxIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL3JlYWxtcy9tZWRpYW10eCIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiI2NTBhZDA5Zi03MDgxLTQyNGItODI4Ni0xM2I3YTA3ZDI0MWEiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJtZWRpYW10eCIsInNlc3Npb25fc3RhdGUiOiJjYzJkNDhjYy1kMmU5LTQ0YjAtODkzZS0wYTdhNjJiZDI1YmQiLCJhY3IiOiIxIiwiYWxsb3dlZC1vcmlnaW5zIjpbIi8qIl0sInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIiwiZGVmYXVsdC1yb2xlcy1tZWRpYW10eCJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoibWVkaWFtdHggcHJvZmlsZSBlbWFpbCIsInNpZCI6ImNjMmQ0OGNjLWQyZTktNDRiMC04OTNlLTBhN2E2MmJkMjViZCIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwibWVkaWFtdHhfcGVybWlzc2lvbnMiOlt7ImFjdGlvbiI6InB1Ymxpc2giLCJwYXRocyI6ImFsbCJ9XSwicHJlZmVycmVkX3VzZXJuYW1lIjoidGVzdHVzZXIifQ.Gevz7rf1qHqFg7cqtSfSP31v_NS0VH7MYfwAdra1t6Yt5rTr9vJzqUeGfjYLQWR3fr4XC58DrPOhNnILCpo7jWRdimCnbPmuuCJ0AYM-Aoi3PAsWZNxgmtopq24_JokbFArY9Y1wSGFvF8puU64lt1jyOOyxf2M4cBHCs_EarCKOwuQmEZxSf8Z-QV9nlfkoTUszDCQTiKyeIkLRHL2Iy7Fw7_T3UI7sxJjVIt0c6HCNJhBBazGsYzmcSQ_GrmhbUteMTg00o6FicqkMBe99uZFnx9wIBm_QbO9hbAkkzF923I-DTAQrFLxT08ESMepDwmzFrmnwWYBLE3u8zuUlCA","expires_in":300,"refresh_expires_in":1800,"refresh_token":"eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI3OTI3Zjg4Zi05YWM4LTRlNmEtYWE1OC1kZmY0MDQzZDRhNGUifQ.eyJleHAiOjE3MDk1NTY1OTIsImlhdCI6MTcwOTU1NDc5MiwianRpIjoiMGVhZWFhMWItYzNhMC00M2YxLWJkZjAtZjI2NTRiODlkOTE3IiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL3JlYWxtcy9tZWRpYW10eCIsImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9yZWFsbXMvbWVkaWFtdHgiLCJzdWIiOiI2NTBhZDA5Zi03MDgxLTQyNGItODI4Ni0xM2I3YTA3ZDI0MWEiLCJ0eXAiOiJSZWZyZXNoIiwiYXpwIjoibWVkaWFtdHgiLCJzZXNzaW9uX3N0YXRlIjoiY2MyZDQ4Y2MtZDJlOS00NGIwLTg5M2UtMGE3YTYyYmQyNWJkIiwic2NvcGUiOiJtZWRpYW10eCBwcm9maWxlIGVtYWlsIiwic2lkIjoiY2MyZDQ4Y2MtZDJlOS00NGIwLTg5M2UtMGE3YTYyYmQyNWJkIn0.yuXV8_JU0TQLuosNdp5xlYMjn7eO5Xq-PusdHzE7bsQ","token_type":"Bearer","not-before-policy":0,"session_state":"cc2d48cc-d2e9-44b0-893e-0a7a62bd25bd","scope":"mediamtx profile email"}
|
||||
```
|
||||
|
||||
### Encrypt the configuration
|
||||
|
||||
|
|
|
@ -97,7 +97,7 @@ components:
|
|||
type: string
|
||||
serverCert:
|
||||
type: string
|
||||
authMethods:
|
||||
rtspAuthMethods:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
|
|
4
go.mod
4
go.mod
|
@ -4,6 +4,7 @@ go 1.21
|
|||
|
||||
require (
|
||||
code.cloudfoundry.org/bytefmt v0.0.0
|
||||
github.com/MicahParks/keyfunc/v3 v3.2.5
|
||||
github.com/abema/go-mp4 v1.2.0
|
||||
github.com/alecthomas/kong v0.8.1
|
||||
github.com/bluenviron/gohlslib v1.2.2
|
||||
|
@ -12,6 +13,7 @@ require (
|
|||
github.com/datarhei/gosrt v0.5.7
|
||||
github.com/fsnotify/fsnotify v1.7.0
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gookit/color v1.5.4
|
||||
github.com/gorilla/websocket v1.5.1
|
||||
|
@ -32,6 +34,7 @@ require (
|
|||
)
|
||||
|
||||
require (
|
||||
github.com/MicahParks/jwkset v0.5.12 // indirect
|
||||
github.com/asticode/go-astikit v0.30.0 // indirect
|
||||
github.com/asticode/go-astits v1.13.0 // indirect
|
||||
github.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c // indirect
|
||||
|
@ -67,6 +70,7 @@ require (
|
|||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/net v0.21.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
google.golang.org/protobuf v1.30.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
8
go.sum
8
go.sum
|
@ -1,3 +1,7 @@
|
|||
github.com/MicahParks/jwkset v0.5.12 h1:wEwKZXB77yHFIHBtYoawNKIUwqC1X24S8tIhWutJHMA=
|
||||
github.com/MicahParks/jwkset v0.5.12/go.mod h1:q8ptTGn/Z9c4MwbcfeCDssADeVQb3Pk7PnVxrvi+2QY=
|
||||
github.com/MicahParks/keyfunc/v3 v3.2.5 h1:eg4s2zd2nfadnAzAsv9xvJCdCfLNy4s/aSiAxRn+aAk=
|
||||
github.com/MicahParks/keyfunc/v3 v3.2.5/go.mod h1:8hmM7h/hNerfF8uC8cFVnT+afxBgh6nKRTR/0vAm5So=
|
||||
github.com/abema/go-mp4 v1.2.0 h1:gi4X8xg/m179N/J15Fn5ugywN9vtI6PLk6iLldHGLAk=
|
||||
github.com/abema/go-mp4 v1.2.0/go.mod h1:vPl9t5ZK7K0x68jh12/+ECWBCXoWuIDtNgPtU2f04ws=
|
||||
github.com/alecthomas/assert/v2 v2.1.0 h1:tbredtNcQnoSd3QBhQWI7QZ3XHOVkw1Moklp2ojoH/0=
|
||||
|
@ -57,6 +61,8 @@ github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QX
|
|||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
|
@ -285,6 +291,8 @@ golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
|||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"reflect"
|
||||
|
@ -17,6 +18,7 @@ import (
|
|||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/bluenviron/mediamtx/internal/auth"
|
||||
"github.com/bluenviron/mediamtx/internal/conf"
|
||||
"github.com/bluenviron/mediamtx/internal/defs"
|
||||
"github.com/bluenviron/mediamtx/internal/logger"
|
||||
|
@ -159,6 +161,7 @@ type API struct {
|
|||
Address string
|
||||
ReadTimeout conf.StringDuration
|
||||
Conf *conf.Conf
|
||||
AuthManager *auth.Manager
|
||||
PathManager PathManager
|
||||
RTSPServer RTSPServer
|
||||
RTSPSServer RTSPServer
|
||||
|
@ -178,7 +181,7 @@ func (a *API) Initialize() error {
|
|||
router := gin.New()
|
||||
router.SetTrustedProxies(nil) //nolint:errcheck
|
||||
|
||||
group := router.Group("/")
|
||||
group := router.Group("/", a.mwAuth)
|
||||
|
||||
group.GET("/v3/config/global/get", a.onConfigGlobalGet)
|
||||
group.PATCH("/v3/config/global/patch", a.onConfigGlobalPatch)
|
||||
|
@ -287,6 +290,30 @@ func (a *API) writeError(ctx *gin.Context, status int, err error) {
|
|||
})
|
||||
}
|
||||
|
||||
func (a *API) mwAuth(ctx *gin.Context) {
|
||||
user, pass, hasCredentials := ctx.Request.BasicAuth()
|
||||
|
||||
err := a.AuthManager.Authenticate(&auth.Request{
|
||||
User: user,
|
||||
Pass: pass,
|
||||
IP: net.ParseIP(ctx.ClientIP()),
|
||||
Action: conf.AuthActionAPI,
|
||||
})
|
||||
if err != nil {
|
||||
if !hasCredentials {
|
||||
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
|
||||
ctx.AbortWithStatus(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// wait some seconds to mitigate brute force attacks
|
||||
<-time.After(auth.PauseAfterError)
|
||||
|
||||
ctx.AbortWithStatus(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (a *API) onConfigGlobalGet(ctx *gin.Context) {
|
||||
a.mutex.RLock()
|
||||
c := a.Conf
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/bluenviron/mediamtx/internal/auth"
|
||||
"github.com/bluenviron/mediamtx/internal/conf"
|
||||
"github.com/bluenviron/mediamtx/internal/logger"
|
||||
"github.com/bluenviron/mediamtx/internal/test"
|
||||
|
@ -105,6 +106,22 @@ func TestPaginate(t *testing.T) {
|
|||
require.Equal(t, []int{4, 5}, items)
|
||||
}
|
||||
|
||||
var authManager = &auth.Manager{
|
||||
Method: conf.AuthMethodInternal,
|
||||
InternalUsers: []conf.AuthInternalUser{
|
||||
{
|
||||
User: "myuser",
|
||||
Pass: "mypass",
|
||||
Permissions: []conf.AuthInternalUserPermission{
|
||||
{
|
||||
Action: conf.AuthActionAPI,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
RTSPAuthMethods: nil,
|
||||
}
|
||||
|
||||
func TestConfigGlobalGet(t *testing.T) {
|
||||
cnf := tempConf(t, "api: yes\n")
|
||||
|
||||
|
@ -112,6 +129,7 @@ func TestConfigGlobalGet(t *testing.T) {
|
|||
Address: "localhost:9997",
|
||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
||||
Conf: cnf,
|
||||
AuthManager: authManager,
|
||||
Parent: &testParent{},
|
||||
}
|
||||
err := api.Initialize()
|
||||
|
@ -121,7 +139,7 @@ func TestConfigGlobalGet(t *testing.T) {
|
|||
hc := &http.Client{Transport: &http.Transport{}}
|
||||
|
||||
var out map[string]interface{}
|
||||
httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/config/global/get", nil, &out)
|
||||
httpRequest(t, hc, http.MethodGet, "http://myuser:mypass@localhost:9997/v3/config/global/get", nil, &out)
|
||||
require.Equal(t, true, out["api"])
|
||||
}
|
||||
|
||||
|
@ -132,6 +150,7 @@ func TestConfigGlobalPatch(t *testing.T) {
|
|||
Address: "localhost:9997",
|
||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
||||
Conf: cnf,
|
||||
AuthManager: authManager,
|
||||
Parent: &testParent{},
|
||||
}
|
||||
err := api.Initialize()
|
||||
|
@ -140,17 +159,18 @@ func TestConfigGlobalPatch(t *testing.T) {
|
|||
|
||||
hc := &http.Client{Transport: &http.Transport{}}
|
||||
|
||||
httpRequest(t, hc, http.MethodPatch, "http://localhost:9997/v3/config/global/patch", map[string]interface{}{
|
||||
"rtmp": false,
|
||||
"readTimeout": "7s",
|
||||
"protocols": []string{"tcp"},
|
||||
"readBufferCount": 4096, // test setting a deprecated parameter
|
||||
}, nil)
|
||||
httpRequest(t, hc, http.MethodPatch, "http://myuser:mypass@localhost:9997/v3/config/global/patch",
|
||||
map[string]interface{}{
|
||||
"rtmp": false,
|
||||
"readTimeout": "7s",
|
||||
"protocols": []string{"tcp"},
|
||||
"readBufferCount": 4096, // test setting a deprecated parameter
|
||||
}, nil)
|
||||
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
var out map[string]interface{}
|
||||
httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/config/global/get", nil, &out)
|
||||
httpRequest(t, hc, http.MethodGet, "http://myuser:mypass@localhost:9997/v3/config/global/get", nil, &out)
|
||||
require.Equal(t, false, out["rtmp"])
|
||||
require.Equal(t, "7s", out["readTimeout"])
|
||||
require.Equal(t, []interface{}{"tcp"}, out["protocols"])
|
||||
|
@ -164,6 +184,7 @@ func TestAPIConfigGlobalPatchUnknownField(t *testing.T) { //nolint:dupl
|
|||
Address: "localhost:9997",
|
||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
||||
Conf: cnf,
|
||||
AuthManager: authManager,
|
||||
Parent: &testParent{},
|
||||
}
|
||||
err := api.Initialize()
|
||||
|
@ -179,7 +200,8 @@ func TestAPIConfigGlobalPatchUnknownField(t *testing.T) { //nolint:dupl
|
|||
|
||||
hc := &http.Client{Transport: &http.Transport{}}
|
||||
|
||||
req, err := http.NewRequest(http.MethodPatch, "http://localhost:9997/v3/config/global/patch", bytes.NewReader(byts))
|
||||
req, err := http.NewRequest(http.MethodPatch, "http://myuser:mypass@localhost:9997/v3/config/global/patch",
|
||||
bytes.NewReader(byts))
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := hc.Do(req)
|
||||
|
@ -197,6 +219,7 @@ func TestAPIConfigPathDefaultsGet(t *testing.T) {
|
|||
Address: "localhost:9997",
|
||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
||||
Conf: cnf,
|
||||
AuthManager: authManager,
|
||||
Parent: &testParent{},
|
||||
}
|
||||
err := api.Initialize()
|
||||
|
@ -206,7 +229,7 @@ func TestAPIConfigPathDefaultsGet(t *testing.T) {
|
|||
hc := &http.Client{Transport: &http.Transport{}}
|
||||
|
||||
var out map[string]interface{}
|
||||
httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/config/pathdefaults/get", nil, &out)
|
||||
httpRequest(t, hc, http.MethodGet, "http://myuser:mypass@localhost:9997/v3/config/pathdefaults/get", nil, &out)
|
||||
require.Equal(t, "publisher", out["source"])
|
||||
}
|
||||
|
||||
|
@ -217,6 +240,7 @@ func TestAPIConfigPathDefaultsPatch(t *testing.T) {
|
|||
Address: "localhost:9997",
|
||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
||||
Conf: cnf,
|
||||
AuthManager: authManager,
|
||||
Parent: &testParent{},
|
||||
}
|
||||
err := api.Initialize()
|
||||
|
@ -225,15 +249,16 @@ func TestAPIConfigPathDefaultsPatch(t *testing.T) {
|
|||
|
||||
hc := &http.Client{Transport: &http.Transport{}}
|
||||
|
||||
httpRequest(t, hc, http.MethodPatch, "http://localhost:9997/v3/config/pathdefaults/patch", map[string]interface{}{
|
||||
"readUser": "myuser",
|
||||
"readPass": "mypass",
|
||||
}, nil)
|
||||
httpRequest(t, hc, http.MethodPatch, "http://myuser:mypass@localhost:9997/v3/config/pathdefaults/patch",
|
||||
map[string]interface{}{
|
||||
"readUser": "myuser",
|
||||
"readPass": "mypass",
|
||||
}, nil)
|
||||
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
var out map[string]interface{}
|
||||
httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/config/pathdefaults/get", nil, &out)
|
||||
httpRequest(t, hc, http.MethodGet, "http://myuser:mypass@localhost:9997/v3/config/pathdefaults/get", nil, &out)
|
||||
require.Equal(t, "myuser", out["readUser"])
|
||||
require.Equal(t, "mypass", out["readPass"])
|
||||
}
|
||||
|
@ -252,6 +277,7 @@ func TestAPIConfigPathsList(t *testing.T) {
|
|||
Address: "localhost:9997",
|
||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
||||
Conf: cnf,
|
||||
AuthManager: authManager,
|
||||
Parent: &testParent{},
|
||||
}
|
||||
err := api.Initialize()
|
||||
|
@ -269,7 +295,7 @@ func TestAPIConfigPathsList(t *testing.T) {
|
|||
hc := &http.Client{Transport: &http.Transport{}}
|
||||
|
||||
var out listRes
|
||||
httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/config/paths/list", nil, &out)
|
||||
httpRequest(t, hc, http.MethodGet, "http://myuser:mypass@localhost:9997/v3/config/paths/list", nil, &out)
|
||||
require.Equal(t, 2, out.ItemCount)
|
||||
require.Equal(t, 1, out.PageCount)
|
||||
require.Equal(t, "path1", out.Items[0]["name"])
|
||||
|
@ -291,6 +317,7 @@ func TestAPIConfigPathsGet(t *testing.T) {
|
|||
Address: "localhost:9997",
|
||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
||||
Conf: cnf,
|
||||
AuthManager: authManager,
|
||||
Parent: &testParent{},
|
||||
}
|
||||
err := api.Initialize()
|
||||
|
@ -300,7 +327,7 @@ func TestAPIConfigPathsGet(t *testing.T) {
|
|||
hc := &http.Client{Transport: &http.Transport{}}
|
||||
|
||||
var out map[string]interface{}
|
||||
httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/config/paths/get/my/path", nil, &out)
|
||||
httpRequest(t, hc, http.MethodGet, "http://myuser:mypass@localhost:9997/v3/config/paths/get/my/path", nil, &out)
|
||||
require.Equal(t, "my/path", out["name"])
|
||||
require.Equal(t, "myuser", out["readUser"])
|
||||
}
|
||||
|
@ -312,6 +339,7 @@ func TestAPIConfigPathsAdd(t *testing.T) {
|
|||
Address: "localhost:9997",
|
||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
||||
Conf: cnf,
|
||||
AuthManager: authManager,
|
||||
Parent: &testParent{},
|
||||
}
|
||||
err := api.Initialize()
|
||||
|
@ -320,15 +348,16 @@ func TestAPIConfigPathsAdd(t *testing.T) {
|
|||
|
||||
hc := &http.Client{Transport: &http.Transport{}}
|
||||
|
||||
httpRequest(t, hc, http.MethodPost, "http://localhost:9997/v3/config/paths/add/my/path", map[string]interface{}{
|
||||
"source": "rtsp://127.0.0.1:9999/mypath",
|
||||
"sourceOnDemand": true,
|
||||
"disablePublisherOverride": true, // test setting a deprecated parameter
|
||||
"rpiCameraVFlip": true,
|
||||
}, nil)
|
||||
httpRequest(t, hc, http.MethodPost, "http://myuser:mypass@localhost:9997/v3/config/paths/add/my/path",
|
||||
map[string]interface{}{
|
||||
"source": "rtsp://127.0.0.1:9999/mypath",
|
||||
"sourceOnDemand": true,
|
||||
"disablePublisherOverride": true, // test setting a deprecated parameter
|
||||
"rpiCameraVFlip": true,
|
||||
}, nil)
|
||||
|
||||
var out map[string]interface{}
|
||||
httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/config/paths/get/my/path", nil, &out)
|
||||
httpRequest(t, hc, http.MethodGet, "http://myuser:mypass@localhost:9997/v3/config/paths/get/my/path", nil, &out)
|
||||
require.Equal(t, "rtsp://127.0.0.1:9999/mypath", out["source"])
|
||||
require.Equal(t, true, out["sourceOnDemand"])
|
||||
require.Equal(t, true, out["disablePublisherOverride"])
|
||||
|
@ -342,6 +371,7 @@ func TestAPIConfigPathsAddUnknownField(t *testing.T) { //nolint:dupl
|
|||
Address: "localhost:9997",
|
||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
||||
Conf: cnf,
|
||||
AuthManager: authManager,
|
||||
Parent: &testParent{},
|
||||
}
|
||||
err := api.Initialize()
|
||||
|
@ -358,7 +388,7 @@ func TestAPIConfigPathsAddUnknownField(t *testing.T) { //nolint:dupl
|
|||
hc := &http.Client{Transport: &http.Transport{}}
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost,
|
||||
"http://localhost:9997/v3/config/paths/add/my/path", bytes.NewReader(byts))
|
||||
"http://myuser:mypass@localhost:9997/v3/config/paths/add/my/path", bytes.NewReader(byts))
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := hc.Do(req)
|
||||
|
@ -376,6 +406,7 @@ func TestAPIConfigPathsPatch(t *testing.T) { //nolint:dupl
|
|||
Address: "localhost:9997",
|
||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
||||
Conf: cnf,
|
||||
AuthManager: authManager,
|
||||
Parent: &testParent{},
|
||||
}
|
||||
err := api.Initialize()
|
||||
|
@ -384,20 +415,22 @@ func TestAPIConfigPathsPatch(t *testing.T) { //nolint:dupl
|
|||
|
||||
hc := &http.Client{Transport: &http.Transport{}}
|
||||
|
||||
httpRequest(t, hc, http.MethodPost, "http://localhost:9997/v3/config/paths/add/my/path", map[string]interface{}{
|
||||
"source": "rtsp://127.0.0.1:9999/mypath",
|
||||
"sourceOnDemand": true,
|
||||
"disablePublisherOverride": true, // test setting a deprecated parameter
|
||||
"rpiCameraVFlip": true,
|
||||
}, nil)
|
||||
httpRequest(t, hc, http.MethodPost, "http://myuser:mypass@localhost:9997/v3/config/paths/add/my/path",
|
||||
map[string]interface{}{
|
||||
"source": "rtsp://127.0.0.1:9999/mypath",
|
||||
"sourceOnDemand": true,
|
||||
"disablePublisherOverride": true, // test setting a deprecated parameter
|
||||
"rpiCameraVFlip": true,
|
||||
}, nil)
|
||||
|
||||
httpRequest(t, hc, http.MethodPatch, "http://localhost:9997/v3/config/paths/patch/my/path", map[string]interface{}{
|
||||
"source": "rtsp://127.0.0.1:9998/mypath",
|
||||
"sourceOnDemand": true,
|
||||
}, nil)
|
||||
httpRequest(t, hc, http.MethodPatch, "http://myuser:mypass@localhost:9997/v3/config/paths/patch/my/path",
|
||||
map[string]interface{}{
|
||||
"source": "rtsp://127.0.0.1:9998/mypath",
|
||||
"sourceOnDemand": true,
|
||||
}, nil)
|
||||
|
||||
var out map[string]interface{}
|
||||
httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/config/paths/get/my/path", nil, &out)
|
||||
httpRequest(t, hc, http.MethodGet, "http://myuser:mypass@localhost:9997/v3/config/paths/get/my/path", nil, &out)
|
||||
require.Equal(t, "rtsp://127.0.0.1:9998/mypath", out["source"])
|
||||
require.Equal(t, true, out["sourceOnDemand"])
|
||||
require.Equal(t, true, out["disablePublisherOverride"])
|
||||
|
@ -411,6 +444,7 @@ func TestAPIConfigPathsReplace(t *testing.T) { //nolint:dupl
|
|||
Address: "localhost:9997",
|
||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
||||
Conf: cnf,
|
||||
AuthManager: authManager,
|
||||
Parent: &testParent{},
|
||||
}
|
||||
err := api.Initialize()
|
||||
|
@ -419,20 +453,22 @@ func TestAPIConfigPathsReplace(t *testing.T) { //nolint:dupl
|
|||
|
||||
hc := &http.Client{Transport: &http.Transport{}}
|
||||
|
||||
httpRequest(t, hc, http.MethodPost, "http://localhost:9997/v3/config/paths/add/my/path", map[string]interface{}{
|
||||
"source": "rtsp://127.0.0.1:9999/mypath",
|
||||
"sourceOnDemand": true,
|
||||
"disablePublisherOverride": true, // test setting a deprecated parameter
|
||||
"rpiCameraVFlip": true,
|
||||
}, nil)
|
||||
httpRequest(t, hc, http.MethodPost, "http://myuser:mypass@localhost:9997/v3/config/paths/add/my/path",
|
||||
map[string]interface{}{
|
||||
"source": "rtsp://127.0.0.1:9999/mypath",
|
||||
"sourceOnDemand": true,
|
||||
"disablePublisherOverride": true, // test setting a deprecated parameter
|
||||
"rpiCameraVFlip": true,
|
||||
}, nil)
|
||||
|
||||
httpRequest(t, hc, http.MethodPost, "http://localhost:9997/v3/config/paths/replace/my/path", map[string]interface{}{
|
||||
"source": "rtsp://127.0.0.1:9998/mypath",
|
||||
"sourceOnDemand": true,
|
||||
}, nil)
|
||||
httpRequest(t, hc, http.MethodPost, "http://myuser:mypass@localhost:9997/v3/config/paths/replace/my/path",
|
||||
map[string]interface{}{
|
||||
"source": "rtsp://127.0.0.1:9998/mypath",
|
||||
"sourceOnDemand": true,
|
||||
}, nil)
|
||||
|
||||
var out map[string]interface{}
|
||||
httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/config/paths/get/my/path", nil, &out)
|
||||
httpRequest(t, hc, http.MethodGet, "http://myuser:mypass@localhost:9997/v3/config/paths/get/my/path", nil, &out)
|
||||
require.Equal(t, "rtsp://127.0.0.1:9998/mypath", out["source"])
|
||||
require.Equal(t, true, out["sourceOnDemand"])
|
||||
require.Equal(t, nil, out["disablePublisherOverride"])
|
||||
|
@ -446,6 +482,7 @@ func TestAPIConfigPathsDelete(t *testing.T) {
|
|||
Address: "localhost:9997",
|
||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
||||
Conf: cnf,
|
||||
AuthManager: authManager,
|
||||
Parent: &testParent{},
|
||||
}
|
||||
err := api.Initialize()
|
||||
|
@ -454,14 +491,15 @@ func TestAPIConfigPathsDelete(t *testing.T) {
|
|||
|
||||
hc := &http.Client{Transport: &http.Transport{}}
|
||||
|
||||
httpRequest(t, hc, http.MethodPost, "http://localhost:9997/v3/config/paths/add/my/path", map[string]interface{}{
|
||||
"source": "rtsp://127.0.0.1:9999/mypath",
|
||||
"sourceOnDemand": true,
|
||||
}, nil)
|
||||
httpRequest(t, hc, http.MethodPost, "http://myuser:mypass@localhost:9997/v3/config/paths/add/my/path",
|
||||
map[string]interface{}{
|
||||
"source": "rtsp://127.0.0.1:9999/mypath",
|
||||
"sourceOnDemand": true,
|
||||
}, nil)
|
||||
|
||||
httpRequest(t, hc, http.MethodDelete, "http://localhost:9997/v3/config/paths/delete/my/path", nil, nil)
|
||||
httpRequest(t, hc, http.MethodDelete, "http://myuser:mypass@localhost:9997/v3/config/paths/delete/my/path", nil, nil)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, "http://localhost:9997/v3/config/paths/get/my/path", nil)
|
||||
req, err := http.NewRequest(http.MethodGet, "http://myuser:mypass@localhost:9997/v3/config/paths/get/my/path", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := hc.Do(req)
|
||||
|
@ -486,6 +524,7 @@ func TestRecordingsList(t *testing.T) {
|
|||
Address: "localhost:9997",
|
||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
||||
Conf: cnf,
|
||||
AuthManager: authManager,
|
||||
Parent: &testParent{},
|
||||
}
|
||||
err = api.Initialize()
|
||||
|
@ -510,7 +549,7 @@ func TestRecordingsList(t *testing.T) {
|
|||
hc := &http.Client{Transport: &http.Transport{}}
|
||||
|
||||
var out interface{}
|
||||
httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/recordings/list", nil, &out)
|
||||
httpRequest(t, hc, http.MethodGet, "http://myuser:mypass@localhost:9997/v3/recordings/list", nil, &out)
|
||||
require.Equal(t, map[string]interface{}{
|
||||
"itemCount": float64(2),
|
||||
"pageCount": float64(1),
|
||||
|
@ -552,6 +591,7 @@ func TestRecordingsGet(t *testing.T) {
|
|||
Address: "localhost:9997",
|
||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
||||
Conf: cnf,
|
||||
AuthManager: authManager,
|
||||
Parent: &testParent{},
|
||||
}
|
||||
err = api.Initialize()
|
||||
|
@ -570,7 +610,7 @@ func TestRecordingsGet(t *testing.T) {
|
|||
hc := &http.Client{Transport: &http.Transport{}}
|
||||
|
||||
var out interface{}
|
||||
httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/recordings/get/mypath1", nil, &out)
|
||||
httpRequest(t, hc, http.MethodGet, "http://myuser:mypass@localhost:9997/v3/recordings/get/mypath1", nil, &out)
|
||||
require.Equal(t, map[string]interface{}{
|
||||
"name": "mypath1",
|
||||
"segments": []interface{}{
|
||||
|
@ -598,6 +638,7 @@ func TestRecordingsDeleteSegment(t *testing.T) {
|
|||
Address: "localhost:9997",
|
||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
||||
Conf: cnf,
|
||||
AuthManager: authManager,
|
||||
Parent: &testParent{},
|
||||
}
|
||||
err = api.Initialize()
|
||||
|
@ -612,16 +653,13 @@ func TestRecordingsDeleteSegment(t *testing.T) {
|
|||
|
||||
hc := &http.Client{Transport: &http.Transport{}}
|
||||
|
||||
u, err := url.Parse("http://myuser:mypass@localhost:9997/v3/recordings/deletesegment")
|
||||
require.NoError(t, err)
|
||||
|
||||
v := url.Values{}
|
||||
v.Set("path", "mypath1")
|
||||
v.Set("start", time.Date(2008, 11, 0o7, 11, 22, 0, 900000000, time.Local).Format(time.RFC3339Nano))
|
||||
|
||||
u := &url.URL{
|
||||
Scheme: "http",
|
||||
Host: "localhost:9997",
|
||||
Path: "/v3/recordings/deletesegment",
|
||||
RawQuery: v.Encode(),
|
||||
}
|
||||
u.RawQuery = v.Encode()
|
||||
|
||||
req, err := http.NewRequest(http.MethodDelete, u.String(), nil)
|
||||
require.NoError(t, err)
|
||||
|
|
|
@ -0,0 +1,327 @@
|
|||
// Package auth contains the authentication system.
|
||||
package auth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/MicahParks/keyfunc/v3"
|
||||
"github.com/bluenviron/gortsplib/v4/pkg/auth"
|
||||
"github.com/bluenviron/gortsplib/v4/pkg/base"
|
||||
"github.com/bluenviron/gortsplib/v4/pkg/headers"
|
||||
"github.com/bluenviron/mediamtx/internal/conf"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
// PauseAfterError is the pause to apply after an authentication failure.
|
||||
PauseAfterError = 2 * time.Second
|
||||
|
||||
rtspAuthRealm = "IPCAM"
|
||||
jwtRefreshPeriod = 60 * 60 * time.Second
|
||||
)
|
||||
|
||||
// Protocol is a protocol.
|
||||
type Protocol string
|
||||
|
||||
// protocols.
|
||||
const (
|
||||
ProtocolRTSP Protocol = "rtsp"
|
||||
ProtocolRTMP Protocol = "rtmp"
|
||||
ProtocolHLS Protocol = "hls"
|
||||
ProtocolWebRTC Protocol = "webrtc"
|
||||
ProtocolSRT Protocol = "srt"
|
||||
)
|
||||
|
||||
// Request is an authentication request.
|
||||
type Request struct {
|
||||
User string
|
||||
Pass string
|
||||
IP net.IP
|
||||
Action conf.AuthAction
|
||||
|
||||
// only for ActionPublish, ActionRead, ActionPlayback
|
||||
Path string
|
||||
Protocol Protocol
|
||||
ID *uuid.UUID
|
||||
Query string
|
||||
RTSPRequest *base.Request
|
||||
RTSPBaseURL *base.URL
|
||||
RTSPNonce string
|
||||
}
|
||||
|
||||
// Error is a authentication error.
|
||||
type Error struct {
|
||||
Message string
|
||||
}
|
||||
|
||||
// Error implements the error interface.
|
||||
func (e Error) Error() string {
|
||||
return "authentication failed: " + e.Message
|
||||
}
|
||||
|
||||
func matchesPermission(perms []conf.AuthInternalUserPermission, req *Request) bool {
|
||||
for _, perm := range perms {
|
||||
if perm.Action == req.Action {
|
||||
if perm.Action == conf.AuthActionPublish ||
|
||||
perm.Action == conf.AuthActionRead ||
|
||||
perm.Action == conf.AuthActionPlayback {
|
||||
switch {
|
||||
case perm.Path == "":
|
||||
return true
|
||||
|
||||
case strings.HasPrefix(perm.Path, "~"):
|
||||
regexp, err := regexp.Compile(perm.Path[1:])
|
||||
if err == nil && regexp.MatchString(req.Path) {
|
||||
return true
|
||||
}
|
||||
|
||||
case perm.Path == req.Path:
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
type customClaims struct {
|
||||
jwt.RegisteredClaims
|
||||
MediaMTXPermissions []conf.AuthInternalUserPermission `json:"mediamtx_permissions"`
|
||||
}
|
||||
|
||||
// Manager is the authentication manager.
|
||||
type Manager struct {
|
||||
Method conf.AuthMethod
|
||||
InternalUsers []conf.AuthInternalUser
|
||||
HTTPAddress string
|
||||
HTTPExclude []conf.AuthInternalUserPermission
|
||||
JWTJWKS string
|
||||
ReadTimeout time.Duration
|
||||
RTSPAuthMethods []headers.AuthMethod
|
||||
|
||||
mutex sync.RWMutex
|
||||
jwtHTTPClient *http.Client
|
||||
jwtLastRefresh time.Time
|
||||
jwtKeyFunc keyfunc.Keyfunc
|
||||
}
|
||||
|
||||
// ReloadInternalUsers reloads InternalUsers.
|
||||
func (m *Manager) ReloadInternalUsers(u []conf.AuthInternalUser) {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
m.InternalUsers = u
|
||||
}
|
||||
|
||||
// Authenticate authenticates a request.
|
||||
func (m *Manager) Authenticate(req *Request) error {
|
||||
err := m.authenticateInner(req)
|
||||
if err != nil {
|
||||
return Error{Message: err.Error()}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) authenticateInner(req *Request) error {
|
||||
// if this is a RTSP request, fill username and password
|
||||
var rtspAuthHeader headers.Authorization
|
||||
if req.RTSPRequest != nil {
|
||||
err := rtspAuthHeader.Unmarshal(req.RTSPRequest.Header["Authorization"])
|
||||
if err == nil {
|
||||
switch rtspAuthHeader.Method {
|
||||
case headers.AuthBasic:
|
||||
req.User = rtspAuthHeader.BasicUser
|
||||
req.Pass = rtspAuthHeader.BasicPass
|
||||
|
||||
case headers.AuthDigestMD5:
|
||||
req.User = rtspAuthHeader.Username
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unsupported RTSP authentication method")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch m.Method {
|
||||
case conf.AuthMethodInternal:
|
||||
return m.authenticateInternal(req, &rtspAuthHeader)
|
||||
|
||||
case conf.AuthMethodHTTP:
|
||||
return m.authenticateHTTP(req)
|
||||
|
||||
default:
|
||||
return m.authenticateJWT(req)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) authenticateInternal(req *Request, rtspAuthHeader *headers.Authorization) error {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
for _, u := range m.InternalUsers {
|
||||
if err := m.authenticateWithUser(req, rtspAuthHeader, &u); err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("authentication failed")
|
||||
}
|
||||
|
||||
func (m *Manager) authenticateWithUser(
|
||||
req *Request,
|
||||
rtspAuthHeader *headers.Authorization,
|
||||
u *conf.AuthInternalUser,
|
||||
) error {
|
||||
if u.User != "any" && !u.User.Check(req.User) {
|
||||
return fmt.Errorf("wrong user")
|
||||
}
|
||||
|
||||
if len(u.IPs) != 0 && !u.IPs.Contains(req.IP) {
|
||||
return fmt.Errorf("IP not allowed")
|
||||
}
|
||||
|
||||
if !matchesPermission(u.Permissions, req) {
|
||||
return fmt.Errorf("user doesn't have permission to perform action")
|
||||
}
|
||||
|
||||
if u.User != "any" {
|
||||
if req.RTSPRequest != nil && rtspAuthHeader.Method == headers.AuthDigestMD5 {
|
||||
err := auth.Validate(
|
||||
req.RTSPRequest,
|
||||
string(u.User),
|
||||
string(u.Pass),
|
||||
req.RTSPBaseURL,
|
||||
m.RTSPAuthMethods,
|
||||
rtspAuthRealm,
|
||||
req.RTSPNonce)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if !u.Pass.Check(req.Pass) {
|
||||
return fmt.Errorf("invalid credentials")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) authenticateHTTP(req *Request) error {
|
||||
if matchesPermission(m.HTTPExclude, req) {
|
||||
return nil
|
||||
}
|
||||
|
||||
enc, _ := json.Marshal(struct {
|
||||
IP string `json:"ip"`
|
||||
User string `json:"user"`
|
||||
Password string `json:"password"`
|
||||
Action string `json:"action"`
|
||||
Path string `json:"path"`
|
||||
Protocol string `json:"protocol"`
|
||||
ID *uuid.UUID `json:"id"`
|
||||
Query string `json:"query"`
|
||||
}{
|
||||
IP: req.IP.String(),
|
||||
User: req.User,
|
||||
Password: req.Pass,
|
||||
Action: string(req.Action),
|
||||
Path: req.Path,
|
||||
Protocol: string(req.Protocol),
|
||||
ID: req.ID,
|
||||
Query: req.Query,
|
||||
})
|
||||
|
||||
res, err := http.Post(m.HTTPAddress, "application/json", bytes.NewReader(enc))
|
||||
if err != nil {
|
||||
return fmt.Errorf("HTTP request failed: %w", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode < 200 || res.StatusCode > 299 {
|
||||
if resBody, err := io.ReadAll(res.Body); err == nil && len(resBody) != 0 {
|
||||
return fmt.Errorf("server replied with code %d: %s", res.StatusCode, string(resBody))
|
||||
}
|
||||
|
||||
return fmt.Errorf("server replied with code %d", res.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) authenticateJWT(req *Request) error {
|
||||
keyfunc, err := m.pullJWTJWKS()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
v, err := url.ParseQuery(req.Query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(v["jwt"]) != 1 {
|
||||
return fmt.Errorf("JWT not provided")
|
||||
}
|
||||
|
||||
var customClaims customClaims
|
||||
_, err = jwt.ParseWithClaims(v["jwt"][0], &customClaims, keyfunc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !matchesPermission(customClaims.MediaMTXPermissions, req) {
|
||||
return fmt.Errorf("user doesn't have permission to perform action")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) pullJWTJWKS() (jwt.Keyfunc, error) {
|
||||
now := time.Now()
|
||||
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
if now.Sub(m.jwtLastRefresh) >= jwtRefreshPeriod {
|
||||
if m.jwtHTTPClient == nil {
|
||||
m.jwtHTTPClient = &http.Client{
|
||||
Timeout: (m.ReadTimeout),
|
||||
Transport: &http.Transport{},
|
||||
}
|
||||
}
|
||||
|
||||
res, err := m.jwtHTTPClient.Get(m.JWTJWKS)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
var raw json.RawMessage
|
||||
err = json.NewDecoder(res.Body).Decode(&raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tmp, err := keyfunc.NewJWKSetJSON(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m.jwtKeyFunc = tmp
|
||||
m.jwtLastRefresh = now
|
||||
}
|
||||
|
||||
return m.jwtKeyFunc.Keyfunc, nil
|
||||
}
|
|
@ -0,0 +1,309 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/bluenviron/gortsplib/v4/pkg/auth"
|
||||
"github.com/bluenviron/gortsplib/v4/pkg/base"
|
||||
"github.com/bluenviron/gortsplib/v4/pkg/headers"
|
||||
"github.com/bluenviron/mediamtx/internal/conf"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func mustParseCIDR(v string) net.IPNet {
|
||||
_, ne, err := net.ParseCIDR(v)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if ipv4 := ne.IP.To4(); ipv4 != nil {
|
||||
return net.IPNet{IP: ipv4, Mask: ne.Mask[len(ne.Mask)-4 : len(ne.Mask)]}
|
||||
}
|
||||
return *ne
|
||||
}
|
||||
|
||||
type testHTTPAuthenticator struct {
|
||||
*http.Server
|
||||
}
|
||||
|
||||
func (ts *testHTTPAuthenticator) initialize(t *testing.T, protocol string, action string) {
|
||||
firstReceived := false
|
||||
|
||||
ts.Server = &http.Server{
|
||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
require.Equal(t, "/auth", r.URL.Path)
|
||||
|
||||
var in struct {
|
||||
IP string `json:"ip"`
|
||||
User string `json:"user"`
|
||||
Password string `json:"password"`
|
||||
Path string `json:"path"`
|
||||
Protocol string `json:"protocol"`
|
||||
ID string `json:"id"`
|
||||
Action string `json:"action"`
|
||||
Query string `json:"query"`
|
||||
}
|
||||
err := json.NewDecoder(r.Body).Decode(&in)
|
||||
require.NoError(t, err)
|
||||
|
||||
var user string
|
||||
if action == "publish" {
|
||||
user = "testpublisher"
|
||||
} else {
|
||||
user = "testreader"
|
||||
}
|
||||
|
||||
if in.IP != "127.0.0.1" ||
|
||||
in.User != user ||
|
||||
in.Password != "testpass" ||
|
||||
in.Path != "teststream" ||
|
||||
in.Protocol != protocol ||
|
||||
(firstReceived && in.ID == "") ||
|
||||
in.Action != action ||
|
||||
(in.Query != "user=testreader&pass=testpass¶m=value" &&
|
||||
in.Query != "user=testpublisher&pass=testpass¶m=value" &&
|
||||
in.Query != "param=value") {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
firstReceived = true
|
||||
}),
|
||||
}
|
||||
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:9120")
|
||||
require.NoError(t, err)
|
||||
|
||||
go ts.Server.Serve(ln)
|
||||
}
|
||||
|
||||
func (ts *testHTTPAuthenticator) close() {
|
||||
ts.Server.Shutdown(context.Background())
|
||||
}
|
||||
|
||||
func TestAuthInternal(t *testing.T) {
|
||||
for _, outcome := range []string{
|
||||
"ok",
|
||||
"wrong user",
|
||||
"wrong pass",
|
||||
"wrong ip",
|
||||
"wrong action",
|
||||
"wrong path",
|
||||
} {
|
||||
for _, encryption := range []string{
|
||||
"plain",
|
||||
"sha256",
|
||||
"argon2",
|
||||
} {
|
||||
t.Run(outcome+" "+encryption, func(t *testing.T) {
|
||||
m := Manager{
|
||||
Method: conf.AuthMethodInternal,
|
||||
InternalUsers: []conf.AuthInternalUser{
|
||||
{
|
||||
IPs: conf.IPNetworks{mustParseCIDR("127.1.1.1/32")},
|
||||
Permissions: []conf.AuthInternalUserPermission{
|
||||
{
|
||||
Action: conf.AuthActionPublish,
|
||||
Path: "mypath",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
HTTPAddress: "",
|
||||
RTSPAuthMethods: nil,
|
||||
}
|
||||
|
||||
switch encryption {
|
||||
case "plain":
|
||||
m.InternalUsers[0].User = conf.Credential("testuser")
|
||||
m.InternalUsers[0].Pass = conf.Credential("testpass")
|
||||
|
||||
case "sha256":
|
||||
m.InternalUsers[0].User = conf.Credential("sha256:rl3rgi4NcZkpAEcacZnQ2VuOfJ0FxAqCRaKB/SwdZoQ=")
|
||||
m.InternalUsers[0].Pass = conf.Credential("sha256:E9JJ8stBJ7QM+nV4ZoUCeHk/gU3tPFh/5YieiJp6n2w=")
|
||||
|
||||
case "argon2":
|
||||
m.InternalUsers[0].User = conf.Credential(
|
||||
"argon2:$argon2id$v=19$m=4096,t=3,p=1$MTIzNDU2Nzg$Ux/LWeTgJQPyfMMJo1myR64+o8rALHoPmlE1i/TR+58")
|
||||
m.InternalUsers[0].Pass = conf.Credential(
|
||||
"argon2:$argon2i$v=19$m=4096,t=3,p=1$MTIzNDU2Nzg$/mrZ42TiTv1mcPnpMUera5oi0SFYbbyueAbdx5sUvWo")
|
||||
}
|
||||
|
||||
switch outcome {
|
||||
case "ok":
|
||||
err := m.Authenticate(&Request{
|
||||
User: "testuser",
|
||||
Pass: "testpass",
|
||||
IP: net.ParseIP("127.1.1.1"),
|
||||
Action: conf.AuthActionPublish,
|
||||
Path: "mypath",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
case "wrong user":
|
||||
err := m.Authenticate(&Request{
|
||||
User: "wrong",
|
||||
Pass: "testpass",
|
||||
IP: net.ParseIP("127.1.1.1"),
|
||||
Action: conf.AuthActionPublish,
|
||||
Path: "mypath",
|
||||
})
|
||||
require.Error(t, err)
|
||||
|
||||
case "wrong pass":
|
||||
err := m.Authenticate(&Request{
|
||||
User: "testuser",
|
||||
Pass: "wrong",
|
||||
IP: net.ParseIP("127.1.1.1"),
|
||||
Action: conf.AuthActionPublish,
|
||||
Path: "mypath",
|
||||
})
|
||||
require.Error(t, err)
|
||||
|
||||
case "wrong ip":
|
||||
err := m.Authenticate(&Request{
|
||||
User: "testuser",
|
||||
Pass: "testpass",
|
||||
IP: net.ParseIP("127.1.1.2"),
|
||||
Action: conf.AuthActionPublish,
|
||||
Path: "mypath",
|
||||
})
|
||||
require.Error(t, err)
|
||||
|
||||
case "wrong action":
|
||||
err := m.Authenticate(&Request{
|
||||
User: "testuser",
|
||||
Pass: "testpass",
|
||||
IP: net.ParseIP("127.1.1.1"),
|
||||
Action: conf.AuthActionRead,
|
||||
Path: "mypath",
|
||||
})
|
||||
require.Error(t, err)
|
||||
|
||||
case "wrong path":
|
||||
err := m.Authenticate(&Request{
|
||||
User: "testuser",
|
||||
Pass: "testpass",
|
||||
IP: net.ParseIP("127.1.1.1"),
|
||||
Action: conf.AuthActionPublish,
|
||||
Path: "wrong",
|
||||
})
|
||||
require.Error(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthInternalRTSPDigest(t *testing.T) {
|
||||
m := Manager{
|
||||
Method: conf.AuthMethodInternal,
|
||||
InternalUsers: []conf.AuthInternalUser{
|
||||
{
|
||||
User: "myuser",
|
||||
Pass: "mypass",
|
||||
IPs: conf.IPNetworks{mustParseCIDR("127.1.1.1/32")},
|
||||
Permissions: []conf.AuthInternalUserPermission{
|
||||
{
|
||||
Action: conf.AuthActionPublish,
|
||||
Path: "mypath",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
HTTPAddress: "",
|
||||
RTSPAuthMethods: []headers.AuthMethod{headers.AuthDigestMD5},
|
||||
}
|
||||
|
||||
u, err := base.ParseURL("rtsp://127.0.0.1:8554/mypath")
|
||||
require.NoError(t, err)
|
||||
|
||||
s, err := auth.NewSender(
|
||||
auth.GenerateWWWAuthenticate([]headers.AuthMethod{headers.AuthDigestMD5}, "IPCAM", "mynonce"),
|
||||
"myuser",
|
||||
"mypass",
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
req := &base.Request{
|
||||
Method: "ANNOUNCE",
|
||||
URL: u,
|
||||
}
|
||||
|
||||
s.AddAuthorization(req)
|
||||
|
||||
err = m.Authenticate(&Request{
|
||||
IP: net.ParseIP("127.1.1.1"),
|
||||
Action: conf.AuthActionPublish,
|
||||
Path: "mypath",
|
||||
RTSPRequest: req,
|
||||
RTSPNonce: "mynonce",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestAuthHTTP(t *testing.T) {
|
||||
for _, outcome := range []string{"ok", "fail"} {
|
||||
t.Run(outcome, func(t *testing.T) {
|
||||
m := Manager{
|
||||
Method: conf.AuthMethodHTTP,
|
||||
HTTPAddress: "http://127.0.0.1:9120/auth",
|
||||
RTSPAuthMethods: nil,
|
||||
}
|
||||
|
||||
au := &testHTTPAuthenticator{}
|
||||
au.initialize(t, "rtsp", "publish")
|
||||
defer au.close()
|
||||
|
||||
if outcome == "ok" {
|
||||
err := m.Authenticate(&Request{
|
||||
User: "testpublisher",
|
||||
Pass: "testpass",
|
||||
IP: net.ParseIP("127.0.0.1"),
|
||||
Action: conf.AuthActionPublish,
|
||||
Path: "teststream",
|
||||
Protocol: ProtocolRTSP,
|
||||
Query: "param=value",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
err := m.Authenticate(&Request{
|
||||
User: "invalid",
|
||||
Pass: "testpass",
|
||||
IP: net.ParseIP("127.0.0.1"),
|
||||
Action: conf.AuthActionPublish,
|
||||
Path: "teststream",
|
||||
Protocol: ProtocolRTSP,
|
||||
Query: "param=value",
|
||||
})
|
||||
require.Error(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthHTTPExclude(t *testing.T) {
|
||||
m := Manager{
|
||||
Method: conf.AuthMethodHTTP,
|
||||
HTTPAddress: "http://not-to-be-used:9120/auth",
|
||||
HTTPExclude: []conf.AuthInternalUserPermission{{
|
||||
Action: conf.AuthActionPublish,
|
||||
}},
|
||||
RTSPAuthMethods: nil,
|
||||
}
|
||||
|
||||
err := m.Authenticate(&Request{
|
||||
User: "",
|
||||
Pass: "",
|
||||
IP: net.ParseIP("127.0.0.1"),
|
||||
Action: conf.AuthActionPublish,
|
||||
Path: "teststream",
|
||||
Protocol: ProtocolRTSP,
|
||||
Query: "param=value",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
package conf
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// AuthAction is an authentication action.
|
||||
type AuthAction string
|
||||
|
||||
// auth actions
|
||||
const (
|
||||
AuthActionPublish AuthAction = "publish"
|
||||
AuthActionRead AuthAction = "read"
|
||||
AuthActionPlayback AuthAction = "playback"
|
||||
AuthActionAPI AuthAction = "api"
|
||||
AuthActionMetrics AuthAction = "metrics"
|
||||
AuthActionPprof AuthAction = "pprof"
|
||||
)
|
||||
|
||||
// MarshalJSON implements json.Marshaler.
|
||||
func (d AuthAction) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(string(d))
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements json.Unmarshaler.
|
||||
func (d *AuthAction) UnmarshalJSON(b []byte) error {
|
||||
var in string
|
||||
if err := json.Unmarshal(b, &in); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch in {
|
||||
case string(AuthActionPublish),
|
||||
string(AuthActionRead),
|
||||
string(AuthActionPlayback),
|
||||
string(AuthActionAPI),
|
||||
string(AuthActionMetrics),
|
||||
string(AuthActionPprof):
|
||||
*d = AuthAction(in)
|
||||
|
||||
default:
|
||||
return fmt.Errorf("invalid auth action: '%s'", in)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalEnv implements env.Unmarshaler.
|
||||
func (d *AuthAction) UnmarshalEnv(_ string, v string) error {
|
||||
return d.UnmarshalJSON([]byte(`"` + v + `"`))
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package conf
|
||||
|
||||
// AuthInternalUserPermission is a permission of a user.
|
||||
type AuthInternalUserPermission struct {
|
||||
Action AuthAction `json:"action"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
// AuthInternalUser is an user.
|
||||
type AuthInternalUser struct {
|
||||
User Credential `json:"user"`
|
||||
Pass Credential `json:"pass"`
|
||||
IPs IPNetworks `json:"ips"`
|
||||
Permissions []AuthInternalUserPermission `json:"permissions"`
|
||||
}
|
|
@ -3,64 +3,61 @@ package conf
|
|||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/bluenviron/gortsplib/v4/pkg/headers"
|
||||
)
|
||||
|
||||
// AuthMethods is the authMethods parameter.
|
||||
type AuthMethods []headers.AuthMethod
|
||||
// AuthMethod is an authentication method.
|
||||
type AuthMethod int
|
||||
|
||||
// authentication methods.
|
||||
const (
|
||||
AuthMethodInternal AuthMethod = iota
|
||||
AuthMethodHTTP
|
||||
AuthMethodJWT
|
||||
)
|
||||
|
||||
// MarshalJSON implements json.Marshaler.
|
||||
func (d AuthMethods) MarshalJSON() ([]byte, error) {
|
||||
out := make([]string, len(d))
|
||||
func (d AuthMethod) MarshalJSON() ([]byte, error) {
|
||||
var out string
|
||||
|
||||
for i, v := range d {
|
||||
switch v {
|
||||
case headers.AuthBasic:
|
||||
out[i] = "basic"
|
||||
switch d {
|
||||
case AuthMethodInternal:
|
||||
out = "internal"
|
||||
|
||||
case headers.AuthDigestMD5:
|
||||
out[i] = "digest"
|
||||
case AuthMethodHTTP:
|
||||
out = "http"
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid authentication method: %v", v)
|
||||
}
|
||||
default:
|
||||
out = "jwt"
|
||||
}
|
||||
|
||||
sort.Strings(out)
|
||||
|
||||
return json.Marshal(out)
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements json.Unmarshaler.
|
||||
func (d *AuthMethods) UnmarshalJSON(b []byte) error {
|
||||
var in []string
|
||||
func (d *AuthMethod) UnmarshalJSON(b []byte) error {
|
||||
var in string
|
||||
if err := json.Unmarshal(b, &in); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*d = nil
|
||||
switch in {
|
||||
case "internal":
|
||||
*d = AuthMethodInternal
|
||||
|
||||
for _, v := range in {
|
||||
switch v {
|
||||
case "basic":
|
||||
*d = append(*d, headers.AuthBasic)
|
||||
case "http":
|
||||
*d = AuthMethodHTTP
|
||||
|
||||
case "digest":
|
||||
*d = append(*d, headers.AuthDigestMD5)
|
||||
case "jwt":
|
||||
*d = AuthMethodJWT
|
||||
|
||||
default:
|
||||
return fmt.Errorf("invalid authentication method: '%s'", v)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("invalid authMethod: '%s'", in)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalEnv implements env.Unmarshaler.
|
||||
func (d *AuthMethods) UnmarshalEnv(_ string, v string) error {
|
||||
byts, _ := json.Marshal(strings.Split(v, ","))
|
||||
return d.UnmarshalJSON(byts)
|
||||
func (d *AuthMethod) UnmarshalEnv(_ string, v string) error {
|
||||
return d.UnmarshalJSON([]byte(`"` + v + `"`))
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"reflect"
|
||||
"sort"
|
||||
|
@ -82,25 +83,58 @@ func copyStructFields(dest interface{}, source interface{}) {
|
|||
}
|
||||
}
|
||||
|
||||
func mustParseCIDR(v string) net.IPNet {
|
||||
_, ne, err := net.ParseCIDR(v)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if ipv4 := ne.IP.To4(); ipv4 != nil {
|
||||
return net.IPNet{IP: ipv4, Mask: ne.Mask[len(ne.Mask)-4 : len(ne.Mask)]}
|
||||
}
|
||||
return *ne
|
||||
}
|
||||
|
||||
func anyPathHasDeprecatedCredentials(paths map[string]*OptionalPath) bool {
|
||||
for _, pa := range paths {
|
||||
if pa != nil {
|
||||
rva := reflect.ValueOf(pa.Values).Elem()
|
||||
if !rva.FieldByName("PublishUser").IsNil() || !rva.FieldByName("PublishPass").IsNil() ||
|
||||
!rva.FieldByName("PublishIPs").IsNil() ||
|
||||
!rva.FieldByName("ReadUser").IsNil() || !rva.FieldByName("ReadPass").IsNil() ||
|
||||
!rva.FieldByName("ReadIPs").IsNil() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Conf is a configuration.
|
||||
type Conf struct {
|
||||
// General
|
||||
LogLevel LogLevel `json:"logLevel"`
|
||||
LogDestinations LogDestinations `json:"logDestinations"`
|
||||
LogFile string `json:"logFile"`
|
||||
ReadTimeout StringDuration `json:"readTimeout"`
|
||||
WriteTimeout StringDuration `json:"writeTimeout"`
|
||||
ReadBufferCount *int `json:"readBufferCount,omitempty"` // deprecated
|
||||
WriteQueueSize int `json:"writeQueueSize"`
|
||||
UDPMaxPayloadSize int `json:"udpMaxPayloadSize"`
|
||||
ExternalAuthenticationURL string `json:"externalAuthenticationURL"`
|
||||
Metrics bool `json:"metrics"`
|
||||
MetricsAddress string `json:"metricsAddress"`
|
||||
PPROF bool `json:"pprof"`
|
||||
PPROFAddress string `json:"pprofAddress"`
|
||||
RunOnConnect string `json:"runOnConnect"`
|
||||
RunOnConnectRestart bool `json:"runOnConnectRestart"`
|
||||
RunOnDisconnect string `json:"runOnDisconnect"`
|
||||
LogLevel LogLevel `json:"logLevel"`
|
||||
LogDestinations LogDestinations `json:"logDestinations"`
|
||||
LogFile string `json:"logFile"`
|
||||
ReadTimeout StringDuration `json:"readTimeout"`
|
||||
WriteTimeout StringDuration `json:"writeTimeout"`
|
||||
ReadBufferCount *int `json:"readBufferCount,omitempty"` // deprecated
|
||||
WriteQueueSize int `json:"writeQueueSize"`
|
||||
UDPMaxPayloadSize int `json:"udpMaxPayloadSize"`
|
||||
Metrics bool `json:"metrics"`
|
||||
MetricsAddress string `json:"metricsAddress"`
|
||||
PPROF bool `json:"pprof"`
|
||||
PPROFAddress string `json:"pprofAddress"`
|
||||
RunOnConnect string `json:"runOnConnect"`
|
||||
RunOnConnectRestart bool `json:"runOnConnectRestart"`
|
||||
RunOnDisconnect string `json:"runOnDisconnect"`
|
||||
|
||||
// Authentication
|
||||
AuthMethod AuthMethod `json:"authMethod"`
|
||||
AuthInternalUsers []AuthInternalUser `json:"authInternalUsers"`
|
||||
AuthHTTPAddress string `json:"authHTTPAddress"`
|
||||
ExternalAuthenticationURL *string `json:"externalAuthenticationURL,omitempty"` // deprecated
|
||||
AuthHTTPExclude []AuthInternalUserPermission `json:"authHTTPExclude"`
|
||||
AuthJWTJWKS string `json:"authJWTJWKS"`
|
||||
|
||||
// API
|
||||
API bool `json:"api"`
|
||||
|
@ -111,20 +145,21 @@ type Conf struct {
|
|||
PlaybackAddress string `json:"playbackAddress"`
|
||||
|
||||
// RTSP server
|
||||
RTSP bool `json:"rtsp"`
|
||||
RTSPDisable *bool `json:"rtspDisable,omitempty"` // deprecated
|
||||
Protocols Protocols `json:"protocols"`
|
||||
Encryption Encryption `json:"encryption"`
|
||||
RTSPAddress string `json:"rtspAddress"`
|
||||
RTSPSAddress string `json:"rtspsAddress"`
|
||||
RTPAddress string `json:"rtpAddress"`
|
||||
RTCPAddress string `json:"rtcpAddress"`
|
||||
MulticastIPRange string `json:"multicastIPRange"`
|
||||
MulticastRTPPort int `json:"multicastRTPPort"`
|
||||
MulticastRTCPPort int `json:"multicastRTCPPort"`
|
||||
ServerKey string `json:"serverKey"`
|
||||
ServerCert string `json:"serverCert"`
|
||||
AuthMethods AuthMethods `json:"authMethods"`
|
||||
RTSP bool `json:"rtsp"`
|
||||
RTSPDisable *bool `json:"rtspDisable,omitempty"` // deprecated
|
||||
Protocols Protocols `json:"protocols"`
|
||||
Encryption Encryption `json:"encryption"`
|
||||
RTSPAddress string `json:"rtspAddress"`
|
||||
RTSPSAddress string `json:"rtspsAddress"`
|
||||
RTPAddress string `json:"rtpAddress"`
|
||||
RTCPAddress string `json:"rtcpAddress"`
|
||||
MulticastIPRange string `json:"multicastIPRange"`
|
||||
MulticastRTPPort int `json:"multicastRTPPort"`
|
||||
MulticastRTCPPort int `json:"multicastRTCPPort"`
|
||||
ServerKey string `json:"serverKey"`
|
||||
ServerCert string `json:"serverCert"`
|
||||
AuthMethods *RTSPAuthMethods `json:"authMethods,omitempty"` // deprecated
|
||||
RTSPAuthMethods RTSPAuthMethods `json:"rtspAuthMethods"`
|
||||
|
||||
// RTMP server
|
||||
RTMP bool `json:"rtmp"`
|
||||
|
@ -149,7 +184,7 @@ type Conf struct {
|
|||
HLSPartDuration StringDuration `json:"hlsPartDuration"`
|
||||
HLSSegmentMaxSize StringSize `json:"hlsSegmentMaxSize"`
|
||||
HLSAllowOrigin string `json:"hlsAllowOrigin"`
|
||||
HLSTrustedProxies IPsOrCIDRs `json:"hlsTrustedProxies"`
|
||||
HLSTrustedProxies IPNetworks `json:"hlsTrustedProxies"`
|
||||
HLSDirectory string `json:"hlsDirectory"`
|
||||
|
||||
// WebRTC server
|
||||
|
@ -160,7 +195,7 @@ type Conf struct {
|
|||
WebRTCServerKey string `json:"webrtcServerKey"`
|
||||
WebRTCServerCert string `json:"webrtcServerCert"`
|
||||
WebRTCAllowOrigin string `json:"webrtcAllowOrigin"`
|
||||
WebRTCTrustedProxies IPsOrCIDRs `json:"webrtcTrustedProxies"`
|
||||
WebRTCTrustedProxies IPNetworks `json:"webrtcTrustedProxies"`
|
||||
WebRTCLocalUDPAddress string `json:"webrtcLocalUDPAddress"`
|
||||
WebRTCLocalTCPAddress string `json:"webrtcLocalTCPAddress"`
|
||||
WebRTCIPsFromInterfaces bool `json:"webrtcIPsFromInterfaces"`
|
||||
|
@ -201,11 +236,57 @@ func (conf *Conf) setDefaults() {
|
|||
conf.WriteTimeout = 10 * StringDuration(time.Second)
|
||||
conf.WriteQueueSize = 512
|
||||
conf.UDPMaxPayloadSize = 1472
|
||||
conf.MetricsAddress = "127.0.0.1:9998"
|
||||
conf.PPROFAddress = "127.0.0.1:9999"
|
||||
conf.MetricsAddress = ":9998"
|
||||
conf.PPROFAddress = ":9999"
|
||||
|
||||
// Authentication
|
||||
conf.AuthInternalUsers = []AuthInternalUser{
|
||||
{
|
||||
User: "any",
|
||||
Pass: "",
|
||||
Permissions: []AuthInternalUserPermission{
|
||||
{
|
||||
Action: AuthActionPublish,
|
||||
},
|
||||
{
|
||||
Action: AuthActionRead,
|
||||
},
|
||||
{
|
||||
Action: AuthActionPlayback,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
User: "any",
|
||||
Pass: "",
|
||||
IPs: IPNetworks{mustParseCIDR("127.0.0.1/32"), mustParseCIDR("::1/128")},
|
||||
Permissions: []AuthInternalUserPermission{
|
||||
{
|
||||
Action: AuthActionAPI,
|
||||
},
|
||||
{
|
||||
Action: AuthActionMetrics,
|
||||
},
|
||||
{
|
||||
Action: AuthActionPprof,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
conf.AuthHTTPExclude = []AuthInternalUserPermission{
|
||||
{
|
||||
Action: AuthActionAPI,
|
||||
},
|
||||
{
|
||||
Action: AuthActionMetrics,
|
||||
},
|
||||
{
|
||||
Action: AuthActionPprof,
|
||||
},
|
||||
}
|
||||
|
||||
// API
|
||||
conf.APIAddress = "127.0.0.1:9997"
|
||||
conf.APIAddress = ":9997"
|
||||
|
||||
// Playback server
|
||||
conf.PlaybackAddress = ":9996"
|
||||
|
@ -226,7 +307,7 @@ func (conf *Conf) setDefaults() {
|
|||
conf.MulticastRTCPPort = 8003
|
||||
conf.ServerKey = "server.key"
|
||||
conf.ServerCert = "server.crt"
|
||||
conf.AuthMethods = AuthMethods{headers.AuthBasic}
|
||||
conf.RTSPAuthMethods = RTSPAuthMethods{headers.AuthBasic}
|
||||
|
||||
// RTMP server
|
||||
conf.RTMP = true
|
||||
|
@ -361,14 +442,67 @@ func (conf *Conf) Validate() error {
|
|||
if conf.UDPMaxPayloadSize > 1472 {
|
||||
return fmt.Errorf("'udpMaxPayloadSize' must be less than 1472")
|
||||
}
|
||||
if conf.ExternalAuthenticationURL != "" {
|
||||
if !strings.HasPrefix(conf.ExternalAuthenticationURL, "http://") &&
|
||||
!strings.HasPrefix(conf.ExternalAuthenticationURL, "https://") {
|
||||
return fmt.Errorf("'externalAuthenticationURL' must be a HTTP URL")
|
||||
|
||||
// Authentication
|
||||
|
||||
if conf.ExternalAuthenticationURL != nil {
|
||||
conf.AuthMethod = AuthMethodHTTP
|
||||
conf.AuthHTTPAddress = *conf.ExternalAuthenticationURL
|
||||
}
|
||||
if conf.AuthHTTPAddress != "" &&
|
||||
!strings.HasPrefix(conf.AuthHTTPAddress, "http://") &&
|
||||
!strings.HasPrefix(conf.AuthHTTPAddress, "https://") {
|
||||
return fmt.Errorf("'externalAuthenticationURL' must be a HTTP URL")
|
||||
}
|
||||
if conf.AuthJWTJWKS != "" &&
|
||||
!strings.HasPrefix(conf.AuthJWTJWKS, "http://") &&
|
||||
!strings.HasPrefix(conf.AuthJWTJWKS, "https://") {
|
||||
return fmt.Errorf("'authJWTJWKS' must be a HTTP URL")
|
||||
}
|
||||
deprecatedCredentialsMode := false
|
||||
if conf.PathDefaults.PublishUser != nil || conf.PathDefaults.PublishPass != nil ||
|
||||
conf.PathDefaults.PublishIPs != nil ||
|
||||
conf.PathDefaults.ReadUser != nil || conf.PathDefaults.ReadPass != nil ||
|
||||
conf.PathDefaults.ReadIPs != nil ||
|
||||
anyPathHasDeprecatedCredentials(conf.OptionalPaths) {
|
||||
conf.AuthInternalUsers = []AuthInternalUser{
|
||||
{
|
||||
User: "any",
|
||||
Pass: "",
|
||||
Permissions: []AuthInternalUserPermission{
|
||||
{
|
||||
Action: AuthActionPlayback,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
User: "any",
|
||||
Pass: "",
|
||||
IPs: IPNetworks{mustParseCIDR("127.0.0.1/32"), mustParseCIDR("::1/128")},
|
||||
Permissions: []AuthInternalUserPermission{
|
||||
{
|
||||
Action: AuthActionAPI,
|
||||
},
|
||||
{
|
||||
Action: AuthActionMetrics,
|
||||
},
|
||||
{
|
||||
Action: AuthActionPprof,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
deprecatedCredentialsMode = true
|
||||
}
|
||||
switch conf.AuthMethod {
|
||||
case AuthMethodHTTP:
|
||||
if conf.AuthHTTPAddress == "" {
|
||||
return fmt.Errorf("'authHTTPAddress' is empty")
|
||||
}
|
||||
|
||||
if contains(conf.AuthMethods, headers.AuthDigestMD5) {
|
||||
return fmt.Errorf("'externalAuthenticationURL' can't be used when 'digest' is in authMethods")
|
||||
case AuthMethodJWT:
|
||||
if conf.AuthJWTJWKS == "" {
|
||||
return fmt.Errorf("'authJWTJWKS' is empty")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -385,6 +519,19 @@ func (conf *Conf) Validate() error {
|
|||
return fmt.Errorf("strict encryption can't be used with the UDP-multicast transport protocol")
|
||||
}
|
||||
}
|
||||
if conf.AuthMethods != nil {
|
||||
conf.RTSPAuthMethods = *conf.AuthMethods
|
||||
}
|
||||
if contains(conf.RTSPAuthMethods, headers.AuthDigestMD5) {
|
||||
if conf.AuthMethod != AuthMethodInternal {
|
||||
return fmt.Errorf("when RTSP digest is enabled, the only supported auth method is 'internal'")
|
||||
}
|
||||
for _, user := range conf.AuthInternalUsers {
|
||||
if user.User.IsHashed() || user.Pass.IsHashed() {
|
||||
return fmt.Errorf("when RTSP digest is enabled, hashed credentials cannot be used")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RTMP
|
||||
|
||||
|
@ -490,7 +637,7 @@ func (conf *Conf) Validate() error {
|
|||
pconf := newPath(&conf.PathDefaults, optional)
|
||||
conf.Paths[name] = pconf
|
||||
|
||||
err := pconf.validate(conf, name)
|
||||
err := pconf.validate(conf, name, deprecatedCredentialsMode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -214,17 +214,6 @@ func TestConfErrors(t *testing.T) {
|
|||
"udpMaxPayloadSize: 5000\n",
|
||||
"'udpMaxPayloadSize' must be less than 1472",
|
||||
},
|
||||
{
|
||||
"invalid externalAuthenticationURL 1",
|
||||
"externalAuthenticationURL: testing\n",
|
||||
"'externalAuthenticationURL' must be a HTTP URL",
|
||||
},
|
||||
{
|
||||
"invalid externalAuthenticationURL 2",
|
||||
"externalAuthenticationURL: http://myurl\n" +
|
||||
"authMethods: [digest]\n",
|
||||
"'externalAuthenticationURL' can't be used when 'digest' is in authMethods",
|
||||
},
|
||||
{
|
||||
"invalid strict encryption 1",
|
||||
"encryption: strict\n" +
|
||||
|
|
|
@ -26,11 +26,8 @@ func (d Encryption) MarshalJSON() ([]byte, error) {
|
|||
case EncryptionOptional:
|
||||
out = "optional"
|
||||
|
||||
case EncryptionStrict:
|
||||
out = "strict"
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid encryption: %v", d)
|
||||
out = "strict"
|
||||
}
|
||||
|
||||
return json.Marshal(out)
|
||||
|
|
|
@ -21,11 +21,8 @@ func (d HLSVariant) MarshalJSON() ([]byte, error) {
|
|||
case HLSVariant(gohlslib.MuxerVariantFMP4):
|
||||
out = "fmp4"
|
||||
|
||||
case HLSVariant(gohlslib.MuxerVariantLowLatency):
|
||||
out = "lowLatency"
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid HLS variant: %v", d)
|
||||
out = "lowLatency"
|
||||
}
|
||||
|
||||
return json.Marshal(out)
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
package conf
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// IPNetworks is a parameter that contains a list of IP networks.
|
||||
type IPNetworks []net.IPNet
|
||||
|
||||
// MarshalJSON implements json.Marshaler.
|
||||
func (d IPNetworks) MarshalJSON() ([]byte, error) {
|
||||
out := make([]string, len(d))
|
||||
|
||||
for i, v := range d {
|
||||
out[i] = v.String()
|
||||
}
|
||||
|
||||
sort.Strings(out)
|
||||
|
||||
return json.Marshal(out)
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements json.Unmarshaler.
|
||||
func (d *IPNetworks) UnmarshalJSON(b []byte) error {
|
||||
var in []string
|
||||
if err := json.Unmarshal(b, &in); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*d = nil
|
||||
|
||||
if len(in) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, t := range in {
|
||||
if _, ipnet, err := net.ParseCIDR(t); err == nil {
|
||||
if ipv4 := ipnet.IP.To4(); ipv4 != nil {
|
||||
*d = append(*d, net.IPNet{IP: ipv4, Mask: ipnet.Mask[len(ipnet.Mask)-4 : len(ipnet.Mask)]})
|
||||
} else {
|
||||
*d = append(*d, *ipnet)
|
||||
}
|
||||
} else if ip := net.ParseIP(t); ip != nil {
|
||||
if ipv4 := ip.To4(); ipv4 != nil {
|
||||
*d = append(*d, net.IPNet{IP: ipv4, Mask: net.CIDRMask(32, 32)})
|
||||
} else {
|
||||
*d = append(*d, net.IPNet{IP: ip, Mask: net.CIDRMask(128, 128)})
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("unable to parse IP/CIDR '%s'", t)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalEnv implements env.Unmarshaler.
|
||||
func (d *IPNetworks) UnmarshalEnv(_ string, v string) error {
|
||||
byts, _ := json.Marshal(strings.Split(v, ","))
|
||||
return d.UnmarshalJSON(byts)
|
||||
}
|
||||
|
||||
// ToTrustedProxies converts IPNetworks into a string slice for SetTrustedProxies.
|
||||
func (d *IPNetworks) ToTrustedProxies() []string {
|
||||
ret := make([]string, len(*d))
|
||||
for i, entry := range *d {
|
||||
ret[i] = entry.String()
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// Contains checks whether the IP is part of one of the networks.
|
||||
func (d IPNetworks) Contains(ip net.IP) bool {
|
||||
for _, network := range d {
|
||||
if network.Contains(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
package conf
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// IPsOrCIDRs is a parameter that contains a list of IPs or CIDRs.
|
||||
type IPsOrCIDRs []fmt.Stringer
|
||||
|
||||
// MarshalJSON implements json.Marshaler.
|
||||
func (d IPsOrCIDRs) MarshalJSON() ([]byte, error) {
|
||||
out := make([]string, len(d))
|
||||
|
||||
for i, v := range d {
|
||||
out[i] = v.String()
|
||||
}
|
||||
|
||||
sort.Strings(out)
|
||||
|
||||
return json.Marshal(out)
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements json.Unmarshaler.
|
||||
func (d *IPsOrCIDRs) UnmarshalJSON(b []byte) error {
|
||||
var in []string
|
||||
if err := json.Unmarshal(b, &in); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*d = nil
|
||||
|
||||
if len(in) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, t := range in {
|
||||
if _, ipnet, err := net.ParseCIDR(t); err == nil {
|
||||
*d = append(*d, ipnet)
|
||||
} else if ip := net.ParseIP(t); ip != nil {
|
||||
*d = append(*d, ip)
|
||||
} else {
|
||||
return fmt.Errorf("unable to parse IP/CIDR '%s'", t)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalEnv implements env.Unmarshaler.
|
||||
func (d *IPsOrCIDRs) UnmarshalEnv(_ string, v string) error {
|
||||
byts, _ := json.Marshal(strings.Split(v, ","))
|
||||
return d.UnmarshalJSON(byts)
|
||||
}
|
||||
|
||||
// ToTrustedProxies converts IPsOrCIDRs into a string slice for SetTrustedProxies.
|
||||
func (d *IPsOrCIDRs) ToTrustedProxies() []string {
|
||||
ret := make([]string, len(*d))
|
||||
for i, entry := range *d {
|
||||
ret[i] = entry.String()
|
||||
}
|
||||
return ret
|
||||
}
|
|
@ -26,11 +26,8 @@ func (d LogDestinations) MarshalJSON() ([]byte, error) {
|
|||
case logger.DestinationFile:
|
||||
v = "file"
|
||||
|
||||
case logger.DestinationSyslog:
|
||||
v = "syslog"
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid log destination: %v", p)
|
||||
v = "syslog"
|
||||
}
|
||||
|
||||
out[i] = v
|
||||
|
|
|
@ -24,11 +24,8 @@ func (d LogLevel) MarshalJSON() ([]byte, error) {
|
|||
case LogLevel(logger.Info):
|
||||
out = "info"
|
||||
|
||||
case LogLevel(logger.Debug):
|
||||
out = "debug"
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid log level: %v", d)
|
||||
out = "debug"
|
||||
}
|
||||
|
||||
return json.Marshal(out)
|
||||
|
|
|
@ -11,7 +11,6 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/bluenviron/gortsplib/v4/pkg/base"
|
||||
"github.com/bluenviron/gortsplib/v4/pkg/headers"
|
||||
)
|
||||
|
||||
var rePathName = regexp.MustCompile(`^[0-9a-zA-Z_\-/\.~]+$`)
|
||||
|
@ -105,13 +104,13 @@ type Path struct {
|
|||
RecordSegmentDuration StringDuration `json:"recordSegmentDuration"`
|
||||
RecordDeleteAfter StringDuration `json:"recordDeleteAfter"`
|
||||
|
||||
// Authentication
|
||||
PublishUser Credential `json:"publishUser"`
|
||||
PublishPass Credential `json:"publishPass"`
|
||||
PublishIPs IPsOrCIDRs `json:"publishIPs"`
|
||||
ReadUser Credential `json:"readUser"`
|
||||
ReadPass Credential `json:"readPass"`
|
||||
ReadIPs IPsOrCIDRs `json:"readIPs"`
|
||||
// Authentication (deprecated)
|
||||
PublishUser *Credential `json:"publishUser,omitempty"` // deprecated
|
||||
PublishPass *Credential `json:"publishPass,omitempty"` // deprecated
|
||||
PublishIPs *IPNetworks `json:"publishIPs,omitempty"` // deprecated
|
||||
ReadUser *Credential `json:"readUser,omitempty"` // deprecated
|
||||
ReadPass *Credential `json:"readPass,omitempty"` // deprecated
|
||||
ReadIPs *IPNetworks `json:"readIPs,omitempty"` // deprecated
|
||||
|
||||
// Publisher source
|
||||
OverridePublisher bool `json:"overridePublisher"`
|
||||
|
@ -250,7 +249,11 @@ func (pconf Path) Clone() *Path {
|
|||
return &dest
|
||||
}
|
||||
|
||||
func (pconf *Path) validate(conf *Conf, name string) error {
|
||||
func (pconf *Path) validate(
|
||||
conf *Conf,
|
||||
name string,
|
||||
deprecatedCredentialsMode bool,
|
||||
) error {
|
||||
pconf.Name = name
|
||||
|
||||
switch {
|
||||
|
@ -375,39 +378,72 @@ func (pconf *Path) validate(conf *Conf, name string) error {
|
|||
}
|
||||
}
|
||||
|
||||
// Authentication
|
||||
// Authentication (deprecated)
|
||||
|
||||
if (pconf.PublishUser != "" && pconf.PublishPass == "") ||
|
||||
(pconf.PublishUser == "" && pconf.PublishPass != "") {
|
||||
return fmt.Errorf("read username and password must be both filled")
|
||||
}
|
||||
if pconf.PublishUser != "" && pconf.Source != "publisher" {
|
||||
return fmt.Errorf("'publishUser' is useless when source is not 'publisher', since " +
|
||||
"the stream is not provided by a publisher, but by a fixed source")
|
||||
}
|
||||
if len(pconf.PublishIPs) > 0 && pconf.Source != "publisher" {
|
||||
return fmt.Errorf("'publishIPs' is useless when source is not 'publisher', since " +
|
||||
"the stream is not provided by a publisher, but by a fixed source")
|
||||
}
|
||||
if (pconf.ReadUser != "" && pconf.ReadPass == "") ||
|
||||
(pconf.ReadUser == "" && pconf.ReadPass != "") {
|
||||
return fmt.Errorf("read username and password must be both filled")
|
||||
}
|
||||
if contains(conf.AuthMethods, headers.AuthDigestMD5) {
|
||||
if pconf.PublishUser.IsHashed() ||
|
||||
pconf.PublishPass.IsHashed() ||
|
||||
pconf.ReadUser.IsHashed() ||
|
||||
pconf.ReadPass.IsHashed() {
|
||||
return fmt.Errorf("hashed credentials can't be used when the digest auth method is available")
|
||||
}
|
||||
}
|
||||
if conf.ExternalAuthenticationURL != "" {
|
||||
if pconf.PublishUser != "" ||
|
||||
len(pconf.PublishIPs) > 0 ||
|
||||
pconf.ReadUser != "" ||
|
||||
len(pconf.ReadIPs) > 0 {
|
||||
return fmt.Errorf("credentials or IPs can't be used together with 'externalAuthenticationURL'")
|
||||
}
|
||||
if deprecatedCredentialsMode {
|
||||
func() {
|
||||
var user Credential = "any"
|
||||
if pconf.PublishUser != nil {
|
||||
user = *pconf.PublishUser
|
||||
}
|
||||
|
||||
var pass Credential
|
||||
if pconf.PublishPass != nil {
|
||||
pass = *pconf.PublishPass
|
||||
}
|
||||
|
||||
ips := IPNetworks{mustParseCIDR("0.0.0.0/0")}
|
||||
if pconf.PublishIPs != nil {
|
||||
ips = *pconf.PublishIPs
|
||||
}
|
||||
|
||||
pathName := name
|
||||
if name == "all_others" || name == "all" {
|
||||
pathName = "~^.*$"
|
||||
}
|
||||
|
||||
conf.AuthInternalUsers = append(conf.AuthInternalUsers, AuthInternalUser{
|
||||
User: user,
|
||||
Pass: pass,
|
||||
IPs: ips,
|
||||
Permissions: []AuthInternalUserPermission{{
|
||||
Action: AuthActionPublish,
|
||||
Path: pathName,
|
||||
}},
|
||||
})
|
||||
}()
|
||||
|
||||
func() {
|
||||
var user Credential = "any"
|
||||
if pconf.ReadUser != nil {
|
||||
user = *pconf.ReadUser
|
||||
}
|
||||
|
||||
var pass Credential
|
||||
if pconf.ReadPass != nil {
|
||||
pass = *pconf.ReadPass
|
||||
}
|
||||
|
||||
ips := IPNetworks{mustParseCIDR("0.0.0.0/0")}
|
||||
if pconf.ReadIPs != nil {
|
||||
ips = *pconf.ReadIPs
|
||||
}
|
||||
|
||||
pathName := name
|
||||
if name == "all_others" || name == "all" {
|
||||
pathName = "~^.*$"
|
||||
}
|
||||
|
||||
conf.AuthInternalUsers = append(conf.AuthInternalUsers, AuthInternalUser{
|
||||
User: user,
|
||||
Pass: pass,
|
||||
IPs: ips,
|
||||
Permissions: []AuthInternalUserPermission{{
|
||||
Action: AuthActionRead,
|
||||
Path: pathName,
|
||||
}},
|
||||
})
|
||||
}()
|
||||
}
|
||||
|
||||
// Publisher source
|
||||
|
|
|
@ -30,11 +30,8 @@ func (d Protocols) MarshalJSON() ([]byte, error) {
|
|||
case Protocol(gortsplib.TransportUDPMulticast):
|
||||
v = "multicast"
|
||||
|
||||
case Protocol(gortsplib.TransportTCP):
|
||||
v = "tcp"
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid protocol: %v", p)
|
||||
v = "tcp"
|
||||
}
|
||||
|
||||
out[i] = v
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
package conf
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/bluenviron/gortsplib/v4/pkg/headers"
|
||||
)
|
||||
|
||||
// RTSPAuthMethods is the rtspAuthMethods parameter.
|
||||
type RTSPAuthMethods []headers.AuthMethod
|
||||
|
||||
// MarshalJSON implements json.Marshaler.
|
||||
func (d RTSPAuthMethods) MarshalJSON() ([]byte, error) {
|
||||
out := make([]string, len(d))
|
||||
|
||||
for i, v := range d {
|
||||
switch v {
|
||||
case headers.AuthBasic:
|
||||
out[i] = "basic"
|
||||
|
||||
default:
|
||||
out[i] = "digest"
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(out)
|
||||
|
||||
return json.Marshal(out)
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements json.Unmarshaler.
|
||||
func (d *RTSPAuthMethods) UnmarshalJSON(b []byte) error {
|
||||
var in []string
|
||||
if err := json.Unmarshal(b, &in); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*d = nil
|
||||
|
||||
for _, v := range in {
|
||||
switch v {
|
||||
case "basic":
|
||||
*d = append(*d, headers.AuthBasic)
|
||||
|
||||
case "digest":
|
||||
*d = append(*d, headers.AuthDigestMD5)
|
||||
|
||||
default:
|
||||
return fmt.Errorf("invalid authentication method: '%s'", v)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalEnv implements env.Unmarshaler.
|
||||
func (d *RTSPAuthMethods) UnmarshalEnv(_ string, v string) error {
|
||||
byts, _ := json.Marshal(strings.Split(v, ","))
|
||||
return d.UnmarshalJSON(byts)
|
||||
}
|
|
@ -30,11 +30,8 @@ func (d RTSPRangeType) MarshalJSON() ([]byte, error) {
|
|||
case RTSPRangeTypeSMPTE:
|
||||
out = "smpte"
|
||||
|
||||
case RTSPRangeTypeUndefined:
|
||||
out = ""
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid rtsp range type: %v", d)
|
||||
out = ""
|
||||
}
|
||||
|
||||
return json.Marshal(out)
|
||||
|
|
|
@ -26,11 +26,8 @@ func (d RTSPTransport) MarshalJSON() ([]byte, error) {
|
|||
case gortsplib.TransportUDPMulticast:
|
||||
out = "multicast"
|
||||
|
||||
case gortsplib.TransportTCP:
|
||||
out = "tcp"
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid protocol: %v", d.Transport)
|
||||
out = "tcp"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -25,8 +25,8 @@ func (s *StringSize) UnmarshalJSON(b []byte) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*s = StringSize(v)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -1,126 +0,0 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/bluenviron/gortsplib/v4/pkg/auth"
|
||||
"github.com/bluenviron/gortsplib/v4/pkg/headers"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/bluenviron/mediamtx/internal/conf"
|
||||
"github.com/bluenviron/mediamtx/internal/defs"
|
||||
)
|
||||
|
||||
func doExternalAuthentication(
|
||||
ur string,
|
||||
accessRequest defs.PathAccessRequest,
|
||||
) error {
|
||||
enc, _ := json.Marshal(struct {
|
||||
IP string `json:"ip"`
|
||||
User string `json:"user"`
|
||||
Password string `json:"password"`
|
||||
Path string `json:"path"`
|
||||
Protocol string `json:"protocol"`
|
||||
ID *uuid.UUID `json:"id"`
|
||||
Action string `json:"action"`
|
||||
Query string `json:"query"`
|
||||
}{
|
||||
IP: accessRequest.IP.String(),
|
||||
User: accessRequest.User,
|
||||
Password: accessRequest.Pass,
|
||||
Path: accessRequest.Name,
|
||||
Protocol: string(accessRequest.Proto),
|
||||
ID: accessRequest.ID,
|
||||
Action: func() string {
|
||||
if accessRequest.Publish {
|
||||
return "publish"
|
||||
}
|
||||
return "read"
|
||||
}(),
|
||||
Query: accessRequest.Query,
|
||||
})
|
||||
res, err := http.Post(ur, "application/json", bytes.NewReader(enc))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode < 200 || res.StatusCode > 299 {
|
||||
if resBody, err := io.ReadAll(res.Body); err == nil && len(resBody) != 0 {
|
||||
return fmt.Errorf("server replied with code %d: %s", res.StatusCode, string(resBody))
|
||||
}
|
||||
return fmt.Errorf("server replied with code %d", res.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func doAuthentication(
|
||||
externalAuthenticationURL string,
|
||||
rtspAuthMethods conf.AuthMethods,
|
||||
pathConf *conf.Path,
|
||||
accessRequest defs.PathAccessRequest,
|
||||
) error {
|
||||
var rtspAuth headers.Authorization
|
||||
if accessRequest.RTSPRequest != nil {
|
||||
err := rtspAuth.Unmarshal(accessRequest.RTSPRequest.Header["Authorization"])
|
||||
if err == nil && rtspAuth.Method == headers.AuthBasic {
|
||||
accessRequest.User = rtspAuth.BasicUser
|
||||
accessRequest.Pass = rtspAuth.BasicPass
|
||||
}
|
||||
}
|
||||
|
||||
if externalAuthenticationURL != "" {
|
||||
err := doExternalAuthentication(
|
||||
externalAuthenticationURL,
|
||||
accessRequest,
|
||||
)
|
||||
if err != nil {
|
||||
return defs.AuthenticationError{Message: fmt.Sprintf("external authentication failed: %s", err)}
|
||||
}
|
||||
}
|
||||
|
||||
var pathIPs conf.IPsOrCIDRs
|
||||
var pathUser conf.Credential
|
||||
var pathPass conf.Credential
|
||||
|
||||
if accessRequest.Publish {
|
||||
pathIPs = pathConf.PublishIPs
|
||||
pathUser = pathConf.PublishUser
|
||||
pathPass = pathConf.PublishPass
|
||||
} else {
|
||||
pathIPs = pathConf.ReadIPs
|
||||
pathUser = pathConf.ReadUser
|
||||
pathPass = pathConf.ReadPass
|
||||
}
|
||||
|
||||
if pathIPs != nil {
|
||||
if !ipEqualOrInRange(accessRequest.IP, pathIPs) {
|
||||
return defs.AuthenticationError{Message: fmt.Sprintf("IP %s not allowed", accessRequest.IP)}
|
||||
}
|
||||
}
|
||||
|
||||
if pathUser != "" {
|
||||
if accessRequest.RTSPRequest != nil && rtspAuth.Method == headers.AuthDigestMD5 {
|
||||
err := auth.Validate(
|
||||
accessRequest.RTSPRequest,
|
||||
string(pathUser),
|
||||
string(pathPass),
|
||||
accessRequest.RTSPBaseURL,
|
||||
rtspAuthMethods,
|
||||
"IPCAM",
|
||||
accessRequest.RTSPNonce)
|
||||
if err != nil {
|
||||
return defs.AuthenticationError{Message: err.Error()}
|
||||
}
|
||||
} else if !pathUser.Check(accessRequest.User) || !pathPass.Check(accessRequest.Pass) {
|
||||
return defs.AuthenticationError{Message: "invalid credentials"}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,155 +0,0 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/bluenviron/gortsplib/v4/pkg/headers"
|
||||
"github.com/bluenviron/mediamtx/internal/conf"
|
||||
"github.com/bluenviron/mediamtx/internal/defs"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type testHTTPAuthenticator struct {
|
||||
*http.Server
|
||||
}
|
||||
|
||||
func (ts *testHTTPAuthenticator) initialize(t *testing.T, protocol string, action string) {
|
||||
firstReceived := false
|
||||
|
||||
ts.Server = &http.Server{
|
||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
require.Equal(t, "/auth", r.URL.Path)
|
||||
|
||||
var in struct {
|
||||
IP string `json:"ip"`
|
||||
User string `json:"user"`
|
||||
Password string `json:"password"`
|
||||
Path string `json:"path"`
|
||||
Protocol string `json:"protocol"`
|
||||
ID string `json:"id"`
|
||||
Action string `json:"action"`
|
||||
Query string `json:"query"`
|
||||
}
|
||||
err := json.NewDecoder(r.Body).Decode(&in)
|
||||
require.NoError(t, err)
|
||||
|
||||
var user string
|
||||
if action == "publish" {
|
||||
user = "testpublisher"
|
||||
} else {
|
||||
user = "testreader"
|
||||
}
|
||||
|
||||
if in.IP != "127.0.0.1" ||
|
||||
in.User != user ||
|
||||
in.Password != "testpass" ||
|
||||
in.Path != "teststream" ||
|
||||
in.Protocol != protocol ||
|
||||
(firstReceived && in.ID == "") ||
|
||||
in.Action != action ||
|
||||
(in.Query != "user=testreader&pass=testpass¶m=value" &&
|
||||
in.Query != "user=testpublisher&pass=testpass¶m=value" &&
|
||||
in.Query != "param=value") {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
firstReceived = true
|
||||
}),
|
||||
}
|
||||
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:9120")
|
||||
require.NoError(t, err)
|
||||
|
||||
go ts.Server.Serve(ln)
|
||||
}
|
||||
|
||||
func (ts *testHTTPAuthenticator) close() {
|
||||
ts.Server.Shutdown(context.Background())
|
||||
}
|
||||
|
||||
func TestAuthSha256(t *testing.T) {
|
||||
err := doAuthentication(
|
||||
"",
|
||||
conf.AuthMethods{headers.AuthBasic},
|
||||
&conf.Path{
|
||||
PublishUser: conf.Credential("sha256:rl3rgi4NcZkpAEcacZnQ2VuOfJ0FxAqCRaKB/SwdZoQ="),
|
||||
PublishPass: conf.Credential("sha256:E9JJ8stBJ7QM+nV4ZoUCeHk/gU3tPFh/5YieiJp6n2w="),
|
||||
},
|
||||
defs.PathAccessRequest{
|
||||
Name: "mypath",
|
||||
Query: "",
|
||||
Publish: true,
|
||||
SkipAuth: false,
|
||||
IP: net.ParseIP("127.0.0.1"),
|
||||
User: "testuser",
|
||||
Pass: "testpass",
|
||||
Proto: defs.AuthProtocolRTSP,
|
||||
ID: nil,
|
||||
RTSPRequest: nil,
|
||||
RTSPBaseURL: nil,
|
||||
RTSPNonce: "",
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestAuthArgon2(t *testing.T) {
|
||||
err := doAuthentication(
|
||||
"",
|
||||
conf.AuthMethods{headers.AuthBasic},
|
||||
&conf.Path{
|
||||
PublishUser: conf.Credential(
|
||||
"argon2:$argon2id$v=19$m=4096,t=3,p=1$MTIzNDU2Nzg$Ux/LWeTgJQPyfMMJo1myR64+o8rALHoPmlE1i/TR+58"),
|
||||
PublishPass: conf.Credential(
|
||||
"argon2:$argon2i$v=19$m=4096,t=3,p=1$MTIzNDU2Nzg$/mrZ42TiTv1mcPnpMUera5oi0SFYbbyueAbdx5sUvWo"),
|
||||
},
|
||||
defs.PathAccessRequest{
|
||||
Name: "mypath",
|
||||
Query: "",
|
||||
Publish: true,
|
||||
SkipAuth: false,
|
||||
IP: net.ParseIP("127.0.0.1"),
|
||||
User: "testuser",
|
||||
Pass: "testpass",
|
||||
Proto: defs.AuthProtocolRTSP,
|
||||
ID: nil,
|
||||
RTSPRequest: nil,
|
||||
RTSPBaseURL: nil,
|
||||
RTSPNonce: "",
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestAuthExternal(t *testing.T) {
|
||||
au := &testHTTPAuthenticator{}
|
||||
au.initialize(t, "rtsp", "publish")
|
||||
defer au.close()
|
||||
|
||||
err := doAuthentication(
|
||||
"http://127.0.0.1:9120/auth",
|
||||
conf.AuthMethods{headers.AuthBasic},
|
||||
&conf.Path{},
|
||||
defs.PathAccessRequest{
|
||||
Name: "teststream",
|
||||
Query: "param=value",
|
||||
Publish: true,
|
||||
SkipAuth: false,
|
||||
IP: net.ParseIP("127.0.0.1"),
|
||||
User: "testpublisher",
|
||||
Pass: "testpass",
|
||||
Proto: defs.AuthProtocolRTSP,
|
||||
ID: nil,
|
||||
RTSPRequest: nil,
|
||||
RTSPBaseURL: nil,
|
||||
RTSPNonce: "",
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
}
|
|
@ -17,6 +17,7 @@ import (
|
|||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/bluenviron/mediamtx/internal/api"
|
||||
"github.com/bluenviron/mediamtx/internal/auth"
|
||||
"github.com/bluenviron/mediamtx/internal/conf"
|
||||
"github.com/bluenviron/mediamtx/internal/confwatcher"
|
||||
"github.com/bluenviron/mediamtx/internal/externalcmd"
|
||||
|
@ -88,6 +89,7 @@ type Core struct {
|
|||
conf *conf.Conf
|
||||
logger *logger.Logger
|
||||
externalCmdPool *externalcmd.Pool
|
||||
authManager *auth.Manager
|
||||
metrics *metrics.Metrics
|
||||
pprof *pprof.PPROF
|
||||
recordCleaner *record.Cleaner
|
||||
|
@ -278,11 +280,24 @@ func (p *Core) createResources(initial bool) error {
|
|||
p.externalCmdPool = externalcmd.NewPool()
|
||||
}
|
||||
|
||||
if p.authManager == nil {
|
||||
p.authManager = &auth.Manager{
|
||||
Method: p.conf.AuthMethod,
|
||||
InternalUsers: p.conf.AuthInternalUsers,
|
||||
HTTPAddress: p.conf.AuthHTTPAddress,
|
||||
HTTPExclude: p.conf.AuthHTTPExclude,
|
||||
JWTJWKS: p.conf.AuthJWTJWKS,
|
||||
ReadTimeout: time.Duration(p.conf.ReadTimeout),
|
||||
RTSPAuthMethods: p.conf.RTSPAuthMethods,
|
||||
}
|
||||
}
|
||||
|
||||
if p.conf.Metrics &&
|
||||
p.metrics == nil {
|
||||
i := &metrics.Metrics{
|
||||
Address: p.conf.MetricsAddress,
|
||||
ReadTimeout: p.conf.ReadTimeout,
|
||||
AuthManager: p.authManager,
|
||||
Parent: p,
|
||||
}
|
||||
err := i.Initialize()
|
||||
|
@ -297,6 +312,7 @@ func (p *Core) createResources(initial bool) error {
|
|||
i := &pprof.PPROF{
|
||||
Address: p.conf.PPROFAddress,
|
||||
ReadTimeout: p.conf.ReadTimeout,
|
||||
AuthManager: p.authManager,
|
||||
Parent: p,
|
||||
}
|
||||
err := i.Initialize()
|
||||
|
@ -322,6 +338,7 @@ func (p *Core) createResources(initial bool) error {
|
|||
Address: p.conf.PlaybackAddress,
|
||||
ReadTimeout: p.conf.ReadTimeout,
|
||||
PathConfs: p.conf.Paths,
|
||||
AuthManager: p.authManager,
|
||||
Parent: p,
|
||||
}
|
||||
err := i.Initialize()
|
||||
|
@ -333,17 +350,16 @@ func (p *Core) createResources(initial bool) error {
|
|||
|
||||
if p.pathManager == nil {
|
||||
p.pathManager = &pathManager{
|
||||
logLevel: p.conf.LogLevel,
|
||||
externalAuthenticationURL: p.conf.ExternalAuthenticationURL,
|
||||
rtspAddress: p.conf.RTSPAddress,
|
||||
authMethods: p.conf.AuthMethods,
|
||||
readTimeout: p.conf.ReadTimeout,
|
||||
writeTimeout: p.conf.WriteTimeout,
|
||||
writeQueueSize: p.conf.WriteQueueSize,
|
||||
udpMaxPayloadSize: p.conf.UDPMaxPayloadSize,
|
||||
pathConfs: p.conf.Paths,
|
||||
externalCmdPool: p.externalCmdPool,
|
||||
parent: p,
|
||||
logLevel: p.conf.LogLevel,
|
||||
authManager: p.authManager,
|
||||
rtspAddress: p.conf.RTSPAddress,
|
||||
readTimeout: p.conf.ReadTimeout,
|
||||
writeTimeout: p.conf.WriteTimeout,
|
||||
writeQueueSize: p.conf.WriteQueueSize,
|
||||
udpMaxPayloadSize: p.conf.UDPMaxPayloadSize,
|
||||
pathConfs: p.conf.Paths,
|
||||
externalCmdPool: p.externalCmdPool,
|
||||
parent: p,
|
||||
}
|
||||
p.pathManager.initialize()
|
||||
|
||||
|
@ -361,7 +377,7 @@ func (p *Core) createResources(initial bool) error {
|
|||
|
||||
i := &rtsp.Server{
|
||||
Address: p.conf.RTSPAddress,
|
||||
AuthMethods: p.conf.AuthMethods,
|
||||
AuthMethods: p.conf.RTSPAuthMethods,
|
||||
ReadTimeout: p.conf.ReadTimeout,
|
||||
WriteTimeout: p.conf.WriteTimeout,
|
||||
WriteQueueSize: p.conf.WriteQueueSize,
|
||||
|
@ -401,7 +417,7 @@ func (p *Core) createResources(initial bool) error {
|
|||
p.rtspsServer == nil {
|
||||
i := &rtsp.Server{
|
||||
Address: p.conf.RTSPSAddress,
|
||||
AuthMethods: p.conf.AuthMethods,
|
||||
AuthMethods: p.conf.RTSPAuthMethods,
|
||||
ReadTimeout: p.conf.ReadTimeout,
|
||||
WriteTimeout: p.conf.WriteTimeout,
|
||||
WriteQueueSize: p.conf.WriteQueueSize,
|
||||
|
@ -500,24 +516,23 @@ func (p *Core) createResources(initial bool) error {
|
|||
if p.conf.HLS &&
|
||||
p.hlsServer == nil {
|
||||
i := &hls.Server{
|
||||
Address: p.conf.HLSAddress,
|
||||
Encryption: p.conf.HLSEncryption,
|
||||
ServerKey: p.conf.HLSServerKey,
|
||||
ServerCert: p.conf.HLSServerCert,
|
||||
ExternalAuthenticationURL: p.conf.ExternalAuthenticationURL,
|
||||
AlwaysRemux: p.conf.HLSAlwaysRemux,
|
||||
Variant: p.conf.HLSVariant,
|
||||
SegmentCount: p.conf.HLSSegmentCount,
|
||||
SegmentDuration: p.conf.HLSSegmentDuration,
|
||||
PartDuration: p.conf.HLSPartDuration,
|
||||
SegmentMaxSize: p.conf.HLSSegmentMaxSize,
|
||||
AllowOrigin: p.conf.HLSAllowOrigin,
|
||||
TrustedProxies: p.conf.HLSTrustedProxies,
|
||||
Directory: p.conf.HLSDirectory,
|
||||
ReadTimeout: p.conf.ReadTimeout,
|
||||
WriteQueueSize: p.conf.WriteQueueSize,
|
||||
PathManager: p.pathManager,
|
||||
Parent: p,
|
||||
Address: p.conf.HLSAddress,
|
||||
Encryption: p.conf.HLSEncryption,
|
||||
ServerKey: p.conf.HLSServerKey,
|
||||
ServerCert: p.conf.HLSServerCert,
|
||||
AlwaysRemux: p.conf.HLSAlwaysRemux,
|
||||
Variant: p.conf.HLSVariant,
|
||||
SegmentCount: p.conf.HLSSegmentCount,
|
||||
SegmentDuration: p.conf.HLSSegmentDuration,
|
||||
PartDuration: p.conf.HLSPartDuration,
|
||||
SegmentMaxSize: p.conf.HLSSegmentMaxSize,
|
||||
AllowOrigin: p.conf.HLSAllowOrigin,
|
||||
TrustedProxies: p.conf.HLSTrustedProxies,
|
||||
Directory: p.conf.HLSDirectory,
|
||||
ReadTimeout: p.conf.ReadTimeout,
|
||||
WriteQueueSize: p.conf.WriteQueueSize,
|
||||
PathManager: p.pathManager,
|
||||
Parent: p,
|
||||
}
|
||||
err := i.Initialize()
|
||||
if err != nil {
|
||||
|
@ -597,6 +612,7 @@ func (p *Core) createResources(initial bool) error {
|
|||
Address: p.conf.APIAddress,
|
||||
ReadTimeout: p.conf.ReadTimeout,
|
||||
Conf: p.conf,
|
||||
AuthManager: p.authManager,
|
||||
PathManager: p.pathManager,
|
||||
RTSPServer: p.rtspServer,
|
||||
RTSPSServer: p.rtspsServer,
|
||||
|
@ -630,16 +646,29 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
|
|||
!reflect.DeepEqual(newConf.LogDestinations, p.conf.LogDestinations) ||
|
||||
newConf.LogFile != p.conf.LogFile
|
||||
|
||||
closeAuthManager := newConf == nil ||
|
||||
newConf.AuthMethod != p.conf.AuthMethod ||
|
||||
newConf.AuthHTTPAddress != p.conf.AuthHTTPAddress ||
|
||||
!reflect.DeepEqual(newConf.AuthHTTPExclude, p.conf.AuthHTTPExclude) ||
|
||||
newConf.AuthJWTJWKS != p.conf.AuthJWTJWKS ||
|
||||
newConf.ReadTimeout != p.conf.ReadTimeout ||
|
||||
!reflect.DeepEqual(newConf.RTSPAuthMethods, p.conf.RTSPAuthMethods)
|
||||
if !closeAuthManager && !reflect.DeepEqual(newConf.AuthInternalUsers, p.conf.AuthInternalUsers) {
|
||||
p.authManager.ReloadInternalUsers(newConf.AuthInternalUsers)
|
||||
}
|
||||
|
||||
closeMetrics := newConf == nil ||
|
||||
newConf.Metrics != p.conf.Metrics ||
|
||||
newConf.MetricsAddress != p.conf.MetricsAddress ||
|
||||
newConf.ReadTimeout != p.conf.ReadTimeout ||
|
||||
closeAuthManager ||
|
||||
closeLogger
|
||||
|
||||
closePPROF := newConf == nil ||
|
||||
newConf.PPROF != p.conf.PPROF ||
|
||||
newConf.PPROFAddress != p.conf.PPROFAddress ||
|
||||
newConf.ReadTimeout != p.conf.ReadTimeout ||
|
||||
closeAuthManager ||
|
||||
closeLogger
|
||||
|
||||
closeRecorderCleaner := newConf == nil ||
|
||||
|
@ -650,6 +679,7 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
|
|||
newConf.Playback != p.conf.Playback ||
|
||||
newConf.PlaybackAddress != p.conf.PlaybackAddress ||
|
||||
newConf.ReadTimeout != p.conf.ReadTimeout ||
|
||||
closeAuthManager ||
|
||||
closeLogger
|
||||
if !closePlaybackServer && p.playbackServer != nil && !reflect.DeepEqual(newConf.Paths, p.conf.Paths) {
|
||||
p.playbackServer.ReloadPathConfs(newConf.Paths)
|
||||
|
@ -657,14 +687,14 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
|
|||
|
||||
closePathManager := newConf == nil ||
|
||||
newConf.LogLevel != p.conf.LogLevel ||
|
||||
newConf.ExternalAuthenticationURL != p.conf.ExternalAuthenticationURL ||
|
||||
newConf.RTSPAddress != p.conf.RTSPAddress ||
|
||||
!reflect.DeepEqual(newConf.AuthMethods, p.conf.AuthMethods) ||
|
||||
!reflect.DeepEqual(newConf.RTSPAuthMethods, p.conf.RTSPAuthMethods) ||
|
||||
newConf.ReadTimeout != p.conf.ReadTimeout ||
|
||||
newConf.WriteTimeout != p.conf.WriteTimeout ||
|
||||
newConf.WriteQueueSize != p.conf.WriteQueueSize ||
|
||||
newConf.UDPMaxPayloadSize != p.conf.UDPMaxPayloadSize ||
|
||||
closeMetrics ||
|
||||
closeAuthManager ||
|
||||
closeLogger
|
||||
if !closePathManager && !reflect.DeepEqual(newConf.Paths, p.conf.Paths) {
|
||||
p.pathManager.ReloadPathConfs(newConf.Paths)
|
||||
|
@ -674,7 +704,7 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
|
|||
newConf.RTSP != p.conf.RTSP ||
|
||||
newConf.Encryption != p.conf.Encryption ||
|
||||
newConf.RTSPAddress != p.conf.RTSPAddress ||
|
||||
!reflect.DeepEqual(newConf.AuthMethods, p.conf.AuthMethods) ||
|
||||
!reflect.DeepEqual(newConf.RTSPAuthMethods, p.conf.RTSPAuthMethods) ||
|
||||
newConf.ReadTimeout != p.conf.ReadTimeout ||
|
||||
newConf.WriteTimeout != p.conf.WriteTimeout ||
|
||||
newConf.WriteQueueSize != p.conf.WriteQueueSize ||
|
||||
|
@ -697,7 +727,7 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
|
|||
newConf.RTSP != p.conf.RTSP ||
|
||||
newConf.Encryption != p.conf.Encryption ||
|
||||
newConf.RTSPSAddress != p.conf.RTSPSAddress ||
|
||||
!reflect.DeepEqual(newConf.AuthMethods, p.conf.AuthMethods) ||
|
||||
!reflect.DeepEqual(newConf.RTSPAuthMethods, p.conf.RTSPAuthMethods) ||
|
||||
newConf.ReadTimeout != p.conf.ReadTimeout ||
|
||||
newConf.WriteTimeout != p.conf.WriteTimeout ||
|
||||
newConf.WriteQueueSize != p.conf.WriteQueueSize ||
|
||||
|
@ -750,7 +780,6 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
|
|||
newConf.HLSEncryption != p.conf.HLSEncryption ||
|
||||
newConf.HLSServerKey != p.conf.HLSServerKey ||
|
||||
newConf.HLSServerCert != p.conf.HLSServerCert ||
|
||||
newConf.ExternalAuthenticationURL != p.conf.ExternalAuthenticationURL ||
|
||||
newConf.HLSAlwaysRemux != p.conf.HLSAlwaysRemux ||
|
||||
newConf.HLSVariant != p.conf.HLSVariant ||
|
||||
newConf.HLSSegmentCount != p.conf.HLSSegmentCount ||
|
||||
|
@ -804,6 +833,7 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
|
|||
newConf.API != p.conf.API ||
|
||||
newConf.APIAddress != p.conf.APIAddress ||
|
||||
newConf.ReadTimeout != p.conf.ReadTimeout ||
|
||||
closeAuthManager ||
|
||||
closePathManager ||
|
||||
closeRTSPServer ||
|
||||
closeRTSPSServer ||
|
||||
|
@ -921,6 +951,10 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
|
|||
p.metrics = nil
|
||||
}
|
||||
|
||||
if closeAuthManager && p.authManager != nil {
|
||||
p.authManager = nil
|
||||
}
|
||||
|
||||
if newConf == nil && p.externalCmdPool != nil {
|
||||
p.Log(logger.Info, "waiting for running hooks")
|
||||
p.externalCmdPool.Close()
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
)
|
||||
|
||||
func ipEqualOrInRange(ip net.IP, ips []fmt.Stringer) bool {
|
||||
for _, item := range ips {
|
||||
switch titem := item.(type) {
|
||||
case net.IP:
|
||||
if titem.Equal(ip) {
|
||||
return true
|
||||
}
|
||||
|
||||
case *net.IPNet:
|
||||
if titem.Contains(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
|
@ -6,6 +6,7 @@ import (
|
|||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/bluenviron/mediamtx/internal/auth"
|
||||
"github.com/bluenviron/mediamtx/internal/conf"
|
||||
"github.com/bluenviron/mediamtx/internal/defs"
|
||||
"github.com/bluenviron/mediamtx/internal/externalcmd"
|
||||
|
@ -47,17 +48,16 @@ type pathManagerParent interface {
|
|||
}
|
||||
|
||||
type pathManager struct {
|
||||
logLevel conf.LogLevel
|
||||
externalAuthenticationURL string
|
||||
rtspAddress string
|
||||
authMethods conf.AuthMethods
|
||||
readTimeout conf.StringDuration
|
||||
writeTimeout conf.StringDuration
|
||||
writeQueueSize int
|
||||
udpMaxPayloadSize int
|
||||
pathConfs map[string]*conf.Path
|
||||
externalCmdPool *externalcmd.Pool
|
||||
parent pathManagerParent
|
||||
logLevel conf.LogLevel
|
||||
authManager *auth.Manager
|
||||
rtspAddress string
|
||||
readTimeout conf.StringDuration
|
||||
writeTimeout conf.StringDuration
|
||||
writeQueueSize int
|
||||
udpMaxPayloadSize int
|
||||
pathConfs map[string]*conf.Path
|
||||
externalCmdPool *externalcmd.Pool
|
||||
parent pathManagerParent
|
||||
|
||||
ctx context.Context
|
||||
ctxCancel func()
|
||||
|
@ -236,8 +236,7 @@ func (pm *pathManager) doFindPathConf(req defs.PathFindPathConfReq) {
|
|||
return
|
||||
}
|
||||
|
||||
err = doAuthentication(pm.externalAuthenticationURL, pm.authMethods,
|
||||
pathConf, req.AccessRequest)
|
||||
err = pm.authManager.Authenticate(req.AccessRequest.ToAuthRequest())
|
||||
if err != nil {
|
||||
req.Res <- defs.PathFindPathConfRes{Err: err}
|
||||
return
|
||||
|
@ -253,8 +252,7 @@ func (pm *pathManager) doDescribe(req defs.PathDescribeReq) {
|
|||
return
|
||||
}
|
||||
|
||||
err = doAuthentication(pm.externalAuthenticationURL, pm.authMethods,
|
||||
pathConf, req.AccessRequest)
|
||||
err = pm.authManager.Authenticate(req.AccessRequest.ToAuthRequest())
|
||||
if err != nil {
|
||||
req.Res <- defs.PathDescribeRes{Err: err}
|
||||
return
|
||||
|
@ -276,8 +274,7 @@ func (pm *pathManager) doAddReader(req defs.PathAddReaderReq) {
|
|||
}
|
||||
|
||||
if !req.AccessRequest.SkipAuth {
|
||||
err = doAuthentication(pm.externalAuthenticationURL, pm.authMethods,
|
||||
pathConf, req.AccessRequest)
|
||||
err = pm.authManager.Authenticate(req.AccessRequest.ToAuthRequest())
|
||||
if err != nil {
|
||||
req.Res <- defs.PathAddReaderRes{Err: err}
|
||||
return
|
||||
|
@ -300,8 +297,7 @@ func (pm *pathManager) doAddPublisher(req defs.PathAddPublisherReq) {
|
|||
}
|
||||
|
||||
if !req.AccessRequest.SkipAuth {
|
||||
err = doAuthentication(pm.externalAuthenticationURL, pm.authMethods,
|
||||
pathConf, req.AccessRequest)
|
||||
err = pm.authManager.Authenticate(req.AccessRequest.ToAuthRequest())
|
||||
if err != nil {
|
||||
req.Res <- defs.PathAddPublisherRes{Err: err}
|
||||
return
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
package defs
|
||||
|
||||
// AuthProtocol is a authentication protocol.
|
||||
type AuthProtocol string
|
||||
|
||||
// authentication protocols.
|
||||
const (
|
||||
AuthProtocolRTSP AuthProtocol = "rtsp"
|
||||
AuthProtocolRTMP AuthProtocol = "rtmp"
|
||||
AuthProtocolHLS AuthProtocol = "hls"
|
||||
AuthProtocolWebRTC AuthProtocol = "webrtc"
|
||||
AuthProtocolSRT AuthProtocol = "srt"
|
||||
)
|
||||
|
||||
// AuthenticationError is a authentication error.
|
||||
type AuthenticationError struct {
|
||||
Message string
|
||||
}
|
||||
|
||||
// Error implements the error interface.
|
||||
func (e AuthenticationError) Error() string {
|
||||
return "authentication failed: " + e.Message
|
||||
}
|
|
@ -8,6 +8,7 @@ import (
|
|||
"github.com/bluenviron/gortsplib/v4/pkg/description"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/bluenviron/mediamtx/internal/auth"
|
||||
"github.com/bluenviron/mediamtx/internal/conf"
|
||||
"github.com/bluenviron/mediamtx/internal/externalcmd"
|
||||
"github.com/bluenviron/mediamtx/internal/stream"
|
||||
|
@ -45,13 +46,35 @@ type PathAccessRequest struct {
|
|||
IP net.IP
|
||||
User string
|
||||
Pass string
|
||||
Proto AuthProtocol
|
||||
Proto auth.Protocol
|
||||
ID *uuid.UUID
|
||||
RTSPRequest *base.Request
|
||||
RTSPBaseURL *base.URL
|
||||
RTSPNonce string
|
||||
}
|
||||
|
||||
// ToAuthRequest converts a path access request into an authentication request.
|
||||
func (r *PathAccessRequest) ToAuthRequest() *auth.Request {
|
||||
return &auth.Request{
|
||||
User: r.User,
|
||||
Pass: r.Pass,
|
||||
IP: r.IP,
|
||||
Action: func() conf.AuthAction {
|
||||
if r.Publish {
|
||||
return conf.AuthActionPublish
|
||||
}
|
||||
return conf.AuthActionRead
|
||||
}(),
|
||||
Path: r.Name,
|
||||
Protocol: r.Proto,
|
||||
ID: r.ID,
|
||||
Query: r.Query,
|
||||
RTSPRequest: r.RTSPRequest,
|
||||
RTSPBaseURL: r.RTSPBaseURL,
|
||||
RTSPNonce: r.RTSPNonce,
|
||||
}
|
||||
}
|
||||
|
||||
// PathFindPathConfRes contains the response of FindPathConf().
|
||||
type PathFindPathConfRes struct {
|
||||
Conf *conf.Path
|
||||
|
|
|
@ -3,6 +3,7 @@ package metrics
|
|||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strconv"
|
||||
|
@ -12,6 +13,7 @@ import (
|
|||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/bluenviron/mediamtx/internal/api"
|
||||
"github.com/bluenviron/mediamtx/internal/auth"
|
||||
"github.com/bluenviron/mediamtx/internal/conf"
|
||||
"github.com/bluenviron/mediamtx/internal/logger"
|
||||
"github.com/bluenviron/mediamtx/internal/protocols/httpp"
|
||||
|
@ -38,6 +40,7 @@ type metricsParent interface {
|
|||
type Metrics struct {
|
||||
Address string
|
||||
ReadTimeout conf.StringDuration
|
||||
AuthManager *auth.Manager
|
||||
Parent metricsParent
|
||||
|
||||
httpServer *httpp.WrappedServer
|
||||
|
@ -57,7 +60,7 @@ func (m *Metrics) Initialize() error {
|
|||
router := gin.New()
|
||||
router.SetTrustedProxies(nil) //nolint:errcheck
|
||||
|
||||
router.GET("/metrics", m.onMetrics)
|
||||
router.GET("/metrics", m.mwAuth, m.onMetrics)
|
||||
|
||||
network, address := restrictnetwork.Restrict("tcp", m.Address)
|
||||
|
||||
|
@ -91,6 +94,30 @@ func (m *Metrics) Log(level logger.Level, format string, args ...interface{}) {
|
|||
m.Parent.Log(level, "[metrics] "+format, args...)
|
||||
}
|
||||
|
||||
func (m *Metrics) mwAuth(ctx *gin.Context) {
|
||||
user, pass, hasCredentials := ctx.Request.BasicAuth()
|
||||
|
||||
err := m.AuthManager.Authenticate(&auth.Request{
|
||||
User: user,
|
||||
Pass: pass,
|
||||
IP: net.ParseIP(ctx.ClientIP()),
|
||||
Action: conf.AuthActionMetrics,
|
||||
})
|
||||
if err != nil {
|
||||
if !hasCredentials {
|
||||
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
|
||||
ctx.AbortWithStatus(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// wait some seconds to mitigate brute force attacks
|
||||
<-time.After(auth.PauseAfterError)
|
||||
|
||||
ctx.Writer.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Metrics) onMetrics(ctx *gin.Context) {
|
||||
out := ""
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/bluenviron/mediamtx/internal/auth"
|
||||
"github.com/bluenviron/mediamtx/internal/conf"
|
||||
"github.com/bluenviron/mediamtx/internal/logger"
|
||||
"github.com/bluenviron/mediamtx/internal/protocols/httpp"
|
||||
|
@ -57,6 +58,7 @@ type Server struct {
|
|||
Address string
|
||||
ReadTimeout conf.StringDuration
|
||||
PathConfs map[string]*conf.Path
|
||||
AuthManager *auth.Manager
|
||||
Parent logger.Writer
|
||||
|
||||
httpServer *httpp.WrappedServer
|
||||
|
@ -128,9 +130,45 @@ func (p *Server) safeFindPathConf(name string) (*conf.Path, error) {
|
|||
return pathConf, err
|
||||
}
|
||||
|
||||
func (p *Server) doAuth(ctx *gin.Context, pathName string) bool {
|
||||
user, pass, hasCredentials := ctx.Request.BasicAuth()
|
||||
|
||||
err := p.AuthManager.Authenticate(&auth.Request{
|
||||
User: user,
|
||||
Pass: pass,
|
||||
IP: net.ParseIP(ctx.ClientIP()),
|
||||
Action: conf.AuthActionPlayback,
|
||||
Path: pathName,
|
||||
})
|
||||
if err != nil {
|
||||
if !hasCredentials {
|
||||
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
|
||||
ctx.Writer.WriteHeader(http.StatusUnauthorized)
|
||||
return false
|
||||
}
|
||||
|
||||
var terr auth.Error
|
||||
errors.As(err, &terr)
|
||||
|
||||
p.Log(logger.Info, "connection %v failed to authenticate: %v", httpp.RemoteAddr(ctx), terr.Message)
|
||||
|
||||
// wait some seconds to mitigate brute force attacks
|
||||
<-time.After(auth.PauseAfterError)
|
||||
|
||||
ctx.Writer.WriteHeader(http.StatusUnauthorized)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *Server) onList(ctx *gin.Context) {
|
||||
pathName := ctx.Query("path")
|
||||
|
||||
if !p.doAuth(ctx, pathName) {
|
||||
return
|
||||
}
|
||||
|
||||
pathConf, err := p.safeFindPathConf(pathName)
|
||||
if err != nil {
|
||||
p.writeError(ctx, http.StatusBadRequest, err)
|
||||
|
@ -182,6 +220,10 @@ func (p *Server) onList(ctx *gin.Context) {
|
|||
func (p *Server) onGet(ctx *gin.Context) {
|
||||
pathName := ctx.Query("path")
|
||||
|
||||
if !p.doAuth(ctx, pathName) {
|
||||
return
|
||||
}
|
||||
|
||||
start, err := time.Parse(time.RFC3339, ctx.Query("start"))
|
||||
if err != nil {
|
||||
p.writeError(ctx, http.StatusBadRequest, fmt.Errorf("invalid start: %w", err))
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
|
||||
"github.com/bluenviron/mediacommon/pkg/formats/fmp4"
|
||||
"github.com/bluenviron/mediacommon/pkg/formats/fmp4/seekablebuffer"
|
||||
"github.com/bluenviron/mediamtx/internal/auth"
|
||||
"github.com/bluenviron/mediamtx/internal/conf"
|
||||
"github.com/bluenviron/mediamtx/internal/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
@ -120,6 +121,23 @@ func writeSegment2(t *testing.T, fpath string) {
|
|||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
var authManager = &auth.Manager{
|
||||
Method: conf.AuthMethodInternal,
|
||||
InternalUsers: []conf.AuthInternalUser{
|
||||
{
|
||||
User: "myuser",
|
||||
Pass: "mypass",
|
||||
Permissions: []conf.AuthInternalUserPermission{
|
||||
{
|
||||
Action: conf.AuthActionPlayback,
|
||||
Path: "mypath",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
RTSPAuthMethods: nil,
|
||||
}
|
||||
|
||||
func TestServerGet(t *testing.T) {
|
||||
dir, err := os.MkdirTemp("", "mediamtx-playback")
|
||||
require.NoError(t, err)
|
||||
|
@ -140,24 +158,22 @@ func TestServerGet(t *testing.T) {
|
|||
RecordPath: filepath.Join(dir, "%path/%Y-%m-%d_%H-%M-%S-%f"),
|
||||
},
|
||||
},
|
||||
Parent: &test.NilLogger{},
|
||||
AuthManager: authManager,
|
||||
Parent: &test.NilLogger{},
|
||||
}
|
||||
err = s.Initialize()
|
||||
require.NoError(t, err)
|
||||
defer s.Close()
|
||||
|
||||
u, err := url.Parse("http://myuser:mypass@localhost:9996/get")
|
||||
require.NoError(t, err)
|
||||
|
||||
v := url.Values{}
|
||||
v.Set("path", "mypath")
|
||||
v.Set("start", time.Date(2008, 11, 0o7, 11, 23, 1, 500000000, time.Local).Format(time.RFC3339Nano))
|
||||
v.Set("duration", "2")
|
||||
v.Set("format", "fmp4")
|
||||
|
||||
u := &url.URL{
|
||||
Scheme: "http",
|
||||
Host: "localhost:9996",
|
||||
Path: "/get",
|
||||
RawQuery: v.Encode(),
|
||||
}
|
||||
u.RawQuery = v.Encode()
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
||||
require.NoError(t, err)
|
||||
|
@ -234,21 +250,19 @@ func TestServerList(t *testing.T) {
|
|||
RecordPath: filepath.Join(dir, "%path/%Y-%m-%d_%H-%M-%S-%f"),
|
||||
},
|
||||
},
|
||||
Parent: &test.NilLogger{},
|
||||
AuthManager: authManager,
|
||||
Parent: &test.NilLogger{},
|
||||
}
|
||||
err = s.Initialize()
|
||||
require.NoError(t, err)
|
||||
defer s.Close()
|
||||
|
||||
u, err := url.Parse("http://myuser:mypass@localhost:9996/list")
|
||||
require.NoError(t, err)
|
||||
|
||||
v := url.Values{}
|
||||
v.Set("path", "mypath")
|
||||
|
||||
u := &url.URL{
|
||||
Scheme: "http",
|
||||
Host: "localhost:9996",
|
||||
Path: "/list",
|
||||
RawQuery: v.Encode(),
|
||||
}
|
||||
u.RawQuery = v.Encode()
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
||||
require.NoError(t, err)
|
||||
|
|
|
@ -2,12 +2,15 @@
|
|||
package pprof
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
// start pprof
|
||||
_ "net/http/pprof"
|
||||
|
||||
"github.com/bluenviron/mediamtx/internal/auth"
|
||||
"github.com/bluenviron/mediamtx/internal/conf"
|
||||
"github.com/bluenviron/mediamtx/internal/logger"
|
||||
"github.com/bluenviron/mediamtx/internal/protocols/httpp"
|
||||
|
@ -22,6 +25,7 @@ type pprofParent interface {
|
|||
type PPROF struct {
|
||||
Address string
|
||||
ReadTimeout conf.StringDuration
|
||||
AuthManager *auth.Manager
|
||||
Parent pprofParent
|
||||
|
||||
httpServer *httpp.WrappedServer
|
||||
|
@ -38,7 +42,7 @@ func (pp *PPROF) Initialize() error {
|
|||
time.Duration(pp.ReadTimeout),
|
||||
"",
|
||||
"",
|
||||
http.DefaultServeMux,
|
||||
pp,
|
||||
pp,
|
||||
)
|
||||
if err != nil {
|
||||
|
@ -60,3 +64,31 @@ func (pp *PPROF) Close() {
|
|||
func (pp *PPROF) Log(level logger.Level, format string, args ...interface{}) {
|
||||
pp.Parent.Log(level, "[pprof] "+format, args...)
|
||||
}
|
||||
|
||||
func (pp *PPROF) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
user, pass, hasCredentials := r.BasicAuth()
|
||||
|
||||
ip, _, _ := net.SplitHostPort(strings.TrimSpace(r.RemoteAddr))
|
||||
|
||||
err := pp.AuthManager.Authenticate(&auth.Request{
|
||||
User: user,
|
||||
Pass: pass,
|
||||
IP: net.ParseIP(ip),
|
||||
Action: conf.AuthActionMetrics,
|
||||
})
|
||||
if err != nil {
|
||||
if !hasCredentials {
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="mediamtx"`)
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// wait some seconds to mitigate brute force attacks
|
||||
<-time.After(auth.PauseAfterError)
|
||||
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
http.DefaultServeMux.ServeHTTP(w, r)
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/bluenviron/mediamtx/internal/auth"
|
||||
"github.com/bluenviron/mediamtx/internal/conf"
|
||||
"github.com/bluenviron/mediamtx/internal/defs"
|
||||
"github.com/bluenviron/mediamtx/internal/logger"
|
||||
|
@ -19,10 +20,6 @@ import (
|
|||
"github.com/bluenviron/mediamtx/internal/restrictnetwork"
|
||||
)
|
||||
|
||||
const (
|
||||
pauseAfterAuthError = 2 * time.Second
|
||||
)
|
||||
|
||||
//go:generate go run ./hlsjsdownloader
|
||||
|
||||
//go:embed index.html
|
||||
|
@ -38,7 +35,7 @@ type httpServer struct {
|
|||
serverKey string
|
||||
serverCert string
|
||||
allowOrigin string
|
||||
trustedProxies conf.IPsOrCIDRs
|
||||
trustedProxies conf.IPNetworks
|
||||
readTimeout conf.StringDuration
|
||||
pathManager serverPathManager
|
||||
parent *Server
|
||||
|
@ -158,11 +155,11 @@ func (s *httpServer) onRequest(ctx *gin.Context) {
|
|||
IP: net.ParseIP(ctx.ClientIP()),
|
||||
User: user,
|
||||
Pass: pass,
|
||||
Proto: defs.AuthProtocolHLS,
|
||||
Proto: auth.ProtocolHLS,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
var terr defs.AuthenticationError
|
||||
var terr auth.Error
|
||||
if errors.As(err, &terr) {
|
||||
if !hasCredentials {
|
||||
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
|
||||
|
@ -173,7 +170,7 @@ func (s *httpServer) onRequest(ctx *gin.Context) {
|
|||
s.Log(logger.Info, "connection %v failed to authenticate: %v", httpp.RemoteAddr(ctx), terr.Message)
|
||||
|
||||
// wait some seconds to mitigate brute force attacks
|
||||
<-time.After(pauseAfterAuthError)
|
||||
<-time.After(auth.PauseAfterError)
|
||||
|
||||
ctx.Writer.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
|
|
|
@ -46,20 +46,19 @@ type muxerGetInstanceReq struct {
|
|||
}
|
||||
|
||||
type muxer struct {
|
||||
parentCtx context.Context
|
||||
remoteAddr string
|
||||
externalAuthenticationURL string
|
||||
variant conf.HLSVariant
|
||||
segmentCount int
|
||||
segmentDuration conf.StringDuration
|
||||
partDuration conf.StringDuration
|
||||
segmentMaxSize conf.StringSize
|
||||
directory string
|
||||
writeQueueSize int
|
||||
wg *sync.WaitGroup
|
||||
pathName string
|
||||
pathManager serverPathManager
|
||||
parent *Server
|
||||
parentCtx context.Context
|
||||
remoteAddr string
|
||||
variant conf.HLSVariant
|
||||
segmentCount int
|
||||
segmentDuration conf.StringDuration
|
||||
partDuration conf.StringDuration
|
||||
segmentMaxSize conf.StringSize
|
||||
directory string
|
||||
writeQueueSize int
|
||||
wg *sync.WaitGroup
|
||||
pathName string
|
||||
pathManager serverPathManager
|
||||
parent *Server
|
||||
|
||||
ctx context.Context
|
||||
ctxCancel func()
|
||||
|
|
|
@ -59,24 +59,23 @@ type serverParent interface {
|
|||
|
||||
// Server is a HLS server.
|
||||
type Server struct {
|
||||
Address string
|
||||
Encryption bool
|
||||
ServerKey string
|
||||
ServerCert string
|
||||
ExternalAuthenticationURL string
|
||||
AlwaysRemux bool
|
||||
Variant conf.HLSVariant
|
||||
SegmentCount int
|
||||
SegmentDuration conf.StringDuration
|
||||
PartDuration conf.StringDuration
|
||||
SegmentMaxSize conf.StringSize
|
||||
AllowOrigin string
|
||||
TrustedProxies conf.IPsOrCIDRs
|
||||
Directory string
|
||||
ReadTimeout conf.StringDuration
|
||||
WriteQueueSize int
|
||||
PathManager serverPathManager
|
||||
Parent serverParent
|
||||
Address string
|
||||
Encryption bool
|
||||
ServerKey string
|
||||
ServerCert string
|
||||
AlwaysRemux bool
|
||||
Variant conf.HLSVariant
|
||||
SegmentCount int
|
||||
SegmentDuration conf.StringDuration
|
||||
PartDuration conf.StringDuration
|
||||
SegmentMaxSize conf.StringSize
|
||||
AllowOrigin string
|
||||
TrustedProxies conf.IPNetworks
|
||||
Directory string
|
||||
ReadTimeout conf.StringDuration
|
||||
WriteQueueSize int
|
||||
PathManager serverPathManager
|
||||
Parent serverParent
|
||||
|
||||
ctx context.Context
|
||||
ctxCancel func()
|
||||
|
@ -218,20 +217,19 @@ outer:
|
|||
|
||||
func (s *Server) createMuxer(pathName string, remoteAddr string) *muxer {
|
||||
r := &muxer{
|
||||
parentCtx: s.ctx,
|
||||
remoteAddr: remoteAddr,
|
||||
externalAuthenticationURL: s.ExternalAuthenticationURL,
|
||||
variant: s.Variant,
|
||||
segmentCount: s.SegmentCount,
|
||||
segmentDuration: s.SegmentDuration,
|
||||
partDuration: s.PartDuration,
|
||||
segmentMaxSize: s.SegmentMaxSize,
|
||||
directory: s.Directory,
|
||||
writeQueueSize: s.WriteQueueSize,
|
||||
wg: &s.wg,
|
||||
pathName: pathName,
|
||||
pathManager: s.PathManager,
|
||||
parent: s,
|
||||
parentCtx: s.ctx,
|
||||
remoteAddr: remoteAddr,
|
||||
variant: s.Variant,
|
||||
segmentCount: s.SegmentCount,
|
||||
segmentDuration: s.SegmentDuration,
|
||||
partDuration: s.PartDuration,
|
||||
segmentMaxSize: s.SegmentMaxSize,
|
||||
directory: s.Directory,
|
||||
writeQueueSize: s.WriteQueueSize,
|
||||
wg: &s.wg,
|
||||
pathName: pathName,
|
||||
pathManager: s.PathManager,
|
||||
parent: s,
|
||||
}
|
||||
r.initialize()
|
||||
s.muxers[pathName] = r
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"github.com/bluenviron/gohlslib/pkg/codecs"
|
||||
"github.com/bluenviron/gortsplib/v4/pkg/description"
|
||||
"github.com/bluenviron/mediacommon/pkg/codecs/h264"
|
||||
"github.com/bluenviron/mediamtx/internal/auth"
|
||||
"github.com/bluenviron/mediamtx/internal/conf"
|
||||
"github.com/bluenviron/mediamtx/internal/defs"
|
||||
"github.com/bluenviron/mediamtx/internal/externalcmd"
|
||||
|
@ -50,7 +51,10 @@ type dummyPathManager struct {
|
|||
stream *stream.Stream
|
||||
}
|
||||
|
||||
func (pm *dummyPathManager) FindPathConf(_ defs.PathFindPathConfReq) (*conf.Path, error) {
|
||||
func (pm *dummyPathManager) FindPathConf(req defs.PathFindPathConfReq) (*conf.Path, error) {
|
||||
if req.AccessRequest.User != "myuser" || req.AccessRequest.Pass != "mypass" {
|
||||
return nil, auth.Error{}
|
||||
}
|
||||
return &conf.Path{}, nil
|
||||
}
|
||||
|
||||
|
@ -68,24 +72,23 @@ func TestServerNotFound(t *testing.T) {
|
|||
} {
|
||||
t.Run(ca, func(t *testing.T) {
|
||||
s := &Server{
|
||||
Address: "127.0.0.1:8888",
|
||||
Encryption: false,
|
||||
ServerKey: "",
|
||||
ServerCert: "",
|
||||
ExternalAuthenticationURL: "",
|
||||
AlwaysRemux: ca == "always remux on",
|
||||
Variant: conf.HLSVariant(gohlslib.MuxerVariantMPEGTS),
|
||||
SegmentCount: 7,
|
||||
SegmentDuration: conf.StringDuration(1 * time.Second),
|
||||
PartDuration: conf.StringDuration(200 * time.Millisecond),
|
||||
SegmentMaxSize: 50 * 1024 * 1024,
|
||||
AllowOrigin: "",
|
||||
TrustedProxies: conf.IPsOrCIDRs{},
|
||||
Directory: "",
|
||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
||||
WriteQueueSize: 512,
|
||||
PathManager: &dummyPathManager{},
|
||||
Parent: &test.NilLogger{},
|
||||
Address: "127.0.0.1:8888",
|
||||
Encryption: false,
|
||||
ServerKey: "",
|
||||
ServerCert: "",
|
||||
AlwaysRemux: ca == "always remux on",
|
||||
Variant: conf.HLSVariant(gohlslib.MuxerVariantMPEGTS),
|
||||
SegmentCount: 7,
|
||||
SegmentDuration: conf.StringDuration(1 * time.Second),
|
||||
PartDuration: conf.StringDuration(200 * time.Millisecond),
|
||||
SegmentMaxSize: 50 * 1024 * 1024,
|
||||
AllowOrigin: "",
|
||||
TrustedProxies: conf.IPNetworks{},
|
||||
Directory: "",
|
||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
||||
WriteQueueSize: 512,
|
||||
PathManager: &dummyPathManager{},
|
||||
Parent: &test.NilLogger{},
|
||||
}
|
||||
err := s.Initialize()
|
||||
require.NoError(t, err)
|
||||
|
@ -94,7 +97,7 @@ func TestServerNotFound(t *testing.T) {
|
|||
hc := &http.Client{Transport: &http.Transport{}}
|
||||
|
||||
func() {
|
||||
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8888/nonexisting/", nil)
|
||||
req, err := http.NewRequest(http.MethodGet, "http://myuser:mypass@127.0.0.1:8888/nonexisting/", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := hc.Do(req)
|
||||
|
@ -104,7 +107,7 @@ func TestServerNotFound(t *testing.T) {
|
|||
}()
|
||||
|
||||
func() {
|
||||
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8888/nonexisting/index.m3u8", nil)
|
||||
req, err := http.NewRequest(http.MethodGet, "http://myuser:mypass@127.0.0.1:8888/nonexisting/index.m3u8", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := hc.Do(req)
|
||||
|
@ -131,31 +134,30 @@ func TestServerRead(t *testing.T) {
|
|||
pathManager := &dummyPathManager{stream: stream}
|
||||
|
||||
s := &Server{
|
||||
Address: "127.0.0.1:8888",
|
||||
Encryption: false,
|
||||
ServerKey: "",
|
||||
ServerCert: "",
|
||||
ExternalAuthenticationURL: "",
|
||||
AlwaysRemux: false,
|
||||
Variant: conf.HLSVariant(gohlslib.MuxerVariantMPEGTS),
|
||||
SegmentCount: 7,
|
||||
SegmentDuration: conf.StringDuration(1 * time.Second),
|
||||
PartDuration: conf.StringDuration(200 * time.Millisecond),
|
||||
SegmentMaxSize: 50 * 1024 * 1024,
|
||||
AllowOrigin: "",
|
||||
TrustedProxies: conf.IPsOrCIDRs{},
|
||||
Directory: "",
|
||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
||||
WriteQueueSize: 512,
|
||||
PathManager: pathManager,
|
||||
Parent: &test.NilLogger{},
|
||||
Address: "127.0.0.1:8888",
|
||||
Encryption: false,
|
||||
ServerKey: "",
|
||||
ServerCert: "",
|
||||
AlwaysRemux: false,
|
||||
Variant: conf.HLSVariant(gohlslib.MuxerVariantMPEGTS),
|
||||
SegmentCount: 7,
|
||||
SegmentDuration: conf.StringDuration(1 * time.Second),
|
||||
PartDuration: conf.StringDuration(200 * time.Millisecond),
|
||||
SegmentMaxSize: 50 * 1024 * 1024,
|
||||
AllowOrigin: "",
|
||||
TrustedProxies: conf.IPNetworks{},
|
||||
Directory: "",
|
||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
||||
WriteQueueSize: 512,
|
||||
PathManager: pathManager,
|
||||
Parent: &test.NilLogger{},
|
||||
}
|
||||
err = s.Initialize()
|
||||
require.NoError(t, err)
|
||||
defer s.Close()
|
||||
|
||||
c := &gohlslib.Client{
|
||||
URI: "http://127.0.0.1:8888/mystream/index.m3u8",
|
||||
URI: "http://myuser:mypass@127.0.0.1:8888/mystream/index.m3u8",
|
||||
}
|
||||
|
||||
recv := make(chan struct{})
|
||||
|
@ -217,24 +219,23 @@ func TestServerRead(t *testing.T) {
|
|||
pathManager := &dummyPathManager{stream: stream}
|
||||
|
||||
s := &Server{
|
||||
Address: "127.0.0.1:8888",
|
||||
Encryption: false,
|
||||
ServerKey: "",
|
||||
ServerCert: "",
|
||||
ExternalAuthenticationURL: "",
|
||||
AlwaysRemux: true,
|
||||
Variant: conf.HLSVariant(gohlslib.MuxerVariantMPEGTS),
|
||||
SegmentCount: 7,
|
||||
SegmentDuration: conf.StringDuration(1 * time.Second),
|
||||
PartDuration: conf.StringDuration(200 * time.Millisecond),
|
||||
SegmentMaxSize: 50 * 1024 * 1024,
|
||||
AllowOrigin: "",
|
||||
TrustedProxies: conf.IPsOrCIDRs{},
|
||||
Directory: "",
|
||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
||||
WriteQueueSize: 512,
|
||||
PathManager: pathManager,
|
||||
Parent: &test.NilLogger{},
|
||||
Address: "127.0.0.1:8888",
|
||||
Encryption: false,
|
||||
ServerKey: "",
|
||||
ServerCert: "",
|
||||
AlwaysRemux: true,
|
||||
Variant: conf.HLSVariant(gohlslib.MuxerVariantMPEGTS),
|
||||
SegmentCount: 7,
|
||||
SegmentDuration: conf.StringDuration(1 * time.Second),
|
||||
PartDuration: conf.StringDuration(200 * time.Millisecond),
|
||||
SegmentMaxSize: 50 * 1024 * 1024,
|
||||
AllowOrigin: "",
|
||||
TrustedProxies: conf.IPNetworks{},
|
||||
Directory: "",
|
||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
||||
WriteQueueSize: 512,
|
||||
PathManager: pathManager,
|
||||
Parent: &test.NilLogger{},
|
||||
}
|
||||
err = s.Initialize()
|
||||
require.NoError(t, err)
|
||||
|
@ -257,7 +258,7 @@ func TestServerRead(t *testing.T) {
|
|||
}
|
||||
|
||||
c := &gohlslib.Client{
|
||||
URI: "http://127.0.0.1:8888/mystream/index.m3u8",
|
||||
URI: "http://myuser:mypass@127.0.0.1:8888/mystream/index.m3u8",
|
||||
}
|
||||
|
||||
recv := make(chan struct{})
|
||||
|
|
|
@ -18,6 +18,7 @@ import (
|
|||
"github.com/google/uuid"
|
||||
|
||||
"github.com/bluenviron/mediamtx/internal/asyncwriter"
|
||||
"github.com/bluenviron/mediamtx/internal/auth"
|
||||
"github.com/bluenviron/mediamtx/internal/conf"
|
||||
"github.com/bluenviron/mediamtx/internal/defs"
|
||||
"github.com/bluenviron/mediamtx/internal/externalcmd"
|
||||
|
@ -28,10 +29,6 @@ import (
|
|||
"github.com/bluenviron/mediamtx/internal/unit"
|
||||
)
|
||||
|
||||
const (
|
||||
pauseAfterAuthError = 2 * time.Second
|
||||
)
|
||||
|
||||
var errNoSupportedCodecs = errors.New(
|
||||
"the stream doesn't contain any supported codec, which are currently H264, MPEG-4 Audio, MPEG-1/2 Audio")
|
||||
|
||||
|
@ -176,15 +173,15 @@ func (c *conn) runRead(conn *rtmp.Conn, u *url.URL) error {
|
|||
IP: c.ip(),
|
||||
User: query.Get("user"),
|
||||
Pass: query.Get("pass"),
|
||||
Proto: defs.AuthProtocolRTMP,
|
||||
Proto: auth.ProtocolRTMP,
|
||||
ID: &c.uuid,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
var terr defs.AuthenticationError
|
||||
var terr auth.Error
|
||||
if errors.As(err, &terr) {
|
||||
// wait some seconds to mitigate brute force attacks
|
||||
<-time.After(pauseAfterAuthError)
|
||||
<-time.After(auth.PauseAfterError)
|
||||
return terr
|
||||
}
|
||||
return err
|
||||
|
@ -405,15 +402,15 @@ func (c *conn) runPublish(conn *rtmp.Conn, u *url.URL) error {
|
|||
IP: c.ip(),
|
||||
User: query.Get("user"),
|
||||
Pass: query.Get("pass"),
|
||||
Proto: defs.AuthProtocolRTMP,
|
||||
Proto: auth.ProtocolRTMP,
|
||||
ID: &c.uuid,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
var terr defs.AuthenticationError
|
||||
var terr auth.Error
|
||||
if errors.As(err, &terr) {
|
||||
// wait some seconds to mitigate brute force attacks
|
||||
<-time.After(pauseAfterAuthError)
|
||||
<-time.After(auth.PauseAfterError)
|
||||
return terr
|
||||
}
|
||||
return err
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
|
||||
"github.com/bluenviron/gortsplib/v4/pkg/description"
|
||||
"github.com/bluenviron/mediamtx/internal/asyncwriter"
|
||||
"github.com/bluenviron/mediamtx/internal/auth"
|
||||
"github.com/bluenviron/mediamtx/internal/conf"
|
||||
"github.com/bluenviron/mediamtx/internal/defs"
|
||||
"github.com/bluenviron/mediamtx/internal/externalcmd"
|
||||
|
@ -65,11 +66,17 @@ type dummyPathManager struct {
|
|||
path *dummyPath
|
||||
}
|
||||
|
||||
func (pm *dummyPathManager) AddPublisher(_ defs.PathAddPublisherReq) (defs.Path, error) {
|
||||
func (pm *dummyPathManager) AddPublisher(req defs.PathAddPublisherReq) (defs.Path, error) {
|
||||
if req.AccessRequest.User != "myuser" || req.AccessRequest.Pass != "mypass" {
|
||||
return nil, auth.Error{}
|
||||
}
|
||||
return pm.path, nil
|
||||
}
|
||||
|
||||
func (pm *dummyPathManager) AddReader(_ defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
|
||||
func (pm *dummyPathManager) AddReader(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
|
||||
if req.AccessRequest.User != "myuser" || req.AccessRequest.Pass != "mypass" {
|
||||
return nil, nil, auth.Error{}
|
||||
}
|
||||
return pm.path, pm.path.stream, nil
|
||||
}
|
||||
|
||||
|
@ -119,7 +126,7 @@ func TestServerPublish(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
defer s.Close()
|
||||
|
||||
u, err := url.Parse("rtmp://127.0.0.1:1935/teststream?user=testpublisher&pass=testpass¶m=value")
|
||||
u, err := url.Parse("rtmp://127.0.0.1:1935/teststream?user=myuser&pass=mypass¶m=value")
|
||||
require.NoError(t, err)
|
||||
|
||||
nconn, err := func() (net.Conn, error) {
|
||||
|
@ -221,7 +228,7 @@ func TestServerRead(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
defer s.Close()
|
||||
|
||||
u, err := url.Parse("rtmp://127.0.0.1:1935/teststream?user=testreader&pass=testpass¶m=value")
|
||||
u, err := url.Parse("rtmp://127.0.0.1:1935/teststream?user=myuser&pass=mypass¶m=value")
|
||||
require.NoError(t, err)
|
||||
|
||||
nconn, err := func() (net.Conn, error) {
|
||||
|
|
|
@ -7,11 +7,12 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/bluenviron/gortsplib/v4"
|
||||
"github.com/bluenviron/gortsplib/v4/pkg/auth"
|
||||
rtspauth "github.com/bluenviron/gortsplib/v4/pkg/auth"
|
||||
"github.com/bluenviron/gortsplib/v4/pkg/base"
|
||||
"github.com/bluenviron/gortsplib/v4/pkg/headers"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/bluenviron/mediamtx/internal/auth"
|
||||
"github.com/bluenviron/mediamtx/internal/conf"
|
||||
"github.com/bluenviron/mediamtx/internal/defs"
|
||||
"github.com/bluenviron/mediamtx/internal/externalcmd"
|
||||
|
@ -20,7 +21,7 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
pauseAfterAuthError = 2 * time.Second
|
||||
rtspAuthRealm = "IPCAM"
|
||||
)
|
||||
|
||||
type conn struct {
|
||||
|
@ -118,7 +119,7 @@ func (c *conn) onDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx,
|
|||
|
||||
if c.authNonce == "" {
|
||||
var err error
|
||||
c.authNonce, err = auth.GenerateNonce()
|
||||
c.authNonce, err = rtspauth.GenerateNonce()
|
||||
if err != nil {
|
||||
return &base.Response{
|
||||
StatusCode: base.StatusInternalServerError,
|
||||
|
@ -131,7 +132,7 @@ func (c *conn) onDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx,
|
|||
Name: ctx.Path,
|
||||
Query: ctx.Query,
|
||||
IP: c.ip(),
|
||||
Proto: defs.AuthProtocolRTSP,
|
||||
Proto: auth.ProtocolRTSP,
|
||||
ID: &c.uuid,
|
||||
RTSPRequest: ctx.Request,
|
||||
RTSPNonce: c.authNonce,
|
||||
|
@ -139,7 +140,7 @@ func (c *conn) onDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx,
|
|||
})
|
||||
|
||||
if res.Err != nil {
|
||||
var terr defs.AuthenticationError
|
||||
var terr auth.Error
|
||||
if errors.As(res.Err, &terr) {
|
||||
res, err := c.handleAuthError(terr)
|
||||
return res, nil, err
|
||||
|
@ -191,13 +192,13 @@ func (c *conn) handleAuthError(authErr error) (*base.Response, error) {
|
|||
return &base.Response{
|
||||
StatusCode: base.StatusUnauthorized,
|
||||
Header: base.Header{
|
||||
"WWW-Authenticate": auth.GenerateWWWAuthenticate(c.authMethods, "IPCAM", c.authNonce),
|
||||
"WWW-Authenticate": rtspauth.GenerateWWWAuthenticate(c.authMethods, rtspAuthRealm, c.authNonce),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// wait some seconds to mitigate brute force attacks
|
||||
<-time.After(pauseAfterAuthError)
|
||||
<-time.After(auth.PauseAfterError)
|
||||
|
||||
return &base.Response{
|
||||
StatusCode: base.StatusUnauthorized,
|
||||
|
|
|
@ -48,7 +48,9 @@ func (p *dummyPath) StartPublisher(req defs.PathStartPublisherReq) (*stream.Stre
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
close(p.streamCreated)
|
||||
|
||||
return p.stream, nil
|
||||
}
|
||||
|
||||
|
@ -123,7 +125,7 @@ func TestServerPublish(t *testing.T) {
|
|||
media0 := test.UniqueMediaH264()
|
||||
|
||||
err = source.StartRecording(
|
||||
"rtsp://testpublisher:testpass@127.0.0.1:8557/teststream?param=value",
|
||||
"rtsp://myuser:mypass@127.0.0.1:8557/teststream?param=value",
|
||||
&description.Session{Medias: []*description.Media{media0}})
|
||||
require.NoError(t, err)
|
||||
defer source.Close()
|
||||
|
@ -211,7 +213,7 @@ func TestServerRead(t *testing.T) {
|
|||
|
||||
reader := gortsplib.Client{}
|
||||
|
||||
u, err := base.ParseURL("rtsp://testreader:testpass@127.0.0.1:8557/teststream?param=value")
|
||||
u, err := base.ParseURL("rtsp://myuser:mypass@127.0.0.1:8557/teststream?param=value")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = reader.Start(u.Scheme, u.Host)
|
||||
|
|
|
@ -9,11 +9,12 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/bluenviron/gortsplib/v4"
|
||||
"github.com/bluenviron/gortsplib/v4/pkg/auth"
|
||||
rtspauth "github.com/bluenviron/gortsplib/v4/pkg/auth"
|
||||
"github.com/bluenviron/gortsplib/v4/pkg/base"
|
||||
"github.com/google/uuid"
|
||||
"github.com/pion/rtp"
|
||||
|
||||
"github.com/bluenviron/mediamtx/internal/auth"
|
||||
"github.com/bluenviron/mediamtx/internal/conf"
|
||||
"github.com/bluenviron/mediamtx/internal/defs"
|
||||
"github.com/bluenviron/mediamtx/internal/externalcmd"
|
||||
|
@ -102,7 +103,7 @@ func (s *session) onAnnounce(c *conn, ctx *gortsplib.ServerHandlerOnAnnounceCtx)
|
|||
|
||||
if c.authNonce == "" {
|
||||
var err error
|
||||
c.authNonce, err = auth.GenerateNonce()
|
||||
c.authNonce, err = rtspauth.GenerateNonce()
|
||||
if err != nil {
|
||||
return &base.Response{
|
||||
StatusCode: base.StatusInternalServerError,
|
||||
|
@ -117,7 +118,7 @@ func (s *session) onAnnounce(c *conn, ctx *gortsplib.ServerHandlerOnAnnounceCtx)
|
|||
Query: ctx.Query,
|
||||
Publish: true,
|
||||
IP: c.ip(),
|
||||
Proto: defs.AuthProtocolRTSP,
|
||||
Proto: auth.ProtocolRTSP,
|
||||
ID: &c.uuid,
|
||||
RTSPRequest: ctx.Request,
|
||||
RTSPBaseURL: nil,
|
||||
|
@ -125,7 +126,7 @@ func (s *session) onAnnounce(c *conn, ctx *gortsplib.ServerHandlerOnAnnounceCtx)
|
|||
},
|
||||
})
|
||||
if err != nil {
|
||||
var terr defs.AuthenticationError
|
||||
var terr auth.Error
|
||||
if errors.As(err, &terr) {
|
||||
return c.handleAuthError(terr)
|
||||
}
|
||||
|
@ -187,7 +188,7 @@ func (s *session) onSetup(c *conn, ctx *gortsplib.ServerHandlerOnSetupCtx,
|
|||
|
||||
if c.authNonce == "" {
|
||||
var err error
|
||||
c.authNonce, err = auth.GenerateNonce()
|
||||
c.authNonce, err = rtspauth.GenerateNonce()
|
||||
if err != nil {
|
||||
return &base.Response{
|
||||
StatusCode: base.StatusInternalServerError,
|
||||
|
@ -201,7 +202,7 @@ func (s *session) onSetup(c *conn, ctx *gortsplib.ServerHandlerOnSetupCtx,
|
|||
Name: ctx.Path,
|
||||
Query: ctx.Query,
|
||||
IP: c.ip(),
|
||||
Proto: defs.AuthProtocolRTSP,
|
||||
Proto: auth.ProtocolRTSP,
|
||||
ID: &c.uuid,
|
||||
RTSPRequest: ctx.Request,
|
||||
RTSPBaseURL: baseURL,
|
||||
|
@ -209,7 +210,7 @@ func (s *session) onSetup(c *conn, ctx *gortsplib.ServerHandlerOnSetupCtx,
|
|||
},
|
||||
})
|
||||
if err != nil {
|
||||
var terr defs.AuthenticationError
|
||||
var terr auth.Error
|
||||
if errors.As(err, &terr) {
|
||||
res, err := c.handleAuthError(terr)
|
||||
return res, nil, err
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
"github.com/google/uuid"
|
||||
|
||||
"github.com/bluenviron/mediamtx/internal/asyncwriter"
|
||||
"github.com/bluenviron/mediamtx/internal/auth"
|
||||
"github.com/bluenviron/mediamtx/internal/conf"
|
||||
"github.com/bluenviron/mediamtx/internal/defs"
|
||||
"github.com/bluenviron/mediamtx/internal/externalcmd"
|
||||
|
@ -24,10 +25,6 @@ import (
|
|||
"github.com/bluenviron/mediamtx/internal/stream"
|
||||
)
|
||||
|
||||
const (
|
||||
pauseAfterAuthError = 2 * time.Second
|
||||
)
|
||||
|
||||
func srtCheckPassphrase(connReq srt.ConnRequest, passphrase string) error {
|
||||
if passphrase == "" {
|
||||
return nil
|
||||
|
@ -171,16 +168,16 @@ func (c *conn) runPublish(req srtNewConnReq, streamID *streamID) (bool, error) {
|
|||
Publish: true,
|
||||
User: streamID.user,
|
||||
Pass: streamID.pass,
|
||||
Proto: defs.AuthProtocolSRT,
|
||||
Proto: auth.ProtocolSRT,
|
||||
ID: &c.uuid,
|
||||
Query: streamID.query,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
var terr defs.AuthenticationError
|
||||
var terr auth.Error
|
||||
if errors.As(err, &terr) {
|
||||
// wait some seconds to mitigate brute force attacks
|
||||
<-time.After(pauseAfterAuthError)
|
||||
<-time.After(auth.PauseAfterError)
|
||||
return false, terr
|
||||
}
|
||||
return false, err
|
||||
|
@ -267,16 +264,16 @@ func (c *conn) runRead(req srtNewConnReq, streamID *streamID) (bool, error) {
|
|||
IP: c.ip(),
|
||||
User: streamID.user,
|
||||
Pass: streamID.pass,
|
||||
Proto: defs.AuthProtocolSRT,
|
||||
Proto: auth.ProtocolSRT,
|
||||
ID: &c.uuid,
|
||||
Query: streamID.query,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
var terr defs.AuthenticationError
|
||||
var terr auth.Error
|
||||
if errors.As(err, &terr) {
|
||||
// wait some seconds to mitigate brute force attacks
|
||||
<-time.After(pauseAfterAuthError)
|
||||
<-time.After(auth.PauseAfterError)
|
||||
return false, err
|
||||
}
|
||||
return false, err
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"github.com/bluenviron/gortsplib/v4/pkg/description"
|
||||
"github.com/bluenviron/mediacommon/pkg/formats/mpegts"
|
||||
"github.com/bluenviron/mediamtx/internal/asyncwriter"
|
||||
"github.com/bluenviron/mediamtx/internal/auth"
|
||||
"github.com/bluenviron/mediamtx/internal/conf"
|
||||
"github.com/bluenviron/mediamtx/internal/defs"
|
||||
"github.com/bluenviron/mediamtx/internal/externalcmd"
|
||||
|
@ -63,11 +64,17 @@ type dummyPathManager struct {
|
|||
path *dummyPath
|
||||
}
|
||||
|
||||
func (pm *dummyPathManager) AddPublisher(_ defs.PathAddPublisherReq) (defs.Path, error) {
|
||||
func (pm *dummyPathManager) AddPublisher(req defs.PathAddPublisherReq) (defs.Path, error) {
|
||||
if req.AccessRequest.User != "myuser" || req.AccessRequest.Pass != "mypass" {
|
||||
return nil, auth.Error{}
|
||||
}
|
||||
return pm.path, nil
|
||||
}
|
||||
|
||||
func (pm *dummyPathManager) AddReader(_ defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
|
||||
func (pm *dummyPathManager) AddReader(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
|
||||
if req.AccessRequest.User != "myuser" || req.AccessRequest.Pass != "mypass" {
|
||||
return nil, nil, auth.Error{}
|
||||
}
|
||||
return pm.path, pm.path.stream, nil
|
||||
}
|
||||
|
||||
|
@ -99,7 +106,7 @@ func TestServerPublish(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
defer s.Close()
|
||||
|
||||
u := "srt://localhost:8890?streamid=publish:mypath"
|
||||
u := "srt://localhost:8890?streamid=publish:mypath:myuser:mypass"
|
||||
|
||||
srtConf := srt.DefaultConfig()
|
||||
address, err := srtConf.UnmarshalURL(u)
|
||||
|
@ -198,7 +205,7 @@ func TestServerRead(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
defer s.Close()
|
||||
|
||||
u := "srt://localhost:8890?streamid=read:mypath"
|
||||
u := "srt://localhost:8890?streamid=read:mypath:myuser:mypass"
|
||||
|
||||
srtConf := srt.DefaultConfig()
|
||||
address, err := srtConf.UnmarshalURL(u)
|
||||
|
|
|
@ -14,6 +14,7 @@ import (
|
|||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/bluenviron/mediamtx/internal/auth"
|
||||
"github.com/bluenviron/mediamtx/internal/conf"
|
||||
"github.com/bluenviron/mediamtx/internal/defs"
|
||||
"github.com/bluenviron/mediamtx/internal/logger"
|
||||
|
@ -56,7 +57,7 @@ type httpServer struct {
|
|||
serverKey string
|
||||
serverCert string
|
||||
allowOrigin string
|
||||
trustedProxies conf.IPsOrCIDRs
|
||||
trustedProxies conf.IPNetworks
|
||||
readTimeout conf.StringDuration
|
||||
pathManager serverPathManager
|
||||
parent *Server
|
||||
|
@ -117,11 +118,11 @@ func (s *httpServer) checkAuthOutsideSession(ctx *gin.Context, path string, publ
|
|||
IP: net.ParseIP(ctx.ClientIP()),
|
||||
User: user,
|
||||
Pass: pass,
|
||||
Proto: defs.AuthProtocolWebRTC,
|
||||
Proto: auth.ProtocolWebRTC,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
var terr defs.AuthenticationError
|
||||
var terr auth.Error
|
||||
if errors.As(err, &terr) {
|
||||
if !hasCredentials {
|
||||
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
|
||||
|
@ -132,7 +133,7 @@ func (s *httpServer) checkAuthOutsideSession(ctx *gin.Context, path string, publ
|
|||
s.Log(logger.Info, "connection %v failed to authenticate: %v", httpp.RemoteAddr(ctx), terr.Message)
|
||||
|
||||
// wait some seconds to mitigate brute force attacks
|
||||
<-time.After(pauseAfterAuthError)
|
||||
<-time.After(auth.PauseAfterError)
|
||||
|
||||
writeError(ctx, http.StatusUnauthorized, terr)
|
||||
return false
|
||||
|
|
|
@ -30,7 +30,6 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
pauseAfterAuthError = 2 * time.Second
|
||||
webrtcTurnSecretExpiration = 24 * 3600 * time.Second
|
||||
webrtcPayloadMaxSize = 1188 // 1200 - 12 (RTP header)
|
||||
)
|
||||
|
@ -182,7 +181,7 @@ type Server struct {
|
|||
ServerKey string
|
||||
ServerCert string
|
||||
AllowOrigin string
|
||||
TrustedProxies conf.IPsOrCIDRs
|
||||
TrustedProxies conf.IPNetworks
|
||||
ReadTimeout conf.StringDuration
|
||||
WriteQueueSize int
|
||||
LocalUDPAddress string
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
|
||||
"github.com/bluenviron/gortsplib/v4/pkg/description"
|
||||
"github.com/bluenviron/mediamtx/internal/asyncwriter"
|
||||
"github.com/bluenviron/mediamtx/internal/auth"
|
||||
"github.com/bluenviron/mediamtx/internal/conf"
|
||||
"github.com/bluenviron/mediamtx/internal/defs"
|
||||
"github.com/bluenviron/mediamtx/internal/externalcmd"
|
||||
|
@ -71,7 +72,10 @@ type dummyPathManager struct {
|
|||
path *dummyPath
|
||||
}
|
||||
|
||||
func (pm *dummyPathManager) FindPathConf(_ defs.PathFindPathConfReq) (*conf.Path, error) {
|
||||
func (pm *dummyPathManager) FindPathConf(req defs.PathFindPathConfReq) (*conf.Path, error) {
|
||||
if req.AccessRequest.User != "myuser" || req.AccessRequest.Pass != "mypass" {
|
||||
return nil, auth.Error{}
|
||||
}
|
||||
return &conf.Path{}, nil
|
||||
}
|
||||
|
||||
|
@ -93,7 +97,7 @@ func TestServerStaticPages(t *testing.T) {
|
|||
ServerKey: "",
|
||||
ServerCert: "",
|
||||
AllowOrigin: "",
|
||||
TrustedProxies: conf.IPsOrCIDRs{},
|
||||
TrustedProxies: conf.IPNetworks{},
|
||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
||||
WriteQueueSize: 512,
|
||||
LocalUDPAddress: "127.0.0.1:8887",
|
||||
|
@ -114,7 +118,7 @@ func TestServerStaticPages(t *testing.T) {
|
|||
|
||||
for _, path := range []string{"/stream", "/stream/publish", "/publish"} {
|
||||
func() {
|
||||
req, err := http.NewRequest(http.MethodGet, "http://localhost:8886"+path, nil)
|
||||
req, err := http.NewRequest(http.MethodGet, "http://myuser:mypass@localhost:8886"+path, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := hc.Do(req)
|
||||
|
@ -139,7 +143,7 @@ func TestServerPublish(t *testing.T) {
|
|||
ServerKey: "",
|
||||
ServerCert: "",
|
||||
AllowOrigin: "",
|
||||
TrustedProxies: conf.IPsOrCIDRs{},
|
||||
TrustedProxies: conf.IPNetworks{},
|
||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
||||
WriteQueueSize: 512,
|
||||
LocalUDPAddress: "127.0.0.1:8887",
|
||||
|
@ -175,8 +179,7 @@ func TestServerPublish(t *testing.T) {
|
|||
require.Equal(t, false, ok)
|
||||
}()
|
||||
|
||||
ur := "http://"
|
||||
ur += "localhost:8886/teststream/whip?param=value"
|
||||
ur := "http://myuser:mypass@localhost:8886/teststream/whip?param=value"
|
||||
|
||||
su, err := url.Parse(ur)
|
||||
require.NoError(t, err)
|
||||
|
@ -260,7 +263,7 @@ func TestServerRead(t *testing.T) {
|
|||
ServerKey: "",
|
||||
ServerCert: "",
|
||||
AllowOrigin: "",
|
||||
TrustedProxies: conf.IPsOrCIDRs{},
|
||||
TrustedProxies: conf.IPNetworks{},
|
||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
||||
WriteQueueSize: 512,
|
||||
LocalUDPAddress: "127.0.0.1:8887",
|
||||
|
@ -277,8 +280,7 @@ func TestServerRead(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
defer s.Close()
|
||||
|
||||
ur := "http://"
|
||||
ur += "localhost:8886/teststream/whep?param=value"
|
||||
ur := "http://myuser:mypass@localhost:8886/teststream/whep?param=value"
|
||||
|
||||
u, err := url.Parse(ur)
|
||||
require.NoError(t, err)
|
||||
|
@ -351,7 +353,7 @@ func TestServerReadNotFound(t *testing.T) {
|
|||
ServerKey: "",
|
||||
ServerCert: "",
|
||||
AllowOrigin: "",
|
||||
TrustedProxies: conf.IPsOrCIDRs{},
|
||||
TrustedProxies: conf.IPNetworks{},
|
||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
||||
WriteQueueSize: 512,
|
||||
LocalUDPAddress: "127.0.0.1:8887",
|
||||
|
@ -370,7 +372,8 @@ func TestServerReadNotFound(t *testing.T) {
|
|||
|
||||
hc := &http.Client{Transport: &http.Transport{}}
|
||||
|
||||
iceServers, err := webrtc.WHIPOptionsICEServers(context.Background(), hc, "http://localhost:8886/nonexisting/whep")
|
||||
iceServers, err := webrtc.WHIPOptionsICEServers(context.Background(), hc,
|
||||
"http://myuser:mypass@localhost:8886/nonexisting/whep")
|
||||
require.NoError(t, err)
|
||||
|
||||
pc, err := pwebrtc.NewPeerConnection(pwebrtc.Configuration{
|
||||
|
@ -386,7 +389,7 @@ func TestServerReadNotFound(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost,
|
||||
"http://localhost:8886/nonexisting/whep", bytes.NewReader([]byte(offer.SDP)))
|
||||
"http://myuser:mypass@localhost:8886/nonexisting/whep", bytes.NewReader([]byte(offer.SDP)))
|
||||
require.NoError(t, err)
|
||||
|
||||
req.Header.Set("Content-Type", "application/sdp")
|
||||
|
|
|
@ -22,6 +22,7 @@ import (
|
|||
pwebrtc "github.com/pion/webrtc/v3"
|
||||
|
||||
"github.com/bluenviron/mediamtx/internal/asyncwriter"
|
||||
"github.com/bluenviron/mediamtx/internal/auth"
|
||||
"github.com/bluenviron/mediamtx/internal/defs"
|
||||
"github.com/bluenviron/mediamtx/internal/externalcmd"
|
||||
"github.com/bluenviron/mediamtx/internal/hooks"
|
||||
|
@ -374,15 +375,15 @@ func (s *session) runPublish() (int, error) {
|
|||
IP: net.ParseIP(ip),
|
||||
User: s.req.user,
|
||||
Pass: s.req.pass,
|
||||
Proto: defs.AuthProtocolWebRTC,
|
||||
Proto: auth.ProtocolWebRTC,
|
||||
ID: &s.uuid,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
var terr defs.AuthenticationError
|
||||
var terr auth.Error
|
||||
if errors.As(err, &terr) {
|
||||
// wait some seconds to mitigate brute force attacks
|
||||
<-time.After(pauseAfterAuthError)
|
||||
<-time.After(auth.PauseAfterError)
|
||||
|
||||
return http.StatusUnauthorized, err
|
||||
}
|
||||
|
@ -505,15 +506,15 @@ func (s *session) runRead() (int, error) {
|
|||
IP: net.ParseIP(ip),
|
||||
User: s.req.user,
|
||||
Pass: s.req.pass,
|
||||
Proto: defs.AuthProtocolWebRTC,
|
||||
Proto: auth.ProtocolWebRTC,
|
||||
ID: &s.uuid,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
var terr1 defs.AuthenticationError
|
||||
var terr1 auth.Error
|
||||
if errors.As(err, &terr1) {
|
||||
// wait some seconds to mitigate brute force attacks
|
||||
<-time.After(pauseAfterAuthError)
|
||||
<-time.After(auth.PauseAfterError)
|
||||
return http.StatusUnauthorized, err
|
||||
}
|
||||
|
||||
|
|
133
mediamtx.yml
133
mediamtx.yml
|
@ -24,32 +24,15 @@ writeQueueSize: 512
|
|||
# This can be decreased to avoid fragmentation on networks with a low UDP MTU.
|
||||
udpMaxPayloadSize: 1472
|
||||
|
||||
# HTTP URL to perform external authentication.
|
||||
# Every time a user wants to authenticate, the server calls this URL
|
||||
# with the POST method and a body containing:
|
||||
# {
|
||||
# "ip": "ip",
|
||||
# "user": "user",
|
||||
# "password": "password",
|
||||
# "path": "path",
|
||||
# "protocol": "rtsp|rtmp|hls|webrtc",
|
||||
# "id": "id",
|
||||
# "action": "read|publish",
|
||||
# "query": "query"
|
||||
# }
|
||||
# If the response code is 20x, authentication is accepted, otherwise
|
||||
# it is discarded.
|
||||
externalAuthenticationURL:
|
||||
|
||||
# Enable Prometheus-compatible metrics.
|
||||
metrics: no
|
||||
# Address of the metrics listener.
|
||||
metricsAddress: 127.0.0.1:9998
|
||||
metricsAddress: :9998
|
||||
|
||||
# Enable pprof-compatible endpoint to monitor performances.
|
||||
pprof: no
|
||||
# Address of the pprof listener.
|
||||
pprofAddress: 127.0.0.1:9999
|
||||
pprofAddress: :9999
|
||||
|
||||
# Command to run when a client connects to the server.
|
||||
# This is terminated with SIGINT when a client disconnects from the server.
|
||||
|
@ -64,13 +47,98 @@ runOnConnectRestart: no
|
|||
# Environment variables are the same of runOnConnect.
|
||||
runOnDisconnect:
|
||||
|
||||
###############################################
|
||||
# Global settings -> Authentication
|
||||
|
||||
# Authentication method. Available values are:
|
||||
# * internal: users are stored in the configuration file
|
||||
# * http: an external HTTP URL is contacted to perform authentication
|
||||
# * jwt: an external identity server provides authentication through JWTs
|
||||
authMethod: internal
|
||||
|
||||
# Internal authentication.
|
||||
# list of users.
|
||||
authInternalUsers:
|
||||
# Default unprivileged user.
|
||||
# Username. 'any' means any user, including anonymous ones.
|
||||
- user: any
|
||||
# Password. Not used in case of 'any' user.
|
||||
pass:
|
||||
# IPs or networks allowed to use this user. An empty list means any IP.
|
||||
ips: []
|
||||
# List of permissions.
|
||||
permissions:
|
||||
# Available actions are: publish, read, playback, api, metrics, pprof.
|
||||
- action: publish
|
||||
# Paths can be set to further restrict access to a specific path.
|
||||
# An empty path means any path.
|
||||
# Regular expressions can be used by using a tilde as prefix.
|
||||
path:
|
||||
- action: read
|
||||
path:
|
||||
- action: playback
|
||||
path:
|
||||
|
||||
# Default administrator.
|
||||
# This allows to use API, metrics and PPROF without authentication,
|
||||
# if the IP is localhost.
|
||||
- user: any
|
||||
pass:
|
||||
ips: ['127.0.0.1', '::1']
|
||||
permissions:
|
||||
- action: api
|
||||
- action: metrics
|
||||
- action: pprof
|
||||
|
||||
# HTTP-based authentication.
|
||||
# URL called to perform authentication. Every time a user wants
|
||||
# to authenticate, the server calls this URL with the POST method
|
||||
# and a body containing:
|
||||
# {
|
||||
# "user": "user",
|
||||
# "password": "password",
|
||||
# "ip": "ip",
|
||||
# "action": "publish|read|playback|api|metrics|pprof",
|
||||
# "path": "path",
|
||||
# "protocol": "rtsp|rtmp|hls|webrtc|srt",
|
||||
# "id": "id",
|
||||
# "query": "query"
|
||||
# }
|
||||
# If the response code is 20x, authentication is accepted, otherwise
|
||||
# it is discarded.
|
||||
authHTTPAddress:
|
||||
# Actions to exclude from HTTP-based authentication.
|
||||
# Format is the same as the one of user permissions.
|
||||
authHTTPExclude:
|
||||
- action: api
|
||||
- action: metrics
|
||||
- action: pprof
|
||||
|
||||
# JWT-based authentication.
|
||||
# Users have to login through an external identity server and obtain a JWT.
|
||||
# This JWT must contain the claim "mediamtx_permissions" with permissions,
|
||||
# for instance:
|
||||
# {
|
||||
# ...
|
||||
# "mediamtx_permissions": [
|
||||
# {
|
||||
# "action": "publish",
|
||||
# "path": "somepath"
|
||||
# }
|
||||
# ]
|
||||
# }
|
||||
# Users are then expected to pass the JWT as a query parameter, i.e. ?jwt=...
|
||||
# This is the JWKS URL that will be used to pull (once) the public key that allows
|
||||
# to validate JWTs.
|
||||
authJWTJWKS:
|
||||
|
||||
###############################################
|
||||
# Global settings -> API
|
||||
|
||||
# Enable controlling the server through the API.
|
||||
api: no
|
||||
# Address of the API listener.
|
||||
apiAddress: 127.0.0.1:9997
|
||||
apiAddress: :9997
|
||||
|
||||
###############################################
|
||||
# Global settings -> Playback server
|
||||
|
@ -117,8 +185,8 @@ serverKey: server.key
|
|||
# Path to the server certificate. This is needed only when encryption is "strict" or "optional".
|
||||
serverCert: server.crt
|
||||
# Authentication methods. Available are "basic" and "digest".
|
||||
# "digest" doesn't provide any additional security and is available for compatibility reasons only.
|
||||
authMethods: [basic]
|
||||
# "digest" doesn't provide any additional security and is available for compatibility only.
|
||||
rtspAuthMethods: [basic]
|
||||
|
||||
###############################################
|
||||
# Global settings -> RTMP server
|
||||
|
@ -327,27 +395,6 @@ pathDefaults:
|
|||
# Set to 0s to disable automatic deletion.
|
||||
recordDeleteAfter: 24h
|
||||
|
||||
###############################################
|
||||
# Default path settings -> Authentication
|
||||
|
||||
# Username required to publish.
|
||||
# Hashed values can be inserted with the "argon2:" or "sha256:" prefix.
|
||||
publishUser:
|
||||
# Password required to publish.
|
||||
# Hashed values can be inserted with the "argon2:" or "sha256:" prefix.
|
||||
publishPass:
|
||||
# IPs or networks (x.x.x.x/24) allowed to publish.
|
||||
publishIPs: []
|
||||
|
||||
# Username required to read.
|
||||
# Hashed values can be inserted with the "argon2:" or "sha256:" prefix.
|
||||
readUser:
|
||||
# password required to read.
|
||||
# Hashed values can be inserted with the "argon2:" or "sha256:" prefix.
|
||||
readPass:
|
||||
# IPs or networks (x.x.x.x/24) allowed to read.
|
||||
readIPs: []
|
||||
|
||||
###############################################
|
||||
# Default path settings -> Publisher source (when source is "publisher")
|
||||
|
||||
|
|
Loading…
Reference in New Issue