mirror of
https://github.com/tinode/chat.git
synced 2026-05-07 20:12:42 +00:00
merge devel
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -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
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user