diff --git a/Makefile b/Makefile index 583d3e1..eb77b24 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/client/main.go b/client/main.go index 674fc9e..d66503f 100644 --- a/client/main.go +++ b/client/main.go @@ -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) diff --git a/go.mod b/go.mod index be0ef36..5a74e03 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/hub.go b/hub.go index 3808559..4ebc416 100644 --- a/hub.go +++ b/hub.go @@ -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 diff --git a/hub_test.go b/hub_test.go index 4edf73e..69454e5 100644 --- a/hub_test.go +++ b/hub_test.go @@ -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 } diff --git a/proxy/proxy_server.go b/proxy/proxy_server.go index da28a7a..596c31d 100644 --- a/proxy/proxy_server.go +++ b/proxy/proxy_server.go @@ -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 } diff --git a/session.go b/session.go index 97efa3a..d08b8ec 100644 --- a/session.go +++ b/session.go @@ -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 diff --git a/session.pb.go b/session.pb.go new file mode 100644 index 0000000..ee10477 --- /dev/null +++ b/session.pb.go @@ -0,0 +1,173 @@ +//* +// Standalone signaling server for the Nextcloud Spreed app. +// Copyright (C) 2024 struktur AG +// +// @author Joachim Bauch +// +// @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 . + +// 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 +} diff --git a/session.proto b/session.proto new file mode 100644 index 0000000..4b1cc71 --- /dev/null +++ b/session.proto @@ -0,0 +1,34 @@ +/** + * Standalone signaling server for the Nextcloud Spreed app. + * Copyright (C) 2024 struktur AG + * + * @author Joachim Bauch + * + * @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 . + */ +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; +} diff --git a/sessionid_codec.go b/sessionid_codec.go new file mode 100644 index 0000000..81de6a8 --- /dev/null +++ b/sessionid_codec.go @@ -0,0 +1,121 @@ +/** + * Standalone signaling server for the Nextcloud Spreed app. + * Copyright (C) 2024 struktur AG + * + * @author Joachim Bauch + * + * @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 . + */ +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 +} diff --git a/sessionid_codec_test.go b/sessionid_codec_test.go new file mode 100644 index 0000000..6961458 --- /dev/null +++ b/sessionid_codec_test.go @@ -0,0 +1,79 @@ +/** + * Standalone signaling server for the Nextcloud Spreed app. + * Copyright (C) 2024 struktur AG + * + * @author Joachim Bauch + * + * @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 . + */ +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) + } +} diff --git a/testclient_test.go b/testclient_test.go index e02422b..2abd8f4 100644 --- a/testclient_test.go +++ b/testclient_test.go @@ -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") }