merge devel

This commit is contained in:
or-else
2026-03-31 11:25:33 +03:00
10 changed files with 96 additions and 12 deletions
+1 -1
View File
@@ -20,7 +20,7 @@ const (
defaultMinLoginLength = 2
defaultMaxLoginLength = 32
defaultMinPasswordLength = 3
defaultMinPasswordLength = 6
)
// Token suitable as a login: starts and ends with a Unicode letter (class L) or number (class N),
+15 -2
View File
@@ -18,6 +18,9 @@ import (
"github.com/tinode/chat/server/store/types"
)
// defaultTimeout is the HTTP client timeout used when no timeout is set in config.
const defaultTimeout = 5 * time.Second
// authenticator is the type to map authentication methods to.
type authenticator struct {
// Logical name of this authenticator
@@ -28,6 +31,8 @@ type authenticator struct {
allowNewAccounts bool
// Use separate endpoints, i.e. add request name to serverUrl path when making requests.
useSeparateEndpoints bool
// HTTP client with a timeout for calls to the auth server.
httpClient *http.Client
// Cache of restricted tag prefixes (namespaces).
rTagNS []string
// Optional regex pattern for checking tokens.
@@ -91,6 +96,8 @@ func (a *authenticator) Init(jsonconf json.RawMessage, name string) error {
AllowNewAccounts bool `json:"allow_new_accounts"`
// Use separate endpoints, i.e. add request name to serverUrl path when making requests.
UseSeparateEndpoints bool `json:"use_separate_endpoints"`
// Timeout for requests to the auth server in seconds. Default is 5.
Timeout int `json:"timeout"`
}
var config configType
@@ -113,6 +120,12 @@ func (a *authenticator) Init(jsonconf json.RawMessage, name string) error {
a.allowNewAccounts = config.AllowNewAccounts
a.useSeparateEndpoints = config.UseSeparateEndpoints
timeout := defaultTimeout
if config.Timeout > 0 {
timeout = time.Duration(config.Timeout) * time.Second
}
a.httpClient = &http.Client{Timeout: timeout}
return nil
}
@@ -137,8 +150,8 @@ func (a *authenticator) callEndpoint(endpoint string, rec *auth.Rec, secret []by
urlToCall = epUrl.String()
}
// Send payload to server using default HTTP client.
post, err := http.Post(urlToCall, "application/json", bytes.NewBuffer(content))
// Send payload to server using client with a configured timeout.
post, err := a.httpClient.Post(urlToCall, "application/json", bytes.NewBuffer(content))
if err != nil {
return nil, err
}
+11 -3
View File
@@ -510,10 +510,18 @@ func (*grpcNodeServer) LargeFileReceive(stream pbx.Node_LargeFileReceiveServer)
}
mimeType := http.DetectContentType(req.Content)
// If DetectContentType fails, use client-provided content type.
// If DetectContentType fails, see if client-provided content type can be used.
if mimeType == "application/octet-stream" {
if contentType := req.Meta.GetMimeType(); contentType != "" {
mimeType = contentType
if userContentType, params, err := mime.ParseMediaType(req.Meta.GetMimeType()); err == nil {
// Make sure the content-type is on the whitelist.
for _, allowed := range allowedMimeTypes {
if strings.HasPrefix(userContentType, allowed) {
if userContentType = mime.FormatMediaType(userContentType, params); userContentType != "" {
mimeType = userContentType
}
break
}
}
}
}
+11 -3
View File
@@ -17,6 +17,7 @@ import (
"time"
"github.com/tinode/chat/server/logs"
"github.com/tinode/chat/server/media"
)
func (sess *Session) sendMessageLp(wrt http.ResponseWriter, msg any) bool {
@@ -138,9 +139,16 @@ func serveLongPoll(wrt http.ResponseWriter, req *http.Request) {
return
}
// TODO(gene): should it be configurable?
// Currently any domain is allowed to get data from the chat server
wrt.Header().Set("Access-Control-Allow-Origin", "*")
// Set CORS header. If an origin allowlist is configured, echo the request
// origin back only when it is permitted; otherwise allow all origins.
if origin := req.Header.Get("Origin"); origin != "" {
if media.IsOriginAllowed(globals.allowedOrigins, origin) {
wrt.Header().Set("Access-Control-Allow-Origin", origin)
wrt.Header().Set("Vary", "Origin")
}
} else {
// No Origin header: non-browser client, no CORS header needed.
}
// Ensure the response is not cached
if req.ProtoAtLeast(1, 1) {
+6 -2
View File
@@ -16,6 +16,7 @@ import (
"github.com/gorilla/websocket"
"github.com/tinode/chat/server/logs"
"github.com/tinode/chat/server/media"
"github.com/tinode/chat/server/store/types"
)
@@ -161,8 +162,11 @@ var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
EnableCompression: globals.wsCompression,
// Allow connections from any Origin
CheckOrigin: func(r *http.Request) bool { return true },
// Validate the Origin header against the configured whitelist.
// If no whitelist is set all origins are permitted (backward-compatible).
CheckOrigin: func(r *http.Request) bool {
return media.IsOriginAllowed(globals.allowedOrigins, r.Header.Get("Origin"))
},
}
func serveWebSocket(wrt http.ResponseWriter, req *http.Request) {
+20
View File
@@ -48,6 +48,7 @@ import (
_ "github.com/tinode/chat/server/push/stdout"
_ "github.com/tinode/chat/server/push/tnpg"
"github.com/tinode/chat/server/media"
"github.com/tinode/chat/server/store"
// Credential validators
@@ -219,6 +220,11 @@ var globals struct {
allowedReactions map[string]bool
// The same reactions as a priority-sorted list.
reactions []string
// allowedOrigins is the list of HTTP Origins permitted for WebSocket and long-poll
// connections. Supports exact matches and wildcards (e.g. https://*.example.com).
// An empty slice means all origins are allowed (backward-compatible default).
allowedOrigins []media.AllowedOrigin
}
// Credential validator config.
@@ -326,6 +332,10 @@ type configType struct {
// AllowedReactions restricts reaction content that clients may use.
AllowedReactions []string `json:"allowed_reactions,omitempty"`
// AllowedOrigins is the list of HTTP Origins permitted for WebSocket and long-poll
// connections. An empty list allows all origins (default, backward compatible).
AllowedOrigins []string `json:"allowed_origins,omitempty"`
// Configs for subsystems
Cluster json.RawMessage `json:"cluster_config"`
Plugin json.RawMessage `json:"plugins"`
@@ -606,6 +616,16 @@ func main() {
}
}
// Allowed origins for WebSocket and long-poll connections.
if len(config.AllowedOrigins) > 0 {
var err error
globals.allowedOrigins, err = media.ParseCORSAllow(config.AllowedOrigins)
if err != nil {
logs.Err.Fatal("Invalid allowed_origins:", err)
}
logs.Info.Println("Allowed origins:", config.AllowedOrigins)
}
// Maximum number of group topic subscribers
globals.maxSubscriberCount = config.MaxSubscriberCount
if globals.maxSubscriberCount <= 1 {
+5 -1
View File
@@ -27,6 +27,10 @@ const (
defaultCacheControl = "max-age=86400"
handlerName = "fs"
// uploadDirMode is the permission bits for the upload directory.
// Owner: rwx, Group: r-x, Other: --- (no world access).
uploadDirMode = 0750
)
type fileConfig struct {
@@ -67,7 +71,7 @@ func (fh *fshandler) Init(jsconf string) error {
return errors.New("failed to parse CORS allowed origins: " + err.Error())
}
// Make sure the upload directory exists.
return os.MkdirAll(fh.FileUploadDirectory, 0777)
return os.MkdirAll(fh.FileUploadDirectory, uploadDirMode)
}
// Headers is used for cache management and serving CORS headers.
+13
View File
@@ -52,6 +52,19 @@ type AllowedOrigin struct {
HasWildcard bool
}
// IsOriginAllowed reports whether origin is permitted by the given allowlist.
// An empty allowlist means all origins are allowed.
// A missing Origin header (empty string) is always allowed (non-browser client).
func IsOriginAllowed(allowed []AllowedOrigin, origin string) bool {
if origin == "" {
return true
}
if len(allowed) == 0 {
return true
}
return matchCORSOrigin(allowed, origin) != ""
}
var fileNamePattern = regexp.MustCompile(`^[-_A-Za-z0-9]+`)
// GetIdFromUrl is a helper method for extracting file ID from a URL.
+9
View File
@@ -106,6 +106,15 @@
"😈", "🙈", "🙉", "🙊", "😇", "🫡",
],
// AllowedOrigins is the list of HTTP Origins permitted for WebSocket and long-poll
// connections. An empty list allows all origins (default).
"allowed_origins": [
// "https://www.example.com",
// "http://example.com",
// "https://*.example.com",
// "http://*.*.example.com"
],
// Large media/blob handlers: large files/images included in messages.
"media": {
// The name of the media handler to use.
+5
View File
@@ -211,6 +211,11 @@ func (v *validator) Init(jsonconf string) error {
v.SMTPPort = defaultPort
}
if v.TLSInsecureSkipVerify {
logs.Warn.Println("email validator: TLS certificate verification is DISABLED " +
"(insecure_skip_verify=true). Do not use in production.")
}
return nil
}