diff --git a/server/hdl_files.go b/server/hdl_files.go index a06d2e3..365bf4f 100644 --- a/server/hdl_files.go +++ b/server/hdl_files.go @@ -17,6 +17,7 @@ import ( "io" "math/rand" "mime" + "net" "net/http" "net/url" "path/filepath" @@ -369,6 +370,7 @@ func largeFileReceiveHTTP(wrt http.ResponseWriter, req *http.Request) { logs.Warn.Printf("media upload: failed to enqueue file lifecycle event file=%s: %v", fdef.Id, err) } + url = normalizeUploadURL(url) params := map[string]any{"url": url, "file_id": fdef.Uid().String()} if globals.mediaGcPeriod > 0 { // How long this file is guaranteed to exist without being attached to a message or a topic. @@ -700,7 +702,7 @@ func (*grpcNodeServer) LargeFileReceive(stream pbx.Node_LargeFileReceiveServer) Code: http.StatusOK, Text: http.StatusText(http.StatusOK), Meta: &pbx.FileMeta{ - Name: url, + Name: normalizeUploadURL(url), MimeType: mimeType, Etag: fdef.ETag, Size: size, @@ -719,6 +721,27 @@ func (*grpcNodeServer) LargeFileReceive(stream pbx.Node_LargeFileReceiveServer) return err } +func normalizeUploadURL(rawURL string) string { + parsed, err := url.Parse(strings.TrimSpace(rawURL)) + if err != nil || parsed.Scheme != "http" || parsed.Host == "" || isLocalUploadHost(parsed.Hostname()) { + return rawURL + } + parsed.Scheme = "https" + return parsed.String() +} + +func isLocalUploadHost(host string) bool { + host = strings.ToLower(strings.TrimSpace(host)) + if host == "" || host == "localhost" { + return true + } + ip := net.ParseIP(host) + if ip == nil { + return false + } + return ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() +} + // largeFileRunGarbageCollection runs every 'period' and deletes up to 'blockSize' unused files. // Returns channel which can be used to stop the process. func largeFileRunGarbageCollection(period time.Duration, blockSize int) chan<- bool { diff --git a/server/hdl_files_test.go b/server/hdl_files_test.go index 8621038..e0c55a2 100644 --- a/server/hdl_files_test.go +++ b/server/hdl_files_test.go @@ -74,6 +74,51 @@ func TestAuthFileRequestFromSessionRequiresTenant(t *testing.T) { } } +func TestNormalizeUploadURL(t *testing.T) { + for _, tc := range []struct { + name string + in string + want string + }{ + { + name: "public http becomes https", + in: "http://api.118im.com/v0/file/s/abc.jpg", + want: "https://api.118im.com/v0/file/s/abc.jpg", + }, + { + name: "query preserved", + in: "http://cdn.example.com/a/b.png?x=1", + want: "https://cdn.example.com/a/b.png?x=1", + }, + { + name: "relative unchanged", + in: "/v0/file/s/abc.jpg", + want: "/v0/file/s/abc.jpg", + }, + { + name: "https unchanged", + in: "https://api.118im.com/v0/file/s/abc.jpg", + want: "https://api.118im.com/v0/file/s/abc.jpg", + }, + { + name: "localhost unchanged", + in: "http://localhost:6060/v0/file/s/abc.jpg", + want: "http://localhost:6060/v0/file/s/abc.jpg", + }, + { + name: "private ip unchanged", + in: "http://10.42.0.1/v0/file/s/abc.jpg", + want: "http://10.42.0.1/v0/file/s/abc.jpg", + }, + } { + t.Run(tc.name, func(t *testing.T) { + if got := normalizeUploadURL(tc.in); got != tc.want { + t.Fatalf("normalizeUploadURL(%q)=%q, want %q", tc.in, got, tc.want) + } + }) + } +} + func TestAuthFileRequestFromTokenReturnsPrincipal(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() diff --git a/server/media/oss/oss.go b/server/media/oss/oss.go index 8ee3fa0..5e00f0c 100644 --- a/server/media/oss/oss.go +++ b/server/media/oss/oss.go @@ -254,7 +254,7 @@ func (oh *ossHandler) Headers(tenantID types.TenantID, method string, url *url.U if redirURL != "" { return http.Header{ - "Location": {redirURL}, + "Location": {normalizeSignedRedirectURL(redirURL)}, "ETag": {`"` + fdef.ETag + `"`}, "Content-Type": {"application/json; charset=utf-8"}, "Cache-Control": {oh.conf.CacheControl}, @@ -263,6 +263,15 @@ func (oh *ossHandler) Headers(tenantID types.TenantID, method string, url *url.U return nil, 0, nil } +func normalizeSignedRedirectURL(rawURL string) string { + parsed, err := url.Parse(strings.TrimSpace(rawURL)) + if err != nil || parsed.Scheme != "http" || parsed.Host == "" { + return rawURL + } + parsed.Scheme = "https" + return parsed.String() +} + // Upload processes request for a file upload. func (oh *ossHandler) Upload(fdef *types.FileDef, file io.Reader) (string, int64, error) { key, err := tenantObjectKey(fdef.TenantID, fdef.Uid().String32(), fdef.CreatedAt) diff --git a/server/media/oss/oss_tenant_test.go b/server/media/oss/oss_tenant_test.go index 62ec123..fb77a67 100644 --- a/server/media/oss/oss_tenant_test.go +++ b/server/media/oss/oss_tenant_test.go @@ -58,6 +58,17 @@ func TestIsOSSObjectNotFound(t *testing.T) { } } +func TestNormalizeSignedRedirectURL(t *testing.T) { + rawURL := "http://im-oss-t1.oss-cn-shenzhen.aliyuncs.com/tenants/1/avatar.jpg?Expires=1&OSSAccessKeyId=ak&Signature=sig" + want := "https://im-oss-t1.oss-cn-shenzhen.aliyuncs.com/tenants/1/avatar.jpg?Expires=1&OSSAccessKeyId=ak&Signature=sig" + if got := normalizeSignedRedirectURL(rawURL); got != want { + t.Fatalf("normalizeSignedRedirectURL()=%q, want %q", got, want) + } + if got := normalizeSignedRedirectURL("/v0/file/s/avatar.jpg"); got != "/v0/file/s/avatar.jpg" { + t.Fatalf("relative URL must be unchanged, got %q", got) + } +} + func TestLoadSysConfig(t *testing.T) { original := store.ThirdPartyConfig store.ThirdPartyConfig = fakeThirdPartyConfig{