Encode session ids using protobufs.

This will reduce their size and improve encoding / decoding performance.

Also fixes GO-2024-3106 (Stack exhaustion in Decoder.Decode in encoding/gob).
This commit is contained in:
Joachim Bauch 2024-10-28 14:19:46 +01:00
commit 9aeb57d74e
No known key found for this signature in database
GPG key ID: 77C1D22D53E15F02
12 changed files with 468 additions and 111 deletions

View file

@ -13,8 +13,10 @@ VERSION := $(shell "$(CURDIR)/scripts/get-version.sh")
TARVERSION := $(shell "$(CURDIR)/scripts/get-version.sh" --tar)
PACKAGENAME := github.com/strukturag/nextcloud-spreed-signaling
ALL_PACKAGES := $(PACKAGENAME) $(PACKAGENAME)/client $(PACKAGENAME)/proxy $(PACKAGENAME)/server
PROTO_FILES := $(basename $(wildcard *.proto))
PROTO_GO_FILES := $(addsuffix .pb.go,$(PROTO_FILES)) $(addsuffix _grpc.pb.go,$(PROTO_FILES))
GRPC_PROTO_FILES := $(basename $(wildcard grpc_*.proto))
PROTO_FILES := $(filter-out $(GRPC_PROTO_FILES),$(basename $(wildcard *.proto)))
PROTO_GO_FILES := $(addsuffix .pb.go,$(PROTO_FILES))
GRPC_PROTO_GO_FILES := $(addsuffix .pb.go,$(GRPC_PROTO_FILES)) $(addsuffix _grpc.pb.go,$(GRPC_PROTO_FILES))
EASYJSON_GO_FILES := \
api_async_easyjson.go \
api_backend_easyjson.go \
@ -22,7 +24,7 @@ EASYJSON_GO_FILES := \
api_proxy_easyjson.go \
api_signaling_easyjson.go
TEST_GO_FILES := $(wildcard *_test.go))
COMMON_GO_FILES := $(filter-out continentmap.go $(PROTO_GO_FILES) $(EASYJSON_GO_FILES) $(TEST_GO_FILES),$(wildcard *.go))
COMMON_GO_FILES := $(filter-out continentmap.go $(PROTO_GO_FILES) $(GRPC_PROTO_GO_FILES) $(EASYJSON_GO_FILES) $(TEST_GO_FILES),$(wildcard *.go))
CLIENT_TEST_GO_FILES := $(wildcard client/*_test.go))
CLIENT_GO_FILES := $(filter-out $(CLIENT_TEST_GO_FILES),$(wildcard client/*.go))
SERVER_TEST_GO_FILES := $(wildcard server/*_test.go))
@ -139,7 +141,7 @@ coverhtml: vet
$*.proto
sed -i -e '1h;2,$$H;$$!d;g' -re 's|// versions.+// source:|// source:|' $*_grpc.pb.go
common: $(EASYJSON_GO_FILES) $(PROTO_GO_FILES)
common: $(EASYJSON_GO_FILES) $(PROTO_GO_FILES) $(GRPC_PROTO_GO_FILES)
$(BINDIR):
mkdir -p "$(BINDIR)"
@ -166,7 +168,7 @@ clean:
rm -f "$(BINDIR)/proxy"
clean-generated: clean
rm -f $(EASYJSON_GO_FILES) $(PROTO_GO_FILES)
rm -f $(EASYJSON_GO_FILES) $(PROTO_GO_FILES) $(GRPC_PROTO_GO_FILES)
build: server proxy

View file

@ -23,7 +23,6 @@ package main
import (
"bytes"
"encoding/base64"
"encoding/json"
"flag"
"fmt"
@ -43,7 +42,6 @@ import (
"github.com/dlintw/goconf"
"github.com/gorilla/mux"
"github.com/gorilla/securecookie"
"github.com/gorilla/websocket"
"github.com/mailru/easyjson"
@ -75,9 +73,6 @@ const (
// Maximum message size allowed from peer.
maxMessageSize = 64 * 1024
privateSessionName = "private-session"
publicSessionName = "public-session"
)
type Stats struct {
@ -120,7 +115,7 @@ type MessagePayload struct {
type SignalingClient struct {
readyWg *sync.WaitGroup
cookie *securecookie.SecureCookie
cookie *signaling.SessionIdCodec
conn *websocket.Conn
@ -135,7 +130,7 @@ type SignalingClient struct {
userId string
}
func NewSignalingClient(cookie *securecookie.SecureCookie, url string, stats *Stats, readyWg *sync.WaitGroup, doneWg *sync.WaitGroup) (*SignalingClient, error) {
func NewSignalingClient(cookie *signaling.SessionIdCodec, url string, stats *Stats, readyWg *sync.WaitGroup, doneWg *sync.WaitGroup) (*SignalingClient, error) {
conn, _, err := websocket.DefaultDialer.Dial(url, nil)
if err != nil {
return nil, err
@ -215,19 +210,15 @@ func (c *SignalingClient) processMessage(message *signaling.ServerMessage) {
}
func (c *SignalingClient) privateToPublicSessionId(privateId string) string {
var data signaling.SessionIdData
if err := c.cookie.Decode(privateSessionName, privateId, &data); err != nil {
data, err := c.cookie.DecodePrivate(privateId)
if err != nil {
panic(fmt.Sprintf("could not decode private session id: %s", err))
}
encoded, err := c.cookie.Encode(publicSessionName, data)
publicId, err := c.cookie.EncodePublic(data)
if err != nil {
panic(fmt.Sprintf("could not encode public id: %s", err))
}
reversed, err := reverseSessionId(encoded)
if err != nil {
panic(fmt.Sprintf("could not reverse session id: %s", err))
}
return reversed
return publicId
}
func (c *SignalingClient) processHelloMessage(message *signaling.ServerMessage) {
@ -493,19 +484,6 @@ func getLocalIP() string {
return ""
}
func reverseSessionId(s string) (string, error) {
// Note that we are assuming base64 encoded strings here.
decoded, err := base64.URLEncoding.DecodeString(s)
if err != nil {
return "", err
}
for i, j := 0, len(decoded)-1; i < j; i, j = i+1, j-1 {
decoded[i], decoded[j] = decoded[j], decoded[i]
}
return base64.URLEncoding.EncodeToString(decoded), nil
}
func main() {
flag.Parse()
log.SetFlags(0)
@ -537,7 +515,7 @@ func main() {
default:
log.Fatalf("The sessions block key must be 16, 24 or 32 bytes but is %d bytes", len(blockKey))
}
cookie := securecookie.New([]byte(hashKey), blockBytes).MaxAge(0)
cookie := signaling.NewSessionIdCodec([]byte(hashKey), blockBytes)
cpus := runtime.NumCPU()
runtime.GOMAXPROCS(cpus)

2
go.mod
View file

@ -6,6 +6,7 @@ require (
github.com/dlintw/goconf v0.0.0-20120228082610-dcc070983490
github.com/fsnotify/fsnotify v1.7.0
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/golang/protobuf v1.5.4
github.com/google/uuid v1.6.0
github.com/gorilla/mux v1.8.1
github.com/gorilla/securecookie v1.1.2
@ -40,7 +41,6 @@ require (
github.com/go-logr/logr v1.3.0 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/btree v1.0.1 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect

98
hub.go
View file

@ -49,8 +49,8 @@ import (
"github.com/dlintw/goconf"
"github.com/golang-jwt/jwt/v4"
"github.com/gorilla/mux"
"github.com/gorilla/securecookie"
"github.com/gorilla/websocket"
"google.golang.org/protobuf/types/known/timestamppb"
)
var (
@ -114,11 +114,6 @@ var (
DefaultTrustedProxies = DefaultPrivateIps()
)
const (
privateSessionName = "private-session"
publicSessionName = "public-session"
)
func init() {
RegisterHubStats()
}
@ -127,7 +122,7 @@ type Hub struct {
version string
events AsyncEvents
upgrader websocket.Upgrader
cookie *securecookie.SecureCookie
cookie *SessionIdCodec
info *WelcomeServerMessage
infoInternal *WelcomeServerMessage
welcome atomic.Value // *ServerMessage
@ -325,7 +320,7 @@ func NewHub(config *goconf.ConfigFile, events AsyncEvents, rpcServer *GrpcServer
ReadBufferSize: websocketReadBufferSize,
WriteBufferSize: websocketWriteBufferSize,
},
cookie: securecookie.New([]byte(hashKey), blockBytes).MaxAge(0),
cookie: NewSessionIdCodec([]byte(hashKey), blockBytes),
info: NewWelcomeServerMessage(version, DefaultFeatures...),
infoInternal: NewWelcomeServerMessage(version, DefaultFeaturesInternal...),
@ -531,35 +526,6 @@ func (h *Hub) Reload(config *goconf.ConfigFile) {
h.rpcClients.Reload(config)
}
func reverseSessionId(s string) (string, error) {
// Note that we are assuming base64 encoded strings here.
decoded, err := base64.URLEncoding.DecodeString(s)
if err != nil {
return "", err
}
for i, j := 0, len(decoded)-1; i < j; i, j = i+1, j-1 {
decoded[i], decoded[j] = decoded[j], decoded[i]
}
return base64.URLEncoding.EncodeToString(decoded), nil
}
func (h *Hub) encodeSessionId(data *SessionIdData, sessionType string) (string, error) {
encoded, err := h.cookie.Encode(sessionType, data)
if err != nil {
return "", err
}
if sessionType == publicSessionName {
// We are reversing the public session ids because clients compare them
// to decide who calls whom. The prefix of the session id is increasing
// (a timestamp) but the suffix the (random) hash.
// By reversing we move the hash to the front, making the comparison of
// session ids "random".
encoded, err = reverseSessionId(encoded)
}
return encoded, err
}
func (h *Hub) getDecodeCache(cache_key string) *LruCache {
hash := fnv.New32a()
hash.Write([]byte(cache_key)) // nolint
@ -587,36 +553,48 @@ func (h *Hub) setDecodedSessionId(id string, sessionType string, data *SessionId
cache.Set(cache_key, data)
}
func (h *Hub) decodeSessionId(id string, sessionType string) *SessionIdData {
func (h *Hub) decodePrivateSessionId(id string) *SessionIdData {
if len(id) == 0 {
return nil
}
cache_key := id + "|" + sessionType
cache_key := id + "|" + privateSessionName
cache := h.getDecodeCache(cache_key)
if result := cache.Get(cache_key); result != nil {
return result.(*SessionIdData)
}
if sessionType == publicSessionName {
var err error
id, err = reverseSessionId(id)
if err != nil {
return nil
}
}
var data SessionIdData
if h.cookie.Decode(sessionType, id, &data) != nil {
data, err := h.cookie.DecodePrivate(id)
if err != nil {
return nil
}
cache.Set(cache_key, &data)
return &data
cache.Set(cache_key, data)
return data
}
func (h *Hub) decodePublicSessionId(id string) *SessionIdData {
if len(id) == 0 {
return nil
}
cache_key := id + "|" + publicSessionName
cache := h.getDecodeCache(cache_key)
if result := cache.Get(cache_key); result != nil {
return result.(*SessionIdData)
}
data, err := h.cookie.DecodePublic(id)
if err != nil {
return nil
}
cache.Set(cache_key, data)
return data
}
func (h *Hub) GetSessionByPublicId(sessionId string) Session {
data := h.decodeSessionId(sessionId, publicSessionName)
data := h.decodePublicSessionId(sessionId)
if data == nil {
return nil
}
@ -632,7 +610,7 @@ func (h *Hub) GetSessionByPublicId(sessionId string) Session {
}
func (h *Hub) GetSessionByResumeId(resumeId string) Session {
data := h.decodeSessionId(resumeId, privateSessionName)
data := h.decodePrivateSessionId(resumeId)
if data == nil {
return nil
}
@ -834,7 +812,7 @@ func (h *Hub) newSessionIdData(backend *Backend) *SessionIdData {
}
sessionIdData := &SessionIdData{
Sid: sid,
Created: time.Now(),
Created: timestamppb.Now(),
BackendId: backend.Id(),
}
return sessionIdData
@ -862,12 +840,12 @@ func (h *Hub) processRegister(c HandlerClient, message *ClientMessage, backend *
}
sessionIdData := h.newSessionIdData(backend)
privateSessionId, err := h.encodeSessionId(sessionIdData, privateSessionName)
privateSessionId, err := h.cookie.EncodePrivate(sessionIdData)
if err != nil {
client.SendMessage(message.NewWrappedErrorServerMessage(err))
return
}
publicSessionId, err := h.encodeSessionId(sessionIdData, publicSessionName)
publicSessionId, err := h.cookie.EncodePublic(sessionIdData)
if err != nil {
client.SendMessage(message.NewWrappedErrorServerMessage(err))
return
@ -1172,7 +1150,7 @@ func (h *Hub) processHello(client HandlerClient, message *ClientMessage) {
return
}
data := h.decodeSessionId(resumeId, privateSessionName)
data := h.decodePrivateSessionId(resumeId)
if data == nil {
statsHubSessionResumeFailed.Inc()
if h.tryProxyResume(client, resumeId, message) {
@ -2165,7 +2143,7 @@ func (h *Hub) processControlMsg(session Session, message *ClientMessage) {
var room *Room
switch msg.Recipient.Type {
case RecipientTypeSession:
data := h.decodeSessionId(msg.Recipient.SessionId, publicSessionName)
data := h.decodePublicSessionId(msg.Recipient.SessionId)
if data != nil {
if msg.Recipient.SessionId == session.PublicId() {
// Don't loop messages to the sender.
@ -2285,12 +2263,12 @@ func (h *Hub) processInternalMsg(sess Session, message *ClientMessage) {
}
sessionIdData := h.newSessionIdData(session.Backend())
privateSessionId, err := h.encodeSessionId(sessionIdData, privateSessionName)
privateSessionId, err := h.cookie.EncodePrivate(sessionIdData)
if err != nil {
log.Printf("Could not encode private virtual session id: %s", err)
return
}
publicSessionId, err := h.encodeSessionId(sessionIdData, publicSessionName)
publicSessionId, err := h.cookie.EncodePublic(sessionIdData)
if err != nil {
log.Printf("Could not encode public virtual session id: %s", err)
return

View file

@ -907,7 +907,7 @@ func TestClientHelloV2(t *testing.T) {
assert.Equal(testDefaultUserId, hello.Hello.UserId, "%+v", hello.Hello)
assert.NotEmpty(hello.Hello.SessionId, "%+v", hello.Hello)
data := hub.decodeSessionId(hello.Hello.SessionId, publicSessionName)
data := hub.decodePublicSessionId(hello.Hello.SessionId)
require.NotNil(data, "Could not decode session id: %s", hello.Hello.SessionId)
hub.mu.RLock()
@ -1281,7 +1281,7 @@ func TestSessionIdsUnordered(t *testing.T) {
assert.Equal(testDefaultUserId, hello.Hello.UserId, "%+v", hello.Hello)
assert.NotEmpty(hello.Hello.SessionId, "%+v", hello.Hello)
data := hub.decodeSessionId(hello.Hello.SessionId, publicSessionName)
data := hub.decodePublicSessionId(hello.Hello.SessionId)
if !assert.NotNil(data, "Could not decode session id: %s", hello.Hello.SessionId) {
break
}

View file

@ -46,10 +46,10 @@ import (
"github.com/golang-jwt/jwt/v4"
"github.com/google/uuid"
"github.com/gorilla/mux"
"github.com/gorilla/securecookie"
"github.com/gorilla/websocket"
"github.com/notedit/janus-go"
"github.com/prometheus/client_golang/prometheus/promhttp"
"google.golang.org/protobuf/types/known/timestamppb"
signaling "github.com/strukturag/nextcloud-spreed-signaling"
)
@ -128,7 +128,7 @@ type ProxyServer struct {
trustedProxies atomic.Pointer[signaling.AllowedIps]
sid atomic.Uint64
cookie *securecookie.SecureCookie
cookie *signaling.SessionIdCodec
sessions map[uint64]*ProxySession
sessionsLock sync.RWMutex
@ -350,7 +350,7 @@ func NewProxyServer(r *mux.Router, version string, config *goconf.ConfigFile) (*
tokens: tokens,
cookie: securecookie.New(hashKey, blockKey).MaxAge(0),
cookie: signaling.NewSessionIdCodec(hashKey, blockKey),
sessions: make(map[uint64]*ProxySession),
clients: make(map[string]signaling.McuClient),
@ -742,8 +742,7 @@ func (s *ProxyServer) processMessage(client *ProxyClient, data []byte) {
var session *ProxySession
if resumeId := message.Hello.ResumeId; resumeId != "" {
var data signaling.SessionIdData
if s.cookie.Decode("session", resumeId, &data) == nil {
if data, err := s.cookie.DecodePublic(resumeId); err == nil {
session = s.GetSession(data.Sid)
}
@ -1331,10 +1330,10 @@ func (s *ProxyServer) NewSession(hello *signaling.HelloProxyClientMessage) (*Pro
sessionIdData := &signaling.SessionIdData{
Sid: sid,
Created: time.Now(),
Created: timestamppb.Now(),
}
encoded, err := s.cookie.Encode("session", sessionIdData)
encoded, err := s.cookie.EncodePublic(sessionIdData)
if err != nil {
return nil, err
}

View file

@ -26,7 +26,6 @@ import (
"encoding/json"
"net/url"
"sync"
"time"
)
type Permission string
@ -48,12 +47,6 @@ var (
}
)
type SessionIdData struct {
Sid uint64
Created time.Time
BackendId string
}
type Session interface {
Context() context.Context
PrivateId() string

173
session.pb.go Normal file
View file

@ -0,0 +1,173 @@
//*
// Standalone signaling server for the Nextcloud Spreed app.
// Copyright (C) 2024 struktur AG
//
// @author Joachim Bauch <bauch@struktur.de>
//
// @license GNU AGPL version 3 or any later version
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: session.proto
package signaling
import (
timestamp "github.com/golang/protobuf/ptypes/timestamp"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type SessionIdData struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Sid uint64 `protobuf:"varint,1,opt,name=Sid,proto3" json:"Sid,omitempty"`
Created *timestamp.Timestamp `protobuf:"bytes,2,opt,name=Created,proto3" json:"Created,omitempty"`
BackendId string `protobuf:"bytes,3,opt,name=BackendId,proto3" json:"BackendId,omitempty"`
}
func (x *SessionIdData) Reset() {
*x = SessionIdData{}
mi := &file_session_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *SessionIdData) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SessionIdData) ProtoMessage() {}
func (x *SessionIdData) ProtoReflect() protoreflect.Message {
mi := &file_session_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SessionIdData.ProtoReflect.Descriptor instead.
func (*SessionIdData) Descriptor() ([]byte, []int) {
return file_session_proto_rawDescGZIP(), []int{0}
}
func (x *SessionIdData) GetSid() uint64 {
if x != nil {
return x.Sid
}
return 0
}
func (x *SessionIdData) GetCreated() *timestamp.Timestamp {
if x != nil {
return x.Created
}
return nil
}
func (x *SessionIdData) GetBackendId() string {
if x != nil {
return x.BackendId
}
return ""
}
var File_session_proto protoreflect.FileDescriptor
var file_session_proto_rawDesc = []byte{
0x0a, 0x0d, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12,
0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x69, 0x6e, 0x67, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67,
0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65,
0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x75, 0x0a, 0x0d, 0x53,
0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x44, 0x61, 0x74, 0x61, 0x12, 0x10, 0x0a, 0x03,
0x53, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x03, 0x53, 0x69, 0x64, 0x12, 0x34,
0x0a, 0x07, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32,
0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, 0x43, 0x72, 0x65,
0x61, 0x74, 0x65, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49,
0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64,
0x49, 0x64, 0x42, 0x3c, 0x5a, 0x3a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d,
0x2f, 0x73, 0x74, 0x72, 0x75, 0x6b, 0x74, 0x75, 0x72, 0x61, 0x67, 0x2f, 0x6e, 0x65, 0x78, 0x74,
0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2d, 0x73, 0x70, 0x72, 0x65, 0x65, 0x64, 0x2d, 0x73, 0x69, 0x67,
0x6e, 0x61, 0x6c, 0x69, 0x6e, 0x67, 0x3b, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x69, 0x6e, 0x67,
0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_session_proto_rawDescOnce sync.Once
file_session_proto_rawDescData = file_session_proto_rawDesc
)
func file_session_proto_rawDescGZIP() []byte {
file_session_proto_rawDescOnce.Do(func() {
file_session_proto_rawDescData = protoimpl.X.CompressGZIP(file_session_proto_rawDescData)
})
return file_session_proto_rawDescData
}
var file_session_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
var file_session_proto_goTypes = []any{
(*SessionIdData)(nil), // 0: signaling.SessionIdData
(*timestamp.Timestamp)(nil), // 1: google.protobuf.Timestamp
}
var file_session_proto_depIdxs = []int32{
1, // 0: signaling.SessionIdData.Created:type_name -> google.protobuf.Timestamp
1, // [1:1] is the sub-list for method output_type
1, // [1:1] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension type_name
1, // [1:1] is the sub-list for extension extendee
0, // [0:1] is the sub-list for field type_name
}
func init() { file_session_proto_init() }
func file_session_proto_init() {
if File_session_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_session_proto_rawDesc,
NumEnums: 0,
NumMessages: 1,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_session_proto_goTypes,
DependencyIndexes: file_session_proto_depIdxs,
MessageInfos: file_session_proto_msgTypes,
}.Build()
File_session_proto = out.File
file_session_proto_rawDesc = nil
file_session_proto_goTypes = nil
file_session_proto_depIdxs = nil
}

34
session.proto Normal file
View file

@ -0,0 +1,34 @@
/**
* Standalone signaling server for the Nextcloud Spreed app.
* Copyright (C) 2024 struktur AG
*
* @author Joachim Bauch <bauch@struktur.de>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
syntax = "proto3";
import "google/protobuf/timestamp.proto";
option go_package = "github.com/strukturag/nextcloud-spreed-signaling;signaling";
package signaling;
message SessionIdData {
uint64 Sid = 1;
google.protobuf.Timestamp Created = 2;
string BackendId = 3;
}

121
sessionid_codec.go Normal file
View file

@ -0,0 +1,121 @@
/**
* Standalone signaling server for the Nextcloud Spreed app.
* Copyright (C) 2024 struktur AG
*
* @author Joachim Bauch <bauch@struktur.de>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package signaling
import (
"encoding/base64"
"fmt"
"github.com/gorilla/securecookie"
"google.golang.org/protobuf/proto"
)
type protoSerializer struct {
}
func (s *protoSerializer) Serialize(src interface{}) ([]byte, error) {
msg, ok := src.(proto.Message)
if !ok {
return nil, fmt.Errorf("can't serialize type %T", src)
}
return proto.Marshal(msg)
}
func (s *protoSerializer) Deserialize(src []byte, dst interface{}) error {
msg, ok := dst.(proto.Message)
if !ok {
return fmt.Errorf("can't deserialize type %T", src)
}
return proto.Unmarshal(src, msg)
}
const (
privateSessionName = "private-session"
publicSessionName = "public-session"
)
type SessionIdCodec struct {
cookie *securecookie.SecureCookie
}
func NewSessionIdCodec(hashKey []byte, blockKey []byte) *SessionIdCodec {
cookie := securecookie.New(hashKey, blockKey).
MaxAge(0).
SetSerializer(&protoSerializer{})
return &SessionIdCodec{
cookie: cookie,
}
}
func (c *SessionIdCodec) EncodePrivate(sessionData *SessionIdData) (string, error) {
return c.cookie.Encode(privateSessionName, sessionData)
}
func reverseSessionId(s string) (string, error) {
// Note that we are assuming base64 encoded strings here.
decoded, err := base64.URLEncoding.DecodeString(s)
if err != nil {
return "", err
}
for i, j := 0, len(decoded)-1; i < j; i, j = i+1, j-1 {
decoded[i], decoded[j] = decoded[j], decoded[i]
}
return base64.URLEncoding.EncodeToString(decoded), nil
}
func (c *SessionIdCodec) EncodePublic(sessionData *SessionIdData) (string, error) {
encoded, err := c.cookie.Encode(publicSessionName, sessionData)
if err != nil {
return "", err
}
// We are reversing the public session ids because clients compare them
// to decide who calls whom. The prefix of the session id is increasing
// (a timestamp) but the suffix the (random) hash.
// By reversing we move the hash to the front, making the comparison of
// session ids "random".
return reverseSessionId(encoded)
}
func (c *SessionIdCodec) DecodePrivate(encodedData string) (*SessionIdData, error) {
var data SessionIdData
if err := c.cookie.Decode(privateSessionName, encodedData, &data); err != nil {
return nil, err
}
return &data, nil
}
func (c *SessionIdCodec) DecodePublic(encodedData string) (*SessionIdData, error) {
encodedData, err := reverseSessionId(encodedData)
if err != nil {
return nil, err
}
var data SessionIdData
if err := c.cookie.Decode(publicSessionName, encodedData, &data); err != nil {
return nil, err
}
return &data, nil
}

79
sessionid_codec_test.go Normal file
View file

@ -0,0 +1,79 @@
/**
* Standalone signaling server for the Nextcloud Spreed app.
* Copyright (C) 2024 struktur AG
*
* @author Joachim Bauch <bauch@struktur.de>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package signaling
import (
"encoding/base64"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
)
func TestReverseSessionId(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
a := base64.URLEncoding.EncodeToString([]byte("12345"))
ar, err := reverseSessionId(a)
require.NoError(err)
require.NotEqual(a, ar)
b := base64.URLEncoding.EncodeToString([]byte("54321"))
br, err := reverseSessionId(b)
require.NoError(err)
require.NotEqual(b, br)
assert.Equal(b, ar)
assert.Equal(a, br)
// Invalid base64.
if s, err := reverseSessionId("hello world!"); !assert.Error(err) {
assert.Fail("should have failed but got %s", s)
}
// Invalid base64 length.
if s, err := reverseSessionId("123"); !assert.Error(err) {
assert.Fail("should have failed but got %s", s)
}
}
func TestPublicPrivate(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
sd := &SessionIdData{
Sid: 1,
Created: timestamppb.Now(),
BackendId: "foo",
}
codec := NewSessionIdCodec([]byte("0123456789012345"), []byte("0123456789012345"))
private, err := codec.EncodePrivate(sd)
require.NoError(err)
public, err := codec.EncodePublic(sd)
require.NoError(err)
assert.NotEqual(private, public)
if data, err := codec.DecodePublic(private); !assert.Error(err) {
assert.Fail("should have failed but got %+v", data)
}
if data, err := codec.DecodePrivate(public); !assert.Error(err) {
assert.Fail("should have failed but got %+v", data)
}
}

View file

@ -66,7 +66,7 @@ func getWebsocketUrl(url string) string {
}
func getPubliceSessionIdData(h *Hub, publicId string) *SessionIdData {
decodedPublic := h.decodeSessionId(publicId, publicSessionName)
decodedPublic := h.decodePublicSessionId(publicId)
if decodedPublic == nil {
panic("invalid public session id")
}
@ -333,7 +333,7 @@ func (c *TestClient) WaitForClientRemoved(ctx context.Context) error {
}
func (c *TestClient) WaitForSessionRemoved(ctx context.Context, sessionId string) error {
data := c.hub.decodeSessionId(sessionId, publicSessionName)
data := c.hub.decodePublicSessionId(sessionId)
if data == nil {
return fmt.Errorf("Invalid session id passed")
}