4 Commits

Author SHA1 Message Date
fe83bd5b71 fix CORS headers 2025-11-14 19:07:11 +00:00
456a0ce108 Enhance favicon handling with content type detection
- Added a new variable to store the content type for the favicon.
- Updated the favicon loading mechanism to determine the content type based on the file extension.
- Adjusted the default fallback to use an embedded favicon.ico instead of orly.png.
- Modified the favicon handler to set the appropriate content type when serving the favicon.
2025-10-10 10:19:49 +01:00
81fbc9b2a4 remove noisy logs 2025-10-10 10:15:51 +01:00
9baf63915c Update README to include capability setup instructions
- Added instructions for setting the `cap_net_bind_service` capability to allow binding to ports 80 and 443 after compilation.
2025-10-10 10:11:23 +01:00
3 changed files with 49 additions and 12 deletions

1
.gitignore vendored
View File

@@ -87,6 +87,7 @@ node_modules/**
!.gitignore
!version
!out.jsonl
!Dockerfile*
# ...even if they are in subdirectories
!*/
/blocklist.json

View File

@@ -1,3 +1,7 @@
= reverse
simple reverse proxy with letsencrypt, nostr nip-05 and go vanity redirects
simple reverse proxy with letsencrypt, nostr nip-05 and go vanity redirects
after compiling, you need to set the cap_net_bind_service capability to allow it to bind to 80 and 443
setcap 'cap_net_bind_service=+ep' /path/to/reverse

54
main.go
View File

@@ -41,6 +41,9 @@ var orlyFavicon []byte
// faviconData holds the favicon data to serve
var faviconData []byte
// faviconContentType holds the content type for the favicon
var faviconContentType string
func main() {
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()
@@ -60,7 +63,7 @@ type C struct {
WTo time.Duration `env:"REVERSE_WTO" usage:"maximum duration before timing out write of the response" default:"5m"`
Idle time.Duration `env:"REVERSE_IDLE" usage:"how long idle connection is kept before closing (set rto, wto to 0 to use this)"`
Certs []string `env:"REVERSE_CERTS" usage:"certificates and the domain they match: eg: mleku.dev:/path/to/cert - this will indicate to load two, one with extension .key and one with .crt, each expected to be PEM encoded TLS private and public keys, respectively"`
Favicon string `env:"REVERSE_FAVICON" usage:"path to custom favicon file to serve as favicon.ico (overrides default orly.png)"`
Favicon string `env:"REVERSE_FAVICON" usage:"path to custom favicon file to serve as favicon.ico (overrides default favicon.ico)"`
}
// GetEnv checks if the first command line argument is "env" and returns
@@ -475,6 +478,14 @@ func setProxy(mapping map[string]string) (http.Handler, error) {
rp := newSingleHostReverseProxy(u)
rp.ErrorLog = log2.New(io.Discard, "", 0)
rp.BufferPool = bufPool{}
rp.ModifyResponse = func(resp *http.Response) error {
// Add CORS headers to all proxied responses
resp.Header.Set("Access-Control-Allow-Origin", "*")
resp.Header.Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
resp.Header.Set("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization, Upgrade, Connection, Sec-WebSocket-Key, Sec-WebSocket-Version, Sec-WebSocket-Protocol, Sec-WebSocket-Extensions")
resp.Header.Set("Access-Control-Max-Age", "86400")
return nil
}
mux.Handle(hn+"/", rp)
addFaviconHandler(hn, mux)
continue
@@ -488,17 +499,23 @@ func setProxy(mapping map[string]string) (http.Handler, error) {
req.Header.Set("X-Forwarded-Proto", "https")
clientIP := remoteIP(req.RemoteAddr)
split := strings.Split(clientIP, ",")
log.I.S(split)
req.Header.Set("X-Forwarded-For", split[0])
// Standard RFC 7239 Forwarded header
appendForwardedHeader(req)
log.I.S(req.Header)
},
Transport: &http.Transport{
Dial: func(netw, addr string) (net.Conn, error) {
return net.DialTimeout(network, ba, 5*time.Second)
},
},
ModifyResponse: func(resp *http.Response) error {
// Add CORS headers to all proxied responses
resp.Header.Set("Access-Control-Allow-Origin", "*")
resp.Header.Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
resp.Header.Set("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization, Upgrade, Connection, Sec-WebSocket-Key, Sec-WebSocket-Version, Sec-WebSocket-Protocol, Sec-WebSocket-Extensions")
resp.Header.Set("Access-Control-Max-Age", "86400")
return nil
},
ErrorLog: log2.New(os.Stderr, "reverse", 0),
BufferPool: bufPool{},
}
@@ -654,11 +671,9 @@ func newSingleHostReverseProxy(target *url.URL) *httputil.ReverseProxy {
req.Header.Set("X-Forwarded-Proto", "https")
clientIP := remoteIP(req.RemoteAddr)
split := strings.Split(clientIP, ",")
log.I.S(split)
req.Header.Set("X-Forwarded-For", split[0])
// Standard RFC 7239 Forwarded header
appendForwardedHeader(req)
log.I.S(req.Header)
}
return &httputil.ReverseProxy{Director: director}
}
@@ -807,7 +822,6 @@ type NostrJSON struct {
// - The handler serves the parsed Nostr DNS data with appropriate HTTP headers
// set for CORS and content type.
func NostrDNS(hn, ba string, mux *http.ServeMux) (err error) {
log.T.Ln(hn, ba)
var fb []byte
if fb, err = os.ReadFile(ba); chk.E(err) {
return
@@ -849,7 +863,6 @@ func NostrDNS(hn, ba string, mux *http.ServeMux) (err error) {
mux.HandleFunc(
hn+"/favicon.ico",
func(writer http.ResponseWriter, request *http.Request) {
log.I.F("favicon")
if _, err = writer.Write(fi); chk.E(err) {
return
}
@@ -861,8 +874,9 @@ func NostrDNS(hn, ba string, mux *http.ServeMux) (err error) {
// loadFavicon loads the favicon data from the configured file or uses the default
func loadFavicon(faviconPath string) error {
if faviconPath == "" {
// Use default orly.png
faviconData = orlyFavicon
// Use default embedded favicon.ico
faviconData = defaultFavicon
faviconContentType = "image/x-icon"
return nil
}
@@ -876,18 +890,36 @@ func loadFavicon(faviconPath string) error {
}
faviconData = data
// Determine content type based on file extension
ext := strings.ToLower(filepath.Ext(faviconPath))
switch ext {
case ".ico":
faviconContentType = "image/x-icon"
case ".png":
faviconContentType = "image/png"
case ".gif":
faviconContentType = "image/gif"
case ".jpg", ".jpeg":
faviconContentType = "image/jpeg"
case ".svg":
faviconContentType = "image/svg+xml"
default:
faviconContentType = "image/x-icon" // default fallback
}
log.I.Ln("loaded custom favicon from", faviconPath)
return nil
}
// addFaviconHandler adds a favicon handler for the given hostname that serves
// the configured favicon as favicon.ico
// the configured favicon (default favicon.ico or custom file) as favicon.ico
func addFaviconHandler(hostname string, mux *http.ServeMux) {
mux.HandleFunc(
hostname+"/favicon.ico",
func(w http.ResponseWriter, r *http.Request) {
log.I.Ln("serving favicon for", hostname)
w.Header().Set("Content-Type", "image/png")
w.Header().Set("Content-Type", faviconContentType)
w.Header().Set("Content-Length", fmt.Sprint(len(faviconData)))
w.Header().Set("Cache-Control", "public, max-age=31536000") // 1 year cache
if _, err := w.Write(faviconData); err != nil {