diff options
Diffstat (limited to 'vendor/github.com/mattermost/mattermost-server/v6/model/remote_cluster.go')
-rw-r--r-- | vendor/github.com/mattermost/mattermost-server/v6/model/remote_cluster.go | 302 |
1 files changed, 302 insertions, 0 deletions
diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/remote_cluster.go b/vendor/github.com/mattermost/mattermost-server/v6/model/remote_cluster.go new file mode 100644 index 00000000..44d11f16 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/remote_cluster.go @@ -0,0 +1,302 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/json" + "errors" + "io" + "net/http" + "regexp" + "strings" + + "golang.org/x/crypto/scrypt" +) + +const ( + RemoteOfflineAfterMillis = 1000 * 60 * 5 // 5 minutes + RemoteNameMinLength = 1 + RemoteNameMaxLength = 64 +) + +var ( + validRemoteNameChars = regexp.MustCompile(`^[a-zA-Z0-9\.\-\_]+$`) +) + +type RemoteCluster struct { + RemoteId string `json:"remote_id"` + RemoteTeamId string `json:"remote_team_id"` + Name string `json:"name"` + DisplayName string `json:"display_name"` + SiteURL string `json:"site_url"` + CreateAt int64 `json:"create_at"` + LastPingAt int64 `json:"last_ping_at"` + Token string `json:"token"` + RemoteToken string `json:"remote_token"` + Topics string `json:"topics"` + CreatorId string `json:"creator_id"` +} + +func (rc *RemoteCluster) PreSave() { + if rc.RemoteId == "" { + rc.RemoteId = NewId() + } + + if rc.DisplayName == "" { + rc.DisplayName = rc.Name + } + + rc.Name = SanitizeUnicode(rc.Name) + rc.DisplayName = SanitizeUnicode(rc.DisplayName) + rc.Name = NormalizeRemoteName(rc.Name) + + if rc.Token == "" { + rc.Token = NewId() + } + + if rc.CreateAt == 0 { + rc.CreateAt = GetMillis() + } + rc.fixTopics() +} + +func (rc *RemoteCluster) IsValid() *AppError { + if !IsValidId(rc.RemoteId) { + return NewAppError("RemoteCluster.IsValid", "model.cluster.is_valid.id.app_error", nil, "id="+rc.RemoteId, http.StatusBadRequest) + } + + if !IsValidRemoteName(rc.Name) { + return NewAppError("RemoteCluster.IsValid", "model.cluster.is_valid.name.app_error", nil, "name="+rc.Name, http.StatusBadRequest) + } + + if rc.CreateAt == 0 { + return NewAppError("RemoteCluster.IsValid", "model.cluster.is_valid.create_at.app_error", nil, "create_at=0", http.StatusBadRequest) + } + + if !IsValidId(rc.CreatorId) { + return NewAppError("RemoteCluster.IsValid", "model.cluster.is_valid.id.app_error", nil, "creator_id="+rc.CreatorId, http.StatusBadRequest) + } + return nil +} + +func IsValidRemoteName(s string) bool { + if len(s) < RemoteNameMinLength || len(s) > RemoteNameMaxLength { + return false + } + return validRemoteNameChars.MatchString(s) +} + +func (rc *RemoteCluster) PreUpdate() { + if rc.DisplayName == "" { + rc.DisplayName = rc.Name + } + + rc.Name = SanitizeUnicode(rc.Name) + rc.DisplayName = SanitizeUnicode(rc.DisplayName) + rc.Name = NormalizeRemoteName(rc.Name) + rc.fixTopics() +} + +func (rc *RemoteCluster) IsOnline() bool { + return rc.LastPingAt > GetMillis()-RemoteOfflineAfterMillis +} + +// fixTopics ensures all topics are separated by one, and only one, space. +func (rc *RemoteCluster) fixTopics() { + trimmed := strings.TrimSpace(rc.Topics) + if trimmed == "" || trimmed == "*" { + rc.Topics = trimmed + return + } + + var sb strings.Builder + sb.WriteString(" ") + + ss := strings.Split(rc.Topics, " ") + for _, c := range ss { + cc := strings.TrimSpace(c) + if cc != "" { + sb.WriteString(cc) + sb.WriteString(" ") + } + } + rc.Topics = sb.String() +} + +func (rc *RemoteCluster) ToRemoteClusterInfo() RemoteClusterInfo { + return RemoteClusterInfo{ + Name: rc.Name, + DisplayName: rc.DisplayName, + CreateAt: rc.CreateAt, + LastPingAt: rc.LastPingAt, + } +} + +func NormalizeRemoteName(name string) string { + return strings.ToLower(name) +} + +// RemoteClusterInfo provides a subset of RemoteCluster fields suitable for sending to clients. +type RemoteClusterInfo struct { + Name string `json:"name"` + DisplayName string `json:"display_name"` + CreateAt int64 `json:"create_at"` + LastPingAt int64 `json:"last_ping_at"` +} + +// RemoteClusterFrame wraps a `RemoteClusterMsg` with credentials specific to a remote cluster. +type RemoteClusterFrame struct { + RemoteId string `json:"remote_id"` + Msg RemoteClusterMsg `json:"msg"` +} + +func (f *RemoteClusterFrame) IsValid() *AppError { + if !IsValidId(f.RemoteId) { + return NewAppError("RemoteClusterFrame.IsValid", "api.remote_cluster.invalid_id.app_error", nil, "RemoteId="+f.RemoteId, http.StatusBadRequest) + } + + if err := f.Msg.IsValid(); err != nil { + return err + } + + return nil +} + +// RemoteClusterMsg represents a message that is sent and received between clusters. +// These are processed and routed via the RemoteClusters service. +type RemoteClusterMsg struct { + Id string `json:"id"` + Topic string `json:"topic"` + CreateAt int64 `json:"create_at"` + Payload json.RawMessage `json:"payload"` +} + +func NewRemoteClusterMsg(topic string, payload json.RawMessage) RemoteClusterMsg { + return RemoteClusterMsg{ + Id: NewId(), + Topic: topic, + CreateAt: GetMillis(), + Payload: payload, + } +} + +func (m RemoteClusterMsg) IsValid() *AppError { + if !IsValidId(m.Id) { + return NewAppError("RemoteClusterMsg.IsValid", "api.remote_cluster.invalid_id.app_error", nil, "Id="+m.Id, http.StatusBadRequest) + } + + if m.Topic == "" { + return NewAppError("RemoteClusterMsg.IsValid", "api.remote_cluster.invalid_topic.app_error", nil, "Topic empty", http.StatusBadRequest) + } + + if len(m.Payload) == 0 { + return NewAppError("RemoteClusterMsg.IsValid", "api.context.invalid_body_param.app_error", map[string]interface{}{"Name": "PayLoad"}, "", http.StatusBadRequest) + } + + return nil +} + +// RemoteClusterPing represents a ping that is sent and received between clusters +// to indicate a connection is alive. This is the payload for a `RemoteClusterMsg`. +type RemoteClusterPing struct { + SentAt int64 `json:"sent_at"` + RecvAt int64 `json:"recv_at"` +} + +// RemoteClusterInvite represents an invitation to establish a simple trust with a remote cluster. +type RemoteClusterInvite struct { + RemoteId string `json:"remote_id"` + RemoteTeamId string `json:"remote_team_id"` + SiteURL string `json:"site_url"` + Token string `json:"token"` +} + +func (rci *RemoteClusterInvite) Encrypt(password string) ([]byte, error) { + raw, err := json.Marshal(&rci) + if err != nil { + return nil, err + } + + // create random salt to be prepended to the blob. + salt := make([]byte, 16) + if _, err = io.ReadFull(rand.Reader, salt); err != nil { + return nil, err + } + + key, err := scrypt.Key([]byte(password), salt, 32768, 8, 1, 32) + if err != nil { + return nil, err + } + + block, err := aes.NewCipher(key[:]) + if err != nil { + return nil, err + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + // create random nonce + nonce := make([]byte, gcm.NonceSize()) + if _, err = io.ReadFull(rand.Reader, nonce); err != nil { + return nil, err + } + + // prefix the nonce to the cyphertext so we don't need to keep track of it. + sealed := gcm.Seal(nonce, nonce, raw, nil) + + return append(salt, sealed...), nil +} + +func (rci *RemoteClusterInvite) Decrypt(encrypted []byte, password string) error { + if len(encrypted) <= 16 { + return errors.New("invalid length") + } + + // first 16 bytes is the salt that was used to derive a key + salt := encrypted[:16] + encrypted = encrypted[16:] + + key, err := scrypt.Key([]byte(password), salt, 32768, 8, 1, 32) + if err != nil { + return err + } + + block, err := aes.NewCipher(key[:]) + if err != nil { + return err + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return err + } + + // nonce was prefixed to the cyphertext when encrypting so we need to extract it. + nonceSize := gcm.NonceSize() + nonce, cyphertext := encrypted[:nonceSize], encrypted[nonceSize:] + + plain, err := gcm.Open(nil, nonce, cyphertext, nil) + if err != nil { + return err + } + + // try to unmarshall the decrypted JSON to this invite struct. + return json.Unmarshal(plain, &rci) +} + +// RemoteClusterQueryFilter provides filter criteria for RemoteClusterStore.GetAll +type RemoteClusterQueryFilter struct { + ExcludeOffline bool + InChannel string + NotInChannel string + Topic string + CreatorId string + OnlyConfirmed bool +} |