Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add authentication via webhook as an optional parameter #224

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.development
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
HTTP_ADDRESS=":8080"
ENABLE_HTTP_REDIRECT=
REACT_APP_API_PATH="http://localhost:8080/api"
# Configure endpoint for authentication (not provided via this project)
#WEBHOOK_URL=http://authentication-backend:8090/webhook
#WEBHOOK_TIMEOUT=5000

# /etc/letsencrypt/live/<your-domain-name>/privkey.pem
SSL_KEY=
Expand Down
3 changes: 3 additions & 0 deletions .env.production
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
HTTP_ADDRESS=":8080"
ENABLE_HTTP_REDIRECT=
REACT_APP_API_PATH="/api"
# Configure endpoint for authentication (not provided via this project)
#WEBHOOK_URL=http://authentication-backend:8090/webhook
#WEBHOOK_TIMEOUT=5000

# /etc/letsencrypt/live/<your-domain-name>/privkey.pem
SSL_KEY=
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,13 @@ will be automatically updated every night. If you are running on a VPS/Cloud ser
export URL=my-server.com
docker-compose up -d
```

## Authentication

To prevent random users from streaming to your server, you can enable the authentication webhook to an external webserver.
If the request succeeds (meaning the stream key is accepted), broadcast-box redirects the stream to an url given by the external server, otherwise the streaming request is dropped.


## URL Parameters

The frontend can be configured by passing these URL Parameters.
Expand Down
62 changes: 62 additions & 0 deletions internal/webrtc/webhook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package webrtc

import (
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
"time"
)

type WebhookPayload struct {
Action string `json:"action"`
StreamKey string `json:"streamKey"`
IP string `json:"ip"`
BearerToken string `json:"bearerToken"`
QueryParams map[string]string `json:"queryParams"`
UserAgent string `json:"userAgent"`
}

type WebhookResponse struct {
Username string `json:"username"`
}

func CallWebhook(url string, timeout int, payload WebhookPayload) (string, int, error) {
start := time.Now()
log.Printf("Starting webhook call to %s with timeout %d ms", url, timeout)

jsonPayload, err := json.Marshal(payload)
if err != nil {
return "", 0, fmt.Errorf("failed to marshal payload: %w", err)
}

client := &http.Client{
Timeout: time.Duration(timeout) * time.Millisecond,
}

req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonPayload))
if err != nil {
return "", 0, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")

log.Printf("Sending webhook request...")
resp, err := client.Do(req)
if err != nil {
log.Printf("Webhook request failed after %v: %v", time.Since(start), err)
return "", 0, fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()

response := WebhookResponse{}
err = json.NewDecoder(resp.Body).Decode(&response)
if err != nil {
log.Printf("Failed to decode webhook response: %v", err)
return "", resp.StatusCode, fmt.Errorf("failed to decode response: %w", err)
}

log.Printf("Received webhook response with status code %d after %v", resp.StatusCode, time.Since(start))

return response.Username, resp.StatusCode, nil
}
79 changes: 73 additions & 6 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"path"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"

Expand All @@ -30,6 +31,11 @@ const (
networkTestFailedMessage = "\033[0;31mNetwork Test failed.\n%s\nPlease see the README and join Discord for help\033[0m"
)

var (
webhookURL string
webhookTimeout int
)

var noBuildDirectoryErr = errors.New("\033[0;31mBuild directory does not exist, run `npm install` and `npm run build` in the web directory.\033[0m")

type (
Expand All @@ -56,6 +62,7 @@ func extractBearerToken(authHeader string) (string, bool) {
return "", false
}

// For streaming
func whipHandler(res http.ResponseWriter, r *http.Request) {
if r.Method == "DELETE" {
return
Expand All @@ -73,13 +80,21 @@ func whipHandler(res http.ResponseWriter, r *http.Request) {
return
}

// Prepare webhook payload
payload := prepareWebhookPayload("publish", streamKey, r)
username, statusCode, err := handleWebhook(payload)
if err != nil {
logHTTPError(res, err.Error(), statusCode)
return
}

offer, err := io.ReadAll(r.Body)
if err != nil {
logHTTPError(res, err.Error(), http.StatusBadRequest)
return
}

answer, err := webrtc.WHIP(string(offer), streamKey)
answer, err := webrtc.WHIP(string(offer), username)
if err != nil {
logHTTPError(res, err.Error(), http.StatusBadRequest)
return
Expand All @@ -91,15 +106,16 @@ func whipHandler(res http.ResponseWriter, r *http.Request) {
fmt.Fprint(res, answer)
}

// For watching
func whepHandler(res http.ResponseWriter, req *http.Request) {
streamKeyHeader := req.Header.Get("Authorization")
if streamKeyHeader == "" {
targetHeader := req.Header.Get("Authorization")
if targetHeader == "" {
logHTTPError(res, "Authorization was not set", http.StatusBadRequest)
return
}

streamKey, ok := extractBearerToken(streamKeyHeader)
if !ok || !validateStreamKey(streamKey) {
target, ok := extractBearerToken(targetHeader)
if !ok || !validateStreamKey(target) {
logHTTPError(res, "Invalid stream key format", http.StatusBadRequest)
return
}
Expand All @@ -110,7 +126,7 @@ func whepHandler(res http.ResponseWriter, req *http.Request) {
return
}

answer, whepSessionId, err := webrtc.WHEP(string(offer), streamKey)
answer, whepSessionId, err := webrtc.WHEP(string(offer), target)
if err != nil {
logHTTPError(res, err.Error(), http.StatusBadRequest)
return
Expand Down Expand Up @@ -268,6 +284,12 @@ func main() {

}

webhookURL = os.Getenv("WEBHOOK_URL")
webhookTimeout, _ := strconv.Atoi(os.Getenv("WEBHOOK_TIMEOUT"))
if webhookTimeout == 0 {
webhookTimeout = 5000 // Default to 5 seconds if not set or invalid
}

mux := http.NewServeMux()
if os.Getenv("DISABLE_FRONTEND") == "" {
mux.Handle("/", indexHTMLWhenNotFound(http.Dir("./web/build")))
Expand Down Expand Up @@ -309,3 +331,48 @@ func main() {
}

}

func prepareWebhookPayload(action, streamKey string, r *http.Request) webrtc.WebhookPayload {
// Convert url.Values to map[string]string
queryParams := make(map[string]string)
for k, v := range r.URL.Query() {
if len(v) > 0 {
queryParams[k] = v[0]
}
}

return webrtc.WebhookPayload{
Action: action,
StreamKey: streamKey,
IP: getIPAddress(r),
BearerToken: streamKey,
QueryParams: queryParams,
UserAgent: r.UserAgent(),
}
}

func handleWebhook(payload webrtc.WebhookPayload) (string, int, error) {
if webhookURL == "" {
return "", http.StatusOK, nil
}

username, statusCode, err := webrtc.CallWebhook(webhookURL, webhookTimeout, payload)
if err != nil {
return username, http.StatusInternalServerError, fmt.Errorf("Webhook call failed: %w", err)
}
if statusCode == http.StatusUnauthorized || statusCode == http.StatusForbidden {
return username, statusCode, fmt.Errorf("Webhook denied access")
}
if statusCode != http.StatusOK {
return username, statusCode, fmt.Errorf("Webhook returned unexpected status")
}
return username, http.StatusOK, nil
}

func getIPAddress(r *http.Request) string {
ip := r.Header.Get("X-Forwarded-For")
if ip == "" {
ip = r.RemoteAddr
}
return ip
}