mirror of
https://github.com/photoprism/photoprism.git
synced 2025-12-12 00:34:13 +01:00
Pkg: Move /service/http/... to /http/... and add package /http/dns
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
@@ -3,7 +3,7 @@ package clean
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/service/http/header"
|
||||
"github.com/photoprism/photoprism/pkg/http/header"
|
||||
)
|
||||
|
||||
// ContentType normalizes media content type strings, see https://en.wikipedia.org/wiki/Media_type.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package clean
|
||||
|
||||
import (
|
||||
"github.com/photoprism/photoprism/pkg/service/http/header"
|
||||
"github.com/photoprism/photoprism/pkg/http/header"
|
||||
)
|
||||
|
||||
// IP returns the sanitized and normalized network address if it is valid, or the default otherwise.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package fs
|
||||
|
||||
import (
|
||||
"github.com/photoprism/photoprism/pkg/service/http/header"
|
||||
"github.com/photoprism/photoprism/pkg/http/header"
|
||||
)
|
||||
|
||||
// TypeAnimated maps animated file types to their mime type.
|
||||
|
||||
@@ -32,7 +32,7 @@ import (
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/service/http/safe"
|
||||
"github.com/photoprism/photoprism/pkg/http/safe"
|
||||
)
|
||||
|
||||
var ignoreCase bool
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
"github.com/gabriel-vasile/mimetype"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/service/http/header"
|
||||
"github.com/photoprism/photoprism/pkg/http/header"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/service/http/header"
|
||||
"github.com/photoprism/photoprism/pkg/http/header"
|
||||
)
|
||||
|
||||
func TestDetectMimeType(t *testing.T) {
|
||||
|
||||
26
pkg/http/dns/dns.go
Normal file
26
pkg/http/dns/dns.go
Normal file
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
Package dns provides helpers for validating and classifying hostnames that are
|
||||
safe to use in cluster URLs, node identifiers, and other HTTP-facing settings.
|
||||
|
||||
Copyright (c) 2018 - 2025 PhotoPrism UG. All rights reserved.
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under Version 3 of the GNU Affero General Public License (the "AGPL"):
|
||||
<https://docs.photoprism.app/license/agpl>
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
The AGPL is supplemented by our Trademark and Brand Guidelines,
|
||||
which describe how our Brand Assets may be used:
|
||||
<https://www.photoprism.app/trademark>
|
||||
|
||||
Feel free to send an email to hello@photoprism.app if you have questions,
|
||||
want to support our work, or just want to say hello.
|
||||
|
||||
Additional information can be found in our Developer Guide:
|
||||
<https://docs.photoprism.app/developer-guide/>
|
||||
*/
|
||||
package dns
|
||||
73
pkg/http/dns/domain.go
Normal file
73
pkg/http/dns/domain.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GetHostname is a var to allow tests to stub os.Hostname.
|
||||
var GetHostname = os.Hostname
|
||||
|
||||
// IsDomain validates a DNS domain (FQDN or single label not allowed here). It must have at least one dot.
|
||||
// Each label must match IsLabel (except overall length and hyphen rules already covered by regex logic).
|
||||
func IsDomain(d string) bool {
|
||||
d = strings.TrimSuffix(strings.ToLower(strings.TrimSpace(d)), ".")
|
||||
if d == "" || strings.Count(d, ".") < 1 || len(d) > 253 {
|
||||
return false
|
||||
}
|
||||
if _, bad := ReservedDomains[d]; bad {
|
||||
return false
|
||||
}
|
||||
if IsLocalSuffix(d) {
|
||||
return false
|
||||
}
|
||||
parts := strings.Split(d, ".")
|
||||
for _, p := range parts {
|
||||
if !IsLabel(p) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// GetSystemDomain tries to determine a usable cluster domain from system configuration.
|
||||
// It uses the system hostname and returns the domain (everything after the first dot) when valid and not reserved.
|
||||
func GetSystemDomain() string {
|
||||
hn, _ := GetHostname()
|
||||
hn = strings.ToLower(strings.TrimSpace(hn))
|
||||
|
||||
if hn == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
if _, bad := NonUniqueHostnames[hn]; bad {
|
||||
return ""
|
||||
}
|
||||
|
||||
// If hostname contains a dot, take the domain part.
|
||||
if i := strings.IndexByte(hn, '.'); i > 0 && i < len(hn)-1 {
|
||||
dom := hn[i+1:]
|
||||
if IsDomain(dom) {
|
||||
return dom
|
||||
}
|
||||
}
|
||||
|
||||
// Try reverse lookup to get FQDN domain, then validate.
|
||||
if addrs, err := net.LookupAddr(hn); err == nil {
|
||||
for _, fqdn := range addrs {
|
||||
fqdn = strings.TrimSuffix(strings.ToLower(fqdn), ".")
|
||||
if fqdn == "" || fqdn == hn {
|
||||
continue
|
||||
}
|
||||
if i := strings.IndexByte(fqdn, '.'); i > 0 && i < len(fqdn)-1 {
|
||||
dom := fqdn[i+1:]
|
||||
if IsDomain(dom) {
|
||||
return dom
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
44
pkg/http/dns/domain_test.go
Normal file
44
pkg/http/dns/domain_test.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_IsDNSLabel(t *testing.T) {
|
||||
good := []string{"a", "node1", "pp-node-01", "n32", "a234567890123456789012345678901"}
|
||||
bad := []string{"", "A", "node_1", "-bad", "bad-", stringsRepeat("a", 33)}
|
||||
for _, s := range good {
|
||||
if !IsLabel(s) {
|
||||
t.Fatalf("expected valid label: %q", s)
|
||||
}
|
||||
}
|
||||
for _, s := range bad {
|
||||
if IsLabel(s) {
|
||||
t.Fatalf("expected invalid label: %q", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_IsDNSDomain(t *testing.T) {
|
||||
good := []string{"example.dev", "sub.domain.dev", "a.b"}
|
||||
bad := []string{"localdomain", "localhost", "a", "EXAMPLE.com", "example.com", "invalid", "test", "x.local"}
|
||||
for _, s := range good {
|
||||
if !IsDomain(s) {
|
||||
t.Fatalf("expected valid domain: %q", s)
|
||||
}
|
||||
}
|
||||
for _, s := range bad {
|
||||
if IsDomain(s) {
|
||||
t.Fatalf("expected invalid domain: %q", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// helper: fast string repeat without importing strings just for tests
|
||||
func stringsRepeat(s string, n int) string {
|
||||
b := make([]byte, 0, len(s)*n)
|
||||
for i := 0; i < n; i++ {
|
||||
b = append(b, s...)
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
16
pkg/http/dns/label.go
Normal file
16
pkg/http/dns/label.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
)
|
||||
|
||||
var labelRegex = regexp.MustCompile(`^[a-z0-9](?:[a-z0-9-]{0,30}[a-z0-9])?$`)
|
||||
|
||||
// IsLabel returns true if s is a valid DNS label per our rules: lowercase, [a-z0-9-], 1–32 chars, starts/ends alnum.
|
||||
func IsLabel(s string) bool {
|
||||
if s == "" || len(s) > 32 {
|
||||
return false
|
||||
}
|
||||
|
||||
return labelRegex.MatchString(s)
|
||||
}
|
||||
42
pkg/http/dns/local.go
Normal file
42
pkg/http/dns/local.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// NonUniqueHostnames lists hostnames that must never be used as node name or to
|
||||
// derive a cluster domain. It is mutable on purpose so tests or operators can
|
||||
// extend the set without changing the package API.
|
||||
var NonUniqueHostnames = map[string]struct{}{
|
||||
"localhost": {},
|
||||
"localhost.localdomain": {},
|
||||
"localdomain": {},
|
||||
}
|
||||
|
||||
// IsLocalSuffix reports whether the provided suffix is considered local-only
|
||||
// (for example mDNS domains ending in .local) and therefore unsuitable when
|
||||
// deriving public cluster domains.
|
||||
func IsLocalSuffix(suffix string) bool {
|
||||
return suffix == "local" || strings.HasSuffix(suffix, ".local")
|
||||
}
|
||||
|
||||
// IsLoopbackHost reports whether host refers to a loopback address that is safe
|
||||
// to contact over plain HTTP during bootstrap. It accepts hostnames (e.g.
|
||||
// "localhost") as well as IPv4/IPv6 addresses and normalises case/whitespace.
|
||||
func IsLoopbackHost(host string) bool {
|
||||
h := strings.TrimSpace(strings.ToLower(host))
|
||||
if h == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
if h == "localhost" {
|
||||
return true
|
||||
}
|
||||
|
||||
if ip := net.ParseIP(h); ip != nil {
|
||||
return ip.IsLoopback()
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
49
pkg/http/dns/local_test.go
Normal file
49
pkg/http/dns/local_test.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package dns
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestIsLocalSuffix(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
input string
|
||||
expect bool
|
||||
}{
|
||||
{"Local mDNS", "local", true},
|
||||
{"Nested local", "sub.local", true},
|
||||
{"Regular domain", "example.dev", false},
|
||||
{"Empty", "", false},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := IsLocalSuffix(tc.input); got != tc.expect {
|
||||
t.Fatalf("IsLocalSuffix(%q) = %v, want %v", tc.input, got, tc.expect)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsLoopbackHost(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
input string
|
||||
expect bool
|
||||
}{
|
||||
{"Empty", "", false},
|
||||
{"Localhost", "localhost", true},
|
||||
{"Mixed case host", "LOCALHOST", true},
|
||||
{"Loopback IPv4", "127.0.0.42", true},
|
||||
{"Loopback IPv6", "::1", true},
|
||||
{"Trim whitespace", " 127.0.0.1 ", true},
|
||||
{"Regular IPv4", "192.168.0.1", false},
|
||||
{"Regular host", "node.example", false},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := IsLoopbackHost(tc.input); got != tc.expect {
|
||||
t.Fatalf("IsLoopbackHost(%q) = %v, want %v", tc.input, got, tc.expect)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
10
pkg/http/dns/reserved.go
Normal file
10
pkg/http/dns/reserved.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package dns
|
||||
|
||||
// ReservedDomains lists special/reserved domains that must not be used as cluster domains.
|
||||
var ReservedDomains = map[string]struct{}{
|
||||
"example.com": {},
|
||||
"example.net": {},
|
||||
"example.org": {},
|
||||
"invalid": {},
|
||||
"test": {},
|
||||
}
|
||||
@@ -13,8 +13,8 @@ import (
|
||||
"github.com/gabriel-vasile/mimetype"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/service/http/header"
|
||||
"github.com/photoprism/photoprism/pkg/service/http/scheme"
|
||||
"github.com/photoprism/photoprism/pkg/http/header"
|
||||
"github.com/photoprism/photoprism/pkg/http/scheme"
|
||||
)
|
||||
|
||||
// DataUrl generates a data URL of the binary data from the specified io.Reader.
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/service/http/header"
|
||||
"github.com/photoprism/photoprism/pkg/http/header"
|
||||
)
|
||||
|
||||
// ContentType returns a normalized video content type strings based on the video file type and codec.
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/service/http/header"
|
||||
"github.com/photoprism/photoprism/pkg/http/header"
|
||||
)
|
||||
|
||||
func TestContentType(t *testing.T) {
|
||||
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/http/header"
|
||||
"github.com/photoprism/photoprism/pkg/media"
|
||||
"github.com/photoprism/photoprism/pkg/service/http/header"
|
||||
)
|
||||
|
||||
// Info represents video file information.
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/service/http/header"
|
||||
"github.com/photoprism/photoprism/pkg/http/header"
|
||||
)
|
||||
|
||||
func TestInfo(t *testing.T) {
|
||||
|
||||
@@ -10,8 +10,8 @@ import (
|
||||
"github.com/abema/go-mp4"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/http/header"
|
||||
"github.com/photoprism/photoprism/pkg/media"
|
||||
"github.com/photoprism/photoprism/pkg/service/http/header"
|
||||
)
|
||||
|
||||
// ProbeFile returns information for the given filename.
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/http/header"
|
||||
"github.com/photoprism/photoprism/pkg/media"
|
||||
"github.com/photoprism/photoprism/pkg/service/http/header"
|
||||
)
|
||||
|
||||
func TestProbeFile(t *testing.T) {
|
||||
|
||||
@@ -2,7 +2,7 @@ package video
|
||||
|
||||
import (
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/service/http/header"
|
||||
"github.com/photoprism/photoprism/pkg/http/header"
|
||||
)
|
||||
|
||||
// Unknown represents an unknown and/or unsupported video format.
|
||||
|
||||
Reference in New Issue
Block a user