Skip to content

Commit a9681a0

Browse files
giavacrg0now
authored andcommitted
Add TURN REST format support
1 parent fb0ab51 commit a9681a0

File tree

4 files changed

+174
-1
lines changed

4 files changed

+174
-1
lines changed

examples/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,18 @@ The only downside is that you can't revoke a single username/password. You need
4848

4949
* -authSecret : Shared secret for the Long Term Credential Mechanism
5050

51+
#### lt-cred-turn-rest
52+
53+
This example shows how to use ephemeral credentials, generated by a REST API, with the user part formatted as `timestamp:username`.
54+
55+
The REST API and TURN server use the same shared secret to compute the credentials.
56+
57+
The timestamp part specifies when the credentials will expire.
58+
59+
This mechanism is described in https://datatracker.ietf.org/doc/html/draft-uberti-behave-turn-rest-00
60+
61+
* -authSecret : Shared secret for the ephemeral Credential Mechanism
62+
5163
#### perm-filter
5264

5365
This example demonstrates the use of a permission handler in the PION TURN server. The example implements a filtering policy that lets clients to connect back to their own host or server-reflexive address but will drop everything else. This will let the client ping-test through but will block essentially all other peer connection attempts.
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// SPDX-FileCopyrightText: 2024 The Pion community <https://pion.ly>
2+
// SPDX-License-Identifier: MIT
3+
4+
// Package main implements a TURN server using
5+
// ephemeral credentials.
6+
package main
7+
8+
import (
9+
"flag"
10+
"log"
11+
"net"
12+
"os"
13+
"os/signal"
14+
"strconv"
15+
"syscall"
16+
17+
"github.com/pion/logging"
18+
"github.com/pion/turn/v3"
19+
)
20+
21+
func main() {
22+
publicIP := flag.String("public-ip", "", "IP Address that TURN can be contacted by.")
23+
port := flag.Int("port", 3478, "Listening port.")
24+
authSecret := flag.String("authSecret", "", "Shared secret for the Long Term Credential Mechanism")
25+
realm := flag.String("realm", "pion.ly", "Realm (defaults to \"pion.ly\")")
26+
flag.Parse()
27+
28+
if len(*publicIP) == 0 {
29+
log.Fatalf("'public-ip' is required")
30+
} else if len(*authSecret) == 0 {
31+
log.Fatalf("'authSecret' is required")
32+
}
33+
34+
// Create a UDP listener to pass into pion/turn
35+
// pion/turn itself doesn't allocate any UDP sockets, but lets the user pass them in
36+
// this allows us to add logging, storage or modify inbound/outbound traffic
37+
udpListener, err := net.ListenPacket("udp4", "0.0.0.0:"+strconv.Itoa(*port))
38+
if err != nil {
39+
log.Panicf("Failed to create TURN server listener: %s", err)
40+
}
41+
42+
// NewLongTermAuthHandler takes a pion.LeveledLogger. This allows you to intercept messages
43+
// and process them yourself.
44+
logger := logging.NewDefaultLeveledLoggerForScope("lt-creds", logging.LogLevelTrace, os.Stdout)
45+
46+
s, err := turn.NewServer(turn.ServerConfig{
47+
Realm: *realm,
48+
AuthHandler: turn.LongTermTURNRESTAuthHandler(*authSecret, logger),
49+
// PacketConnConfigs is a list of UDP Listeners and the configuration around them
50+
PacketConnConfigs: []turn.PacketConnConfig{
51+
{
52+
PacketConn: udpListener,
53+
RelayAddressGenerator: &turn.RelayAddressGeneratorStatic{
54+
RelayAddress: net.ParseIP(*publicIP), // Claim that we are listening on IP passed by user (This should be your Public IP)
55+
Address: "0.0.0.0", // But actually be listening on every interface
56+
},
57+
},
58+
},
59+
})
60+
if err != nil {
61+
log.Panic(err)
62+
}
63+
64+
// Block until user sends SIGINT or SIGTERM
65+
sigs := make(chan os.Signal, 1)
66+
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
67+
<-sigs
68+
69+
if err = s.Close(); err != nil {
70+
log.Panic(err)
71+
}
72+
}

lt_cred.go

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import ( //nolint:gci
99
"encoding/base64"
1010
"net"
1111
"strconv"
12+
"strings"
1213
"time"
1314

1415
"github.com/pion/logging"
@@ -22,6 +23,15 @@ func GenerateLongTermCredentials(sharedSecret string, duration time.Duration) (s
2223
return username, password, err
2324
}
2425

26+
// GenerateLongTermTURNRESTCredentials can be used to create credentials valid for [duration] time
27+
func GenerateLongTermTURNRESTCredentials(sharedSecret string, user string, duration time.Duration) (string, string, error) {
28+
t := time.Now().Add(duration).Unix()
29+
timestamp := strconv.FormatInt(t, 10)
30+
username := timestamp + ":" + user
31+
password, err := longTermCredentials(username, sharedSecret)
32+
return username, password, err
33+
}
34+
2535
func longTermCredentials(username string, sharedSecret string) (string, error) {
2636
mac := hmac.New(sha1.New, []byte(sharedSecret))
2737
_, err := mac.Write([]byte(username))
@@ -33,7 +43,7 @@ func longTermCredentials(username string, sharedSecret string) (string, error) {
3343
}
3444

3545
// NewLongTermAuthHandler returns a turn.AuthAuthHandler used with Long Term (or Time Windowed) Credentials.
36-
// See: https://tools.ietf.org/search/rfc5389#section-10.2
46+
// See: https://datatracker.ietf.org/doc/html/rfc8489#section-9.2
3747
func NewLongTermAuthHandler(sharedSecret string, l logging.LeveledLogger) AuthHandler {
3848
if l == nil {
3949
l = logging.NewDefaultLoggerFactory().NewLogger("turn")
@@ -57,3 +67,34 @@ func NewLongTermAuthHandler(sharedSecret string, l logging.LeveledLogger) AuthHa
5767
return GenerateAuthKey(username, realm, password), true
5868
}
5969
}
70+
71+
// LongTermTURNRESTAuthHandler returns a turn.AuthAuthHandler that can be used to authenticate
72+
// time-windowed ephemeral credentials generated by the TURN REST API as described in
73+
// https://datatracker.ietf.org/doc/html/draft-uberti-behave-turn-rest-00
74+
//
75+
// The supported format of is timestamp:username, where username is an arbitrary user id and the
76+
// timestamp specifies the expiry of the credential.
77+
func LongTermTURNRESTAuthHandler(sharedSecret string, l logging.LeveledLogger) AuthHandler {
78+
if l == nil {
79+
l = logging.NewDefaultLoggerFactory().NewLogger("turn")
80+
}
81+
return func(username, realm string, srcAddr net.Addr) (key []byte, ok bool) {
82+
l.Tracef("Authentication username=%q realm=%q srcAddr=%v\n", username, realm, srcAddr)
83+
timestamp := strings.Split(username, ":")[0]
84+
t, err := strconv.Atoi(timestamp)
85+
if err != nil {
86+
l.Errorf("Invalid time-windowed username %q", username)
87+
return nil, false
88+
}
89+
if int64(t) < time.Now().Unix() {
90+
l.Errorf("Expired time-windowed username %q", username)
91+
return nil, false
92+
}
93+
password, err := longTermCredentials(username, sharedSecret)
94+
if err != nil {
95+
l.Error(err.Error())
96+
return nil, false
97+
}
98+
return GenerateAuthKey(username, realm, password), true
99+
}
100+
}

lt_cred_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,51 @@ func TestNewLongTermAuthHandler(t *testing.T) {
7575
assert.NoError(t, conn.Close())
7676
assert.NoError(t, server.Close())
7777
}
78+
79+
func TestLongTermTURNRESTAuthHandler(t *testing.T) {
80+
const sharedSecret = "HELLO_WORLD"
81+
82+
serverListener, err := net.ListenPacket("udp4", "0.0.0.0:3478")
83+
assert.NoError(t, err)
84+
85+
server, err := NewServer(ServerConfig{
86+
AuthHandler: LongTermTURNRESTAuthHandler(sharedSecret, nil),
87+
PacketConnConfigs: []PacketConnConfig{
88+
{
89+
PacketConn: serverListener,
90+
RelayAddressGenerator: &RelayAddressGeneratorStatic{
91+
RelayAddress: net.ParseIP("127.0.0.1"),
92+
Address: "0.0.0.0",
93+
},
94+
},
95+
},
96+
Realm: "pion.ly",
97+
LoggerFactory: logging.NewDefaultLoggerFactory(),
98+
})
99+
assert.NoError(t, err)
100+
101+
conn, err := net.ListenPacket("udp4", "0.0.0.0:0")
102+
assert.NoError(t, err)
103+
104+
username, password, err := GenerateLongTermTURNRESTCredentials(sharedSecret, "testuser", time.Minute)
105+
assert.NoError(t, err)
106+
107+
client, err := NewClient(&ClientConfig{
108+
STUNServerAddr: "0.0.0.0:3478",
109+
TURNServerAddr: "0.0.0.0:3478",
110+
Conn: conn,
111+
Username: username,
112+
Password: password,
113+
LoggerFactory: logging.NewDefaultLoggerFactory(),
114+
})
115+
assert.NoError(t, err)
116+
assert.NoError(t, client.Listen())
117+
118+
relayConn, err := client.Allocate()
119+
assert.NoError(t, err)
120+
121+
client.Close()
122+
assert.NoError(t, relayConn.Close())
123+
assert.NoError(t, conn.Close())
124+
assert.NoError(t, server.Close())
125+
}

0 commit comments

Comments
 (0)