github.com/masterhung0112/hk_server/v5@v5.0.0-20220302090640-ec71aef15e1c/model/remote_cluster.go (about) 1 // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. 2 // See LICENSE.txt for license information. 3 4 package model 5 6 import ( 7 "crypto/aes" 8 "crypto/cipher" 9 "crypto/rand" 10 "encoding/json" 11 "errors" 12 "io" 13 "net/http" 14 "regexp" 15 "strings" 16 17 "golang.org/x/crypto/scrypt" 18 ) 19 20 const ( 21 RemoteOfflineAfterMillis = 1000 * 60 * 5 // 5 minutes 22 RemoteNameMinLength = 1 23 RemoteNameMaxLength = 64 24 ) 25 26 var ( 27 validRemoteNameChars = regexp.MustCompile(`^[a-zA-Z0-9\.\-\_]+$`) 28 ) 29 30 type RemoteCluster struct { 31 RemoteId string `json:"remote_id"` 32 RemoteTeamId string `json:"remote_team_id"` 33 Name string `json:"name"` 34 DisplayName string `json:"display_name"` 35 SiteURL string `json:"site_url"` 36 CreateAt int64 `json:"create_at"` 37 LastPingAt int64 `json:"last_ping_at"` 38 Token string `json:"token"` 39 RemoteToken string `json:"remote_token"` 40 Topics string `json:"topics"` 41 CreatorId string `json:"creator_id"` 42 } 43 44 func (rc *RemoteCluster) PreSave() { 45 if rc.RemoteId == "" { 46 rc.RemoteId = NewId() 47 } 48 49 if rc.DisplayName == "" { 50 rc.DisplayName = rc.Name 51 } 52 53 rc.Name = SanitizeUnicode(rc.Name) 54 rc.DisplayName = SanitizeUnicode(rc.DisplayName) 55 rc.Name = NormalizeRemoteName(rc.Name) 56 57 if rc.Token == "" { 58 rc.Token = NewId() 59 } 60 61 if rc.CreateAt == 0 { 62 rc.CreateAt = GetMillis() 63 } 64 rc.fixTopics() 65 } 66 67 func (rc *RemoteCluster) IsValid() *AppError { 68 if !IsValidId(rc.RemoteId) { 69 return NewAppError("RemoteCluster.IsValid", "model.cluster.is_valid.id.app_error", nil, "id="+rc.RemoteId, http.StatusBadRequest) 70 } 71 72 if !IsValidRemoteName(rc.Name) { 73 return NewAppError("RemoteCluster.IsValid", "model.cluster.is_valid.name.app_error", nil, "name="+rc.Name, http.StatusBadRequest) 74 } 75 76 if rc.CreateAt == 0 { 77 return NewAppError("RemoteCluster.IsValid", "model.cluster.is_valid.create_at.app_error", nil, "create_at=0", http.StatusBadRequest) 78 } 79 80 if !IsValidId(rc.CreatorId) { 81 return NewAppError("RemoteCluster.IsValid", "model.cluster.is_valid.id.app_error", nil, "creator_id="+rc.CreatorId, http.StatusBadRequest) 82 } 83 return nil 84 } 85 86 func IsValidRemoteName(s string) bool { 87 if len(s) < RemoteNameMinLength || len(s) > RemoteNameMaxLength { 88 return false 89 } 90 return validRemoteNameChars.MatchString(s) 91 } 92 93 func (rc *RemoteCluster) PreUpdate() { 94 if rc.DisplayName == "" { 95 rc.DisplayName = rc.Name 96 } 97 98 rc.Name = SanitizeUnicode(rc.Name) 99 rc.DisplayName = SanitizeUnicode(rc.DisplayName) 100 rc.Name = NormalizeRemoteName(rc.Name) 101 rc.fixTopics() 102 } 103 104 func (rc *RemoteCluster) IsOnline() bool { 105 return rc.LastPingAt > GetMillis()-RemoteOfflineAfterMillis 106 } 107 108 // fixTopics ensures all topics are separated by one, and only one, space. 109 func (rc *RemoteCluster) fixTopics() { 110 trimmed := strings.TrimSpace(rc.Topics) 111 if trimmed == "" || trimmed == "*" { 112 rc.Topics = trimmed 113 return 114 } 115 116 var sb strings.Builder 117 sb.WriteString(" ") 118 119 ss := strings.Split(rc.Topics, " ") 120 for _, c := range ss { 121 cc := strings.TrimSpace(c) 122 if cc != "" { 123 sb.WriteString(cc) 124 sb.WriteString(" ") 125 } 126 } 127 rc.Topics = sb.String() 128 } 129 130 func (rc *RemoteCluster) ToJSON() (string, error) { 131 b, err := json.Marshal(rc) 132 if err != nil { 133 return "", err 134 } 135 return string(b), nil 136 } 137 138 func (rc *RemoteCluster) ToRemoteClusterInfo() RemoteClusterInfo { 139 return RemoteClusterInfo{ 140 Name: rc.Name, 141 DisplayName: rc.DisplayName, 142 CreateAt: rc.CreateAt, 143 LastPingAt: rc.LastPingAt, 144 } 145 } 146 147 func NormalizeRemoteName(name string) string { 148 return strings.ToLower(name) 149 } 150 151 func RemoteClusterFromJSON(data io.Reader) (*RemoteCluster, *AppError) { 152 var rc RemoteCluster 153 err := json.NewDecoder(data).Decode(&rc) 154 if err != nil { 155 return nil, NewAppError("RemoteClusterFromJSON", "model.utils.decode_json.app_error", nil, err.Error(), http.StatusBadRequest) 156 } 157 return &rc, nil 158 } 159 160 // RemoteClusterInfo provides a subset of RemoteCluster fields suitable for sending to clients. 161 type RemoteClusterInfo struct { 162 Name string `json:"name"` 163 DisplayName string `json:"display_name"` 164 CreateAt int64 `json:"create_at"` 165 LastPingAt int64 `json:"last_ping_at"` 166 } 167 168 // RemoteClusterFrame wraps a `RemoteClusterMsg` with credentials specific to a remote cluster. 169 type RemoteClusterFrame struct { 170 RemoteId string `json:"remote_id"` 171 Msg RemoteClusterMsg `json:"msg"` 172 } 173 174 func (f *RemoteClusterFrame) IsValid() *AppError { 175 if !IsValidId(f.RemoteId) { 176 return NewAppError("RemoteClusterFrame.IsValid", "api.remote_cluster.invalid_id.app_error", nil, "RemoteId="+f.RemoteId, http.StatusBadRequest) 177 } 178 179 if err := f.Msg.IsValid(); err != nil { 180 return err 181 } 182 183 return nil 184 } 185 186 func RemoteClusterFrameFromJSON(data io.Reader) (*RemoteClusterFrame, *AppError) { 187 var frame RemoteClusterFrame 188 err := json.NewDecoder(data).Decode(&frame) 189 if err != nil { 190 return nil, NewAppError("RemoteClusterFrameFromJSON", "model.utils.decode_json.app_error", nil, err.Error(), http.StatusBadRequest) 191 } 192 return &frame, nil 193 } 194 195 // RemoteClusterMsg represents a message that is sent and received between clusters. 196 // These are processed and routed via the RemoteClusters service. 197 type RemoteClusterMsg struct { 198 Id string `json:"id"` 199 Topic string `json:"topic"` 200 CreateAt int64 `json:"create_at"` 201 Payload json.RawMessage `json:"payload"` 202 } 203 204 func NewRemoteClusterMsg(topic string, payload json.RawMessage) RemoteClusterMsg { 205 return RemoteClusterMsg{ 206 Id: NewId(), 207 Topic: topic, 208 CreateAt: GetMillis(), 209 Payload: payload, 210 } 211 } 212 213 func (m RemoteClusterMsg) IsValid() *AppError { 214 if !IsValidId(m.Id) { 215 return NewAppError("RemoteClusterMsg.IsValid", "api.remote_cluster.invalid_id.app_error", nil, "Id="+m.Id, http.StatusBadRequest) 216 } 217 218 if m.Topic == "" { 219 return NewAppError("RemoteClusterMsg.IsValid", "api.remote_cluster.invalid_topic.app_error", nil, "Topic empty", http.StatusBadRequest) 220 } 221 222 if len(m.Payload) == 0 { 223 return NewAppError("RemoteClusterMsg.IsValid", "api.context.invalid_body_param.app_error", map[string]interface{}{"Name": "PayLoad"}, "", http.StatusBadRequest) 224 } 225 226 return nil 227 } 228 229 func RemoteClusterMsgFromJSON(data io.Reader) (RemoteClusterMsg, *AppError) { 230 var msg RemoteClusterMsg 231 err := json.NewDecoder(data).Decode(&msg) 232 if err != nil { 233 return RemoteClusterMsg{}, NewAppError("RemoteClusterMsgFromJSON", "model.utils.decode_json.app_error", nil, err.Error(), http.StatusBadRequest) 234 } 235 return msg, nil 236 } 237 238 // RemoteClusterPing represents a ping that is sent and received between clusters 239 // to indicate a connection is alive. This is the payload for a `RemoteClusterMsg`. 240 type RemoteClusterPing struct { 241 SentAt int64 `json:"sent_at"` 242 RecvAt int64 `json:"recv_at"` 243 } 244 245 func RemoteClusterPingFromRawJSON(raw json.RawMessage) (RemoteClusterPing, *AppError) { 246 var ping RemoteClusterPing 247 err := json.Unmarshal(raw, &ping) 248 if err != nil { 249 return RemoteClusterPing{}, NewAppError("RemoteClusterPingFromRawJSON", "model.utils.decode_json.app_error", nil, err.Error(), http.StatusBadRequest) 250 } 251 return ping, nil 252 } 253 254 // RemoteClusterInvite represents an invitation to establish a simple trust with a remote cluster. 255 type RemoteClusterInvite struct { 256 RemoteId string `json:"remote_id"` 257 RemoteTeamId string `json:"remote_team_id"` 258 SiteURL string `json:"site_url"` 259 Token string `json:"token"` 260 } 261 262 func RemoteClusterInviteFromRawJSON(raw json.RawMessage) (*RemoteClusterInvite, *AppError) { 263 var invite RemoteClusterInvite 264 err := json.Unmarshal(raw, &invite) 265 if err != nil { 266 return nil, NewAppError("RemoteClusterInviteFromRawJSON", "model.utils.decode_json.app_error", nil, err.Error(), http.StatusBadRequest) 267 } 268 return &invite, nil 269 } 270 271 func (rci *RemoteClusterInvite) Encrypt(password string) ([]byte, error) { 272 raw, err := json.Marshal(&rci) 273 if err != nil { 274 return nil, err 275 } 276 277 // create random salt to be prepended to the blob. 278 salt := make([]byte, 16) 279 if _, err = io.ReadFull(rand.Reader, salt); err != nil { 280 return nil, err 281 } 282 283 key, err := scrypt.Key([]byte(password), salt, 32768, 8, 1, 32) 284 if err != nil { 285 return nil, err 286 } 287 288 block, err := aes.NewCipher(key[:]) 289 if err != nil { 290 return nil, err 291 } 292 293 gcm, err := cipher.NewGCM(block) 294 if err != nil { 295 return nil, err 296 } 297 298 // create random nonce 299 nonce := make([]byte, gcm.NonceSize()) 300 if _, err = io.ReadFull(rand.Reader, nonce); err != nil { 301 return nil, err 302 } 303 304 // prefix the nonce to the cyphertext so we don't need to keep track of it. 305 sealed := gcm.Seal(nonce, nonce, raw, nil) 306 307 return append(salt, sealed...), nil 308 } 309 310 func (rci *RemoteClusterInvite) Decrypt(encrypted []byte, password string) error { 311 if len(encrypted) <= 16 { 312 return errors.New("invalid length") 313 } 314 315 // first 16 bytes is the salt that was used to derive a key 316 salt := encrypted[:16] 317 encrypted = encrypted[16:] 318 319 key, err := scrypt.Key([]byte(password), salt, 32768, 8, 1, 32) 320 if err != nil { 321 return err 322 } 323 324 block, err := aes.NewCipher(key[:]) 325 if err != nil { 326 return err 327 } 328 329 gcm, err := cipher.NewGCM(block) 330 if err != nil { 331 return err 332 } 333 334 // nonce was prefixed to the cyphertext when encrypting so we need to extract it. 335 nonceSize := gcm.NonceSize() 336 nonce, cyphertext := encrypted[:nonceSize], encrypted[nonceSize:] 337 338 plain, err := gcm.Open(nil, nonce, cyphertext, nil) 339 if err != nil { 340 return err 341 } 342 343 // try to unmarshall the decrypted JSON to this invite struct. 344 return json.Unmarshal(plain, &rci) 345 } 346 347 // RemoteClusterQueryFilter provides filter criteria for RemoteClusterStore.GetAll 348 type RemoteClusterQueryFilter struct { 349 ExcludeOffline bool 350 InChannel string 351 NotInChannel string 352 Topic string 353 CreatorId string 354 OnlyConfirmed bool 355 }