ots/pkg/client/sanity.go

143 lines
4.1 KiB
Go
Raw Normal View History

package client
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"regexp"
"strings"
"github.com/Luzifer/ots/pkg/customization"
"github.com/ryanuber/go-glob"
)
var (
// ErrAttachmentsDisabled signalizes the instance has attachments
// disabled but the checked secret contains attachments
ErrAttachmentsDisabled = errors.New("attachments are disabled on this instance")
// ErrAttachmentsTooLarge signalizes the size of the attached files
// exceeds the configured maximum size of the given instance
ErrAttachmentsTooLarge = errors.New("attachment size exceeds allowed size")
// ErrAttachmentTypeNotAllowed signalizes any file does not match
// the allowed extensions / mime types
ErrAttachmentTypeNotAllowed = errors.New("attachment type is not allowed")
errSettingsNotFound = errors.New("settings not found")
mimeRegex = regexp.MustCompile(`^(?:[a-z]+|\*)\/(?:[a-zA-Z0-9.+_-]+|\*)$`)
)
// SanityCheck fetches the instance settings and validates the secret
// against those settings (matching file size, disabled attachments,
// allowed file types, ...)
func SanityCheck(instanceURL string, secret Secret) error {
cust, err := loadSettings(instanceURL)
if err != nil {
if errors.Is(err, errSettingsNotFound) {
// Sanity check is not possible when the API endpoint is not
// supported, therefore we ignore this.
return nil
}
return fmt.Errorf("fetching settings: %w", err)
}
// Check whether attachments are allowed at all
if cust.DisableFileAttachment && len(secret.Attachments) > 0 {
return ErrAttachmentsDisabled
}
// Check whether attachments are too large
var totalAttachmentSize int64
for _, a := range secret.Attachments {
totalAttachmentSize += int64(len(a.Content))
}
if cust.MaxAttachmentSizeTotal > 0 && totalAttachmentSize > cust.MaxAttachmentSizeTotal {
return ErrAttachmentsTooLarge
}
// Check for allowed types
if cust.AcceptedFileTypes != "" {
allowed := strings.Split(cust.AcceptedFileTypes, ",")
for _, a := range secret.Attachments {
if !attachmentAllowed(a, allowed) {
return ErrAttachmentTypeNotAllowed
}
}
}
return nil
}
func attachmentAllowed(file SecretAttachment, allowed []string) bool {
mimeType, _, _ := strings.Cut(file.Type, ";")
logger := Logger.WithField("content-type", mimeType)
for _, a := range allowed {
switch {
case mimeRegex.MatchString(a):
// That's a mime type
if glob.Glob(a, mimeType) {
// The mime "glob" matches the file type
logger.WithField("allowed_by", a).Debug("attachment allowed")
return true
}
case a[0] == '.':
// That's a file extension
if strings.HasSuffix(file.Name, a) {
// The filename has the right extension
logger.WithField("allowed_by", a).Debug("attachment allowed")
return true
}
}
}
logger.Debug("attachment type not allowed")
return false
}
func loadSettings(instanceURL string) (c customization.Customize, err error) {
u, err := url.Parse(instanceURL)
if err != nil {
return c, fmt.Errorf("parsing instance URL: %w", err)
}
createURL := u.JoinPath(strings.Join([]string{".", "api", "settings"}, "/"))
ctx, cancel := context.WithTimeout(context.Background(), RequestTimeout)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, createURL.String(), nil)
if err != nil {
return c, fmt.Errorf("creating request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", UserAgent)
resp, err := HTTPClient.Do(req)
if err != nil {
return c, fmt.Errorf("executing request: %w", err)
}
defer resp.Body.Close() //nolint:errcheck // possible leaked-fd, lib should not log, potential short-lived leak
if resp.StatusCode == http.StatusNotFound {
return c, errSettingsNotFound
}
if resp.StatusCode != http.StatusOK {
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return c, fmt.Errorf("unexpected HTTP status %d", resp.StatusCode)
}
return c, fmt.Errorf("unexpected HTTP status %d (%s)", resp.StatusCode, respBody)
}
if err = json.NewDecoder(resp.Body).Decode(&c); err != nil {
return c, fmt.Errorf("decoding response: %w", err)
}
return c, nil
}