feat: initial release with 48 Mattermost MCP tools
- 27 base tools: channels, messaging, users, reactions, files, DMs - Team management: invite/remove users, get stats, list members - Slash commands: execute /remind, /poll, etc. - Webhook management: incoming and outgoing webhooks - System tools: server config, logs, bulk status updates - Channel admin: create, invite, leave, delete channels - Read-only mode for safe exploration - Dual token support (bot + PAT) for enhanced security - Apache 2.0 licensed
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const MaxFileSize = 50 * 1024 * 1024
|
||||
|
||||
var AllowedMimeTypes = []string{
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/gif",
|
||||
"image/webp",
|
||||
"application/pdf",
|
||||
"text/plain",
|
||||
"text/markdown",
|
||||
"application/msword",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
}
|
||||
|
||||
var DangerousExtensions = []string{
|
||||
".exe", ".sh", ".bat", ".cmd", ".com", ".scr", ".pif", ".vbs",
|
||||
".js", ".jse", ".wsf", ".wsh", ".ps1", ".ps2", ".msc",
|
||||
".dll", ".so", ".dylib", ".bin",
|
||||
}
|
||||
|
||||
func ValidatePath(base, target string) (string, error) {
|
||||
cleanTarget := filepath.Clean(target)
|
||||
if filepath.IsAbs(cleanTarget) {
|
||||
return "", fmt.Errorf("absolute paths not allowed: %s", target)
|
||||
}
|
||||
|
||||
fullPath := filepath.Join(base, cleanTarget)
|
||||
realBase, err := filepath.Abs(base)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get absolute path for base: %w", err)
|
||||
}
|
||||
|
||||
realTarget, err := filepath.Abs(fullPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get absolute path for target: %w", err)
|
||||
}
|
||||
|
||||
if !filepath.HasPrefix(realTarget, realBase) {
|
||||
return "", fmt.Errorf("path traversal detected: %s", target)
|
||||
}
|
||||
|
||||
return realTarget, nil
|
||||
}
|
||||
|
||||
func Exists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return !os.IsNotExist(err)
|
||||
}
|
||||
|
||||
func IsValidPath(path string) bool {
|
||||
cleanPath := filepath.Clean(path)
|
||||
if strings.Contains(cleanPath, "..") {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func SanitizePath(path string) (string, error) {
|
||||
if !IsValidPath(path) {
|
||||
return "", fmt.Errorf("invalid path: path traversal detected: %s", path)
|
||||
}
|
||||
return filepath.Clean(path), nil
|
||||
}
|
||||
|
||||
func ValidateFileSize(size int64) error {
|
||||
if size > MaxFileSize {
|
||||
return fmt.Errorf("file size %d bytes exceeds maximum allowed size of %d bytes (50MB)", size, MaxFileSize)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func IsAllowedMimeType(mimeType string) bool {
|
||||
baseMimeType := strings.Split(mimeType, ";")[0]
|
||||
baseMimeType = strings.TrimSpace(baseMimeType)
|
||||
for _, allowed := range AllowedMimeTypes {
|
||||
if strings.EqualFold(baseMimeType, allowed) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(baseMimeType, "application/vnd.openxmlformats-officedocument") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func DetectMimeType(data []byte) string {
|
||||
return http.DetectContentType(data)
|
||||
}
|
||||
|
||||
func ValidateMimeType(data []byte) (string, error) {
|
||||
mimeType := DetectMimeType(data)
|
||||
if !IsAllowedMimeType(mimeType) {
|
||||
return "", fmt.Errorf("MIME type %q is not in the allowed whitelist", mimeType)
|
||||
}
|
||||
return mimeType, nil
|
||||
}
|
||||
|
||||
func IsDangerousExtension(filename string) bool {
|
||||
ext := strings.ToLower(filepath.Ext(filename))
|
||||
for _, dangerous := range DangerousExtensions {
|
||||
if ext == dangerous {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func ValidateFilename(filename string) error {
|
||||
if filename == "" {
|
||||
return fmt.Errorf("filename cannot be empty")
|
||||
}
|
||||
if IsDangerousExtension(filename) {
|
||||
return fmt.Errorf("file type %q is not allowed for security reasons", filepath.Ext(filename))
|
||||
}
|
||||
if strings.Contains(filename, "\x00") {
|
||||
return fmt.Errorf("filename contains invalid characters")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetFileInfo(path string) (os.FileInfo, error) {
|
||||
return os.Stat(path)
|
||||
}
|
||||
|
||||
func CheckDiskSpace(path string, requiredBytes int64) error {
|
||||
dir := filepath.Dir(path)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create directory: %w", err)
|
||||
}
|
||||
testFile := filepath.Join(dir, ".write_test_"+fmt.Sprintf("%d", os.Getpid()))
|
||||
f, err := os.Create(testFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("insufficient permissions or disk space at %s: %w", dir, err)
|
||||
}
|
||||
f.Close()
|
||||
os.Remove(testFile)
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user