utils/web.go
Rama main · réplica de lectura del repositorio original.
package utils
import (
"net"
"net/http"
"path"
"runtime"
"strings"
"sync"
"time"
)
// The IP addresses of this machine are cached for this long, since looking
// them up for every request is wasteful, but they may change over time
const localIPCacheDuration = time.Minute
var (
localIPMut sync.Mutex
localIP map[string]bool
localIPUpdated time.Time
)
// CanonicalURLPath resolves ".." and "." elements, collapses repeated slashes
// and adds a leading slash. A trailing slash is kept, since it tells
// directories and files apart.
func CanonicalURLPath(p string) string {
if runtime.GOOS == "windows" {
// Backslash is a path separator here, and must not survive cleaning
p = strings.ReplaceAll(p, "\\", "/")
}
cleaned := path.Clean("/" + p)
if cleaned != "/" && strings.HasSuffix(p, "/") {
cleaned += "/"
}
return cleaned
}
// GetHost returns the host of a request, handling both IPv4 and IPv6.
// Unlike GetDomain, it does not collapse loopback addresses, so URLs built
// from it stay same-origin with the request.
func GetHost(req *http.Request) string {
host, _, err := net.SplitHostPort(req.Host)
if err != nil {
// No port in host, use as-is
return req.Host
}
return host
}
// IsLocalIP checks if the given host is a loopback address or one of the IP
// addresses of this machine
func IsLocalIP(host string) bool {
ip := net.ParseIP(host)
if ip == nil {
return false
}
if ip.IsLoopback() {
return true
}
localIPMut.Lock()
defer localIPMut.Unlock()
if localIP == nil || time.Since(localIPUpdated) > localIPCacheDuration {
// If the addresses can not be listed, cache an empty set, so that
// a persistent failure does not mean one lookup per request
addrs, _ := net.InterfaceAddrs()
localIP = make(map[string]bool, len(addrs))
for _, addr := range addrs {
if ipNet, ok := addr.(*net.IPNet); ok {
localIP[ipNet.IP.String()] = true
}
}
localIPUpdated = time.Now()
}
return localIP[ip.String()]
}
// GetDomain returns the host/domain of a request, handling both IPv4 and IPv6.
// The loopback hosts 127.0.0.1 and ::1, and also the IP addresses of this
// machine, are collapsed to "localhost", so that a single localhost/ document
// root serves them all.
func GetDomain(req *http.Request) string {
host := GetHost(req)
if IsLocalIP(host) {
return "localhost"
}
return host
}
// JoinHostPort combines a host and a colonPort (like ":8080") into an address.
// Handles IPv6 by adding brackets as needed. If host is empty, returns colonPort as-is.
func JoinHostPort(host, colonPort string) string {
if host == "" {
return colonPort
}
_, port, err := net.SplitHostPort(colonPort)
if err != nil {
// colonPort might just be a port number without colon
port = colonPort
}
return net.JoinHostPort(host, port)
}
// IsIPv6 checks if the given string is an IPv6 address
func IsIPv6(host string) bool {
ip := net.ParseIP(host)
return ip != nil && ip.To4() == nil
}
// HostPortToURL converts an address to a URL-friendly host:port string.
// Converts ":8080" to "localhost:8080" and ensures IPv6 addresses have brackets.
func HostPortToURL(addr string) string {
host, port, err := net.SplitHostPort(addr)
if err != nil {
// No port, check if it's an IPv6 that needs brackets
if IsIPv6(addr) {
return "[" + addr + "]"
}
return addr
}
if host == "" {
host = "localhost"
}
return net.JoinHostPort(host, port)
}