Skip to content

Commit b4c6072

Browse files
committed
Add Webhook Support
Resolves #140 #375
1 parent a925559 commit b4c6072

4 files changed

Lines changed: 95 additions & 28 deletions

File tree

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ The frontend can be configured by passing these URL Parameters.
206206

207207
The backend can be configured with the following environment variables.
208208

209+
- `WEBHOOK_URL` - URL for Webhook Backend. Provides authentication and logging
209210
- `DISABLE_STATUS` - Disable the status API
210211
- `DISABLE_FRONTEND` - Disable the serving of frontend. Only REST APIs + WebRTC is enabled.
211212
- `HTTP_ADDRESS` - HTTP Server Address
@@ -235,6 +236,16 @@ The backend can be configured with the following environment variables.
235236
- `DEBUG_PRINT_OFFER` - Print WebRTC Offers from client to Broadcast Box. Debug things like accepted codecs.
236237
- `DEBUG_PRINT_ANSWER` - Print WebRTC Answers from Broadcast Box to Browser. Debug things like IP/Ports returned to client.
237238

239+
## Authentication and Logging
240+
241+
To prevent random users from streaming to your server, you can set the `WEBHOOK_URL` and validate/process requests in your code.
242+
243+
If the request succeeds (meaning the stream key is accepted), broadcast-box redirects the stream to an url given
244+
by the external server, otherwise the streaming request is dropped.
245+
246+
See [here](examples/webhook-server.go). For an example Webhook Server that only allows the stream `broadcastBoxRulez`
247+
248+
238249
## Network Test on Start
239250

240251
When running in Docker Broadcast Box runs a network tests on startup. This tests that WebRTC traffic can be established

examples/webhook-server.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"log"
6+
"net/http"
7+
)
8+
9+
type webhookPayload struct {
10+
Action string `json:"action"`
11+
IP string `json:"ip"`
12+
BearerToken string `json:"bearerToken"`
13+
QueryParams map[string]string `json:"queryParams"`
14+
UserAgent string `json:"userAgent"`
15+
}
16+
17+
type webhookResponse struct {
18+
StreamKey string `json:"streamKey"`
19+
}
20+
21+
func main() {
22+
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
23+
if r.Method != "POST" {
24+
http.Error(w, "Only POST method is accepted", http.StatusMethodNotAllowed)
25+
return
26+
}
27+
28+
var payload webhookPayload
29+
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
30+
http.Error(w, "Invalid JSON", http.StatusBadRequest)
31+
return
32+
}
33+
34+
if payload.BearerToken == "broadcastBoxRulez" {
35+
w.WriteHeader(http.StatusOK)
36+
json.NewEncoder(w).Encode(webhookResponse{StreamKey: payload.BearerToken})
37+
} else {
38+
w.WriteHeader(http.StatusForbidden)
39+
json.NewEncoder(w).Encode(webhookResponse{})
40+
}
41+
})
42+
43+
log.Println("Server listening on port 8081")
44+
if err := http.ListenAndServe("127.0.0.1:8081", nil); err != nil {
45+
log.Fatalf("Could not start server: %s\n", err)
46+
}
47+
}

internal/webhook/webhook.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"time"
99
)
1010

11+
const defaultTimeout = time.Second * 5
12+
1113
type webhookPayload struct {
1214
Action string `json:"action"`
1315
IP string `json:"ip"`
@@ -20,7 +22,7 @@ type webhookResponse struct {
2022
StreamKey string `json:"streamKey"`
2123
}
2224

23-
func CallWebhook(url, action, bearerToken string, timeout int, r *http.Request) (string, error) {
25+
func CallWebhook(url, action, bearerToken string, r *http.Request) (string, error) {
2426
start := time.Now()
2527

2628
queryParams := make(map[string]string)
@@ -41,17 +43,15 @@ func CallWebhook(url, action, bearerToken string, timeout int, r *http.Request)
4143
return "", fmt.Errorf("failed to marshal payload: %w", err)
4244
}
4345

44-
client := &http.Client{
45-
Timeout: time.Duration(timeout) * time.Millisecond,
46-
}
47-
4846
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonPayload))
4947
if err != nil {
5048
return "", fmt.Errorf("failed to create request: %w", err)
5149
}
5250
req.Header.Set("Content-Type", "application/json")
5351

54-
resp, err := client.Do(req)
52+
resp, err := (&http.Client{
53+
Timeout: defaultTimeout,
54+
}).Do(req)
5555
if err != nil {
5656
return "", fmt.Errorf("webhook request failed after %v: %w", time.Since(start), err)
5757
}

main.go

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"time"
1717

1818
"github.com/glimesh/broadcast-box/internal/networktest"
19+
"github.com/glimesh/broadcast-box/internal/webhook"
1920
"github.com/glimesh/broadcast-box/internal/webrtc"
2021
"github.com/joho/godotenv"
2122
)
@@ -44,7 +45,7 @@ type (
4445
}
4546
)
4647

47-
func getStreamKey(r *http.Request) (string, error) {
48+
func getStreamKey(action string, r *http.Request) (streamKey string, err error) {
4849
authorizationHeader := r.Header.Get("Authorization")
4950
if authorizationHeader == "" {
5051
return "", errAuthorizationNotSet
@@ -55,13 +56,19 @@ func getStreamKey(r *http.Request) (string, error) {
5556
return "", errInvalidStreamKey
5657
}
5758

58-
bearerToken := strings.TrimPrefix(authorizationHeader, bearerPrefix)
59-
if !streamKeyRegex.MatchString(bearerToken) {
60-
return "", errInvalidStreamKey
59+
streamKey = strings.TrimPrefix(authorizationHeader, bearerPrefix)
60+
if webhookUrl := os.Getenv("WEBHOOK_URL"); webhookUrl != "" {
61+
streamKey, err = webhook.CallWebhook(webhookUrl, action, streamKey, r)
62+
if err != nil {
63+
return "", err
64+
}
6165
}
6266

63-
return bearerToken, nil
67+
if !streamKeyRegex.MatchString(streamKey) {
68+
return "", errInvalidStreamKey
69+
}
6470

71+
return streamKey, nil
6572
}
6673

6774
func logHTTPError(w http.ResponseWriter, err string, code int) {
@@ -74,9 +81,10 @@ func whipHandler(res http.ResponseWriter, r *http.Request) {
7481
return
7582
}
7683

77-
streamKey, err := getStreamKey(r)
84+
streamKey, err := getStreamKey("whip-connect", r)
7885
if err != nil {
7986
logHTTPError(res, err.Error(), http.StatusBadRequest)
87+
return
8088
}
8189

8290
offer, err := io.ReadAll(r.Body)
@@ -104,9 +112,10 @@ func whepHandler(res http.ResponseWriter, req *http.Request) {
104112
return
105113
}
106114

107-
streamKey, err := getStreamKey(req)
115+
streamKey, err := getStreamKey("whep-connect", req)
108116
if err != nil {
109117
logHTTPError(res, err.Error(), http.StatusBadRequest)
118+
return
110119
}
111120

112121
offer, err := io.ReadAll(req.Body)
@@ -207,25 +216,25 @@ func corsHandler(next func(w http.ResponseWriter, r *http.Request)) http.Handler
207216
}
208217
}
209218

210-
func main() {
211-
loadConfigs := func() error {
212-
if os.Getenv("APP_ENV") == "development" {
213-
log.Println("Loading `" + envFileDev + "`")
214-
return godotenv.Load(envFileDev)
215-
} else {
216-
log.Println("Loading `" + envFileProd + "`")
217-
if err := godotenv.Load(envFileProd); err != nil {
218-
return err
219-
}
220-
221-
if _, err := os.Stat("./web/build"); os.IsNotExist(err) && os.Getenv("DISABLE_FRONTEND") == "" {
222-
return errNoBuildDirectoryErr
223-
}
219+
func loadConfigs() error {
220+
if os.Getenv("APP_ENV") == "development" {
221+
log.Println("Loading `" + envFileDev + "`")
222+
return godotenv.Load(envFileDev)
223+
} else {
224+
log.Println("Loading `" + envFileProd + "`")
225+
if err := godotenv.Load(envFileProd); err != nil {
226+
return err
227+
}
224228

225-
return nil
229+
if _, err := os.Stat("./web/build"); os.IsNotExist(err) && os.Getenv("DISABLE_FRONTEND") == "" {
230+
return errNoBuildDirectoryErr
226231
}
232+
233+
return nil
227234
}
235+
}
228236

237+
func main() {
229238
if err := loadConfigs(); err != nil {
230239
log.Println("Failed to find config in CWD, changing CWD to executable path")
231240

0 commit comments

Comments
 (0)