Skip to content

Commit

Permalink
Add TURN REST format support
Browse files Browse the repository at this point in the history
  • Loading branch information
giavac committed Feb 6, 2024
1 parent a980322 commit 64db7d0
Show file tree
Hide file tree
Showing 4 changed files with 168 additions and 1 deletion.
72 changes: 72 additions & 0 deletions examples/turn-server/lt-cred-turn-rest/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Package main implements a TURN server using
// ephemeral credentials.
package main

import (
"flag"
"log"
"net"
"os"
"os/signal"
"strconv"
"syscall"

"github.com/pion/logging"
"github.com/pion/turn/v3"
)

func main() {
publicIP := flag.String("public-ip", "", "IP Address that TURN can be contacted by.")
port := flag.Int("port", 3478, "Listening port.")
authSecret := flag.String("authSecret", "", "Shared secret for the Long Term Credential Mechanism")
realm := flag.String("realm", "pion.ly", "Realm (defaults to \"pion.ly\")")
flag.Parse()

if len(*publicIP) == 0 {
log.Fatalf("'public-ip' is required")
} else if len(*authSecret) == 0 {
log.Fatalf("'authSecret' is required")
}

// Create a UDP listener to pass into pion/turn
// pion/turn itself doesn't allocate any UDP sockets, but lets the user pass them in
// this allows us to add logging, storage or modify inbound/outbound traffic
udpListener, err := net.ListenPacket("udp4", "0.0.0.0:"+strconv.Itoa(*port))
if err != nil {
log.Panicf("Failed to create TURN server listener: %s", err)
}

// NewLongTermAuthHandler takes a pion.LeveledLogger. This allows you to intercept messages
// and process them yourself.
logger := logging.NewDefaultLeveledLoggerForScope("lt-creds", logging.LogLevelTrace, os.Stdout)

s, err := turn.NewServer(turn.ServerConfig{
Realm: *realm,
// Set AuthHandler callback
// This is called everytime a user tries to authenticate with the TURN server
// Return the key for that user, or false when no user is found
AuthHandler: turn.LongTermTURNRESTAuthHandler(*authSecret, logger),
// PacketConnConfigs is a list of UDP Listeners and the configuration around them
PacketConnConfigs: []turn.PacketConnConfig{
{
PacketConn: udpListener,
RelayAddressGenerator: &turn.RelayAddressGeneratorStatic{
RelayAddress: net.ParseIP(*publicIP), // Claim that we are listening on IP passed by user (This should be your Public IP)
Address: "0.0.0.0", // But actually be listening on every interface
},
},
},
})
if err != nil {
log.Panic(err)
}

// Block until user sends SIGINT or SIGTERM
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
<-sigs

if err = s.Close(); err != nil {
log.Panic(err)
}
}
11 changes: 10 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/pion/turn/v3

go 1.13
go 1.19

require (
github.com/pion/logging v0.2.2
Expand All @@ -10,3 +10,12 @@ require (
github.com/stretchr/testify v1.8.4
golang.org/x/sys v0.15.0
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pion/dtls/v2 v2.2.7 // indirect
github.com/pion/transport/v2 v2.2.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/crypto v0.12.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
38 changes: 38 additions & 0 deletions lt_cred.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import ( //nolint:gci
"encoding/base64"
"net"
"strconv"
"strings"
"time"

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

// GenerateLongTermTURNRESTCredentials can be used to create credentials valid for [duration] time
func GenerateLongTermTURNRESTCredentials(sharedSecret string, user string, duration time.Duration) (string, string, error) {
t := time.Now().Add(duration).Unix()
timestamp := strconv.FormatInt(t, 10)
username := timestamp + ":" + user
password, err := longTermCredentials(username, sharedSecret)
return username, password, err
}

func longTermCredentials(username string, sharedSecret string) (string, error) {
mac := hmac.New(sha1.New, []byte(sharedSecret))
_, err := mac.Write([]byte(username))
Expand Down Expand Up @@ -57,3 +67,31 @@ func NewLongTermAuthHandler(sharedSecret string, l logging.LeveledLogger) AuthHa
return GenerateAuthKey(username, realm, password), true
}
}

// LongTermTURNRESTAuthHandler returns a turn.AuthAuthHandler used with Long Term (or Time Windowed) Credentials.
// https://tools.ietf.org/search/rfc5389#section-10.2
// It supports the format timestamp:username used with the TURN REST API
func LongTermTURNRESTAuthHandler(sharedSecret string, l logging.LeveledLogger) AuthHandler {
if l == nil {
l = logging.NewDefaultLoggerFactory().NewLogger("turn")
}
return func(username, realm string, srcAddr net.Addr) (key []byte, ok bool) {
l.Tracef("Authentication username=%q realm=%q srcAddr=%v\n", username, realm, srcAddr)
timestamp := strings.Split(username, ":")[0]
t, err := strconv.Atoi(timestamp)
if err != nil {
l.Errorf("Invalid time-windowed username %q", username)
return nil, false
}

Check warning on line 85 in lt_cred.go

View check run for this annotation

Codecov / codecov/patch

lt_cred.go#L83-L85

Added lines #L83 - L85 were not covered by tests
if int64(t) < time.Now().Unix() {
l.Errorf("Expired time-windowed username %q", username)
return nil, false
}

Check warning on line 89 in lt_cred.go

View check run for this annotation

Codecov / codecov/patch

lt_cred.go#L87-L89

Added lines #L87 - L89 were not covered by tests
password, err := longTermCredentials(username, sharedSecret)
if err != nil {
l.Error(err.Error())
return nil, false
}

Check warning on line 94 in lt_cred.go

View check run for this annotation

Codecov / codecov/patch

lt_cred.go#L92-L94

Added lines #L92 - L94 were not covered by tests
return GenerateAuthKey(username, realm, password), true
}
}
48 changes: 48 additions & 0 deletions lt_cred_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,51 @@ func TestNewLongTermAuthHandler(t *testing.T) {
assert.NoError(t, conn.Close())
assert.NoError(t, server.Close())
}

func TestLongTermTURNRESTAuthHandler(t *testing.T) {
const sharedSecret = "HELLO_WORLD"

serverListener, err := net.ListenPacket("udp4", "0.0.0.0:3478")
assert.NoError(t, err)

server, err := NewServer(ServerConfig{
AuthHandler: LongTermTURNRESTAuthHandler(sharedSecret, nil),
PacketConnConfigs: []PacketConnConfig{
{
PacketConn: serverListener,
RelayAddressGenerator: &RelayAddressGeneratorStatic{
RelayAddress: net.ParseIP("127.0.0.1"),
Address: "0.0.0.0",
},
},
},
Realm: "pion.ly",
LoggerFactory: logging.NewDefaultLoggerFactory(),
})
assert.NoError(t, err)

conn, err := net.ListenPacket("udp4", "0.0.0.0:0")
assert.NoError(t, err)

username, password, err := GenerateLongTermTURNRESTCredentials(sharedSecret, "testuser", time.Minute)
assert.NoError(t, err)

client, err := NewClient(&ClientConfig{
STUNServerAddr: "0.0.0.0:3478",
TURNServerAddr: "0.0.0.0:3478",
Conn: conn,
Username: username,
Password: password,
LoggerFactory: logging.NewDefaultLoggerFactory(),
})
assert.NoError(t, err)
assert.NoError(t, client.Listen())

relayConn, err := client.Allocate()
assert.NoError(t, err)

client.Close()
assert.NoError(t, relayConn.Close())
assert.NoError(t, conn.Close())
assert.NoError(t, server.Close())
}

0 comments on commit 64db7d0

Please sign in to comment.