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  }