snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
This commit is contained in:
546
server/public/model/remote_cluster.go
Normal file
546
server/public/model/remote_cluster.go
Normal file
@@ -0,0 +1,546 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/pbkdf2"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/scrypt"
|
||||
)
|
||||
|
||||
const (
|
||||
RemoteOfflineAfterMillis = 1000 * 60 * 5 // 5 minutes
|
||||
RemoteNameMinLength = 1
|
||||
RemoteNameMaxLength = 64
|
||||
|
||||
SiteURLPending = "pending_"
|
||||
|
||||
// Deprecated: SiteURLPlugin was used as a prefix for plugin-based remote SiteURLs.
|
||||
// New registrations store the plugin-provided SiteURL directly. Use PluginID field
|
||||
// to identify plugin-based remotes.
|
||||
SiteURLPlugin = "plugin_"
|
||||
|
||||
BitflagOptionAutoShareDMs Bitmask = 1 << iota // Any new DM/GM is automatically shared
|
||||
BitflagOptionAutoInvited // Remote is automatically invited to all shared channels
|
||||
)
|
||||
|
||||
var (
|
||||
validRemoteNameChars = regexp.MustCompile(`^[a-zA-Z0-9\.\-\_]+$`)
|
||||
|
||||
ErrOfflineRemote = errors.New("remote is offline")
|
||||
)
|
||||
|
||||
type Bitmask uint32
|
||||
|
||||
func (bm *Bitmask) IsBitSet(flag Bitmask) bool {
|
||||
return *bm != 0
|
||||
}
|
||||
|
||||
func (bm *Bitmask) SetBit(flag Bitmask) {
|
||||
*bm |= flag
|
||||
}
|
||||
|
||||
func (bm *Bitmask) UnsetBit(flag Bitmask) {
|
||||
*bm &= ^flag
|
||||
}
|
||||
|
||||
type RemoteCluster struct {
|
||||
RemoteId string `json:"remote_id"`
|
||||
RemoteTeamId string `json:"remote_team_id"` // Deprecated: this field is no longer used. It's only kept for backwards compatibility.
|
||||
Name string `json:"name"`
|
||||
DisplayName string `json:"display_name"`
|
||||
SiteURL string `json:"site_url"`
|
||||
DefaultTeamId string `json:"default_team_id"`
|
||||
CreateAt int64 `json:"create_at"`
|
||||
DeleteAt int64 `json:"delete_at"`
|
||||
LastPingAt int64 `json:"last_ping_at"`
|
||||
LastGlobalUserSyncAt int64 `json:"last_global_user_sync_at"` // Timestamp of last global user sync
|
||||
Token string `json:"token"`
|
||||
RemoteToken string `json:"remote_token"`
|
||||
Topics string `json:"topics"`
|
||||
CreatorId string `json:"creator_id"`
|
||||
PluginID string `json:"plugin_id"` // non-empty when sync message are to be delivered via plugin API
|
||||
Options Bitmask `json:"options"` // bit-flag set of options
|
||||
}
|
||||
|
||||
func (rc *RemoteCluster) Auditable() map[string]any {
|
||||
return map[string]any{
|
||||
"remote_id": rc.RemoteId,
|
||||
"remote_team_id": rc.RemoteTeamId,
|
||||
"name": rc.Name,
|
||||
"display_name": rc.DisplayName,
|
||||
"site_url": rc.SiteURL,
|
||||
"default_team_id": rc.DefaultTeamId,
|
||||
"create_at": rc.CreateAt,
|
||||
"delete_at": rc.DeleteAt,
|
||||
"last_ping_at": rc.LastPingAt,
|
||||
"last_global_user_sync_at": rc.LastGlobalUserSyncAt,
|
||||
"creator_id": rc.CreatorId,
|
||||
"plugin_id": rc.PluginID,
|
||||
"options": rc.Options,
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
if rc.SiteURL == "" {
|
||||
return NewAppError("RemoteCluster.IsValid", "model.cluster.is_valid.site_url.app_error", nil, "site_url is empty", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if rc.DefaultTeamId != "" && !IsValidId(rc.DefaultTeamId) {
|
||||
return NewAppError("RemoteCluster.IsValid", "model.cluster.is_valid.id.app_error", nil, "default_team_id="+rc.DefaultTeamId, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rc *RemoteCluster) Sanitize() {
|
||||
rc.Token = ""
|
||||
rc.RemoteToken = ""
|
||||
}
|
||||
|
||||
type RemoteClusterPatch struct {
|
||||
DisplayName *string `json:"display_name"`
|
||||
DefaultTeamId *string `json:"default_team_id"`
|
||||
}
|
||||
|
||||
func (rcp *RemoteClusterPatch) Auditable() map[string]any {
|
||||
return map[string]any{
|
||||
"display_name": rcp.DisplayName,
|
||||
"default_team_id": rcp.DefaultTeamId,
|
||||
}
|
||||
}
|
||||
|
||||
func (rc *RemoteCluster) Patch(patch *RemoteClusterPatch) {
|
||||
if patch.DisplayName != nil {
|
||||
rc.DisplayName = *patch.DisplayName
|
||||
}
|
||||
|
||||
if patch.DefaultTeamId != nil {
|
||||
rc.DefaultTeamId = *patch.DefaultTeamId
|
||||
}
|
||||
}
|
||||
|
||||
type RemoteClusterWithPassword struct {
|
||||
*RemoteCluster
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type RemoteClusterWithInvite struct {
|
||||
RemoteCluster *RemoteCluster `json:"remote_cluster"`
|
||||
Invite string `json:"invite"`
|
||||
Password string `json:"password,omitempty"`
|
||||
}
|
||||
|
||||
func (rc *RemoteCluster) IsOptionFlagSet(flag Bitmask) bool {
|
||||
return rc.Options.IsBitSet(flag)
|
||||
}
|
||||
|
||||
func (rc *RemoteCluster) SetOptionFlag(flag Bitmask) {
|
||||
rc.Options.SetBit(flag)
|
||||
}
|
||||
|
||||
func (rc *RemoteCluster) UnsetOptionFlag(flag Bitmask) {
|
||||
rc.Options.UnsetBit(flag)
|
||||
}
|
||||
|
||||
func IsValidRemoteName(s string) bool {
|
||||
if len(s) < RemoteNameMinLength || len(s) > RemoteNameMaxLength {
|
||||
return false
|
||||
}
|
||||
return validRemoteNameChars.MatchString(s)
|
||||
}
|
||||
|
||||
// CleanRemoteName converts an arbitrary string into a slug compatible with
|
||||
// IsValidRemoteName: lowercased, with spaces and other disallowed characters
|
||||
// replaced by hyphens. The result is truncated to RemoteNameMaxLength. If
|
||||
// the cleaned value is still invalid (e.g. empty input), a new ID is
|
||||
// substituted so the caller always receives a valid name.
|
||||
func CleanRemoteName(s string) string {
|
||||
s = strings.ToLower(strings.ReplaceAll(s, " ", "-"))
|
||||
s = strings.TrimSpace(s)
|
||||
|
||||
for _, c := range s {
|
||||
char := fmt.Sprintf("%c", c)
|
||||
if !validRemoteNameChars.MatchString(char) {
|
||||
s = strings.ReplaceAll(s, char, "-")
|
||||
}
|
||||
}
|
||||
|
||||
s = strings.Trim(s, "-")
|
||||
|
||||
if len(s) > RemoteNameMaxLength {
|
||||
s = strings.Trim(s[:RemoteNameMaxLength], "-")
|
||||
}
|
||||
|
||||
if !IsValidRemoteName(s) {
|
||||
s = NewId()
|
||||
}
|
||||
|
||||
return 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
|
||||
}
|
||||
|
||||
func (rc *RemoteCluster) IsConfirmed() bool {
|
||||
if rc.IsPlugin() {
|
||||
return true // local plugins are automatically confirmed
|
||||
}
|
||||
|
||||
if rc.SiteURL != "" && !strings.HasPrefix(rc.SiteURL, SiteURLPending) {
|
||||
return true // empty or pending siteurl are not confirmed
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (rc *RemoteCluster) IsPlugin() bool {
|
||||
return rc.PluginID != ""
|
||||
}
|
||||
|
||||
func (rc *RemoteCluster) GetSiteURL() string {
|
||||
siteURL := rc.SiteURL
|
||||
if strings.HasPrefix(siteURL, SiteURLPending) {
|
||||
siteURL = "..."
|
||||
}
|
||||
if strings.HasPrefix(siteURL, SiteURLPending) || strings.HasPrefix(siteURL, SiteURLPlugin) {
|
||||
siteURL = "plugin"
|
||||
}
|
||||
return siteURL
|
||||
}
|
||||
|
||||
// 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.SplitSeq(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{
|
||||
RemoteId: rc.RemoteId,
|
||||
Name: rc.Name,
|
||||
DisplayName: rc.DisplayName,
|
||||
CreateAt: rc.CreateAt,
|
||||
DeleteAt: rc.DeleteAt,
|
||||
LastPingAt: rc.LastPingAt,
|
||||
SiteURL: rc.SiteURL,
|
||||
}
|
||||
}
|
||||
|
||||
func NormalizeRemoteName(name string) string {
|
||||
return strings.ToLower(name)
|
||||
}
|
||||
|
||||
// RemoteClusterInfo provides a subset of RemoteCluster fields suitable for sending to clients.
|
||||
type RemoteClusterInfo struct {
|
||||
RemoteId string `json:"remote_id"`
|
||||
Name string `json:"name"`
|
||||
DisplayName string `json:"display_name"`
|
||||
CreateAt int64 `json:"create_at"`
|
||||
DeleteAt int64 `json:"delete_at"`
|
||||
LastPingAt int64 `json:"last_ping_at"`
|
||||
SiteURL string `json:"site_url,omitempty"`
|
||||
}
|
||||
|
||||
// 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) Auditable() map[string]any {
|
||||
return map[string]any{
|
||||
"remote_id": f.RemoteId,
|
||||
"msg_id": f.Msg.Id,
|
||||
"topic": f.Msg.Topic,
|
||||
}
|
||||
}
|
||||
|
||||
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 appErr := f.Msg.IsValid(); appErr != nil {
|
||||
return appErr
|
||||
}
|
||||
|
||||
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]any{"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"` // Deprecated: this field is no longer used. It's only kept for backwards compatibility.
|
||||
SiteURL string `json:"site_url"`
|
||||
Token string `json:"token"`
|
||||
RefreshedToken string `json:"refreshed_token,omitempty"` // New token generated by the remote cluster when accepting an invitation
|
||||
Version int `json:"version,omitempty"`
|
||||
}
|
||||
|
||||
func (rci *RemoteClusterInvite) IsValid() *AppError {
|
||||
if !IsValidId(rci.RemoteId) {
|
||||
return NewAppError("RemoteClusterInvite.IsValid", "model.remote_cluster_invite.is_valid.remote_id.app_error", nil, "id="+rci.RemoteId, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if rci.Token == "" {
|
||||
return NewAppError("RemoteClusterInvite.IsValid", "model.remote_cluster_invite.is_valid.token.app_error", nil, "Token empty", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if _, err := url.ParseRequestURI(rci.SiteURL); err != nil {
|
||||
return NewAppError("RemoteClusterInvite.IsValid", "model.remote_cluster_invite.is_valid.site_url.app_error", nil, "", http.StatusBadRequest).Wrap(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
var key []byte
|
||||
if rci.Version >= 3 {
|
||||
// Use PBKDF2 for version 3 and above
|
||||
key, err = pbkdf2.Key(sha256.New, password, salt, 600000, 32)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
// Use scrypt for older versions
|
||||
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 //nolint:makezero
|
||||
}
|
||||
|
||||
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:]
|
||||
|
||||
// Try PBKDF2 first (for version 3+)
|
||||
if err := rci.tryDecrypt(encrypted, password, salt, true); err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Fall back to scrypt (for older versions)
|
||||
return rci.tryDecrypt(encrypted, password, salt, false)
|
||||
}
|
||||
|
||||
func (rci *RemoteClusterInvite) tryDecrypt(encrypted []byte, password string, salt []byte, usePBKDF2 bool) error {
|
||||
var key []byte
|
||||
var err error
|
||||
|
||||
if usePBKDF2 {
|
||||
// Use PBKDF2 for version 3 and above
|
||||
key, err = pbkdf2.Key(sha256.New, password, salt, 600000, 32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// Use scrypt for older versions
|
||||
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)
|
||||
}
|
||||
|
||||
type RemoteClusterAcceptInvite struct {
|
||||
Name string `json:"name"`
|
||||
DisplayName string `json:"display_name"`
|
||||
DefaultTeamId string `json:"default_team_id"`
|
||||
Invite string `json:"invite"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
// RemoteClusterQueryFilter provides filter criteria for RemoteClusterStore.GetAll
|
||||
type RemoteClusterQueryFilter struct {
|
||||
ExcludeOffline bool
|
||||
InChannel string
|
||||
NotInChannel string
|
||||
Topic string
|
||||
CreatorId string
|
||||
OnlyConfirmed bool
|
||||
PluginID string
|
||||
OnlyPlugins bool
|
||||
ExcludePlugins bool
|
||||
RequireOptions Bitmask
|
||||
IncludeDeleted bool
|
||||
}
|
||||
Reference in New Issue
Block a user