From c442a72905e70b3644270d649c1533dcdf3afe63 Mon Sep 17 00:00:00 2001 From: Joachim Bauch Date: Wed, 5 Jun 2024 14:03:10 +0200 Subject: [PATCH 01/24] Start implementing federation. --- api_signaling.go | 127 +++++- api_signaling_easyjson.go | 826 +++++++++++++++++++++++++++----------- clientsession.go | 30 ++ federation.go | 364 +++++++++++++++++ hub.go | 70 +++- room.go | 3 +- 6 files changed, 1162 insertions(+), 258 deletions(-) create mode 100644 federation.go diff --git a/api_signaling.go b/api_signaling.go index 4bb15159..d92aebc8 100644 --- a/api_signaling.go +++ b/api_signaling.go @@ -121,7 +121,7 @@ func (m *ClientMessage) CheckValid() error { return nil } -func (m *ClientMessage) String() string { +func (m ClientMessage) String() string { data, err := json.Marshal(m) if err != nil { return fmt.Sprintf("Could not serialize %#v: %s", m, err) @@ -311,9 +311,21 @@ func (m *WelcomeServerMessage) RemoveFeature(feature ...string) { m.Features = newFeatures } +func (m *WelcomeServerMessage) HasFeature(feature string) bool { + for _, f := range m.Features { + f = strings.TrimSpace(f) + if f == feature { + return true + } + } + + return false +} + const ( - HelloClientTypeClient = "client" - HelloClientTypeInternal = "internal" + HelloClientTypeClient = "client" + HelloClientTypeInternal = "internal" + HelloClientTypeFederation = "federation" HelloClientTypeVirtual = "virtual" ) @@ -363,12 +375,53 @@ func (p *HelloV2AuthParams) CheckValid() error { return nil } +type AuthTokenClaims interface { + TokenSubject() string + TokenUserData() json.RawMessage + + VerifyIssuedAt(cmp time.Time, req bool) bool + VerifyExpiresAt(cmp time.Time, req bool) bool +} + type HelloV2TokenClaims struct { jwt.RegisteredClaims UserData json.RawMessage `json:"userdata,omitempty"` } +func (c *HelloV2TokenClaims) TokenSubject() string { + return c.Subject +} + +func (c *HelloV2TokenClaims) TokenUserData() json.RawMessage { + return c.UserData +} + +type FederationAuthParams struct { + Token string `json:"token"` +} + +func (p *FederationAuthParams) CheckValid() error { + if p.Token == "" { + return fmt.Errorf("token missing") + } + return nil +} + +type FederationTokenClaims struct { + jwt.RegisteredClaims + + UserData json.RawMessage `json:"userdata,omitempty"` +} + +func (c *FederationTokenClaims) TokenSubject() string { + return c.Subject +} + +func (c *FederationTokenClaims) TokenUserData() json.RawMessage { + return c.UserData +} + type HelloClientMessageAuth struct { // The client type that is connecting. Leave empty to use the default // "HelloClientTypeClient" @@ -379,8 +432,9 @@ type HelloClientMessageAuth struct { Url string `json:"url"` parsedUrl *url.URL - internalParams ClientTypeInternalAuthParams - helloV2Params HelloV2AuthParams + internalParams ClientTypeInternalAuthParams + helloV2Params HelloV2AuthParams + federationParams FederationAuthParams } // Type "hello" @@ -409,6 +463,8 @@ func (m *HelloClientMessage) CheckValid() error { } switch m.Auth.Type { case HelloClientTypeClient: + fallthrough + case HelloClientTypeFederation: if m.Auth.Url == "" { return fmt.Errorf("url missing") } else if u, err := url.ParseRequestURI(m.Auth.Url); err != nil { @@ -425,10 +481,19 @@ func (m *HelloClientMessage) CheckValid() error { case HelloVersionV1: // No additional validation necessary. case HelloVersionV2: - if err := json.Unmarshal(m.Auth.Params, &m.Auth.helloV2Params); err != nil { - return err - } else if err := m.Auth.helloV2Params.CheckValid(); err != nil { - return err + switch m.Auth.Type { + case HelloClientTypeClient: + if err := json.Unmarshal(m.Auth.Params, &m.Auth.helloV2Params); err != nil { + return err + } else if err := m.Auth.helloV2Params.CheckValid(); err != nil { + return err + } + case HelloClientTypeFederation: + if err := json.Unmarshal(m.Auth.Params, &m.Auth.federationParams); err != nil { + return err + } else if err := m.Auth.federationParams.CheckValid(); err != nil { + return err + } } } case HelloClientTypeInternal: @@ -456,6 +521,7 @@ const ( ServerFeatureHelloV2 = "hello-v2" ServerFeatureSwitchTo = "switchto" ServerFeatureDialout = "dialout" + ServerFeatureFederation = "federation" // Features to send to internal clients only. ServerFeatureInternalVirtualSessions = "virtual-sessions" @@ -474,6 +540,7 @@ var ( ServerFeatureHelloV2, ServerFeatureSwitchTo, ServerFeatureDialout, + ServerFeatureFederation, } DefaultFeaturesInternal = []string{ ServerFeatureInternalVirtualSessions, @@ -483,6 +550,7 @@ var ( ServerFeatureHelloV2, ServerFeatureSwitchTo, ServerFeatureDialout, + ServerFeatureFederation, } DefaultWelcomeFeatures = []string{ ServerFeatureAudioVideoPermissions, @@ -493,6 +561,7 @@ var ( ServerFeatureHelloV2, ServerFeatureSwitchTo, ServerFeatureDialout, + ServerFeatureFederation, } ) @@ -526,10 +595,50 @@ type ByeServerMessage struct { type RoomClientMessage struct { RoomId string `json:"roomid"` SessionId string `json:"sessionid,omitempty"` + + Federation *RoomFederationMessage `json:"federation,omitempty"` } func (m *RoomClientMessage) CheckValid() error { // No additional validation required. + if m.Federation != nil { + if err := m.Federation.CheckValid(); err != nil { + return err + } + } + + return nil +} + +type RoomFederationMessage struct { + SignalingUrl string `json:"signaling"` + parsedSignalingUrl *url.URL + + NextcloudUrl string `json:"url"` + parsedNextcloudUrl *url.URL + + Token string `json:"token"` +} + +func (m *RoomFederationMessage) CheckValid() error { + if m.SignalingUrl == "" { + return errors.New("signaling url missing") + } else if u, err := url.Parse(m.SignalingUrl); err != nil { + return fmt.Errorf("invalid signaling url: %w", err) + } else { + m.parsedSignalingUrl = u + } + if m.NextcloudUrl == "" { + return errors.New("nextcloud url missing") + } else if u, err := url.Parse(m.NextcloudUrl); err != nil { + return fmt.Errorf("invalid nextcloud url: %w", err) + } else { + m.parsedNextcloudUrl = u + } + if m.Token == "" { + return errors.New("token missing") + } + return nil } diff --git a/api_signaling_easyjson.go b/api_signaling_easyjson.go index 5b43864c..8ece1c60 100644 --- a/api_signaling_easyjson.go +++ b/api_signaling_easyjson.go @@ -893,7 +893,87 @@ func (v *RoomFlagsServerMessage) UnmarshalJSON(data []byte) error { func (v *RoomFlagsServerMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling6(l, v) } -func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling7(in *jlexer.Lexer, out *RoomEventServerMessage) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling7(in *jlexer.Lexer, out *RoomFederationMessage) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "signaling": + out.SignalingUrl = string(in.String()) + case "url": + out.NextcloudUrl = string(in.String()) + case "token": + out.Token = string(in.String()) + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling7(out *jwriter.Writer, in RoomFederationMessage) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"signaling\":" + out.RawString(prefix[1:]) + out.String(string(in.SignalingUrl)) + } + { + const prefix string = ",\"url\":" + out.RawString(prefix) + out.String(string(in.NextcloudUrl)) + } + { + const prefix string = ",\"token\":" + out.RawString(prefix) + out.String(string(in.Token)) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v RoomFederationMessage) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling7(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v RoomFederationMessage) MarshalEasyJSON(w *jwriter.Writer) { + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling7(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *RoomFederationMessage) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling7(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *RoomFederationMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling7(l, v) +} +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling8(in *jlexer.Lexer, out *RoomEventServerMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -1028,7 +1108,7 @@ func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling7(in *jlex in.Consumed() } } -func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling7(out *jwriter.Writer, in RoomEventServerMessage) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling8(out *jwriter.Writer, in RoomEventServerMessage) { out.RawByte('{') first := true _ = first @@ -1130,27 +1210,27 @@ func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling7(out *jwr // MarshalJSON supports json.Marshaler interface func (v RoomEventServerMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling7(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling8(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v RoomEventServerMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling7(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling8(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *RoomEventServerMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling7(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling8(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *RoomEventServerMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling7(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling8(l, v) } -func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling8(in *jlexer.Lexer, out *RoomEventMessageDataChat) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling9(in *jlexer.Lexer, out *RoomEventMessageDataChat) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -1213,7 +1293,7 @@ func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling8(in *jlex in.Consumed() } } -func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling8(out *jwriter.Writer, in RoomEventMessageDataChat) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling9(out *jwriter.Writer, in RoomEventMessageDataChat) { out.RawByte('{') first := true _ = first @@ -1251,27 +1331,27 @@ func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling8(out *jwr // MarshalJSON supports json.Marshaler interface func (v RoomEventMessageDataChat) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling8(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling9(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v RoomEventMessageDataChat) MarshalEasyJSON(w *jwriter.Writer) { - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling8(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling9(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *RoomEventMessageDataChat) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling8(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling9(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *RoomEventMessageDataChat) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling8(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling9(l, v) } -func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling9(in *jlexer.Lexer, out *RoomEventMessageData) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling10(in *jlexer.Lexer, out *RoomEventMessageData) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -1312,7 +1392,7 @@ func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling9(in *jlex in.Consumed() } } -func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling9(out *jwriter.Writer, in RoomEventMessageData) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling10(out *jwriter.Writer, in RoomEventMessageData) { out.RawByte('{') first := true _ = first @@ -1332,27 +1412,27 @@ func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling9(out *jwr // MarshalJSON supports json.Marshaler interface func (v RoomEventMessageData) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling9(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling10(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v RoomEventMessageData) MarshalEasyJSON(w *jwriter.Writer) { - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling9(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling10(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *RoomEventMessageData) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling9(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling10(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *RoomEventMessageData) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling9(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling10(l, v) } -func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling10(in *jlexer.Lexer, out *RoomEventMessage) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling11(in *jlexer.Lexer, out *RoomEventMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -1387,7 +1467,7 @@ func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling10(in *jle in.Consumed() } } -func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling10(out *jwriter.Writer, in RoomEventMessage) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling11(out *jwriter.Writer, in RoomEventMessage) { out.RawByte('{') first := true _ = first @@ -1407,27 +1487,27 @@ func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling10(out *jw // MarshalJSON supports json.Marshaler interface func (v RoomEventMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling10(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling11(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v RoomEventMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling10(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling11(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *RoomEventMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling10(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling11(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *RoomEventMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling10(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling11(l, v) } -func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling11(in *jlexer.Lexer, out *RoomErrorDetails) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling12(in *jlexer.Lexer, out *RoomErrorDetails) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -1466,7 +1546,7 @@ func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling11(in *jle in.Consumed() } } -func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling11(out *jwriter.Writer, in RoomErrorDetails) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling12(out *jwriter.Writer, in RoomErrorDetails) { out.RawByte('{') first := true _ = first @@ -1485,27 +1565,27 @@ func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling11(out *jw // MarshalJSON supports json.Marshaler interface func (v RoomErrorDetails) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling11(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling12(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v RoomErrorDetails) MarshalEasyJSON(w *jwriter.Writer) { - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling11(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling12(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *RoomErrorDetails) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling11(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling12(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *RoomErrorDetails) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling11(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling12(l, v) } -func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling12(in *jlexer.Lexer, out *RoomDisinviteEventServerMessage) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling13(in *jlexer.Lexer, out *RoomDisinviteEventServerMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -1642,7 +1722,7 @@ func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling12(in *jle in.Consumed() } } -func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling12(out *jwriter.Writer, in RoomDisinviteEventServerMessage) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling13(out *jwriter.Writer, in RoomDisinviteEventServerMessage) { out.RawByte('{') first := true _ = first @@ -1749,27 +1829,27 @@ func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling12(out *jw // MarshalJSON supports json.Marshaler interface func (v RoomDisinviteEventServerMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling12(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling13(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v RoomDisinviteEventServerMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling12(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling13(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *RoomDisinviteEventServerMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling12(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling13(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *RoomDisinviteEventServerMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling12(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling13(l, v) } -func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling13(in *jlexer.Lexer, out *RoomClientMessage) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling14(in *jlexer.Lexer, out *RoomClientMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -1792,6 +1872,16 @@ func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling13(in *jle out.RoomId = string(in.String()) case "sessionid": out.SessionId = string(in.String()) + case "federation": + if in.IsNull() { + in.Skip() + out.Federation = nil + } else { + if out.Federation == nil { + out.Federation = new(RoomFederationMessage) + } + (*out.Federation).UnmarshalEasyJSON(in) + } default: in.SkipRecursive() } @@ -1802,7 +1892,7 @@ func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling13(in *jle in.Consumed() } } -func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling13(out *jwriter.Writer, in RoomClientMessage) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling14(out *jwriter.Writer, in RoomClientMessage) { out.RawByte('{') first := true _ = first @@ -1816,33 +1906,38 @@ func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling13(out *jw out.RawString(prefix) out.String(string(in.SessionId)) } + if in.Federation != nil { + const prefix string = ",\"federation\":" + out.RawString(prefix) + (*in.Federation).MarshalEasyJSON(out) + } out.RawByte('}') } // MarshalJSON supports json.Marshaler interface func (v RoomClientMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling13(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling14(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v RoomClientMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling13(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling14(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *RoomClientMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling13(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling14(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *RoomClientMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling13(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling14(l, v) } -func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling14(in *jlexer.Lexer, out *RemoveSessionInternalClientMessage) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling15(in *jlexer.Lexer, out *RemoveSessionInternalClientMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -1877,7 +1972,7 @@ func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling14(in *jle in.Consumed() } } -func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling14(out *jwriter.Writer, in RemoveSessionInternalClientMessage) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling15(out *jwriter.Writer, in RemoveSessionInternalClientMessage) { out.RawByte('{') first := true _ = first @@ -1908,27 +2003,27 @@ func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling14(out *jw // MarshalJSON supports json.Marshaler interface func (v RemoveSessionInternalClientMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling14(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling15(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v RemoveSessionInternalClientMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling14(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling15(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *RemoveSessionInternalClientMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling14(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling15(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *RemoveSessionInternalClientMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling14(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling15(l, v) } -func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling15(in *jlexer.Lexer, out *MessageServerMessageSender) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling16(in *jlexer.Lexer, out *MessageServerMessageSender) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -1963,7 +2058,7 @@ func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling15(in *jle in.Consumed() } } -func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling15(out *jwriter.Writer, in MessageServerMessageSender) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling16(out *jwriter.Writer, in MessageServerMessageSender) { out.RawByte('{') first := true _ = first @@ -1988,27 +2083,27 @@ func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling15(out *jw // MarshalJSON supports json.Marshaler interface func (v MessageServerMessageSender) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling15(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling16(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v MessageServerMessageSender) MarshalEasyJSON(w *jwriter.Writer) { - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling15(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling16(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *MessageServerMessageSender) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling15(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling16(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *MessageServerMessageSender) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling15(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling16(l, v) } -func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling16(in *jlexer.Lexer, out *MessageServerMessageDataChat) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling17(in *jlexer.Lexer, out *MessageServerMessageDataChat) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -2039,7 +2134,7 @@ func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling16(in *jle in.Consumed() } } -func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling16(out *jwriter.Writer, in MessageServerMessageDataChat) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling17(out *jwriter.Writer, in MessageServerMessageDataChat) { out.RawByte('{') first := true _ = first @@ -2054,27 +2149,27 @@ func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling16(out *jw // MarshalJSON supports json.Marshaler interface func (v MessageServerMessageDataChat) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling16(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling17(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v MessageServerMessageDataChat) MarshalEasyJSON(w *jwriter.Writer) { - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling16(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling17(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *MessageServerMessageDataChat) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling16(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling17(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *MessageServerMessageDataChat) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling16(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling17(l, v) } -func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling17(in *jlexer.Lexer, out *MessageServerMessageData) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling18(in *jlexer.Lexer, out *MessageServerMessageData) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -2115,7 +2210,7 @@ func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling17(in *jle in.Consumed() } } -func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling17(out *jwriter.Writer, in MessageServerMessageData) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling18(out *jwriter.Writer, in MessageServerMessageData) { out.RawByte('{') first := true _ = first @@ -2135,27 +2230,27 @@ func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling17(out *jw // MarshalJSON supports json.Marshaler interface func (v MessageServerMessageData) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling17(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling18(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v MessageServerMessageData) MarshalEasyJSON(w *jwriter.Writer) { - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling17(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling18(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *MessageServerMessageData) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling17(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling18(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *MessageServerMessageData) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling17(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling18(l, v) } -func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling18(in *jlexer.Lexer, out *MessageServerMessage) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling19(in *jlexer.Lexer, out *MessageServerMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -2208,7 +2303,7 @@ func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling18(in *jle in.Consumed() } } -func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling18(out *jwriter.Writer, in MessageServerMessage) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling19(out *jwriter.Writer, in MessageServerMessage) { out.RawByte('{') first := true _ = first @@ -2237,27 +2332,27 @@ func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling18(out *jw // MarshalJSON supports json.Marshaler interface func (v MessageServerMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling18(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling19(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v MessageServerMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling18(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling19(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *MessageServerMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling18(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling19(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *MessageServerMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling18(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling19(l, v) } -func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling19(in *jlexer.Lexer, out *MessageClientMessageRecipient) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling20(in *jlexer.Lexer, out *MessageClientMessageRecipient) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -2292,7 +2387,7 @@ func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling19(in *jle in.Consumed() } } -func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling19(out *jwriter.Writer, in MessageClientMessageRecipient) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling20(out *jwriter.Writer, in MessageClientMessageRecipient) { out.RawByte('{') first := true _ = first @@ -2317,27 +2412,27 @@ func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling19(out *jw // MarshalJSON supports json.Marshaler interface func (v MessageClientMessageRecipient) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling19(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling20(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v MessageClientMessageRecipient) MarshalEasyJSON(w *jwriter.Writer) { - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling19(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling20(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *MessageClientMessageRecipient) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling19(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling20(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *MessageClientMessageRecipient) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling19(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling20(l, v) } -func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling20(in *jlexer.Lexer, out *MessageClientMessageData) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling21(in *jlexer.Lexer, out *MessageClientMessageData) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -2396,7 +2491,7 @@ func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling20(in *jle in.Consumed() } } -func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling20(out *jwriter.Writer, in MessageClientMessageData) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling21(out *jwriter.Writer, in MessageClientMessageData) { out.RawByte('{') first := true _ = first @@ -2453,27 +2548,27 @@ func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling20(out *jw // MarshalJSON supports json.Marshaler interface func (v MessageClientMessageData) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling20(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling21(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v MessageClientMessageData) MarshalEasyJSON(w *jwriter.Writer) { - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling20(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling21(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *MessageClientMessageData) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling20(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling21(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *MessageClientMessageData) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling20(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling21(l, v) } -func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling21(in *jlexer.Lexer, out *MessageClientMessage) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling22(in *jlexer.Lexer, out *MessageClientMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -2508,7 +2603,7 @@ func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling21(in *jle in.Consumed() } } -func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling21(out *jwriter.Writer, in MessageClientMessage) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling22(out *jwriter.Writer, in MessageClientMessage) { out.RawByte('{') first := true _ = first @@ -2528,27 +2623,27 @@ func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling21(out *jw // MarshalJSON supports json.Marshaler interface func (v MessageClientMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling21(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling22(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v MessageClientMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling21(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling22(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *MessageClientMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling21(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling22(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *MessageClientMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling21(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling22(l, v) } -func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling22(in *jlexer.Lexer, out *InternalServerMessage) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling23(in *jlexer.Lexer, out *InternalServerMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -2589,7 +2684,7 @@ func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling22(in *jle in.Consumed() } } -func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling22(out *jwriter.Writer, in InternalServerMessage) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling23(out *jwriter.Writer, in InternalServerMessage) { out.RawByte('{') first := true _ = first @@ -2609,27 +2704,27 @@ func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling22(out *jw // MarshalJSON supports json.Marshaler interface func (v InternalServerMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling22(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling23(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v InternalServerMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling22(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling23(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *InternalServerMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling22(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling23(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *InternalServerMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling22(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling23(l, v) } -func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling23(in *jlexer.Lexer, out *InternalServerDialoutRequest) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling24(in *jlexer.Lexer, out *InternalServerDialoutRequest) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -2672,7 +2767,7 @@ func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling23(in *jle in.Consumed() } } -func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling23(out *jwriter.Writer, in InternalServerDialoutRequest) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling24(out *jwriter.Writer, in InternalServerDialoutRequest) { out.RawByte('{') first := true _ = first @@ -2701,27 +2796,27 @@ func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling23(out *jw // MarshalJSON supports json.Marshaler interface func (v InternalServerDialoutRequest) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling23(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling24(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v InternalServerDialoutRequest) MarshalEasyJSON(w *jwriter.Writer) { - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling23(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling24(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *InternalServerDialoutRequest) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling23(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling24(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *InternalServerDialoutRequest) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling23(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling24(l, v) } -func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling24(in *jlexer.Lexer, out *InternalClientMessage) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling25(in *jlexer.Lexer, out *InternalClientMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -2802,7 +2897,7 @@ func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling24(in *jle in.Consumed() } } -func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling24(out *jwriter.Writer, in InternalClientMessage) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling25(out *jwriter.Writer, in InternalClientMessage) { out.RawByte('{') first := true _ = first @@ -2842,27 +2937,27 @@ func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling24(out *jw // MarshalJSON supports json.Marshaler interface func (v InternalClientMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling24(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling25(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v InternalClientMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling24(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling25(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *InternalClientMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling24(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling25(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *InternalClientMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling24(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling25(l, v) } -func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling25(in *jlexer.Lexer, out *InCallInternalClientMessage) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling26(in *jlexer.Lexer, out *InCallInternalClientMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -2893,7 +2988,7 @@ func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling25(in *jle in.Consumed() } } -func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling25(out *jwriter.Writer, in InCallInternalClientMessage) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling26(out *jwriter.Writer, in InCallInternalClientMessage) { out.RawByte('{') first := true _ = first @@ -2908,27 +3003,27 @@ func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling25(out *jw // MarshalJSON supports json.Marshaler interface func (v InCallInternalClientMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling25(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling26(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v InCallInternalClientMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling25(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling26(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *InCallInternalClientMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling25(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling26(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *InCallInternalClientMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling25(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling26(l, v) } -func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling26(in *jlexer.Lexer, out *HelloV2TokenClaims) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling27(in *jlexer.Lexer, out *HelloV2TokenClaims) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -3007,7 +3102,7 @@ func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling26(in *jle in.Consumed() } } -func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling26(out *jwriter.Writer, in HelloV2TokenClaims) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling27(out *jwriter.Writer, in HelloV2TokenClaims) { out.RawByte('{') first := true _ = first @@ -3093,27 +3188,27 @@ func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling26(out *jw // MarshalJSON supports json.Marshaler interface func (v HelloV2TokenClaims) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling26(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling27(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v HelloV2TokenClaims) MarshalEasyJSON(w *jwriter.Writer) { - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling26(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling27(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *HelloV2TokenClaims) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling26(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling27(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *HelloV2TokenClaims) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling26(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling27(l, v) } -func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling27(in *jlexer.Lexer, out *HelloV2AuthParams) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling28(in *jlexer.Lexer, out *HelloV2AuthParams) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -3144,7 +3239,7 @@ func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling27(in *jle in.Consumed() } } -func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling27(out *jwriter.Writer, in HelloV2AuthParams) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling28(out *jwriter.Writer, in HelloV2AuthParams) { out.RawByte('{') first := true _ = first @@ -3159,27 +3254,27 @@ func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling27(out *jw // MarshalJSON supports json.Marshaler interface func (v HelloV2AuthParams) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling27(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling28(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v HelloV2AuthParams) MarshalEasyJSON(w *jwriter.Writer) { - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling27(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling28(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *HelloV2AuthParams) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling27(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling28(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *HelloV2AuthParams) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling27(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling28(l, v) } -func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling28(in *jlexer.Lexer, out *HelloServerMessage) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling29(in *jlexer.Lexer, out *HelloServerMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -3226,7 +3321,7 @@ func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling28(in *jle in.Consumed() } } -func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling28(out *jwriter.Writer, in HelloServerMessage) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling29(out *jwriter.Writer, in HelloServerMessage) { out.RawByte('{') first := true _ = first @@ -3261,27 +3356,27 @@ func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling28(out *jw // MarshalJSON supports json.Marshaler interface func (v HelloServerMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling28(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling29(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v HelloServerMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling28(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling29(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *HelloServerMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling28(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling29(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *HelloServerMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling28(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling29(l, v) } -func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling29(in *jlexer.Lexer, out *HelloClientMessageAuth) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling30(in *jlexer.Lexer, out *HelloClientMessageAuth) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -3318,7 +3413,7 @@ func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling29(in *jle in.Consumed() } } -func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling29(out *jwriter.Writer, in HelloClientMessageAuth) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling30(out *jwriter.Writer, in HelloClientMessageAuth) { out.RawByte('{') first := true _ = first @@ -3349,27 +3444,27 @@ func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling29(out *jw // MarshalJSON supports json.Marshaler interface func (v HelloClientMessageAuth) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling29(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling30(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v HelloClientMessageAuth) MarshalEasyJSON(w *jwriter.Writer) { - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling29(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling30(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *HelloClientMessageAuth) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling29(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling30(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *HelloClientMessageAuth) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling29(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling30(l, v) } -func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling30(in *jlexer.Lexer, out *HelloClientMessage) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling31(in *jlexer.Lexer, out *HelloClientMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -3435,7 +3530,7 @@ func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling30(in *jle in.Consumed() } } -func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling30(out *jwriter.Writer, in HelloClientMessage) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling31(out *jwriter.Writer, in HelloClientMessage) { out.RawByte('{') first := true _ = first @@ -3474,27 +3569,278 @@ func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling30(out *jw // MarshalJSON supports json.Marshaler interface func (v HelloClientMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling30(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling31(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v HelloClientMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling30(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling31(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *HelloClientMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling30(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling31(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *HelloClientMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling30(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling31(l, v) +} +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling32(in *jlexer.Lexer, out *FederationTokenClaims) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "userdata": + if data := in.Raw(); in.Ok() { + in.AddError((out.UserData).UnmarshalJSON(data)) + } + case "iss": + out.Issuer = string(in.String()) + case "sub": + out.Subject = string(in.String()) + case "aud": + if data := in.Raw(); in.Ok() { + in.AddError((out.Audience).UnmarshalJSON(data)) + } + case "exp": + if in.IsNull() { + in.Skip() + out.ExpiresAt = nil + } else { + if out.ExpiresAt == nil { + out.ExpiresAt = new(_v4.NumericDate) + } + if data := in.Raw(); in.Ok() { + in.AddError((*out.ExpiresAt).UnmarshalJSON(data)) + } + } + case "nbf": + if in.IsNull() { + in.Skip() + out.NotBefore = nil + } else { + if out.NotBefore == nil { + out.NotBefore = new(_v4.NumericDate) + } + if data := in.Raw(); in.Ok() { + in.AddError((*out.NotBefore).UnmarshalJSON(data)) + } + } + case "iat": + if in.IsNull() { + in.Skip() + out.IssuedAt = nil + } else { + if out.IssuedAt == nil { + out.IssuedAt = new(_v4.NumericDate) + } + if data := in.Raw(); in.Ok() { + in.AddError((*out.IssuedAt).UnmarshalJSON(data)) + } + } + case "jti": + out.ID = string(in.String()) + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling32(out *jwriter.Writer, in FederationTokenClaims) { + out.RawByte('{') + first := true + _ = first + if len(in.UserData) != 0 { + const prefix string = ",\"userdata\":" + first = false + out.RawString(prefix[1:]) + out.Raw((in.UserData).MarshalJSON()) + } + if in.Issuer != "" { + const prefix string = ",\"iss\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + out.String(string(in.Issuer)) + } + if in.Subject != "" { + const prefix string = ",\"sub\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + out.String(string(in.Subject)) + } + if len(in.Audience) != 0 { + const prefix string = ",\"aud\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + out.Raw((in.Audience).MarshalJSON()) + } + if in.ExpiresAt != nil { + const prefix string = ",\"exp\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + out.Raw((*in.ExpiresAt).MarshalJSON()) + } + if in.NotBefore != nil { + const prefix string = ",\"nbf\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + out.Raw((*in.NotBefore).MarshalJSON()) + } + if in.IssuedAt != nil { + const prefix string = ",\"iat\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + out.Raw((*in.IssuedAt).MarshalJSON()) + } + if in.ID != "" { + const prefix string = ",\"jti\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + out.String(string(in.ID)) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v FederationTokenClaims) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling32(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v FederationTokenClaims) MarshalEasyJSON(w *jwriter.Writer) { + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling32(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *FederationTokenClaims) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling32(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *FederationTokenClaims) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling32(l, v) +} +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling33(in *jlexer.Lexer, out *FederationAuthParams) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "token": + out.Token = string(in.String()) + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling33(out *jwriter.Writer, in FederationAuthParams) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"token\":" + out.RawString(prefix[1:]) + out.String(string(in.Token)) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v FederationAuthParams) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling33(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v FederationAuthParams) MarshalEasyJSON(w *jwriter.Writer) { + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling33(w, v) } -func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling31(in *jlexer.Lexer, out *EventServerMessageSwitchTo) { + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *FederationAuthParams) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling33(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *FederationAuthParams) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling33(l, v) +} +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling34(in *jlexer.Lexer, out *EventServerMessageSwitchTo) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -3529,7 +3875,7 @@ func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling31(in *jle in.Consumed() } } -func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling31(out *jwriter.Writer, in EventServerMessageSwitchTo) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling34(out *jwriter.Writer, in EventServerMessageSwitchTo) { out.RawByte('{') first := true _ = first @@ -3549,27 +3895,27 @@ func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling31(out *jw // MarshalJSON supports json.Marshaler interface func (v EventServerMessageSwitchTo) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling31(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling34(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v EventServerMessageSwitchTo) MarshalEasyJSON(w *jwriter.Writer) { - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling31(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling34(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *EventServerMessageSwitchTo) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling31(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling34(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *EventServerMessageSwitchTo) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling31(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling34(l, v) } -func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling32(in *jlexer.Lexer, out *EventServerMessageSessionEntry) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling35(in *jlexer.Lexer, out *EventServerMessageSessionEntry) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -3608,7 +3954,7 @@ func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling32(in *jle in.Consumed() } } -func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling32(out *jwriter.Writer, in EventServerMessageSessionEntry) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling35(out *jwriter.Writer, in EventServerMessageSessionEntry) { out.RawByte('{') first := true _ = first @@ -3638,27 +3984,27 @@ func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling32(out *jw // MarshalJSON supports json.Marshaler interface func (v EventServerMessageSessionEntry) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling32(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling35(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v EventServerMessageSessionEntry) MarshalEasyJSON(w *jwriter.Writer) { - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling32(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling35(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *EventServerMessageSessionEntry) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling32(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling35(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *EventServerMessageSessionEntry) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling32(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling35(l, v) } -func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling33(in *jlexer.Lexer, out *EventServerMessage) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling36(in *jlexer.Lexer, out *EventServerMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -3836,7 +4182,7 @@ func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling33(in *jle in.Consumed() } } -func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling33(out *jwriter.Writer, in EventServerMessage) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling36(out *jwriter.Writer, in EventServerMessage) { out.RawByte('{') first := true _ = first @@ -3936,27 +4282,27 @@ func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling33(out *jw // MarshalJSON supports json.Marshaler interface func (v EventServerMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling33(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling36(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v EventServerMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling33(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling36(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *EventServerMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling33(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling36(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *EventServerMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling33(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling36(l, v) } -func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling34(in *jlexer.Lexer, out *Error) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling37(in *jlexer.Lexer, out *Error) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -3993,7 +4339,7 @@ func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling34(in *jle in.Consumed() } } -func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling34(out *jwriter.Writer, in Error) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling37(out *jwriter.Writer, in Error) { out.RawByte('{') first := true _ = first @@ -4018,27 +4364,27 @@ func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling34(out *jw // MarshalJSON supports json.Marshaler interface func (v Error) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling34(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling37(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v Error) MarshalEasyJSON(w *jwriter.Writer) { - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling34(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling37(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *Error) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling34(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling37(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *Error) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling34(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling37(l, v) } -func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling35(in *jlexer.Lexer, out *DialoutStatusInternalClientMessage) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling38(in *jlexer.Lexer, out *DialoutStatusInternalClientMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -4077,7 +4423,7 @@ func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling35(in *jle in.Consumed() } } -func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling35(out *jwriter.Writer, in DialoutStatusInternalClientMessage) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling38(out *jwriter.Writer, in DialoutStatusInternalClientMessage) { out.RawByte('{') first := true _ = first @@ -4112,27 +4458,27 @@ func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling35(out *jw // MarshalJSON supports json.Marshaler interface func (v DialoutStatusInternalClientMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling35(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling38(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v DialoutStatusInternalClientMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling35(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling38(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *DialoutStatusInternalClientMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling35(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling38(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *DialoutStatusInternalClientMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling35(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling38(l, v) } -func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling36(in *jlexer.Lexer, out *DialoutInternalClientMessage) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling39(in *jlexer.Lexer, out *DialoutInternalClientMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -4185,7 +4531,7 @@ func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling36(in *jle in.Consumed() } } -func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling36(out *jwriter.Writer, in DialoutInternalClientMessage) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling39(out *jwriter.Writer, in DialoutInternalClientMessage) { out.RawByte('{') first := true _ = first @@ -4215,27 +4561,27 @@ func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling36(out *jw // MarshalJSON supports json.Marshaler interface func (v DialoutInternalClientMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling36(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling39(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v DialoutInternalClientMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling36(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling39(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *DialoutInternalClientMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling36(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling39(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *DialoutInternalClientMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling36(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling39(l, v) } -func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling37(in *jlexer.Lexer, out *ControlServerMessage) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling40(in *jlexer.Lexer, out *ControlServerMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -4288,7 +4634,7 @@ func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling37(in *jle in.Consumed() } } -func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling37(out *jwriter.Writer, in ControlServerMessage) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling40(out *jwriter.Writer, in ControlServerMessage) { out.RawByte('{') first := true _ = first @@ -4317,27 +4663,27 @@ func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling37(out *jw // MarshalJSON supports json.Marshaler interface func (v ControlServerMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling37(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling40(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v ControlServerMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling37(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling40(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *ControlServerMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling37(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling40(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *ControlServerMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling37(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling40(l, v) } -func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling38(in *jlexer.Lexer, out *ControlClientMessage) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling41(in *jlexer.Lexer, out *ControlClientMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -4372,7 +4718,7 @@ func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling38(in *jle in.Consumed() } } -func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling38(out *jwriter.Writer, in ControlClientMessage) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling41(out *jwriter.Writer, in ControlClientMessage) { out.RawByte('{') first := true _ = first @@ -4392,27 +4738,27 @@ func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling38(out *jw // MarshalJSON supports json.Marshaler interface func (v ControlClientMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling38(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling41(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v ControlClientMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling38(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling41(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *ControlClientMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling38(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling41(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *ControlClientMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling38(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling41(l, v) } -func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling39(in *jlexer.Lexer, out *CommonSessionInternalClientMessage) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling42(in *jlexer.Lexer, out *CommonSessionInternalClientMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -4445,7 +4791,7 @@ func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling39(in *jle in.Consumed() } } -func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling39(out *jwriter.Writer, in CommonSessionInternalClientMessage) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling42(out *jwriter.Writer, in CommonSessionInternalClientMessage) { out.RawByte('{') first := true _ = first @@ -4465,27 +4811,27 @@ func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling39(out *jw // MarshalJSON supports json.Marshaler interface func (v CommonSessionInternalClientMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling39(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling42(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v CommonSessionInternalClientMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling39(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling42(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *CommonSessionInternalClientMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling39(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling42(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *CommonSessionInternalClientMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling39(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling42(l, v) } -func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling40(in *jlexer.Lexer, out *ClientTypeInternalAuthParams) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling43(in *jlexer.Lexer, out *ClientTypeInternalAuthParams) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -4520,7 +4866,7 @@ func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling40(in *jle in.Consumed() } } -func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling40(out *jwriter.Writer, in ClientTypeInternalAuthParams) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling43(out *jwriter.Writer, in ClientTypeInternalAuthParams) { out.RawByte('{') first := true _ = first @@ -4545,27 +4891,27 @@ func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling40(out *jw // MarshalJSON supports json.Marshaler interface func (v ClientTypeInternalAuthParams) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling40(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling43(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v ClientTypeInternalAuthParams) MarshalEasyJSON(w *jwriter.Writer) { - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling40(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling43(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *ClientTypeInternalAuthParams) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling40(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling43(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *ClientTypeInternalAuthParams) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling40(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling43(l, v) } -func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling41(in *jlexer.Lexer, out *ClientMessage) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling44(in *jlexer.Lexer, out *ClientMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -4668,7 +5014,7 @@ func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling41(in *jle in.Consumed() } } -func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling41(out *jwriter.Writer, in ClientMessage) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling44(out *jwriter.Writer, in ClientMessage) { out.RawByte('{') first := true _ = first @@ -4729,27 +5075,27 @@ func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling41(out *jw // MarshalJSON supports json.Marshaler interface func (v ClientMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling41(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling44(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v ClientMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling41(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling44(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *ClientMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling41(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling44(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *ClientMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling41(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling44(l, v) } -func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling42(in *jlexer.Lexer, out *ByeServerMessage) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling45(in *jlexer.Lexer, out *ByeServerMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -4780,7 +5126,7 @@ func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling42(in *jle in.Consumed() } } -func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling42(out *jwriter.Writer, in ByeServerMessage) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling45(out *jwriter.Writer, in ByeServerMessage) { out.RawByte('{') first := true _ = first @@ -4795,27 +5141,27 @@ func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling42(out *jw // MarshalJSON supports json.Marshaler interface func (v ByeServerMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling42(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling45(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v ByeServerMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling42(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling45(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *ByeServerMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling42(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling45(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *ByeServerMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling42(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling45(l, v) } -func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling43(in *jlexer.Lexer, out *ByeClientMessage) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling46(in *jlexer.Lexer, out *ByeClientMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -4844,7 +5190,7 @@ func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling43(in *jle in.Consumed() } } -func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling43(out *jwriter.Writer, in ByeClientMessage) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling46(out *jwriter.Writer, in ByeClientMessage) { out.RawByte('{') first := true _ = first @@ -4854,27 +5200,27 @@ func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling43(out *jw // MarshalJSON supports json.Marshaler interface func (v ByeClientMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling43(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling46(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v ByeClientMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling43(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling46(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *ByeClientMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling43(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling46(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *ByeClientMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling43(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling46(l, v) } -func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling44(in *jlexer.Lexer, out *AnswerOfferMessage) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling47(in *jlexer.Lexer, out *AnswerOfferMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -4935,7 +5281,7 @@ func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling44(in *jle in.Consumed() } } -func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling44(out *jwriter.Writer, in AnswerOfferMessage) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling47(out *jwriter.Writer, in AnswerOfferMessage) { out.RawByte('{') first := true _ = first @@ -4997,27 +5343,27 @@ func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling44(out *jw // MarshalJSON supports json.Marshaler interface func (v AnswerOfferMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling44(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling47(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v AnswerOfferMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling44(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling47(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *AnswerOfferMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling44(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling47(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *AnswerOfferMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling44(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling47(l, v) } -func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling45(in *jlexer.Lexer, out *AddSessionOptions) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling48(in *jlexer.Lexer, out *AddSessionOptions) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -5050,7 +5396,7 @@ func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling45(in *jle in.Consumed() } } -func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling45(out *jwriter.Writer, in AddSessionOptions) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling48(out *jwriter.Writer, in AddSessionOptions) { out.RawByte('{') first := true _ = first @@ -5076,27 +5422,27 @@ func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling45(out *jw // MarshalJSON supports json.Marshaler interface func (v AddSessionOptions) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling45(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling48(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v AddSessionOptions) MarshalEasyJSON(w *jwriter.Writer) { - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling45(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling48(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *AddSessionOptions) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling45(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling48(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *AddSessionOptions) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling45(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling48(l, v) } -func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling46(in *jlexer.Lexer, out *AddSessionInternalClientMessage) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling49(in *jlexer.Lexer, out *AddSessionInternalClientMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -5157,7 +5503,7 @@ func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling46(in *jle in.Consumed() } } -func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling46(out *jwriter.Writer, in AddSessionInternalClientMessage) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling49(out *jwriter.Writer, in AddSessionInternalClientMessage) { out.RawByte('{') first := true _ = first @@ -5228,23 +5574,23 @@ func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling46(out *jw // MarshalJSON supports json.Marshaler interface func (v AddSessionInternalClientMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling46(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling49(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v AddSessionInternalClientMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling46(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling49(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *AddSessionInternalClientMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling46(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling49(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *AddSessionInternalClientMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling46(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling49(l, v) } diff --git a/clientsession.go b/clientsession.go index d4e8c40e..efffc811 100644 --- a/clientsession.go +++ b/clientsession.go @@ -72,6 +72,7 @@ type ClientSession struct { client HandlerClient room atomic.Pointer[Room] roomJoinTime atomic.Int64 + federation atomic.Pointer[FederationClient] roomSessionIdLock sync.RWMutex roomSessionId string @@ -332,6 +333,21 @@ func (s *ClientSession) SetRoom(room *Room) { s.seenJoinedEvents = nil } +func (s *ClientSession) GetFederationClient() *FederationClient { + return s.federation.Load() +} + +func (s *ClientSession) SetFederationClient(federation *FederationClient) { + s.mu.Lock() + defer s.mu.Unlock() + + s.doLeaveRoom(true) + + if prev := s.federation.Swap(federation); prev != nil { + prev.Close() + } +} + func (s *ClientSession) GetRoom() *Room { return s.room.Load() } @@ -374,6 +390,10 @@ func (s *ClientSession) closeAndWait(wait bool) { s.closeFunc() s.hub.removeSession(s) + if prev := s.federation.Swap(nil); prev != nil { + prev.Close() + } + s.mu.Lock() defer s.mu.Unlock() if s.userId != "" { @@ -467,9 +487,19 @@ func (s *ClientSession) LeaveCall() { } func (s *ClientSession) LeaveRoom(notify bool) *Room { + if prev := s.federation.Swap(nil); prev != nil { + // Session was connected to a federation room. + prev.Close() + return nil + } + s.mu.Lock() defer s.mu.Unlock() + return s.doLeaveRoom(notify) +} + +func (s *ClientSession) doLeaveRoom(notify bool) *Room { room := s.GetRoom() if room == nil { return nil diff --git a/federation.go b/federation.go new file mode 100644 index 00000000..e1f6178a --- /dev/null +++ b/federation.go @@ -0,0 +1,364 @@ +/** + * 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 ( + "context" + "crypto/tls" + "encoding/json" + "errors" + "fmt" + "log" + "strconv" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/gorilla/websocket" + easyjson "github.com/mailru/easyjson" +) + +var ( + ErrFederationNotSupported = NewError("federation_unsupported", "The target server does not support federation.") +) + +type FederationClient struct { + session *ClientSession + roomId string + roomSessionId string + federation *RoomFederationMessage + + mu sync.Mutex + conn *websocket.Conn + closer *Closer + + helloMu sync.Mutex + helloMsgId string + helloAuth *FederationAuthParams + hello atomic.Pointer[HelloServerMessage] +} + +func NewFederationClient(ctx context.Context, session *ClientSession, room *RoomClientMessage) (*FederationClient, error) { + var dialer websocket.Dialer + dialer.TLSClientConfig = &tls.Config{ + InsecureSkipVerify: true, + } + + u := *room.Federation.parsedSignalingUrl + switch u.Scheme { + case "http": + u.Scheme = "ws" + case "https": + u.Scheme = "wss" + } + conn, response, err := dialer.DialContext(ctx, u.String(), nil) + if err != nil { + return nil, err + } + + features := strings.Split(response.Header.Get("X-Spreed-Signaling-Features"), ",") + supportsFederation := false + for _, f := range features { + f = strings.TrimSpace(f) + if f == ServerFeatureFederation { + supportsFederation = true + break + } + } + if !supportsFederation { + if err := conn.Close(); err != nil { + log.Printf("Error closing federation connection to %s: %s", room.Federation.parsedSignalingUrl.String(), err) + } + + return nil, ErrFederationNotSupported + } + + result := &FederationClient{ + session: session, + roomId: room.RoomId, + roomSessionId: room.SessionId, + federation: room.Federation, + + conn: conn, + closer: NewCloser(), + } + log.Printf("Creating federation connection to %s for %s", result.URL(), result.session.PublicId()) + + go result.readPump() + go result.writePump() + return result, nil +} + +func (c *FederationClient) URL() string { + return c.federation.parsedSignalingUrl.String() +} + +func (c *FederationClient) Close() { + c.closer.Close() + if err := c.conn.Close(); err != nil { + log.Printf("Error closing federation connection to %s: %s", c.URL(), err) + } +} + +func (c *FederationClient) readPump() { + defer func() { + c.Close() + }() + + c.mu.Lock() + conn := c.conn + c.mu.Unlock() + if conn == nil { + log.Printf("Connection to %s closed while starting readPump", c.URL()) + return + } + + conn.SetReadLimit(maxMessageSize) + conn.SetPongHandler(func(msg string) error { + now := time.Now() + conn.SetReadDeadline(now.Add(pongWait)) // nolint + return nil + }) + + for { + conn.SetReadDeadline(time.Now().Add(pongWait)) // nolint + msgType, data, err := conn.ReadMessage() + if err != nil { + log.Printf("Error reading: %s", err) + break + } + + if msgType != websocket.TextMessage { + continue + } + + var msg ServerMessage + if err := json.Unmarshal(data, &msg); err != nil { + log.Printf("Error unmarshalling %s from %s: %s", string(data), c.URL(), err) + continue + } + + if c.hello.Load() == nil { + switch msg.Type { + case "welcome": + c.processWelcome(&msg) + default: + c.processHello(&msg) + } + continue + } + + c.processMessage(&msg) + } +} + +func (c *FederationClient) sendPing() bool { + c.mu.Lock() + defer c.mu.Unlock() + if c.conn == nil { + return false + } + + now := time.Now().UnixNano() + msg := strconv.FormatInt(now, 10) + c.conn.SetWriteDeadline(time.Now().Add(writeWait)) // nolint + if err := c.conn.WriteMessage(websocket.PingMessage, []byte(msg)); err != nil { + log.Printf("Could not send ping to federated client %s: %v", c.session.PublicId(), err) + return false + } + + return true +} + +func (c *FederationClient) writePump() { + ticker := time.NewTicker(pingPeriod) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + if !c.sendPing() { + return + } + case <-c.closer.C: + return + } + } +} + +func (c *FederationClient) closeWithError(err error) { + c.Close() + var e *Error + if !errors.As(err, &e) { + e = NewError("federation_error", err.Error()) + } + c.session.SendMessage(&ServerMessage{ + Type: "error", + Error: e, + }) +} + +func (c *FederationClient) sendHello(auth *FederationAuthParams) error { + c.helloMu.Lock() + defer c.helloMu.Unlock() + + return c.sendHelloLocked(auth) +} + +func (c *FederationClient) sendHelloLocked(auth *FederationAuthParams) error { + c.helloMsgId = newRandomString(8) + + authData, err := json.Marshal(auth) + if err != nil { + return fmt.Errorf("Error marshalling hello auth message %+v for %s: %s", auth, c.session.PublicId(), err) + } + + c.helloAuth = auth + return c.SendMessage(&ClientMessage{ + Id: c.helloMsgId, + Type: "hello", + Hello: &HelloClientMessage{ + Version: HelloVersionV2, + Auth: &HelloClientMessageAuth{ + Type: HelloClientTypeFederation, + Url: c.federation.NextcloudUrl, + Params: authData, + }, + }, + }) +} + +func (c *FederationClient) processWelcome(msg *ServerMessage) { + if !msg.Welcome.HasFeature(ServerFeatureFederation) { + c.closeWithError(ErrFederationNotSupported) + return + } + + federationParams := &FederationAuthParams{ + Token: c.federation.Token, + } + if err := c.sendHello(federationParams); err != nil { + log.Printf("Error sending hello message to %s for %s: %s", c.URL(), c.session.PublicId(), err) + c.closeWithError(err) + } +} + +func (c *FederationClient) processHello(msg *ServerMessage) { + c.helloMu.Lock() + defer c.helloMu.Unlock() + + if msg.Id != c.helloMsgId { + log.Printf("Received hello response %+v for unknown request, expected %s", msg, c.helloMsgId) + c.sendHelloLocked(c.helloAuth) + return + } + + c.helloMsgId = "" + if msg.Type == "error" { + c.closeWithError(msg.Error) + return + } else if msg.Type != "hello" { + log.Printf("Received unknown hello response %+v", msg) + c.sendHelloLocked(c.helloAuth) + return + } + + c.hello.Store(msg.Hello) + if err := c.joinRoom(); err != nil { + c.closeWithError(err) + } +} + +func (c *FederationClient) joinRoom() error { + return c.SendMessage(&ClientMessage{ + Type: "room", + Room: &RoomClientMessage{ + RoomId: c.roomId, + SessionId: c.roomSessionId, + }, + }) +} + +func (c *FederationClient) processMessage(msg *ServerMessage) { + hello := c.hello.Load() + switch msg.Type { + case "message": + if r := msg.Message.Recipient; r != nil && r.Type == RecipientTypeSession && hello != nil && r.SessionId == hello.SessionId { + msg.Message.Recipient.SessionId = c.session.PublicId() + } + } + c.session.SendMessage(msg) +} + +func (c *FederationClient) ProxyMessage(message *ClientMessage) error { + switch message.Type { + case "message": + if r := message.Message.Recipient; r.Type == RecipientTypeSession && r.SessionId == c.session.PublicId() { + message.Message.Recipient.SessionId = c.hello.Load().SessionId + } + } + + return c.SendMessage(message) +} + +func (c *FederationClient) SendMessage(message *ClientMessage) error { + c.mu.Lock() + defer c.mu.Unlock() + + return c.sendMessageLocked(message) +} + +func (c *FederationClient) sendMessageLocked(message *ClientMessage) error { + if c.conn == nil { + return ErrNotConnected + } + + c.conn.SetWriteDeadline(time.Now().Add(writeWait)) // nolint + writer, err := c.conn.NextWriter(websocket.TextMessage) + if err == nil { + if m, ok := (interface{}(message)).(easyjson.Marshaler); ok { + _, err = easyjson.MarshalToWriter(m, writer) + } else { + err = json.NewEncoder(writer).Encode(message) + } + } + if err == nil { + err = writer.Close() + } + if err != nil { + if err == websocket.ErrCloseSent { + // Already sent a "close", won't be able to send anything else. + return err + } + + log.Printf("Could not send message %+v for %s to federated client %s: %v", message, c.session.PublicId(), c.URL(), err) + closeData := websocket.FormatCloseMessage(websocket.CloseInternalServerErr, "") + c.conn.SetWriteDeadline(time.Now().Add(writeWait)) // nolint + if err := c.conn.WriteMessage(websocket.CloseMessage, closeData); err != nil { + log.Printf("Could not send close message for %s to federated client %s: %v", c.session.PublicId(), c.URL(), err) + } + return err + } + + return nil +} diff --git a/hub.go b/hub.go index 42d9c537..c9550f03 100644 --- a/hub.go +++ b/hub.go @@ -1005,6 +1005,16 @@ func (h *Hub) processMessage(client HandlerClient, data []byte) { return } + isLocalMessage := message.Type == "room" || + message.Type == "hello" || + message.Type == "bye" + if cs, ok := session.(*ClientSession); ok && !isLocalMessage { + if federated := cs.GetFederationClient(); federated != nil { + federated.ProxyMessage(&message) + return + } + } + switch message.Type { case "room": h.processRoom(session, &message) @@ -1202,6 +1212,8 @@ func (h *Hub) processHello(client HandlerClient, message *ClientMessage) { switch message.Hello.Auth.Type { case HelloClientTypeClient: + fallthrough + case HelloClientTypeFederation: h.processHelloClient(client, message) case HelloClientTypeInternal: h.processHelloInternal(client, message) @@ -1240,7 +1252,19 @@ func (h *Hub) processHelloV2(ctx context.Context, client HandlerClient, message return nil, nil, InvalidBackendUrl } - token, err := jwt.ParseWithClaims(message.Hello.Auth.helloV2Params.Token, &HelloV2TokenClaims{}, func(token *jwt.Token) (interface{}, error) { + var tokenString string + var tokenClaims jwt.Claims + switch message.Hello.Auth.Type { + case HelloClientTypeClient: + tokenString = message.Hello.Auth.helloV2Params.Token + tokenClaims = &HelloV2TokenClaims{} + case HelloClientTypeFederation: + tokenString = message.Hello.Auth.federationParams.Token + tokenClaims = &FederationTokenClaims{} + default: + return nil, nil, InvalidClientType + } + token, err := jwt.ParseWithClaims(tokenString, tokenClaims, func(token *jwt.Token) (interface{}, error) { // Only public-private-key algorithms are supported. var loadKeyFunc func([]byte) (interface{}, error) switch token.Method.(type) { @@ -1316,15 +1340,26 @@ func (h *Hub) processHelloV2(ctx context.Context, client HandlerClient, message return nil, nil, InvalidToken } - claims, ok := token.Claims.(*HelloV2TokenClaims) - if !ok || !token.Valid { - return nil, nil, InvalidToken + var authTokenClaims AuthTokenClaims + switch message.Hello.Auth.Type { + case HelloClientTypeClient: + claims, ok := token.Claims.(*HelloV2TokenClaims) + if !ok || !token.Valid { + return nil, nil, InvalidToken + } + authTokenClaims = claims + case HelloClientTypeFederation: + claims, ok := token.Claims.(*FederationTokenClaims) + if !ok || !token.Valid { + return nil, nil, InvalidToken + } + authTokenClaims = claims } now := time.Now() - if !claims.VerifyIssuedAt(now, true) { + if !authTokenClaims.VerifyIssuedAt(now, true) { return nil, nil, TokenNotValidYet } - if !claims.VerifyExpiresAt(now, true) { + if !authTokenClaims.VerifyExpiresAt(now, true) { return nil, nil, TokenExpired } @@ -1332,8 +1367,8 @@ func (h *Hub) processHelloV2(ctx context.Context, client HandlerClient, message Type: "auth", Auth: &BackendClientAuthResponse{ Version: message.Hello.Version, - UserId: claims.Subject, - User: claims.UserData, + UserId: authTokenClaims.TokenSubject(), + User: authTokenClaims.TokenUserData(), }, } return backend, auth, nil @@ -1492,6 +1527,25 @@ func (h *Hub) processRoom(sess Session, message *ClientMessage) { return } + if federation := message.Room.Federation; federation != nil { + // TODO: Handle case where session already is in a federated room on the same server. + client, err := NewFederationClient(session.Context(), session, message.Room) + if err != nil { + log.Printf("Error creating federation client for %s to join room %s: %s", session.PublicId(), roomId, err) + session.SendMessage(message.NewErrorServerMessage( + NewErrorDetail("federation_error", "Failed to create federation client.", nil), + )) + return + } + + session.SetFederationClient(client) + h.mu.Lock() + // The session now joined a room, don't expire if it is anonymous. + delete(h.anonymousSessions, session) + h.mu.Unlock() + return + } + if room := h.getRoomForBackend(roomId, session.Backend()); room != nil && room.HasSession(session) { // Session already is in that room, no action needed. roomSessionId := message.Room.SessionId diff --git a/room.go b/room.go index a84230c7..39ce6bdc 100644 --- a/room.go +++ b/room.go @@ -709,7 +709,8 @@ func (r *Room) PublishUsersInCallChangedAll(inCall int) { continue } - if session.ClientType() == HelloClientTypeInternal { + if session.ClientType() == HelloClientTypeInternal || + session.ClientType() == HelloClientTypeFederation { continue } From e08ce211e146700f368518f627f9ec0e5f784ad2 Mon Sep 17 00:00:00 2001 From: Joachim Bauch Date: Wed, 17 Jul 2024 09:44:47 +0200 Subject: [PATCH 02/24] Include original id in request to join federated room so client can map the response. --- federation.go | 16 +++++++++++++--- hub.go | 2 +- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/federation.go b/federation.go index e1f6178a..d25c02f9 100644 --- a/federation.go +++ b/federation.go @@ -43,7 +43,9 @@ var ( ) type FederationClient struct { - session *ClientSession + session *ClientSession + message *ClientMessage + roomId string roomSessionId string federation *RoomFederationMessage @@ -58,12 +60,17 @@ type FederationClient struct { hello atomic.Pointer[HelloServerMessage] } -func NewFederationClient(ctx context.Context, session *ClientSession, room *RoomClientMessage) (*FederationClient, error) { +func NewFederationClient(ctx context.Context, session *ClientSession, message *ClientMessage) (*FederationClient, error) { + if message.Type != "room" || message.Room == nil { + return nil, fmt.Errorf("expected room message, got %+v", message) + } + var dialer websocket.Dialer dialer.TLSClientConfig = &tls.Config{ InsecureSkipVerify: true, } + room := message.Room u := *room.Federation.parsedSignalingUrl switch u.Scheme { case "http": @@ -94,7 +101,9 @@ func NewFederationClient(ctx context.Context, session *ClientSession, room *Room } result := &FederationClient{ - session: session, + session: session, + message: message, + roomId: room.RoomId, roomSessionId: room.SessionId, federation: room.Federation, @@ -291,6 +300,7 @@ func (c *FederationClient) processHello(msg *ServerMessage) { func (c *FederationClient) joinRoom() error { return c.SendMessage(&ClientMessage{ + Id: c.message.Id, Type: "room", Room: &RoomClientMessage{ RoomId: c.roomId, diff --git a/hub.go b/hub.go index c9550f03..624e1721 100644 --- a/hub.go +++ b/hub.go @@ -1529,7 +1529,7 @@ func (h *Hub) processRoom(sess Session, message *ClientMessage) { if federation := message.Room.Federation; federation != nil { // TODO: Handle case where session already is in a federated room on the same server. - client, err := NewFederationClient(session.Context(), session, message.Room) + client, err := NewFederationClient(session.Context(), session, message) if err != nil { log.Printf("Error creating federation client for %s to join room %s: %s", session.PublicId(), roomId, err) session.SendMessage(message.NewErrorServerMessage( From 43b8ea546e901578bd3e9d2b4e0973fe0cbc24f5 Mon Sep 17 00:00:00 2001 From: Joachim Bauch Date: Wed, 17 Jul 2024 10:31:25 +0200 Subject: [PATCH 03/24] Rewrite more session ids for proxied federated events. --- federation.go | 97 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 94 insertions(+), 3 deletions(-) diff --git a/federation.go b/federation.go index d25c02f9..31899896 100644 --- a/federation.go +++ b/federation.go @@ -309,12 +309,103 @@ func (c *FederationClient) joinRoom() error { }) } +func (c *FederationClient) updateEventUsers(users []map[string]interface{}, localSessionId string, remoteSessionId string) { + for _, u := range users { + key := "sessionId" + sid, found := u[key] + if !found { + key := "sessionid" + sid, found = u[key] + } + if found { + if sid, ok := sid.(string); ok && sid == remoteSessionId { + u[key] = localSessionId + break + } + } + } +} + +func (c *FederationClient) updateRecipient(recipient *MessageClientMessageRecipient, localSessionId string, remoteSessionId string) { + if recipient != nil && recipient.Type == RecipientTypeSession && remoteSessionId != "" && recipient.SessionId == remoteSessionId { + recipient.SessionId = localSessionId + } +} + +func (c *FederationClient) updateSender(sender *MessageServerMessageSender, localSessionId string, remoteSessionId string) { + if sender != nil && sender.Type == RecipientTypeSession && remoteSessionId != "" && sender.SessionId == remoteSessionId { + sender.SessionId = localSessionId + } +} + func (c *FederationClient) processMessage(msg *ServerMessage) { - hello := c.hello.Load() + localSessionId := c.session.PublicId() + var remoteSessionId string + if hello := c.hello.Load(); hello != nil { + remoteSessionId = hello.SessionId + } switch msg.Type { + case "control": + c.updateRecipient(msg.Control.Recipient, localSessionId, remoteSessionId) + c.updateSender(msg.Control.Sender, localSessionId, remoteSessionId) + case "event": + switch msg.Event.Target { + case "participants": + switch msg.Event.Type { + case "update": + if remoteSessionId != "" { + c.updateEventUsers(msg.Event.Update.Changed, localSessionId, remoteSessionId) + c.updateEventUsers(msg.Event.Update.Users, localSessionId, remoteSessionId) + } + case "flags": + if remoteSessionId != "" && msg.Event.Flags.SessionId == remoteSessionId { + msg.Event.Flags.SessionId = localSessionId + } + } + case "room": + switch msg.Event.Type { + case "join": + if remoteSessionId != "" { + for _, j := range msg.Event.Join { + if j.SessionId == remoteSessionId { + j.SessionId = localSessionId + break + } + } + } + case "leave": + if remoteSessionId != "" { + for idx, j := range msg.Event.Leave { + if j == remoteSessionId { + msg.Event.Leave[idx] = localSessionId + break + } + } + } + } + } case "message": - if r := msg.Message.Recipient; r != nil && r.Type == RecipientTypeSession && hello != nil && r.SessionId == hello.SessionId { - msg.Message.Recipient.SessionId = c.session.PublicId() + c.updateRecipient(msg.Message.Recipient, localSessionId, remoteSessionId) + c.updateSender(msg.Message.Sender, localSessionId, remoteSessionId) + if remoteSessionId != "" && len(msg.Message.Data) > 0 { + var ao AnswerOfferMessage + if json.Unmarshal(msg.Message.Data, &ao) == nil && (ao.Type == "offer" || ao.Type == "answer") { + changed := false + if ao.From == remoteSessionId { + ao.From = localSessionId + changed = true + } + if ao.To == remoteSessionId { + ao.To = localSessionId + changed = true + } + + if changed { + if data, err := json.Marshal(ao); err == nil { + msg.Message.Data = data + } + } + } } } c.session.SendMessage(msg) From 148c7792c560491c0578a01a45b49d62dd165328 Mon Sep 17 00:00:00 2001 From: Joachim Bauch Date: Wed, 17 Jul 2024 13:49:33 +0200 Subject: [PATCH 04/24] federation: Send "bye" before closing, use message id for error responses. --- federation.go | 49 +++++++++++++++++++++++++++++++++++++++++++------ hub.go | 2 +- 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/federation.go b/federation.go index 31899896..f82b7646 100644 --- a/federation.go +++ b/federation.go @@ -44,7 +44,7 @@ var ( type FederationClient struct { session *ClientSession - message *ClientMessage + message atomic.Pointer[ClientMessage] roomId string roomSessionId string @@ -60,7 +60,7 @@ type FederationClient struct { hello atomic.Pointer[HelloServerMessage] } -func NewFederationClient(ctx context.Context, session *ClientSession, message *ClientMessage) (*FederationClient, error) { +func NewFederationClient(ctx context.Context, hub *Hub, session *ClientSession, message *ClientMessage) (*FederationClient, error) { if message.Type != "room" || message.Room == nil { return nil, fmt.Errorf("expected room message, got %+v", message) } @@ -102,7 +102,6 @@ func NewFederationClient(ctx context.Context, session *ClientSession, message *C result := &FederationClient{ session: session, - message: message, roomId: room.RoomId, roomSessionId: room.SessionId, @@ -111,10 +110,23 @@ func NewFederationClient(ctx context.Context, session *ClientSession, message *C conn: conn, closer: NewCloser(), } + result.message.Store(message) log.Printf("Creating federation connection to %s for %s", result.URL(), result.session.PublicId()) - go result.readPump() - go result.writePump() + go func() { + hub.readPumpActive.Add(1) + defer hub.readPumpActive.Add(-1) + + result.readPump() + }() + + go func() { + hub.writePumpActive.Add(1) + defer hub.writePumpActive.Add(-1) + + result.writePump() + }() + return result, nil } @@ -124,9 +136,23 @@ func (c *FederationClient) URL() string { func (c *FederationClient) Close() { c.closer.Close() + c.mu.Lock() + defer c.mu.Unlock() + if c.conn == nil { + return + } + + if err := c.sendMessageLocked(&ClientMessage{ + Type: "bye", + }); err != nil { + log.Printf("Error sending bye on federation connection to %s: %s", c.URL(), err) + } + if err := c.conn.Close(); err != nil { log.Printf("Error closing federation connection to %s: %s", c.URL(), err) } + + c.conn = nil } func (c *FederationClient) readPump() { @@ -221,7 +247,14 @@ func (c *FederationClient) closeWithError(err error) { if !errors.As(err, &e) { e = NewError("federation_error", err.Error()) } + + var id string + if message := c.message.Swap(nil); message != nil { + id = message.Id + } + c.session.SendMessage(&ServerMessage{ + Id: id, Type: "error", Error: e, }) @@ -299,8 +332,12 @@ func (c *FederationClient) processHello(msg *ServerMessage) { } func (c *FederationClient) joinRoom() error { + var id string + if message := c.message.Swap(nil); message != nil { + id = message.Id + } return c.SendMessage(&ClientMessage{ - Id: c.message.Id, + Id: id, Type: "room", Room: &RoomClientMessage{ RoomId: c.roomId, diff --git a/hub.go b/hub.go index 624e1721..12aa06e2 100644 --- a/hub.go +++ b/hub.go @@ -1529,7 +1529,7 @@ func (h *Hub) processRoom(sess Session, message *ClientMessage) { if federation := message.Room.Federation; federation != nil { // TODO: Handle case where session already is in a federated room on the same server. - client, err := NewFederationClient(session.Context(), session, message) + client, err := NewFederationClient(session.Context(), h, session, message) if err != nil { log.Printf("Error creating federation client for %s to join room %s: %s", session.PublicId(), roomId, err) session.SendMessage(message.NewErrorServerMessage( From 6243ef1f0b5eb0bda5a0f65b3bca457fa3e54047 Mon Sep 17 00:00:00 2001 From: Joachim Bauch Date: Wed, 17 Jul 2024 16:21:49 +0200 Subject: [PATCH 05/24] Make sure response to federated room leave contains id from request. --- clientsession.go | 9 ++++++++- federation.go | 40 ++++++++++++++++++++++++++++++++++++++-- hub.go | 2 +- 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/clientsession.go b/clientsession.go index efffc811..44d68c3f 100644 --- a/clientsession.go +++ b/clientsession.go @@ -487,9 +487,16 @@ func (s *ClientSession) LeaveCall() { } func (s *ClientSession) LeaveRoom(notify bool) *Room { + return s.LeaveRoomWithMessage(notify, nil) +} + +func (s *ClientSession) LeaveRoomWithMessage(notify bool, message *ClientMessage) *Room { if prev := s.federation.Swap(nil); prev != nil { // Session was connected to a federation room. - prev.Close() + if err := prev.Leave(message); err != nil { + log.Printf("Error leaving room for session %s on federation client %s: %s", s.PublicId(), prev.URL(), err) + prev.Close() + } return nil } diff --git a/federation.go b/federation.go index f82b7646..1815b0c1 100644 --- a/federation.go +++ b/federation.go @@ -58,6 +58,8 @@ type FederationClient struct { helloMsgId string helloAuth *FederationAuthParams hello atomic.Pointer[HelloServerMessage] + + closeOnLeave atomic.Bool } func NewFederationClient(ctx context.Context, hub *Hub, session *ClientSession, message *ClientMessage) (*FederationClient, error) { @@ -134,6 +136,27 @@ func (c *FederationClient) URL() string { return c.federation.parsedSignalingUrl.String() } +func (c *FederationClient) Leave(message *ClientMessage) error { + c.mu.Lock() + defer c.mu.Unlock() + + if message == nil { + message = &ClientMessage{ + Type: "room", + Room: &RoomClientMessage{ + RoomId: "", + }, + } + } + + if err := c.sendMessageLocked(message); err != nil { + return err + } + + c.closeOnLeave.Store(true) + return nil +} + func (c *FederationClient) Close() { c.closer.Close() c.mu.Lock() @@ -381,6 +404,8 @@ func (c *FederationClient) processMessage(msg *ServerMessage) { if hello := c.hello.Load(); hello != nil { remoteSessionId = hello.SessionId } + + var doClose bool switch msg.Type { case "control": c.updateRecipient(msg.Control.Recipient, localSessionId, remoteSessionId) @@ -415,12 +440,19 @@ func (c *FederationClient) processMessage(msg *ServerMessage) { for idx, j := range msg.Event.Leave { if j == remoteSessionId { msg.Event.Leave[idx] = localSessionId + if c.closeOnLeave.Load() { + doClose = true + } break } } } } } + case "room": + if msg.Room.RoomId == "" && c.closeOnLeave.Load() { + doClose = true + } case "message": c.updateRecipient(msg.Message.Recipient, localSessionId, remoteSessionId) c.updateSender(msg.Message.Sender, localSessionId, remoteSessionId) @@ -446,13 +478,17 @@ func (c *FederationClient) processMessage(msg *ServerMessage) { } } c.session.SendMessage(msg) + + if doClose { + c.Close() + } } func (c *FederationClient) ProxyMessage(message *ClientMessage) error { switch message.Type { case "message": - if r := message.Message.Recipient; r.Type == RecipientTypeSession && r.SessionId == c.session.PublicId() { - message.Message.Recipient.SessionId = c.hello.Load().SessionId + if hello := c.hello.Load(); hello != nil { + c.updateRecipient(&message.Message.Recipient, hello.SessionId, c.session.PublicId()) } } diff --git a/hub.go b/hub.go index 12aa06e2..30513748 100644 --- a/hub.go +++ b/hub.go @@ -1516,7 +1516,7 @@ func (h *Hub) processRoom(sess Session, message *ClientMessage) { roomId := message.Room.RoomId if roomId == "" { // We can handle leaving a room directly. - if session.LeaveRoom(true) != nil { + if session.LeaveRoomWithMessage(true, message) != nil { // User was in a room before, so need to notify about leaving it. h.sendRoom(session, message, nil) if session.UserId() == "" && session.ClientType() != HelloClientTypeInternal { From f271faac68c1aef47a9d8b0c046701ec2534a23d Mon Sep 17 00:00:00 2001 From: Joachim Bauch Date: Wed, 17 Jul 2024 16:39:57 +0200 Subject: [PATCH 06/24] Add tests for federation code. --- federation_test.go | 345 +++++++++++++++++++++++++++++++++++++++++++++ go.mod | 3 + hub_test.go | 2 +- testclient_test.go | 26 +++- 4 files changed, 372 insertions(+), 4 deletions(-) create mode 100644 federation_test.go diff --git a/federation_test.go b/federation_test.go new file mode 100644 index 00000000..938bc019 --- /dev/null +++ b/federation_test.go @@ -0,0 +1,345 @@ +/** + * 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 ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_FederationInvalidToken(t *testing.T) { + CatchLogForTest(t) + + assert := assert.New(t) + require := require.New(t) + + _, hub2, server1, server2 := CreateClusteredHubsForTest(t) + + client := NewTestClient(t, server2, hub2) + defer client.CloseWithBye() + require.NoError(client.SendHelloV2(testDefaultUserId + "2")) + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + _, err := client.RunUntilHello(ctx) + require.NoError(err) + + msg := &ClientMessage{ + Id: "join-room-fed", + Type: "room", + Room: &RoomClientMessage{ + RoomId: "test-room", + SessionId: "room-session-id", + Federation: &RoomFederationMessage{ + SignalingUrl: server1.URL + "/spreed", + NextcloudUrl: server1.URL, + Token: "invalid-token", + }, + }, + } + require.NoError(client.WriteJSON(msg)) + + if message, err := client.RunUntilMessage(ctx); assert.NoError(err) { + assert.Equal(msg.Id, message.Id) + require.Equal("error", message.Type) + require.Equal("invalid_token", message.Error.Code) + } +} + +func Test_Federation(t *testing.T) { + CatchLogForTest(t) + + assert := assert.New(t) + require := require.New(t) + + hub1, hub2, server1, server2 := CreateClusteredHubsForTest(t) + + client1 := NewTestClient(t, server1, hub1) + defer client1.CloseWithBye() + require.NoError(client1.SendHelloV2(testDefaultUserId + "1")) + + client2 := NewTestClient(t, server2, hub2) + defer client2.CloseWithBye() + require.NoError(client2.SendHelloV2(testDefaultUserId + "2")) + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + hello1, err := client1.RunUntilHello(ctx) + require.NoError(err) + + hello2, err := client2.RunUntilHello(ctx) + require.NoError(err) + + roomId := "test-room" + room1, err := client1.JoinRoom(ctx, roomId) + require.NoError(err) + require.Equal(roomId, room1.Room.RoomId) + + assert.NoError(client1.RunUntilJoined(ctx, hello1.Hello)) + + now := time.Now() + token, err := client1.CreateHelloV2Token(testDefaultUserId+"2", now, now.Add(time.Minute)) + require.NoError(err) + + msg := &ClientMessage{ + Id: "join-room-fed", + Type: "room", + Room: &RoomClientMessage{ + RoomId: roomId, + SessionId: roomId + "-" + hello2.Hello.SessionId, + Federation: &RoomFederationMessage{ + SignalingUrl: server1.URL + "/spreed", + NextcloudUrl: server1.URL, + Token: token, + }, + }, + } + require.NoError(client2.WriteJSON(msg)) + + if message, err := client2.RunUntilMessage(ctx); assert.NoError(err) { + assert.Equal(msg.Id, message.Id) + require.Equal("room", message.Type) + require.Equal(roomId, message.Room.RoomId) + } + + // The client1 will see the remote session id for client2. + var remoteSessionId string + if message, err := client1.RunUntilMessage(ctx); assert.NoError(err) { + client1.checkSingleMessageJoined(message) + evt := message.Event.Join[0] + remoteSessionId = evt.SessionId + assert.NotEqual(hello2.Hello.SessionId, remoteSessionId) + assert.Equal(testDefaultUserId+"2", evt.UserId) + } + + // The client2 will see its own session id, not the one from the remote server. + assert.NoError(client2.RunUntilJoined(ctx, hello1.Hello, hello2.Hello)) + + data1 := "from-1-to-2" + data2 := "from-2-to-1" + if assert.NoError(client1.SendMessage(MessageClientMessageRecipient{ + Type: "session", + SessionId: remoteSessionId, + }, data1)) { + var payload string + if assert.NoError(checkReceiveClientMessage(ctx, client2, "session", hello1.Hello, &payload)) { + assert.Equal(data1, payload) + } + } + + if assert.NoError(client1.SendControl(MessageClientMessageRecipient{ + Type: "session", + SessionId: remoteSessionId, + }, data1)) { + var payload string + if assert.NoError(checkReceiveClientControl(ctx, client2, "session", hello1.Hello, &payload)) { + assert.Equal(data1, payload) + } + } + + if assert.NoError(client2.SendMessage(MessageClientMessageRecipient{ + Type: "session", + SessionId: hello1.Hello.SessionId, + }, data2)) { + var payload string + if assert.NoError(checkReceiveClientMessage(ctx, client1, "session", &HelloServerMessage{ + SessionId: remoteSessionId, + UserId: testDefaultUserId + "2", + }, &payload)) { + assert.Equal(data2, payload) + } + } + + if assert.NoError(client2.SendControl(MessageClientMessageRecipient{ + Type: "session", + SessionId: hello1.Hello.SessionId, + }, data2)) { + var payload string + if assert.NoError(checkReceiveClientControl(ctx, client1, "session", &HelloServerMessage{ + SessionId: remoteSessionId, + UserId: testDefaultUserId + "2", + }, &payload)) { + assert.Equal(data2, payload) + } + } + + data3 := "from-2-to-2" + // Clients can't send to their own (local) session id. + if assert.NoError(client2.SendMessage(MessageClientMessageRecipient{ + Type: "session", + SessionId: hello2.Hello.SessionId, + }, data3)) { + ctx2, cancel2 := context.WithTimeout(ctx, 200*time.Millisecond) + defer cancel2() + + if message, err := client2.RunUntilMessage(ctx2); err != nil && err != ErrNoMessageReceived && err != context.DeadlineExceeded { + t.Error(err) + } else { + assert.Nil(message) + } + } + + // Clients can't send to their own (remote) session id. + if assert.NoError(client2.SendMessage(MessageClientMessageRecipient{ + Type: "session", + SessionId: remoteSessionId, + }, data3)) { + ctx2, cancel2 := context.WithTimeout(ctx, 200*time.Millisecond) + defer cancel2() + + if message, err := client2.RunUntilMessage(ctx2); err != nil && err != ErrNoMessageReceived && err != context.DeadlineExceeded { + t.Error(err) + } else { + assert.Nil(message) + } + } + + // Simulate request from the backend that somebody joined the call. + users := []map[string]interface{}{ + { + "sessionId": remoteSessionId, + "inCall": 1, + }, + } + room := hub1.getRoom(roomId) + require.NotNil(room) + room.PublishUsersInCallChanged(users, users) + var event *EventServerMessage + assert.NoError(checkReceiveClientEvent(ctx, client2, "update", &event)) + assert.Equal(hello2.Hello.SessionId, event.Update.Users[0]["sessionId"]) + + room3, err := client2.JoinRoom(ctx, "") + if assert.NoError(err) { + assert.Equal("", room3.Room.RoomId) + } +} + +func Test_FederationMedia(t *testing.T) { + CatchLogForTest(t) + + CatchLogForTest(t) + + assert := assert.New(t) + require := require.New(t) + + hub1, hub2, server1, server2 := CreateClusteredHubsForTest(t) + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + mcu1, err := NewTestMCU() + require.NoError(err) + require.NoError(mcu1.Start(ctx)) + defer mcu1.Stop() + + hub1.SetMcu(mcu1) + + mcu2, err := NewTestMCU() + require.NoError(err) + require.NoError(mcu2.Start(ctx)) + defer mcu2.Stop() + + hub2.SetMcu(mcu2) + + client1 := NewTestClient(t, server1, hub1) + defer client1.CloseWithBye() + require.NoError(client1.SendHelloV2(testDefaultUserId + "1")) + + client2 := NewTestClient(t, server2, hub2) + defer client2.CloseWithBye() + require.NoError(client2.SendHelloV2(testDefaultUserId + "2")) + + hello1, err := client1.RunUntilHello(ctx) + require.NoError(err) + + hello2, err := client2.RunUntilHello(ctx) + require.NoError(err) + + roomId := "test-room" + room1, err := client1.JoinRoom(ctx, roomId) + require.NoError(err) + require.Equal(roomId, room1.Room.RoomId) + + assert.NoError(client1.RunUntilJoined(ctx, hello1.Hello)) + + now := time.Now() + token, err := client1.CreateHelloV2Token(testDefaultUserId+"2", now, now.Add(time.Minute)) + require.NoError(err) + + msg := &ClientMessage{ + Id: "join-room-fed", + Type: "room", + Room: &RoomClientMessage{ + RoomId: roomId, + SessionId: roomId + "-" + hello2.Hello.SessionId, + Federation: &RoomFederationMessage{ + SignalingUrl: server1.URL + "/spreed", + NextcloudUrl: server1.URL, + Token: token, + }, + }, + } + require.NoError(client2.WriteJSON(msg)) + + if message, err := client2.RunUntilMessage(ctx); assert.NoError(err) { + assert.Equal(msg.Id, message.Id) + require.Equal("room", message.Type) + require.Equal(roomId, message.Room.RoomId) + } + + // The client1 will see the remote session id for client2. + var remoteSessionId string + if message, err := client1.RunUntilMessage(ctx); assert.NoError(err) { + client1.checkSingleMessageJoined(message) + evt := message.Event.Join[0] + remoteSessionId = evt.SessionId + assert.NotEqual(hello2.Hello.SessionId, remoteSessionId) + assert.Equal(testDefaultUserId+"2", evt.UserId) + } + + // The client2 will see its own session id, not the one from the remote server. + assert.NoError(client2.RunUntilJoined(ctx, hello1.Hello, hello2.Hello)) + + require.NoError(client2.SendMessage(MessageClientMessageRecipient{ + Type: "session", + SessionId: hello2.Hello.SessionId, + }, MessageClientMessageData{ + Type: "offer", + Sid: "12345", + RoomType: "screen", + Payload: map[string]interface{}{ + "sdp": MockSdpOfferAudioAndVideo, + }, + })) + + require.NoError(client2.RunUntilAnswerFromSender(ctx, MockSdpAnswerAudioAndVideo, &MessageServerMessageSender{ + Type: "session", + SessionId: hello2.Hello.SessionId, + UserId: hello2.Hello.UserId, + })) +} diff --git a/go.mod b/go.mod index 7b896a9b..a4059432 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/oschwald/maxminddb-golang v1.13.1 github.com/pion/sdp/v3 v3.0.9 github.com/prometheus/client_golang v1.19.1 + github.com/stretchr/testify v1.9.0 go.etcd.io/etcd/api/v3 v3.5.15 go.etcd.io/etcd/client/pkg/v3 v3.5.15 go.etcd.io/etcd/client/v3 v3.5.15 @@ -56,6 +57,7 @@ require ( github.com/nats-io/nkeys v0.4.7 // indirect github.com/nats-io/nuid v1.0.1 // indirect github.com/pion/randutil v0.1.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.5.0 // indirect github.com/prometheus/common v0.48.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect @@ -87,5 +89,6 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect sigs.k8s.io/yaml v1.2.0 // indirect ) diff --git a/hub_test.go b/hub_test.go index 70f6f290..2f75e79a 100644 --- a/hub_test.go +++ b/hub_test.go @@ -713,7 +713,7 @@ func registerBackendHandlerUrl(t *testing.T, router *mux.Router, url string) { if os.Getenv("SKIP_V2_CAPABILITIES") != "" { useV2 = false } - if strings.Contains(t.Name(), "V2") && useV2 { + if (strings.Contains(t.Name(), "V2") && useV2) || strings.Contains(t.Name(), "Federation") { key := getPublicAuthToken(t) public, err := x509.MarshalPKIXPublicKey(key) if err != nil { diff --git a/testclient_test.go b/testclient_test.go index 2dc6a83d..e65d1faa 100644 --- a/testclient_test.go +++ b/testclient_test.go @@ -401,14 +401,14 @@ func (c *TestClient) SendHelloV2(userid string) error { return c.SendHelloV2WithTimes(userid, now, now.Add(time.Minute)) } -func (c *TestClient) SendHelloV2WithTimes(userid string, issuedAt time.Time, expiresAt time.Time) error { +func (c *TestClient) CreateHelloV2Token(userid string, issuedAt time.Time, expiresAt time.Time) (string, error) { userdata := map[string]string{ "displayname": "Displayname " + userid, } data, err := json.Marshal(userdata) if err != nil { - c.t.Fatal(err) + return "", err } claims := &HelloV2TokenClaims{ @@ -434,7 +434,11 @@ func (c *TestClient) SendHelloV2WithTimes(userid string, issuedAt time.Time, exp token = jwt.NewWithClaims(jwt.SigningMethodRS256, claims) } private := getPrivateAuthToken(c.t) - tokenString, err := token.SignedString(private) + return token.SignedString(private) +} + +func (c *TestClient) SendHelloV2WithTimes(userid string, issuedAt time.Time, expiresAt time.Time) error { + tokenString, err := c.CreateHelloV2Token(userid, issuedAt, expiresAt) if err != nil { c.t.Fatal(err) } @@ -724,6 +728,9 @@ func (c *TestClient) JoinRoomWithRoomSession(ctx context.Context, roomId string, if err := checkMessageType(message, "room"); err != nil { return nil, err } + if message.Id != msg.Id { + return nil, fmt.Errorf("expected message id %s, got %s", msg.Id, message.Id) + } return message, nil } @@ -993,6 +1000,10 @@ func (c *TestClient) RunUntilOffer(ctx context.Context, offer string) error { } func (c *TestClient) RunUntilAnswer(ctx context.Context, answer string) error { + return c.RunUntilAnswerFromSender(ctx, answer, nil) +} + +func (c *TestClient) RunUntilAnswerFromSender(ctx context.Context, answer string, sender *MessageServerMessageSender) error { message, err := c.RunUntilMessage(ctx) if err != nil { return err @@ -1003,6 +1014,15 @@ func (c *TestClient) RunUntilAnswer(ctx context.Context, answer string) error { return err } + if sender != nil { + if err := checkMessageSender(c.hub, message.Message.Sender, sender.Type, &HelloServerMessage{ + SessionId: sender.SessionId, + UserId: sender.UserId, + }); err != nil { + return err + } + } + var data map[string]interface{} if err := json.Unmarshal(message.Message.Data, &data); err != nil { return err From 0451cea5dbe0ea20d577bf4dff2234af396f6aca Mon Sep 17 00:00:00 2001 From: Joachim Bauch Date: Thu, 18 Jul 2024 09:16:18 +0200 Subject: [PATCH 07/24] Fix missing events if federated session leaves and joins again. --- clientsession.go | 7 ++- federation_test.go | 129 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 133 insertions(+), 3 deletions(-) diff --git a/clientsession.go b/clientsession.go index 44d68c3f..b7a90955 100644 --- a/clientsession.go +++ b/clientsession.go @@ -322,7 +322,11 @@ func (s *ClientSession) UserData() json.RawMessage { func (s *ClientSession) SetRoom(room *Room) { s.room.Store(room) - if room != nil { + s.onRoomSet(room != nil) +} + +func (s *ClientSession) onRoomSet(hasRoom bool) { + if hasRoom { s.roomJoinTime.Store(time.Now().UnixNano()) } else { s.roomJoinTime.Store(0) @@ -342,6 +346,7 @@ func (s *ClientSession) SetFederationClient(federation *FederationClient) { defer s.mu.Unlock() s.doLeaveRoom(true) + s.onRoomSet(federation != nil) if prev := s.federation.Swap(federation); prev != nil { prev.Close() diff --git a/federation_test.go b/federation_test.go index 938bc019..5002f645 100644 --- a/federation_test.go +++ b/federation_test.go @@ -140,6 +140,51 @@ func Test_Federation(t *testing.T) { // The client2 will see its own session id, not the one from the remote server. assert.NoError(client2.RunUntilJoined(ctx, hello1.Hello, hello2.Hello)) + // Leaving and re-joining a room as "direct" session will trigger correct events. + if room, err := client1.JoinRoom(ctx, ""); assert.NoError(err) { + assert.Equal("", room.Room.RoomId) + } + + assert.NoError(client2.RunUntilLeft(ctx, hello1.Hello)) + + if room, err := client1.JoinRoom(ctx, roomId); assert.NoError(err) { + assert.Equal(roomId, room.Room.RoomId) + } + + assert.NoError(client1.RunUntilJoined(ctx, hello1.Hello, &HelloServerMessage{ + SessionId: remoteSessionId, + UserId: hello2.Hello.UserId, + })) + assert.NoError(client2.RunUntilJoined(ctx, hello1.Hello)) + + // Leaving and re-joining a room as "federated" session will trigger correct events. + if room, err := client2.JoinRoom(ctx, ""); assert.NoError(err) { + assert.Equal("", room.Room.RoomId) + } + + assert.NoError(client1.RunUntilLeft(ctx, &HelloServerMessage{ + SessionId: remoteSessionId, + UserId: hello2.Hello.UserId, + })) + + require.NoError(client2.WriteJSON(msg)) + if message, err := client2.RunUntilMessage(ctx); assert.NoError(err) { + assert.Equal(msg.Id, message.Id) + require.Equal("room", message.Type) + require.Equal(roomId, message.Room.RoomId) + } + + // Client1 will receive the updated "remoteSessionId" + if message, err := client1.RunUntilMessage(ctx); assert.NoError(err) { + client1.checkSingleMessageJoined(message) + evt := message.Event.Join[0] + remoteSessionId = evt.SessionId + assert.NotEqual(hello2.Hello.SessionId, remoteSessionId) + assert.Equal(testDefaultUserId+"2", evt.UserId) + } + assert.NoError(client2.RunUntilJoined(ctx, hello1.Hello, hello2.Hello)) + + // Test sending messages between sessions. data1 := "from-1-to-2" data2 := "from-2-to-1" if assert.NoError(client1.SendMessage(MessageClientMessageRecipient{ @@ -230,9 +275,91 @@ func Test_Federation(t *testing.T) { require.NotNil(room) room.PublishUsersInCallChanged(users, users) var event *EventServerMessage + assert.NoError(checkReceiveClientEvent(ctx, client1, "update", &event)) + assert.Equal(remoteSessionId, event.Update.Users[0]["sessionId"]) + assert.NoError(checkReceiveClientEvent(ctx, client2, "update", &event)) assert.Equal(hello2.Hello.SessionId, event.Update.Users[0]["sessionId"]) + // Joining another "direct" session will trigger correct events. + + client3 := NewTestClient(t, server1, hub1) + defer client3.CloseWithBye() + require.NoError(client3.SendHelloV2(testDefaultUserId + "3")) + + hello3, err := client3.RunUntilHello(ctx) + require.NoError(err) + + if room, err := client3.JoinRoom(ctx, roomId); assert.NoError(err) { + require.Equal(roomId, room.Room.RoomId) + } + + assert.NoError(client1.RunUntilJoined(ctx, hello3.Hello)) + assert.NoError(client2.RunUntilJoined(ctx, hello3.Hello)) + + assert.NoError(client3.RunUntilJoined(ctx, hello1.Hello, &HelloServerMessage{ + SessionId: remoteSessionId, + UserId: hello2.Hello.UserId, + }, hello3.Hello)) + + // Joining another "federated" session will trigger correct events. + + client4 := NewTestClient(t, server2, hub1) + defer client4.CloseWithBye() + require.NoError(client4.SendHelloV2(testDefaultUserId + "4")) + + hello4, err := client4.RunUntilHello(ctx) + require.NoError(err) + + token, err = client4.CreateHelloV2Token(testDefaultUserId+"4", now, now.Add(time.Minute)) + require.NoError(err) + + msg = &ClientMessage{ + Id: "join-room-fed", + Type: "room", + Room: &RoomClientMessage{ + RoomId: roomId, + SessionId: roomId + "-" + hello4.Hello.SessionId, + Federation: &RoomFederationMessage{ + SignalingUrl: server1.URL + "/spreed", + NextcloudUrl: server1.URL, + Token: token, + }, + }, + } + require.NoError(client4.WriteJSON(msg)) + + if message, err := client4.RunUntilMessage(ctx); assert.NoError(err) { + assert.Equal(msg.Id, message.Id) + require.Equal("room", message.Type) + require.Equal(roomId, message.Room.RoomId) + } + + // The client1 will see the remote session id for client2. + var remoteSessionId4 string + if message, err := client1.RunUntilMessage(ctx); assert.NoError(err) { + client1.checkSingleMessageJoined(message) + evt := message.Event.Join[0] + remoteSessionId4 = evt.SessionId + assert.NotEqual(hello4.Hello.SessionId, remoteSessionId) + assert.Equal(testDefaultUserId+"4", evt.UserId) + } + + assert.NoError(client2.RunUntilJoined(ctx, &HelloServerMessage{ + SessionId: remoteSessionId4, + UserId: hello4.Hello.UserId, + })) + + assert.NoError(client3.RunUntilJoined(ctx, &HelloServerMessage{ + SessionId: remoteSessionId4, + UserId: hello4.Hello.UserId, + })) + + assert.NoError(client4.RunUntilJoined(ctx, hello1.Hello, &HelloServerMessage{ + SessionId: remoteSessionId, + UserId: hello2.Hello.UserId, + }, hello3.Hello, hello4.Hello)) + room3, err := client2.JoinRoom(ctx, "") if assert.NoError(err) { assert.Equal("", room3.Room.RoomId) @@ -242,8 +369,6 @@ func Test_Federation(t *testing.T) { func Test_FederationMedia(t *testing.T) { CatchLogForTest(t) - CatchLogForTest(t) - assert := assert.New(t) require := require.New(t) From c055b123457d4d3a00651b31ccdb09f2ee781a19 Mon Sep 17 00:00:00 2001 From: Joachim Bauch Date: Thu, 18 Jul 2024 09:54:36 +0200 Subject: [PATCH 08/24] Support different federated room ids. --- api_signaling.go | 3 ++- api_signaling_easyjson.go | 7 +++++ federation.go | 56 +++++++++++++++++++++++++++++++++------ federation_test.go | 12 ++++++--- 4 files changed, 65 insertions(+), 13 deletions(-) diff --git a/api_signaling.go b/api_signaling.go index d92aebc8..7268399a 100644 --- a/api_signaling.go +++ b/api_signaling.go @@ -617,7 +617,8 @@ type RoomFederationMessage struct { NextcloudUrl string `json:"url"` parsedNextcloudUrl *url.URL - Token string `json:"token"` + RoomId string `json:"roomid,omitempty"` + Token string `json:"token"` } func (m *RoomFederationMessage) CheckValid() error { diff --git a/api_signaling_easyjson.go b/api_signaling_easyjson.go index 8ece1c60..62f83d31 100644 --- a/api_signaling_easyjson.go +++ b/api_signaling_easyjson.go @@ -916,6 +916,8 @@ func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling7(in *jlex out.SignalingUrl = string(in.String()) case "url": out.NextcloudUrl = string(in.String()) + case "roomid": + out.RoomId = string(in.String()) case "token": out.Token = string(in.String()) default: @@ -942,6 +944,11 @@ func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling7(out *jwr out.RawString(prefix) out.String(string(in.NextcloudUrl)) } + if in.RoomId != "" { + const prefix string = ",\"roomid\":" + out.RawString(prefix) + out.String(string(in.RoomId)) + } { const prefix string = ",\"token\":" out.RawString(prefix) diff --git a/federation.go b/federation.go index 1815b0c1..6a6c6ce7 100644 --- a/federation.go +++ b/federation.go @@ -47,6 +47,8 @@ type FederationClient struct { message atomic.Pointer[ClientMessage] roomId string + remoteRoomId string + changeRoomId bool roomSessionId string federation *RoomFederationMessage @@ -102,10 +104,17 @@ func NewFederationClient(ctx context.Context, hub *Hub, session *ClientSession, return nil, ErrFederationNotSupported } + remoteRoomId := room.Federation.RoomId + if remoteRoomId == "" { + remoteRoomId = room.RoomId + } + result := &FederationClient{ session: session, roomId: room.RoomId, + remoteRoomId: remoteRoomId, + changeRoomId: room.RoomId != remoteRoomId, roomSessionId: room.SessionId, federation: room.Federation, @@ -363,7 +372,7 @@ func (c *FederationClient) joinRoom() error { Id: id, Type: "room", Room: &RoomClientMessage{ - RoomId: c.roomId, + RoomId: c.remoteRoomId, SessionId: c.roomSessionId, }, }) @@ -386,13 +395,13 @@ func (c *FederationClient) updateEventUsers(users []map[string]interface{}, loca } } -func (c *FederationClient) updateRecipient(recipient *MessageClientMessageRecipient, localSessionId string, remoteSessionId string) { +func (c *FederationClient) updateSessionRecipient(recipient *MessageClientMessageRecipient, localSessionId string, remoteSessionId string) { if recipient != nil && recipient.Type == RecipientTypeSession && remoteSessionId != "" && recipient.SessionId == remoteSessionId { recipient.SessionId = localSessionId } } -func (c *FederationClient) updateSender(sender *MessageServerMessageSender, localSessionId string, remoteSessionId string) { +func (c *FederationClient) updateSessionSender(sender *MessageServerMessageSender, localSessionId string, remoteSessionId string) { if sender != nil && sender.Type == RecipientTypeSession && remoteSessionId != "" && sender.SessionId == remoteSessionId { sender.SessionId = localSessionId } @@ -408,21 +417,31 @@ func (c *FederationClient) processMessage(msg *ServerMessage) { var doClose bool switch msg.Type { case "control": - c.updateRecipient(msg.Control.Recipient, localSessionId, remoteSessionId) - c.updateSender(msg.Control.Sender, localSessionId, remoteSessionId) + c.updateSessionRecipient(msg.Control.Recipient, localSessionId, remoteSessionId) + c.updateSessionSender(msg.Control.Sender, localSessionId, remoteSessionId) case "event": switch msg.Event.Target { case "participants": switch msg.Event.Type { case "update": + if c.changeRoomId && msg.Event.Update.RoomId == c.remoteRoomId { + msg.Event.Update.RoomId = c.roomId + } if remoteSessionId != "" { c.updateEventUsers(msg.Event.Update.Changed, localSessionId, remoteSessionId) c.updateEventUsers(msg.Event.Update.Users, localSessionId, remoteSessionId) } case "flags": + if c.changeRoomId && msg.Event.Flags.RoomId == c.remoteRoomId { + msg.Event.Flags.RoomId = c.roomId + } if remoteSessionId != "" && msg.Event.Flags.SessionId == remoteSessionId { msg.Event.Flags.SessionId = localSessionId } + case "message": + if c.changeRoomId && msg.Event.Message.RoomId == c.remoteRoomId { + msg.Event.Message.RoomId = c.roomId + } } case "room": switch msg.Event.Type { @@ -447,15 +466,36 @@ func (c *FederationClient) processMessage(msg *ServerMessage) { } } } + case "message": + if c.changeRoomId && msg.Event.Message.RoomId == c.remoteRoomId { + msg.Event.Message.RoomId = c.roomId + } + } + case "roomlist": + switch msg.Event.Type { + case "invite": + if c.changeRoomId && msg.Event.Invite.RoomId == c.remoteRoomId { + msg.Event.Invite.RoomId = c.roomId + } + case "disinvite": + if c.changeRoomId && msg.Event.Disinvite.RoomId == c.remoteRoomId { + msg.Event.Disinvite.RoomId = c.roomId + } + case "update": + if c.changeRoomId && msg.Event.Update.RoomId == c.remoteRoomId { + msg.Event.Update.RoomId = c.roomId + } } } case "room": if msg.Room.RoomId == "" && c.closeOnLeave.Load() { doClose = true + } else if c.changeRoomId && msg.Room.RoomId == c.remoteRoomId { + msg.Room.RoomId = c.roomId } case "message": - c.updateRecipient(msg.Message.Recipient, localSessionId, remoteSessionId) - c.updateSender(msg.Message.Sender, localSessionId, remoteSessionId) + c.updateSessionRecipient(msg.Message.Recipient, localSessionId, remoteSessionId) + c.updateSessionSender(msg.Message.Sender, localSessionId, remoteSessionId) if remoteSessionId != "" && len(msg.Message.Data) > 0 { var ao AnswerOfferMessage if json.Unmarshal(msg.Message.Data, &ao) == nil && (ao.Type == "offer" || ao.Type == "answer") { @@ -488,7 +528,7 @@ func (c *FederationClient) ProxyMessage(message *ClientMessage) error { switch message.Type { case "message": if hello := c.hello.Load(); hello != nil { - c.updateRecipient(&message.Message.Recipient, hello.SessionId, c.session.PublicId()) + c.updateSessionRecipient(&message.Message.Recipient, hello.SessionId, c.session.PublicId()) } } diff --git a/federation_test.go b/federation_test.go index 5002f645..b317d67c 100644 --- a/federation_test.go +++ b/federation_test.go @@ -96,6 +96,7 @@ func Test_Federation(t *testing.T) { require.NoError(err) roomId := "test-room" + federatedRoomId := roomId + "@federated" room1, err := client1.JoinRoom(ctx, roomId) require.NoError(err) require.Equal(roomId, room1.Room.RoomId) @@ -110,11 +111,12 @@ func Test_Federation(t *testing.T) { Id: "join-room-fed", Type: "room", Room: &RoomClientMessage{ - RoomId: roomId, - SessionId: roomId + "-" + hello2.Hello.SessionId, + RoomId: federatedRoomId, + SessionId: federatedRoomId + "-" + hello2.Hello.SessionId, Federation: &RoomFederationMessage{ SignalingUrl: server1.URL + "/spreed", NextcloudUrl: server1.URL, + RoomId: roomId, Token: token, }, }, @@ -124,7 +126,7 @@ func Test_Federation(t *testing.T) { if message, err := client2.RunUntilMessage(ctx); assert.NoError(err) { assert.Equal(msg.Id, message.Id) require.Equal("room", message.Type) - require.Equal(roomId, message.Room.RoomId) + require.Equal(federatedRoomId, message.Room.RoomId) } // The client1 will see the remote session id for client2. @@ -171,7 +173,7 @@ func Test_Federation(t *testing.T) { if message, err := client2.RunUntilMessage(ctx); assert.NoError(err) { assert.Equal(msg.Id, message.Id) require.Equal("room", message.Type) - require.Equal(roomId, message.Room.RoomId) + require.Equal(federatedRoomId, message.Room.RoomId) } // Client1 will receive the updated "remoteSessionId" @@ -277,9 +279,11 @@ func Test_Federation(t *testing.T) { var event *EventServerMessage assert.NoError(checkReceiveClientEvent(ctx, client1, "update", &event)) assert.Equal(remoteSessionId, event.Update.Users[0]["sessionId"]) + assert.Equal(roomId, event.Update.RoomId) assert.NoError(checkReceiveClientEvent(ctx, client2, "update", &event)) assert.Equal(hello2.Hello.SessionId, event.Update.Users[0]["sessionId"]) + assert.Equal(federatedRoomId, event.Update.RoomId) // Joining another "direct" session will trigger correct events. From 4f95416b945aa85f78f540f76fc8b4767e5761d6 Mon Sep 17 00:00:00 2001 From: Joachim Bauch Date: Thu, 18 Jul 2024 10:08:14 +0200 Subject: [PATCH 09/24] Send websocket close message before closing connection and don't log error if explicitly closed. --- client.go | 2 +- federation.go | 18 ++++++++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/client.go b/client.go index c7d1cc13..c9f1de09 100644 --- a/client.go +++ b/client.go @@ -365,7 +365,7 @@ func (c *Client) ReadPump() { messageType, reader, err := conn.NextReader() if err != nil { // Gorilla websocket hides the original net.Error, so also compare error messages - if errors.Is(err, net.ErrClosed) || strings.Contains(err.Error(), net.ErrClosed.Error()) { + if errors.Is(err, net.ErrClosed) || errors.Is(err, websocket.ErrCloseSent) || strings.Contains(err.Error(), net.ErrClosed.Error()) { break } else if _, ok := err.(*websocket.CloseError); !ok || websocket.IsUnexpectedCloseError(err, websocket.CloseNormalClosure, diff --git a/federation.go b/federation.go index 6a6c6ce7..9ad492ab 100644 --- a/federation.go +++ b/federation.go @@ -28,6 +28,7 @@ import ( "errors" "fmt" "log" + "net" "strconv" "strings" "sync" @@ -158,7 +159,7 @@ func (c *FederationClient) Leave(message *ClientMessage) error { } } - if err := c.sendMessageLocked(message); err != nil { + if err := c.sendMessageLocked(message); err != nil && !errors.Is(err, websocket.ErrCloseSent) { return err } @@ -176,10 +177,16 @@ func (c *FederationClient) Close() { if err := c.sendMessageLocked(&ClientMessage{ Type: "bye", - }); err != nil { + }); err != nil && !errors.Is(err, websocket.ErrCloseSent) { log.Printf("Error sending bye on federation connection to %s: %s", c.URL(), err) } + closeMessage := websocket.FormatCloseMessage(websocket.CloseNormalClosure, "") + deadline := time.Now().Add(writeWait) + if err := c.conn.WriteControl(websocket.CloseMessage, closeMessage, deadline); err != nil && !errors.Is(err, websocket.ErrCloseSent) { + log.Printf("Error sending close message on federation connection to %s: %s", c.URL(), err) + } + if err := c.conn.Close(); err != nil { log.Printf("Error closing federation connection to %s: %s", c.URL(), err) } @@ -211,6 +218,13 @@ func (c *FederationClient) readPump() { conn.SetReadDeadline(time.Now().Add(pongWait)) // nolint msgType, data, err := conn.ReadMessage() if err != nil { + // Gorilla websocket hides the original net.Error, so also compare error messages + if c.closer.IsClosed() && (errors.Is(err, net.ErrClosed) || errors.Is(err, websocket.ErrCloseSent) || strings.Contains(err.Error(), net.ErrClosed.Error())) { + break + } else if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseNoStatusReceived) { + break + } + log.Printf("Error reading: %s", err) break } From cf2e3aa5e87902df47e9312c472470d8ef17b479 Mon Sep 17 00:00:00 2001 From: Joachim Bauch Date: Thu, 18 Jul 2024 11:19:49 +0200 Subject: [PATCH 10/24] Return details for errors while creating federated clients. --- hub.go | 53 +++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 47 insertions(+), 6 deletions(-) diff --git a/hub.go b/hub.go index 30513748..a0477235 100644 --- a/hub.go +++ b/hub.go @@ -27,6 +27,7 @@ import ( "crypto/ed25519" "crypto/hmac" "crypto/sha256" + "crypto/tls" "crypto/x509" "encoding/base64" "encoding/hex" @@ -1528,21 +1529,61 @@ func (h *Hub) processRoom(sess Session, message *ClientMessage) { } if federation := message.Room.Federation; federation != nil { + h.mu.Lock() + // The session will join a room, make sure it doesn't expire while connecting. + delete(h.anonymousSessions, session) + h.mu.Unlock() + // TODO: Handle case where session already is in a federated room on the same server. client, err := NewFederationClient(session.Context(), h, session, message) if err != nil { - log.Printf("Error creating federation client for %s to join room %s: %s", session.PublicId(), roomId, err) + if session.UserId() == "" { + h.startWaitAnonymousSessionRoom(session) + } + var ae *Error + if errors.As(err, &ae) { + session.SendMessage(message.NewErrorServerMessage(ae)) + return + } + + var details interface{} + var ce *tls.CertificateVerificationError + if errors.As(err, &ce) { + details = map[string]string{ + "code": "certificate_verification_error", + "message": ce.Error(), + } + } + var ne net.Error + if details == nil && errors.As(err, &ne) { + details = map[string]string{ + "code": "network_error", + "message": ne.Error(), + } + } + if details == nil { + var we websocket.HandshakeError + if errors.Is(err, websocket.ErrBadHandshake) { + details = map[string]string{ + "code": "network_error", + "message": err.Error(), + } + } else if errors.As(err, &we) { + details = map[string]string{ + "code": "network_error", + "message": we.Error(), + } + } + } + + log.Printf("Error creating federation client to %s for %s to join room %s: %s", federation.SignalingUrl, session.PublicId(), roomId, err) session.SendMessage(message.NewErrorServerMessage( - NewErrorDetail("federation_error", "Failed to create federation client.", nil), + NewErrorDetail("federation_error", "Failed to create federation client.", details), )) return } session.SetFederationClient(client) - h.mu.Lock() - // The session now joined a room, don't expire if it is anonymous. - delete(h.anonymousSessions, session) - h.mu.Unlock() return } From deb7f713a7a66cc02b3b1845bf15dad2ca2955cc Mon Sep 17 00:00:00 2001 From: Joachim Bauch Date: Thu, 18 Jul 2024 11:27:05 +0200 Subject: [PATCH 11/24] Add option to disable certificate validation for federation --- federation.go | 8 +++++--- hub.go | 9 +++++++++ server.conf.in | 6 ++++++ 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/federation.go b/federation.go index 9ad492ab..15b6b5fa 100644 --- a/federation.go +++ b/federation.go @@ -71,11 +71,13 @@ func NewFederationClient(ctx context.Context, hub *Hub, session *ClientSession, } var dialer websocket.Dialer - dialer.TLSClientConfig = &tls.Config{ - InsecureSkipVerify: true, - } room := message.Room + if hub.skipFederationVerify { + dialer.TLSClientConfig = &tls.Config{ + InsecureSkipVerify: true, + } + } u := *room.Federation.parsedSignalingUrl switch u.Scheme { case "http": diff --git a/hub.go b/hub.go index a0477235..c758c358 100644 --- a/hub.go +++ b/hub.go @@ -179,6 +179,8 @@ type Hub struct { rpcClients *GrpcClients throttler Throttler + + skipFederationVerify bool } func NewHub(config *goconf.ConfigFile, events AsyncEvents, rpcServer *GrpcServer, rpcClients *GrpcClients, etcdClient *EtcdClient, r *mux.Router, version string) (*Hub, error) { @@ -242,6 +244,11 @@ func NewHub(config *goconf.ConfigFile, events AsyncEvents, rpcServer *GrpcServer return nil, err } + skipFederationVerify, _ := config.GetBool("federation", "skipverify") + if skipFederationVerify { + log.Println("WARNING: Federation target verification is disabled!") + } + if !trustedProxiesIps.Empty() { log.Printf("Trusted proxies: %s", trustedProxiesIps) } else { @@ -350,6 +357,8 @@ func NewHub(config *goconf.ConfigFile, events AsyncEvents, rpcServer *GrpcServer rpcClients: rpcClients, throttler: throttler, + + skipFederationVerify: skipFederationVerify, } hub.trustedProxies.Store(trustedProxiesIps) if len(geoipOverrides) > 0 { diff --git a/server.conf.in b/server.conf.in index b748cacd..74238c9f 100644 --- a/server.conf.in +++ b/server.conf.in @@ -55,6 +55,12 @@ blockkey = -encryption-key- # value as configured in the respective internal services. internalsecret = the-shared-secret-for-internal-clients +[federation] +# If set to "true", certificate validation of federation targets will be skipped. +# This should only be enabled during development, e.g. to work with self-signed +# certificates. +#skipverify = false + [backend] # Type of backend configuration. # Defaults to "static". From 1ec09d6f0f1b5e88a5766b024688066d2eb2f799 Mon Sep 17 00:00:00 2001 From: Joachim Bauch Date: Thu, 18 Jul 2024 11:34:29 +0200 Subject: [PATCH 12/24] Support timeouts for federation connections. --- hub.go | 15 ++++++++++++++- server.conf.in | 3 +++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/hub.go b/hub.go index c758c358..4f4a5ce2 100644 --- a/hub.go +++ b/hub.go @@ -76,6 +76,9 @@ var ( // MCU requests will be cancelled if they take too long. defaultMcuTimeoutSeconds = 10 + // Federation requests will be cancelled if they take too long. + defaultFederationTimeoutSeconds = 10 + // New connections have to send a "Hello" request after 2 seconds. initialHelloTimeout = 2 * time.Second @@ -181,6 +184,7 @@ type Hub struct { throttler Throttler skipFederationVerify bool + federationTimeout time.Duration } func NewHub(config *goconf.ConfigFile, events AsyncEvents, rpcServer *GrpcServer, rpcClients *GrpcClients, etcdClient *EtcdClient, r *mux.Router, version string) (*Hub, error) { @@ -248,6 +252,11 @@ func NewHub(config *goconf.ConfigFile, events AsyncEvents, rpcServer *GrpcServer if skipFederationVerify { log.Println("WARNING: Federation target verification is disabled!") } + federationTimeoutSeconds, _ := config.GetInt("federation", "timeout") + if federationTimeoutSeconds <= 0 { + federationTimeoutSeconds = defaultFederationTimeoutSeconds + } + federationTimeout := time.Duration(federationTimeoutSeconds) * time.Second if !trustedProxiesIps.Empty() { log.Printf("Trusted proxies: %s", trustedProxiesIps) @@ -359,6 +368,7 @@ func NewHub(config *goconf.ConfigFile, events AsyncEvents, rpcServer *GrpcServer throttler: throttler, skipFederationVerify: skipFederationVerify, + federationTimeout: federationTimeout, } hub.trustedProxies.Store(trustedProxiesIps) if len(geoipOverrides) > 0 { @@ -1543,8 +1553,11 @@ func (h *Hub) processRoom(sess Session, message *ClientMessage) { delete(h.anonymousSessions, session) h.mu.Unlock() + ctx, cancel := context.WithTimeout(session.Context(), h.federationTimeout) + defer cancel() + // TODO: Handle case where session already is in a federated room on the same server. - client, err := NewFederationClient(session.Context(), h, session, message) + client, err := NewFederationClient(ctx, h, session, message) if err != nil { if session.UserId() == "" { h.startWaitAnonymousSessionRoom(session) diff --git a/server.conf.in b/server.conf.in index 74238c9f..85630d5a 100644 --- a/server.conf.in +++ b/server.conf.in @@ -61,6 +61,9 @@ internalsecret = the-shared-secret-for-internal-clients # certificates. #skipverify = false +# Timeout in seconds for requests to federation targets. +#timeout = 10 + [backend] # Type of backend configuration. # Defaults to "static". From 1873d151dc4da43aa2d5dfe63947bbdbe897d5a3 Mon Sep 17 00:00:00 2001 From: Joachim Bauch Date: Thu, 18 Jul 2024 12:03:15 +0200 Subject: [PATCH 13/24] Add information on federation to documentation. --- docs/standalone-signaling-api-v1.md | 54 ++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/docs/standalone-signaling-api-v1.md b/docs/standalone-signaling-api-v1.md index d05ae68d..5496247e 100644 --- a/docs/standalone-signaling-api-v1.md +++ b/docs/standalone-signaling-api-v1.md @@ -437,7 +437,8 @@ Message format (Client -> Server): - The client can ask about joining a room using this request. - The session id received from the PHP backend must be passed as `sessionid`. -- The `roomid` can be empty to leave the room. +- The `roomid` can be empty to leave the room the client is currently in + (local or federated). - A session can only be connected to one room, i.e. joining a room will leave the room currently in. @@ -524,6 +525,57 @@ user, the backend returns an error and the room request will be rejected. to the room. +## Join federated room + +If the features list contains the id `federation`, the signaling server supports +joining rooms on external signaling servers for Nextcloud instances not +configured in the local server. + +Message format (Client -> Server): + + { + "id": "unique-request-id", + "type": "room", + "room": { + "roomid": "the-local-room-id", + "sessionid": "the-nextcloud-session-id", + "federation": { + "signaling": "wss://remote.domain.invalid/path/to/signaling/spreed", + "url": "https://remote.domain.invalid/path/to/nextcloud/", + "roomid": "the-remote-room-id", + "token": "hello-v2-auth-token-for-remote-signaling-server" + } + } + } + +- The remote room id is optional. If omitted, the local room id will be used. +- If a session joins a federated room, any local room will be left. + +Message format (Server -> Client): + + { + "id": "unique-request-id-from-request", + "type": "room", + "room": { + "roomid": "the-local-room-id", + "properties": { + ...additional room properties... + } + } + } + +- Sent to confirm a request from the client. + + +### Error codes + +- `federation_unsupported`: Federation is not supported by the target server. +- `federation_error`: Error while creating connection to target server + (additional information might be available in `details`). + +Also the error codes from joining a regular room could be returned. + + ## Leave room To leave a room, a [join room](#join-room) message must be sent with an empty From c3d24b60ead0d513c5ba7ce7e7bfaf27cb9b090b Mon Sep 17 00:00:00 2001 From: Joachim Bauch Date: Thu, 18 Jul 2024 14:49:04 +0200 Subject: [PATCH 14/24] Expect base signaling url for federation requests. --- api_signaling.go | 7 ++++++- docs/standalone-signaling-api-v1.md | 2 +- federation.go | 2 +- federation_test.go | 8 ++++---- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/api_signaling.go b/api_signaling.go index 7268399a..2c211e34 100644 --- a/api_signaling.go +++ b/api_signaling.go @@ -624,7 +624,12 @@ type RoomFederationMessage struct { func (m *RoomFederationMessage) CheckValid() error { if m.SignalingUrl == "" { return errors.New("signaling url missing") - } else if u, err := url.Parse(m.SignalingUrl); err != nil { + } + + if m.SignalingUrl[len(m.SignalingUrl)-1] != '/' { + m.SignalingUrl += "/" + } + if u, err := url.Parse(m.SignalingUrl); err != nil { return fmt.Errorf("invalid signaling url: %w", err) } else { m.parsedSignalingUrl = u diff --git a/docs/standalone-signaling-api-v1.md b/docs/standalone-signaling-api-v1.md index 5496247e..62e190fe 100644 --- a/docs/standalone-signaling-api-v1.md +++ b/docs/standalone-signaling-api-v1.md @@ -540,7 +540,7 @@ Message format (Client -> Server): "roomid": "the-local-room-id", "sessionid": "the-nextcloud-session-id", "federation": { - "signaling": "wss://remote.domain.invalid/path/to/signaling/spreed", + "signaling": "wss://remote.domain.invalid/path/to/signaling/", "url": "https://remote.domain.invalid/path/to/nextcloud/", "roomid": "the-remote-room-id", "token": "hello-v2-auth-token-for-remote-signaling-server" diff --git a/federation.go b/federation.go index 15b6b5fa..df5d859c 100644 --- a/federation.go +++ b/federation.go @@ -85,7 +85,7 @@ func NewFederationClient(ctx context.Context, hub *Hub, session *ClientSession, case "https": u.Scheme = "wss" } - conn, response, err := dialer.DialContext(ctx, u.String(), nil) + conn, response, err := dialer.DialContext(ctx, u.String()+"spreed", nil) if err != nil { return nil, err } diff --git a/federation_test.go b/federation_test.go index b317d67c..c35586b3 100644 --- a/federation_test.go +++ b/federation_test.go @@ -55,7 +55,7 @@ func Test_FederationInvalidToken(t *testing.T) { RoomId: "test-room", SessionId: "room-session-id", Federation: &RoomFederationMessage{ - SignalingUrl: server1.URL + "/spreed", + SignalingUrl: server1.URL, NextcloudUrl: server1.URL, Token: "invalid-token", }, @@ -114,7 +114,7 @@ func Test_Federation(t *testing.T) { RoomId: federatedRoomId, SessionId: federatedRoomId + "-" + hello2.Hello.SessionId, Federation: &RoomFederationMessage{ - SignalingUrl: server1.URL + "/spreed", + SignalingUrl: server1.URL, NextcloudUrl: server1.URL, RoomId: roomId, Token: token, @@ -325,7 +325,7 @@ func Test_Federation(t *testing.T) { RoomId: roomId, SessionId: roomId + "-" + hello4.Hello.SessionId, Federation: &RoomFederationMessage{ - SignalingUrl: server1.URL + "/spreed", + SignalingUrl: server1.URL, NextcloudUrl: server1.URL, Token: token, }, @@ -427,7 +427,7 @@ func Test_FederationMedia(t *testing.T) { RoomId: roomId, SessionId: roomId + "-" + hello2.Hello.SessionId, Federation: &RoomFederationMessage{ - SignalingUrl: server1.URL + "/spreed", + SignalingUrl: server1.URL, NextcloudUrl: server1.URL, Token: token, }, From ffa70f3f6692c77fb3dac7f79d1de868f25c1fd6 Mon Sep 17 00:00:00 2001 From: Joachim Bauch Date: Thu, 18 Jul 2024 15:48:03 +0200 Subject: [PATCH 15/24] Add missing error handling. --- federation.go | 8 ++++++-- federation_test.go | 8 ++++---- hub.go | 4 +++- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/federation.go b/federation.go index df5d859c..dff30db9 100644 --- a/federation.go +++ b/federation.go @@ -359,7 +359,9 @@ func (c *FederationClient) processHello(msg *ServerMessage) { if msg.Id != c.helloMsgId { log.Printf("Received hello response %+v for unknown request, expected %s", msg, c.helloMsgId) - c.sendHelloLocked(c.helloAuth) + if err := c.sendHelloLocked(c.helloAuth); err != nil { + c.closeWithError(err) + } return } @@ -369,7 +371,9 @@ func (c *FederationClient) processHello(msg *ServerMessage) { return } else if msg.Type != "hello" { log.Printf("Received unknown hello response %+v", msg) - c.sendHelloLocked(c.helloAuth) + if err := c.sendHelloLocked(c.helloAuth); err != nil { + c.closeWithError(err) + } return } diff --git a/federation_test.go b/federation_test.go index c35586b3..c5465b2f 100644 --- a/federation_test.go +++ b/federation_test.go @@ -132,7 +132,7 @@ func Test_Federation(t *testing.T) { // The client1 will see the remote session id for client2. var remoteSessionId string if message, err := client1.RunUntilMessage(ctx); assert.NoError(err) { - client1.checkSingleMessageJoined(message) + assert.NoError(client1.checkSingleMessageJoined(message)) evt := message.Event.Join[0] remoteSessionId = evt.SessionId assert.NotEqual(hello2.Hello.SessionId, remoteSessionId) @@ -178,7 +178,7 @@ func Test_Federation(t *testing.T) { // Client1 will receive the updated "remoteSessionId" if message, err := client1.RunUntilMessage(ctx); assert.NoError(err) { - client1.checkSingleMessageJoined(message) + assert.NoError(client1.checkSingleMessageJoined(message)) evt := message.Event.Join[0] remoteSessionId = evt.SessionId assert.NotEqual(hello2.Hello.SessionId, remoteSessionId) @@ -342,7 +342,7 @@ func Test_Federation(t *testing.T) { // The client1 will see the remote session id for client2. var remoteSessionId4 string if message, err := client1.RunUntilMessage(ctx); assert.NoError(err) { - client1.checkSingleMessageJoined(message) + assert.NoError(client1.checkSingleMessageJoined(message)) evt := message.Event.Join[0] remoteSessionId4 = evt.SessionId assert.NotEqual(hello4.Hello.SessionId, remoteSessionId) @@ -444,7 +444,7 @@ func Test_FederationMedia(t *testing.T) { // The client1 will see the remote session id for client2. var remoteSessionId string if message, err := client1.RunUntilMessage(ctx); assert.NoError(err) { - client1.checkSingleMessageJoined(message) + assert.NoError(client1.checkSingleMessageJoined(message)) evt := message.Event.Join[0] remoteSessionId = evt.SessionId assert.NotEqual(hello2.Hello.SessionId, remoteSessionId) diff --git a/hub.go b/hub.go index 4f4a5ce2..d7b1775b 100644 --- a/hub.go +++ b/hub.go @@ -1030,7 +1030,9 @@ func (h *Hub) processMessage(client HandlerClient, data []byte) { message.Type == "bye" if cs, ok := session.(*ClientSession); ok && !isLocalMessage { if federated := cs.GetFederationClient(); federated != nil { - federated.ProxyMessage(&message) + if err := federated.ProxyMessage(&message); err != nil { + client.SendMessage(message.NewWrappedErrorServerMessage(err)) + } return } } From a256789f20d4fbfc9f06bd09b404b0908b94c7cd Mon Sep 17 00:00:00 2001 From: Joachim Bauch Date: Wed, 24 Jul 2024 10:23:10 +0200 Subject: [PATCH 16/24] Support reconnecting the internal federated connection. --- api_signaling.go | 5 + api_signaling_easyjson.go | 15 ++ clientsession.go | 2 +- docs/standalone-signaling-api-v1.md | 38 ++++ federation.go | 316 +++++++++++++++++++++------- federation_test.go | 248 ++++++++++++++++++++++ 6 files changed, 545 insertions(+), 79 deletions(-) diff --git a/api_signaling.go b/api_signaling.go index 2c211e34..5434e500 100644 --- a/api_signaling.go +++ b/api_signaling.go @@ -48,6 +48,10 @@ var ( ErrInvalidSdp = NewError("invalid_sdp", "Payload does not contain a valid SDP.") ) +func makePtr[T any](v T) *T { + return &v +} + // ClientMessage is a message that is sent from a client to the server. type ClientMessage struct { json.Marshaler @@ -1024,6 +1028,7 @@ type EventServerMessage struct { Leave []string `json:"leave,omitempty"` Change []*EventServerMessageSessionEntry `json:"change,omitempty"` SwitchTo *EventServerMessageSwitchTo `json:"switchto,omitempty"` + Resumed *bool `json:"resumed,omitempty"` // Used for target "roomlist" / "participants" Invite *RoomEventServerMessage `json:"invite,omitempty"` diff --git a/api_signaling_easyjson.go b/api_signaling_easyjson.go index 62f83d31..b8264bbe 100644 --- a/api_signaling_easyjson.go +++ b/api_signaling_easyjson.go @@ -4129,6 +4129,16 @@ func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling36(in *jle } (*out.SwitchTo).UnmarshalEasyJSON(in) } + case "resumed": + if in.IsNull() { + in.Skip() + out.Resumed = nil + } else { + if out.Resumed == nil { + out.Resumed = new(bool) + } + *out.Resumed = bool(in.Bool()) + } case "invite": if in.IsNull() { in.Skip() @@ -4258,6 +4268,11 @@ func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling36(out *jw out.RawString(prefix) (*in.SwitchTo).MarshalEasyJSON(out) } + if in.Resumed != nil { + const prefix string = ",\"resumed\":" + out.RawString(prefix) + out.Bool(bool(*in.Resumed)) + } if in.Invite != nil { const prefix string = ",\"invite\":" out.RawString(prefix) diff --git a/clientsession.go b/clientsession.go index b7a90955..5e2fe4f5 100644 --- a/clientsession.go +++ b/clientsession.go @@ -348,7 +348,7 @@ func (s *ClientSession) SetFederationClient(federation *FederationClient) { s.doLeaveRoom(true) s.onRoomSet(federation != nil) - if prev := s.federation.Swap(federation); prev != nil { + if prev := s.federation.Swap(federation); prev != nil && prev != federation { prev.Close() } } diff --git a/docs/standalone-signaling-api-v1.md b/docs/standalone-signaling-api-v1.md index 62e190fe..2fe9b49d 100644 --- a/docs/standalone-signaling-api-v1.md +++ b/docs/standalone-signaling-api-v1.md @@ -576,6 +576,44 @@ Message format (Server -> Client): Also the error codes from joining a regular room could be returned. +### Events + +The signaling server tries to resume the internal proxy session if the +connection to the remote server gets interrupted. To notify clients about these +interruptions, two additional events may be sent from the server to the client: + +Connection was interrupted (Server -> Client): + + { + "type": "event", + "event": { + "target": "room", + "type": "federation_interrupted" + } + } + + +Connection was resumed (Server -> Client): + + { + "type": "event", + "event": { + "target": "room", + "type": "federation_resumed", + "resumed": true + } + } + +The `resumed` flag will be `true` if the existing internal session could be +resumed (i.e. the client stayed in the remote room), or `false` if a new +internal session was created. + +If a new internal session was created, the client will receive another `room` +event for the joined room and `join` events for the different participants in +the room. This should be handled the same as if the direct session could not +be resumed on reconnect. + + ## Leave room To leave a room, a [join room](#join-room) message must be sent with an empty diff --git a/federation.go b/federation.go index dff30db9..3625dfd1 100644 --- a/federation.go +++ b/federation.go @@ -39,11 +39,17 @@ import ( easyjson "github.com/mailru/easyjson" ) +const ( + initialFederationReconnectInterval = 100 * time.Millisecond + maxFederationReconnectInterval = 8 * time.Second +) + var ( ErrFederationNotSupported = NewError("federation_unsupported", "The target server does not support federation.") ) type FederationClient struct { + hub *Hub session *ClientSession message atomic.Pointer[ClientMessage] @@ -53,31 +59,39 @@ type FederationClient struct { roomSessionId string federation *RoomFederationMessage - mu sync.Mutex - conn *websocket.Conn - closer *Closer + mu sync.Mutex + dialer *websocket.Dialer + url string + conn *websocket.Conn + closer *Closer + reconnectDelay time.Duration + reconnecting bool + reconnectFunc *time.Timer helloMu sync.Mutex helloMsgId string helloAuth *FederationAuthParams + resumeId string hello atomic.Pointer[HelloServerMessage] + pendingMessages []*ClientMessage + closeOnLeave atomic.Bool } func NewFederationClient(ctx context.Context, hub *Hub, session *ClientSession, message *ClientMessage) (*FederationClient, error) { - if message.Type != "room" || message.Room == nil { - return nil, fmt.Errorf("expected room message, got %+v", message) + if message.Type != "room" || message.Room == nil || message.Room.Federation == nil { + return nil, fmt.Errorf("expected federation room message, got %+v", message) } var dialer websocket.Dialer - - room := message.Room if hub.skipFederationVerify { dialer.TLSClientConfig = &tls.Config{ InsecureSkipVerify: true, } } + + room := message.Room u := *room.Federation.parsedSignalingUrl switch u.Scheme { case "http": @@ -85,27 +99,7 @@ func NewFederationClient(ctx context.Context, hub *Hub, session *ClientSession, case "https": u.Scheme = "wss" } - conn, response, err := dialer.DialContext(ctx, u.String()+"spreed", nil) - if err != nil { - return nil, err - } - - features := strings.Split(response.Header.Get("X-Spreed-Signaling-Features"), ",") - supportsFederation := false - for _, f := range features { - f = strings.TrimSpace(f) - if f == ServerFeatureFederation { - supportsFederation = true - break - } - } - if !supportsFederation { - if err := conn.Close(); err != nil { - log.Printf("Error closing federation connection to %s: %s", room.Federation.parsedSignalingUrl.String(), err) - } - - return nil, ErrFederationNotSupported - } + url := u.String() + "spreed" remoteRoomId := room.Federation.RoomId if remoteRoomId == "" { @@ -113,6 +107,7 @@ func NewFederationClient(ctx context.Context, hub *Hub, session *ClientSession, } result := &FederationClient{ + hub: hub, session: session, roomId: room.RoomId, @@ -121,18 +116,17 @@ func NewFederationClient(ctx context.Context, hub *Hub, session *ClientSession, roomSessionId: room.SessionId, federation: room.Federation, - conn: conn, + reconnectDelay: initialFederationReconnectInterval, + + dialer: &dialer, + url: url, closer: NewCloser(), } result.message.Store(message) - log.Printf("Creating federation connection to %s for %s", result.URL(), result.session.PublicId()) - - go func() { - hub.readPumpActive.Add(1) - defer hub.readPumpActive.Add(-1) - result.readPump() - }() + if err := result.connect(ctx); err != nil { + return nil, err + } go func() { hub.writePumpActive.Add(1) @@ -148,6 +142,52 @@ func (c *FederationClient) URL() string { return c.federation.parsedSignalingUrl.String() } +func (c *FederationClient) connect(ctx context.Context) error { + log.Printf("Creating federation connection to %s for %s", c.URL(), c.session.PublicId()) + conn, response, err := c.dialer.DialContext(ctx, c.url, nil) + if err != nil { + return err + } + + features := strings.Split(response.Header.Get("X-Spreed-Signaling-Features"), ",") + supportsFederation := false + for _, f := range features { + f = strings.TrimSpace(f) + if f == ServerFeatureFederation { + supportsFederation = true + break + } + } + if !supportsFederation { + if err := conn.Close(); err != nil { + log.Printf("Error closing federation connection to %s: %s", c.URL(), err) + } + + return ErrFederationNotSupported + } + + log.Printf("Federation connection established to %s for %s", c.URL(), c.session.PublicId()) + + c.mu.Lock() + defer c.mu.Unlock() + + if c.reconnectFunc != nil { + c.reconnectFunc.Stop() + c.reconnectFunc = nil + } + + c.conn = conn + + go func() { + c.hub.readPumpActive.Add(1) + defer c.hub.readPumpActive.Add(-1) + + c.readPump(conn) + }() + + return nil +} + func (c *FederationClient) Leave(message *ClientMessage) error { c.mu.Lock() defer c.mu.Unlock() @@ -171,16 +211,24 @@ func (c *FederationClient) Leave(message *ClientMessage) error { func (c *FederationClient) Close() { c.closer.Close() + c.mu.Lock() defer c.mu.Unlock() + + c.closeConnection(true) +} + +func (c *FederationClient) closeConnection(withBye bool) { if c.conn == nil { return } - if err := c.sendMessageLocked(&ClientMessage{ - Type: "bye", - }); err != nil && !errors.Is(err, websocket.ErrCloseSent) { - log.Printf("Error sending bye on federation connection to %s: %s", c.URL(), err) + if withBye { + if err := c.sendMessageLocked(&ClientMessage{ + Type: "bye", + }); err != nil && !errors.Is(err, websocket.ErrCloseSent) { + log.Printf("Error sending bye on federation connection to %s: %s", c.URL(), err) + } } closeMessage := websocket.FormatCloseMessage(websocket.CloseNormalClosure, "") @@ -196,19 +244,58 @@ func (c *FederationClient) Close() { c.conn = nil } -func (c *FederationClient) readPump() { - defer func() { - c.Close() - }() +func (c *FederationClient) resetReconnect() { + c.mu.Lock() + defer c.mu.Unlock() + c.reconnectDelay = initialFederationReconnectInterval +} +func (c *FederationClient) scheduleReconnect() { c.mu.Lock() - conn := c.conn - c.mu.Unlock() - if conn == nil { - log.Printf("Connection to %s closed while starting readPump", c.URL()) + defer c.mu.Unlock() + + c.scheduleReconnectLocked() +} + +func (c *FederationClient) scheduleReconnectLocked() { + c.reconnecting = true + if c.hello.Swap(nil) != nil { + c.session.SendMessage(&ServerMessage{ + Type: "event", + Event: &EventServerMessage{ + Target: "room", + Type: "federation_interrupted", + }, + }) + } + c.closeConnection(false) + + if c.reconnectFunc != nil { + c.reconnectFunc.Stop() + } + c.reconnectFunc = time.AfterFunc(c.reconnectDelay, c.reconnect) + c.reconnectDelay *= 2 + if c.reconnectDelay > maxFederationReconnectInterval { + c.reconnectDelay = maxFederationReconnectInterval + } +} + +func (c *FederationClient) reconnect() { + if c.closer.IsClosed() { + return + } + + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(c.hub.federationTimeout)) + defer cancel() + + if err := c.connect(ctx); err != nil { + log.Printf("Error connecting to federation server %s for %s: %s", c.URL(), c.session.PublicId(), err) + c.scheduleReconnect() return } +} +func (c *FederationClient) readPump(conn *websocket.Conn) { conn.SetReadLimit(maxMessageSize) conn.SetPongHandler(func(msg string) error { now := time.Now() @@ -222,12 +309,15 @@ func (c *FederationClient) readPump() { if err != nil { // Gorilla websocket hides the original net.Error, so also compare error messages if c.closer.IsClosed() && (errors.Is(err, net.ErrClosed) || errors.Is(err, websocket.ErrCloseSent) || strings.Contains(err.Error(), net.ErrClosed.Error())) { + // Connection closed locally, no need to reconnect. break - } else if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseNoStatusReceived) { - break } - log.Printf("Error reading: %s", err) + if !websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseNoStatusReceived) { + log.Printf("Error reading from %s for %s: %s", c.URL(), c.session.PublicId(), err) + } + + c.scheduleReconnect() break } @@ -255,22 +345,20 @@ func (c *FederationClient) readPump() { } } -func (c *FederationClient) sendPing() bool { +func (c *FederationClient) sendPing() { c.mu.Lock() defer c.mu.Unlock() if c.conn == nil { - return false + return } now := time.Now().UnixNano() msg := strconv.FormatInt(now, 10) c.conn.SetWriteDeadline(time.Now().Add(writeWait)) // nolint if err := c.conn.WriteMessage(websocket.PingMessage, []byte(msg)); err != nil { - log.Printf("Could not send ping to federated client %s: %v", c.session.PublicId(), err) - return false + log.Printf("Could not send ping to federated client %s for %s: %v", c.URL(), c.session.PublicId(), err) + c.scheduleReconnectLocked() } - - return true } func (c *FederationClient) writePump() { @@ -280,9 +368,7 @@ func (c *FederationClient) writePump() { for { select { case <-ticker.C: - if !c.sendPing() { - return - } + c.sendPing() case <-c.closer.C: return } @@ -324,18 +410,23 @@ func (c *FederationClient) sendHelloLocked(auth *FederationAuthParams) error { } c.helloAuth = auth - return c.SendMessage(&ClientMessage{ + msg := &ClientMessage{ Id: c.helloMsgId, Type: "hello", Hello: &HelloClientMessage{ Version: HelloVersionV2, - Auth: &HelloClientMessageAuth{ - Type: HelloClientTypeFederation, - Url: c.federation.NextcloudUrl, - Params: authData, - }, }, - }) + } + if resumeId := c.resumeId; resumeId != "" { + msg.Hello.ResumeId = resumeId + } else { + msg.Hello.Auth = &HelloClientMessageAuth{ + Type: HelloClientTypeFederation, + Url: c.federation.NextcloudUrl, + Params: authData, + } + } + return c.SendMessage(msg) } func (c *FederationClient) processWelcome(msg *ServerMessage) { @@ -354,6 +445,8 @@ func (c *FederationClient) processWelcome(msg *ServerMessage) { } func (c *FederationClient) processHello(msg *ServerMessage) { + c.resetReconnect() + c.helloMu.Lock() defer c.helloMu.Unlock() @@ -367,10 +460,22 @@ func (c *FederationClient) processHello(msg *ServerMessage) { c.helloMsgId = "" if msg.Type == "error" { - c.closeWithError(msg.Error) + switch msg.Error.Code { + case "no_such_session": + // Resume failed (e.g. remote has restarted), try to connect new session + // which may fail if the auth token has expired in the meantime. + c.resumeId = "" + c.pendingMessages = nil + if err := c.sendHelloLocked(c.helloAuth); err != nil { + c.closeWithError(err) + } + default: + log.Printf("Received hello error from federated client for %s to %s: %+v", c.session.PublicId(), c.URL(), msg) + c.closeWithError(msg.Error) + } return } else if msg.Type != "hello" { - log.Printf("Received unknown hello response %+v", msg) + log.Printf("Received unknown hello response from federated client for %s to %s: %+v", c.session.PublicId(), c.URL(), msg) if err := c.sendHelloLocked(c.helloAuth); err != nil { c.closeWithError(err) } @@ -378,8 +483,53 @@ func (c *FederationClient) processHello(msg *ServerMessage) { } c.hello.Store(msg.Hello) - if err := c.joinRoom(); err != nil { - c.closeWithError(err) + if c.resumeId == "" { + c.resumeId = msg.Hello.ResumeId + if c.reconnecting { + c.session.SendMessage(&ServerMessage{ + Type: "event", + Event: &EventServerMessage{ + Target: "room", + Type: "federation_resumed", + Resumed: makePtr(false), + }, + }) + // Setting the federation client will reset any information on previously + // received "join" events. + c.session.SetFederationClient(c) + } + + if err := c.joinRoom(); err != nil { + c.closeWithError(err) + } + } else { + c.session.SendMessage(&ServerMessage{ + Type: "event", + Event: &EventServerMessage{ + Target: "room", + Type: "federation_resumed", + Resumed: makePtr(true), + }, + }) + + if count := len(c.pendingMessages); count > 0 { + messages := c.pendingMessages + c.pendingMessages = nil + + log.Printf("Sending %d pending messages to %s for %s", count, c.URL(), c.session.PublicId()) + + c.helloMu.Unlock() + defer c.helloMu.Lock() + + c.mu.Lock() + defer c.mu.Unlock() + for _, msg := range messages { + if err := c.sendMessageLocked(msg); err != nil { + log.Printf("Error sending pending message %+v on federation connection to %s: %s", msg, c.URL(), err) + break + } + } + } } } @@ -562,9 +712,23 @@ func (c *FederationClient) SendMessage(message *ClientMessage) error { return c.sendMessageLocked(message) } +func (c *FederationClient) deferMessage(message *ClientMessage) { + c.helloMu.Lock() + defer c.helloMu.Unlock() + if c.resumeId == "" { + return + } + + c.pendingMessages = append(c.pendingMessages, message) + if len(c.pendingMessages) >= warnPendingMessagesCount { + log.Printf("Session %s has %d pending federated messages", c.session.PublicId(), len(c.pendingMessages)) + } +} + func (c *FederationClient) sendMessageLocked(message *ClientMessage) error { if c.conn == nil { - return ErrNotConnected + c.deferMessage(message) + return nil } c.conn.SetWriteDeadline(time.Now().Add(writeWait)) // nolint @@ -586,12 +750,8 @@ func (c *FederationClient) sendMessageLocked(message *ClientMessage) error { } log.Printf("Could not send message %+v for %s to federated client %s: %v", message, c.session.PublicId(), c.URL(), err) - closeData := websocket.FormatCloseMessage(websocket.CloseInternalServerErr, "") - c.conn.SetWriteDeadline(time.Now().Add(writeWait)) // nolint - if err := c.conn.WriteMessage(websocket.CloseMessage, closeData); err != nil { - log.Printf("Could not send close message for %s to federated client %s: %v", c.session.PublicId(), c.URL(), err) - } - return err + c.deferMessage(message) + c.scheduleReconnectLocked() } return nil diff --git a/federation_test.go b/federation_test.go index c5465b2f..ba00fdac 100644 --- a/federation_test.go +++ b/federation_test.go @@ -472,3 +472,251 @@ func Test_FederationMedia(t *testing.T) { UserId: hello2.Hello.UserId, })) } + +func Test_FederationResume(t *testing.T) { + CatchLogForTest(t) + + assert := assert.New(t) + require := require.New(t) + + hub1, hub2, server1, server2 := CreateClusteredHubsForTest(t) + + client1 := NewTestClient(t, server1, hub1) + defer client1.CloseWithBye() + require.NoError(client1.SendHelloV2(testDefaultUserId + "1")) + + client2 := NewTestClient(t, server2, hub2) + defer client2.CloseWithBye() + require.NoError(client2.SendHelloV2(testDefaultUserId + "2")) + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + hello1, err := client1.RunUntilHello(ctx) + require.NoError(err) + + hello2, err := client2.RunUntilHello(ctx) + require.NoError(err) + + roomId := "test-room" + federatedRoomId := roomId + "@federated" + room1, err := client1.JoinRoom(ctx, roomId) + require.NoError(err) + require.Equal(roomId, room1.Room.RoomId) + + assert.NoError(client1.RunUntilJoined(ctx, hello1.Hello)) + + now := time.Now() + token, err := client1.CreateHelloV2Token(testDefaultUserId+"2", now, now.Add(time.Minute)) + require.NoError(err) + + msg := &ClientMessage{ + Id: "join-room-fed", + Type: "room", + Room: &RoomClientMessage{ + RoomId: federatedRoomId, + SessionId: federatedRoomId + "-" + hello2.Hello.SessionId, + Federation: &RoomFederationMessage{ + SignalingUrl: server1.URL, + NextcloudUrl: server1.URL, + RoomId: roomId, + Token: token, + }, + }, + } + require.NoError(client2.WriteJSON(msg)) + + if message, err := client2.RunUntilMessage(ctx); assert.NoError(err) { + assert.Equal(msg.Id, message.Id) + require.Equal("room", message.Type) + require.Equal(federatedRoomId, message.Room.RoomId) + } + + // The client1 will see the remote session id for client2. + var remoteSessionId string + if message, err := client1.RunUntilMessage(ctx); assert.NoError(err) { + assert.NoError(client1.checkSingleMessageJoined(message)) + evt := message.Event.Join[0] + remoteSessionId = evt.SessionId + assert.NotEqual(hello2.Hello.SessionId, remoteSessionId) + assert.Equal(testDefaultUserId+"2", evt.UserId) + } + + // The client2 will see its own session id, not the one from the remote server. + assert.NoError(client2.RunUntilJoined(ctx, hello1.Hello, hello2.Hello)) + + session2 := hub2.GetSessionByPublicId(hello2.Hello.SessionId).(*ClientSession) + fed2 := session2.GetFederationClient() + require.NotNil(fed2) + fed2.mu.Lock() + err = fed2.conn.Close() + + data2 := "from-2-to-1" + assert.NoError(client2.SendMessage(MessageClientMessageRecipient{ + Type: "session", + SessionId: hello1.Hello.SessionId, + }, data2)) + fed2.mu.Unlock() + assert.NoError(err) + + if message, err := client2.RunUntilMessage(ctx); assert.NoError(err) { + assert.Equal("event", message.Type) + assert.Equal("room", message.Event.Target) + assert.Equal("federation_interrupted", message.Event.Type) + } + + if message, err := client2.RunUntilMessage(ctx); assert.NoError(err) { + assert.Equal("event", message.Type) + assert.Equal("room", message.Event.Target) + assert.Equal("federation_resumed", message.Event.Type) + assert.NotNil(message.Event.Resumed) + assert.True(*message.Event.Resumed) + } + + ctx1, cancel1 := context.WithTimeout(context.Background(), 200*time.Millisecond) + defer cancel1() + + var payload string + if assert.NoError(checkReceiveClientMessage(ctx, client1, "session", &HelloServerMessage{ + SessionId: remoteSessionId, + UserId: testDefaultUserId + "2", + }, &payload)) { + assert.Equal(data2, payload) + } + + if message, err := client1.RunUntilMessage(ctx1); err != nil && err != ErrNoMessageReceived && err != context.DeadlineExceeded { + assert.NoError(err) + } else { + assert.Nil(message) + } + + ctx2, cancel2 := context.WithTimeout(context.Background(), 200*time.Millisecond) + defer cancel2() + + if message, err := client2.RunUntilMessage(ctx2); err != nil && err != ErrNoMessageReceived && err != context.DeadlineExceeded { + assert.NoError(err) + } else { + assert.Nil(message) + } +} + +func Test_FederationResumeNewSession(t *testing.T) { + CatchLogForTest(t) + + assert := assert.New(t) + require := require.New(t) + + hub1, hub2, server1, server2 := CreateClusteredHubsForTest(t) + + client1 := NewTestClient(t, server1, hub1) + defer client1.CloseWithBye() + require.NoError(client1.SendHelloV2(testDefaultUserId + "1")) + + client2 := NewTestClient(t, server2, hub2) + defer client2.CloseWithBye() + require.NoError(client2.SendHelloV2(testDefaultUserId + "2")) + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + hello1, err := client1.RunUntilHello(ctx) + require.NoError(err) + + hello2, err := client2.RunUntilHello(ctx) + require.NoError(err) + + roomId := "test-room" + federatedRoomId := roomId + "@federated" + room1, err := client1.JoinRoom(ctx, roomId) + require.NoError(err) + require.Equal(roomId, room1.Room.RoomId) + + assert.NoError(client1.RunUntilJoined(ctx, hello1.Hello)) + + now := time.Now() + token, err := client1.CreateHelloV2Token(testDefaultUserId+"2", now, now.Add(time.Minute)) + require.NoError(err) + + msg := &ClientMessage{ + Id: "join-room-fed", + Type: "room", + Room: &RoomClientMessage{ + RoomId: federatedRoomId, + SessionId: federatedRoomId + "-" + hello2.Hello.SessionId, + Federation: &RoomFederationMessage{ + SignalingUrl: server1.URL, + NextcloudUrl: server1.URL, + RoomId: roomId, + Token: token, + }, + }, + } + require.NoError(client2.WriteJSON(msg)) + + if message, err := client2.RunUntilMessage(ctx); assert.NoError(err) { + assert.Equal(msg.Id, message.Id) + require.Equal("room", message.Type) + require.Equal(federatedRoomId, message.Room.RoomId) + } + + // The client1 will see the remote session id for client2. + var remoteSessionId string + if message, err := client1.RunUntilMessage(ctx); assert.NoError(err) { + assert.NoError(client1.checkSingleMessageJoined(message)) + evt := message.Event.Join[0] + remoteSessionId = evt.SessionId + assert.NotEqual(hello2.Hello.SessionId, remoteSessionId) + assert.Equal(hello2.Hello.UserId, evt.UserId) + } + + // The client2 will see its own session id, not the one from the remote server. + assert.NoError(client2.RunUntilJoined(ctx, hello1.Hello, hello2.Hello)) + + remoteSession2 := hub1.GetSessionByPublicId(remoteSessionId).(*ClientSession) + // Simulate disconnected federated client with an expired session. + if client := remoteSession2.GetClient(); client != nil { + remoteSession2.ClearClient(client) + client.Close() + } + remoteSession2.Close() + + if message, err := client2.RunUntilMessage(ctx); assert.NoError(err) { + assert.Equal("event", message.Type) + assert.Equal("room", message.Event.Target) + assert.Equal("federation_interrupted", message.Event.Type) + } + + if message, err := client2.RunUntilMessage(ctx); assert.NoError(err) { + assert.Equal("event", message.Type) + assert.Equal("room", message.Event.Target) + assert.Equal("federation_resumed", message.Event.Type) + assert.NotNil(message.Event.Resumed) + assert.False(*message.Event.Resumed) + } + + // Client1 will get a "leave" for the expired session and a "join" with the + // new remote session id. + assert.NoError(client1.RunUntilLeft(ctx, &HelloServerMessage{ + SessionId: remoteSessionId, + UserId: hello2.Hello.UserId, + })) + if message, err := client1.RunUntilMessage(ctx); assert.NoError(err) { + assert.NoError(client1.checkSingleMessageJoined(message)) + evt := message.Event.Join[0] + assert.NotEqual(remoteSessionId, evt.SessionId) + assert.NotEqual(hello2.Hello.SessionId, remoteSessionId) + remoteSessionId = evt.SessionId + assert.NotEqual(hello2.Hello.SessionId, remoteSessionId) + assert.Equal(hello2.Hello.UserId, evt.UserId) + } + + // client2 will join the room again after the reconnect with the new + // session and get "joined" events for all sessions in the room (including + // its own). + if message, err := client2.RunUntilMessage(ctx); assert.NoError(err) { + assert.Equal("", message.Id) + require.Equal("room", message.Type) + require.Equal(federatedRoomId, message.Room.RoomId) + } + assert.NoError(client2.RunUntilJoined(ctx, hello1.Hello, hello2.Hello)) +} From fd651d72130d134c0a5fc6de9f4acb05500bf00e Mon Sep 17 00:00:00 2001 From: Joachim Bauch Date: Wed, 24 Jul 2024 12:21:39 +0200 Subject: [PATCH 17/24] Check for capability feature "federation-v2" for federated connections. --- capabilities.go | 3 +++ hub.go | 4 ++++ hub_test.go | 3 +++ 3 files changed, 10 insertions(+) diff --git a/capabilities.go b/capabilities.go index 433b15c4..45b7b546 100644 --- a/capabilities.go +++ b/capabilities.go @@ -43,6 +43,9 @@ const ( // Name of capability to enable the "v3" API for the signaling endpoint. FeatureSignalingV3Api = "signaling-v3" + // Name of capability that is set if the server supports Federation V2. + FeatureFederationV2 = "federation-v2" + // minCapabilitiesCacheDuration specifies the minimum duration to cache // capabilities. // This could overwrite the "max-age" from a "Cache-Control" header. diff --git a/hub.go b/hub.go index d7b1775b..05a7d5d8 100644 --- a/hub.go +++ b/hub.go @@ -1281,6 +1281,10 @@ func (h *Hub) processHelloV2(ctx context.Context, client HandlerClient, message tokenString = message.Hello.Auth.helloV2Params.Token tokenClaims = &HelloV2TokenClaims{} case HelloClientTypeFederation: + if !h.backend.capabilities.HasCapabilityFeature(ctx, url, FeatureFederationV2) { + return nil, nil, ErrFederationNotSupported + } + tokenString = message.Hello.Auth.federationParams.Token tokenClaims = &FederationTokenClaims{} default: diff --git a/hub_test.go b/hub_test.go index 2f75e79a..3cf8ea21 100644 --- a/hub_test.go +++ b/hub_test.go @@ -699,6 +699,9 @@ func registerBackendHandlerUrl(t *testing.T, router *mux.Router, url string) { if strings.Contains(t.Name(), "V3Api") { features = append(features, "signaling-v3") } + if strings.Contains(t.Name(), "Federation") { + features = append(features, "federation-v2") + } signaling := map[string]interface{}{ "foo": "bar", "baz": 42, From 86721347f9af23fa36602ee2504447b741f6dcb9 Mon Sep 17 00:00:00 2001 From: Joachim Bauch Date: Thu, 25 Jul 2024 08:24:20 +0200 Subject: [PATCH 18/24] Include "federated" in "join" events for fed. sessions. --- api_signaling.go | 2 ++ api_signaling_easyjson.go | 7 +++++++ docs/standalone-signaling-api-v1.md | 4 ++++ federation_test.go | 7 +++++++ room.go | 2 ++ 5 files changed, 22 insertions(+) diff --git a/api_signaling.go b/api_signaling.go index 5434e500..2edb10aa 100644 --- a/api_signaling.go +++ b/api_signaling.go @@ -1053,6 +1053,7 @@ type EventServerMessageSessionEntry struct { UserId string `json:"userid"` User json.RawMessage `json:"user,omitempty"` RoomSessionId string `json:"roomsessionid,omitempty"` + Federated bool `json:"federated,omitempty"` } func (e *EventServerMessageSessionEntry) Clone() *EventServerMessageSessionEntry { @@ -1061,6 +1062,7 @@ func (e *EventServerMessageSessionEntry) Clone() *EventServerMessageSessionEntry UserId: e.UserId, User: e.User, RoomSessionId: e.RoomSessionId, + Federated: e.Federated, } } diff --git a/api_signaling_easyjson.go b/api_signaling_easyjson.go index b8264bbe..773039fc 100644 --- a/api_signaling_easyjson.go +++ b/api_signaling_easyjson.go @@ -3951,6 +3951,8 @@ func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling35(in *jle } case "roomsessionid": out.RoomSessionId = string(in.String()) + case "federated": + out.Federated = bool(in.Bool()) default: in.SkipRecursive() } @@ -3985,6 +3987,11 @@ func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling35(out *jw out.RawString(prefix) out.String(string(in.RoomSessionId)) } + if in.Federated { + const prefix string = ",\"federated\":" + out.RawString(prefix) + out.Bool(bool(in.Federated)) + } out.RawByte('}') } diff --git a/docs/standalone-signaling-api-v1.md b/docs/standalone-signaling-api-v1.md index 2fe9b49d..6646bdc6 100644 --- a/docs/standalone-signaling-api-v1.md +++ b/docs/standalone-signaling-api-v1.md @@ -650,6 +650,10 @@ Room event session object: } } +If a session is federated, an additional entry `"federated": true` will be +available. + + Message format (Server -> Client, user(s) left): { diff --git a/federation_test.go b/federation_test.go index ba00fdac..c615e3b5 100644 --- a/federation_test.go +++ b/federation_test.go @@ -137,6 +137,7 @@ func Test_Federation(t *testing.T) { remoteSessionId = evt.SessionId assert.NotEqual(hello2.Hello.SessionId, remoteSessionId) assert.Equal(testDefaultUserId+"2", evt.UserId) + assert.True(evt.Federated) } // The client2 will see its own session id, not the one from the remote server. @@ -183,6 +184,7 @@ func Test_Federation(t *testing.T) { remoteSessionId = evt.SessionId assert.NotEqual(hello2.Hello.SessionId, remoteSessionId) assert.Equal(testDefaultUserId+"2", evt.UserId) + assert.True(evt.Federated) } assert.NoError(client2.RunUntilJoined(ctx, hello1.Hello, hello2.Hello)) @@ -347,6 +349,7 @@ func Test_Federation(t *testing.T) { remoteSessionId4 = evt.SessionId assert.NotEqual(hello4.Hello.SessionId, remoteSessionId) assert.Equal(testDefaultUserId+"4", evt.UserId) + assert.True(evt.Federated) } assert.NoError(client2.RunUntilJoined(ctx, &HelloServerMessage{ @@ -449,6 +452,7 @@ func Test_FederationMedia(t *testing.T) { remoteSessionId = evt.SessionId assert.NotEqual(hello2.Hello.SessionId, remoteSessionId) assert.Equal(testDefaultUserId+"2", evt.UserId) + assert.True(evt.Federated) } // The client2 will see its own session id, not the one from the remote server. @@ -540,6 +544,7 @@ func Test_FederationResume(t *testing.T) { remoteSessionId = evt.SessionId assert.NotEqual(hello2.Hello.SessionId, remoteSessionId) assert.Equal(testDefaultUserId+"2", evt.UserId) + assert.True(evt.Federated) } // The client2 will see its own session id, not the one from the remote server. @@ -667,6 +672,7 @@ func Test_FederationResumeNewSession(t *testing.T) { remoteSessionId = evt.SessionId assert.NotEqual(hello2.Hello.SessionId, remoteSessionId) assert.Equal(hello2.Hello.UserId, evt.UserId) + assert.True(evt.Federated) } // The client2 will see its own session id, not the one from the remote server. @@ -708,6 +714,7 @@ func Test_FederationResumeNewSession(t *testing.T) { remoteSessionId = evt.SessionId assert.NotEqual(hello2.Hello.SessionId, remoteSessionId) assert.Equal(hello2.Hello.UserId, evt.UserId) + assert.True(evt.Federated) } // client2 will join the room again after the reconnect with the new diff --git a/room.go b/room.go index 39ce6bdc..2ef1f890 100644 --- a/room.go +++ b/room.go @@ -367,6 +367,7 @@ func (r *Room) notifySessionJoined(sessionId string) { } if s, ok := s.(*ClientSession); ok { entry.RoomSessionId = s.RoomSessionId() + entry.Federated = s.ClientType() == HelloClientTypeFederation } events = append(events, entry) } @@ -535,6 +536,7 @@ func (r *Room) PublishSessionJoined(session Session, sessionData *RoomSessionDat } if session, ok := session.(*ClientSession); ok { message.Event.Join[0].RoomSessionId = session.RoomSessionId() + message.Event.Join[0].Federated = session.ClientType() == HelloClientTypeFederation } if err := r.publish(message); err != nil { log.Printf("Could not publish session joined message in room %s: %s", r.Id(), err) From 2f76deab36ce5594ef608f1bafae6952ab381643 Mon Sep 17 00:00:00 2001 From: Joachim Bauch Date: Thu, 25 Jul 2024 09:25:13 +0200 Subject: [PATCH 19/24] Detect duplicate join requests for federated clients. --- federation.go | 35 ++++++++++++--- federation_test.go | 107 +++++++++++++++++++++++++++++++++++++++++++++ hub.go | 15 +++++++ 3 files changed, 152 insertions(+), 5 deletions(-) diff --git a/federation.go b/federation.go index 3625dfd1..96f5b962 100644 --- a/federation.go +++ b/federation.go @@ -53,11 +53,12 @@ type FederationClient struct { session *ClientSession message atomic.Pointer[ClientMessage] - roomId string - remoteRoomId string - changeRoomId bool - roomSessionId string - federation *RoomFederationMessage + roomId string + remoteRoomId string + changeRoomId bool + roomSessionId string + roomProperties atomic.Pointer[json.RawMessage] + federation *RoomFederationMessage mu sync.Mutex dialer *websocket.Dialer @@ -142,6 +143,25 @@ func (c *FederationClient) URL() string { return c.federation.parsedSignalingUrl.String() } +func (c *FederationClient) IsSameRoom(room *RoomClientMessage) (string, json.RawMessage, bool) { + federation := room.Federation + remoteRoomId := federation.RoomId + if remoteRoomId == "" { + remoteRoomId = room.RoomId + } + + if c.remoteRoomId != remoteRoomId || c.federation.NextcloudUrl != federation.NextcloudUrl { + return "", nil, false + } + + var properties json.RawMessage + if roomProperties := c.roomProperties.Load(); roomProperties != nil { + properties = *roomProperties + } + + return room.RoomId, properties, true +} + func (c *FederationClient) connect(ctx context.Context) error { log.Printf("Creating federation connection to %s for %s", c.URL(), c.session.PublicId()) conn, response, err := c.dialer.DialContext(ctx, c.url, nil) @@ -663,6 +683,11 @@ func (c *FederationClient) processMessage(msg *ServerMessage) { } else if c.changeRoomId && msg.Room.RoomId == c.remoteRoomId { msg.Room.RoomId = c.roomId } + if len(msg.Room.Properties) > 0 { + c.roomProperties.Store(&msg.Room.Properties) + } else { + c.roomProperties.Store(nil) + } case "message": c.updateSessionRecipient(msg.Message.Recipient, localSessionId, remoteSessionId) c.updateSessionSender(msg.Message.Sender, localSessionId, remoteSessionId) diff --git a/federation_test.go b/federation_test.go index c615e3b5..81e8496f 100644 --- a/federation_test.go +++ b/federation_test.go @@ -23,6 +23,7 @@ package signaling import ( "context" + "encoding/json" "testing" "time" @@ -373,6 +374,112 @@ func Test_Federation(t *testing.T) { } } +func Test_FederationJoinRoomTwice(t *testing.T) { + CatchLogForTest(t) + + assert := assert.New(t) + require := require.New(t) + + hub1, hub2, server1, server2 := CreateClusteredHubsForTest(t) + + client1 := NewTestClient(t, server1, hub1) + defer client1.CloseWithBye() + require.NoError(client1.SendHelloV2(testDefaultUserId + "1")) + + client2 := NewTestClient(t, server2, hub2) + defer client2.CloseWithBye() + require.NoError(client2.SendHelloV2(testDefaultUserId + "2")) + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + hello1, err := client1.RunUntilHello(ctx) + require.NoError(err) + + hello2, err := client2.RunUntilHello(ctx) + require.NoError(err) + + roomId := "test-room" + federatedRoomId := roomId + "@federated" + room1, err := client1.JoinRoom(ctx, roomId) + require.NoError(err) + require.Equal(roomId, room1.Room.RoomId) + + assert.NoError(client1.RunUntilJoined(ctx, hello1.Hello)) + + now := time.Now() + token, err := client1.CreateHelloV2Token(testDefaultUserId+"2", now, now.Add(time.Minute)) + require.NoError(err) + + msg := &ClientMessage{ + Id: "join-room-fed", + Type: "room", + Room: &RoomClientMessage{ + RoomId: federatedRoomId, + SessionId: federatedRoomId + "-" + hello2.Hello.SessionId, + Federation: &RoomFederationMessage{ + SignalingUrl: server1.URL, + NextcloudUrl: server1.URL, + RoomId: roomId, + Token: token, + }, + }, + } + require.NoError(client2.WriteJSON(msg)) + + if message, err := client2.RunUntilMessage(ctx); assert.NoError(err) { + assert.Equal(msg.Id, message.Id) + require.Equal("room", message.Type) + require.Equal(federatedRoomId, message.Room.RoomId) + } + + // The client1 will see the remote session id for client2. + var remoteSessionId string + if message, err := client1.RunUntilMessage(ctx); assert.NoError(err) { + assert.NoError(client1.checkSingleMessageJoined(message)) + evt := message.Event.Join[0] + remoteSessionId = evt.SessionId + assert.NotEqual(hello2.Hello.SessionId, remoteSessionId) + assert.Equal(hello2.Hello.UserId, evt.UserId) + assert.True(evt.Federated) + } + + // The client2 will see its own session id, not the one from the remote server. + assert.NoError(client2.RunUntilJoined(ctx, hello1.Hello, hello2.Hello)) + + msg2 := &ClientMessage{ + Id: "join-room-fed-2", + Type: "room", + Room: &RoomClientMessage{ + RoomId: federatedRoomId, + SessionId: federatedRoomId + "-" + hello2.Hello.SessionId, + Federation: &RoomFederationMessage{ + SignalingUrl: server1.URL, + NextcloudUrl: server1.URL, + RoomId: roomId, + Token: token, + }, + }, + } + require.NoError(client2.WriteJSON(msg2)) + + if message, err := client2.RunUntilMessage(ctx); assert.NoError(err) { + assert.Equal(msg2.Id, message.Id) + if assert.Equal("error", message.Type) { + assert.Equal("already_joined", message.Error.Code) + } + if assert.NotNil(message.Error.Details) { + var roomMsg RoomErrorDetails + if assert.NoError(json.Unmarshal(message.Error.Details, &roomMsg)) { + if assert.NotNil(roomMsg.Room) { + assert.Equal(federatedRoomId, roomMsg.Room.RoomId) + assert.Equal(string(testRoomProperties), string(roomMsg.Room.Properties)) + } + } + } + } +} + func Test_FederationMedia(t *testing.T) { CatchLogForTest(t) diff --git a/hub.go b/hub.go index 05a7d5d8..693804e0 100644 --- a/hub.go +++ b/hub.go @@ -1559,6 +1559,21 @@ func (h *Hub) processRoom(sess Session, message *ClientMessage) { delete(h.anonymousSessions, session) h.mu.Unlock() + if client := session.GetFederationClient(); client != nil { + if remoteRoomId, properties, found := client.IsSameRoom(message.Room); found { + // TODO: Do we need to update the remote room session id? + session.SendMessage(message.NewErrorServerMessage( + NewErrorDetail("already_joined", "Already joined this room.", &RoomErrorDetails{ + Room: &RoomServerMessage{ + RoomId: remoteRoomId, + Properties: properties, + }, + }), + )) + return + } + } + ctx, cancel := context.WithTimeout(session.Context(), h.federationTimeout) defer cancel() From f4b6f236da2a218097e3d54c09263d2b6d846b1a Mon Sep 17 00:00:00 2001 From: Joachim Bauch Date: Thu, 25 Jul 2024 11:12:40 +0200 Subject: [PATCH 20/24] Support switching federated rooms while reusing internal connections. --- federation.go | 159 ++++++++++++++++++++++++++++----------------- federation_test.go | 108 ++++++++++++++++++++++++++++++ hub.go | 35 +++++----- 3 files changed, 224 insertions(+), 78 deletions(-) diff --git a/federation.go b/federation.go index 96f5b962..0c137533 100644 --- a/federation.go +++ b/federation.go @@ -53,12 +53,10 @@ type FederationClient struct { session *ClientSession message atomic.Pointer[ClientMessage] - roomId string - remoteRoomId string - changeRoomId bool - roomSessionId string - roomProperties atomic.Pointer[json.RawMessage] - federation *RoomFederationMessage + roomId atomic.Value + remoteRoomId atomic.Value + changeRoomId atomic.Bool + federation atomic.Pointer[RoomFederationMessage] mu sync.Mutex dialer *websocket.Dialer @@ -111,18 +109,16 @@ func NewFederationClient(ctx context.Context, hub *Hub, session *ClientSession, hub: hub, session: session, - roomId: room.RoomId, - remoteRoomId: remoteRoomId, - changeRoomId: room.RoomId != remoteRoomId, - roomSessionId: room.SessionId, - federation: room.Federation, - reconnectDelay: initialFederationReconnectInterval, dialer: &dialer, url: url, closer: NewCloser(), } + result.roomId.Store(room.RoomId) + result.remoteRoomId.Store(remoteRoomId) + result.changeRoomId.Store(room.RoomId != remoteRoomId) + result.federation.Store(room.Federation) result.message.Store(message) if err := result.connect(ctx); err != nil { @@ -140,26 +136,13 @@ func NewFederationClient(ctx context.Context, hub *Hub, session *ClientSession, } func (c *FederationClient) URL() string { - return c.federation.parsedSignalingUrl.String() + return c.federation.Load().parsedSignalingUrl.String() } -func (c *FederationClient) IsSameRoom(room *RoomClientMessage) (string, json.RawMessage, bool) { - federation := room.Federation - remoteRoomId := federation.RoomId - if remoteRoomId == "" { - remoteRoomId = room.RoomId - } - - if c.remoteRoomId != remoteRoomId || c.federation.NextcloudUrl != federation.NextcloudUrl { - return "", nil, false - } - - var properties json.RawMessage - if roomProperties := c.roomProperties.Load(); roomProperties != nil { - properties = *roomProperties - } - - return room.RoomId, properties, true +func (c *FederationClient) CanReuse(federation *RoomFederationMessage) bool { + fed := c.federation.Load() + return fed.NextcloudUrl == federation.NextcloudUrl && + fed.SignalingUrl == federation.SignalingUrl } func (c *FederationClient) connect(ctx context.Context) error { @@ -208,6 +191,17 @@ func (c *FederationClient) connect(ctx context.Context) error { return nil } +func (c *FederationClient) ChangeRoom(message *ClientMessage) error { + if message.Room == nil || message.Room.Federation == nil { + return fmt.Errorf("expected federation room message, got %+v", message) + } else if !c.CanReuse(message.Room.Federation) { + return fmt.Errorf("can't reuse federation client to join room in %+v", message) + } + + c.message.Swap(message) + return c.joinRoom() +} + func (c *FederationClient) Leave(message *ClientMessage) error { c.mu.Lock() defer c.mu.Unlock() @@ -442,7 +436,7 @@ func (c *FederationClient) sendHelloLocked(auth *FederationAuthParams) error { } else { msg.Hello.Auth = &HelloClientMessageAuth{ Type: HelloClientTypeFederation, - Url: c.federation.NextcloudUrl, + Url: c.federation.Load().NextcloudUrl, Params: authData, } } @@ -456,7 +450,7 @@ func (c *FederationClient) processWelcome(msg *ServerMessage) { } federationParams := &FederationAuthParams{ - Token: c.federation.Token, + Token: c.federation.Load().Token, } if err := c.sendHello(federationParams); err != nil { log.Printf("Error sending hello message to %s for %s: %s", c.URL(), c.session.PublicId(), err) @@ -554,16 +548,24 @@ func (c *FederationClient) processHello(msg *ServerMessage) { } func (c *FederationClient) joinRoom() error { - var id string - if message := c.message.Swap(nil); message != nil { - id = message.Id + message := c.message.Load() + if message == nil { + // Should not happen as the connection has been closed with an error already. + return ErrNotConnected + } + + room := message.Room + remoteRoomId := room.Federation.RoomId + if remoteRoomId == "" { + remoteRoomId = room.RoomId } + return c.SendMessage(&ClientMessage{ - Id: id, + Id: message.Id, Type: "room", Room: &RoomClientMessage{ - RoomId: c.remoteRoomId, - SessionId: c.roomSessionId, + RoomId: remoteRoomId, + SessionId: room.SessionId, }, }) } @@ -604,6 +606,9 @@ func (c *FederationClient) processMessage(msg *ServerMessage) { remoteSessionId = hello.SessionId } + remoteRoomId := c.remoteRoomId.Load().(string) + roomId := c.roomId.Load().(string) + var doClose bool switch msg.Type { case "control": @@ -614,23 +619,23 @@ func (c *FederationClient) processMessage(msg *ServerMessage) { case "participants": switch msg.Event.Type { case "update": - if c.changeRoomId && msg.Event.Update.RoomId == c.remoteRoomId { - msg.Event.Update.RoomId = c.roomId + if c.changeRoomId.Load() && msg.Event.Update.RoomId == remoteRoomId { + msg.Event.Update.RoomId = roomId } if remoteSessionId != "" { c.updateEventUsers(msg.Event.Update.Changed, localSessionId, remoteSessionId) c.updateEventUsers(msg.Event.Update.Users, localSessionId, remoteSessionId) } case "flags": - if c.changeRoomId && msg.Event.Flags.RoomId == c.remoteRoomId { - msg.Event.Flags.RoomId = c.roomId + if c.changeRoomId.Load() && msg.Event.Flags.RoomId == remoteRoomId { + msg.Event.Flags.RoomId = roomId } if remoteSessionId != "" && msg.Event.Flags.SessionId == remoteSessionId { msg.Event.Flags.SessionId = localSessionId } case "message": - if c.changeRoomId && msg.Event.Message.RoomId == c.remoteRoomId { - msg.Event.Message.RoomId = c.roomId + if c.changeRoomId.Load() && msg.Event.Message.RoomId == remoteRoomId { + msg.Event.Message.RoomId = roomId } } case "room": @@ -657,36 +662,66 @@ func (c *FederationClient) processMessage(msg *ServerMessage) { } } case "message": - if c.changeRoomId && msg.Event.Message.RoomId == c.remoteRoomId { - msg.Event.Message.RoomId = c.roomId + if c.changeRoomId.Load() && msg.Event.Message.RoomId == remoteRoomId { + msg.Event.Message.RoomId = roomId } } case "roomlist": switch msg.Event.Type { case "invite": - if c.changeRoomId && msg.Event.Invite.RoomId == c.remoteRoomId { - msg.Event.Invite.RoomId = c.roomId + if c.changeRoomId.Load() && msg.Event.Invite.RoomId == remoteRoomId { + msg.Event.Invite.RoomId = roomId } case "disinvite": - if c.changeRoomId && msg.Event.Disinvite.RoomId == c.remoteRoomId { - msg.Event.Disinvite.RoomId = c.roomId + if c.changeRoomId.Load() && msg.Event.Disinvite.RoomId == remoteRoomId { + msg.Event.Disinvite.RoomId = roomId } case "update": - if c.changeRoomId && msg.Event.Update.RoomId == c.remoteRoomId { - msg.Event.Update.RoomId = c.roomId + if c.changeRoomId.Load() && msg.Event.Update.RoomId == remoteRoomId { + msg.Event.Update.RoomId = roomId + } + } + } + case "error": + if c.changeRoomId.Load() && msg.Error.Code == "already_joined" { + if len(msg.Error.Details) > 0 { + var details RoomErrorDetails + if err := json.Unmarshal(msg.Error.Details, &details); err == nil && details.Room != nil { + if details.Room.RoomId == remoteRoomId { + details.Room.RoomId = roomId + if data, err := json.Marshal(details); err == nil { + msg.Error.Details = data + } + } } } } case "room": + if message := c.message.Load(); message != nil { + if msg.Id != "" && message.Id == msg.Id { + // Got response to initial join request, clear id so future join + // requests will not be mapped to any client callbacks. + message.Id = "" + c.message.Store(message) + } + + room := message.Room + roomId = room.RoomId + remoteRoomId = room.Federation.RoomId + if remoteRoomId == "" { + remoteRoomId = room.RoomId + } + + c.roomId.Store(room.RoomId) + c.remoteRoomId.Store(remoteRoomId) + c.changeRoomId.Store(room.RoomId != remoteRoomId) + c.federation.Store(room.Federation) + } + if msg.Room.RoomId == "" && c.closeOnLeave.Load() { doClose = true - } else if c.changeRoomId && msg.Room.RoomId == c.remoteRoomId { - msg.Room.RoomId = c.roomId - } - if len(msg.Room.Properties) > 0 { - c.roomProperties.Store(&msg.Room.Properties) - } else { - c.roomProperties.Store(nil) + } else if c.changeRoomId.Load() && msg.Room.RoomId == remoteRoomId { + msg.Room.RoomId = roomId } case "message": c.updateSessionRecipient(msg.Message.Recipient, localSessionId, remoteSessionId) @@ -752,7 +787,11 @@ func (c *FederationClient) deferMessage(message *ClientMessage) { func (c *FederationClient) sendMessageLocked(message *ClientMessage) error { if c.conn == nil { - c.deferMessage(message) + if message.Type != "room" { + // Join requests will be automatically sent after the hello response has + // been received. + c.deferMessage(message) + } return nil } diff --git a/federation_test.go b/federation_test.go index 81e8496f..859a835a 100644 --- a/federation_test.go +++ b/federation_test.go @@ -480,6 +480,114 @@ func Test_FederationJoinRoomTwice(t *testing.T) { } } +func Test_FederationChangeRoom(t *testing.T) { + CatchLogForTest(t) + + assert := assert.New(t) + require := require.New(t) + + hub1, hub2, server1, server2 := CreateClusteredHubsForTest(t) + + client1 := NewTestClient(t, server1, hub1) + defer client1.CloseWithBye() + require.NoError(client1.SendHelloV2(testDefaultUserId + "1")) + + client2 := NewTestClient(t, server2, hub2) + defer client2.CloseWithBye() + require.NoError(client2.SendHelloV2(testDefaultUserId + "2")) + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + hello1, err := client1.RunUntilHello(ctx) + require.NoError(err) + + hello2, err := client2.RunUntilHello(ctx) + require.NoError(err) + + roomId := "test-room" + federatedRoomId := roomId + "@federated" + room1, err := client1.JoinRoom(ctx, roomId) + require.NoError(err) + require.Equal(roomId, room1.Room.RoomId) + + assert.NoError(client1.RunUntilJoined(ctx, hello1.Hello)) + + now := time.Now() + token, err := client1.CreateHelloV2Token(testDefaultUserId+"2", now, now.Add(time.Minute)) + require.NoError(err) + + msg := &ClientMessage{ + Id: "join-room-fed", + Type: "room", + Room: &RoomClientMessage{ + RoomId: federatedRoomId, + SessionId: federatedRoomId + "-" + hello2.Hello.SessionId, + Federation: &RoomFederationMessage{ + SignalingUrl: server1.URL, + NextcloudUrl: server1.URL, + RoomId: roomId, + Token: token, + }, + }, + } + require.NoError(client2.WriteJSON(msg)) + + if message, err := client2.RunUntilMessage(ctx); assert.NoError(err) { + assert.Equal(msg.Id, message.Id) + require.Equal("room", message.Type) + require.Equal(federatedRoomId, message.Room.RoomId) + } + + session2 := hub2.GetSessionByPublicId(hello2.Hello.SessionId).(*ClientSession) + fed := session2.GetFederationClient() + require.NotNil(fed) + localAddr := fed.conn.LocalAddr() + + // The client1 will see the remote session id for client2. + var remoteSessionId string + if message, err := client1.RunUntilMessage(ctx); assert.NoError(err) { + assert.NoError(client1.checkSingleMessageJoined(message)) + evt := message.Event.Join[0] + remoteSessionId = evt.SessionId + assert.NotEqual(hello2.Hello.SessionId, remoteSessionId) + assert.Equal(hello2.Hello.UserId, evt.UserId) + assert.True(evt.Federated) + } + + // The client2 will see its own session id, not the one from the remote server. + assert.NoError(client2.RunUntilJoined(ctx, hello1.Hello, hello2.Hello)) + + roomId2 := roomId + "-2" + federatedRoomId2 := roomId2 + "@federated" + msg2 := &ClientMessage{ + Id: "join-room-fed-2", + Type: "room", + Room: &RoomClientMessage{ + RoomId: federatedRoomId2, + SessionId: federatedRoomId2 + "-" + hello2.Hello.SessionId, + Federation: &RoomFederationMessage{ + SignalingUrl: server1.URL, + NextcloudUrl: server1.URL, + RoomId: roomId2, + Token: token, + }, + }, + } + require.NoError(client2.WriteJSON(msg2)) + + if message, err := client2.RunUntilMessage(ctx); assert.NoError(err) { + assert.Equal(msg2.Id, message.Id) + require.Equal("room", message.Type) + require.Equal(federatedRoomId2, message.Room.RoomId) + } + + fed2 := session2.GetFederationClient() + require.NotNil(fed2) + localAddr2 := fed2.conn.LocalAddr() + assert.Equal(localAddr, localAddr2) +} + func Test_FederationMedia(t *testing.T) { CatchLogForTest(t) diff --git a/hub.go b/hub.go index 693804e0..a2ec81e1 100644 --- a/hub.go +++ b/hub.go @@ -1559,28 +1559,27 @@ func (h *Hub) processRoom(sess Session, message *ClientMessage) { delete(h.anonymousSessions, session) h.mu.Unlock() - if client := session.GetFederationClient(); client != nil { - if remoteRoomId, properties, found := client.IsSameRoom(message.Room); found { - // TODO: Do we need to update the remote room session id? - session.SendMessage(message.NewErrorServerMessage( - NewErrorDetail("already_joined", "Already joined this room.", &RoomErrorDetails{ - Room: &RoomServerMessage{ - RoomId: remoteRoomId, - Properties: properties, - }, - }), - )) - return - } - } - ctx, cancel := context.WithTimeout(session.Context(), h.federationTimeout) defer cancel() - // TODO: Handle case where session already is in a federated room on the same server. - client, err := NewFederationClient(ctx, h, session, message) + client := session.GetFederationClient() + var err error + if client != nil { + if client.CanReuse(federation) { + err = client.ChangeRoom(message) + if errors.Is(err, ErrNotConnected) { + client = nil + } + } else { + client = nil + } + } + if client == nil { + client, err = NewFederationClient(ctx, h, session, message) + } + if err != nil { - if session.UserId() == "" { + if session.UserId() == "" && client == nil { h.startWaitAnonymousSessionRoom(session) } var ae *Error From 347abb2c7f085ff55c51b278e36ecc22d8127ccb Mon Sep 17 00:00:00 2001 From: Joachim Bauch Date: Thu, 25 Jul 2024 11:36:41 +0200 Subject: [PATCH 21/24] Ignore already closed connections when closing websocket. --- federation.go | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/federation.go b/federation.go index 0c137533..aca2020c 100644 --- a/federation.go +++ b/federation.go @@ -48,6 +48,13 @@ var ( ErrFederationNotSupported = NewError("federation_unsupported", "The target server does not support federation.") ) +func isClosedError(err error) bool { + return errors.Is(err, net.ErrClosed) || + errors.Is(err, websocket.ErrCloseSent) || + // Gorilla websocket hides the original net.Error, so also compare error messages + strings.Contains(err.Error(), net.ErrClosed.Error()) +} + type FederationClient struct { hub *Hub session *ClientSession @@ -240,18 +247,18 @@ func (c *FederationClient) closeConnection(withBye bool) { if withBye { if err := c.sendMessageLocked(&ClientMessage{ Type: "bye", - }); err != nil && !errors.Is(err, websocket.ErrCloseSent) { + }); err != nil && !isClosedError(err) { log.Printf("Error sending bye on federation connection to %s: %s", c.URL(), err) } } closeMessage := websocket.FormatCloseMessage(websocket.CloseNormalClosure, "") deadline := time.Now().Add(writeWait) - if err := c.conn.WriteControl(websocket.CloseMessage, closeMessage, deadline); err != nil && !errors.Is(err, websocket.ErrCloseSent) { + if err := c.conn.WriteControl(websocket.CloseMessage, closeMessage, deadline); err != nil && !isClosedError(err) { log.Printf("Error sending close message on federation connection to %s: %s", c.URL(), err) } - if err := c.conn.Close(); err != nil { + if err := c.conn.Close(); err != nil && !isClosedError(err) { log.Printf("Error closing federation connection to %s: %s", c.URL(), err) } @@ -321,8 +328,7 @@ func (c *FederationClient) readPump(conn *websocket.Conn) { conn.SetReadDeadline(time.Now().Add(pongWait)) // nolint msgType, data, err := conn.ReadMessage() if err != nil { - // Gorilla websocket hides the original net.Error, so also compare error messages - if c.closer.IsClosed() && (errors.Is(err, net.ErrClosed) || errors.Is(err, websocket.ErrCloseSent) || strings.Contains(err.Error(), net.ErrClosed.Error())) { + if c.closer.IsClosed() && isClosedError(err) { // Connection closed locally, no need to reconnect. break } From b8b6f37765f154fa1a67e209c526b7e4d995349a Mon Sep 17 00:00:00 2001 From: Joachim Bauch Date: Wed, 31 Jul 2024 08:42:40 +0200 Subject: [PATCH 22/24] Use different NATS servers for federation tests. Otherwise it would be possible to send events to remote sessions (through NATS) even if they are connected to a separate signaling server which has it's own NATS server in reality. --- hub_test.go | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/hub_test.go b/hub_test.go index 3cf8ea21..c8c7b37b 100644 --- a/hub_test.go +++ b/hub_test.go @@ -196,11 +196,17 @@ func CreateClusteredHubsForTestWithConfig(t *testing.T, getConfigFunc func(*http server2.Close() }) - nats := startLocalNatsServer(t) + nats1 := startLocalNatsServer(t) + var nats2 string + if strings.Contains(t.Name(), "Federation") { + nats2 = startLocalNatsServer(t) + } else { + nats2 = nats1 + } grpcServer1, addr1 := NewGrpcServerForTest(t) grpcServer2, addr2 := NewGrpcServerForTest(t) - events1, err := NewAsyncEvents(nats) + events1, err := NewAsyncEvents(nats1) if err != nil { t.Fatal(err) } @@ -220,7 +226,7 @@ func CreateClusteredHubsForTestWithConfig(t *testing.T, getConfigFunc func(*http if err != nil { t.Fatal(err) } - events2, err := NewAsyncEvents(nats) + events2, err := NewAsyncEvents(nats2) if err != nil { t.Fatal(err) } From c83c42c8ff6f758449bed43c9082d600bff00b50 Mon Sep 17 00:00:00 2001 From: Joachim Bauch Date: Wed, 31 Jul 2024 08:44:34 +0200 Subject: [PATCH 23/24] Add special handling for "forceMute" control event. --- federation.go | 14 ++++++++++++++ federation_test.go | 17 +++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/federation.go b/federation.go index aca2020c..ca012134 100644 --- a/federation.go +++ b/federation.go @@ -620,6 +620,20 @@ func (c *FederationClient) processMessage(msg *ServerMessage) { case "control": c.updateSessionRecipient(msg.Control.Recipient, localSessionId, remoteSessionId) c.updateSessionSender(msg.Control.Sender, localSessionId, remoteSessionId) + // Special handling for "forceMute" event. + if len(msg.Control.Data) > 0 && msg.Control.Data[0] == '{' { + var data map[string]interface{} + if err := json.Unmarshal(msg.Control.Data, &data); err == nil { + if action, found := data["action"]; found && action == "forceMute" { + if peerId, found := data["peerId"]; found && peerId == remoteSessionId { + data["peerId"] = localSessionId + if d, err := json.Marshal(data); err == nil { + msg.Control.Data = d + } + } + } + } + } case "event": switch msg.Event.Target { case "participants": diff --git a/federation_test.go b/federation_test.go index 859a835a..0e31f155 100644 --- a/federation_test.go +++ b/federation_test.go @@ -238,6 +238,23 @@ func Test_Federation(t *testing.T) { } } + // Special handling for the "forceMute" control event. + forceMute := map[string]any{ + "action": "forceMute", + "peerId": remoteSessionId, + } + if assert.NoError(client1.SendControl(MessageClientMessageRecipient{ + Type: "session", + SessionId: remoteSessionId, + }, forceMute)) { + var payload map[string]any + if assert.NoError(checkReceiveClientControl(ctx, client2, "session", hello1.Hello, &payload)) { + // The sessionId in "peerId" will be replaced with the local one. + forceMute["peerId"] = hello2.Hello.SessionId + assert.Equal(forceMute, payload) + } + } + data3 := "from-2-to-2" // Clients can't send to their own (local) session id. if assert.NoError(client2.SendMessage(MessageClientMessageRecipient{ From 11a1f365d9a6b842ed891bb7ab7bd7e5dbd712d9 Mon Sep 17 00:00:00 2001 From: Joachim Bauch Date: Mon, 5 Aug 2024 08:38:30 +0200 Subject: [PATCH 24/24] Convert actorId / actorType in participants updates for federated users. See https://github.com/nextcloud/spreed/pull/12863 for details. --- api_signaling.go | 14 ++++++++++++++ federation.go | 47 ++++++++++++++++++++++++++++++++++++++-------- federation_test.go | 35 ++++++++++++++++++++++++++++++++-- 3 files changed, 86 insertions(+), 10 deletions(-) diff --git a/api_signaling.go b/api_signaling.go index 2edb10aa..84851f73 100644 --- a/api_signaling.go +++ b/api_signaling.go @@ -41,6 +41,9 @@ const ( // Version 2.0 validates auth params encoded as JWT. HelloVersionV2 = "2.0" + + ActorTypeUsers = "users" + ActorTypeFederatedUsers = "federated_users" ) var ( @@ -52,6 +55,17 @@ func makePtr[T any](v T) *T { return &v } +func getStringMapEntry[T any](m map[string]interface{}, key string) (s T, ok bool) { + var defaultValue T + v, found := m[key] + if !found { + return defaultValue, false + } + + s, ok = v.(T) + return +} + // ClientMessage is a message that is sent from a client to the server. type ClientMessage struct { json.Marshaler diff --git a/federation.go b/federation.go index ca012134..b93dacc3 100644 --- a/federation.go +++ b/federation.go @@ -55,6 +55,18 @@ func isClosedError(err error) bool { strings.Contains(err.Error(), net.ErrClosed.Error()) } +func getCloudUrl(s string) string { + if strings.HasPrefix(s, "https://") { + s = s[8:] + } else { + s = strings.TrimPrefix(s, "http://") + } + if pos := strings.Index(s, "/ocs/v"); pos != -1 { + s = s[:pos] + } + return s +} + type FederationClient struct { hub *Hub session *ClientSession @@ -577,17 +589,36 @@ func (c *FederationClient) joinRoom() error { } func (c *FederationClient) updateEventUsers(users []map[string]interface{}, localSessionId string, remoteSessionId string) { + localCloudUrl := "@" + getCloudUrl(c.session.BackendUrl()) + localCloudUrlLen := len(localCloudUrl) + remoteCloudUrl := "@" + getCloudUrl(c.federation.Load().NextcloudUrl) + checkSessionId := true for _, u := range users { - key := "sessionId" - sid, found := u[key] - if !found { - key := "sessionid" - sid, found = u[key] + if actorType, found := getStringMapEntry[string](u, "actorType"); found { + if actorId, found := getStringMapEntry[string](u, "actorId"); found { + switch actorType { + case ActorTypeFederatedUsers: + if strings.HasSuffix(actorId, localCloudUrl) { + u["actorId"] = actorId[:len(actorId)-localCloudUrlLen] + u["actorType"] = ActorTypeUsers + } + case ActorTypeUsers: + u["actorId"] = actorId + remoteCloudUrl + u["actorType"] = ActorTypeFederatedUsers + } + } } - if found { - if sid, ok := sid.(string); ok && sid == remoteSessionId { + + if checkSessionId { + key := "sessionId" + sid, found := getStringMapEntry[string](u, key) + if !found { + key := "sessionid" + sid, found = getStringMapEntry[string](u, key) + } + if found && sid == remoteSessionId { u[key] = localSessionId - break + checkSessionId = false } } } diff --git a/federation_test.go b/federation_test.go index 0e31f155..a56edae2 100644 --- a/federation_test.go +++ b/federation_test.go @@ -24,6 +24,7 @@ package signaling import ( "context" "encoding/json" + "strings" "testing" "time" @@ -286,23 +287,53 @@ func Test_Federation(t *testing.T) { } } - // Simulate request from the backend that somebody joined the call. + // Simulate request from the backend that a federated user joined the call. users := []map[string]interface{}{ { "sessionId": remoteSessionId, "inCall": 1, + "actorId": "remoteUser@" + strings.TrimPrefix(server2.URL, "http://"), + "actorType": "federated_users", }, } room := hub1.getRoom(roomId) require.NotNil(room) room.PublishUsersInCallChanged(users, users) var event *EventServerMessage + // For the local user, it's a federated user on server 2 that joined. assert.NoError(checkReceiveClientEvent(ctx, client1, "update", &event)) assert.Equal(remoteSessionId, event.Update.Users[0]["sessionId"]) + assert.Equal("remoteUser@"+strings.TrimPrefix(server2.URL, "http://"), event.Update.Users[0]["actorId"]) + assert.Equal("federated_users", event.Update.Users[0]["actorType"]) assert.Equal(roomId, event.Update.RoomId) - + // For the federated user, it's a local user that joined. assert.NoError(checkReceiveClientEvent(ctx, client2, "update", &event)) assert.Equal(hello2.Hello.SessionId, event.Update.Users[0]["sessionId"]) + assert.Equal("remoteUser", event.Update.Users[0]["actorId"]) + assert.Equal("users", event.Update.Users[0]["actorType"]) + assert.Equal(federatedRoomId, event.Update.RoomId) + + // Simulate request from the backend that a local user joined the call. + users = []map[string]interface{}{ + { + "sessionId": hello1.Hello.SessionId, + "inCall": 1, + "actorId": "localUser", + "actorType": "users", + }, + } + room.PublishUsersInCallChanged(users, users) + // For the local user, it's a local user that joined. + assert.NoError(checkReceiveClientEvent(ctx, client1, "update", &event)) + assert.Equal(hello1.Hello.SessionId, event.Update.Users[0]["sessionId"]) + assert.Equal("localUser", event.Update.Users[0]["actorId"]) + assert.Equal("users", event.Update.Users[0]["actorType"]) + assert.Equal(roomId, event.Update.RoomId) + // For the federated user, it's a federated user on server 1 that joined. + assert.NoError(checkReceiveClientEvent(ctx, client2, "update", &event)) + assert.Equal(hello1.Hello.SessionId, event.Update.Users[0]["sessionId"]) + assert.Equal("localUser@"+strings.TrimPrefix(server1.URL, "http://"), event.Update.Users[0]["actorId"]) + assert.Equal("federated_users", event.Update.Users[0]["actorType"]) assert.Equal(federatedRoomId, event.Update.RoomId) // Joining another "direct" session will trigger correct events.