Full Mattermost server source with integrated Community Enterprise features. Includes vendor directory for offline/air-gapped builds. Structure: - enterprise-impl/: Enterprise feature implementations - enterprise-community/: Init files that register implementations - enterprise/: Bridge imports (community_imports.go) - vendor/: All dependencies for offline builds Build (online): go build ./cmd/mattermost Build (offline/air-gapped): go build -mod=vendor ./cmd/mattermost 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
268 lines
7.9 KiB
Go
268 lines
7.9 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package commands
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"runtime"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/Masterminds/semver/v3"
|
|
"github.com/pkg/errors"
|
|
"github.com/spf13/cobra"
|
|
"github.com/spf13/viper"
|
|
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
|
|
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
|
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
|
)
|
|
|
|
const (
|
|
shellCompletionMaxItems = 50 // Maximum number of items that will be loaded and shown in shell completion.
|
|
shellCompleteTimeout = 5 * time.Second
|
|
)
|
|
|
|
var (
|
|
insecureSignatureAlgorithms = map[x509.SignatureAlgorithm]bool{
|
|
x509.SHA1WithRSA: true,
|
|
x509.DSAWithSHA1: true,
|
|
x509.ECDSAWithSHA1: true,
|
|
}
|
|
expectedSocketMode = os.ModeSocket | 0600
|
|
)
|
|
|
|
func CheckVersionMatch(version, serverVersion string) (bool, error) {
|
|
mmctlVersionParsed, err := semver.NewVersion(version)
|
|
if err != nil {
|
|
return false, errors.Wrapf(err, "Cannot parse version range %s", version)
|
|
}
|
|
|
|
// Split and recombine the server version string
|
|
parts := strings.Split(serverVersion, ".")
|
|
if len(parts) < 3 {
|
|
return false, fmt.Errorf("incorrect server version format: %s", serverVersion)
|
|
}
|
|
serverVersion = strings.Join(parts[:3], ".")
|
|
|
|
serverVersionParsed, err := semver.NewVersion(serverVersion)
|
|
if err != nil {
|
|
return false, errors.Wrapf(err, "Cannot parse version range %s", serverVersion)
|
|
}
|
|
|
|
if serverVersionParsed.Major() != mmctlVersionParsed.Major() {
|
|
return false, nil
|
|
}
|
|
|
|
if mmctlVersionParsed.Minor() > serverVersionParsed.Minor() {
|
|
return false, nil
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
func getClient(ctx context.Context, cmd *cobra.Command) (*model.Client4, string, bool, error) {
|
|
useLocal := viper.GetBool("local")
|
|
|
|
if !useLocal {
|
|
// Assume local mode if no server address is provided
|
|
credentials, err := GetCurrentCredentials()
|
|
if err != nil {
|
|
cmd.PrintErrln("Warning: Unable to retrieve credentials, assuming --local mode")
|
|
useLocal = true
|
|
} else if credentials == nil {
|
|
cmd.PrintErrln("Warning: No credentials found, assuming --local mode")
|
|
useLocal = true
|
|
}
|
|
}
|
|
|
|
if useLocal {
|
|
c, err := InitUnixClient(viper.GetString("local-socket-path"))
|
|
if err != nil {
|
|
return nil, "", true, err
|
|
}
|
|
printer.SetServerAddres("local instance")
|
|
|
|
return c, "", true, nil
|
|
}
|
|
|
|
c, serverVersion, err := InitClient(ctx, viper.GetBool("insecure-sha1-intermediate"), viper.GetBool("insecure-tls-version"))
|
|
if err != nil {
|
|
return nil, "", false, err
|
|
}
|
|
|
|
return c, serverVersion, false, nil
|
|
}
|
|
|
|
func withClient(fn func(c client.Client, cmd *cobra.Command, args []string) error) func(cmd *cobra.Command, args []string) error {
|
|
return func(cmd *cobra.Command, args []string) error {
|
|
ctx := context.TODO()
|
|
c, serverVersion, local, err := getClient(ctx, cmd)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create client: %w", err)
|
|
}
|
|
if local {
|
|
return fn(c, cmd, args)
|
|
}
|
|
|
|
if Version != "unspecified" { // unspecified version indicates that we are on dev mode.
|
|
valid, err := CheckVersionMatch(Version, serverVersion)
|
|
if err != nil {
|
|
return fmt.Errorf("could not check version mismatch: %w", err)
|
|
}
|
|
if !valid {
|
|
if viper.GetBool("strict") {
|
|
printer.PrintError("ERROR: server version " + serverVersion + " doesn't match with mmctl version " + Version + ". Strict flag is set, so the command will not be run")
|
|
os.Exit(1)
|
|
}
|
|
if !viper.GetBool("suppress-warnings") {
|
|
printer.PrintWarning("server version " + serverVersion + " doesn't match mmctl version " + Version)
|
|
}
|
|
}
|
|
}
|
|
|
|
printer.SetServerAddres(c.APIURL)
|
|
return fn(c, cmd, args)
|
|
}
|
|
}
|
|
|
|
func localOnlyPrecheck(cmd *cobra.Command, args []string) {
|
|
local := viper.GetBool("local")
|
|
if !local {
|
|
fmt.Fprintln(os.Stderr, "This command can only be run in local mode")
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func disableLocalPrecheck(cmd *cobra.Command, args []string) {
|
|
local := viper.GetBool("local")
|
|
if local {
|
|
fmt.Fprintln(os.Stderr, "This command cannot be run in local mode")
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func isValidChain(chain []*x509.Certificate) bool {
|
|
// check all certs but the root one
|
|
certs := chain[:len(chain)-1]
|
|
|
|
for _, cert := range certs {
|
|
if _, ok := insecureSignatureAlgorithms[cert.SignatureAlgorithm]; ok {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func VerifyCertificates(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
|
|
// loop over certificate chains
|
|
if slices.ContainsFunc(verifiedChains, isValidChain) {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("insecure algorithm found in the certificate chain. Use --insecure-sha1-intermediate flag to ignore. Aborting")
|
|
}
|
|
|
|
func NewAPIv4Client(instanceURL string, allowInsecureSHA1, allowInsecureTLS bool) *model.Client4 {
|
|
client := model.NewAPIv4Client(instanceURL)
|
|
userAgent := fmt.Sprintf("mmctl/%s (%s)", Version, runtime.GOOS)
|
|
client.HTTPHeader = map[string]string{"User-Agent": userAgent}
|
|
|
|
tlsConfig := &tls.Config{
|
|
MinVersion: tls.VersionTLS12,
|
|
}
|
|
|
|
if allowInsecureTLS {
|
|
tlsConfig.MinVersion = tls.VersionTLS10
|
|
}
|
|
|
|
if !allowInsecureSHA1 {
|
|
tlsConfig.VerifyPeerCertificate = VerifyCertificates
|
|
}
|
|
|
|
client.HTTPClient = &http.Client{
|
|
Transport: &http.Transport{
|
|
TLSClientConfig: tlsConfig,
|
|
Proxy: http.ProxyFromEnvironment,
|
|
},
|
|
}
|
|
|
|
return client
|
|
}
|
|
|
|
func InitClientWithUsernameAndPassword(ctx context.Context, username, password, instanceURL string, allowInsecureSHA1, allowInsecureTLS bool) (*model.Client4, string, error) {
|
|
client := NewAPIv4Client(instanceURL, allowInsecureSHA1, allowInsecureTLS)
|
|
|
|
_, resp, err := client.Login(ctx, username, password)
|
|
if err != nil {
|
|
return nil, "", checkInsecureTLSError(err, allowInsecureTLS)
|
|
}
|
|
return client, resp.ServerVersion, nil
|
|
}
|
|
|
|
func InitClientWithMFA(ctx context.Context, username, password, mfaToken, instanceURL string, allowInsecureSHA1, allowInsecureTLS bool) (*model.Client4, string, error) {
|
|
client := NewAPIv4Client(instanceURL, allowInsecureSHA1, allowInsecureTLS)
|
|
_, resp, err := client.LoginWithMFA(ctx, username, password, mfaToken)
|
|
if err != nil {
|
|
return nil, "", checkInsecureTLSError(err, allowInsecureTLS)
|
|
}
|
|
return client, resp.ServerVersion, nil
|
|
}
|
|
|
|
func InitClientWithCredentials(ctx context.Context, credentials *Credentials, allowInsecureSHA1, allowInsecureTLS bool) (*model.Client4, string, error) {
|
|
client := NewAPIv4Client(credentials.InstanceURL, allowInsecureSHA1, allowInsecureTLS)
|
|
|
|
client.AuthType = model.HeaderBearer
|
|
client.AuthToken = credentials.AuthToken
|
|
|
|
_, resp, err := client.GetMe(ctx, "")
|
|
if err != nil {
|
|
return nil, "", checkInsecureTLSError(err, allowInsecureTLS)
|
|
}
|
|
|
|
return client, resp.ServerVersion, nil
|
|
}
|
|
|
|
func InitClient(ctx context.Context, allowInsecureSHA1, allowInsecureTLS bool) (*model.Client4, string, error) {
|
|
credentials, err := GetCurrentCredentials()
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
return InitClientWithCredentials(ctx, credentials, allowInsecureSHA1, allowInsecureTLS)
|
|
}
|
|
|
|
func InitWebSocketClient() (*model.WebSocketClient, error) {
|
|
credentials, err := GetCurrentCredentials()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
client, appErr := model.NewWebSocketClient4(strings.Replace(credentials.InstanceURL, "http", "ws", 1), credentials.AuthToken)
|
|
if appErr != nil {
|
|
return nil, errors.Wrap(appErr, "unable to create the websockets connection")
|
|
}
|
|
return client, nil
|
|
}
|
|
|
|
func InitUnixClient(socketPath string) (*model.Client4, error) {
|
|
if err := checkValidSocket(socketPath); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return model.NewAPIv4SocketClient(socketPath), nil
|
|
}
|
|
|
|
func checkInsecureTLSError(err error, allowInsecureTLS bool) error {
|
|
if (strings.Contains(err.Error(), "tls: protocol version not supported") ||
|
|
strings.Contains(err.Error(), "tls: server selected unsupported protocol version")) && !allowInsecureTLS {
|
|
return errors.New("won't perform action through an insecure TLS connection. Please add --insecure-tls-version to bypass this check")
|
|
}
|
|
return err
|
|
}
|