Skip to content
Snippets Groups Projects
Commit b331773a authored by ale's avatar ale
Browse files

implement static redirect map in HTTP front-end

Add an option to load a file containing redirects (source, target paths)
and serve them from the HTTP server, to allow renaming a radio without
breaking old links.
parent 56b0e1d5
Branches
Tags
No related merge requests found
...@@ -25,6 +25,7 @@ var ( ...@@ -25,6 +25,7 @@ var (
templateDir = flag.String("template-dir", "/usr/share/autoradio/htdocs/templates", "HTML templates directory") templateDir = flag.String("template-dir", "/usr/share/autoradio/htdocs/templates", "HTML templates directory")
lbPolicy = flag.String("lb-policy", "listeners_available,listeners_score,weighted", "Load balancing rules specification (see godoc documentation for details)") lbPolicy = flag.String("lb-policy", "listeners_available,listeners_score,weighted", "Load balancing rules specification (see godoc documentation for details)")
nameservers = flag.String("nameservers", "", "Comma-separated list of name servers (not IPs) for the zone specified in --domain") nameservers = flag.String("nameservers", "", "Comma-separated list of name servers (not IPs) for the zone specified in --domain")
redirectMap = flag.String("redirect-map", "", "File containing a list of source path / target redirects, space-separated, one per line")
// Default DNS TTL (seconds). // Default DNS TTL (seconds).
dnsTtl = 5 dnsTtl = 5
...@@ -78,9 +79,15 @@ func main() { ...@@ -78,9 +79,15 @@ func main() {
dnsRed := fe.NewDNSRedirector(client, *domain, *publicIPs, dnsTtl, ns) dnsRed := fe.NewDNSRedirector(client, *domain, *publicIPs, dnsTtl, ns)
dnsRed.Start(fmt.Sprintf(":%d", *dnsPort)) dnsRed.Start(fmt.Sprintf(":%d", *dnsPort))
red, err := fe.NewHTTPRedirector(client, *domain, *lbPolicy, *staticDir, *templateDir) httpRed, err := fe.NewHTTPRedirector(client, *domain, *lbPolicy, *staticDir, *templateDir)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
red.Run(fmt.Sprintf(":%d", *httpPort)) if *redirectMap != "" {
if err := httpRed.LoadStaticRedirects(*redirectMap); err != nil {
// An error loading the redirect map should not be fatal.
log.Printf("Warning: could not load static redirect map: %v", err)
}
}
httpRed.Run(fmt.Sprintf(":%d", *httpPort))
} }
package fe package fe
import ( import (
"bufio"
"bytes" "bytes"
"errors"
"flag" "flag"
"fmt" "fmt"
"html/template" "html/template"
...@@ -10,6 +12,7 @@ import ( ...@@ -10,6 +12,7 @@ import (
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
"os"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
...@@ -99,6 +102,7 @@ type HTTPRedirector struct { ...@@ -99,6 +102,7 @@ type HTTPRedirector struct {
lb *autoradioLoadBalancer lb *autoradioLoadBalancer
client *autoradio.Client client *autoradio.Client
template *template.Template template *template.Template
redirects map[string]string
} }
// NewHTTPRedirector creates a new HTTP redirector. // NewHTTPRedirector creates a new HTTP redirector.
...@@ -181,7 +185,7 @@ func (h *HTTPRedirector) withMount(f func(*autoradio.Mount, http.ResponseWriter, ...@@ -181,7 +185,7 @@ func (h *HTTPRedirector) withMount(f func(*autoradio.Mount, http.ResponseWriter,
mountPath := strings.TrimSuffix(r.URL.Path, ".m3u") mountPath := strings.TrimSuffix(r.URL.Path, ".m3u")
mount, err := h.client.GetMount(mountPath) mount, err := h.client.GetMount(mountPath)
if err != nil { if err != nil {
http.Error(w, "Not Found", http.StatusNotFound) http.NotFound(w, r)
return return
} }
f(mount, w, r) f(mount, w, r)
...@@ -212,20 +216,7 @@ func (h *HTTPRedirector) serveRelay(mount *autoradio.Mount, w http.ResponseWrite ...@@ -212,20 +216,7 @@ func (h *HTTPRedirector) serveRelay(mount *autoradio.Mount, w http.ResponseWrite
if strings.HasSuffix(r.URL.Path, ".m3u") { if strings.HasSuffix(r.URL.Path, ".m3u") {
h.serveM3U(mount, w, r) h.serveM3U(mount, w, r)
} else { } else {
targetURL := streamURL(relayAddr, mount.Name) sendRedirect(w, r, streamURL(relayAddr, mount.Name))
// Firefox apparently caches redirects regardless of
// the status code, so we have to add some quite
// aggressive cache-busting headers. We serve a status
// code of 307 to HTTP/1.1 clients, 302 otherwise.
w.Header().Set("Cache-Control", "max-age=0,no-cache,no-store")
w.Header().Set("Pragma", "no-cache")
w.Header().Set("Expires", "-1")
code := 302
if r.ProtoMinor == 1 {
code = 307
}
http.Redirect(w, r, targetURL, code)
} }
} }
...@@ -349,6 +340,8 @@ func (h *HTTPRedirector) createHandler() http.Handler { ...@@ -349,6 +340,8 @@ func (h *HTTPRedirector) createHandler() http.Handler {
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
switch { switch {
case r.Method == "SOURCE" || r.Method == "PUT": case r.Method == "SOURCE" || r.Method == "PUT":
// Icecast 2.4 started supporting the PUT
// method for sources, along the old SOURCE.
sourceHandler.ServeHTTP(w, r) sourceHandler.ServeHTTP(w, r)
case r.URL.Path == "" || r.URL.Path == "/": case r.URL.Path == "" || r.URL.Path == "/":
statusPageHandler.ServeHTTP(w, r) statusPageHandler.ServeHTTP(w, r)
...@@ -357,8 +350,55 @@ func (h *HTTPRedirector) createHandler() http.Handler { ...@@ -357,8 +350,55 @@ func (h *HTTPRedirector) createHandler() http.Handler {
} }
}) })
// Instrument the resulting HTTP handler. // If a redirect map is present, run the redirect handler in
return logHandler(trackRequestsHandler(mux)) // front of everything.
var rooth http.Handler = mux
if h.redirects != nil {
rooth = h.redirectHandler(rooth)
}
// Instrument the resulting HTTP handler, and map global
// redirects.
return logHandler(trackRequestsHandler(rooth))
}
// Create a handler that will detect requests matching our redirect
// map, and will serve a redirect for them.
func (h *HTTPRedirector) redirectHandler(wrap http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if target, ok := h.redirects[r.URL.Path]; ok {
sendRedirect(w, r, target)
return
}
wrap.ServeHTTP(w, r)
})
}
func parseRedirects(r io.Reader) (map[string]string, error) {
out := make(map[string]string)
s := bufio.NewScanner(r)
for s.Scan() {
line := s.Text()
if line == "" || strings.HasPrefix(line, "#") {
continue
}
fields := strings.Fields(line)
if len(fields) != 2 {
return nil, errors.New("syntax error")
}
out[fields[0]] = fields[1]
}
return out, nil
}
func (h *HTTPRedirector) LoadStaticRedirects(filename string) error {
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close()
h.redirects, err = parseRedirects(f)
return err
} }
// Run starts the HTTP server on the given addr. Does not return. // Run starts the HTTP server on the given addr. Does not return.
...@@ -382,3 +422,18 @@ func addDefaultHeaders(w http.ResponseWriter) { ...@@ -382,3 +422,18 @@ func addDefaultHeaders(w http.ResponseWriter) {
w.Header().Set("Expires", "-1") w.Header().Set("Expires", "-1")
w.Header().Set("Cache-Control", "no-store") w.Header().Set("Cache-Control", "no-store")
} }
func sendRedirect(w http.ResponseWriter, r *http.Request, target string) {
// Firefox apparently caches redirects regardless of
// the status code, so we have to add some quite
// aggressive cache-busting headers. We serve a status
// code of 307 to HTTP/1.1 clients, 302 otherwise.
w.Header().Set("Cache-Control", "max-age=0,no-cache,no-store")
w.Header().Set("Pragma", "no-cache")
w.Header().Set("Expires", "-1")
code := http.StatusFound
if r.ProtoMinor == 1 {
code = http.StatusTemporaryRedirect
}
http.Redirect(w, r, target, code)
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment