diff --git a/cmd/redirectord/redirectord.go b/cmd/redirectord/redirectord.go index 40328916a15178f95f473d38249f7268d789ff77..a1b2024a20740af8f4adc368863df45c116e9de1 100644 --- a/cmd/redirectord/redirectord.go +++ b/cmd/redirectord/redirectord.go @@ -25,6 +25,7 @@ var ( 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)") 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). dnsTtl = 5 @@ -78,9 +79,15 @@ func main() { dnsRed := fe.NewDNSRedirector(client, *domain, *publicIPs, dnsTtl, ns) 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 { 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)) } diff --git a/fe/http.go b/fe/http.go index a37a4fe9bad6475b0ac623fadc643d536e430e93..837aaf1dbb49fc78390ff7f8edf48f5d6c95db99 100644 --- a/fe/http.go +++ b/fe/http.go @@ -1,7 +1,9 @@ package fe import ( + "bufio" "bytes" + "errors" "flag" "fmt" "html/template" @@ -10,6 +12,7 @@ import ( "net" "net/http" "net/url" + "os" "path/filepath" "strconv" "strings" @@ -99,6 +102,7 @@ type HTTPRedirector struct { lb *autoradioLoadBalancer client *autoradio.Client template *template.Template + redirects map[string]string } // NewHTTPRedirector creates a new HTTP redirector. @@ -181,7 +185,7 @@ func (h *HTTPRedirector) withMount(f func(*autoradio.Mount, http.ResponseWriter, mountPath := strings.TrimSuffix(r.URL.Path, ".m3u") mount, err := h.client.GetMount(mountPath) if err != nil { - http.Error(w, "Not Found", http.StatusNotFound) + http.NotFound(w, r) return } f(mount, w, r) @@ -212,20 +216,7 @@ func (h *HTTPRedirector) serveRelay(mount *autoradio.Mount, w http.ResponseWrite if strings.HasSuffix(r.URL.Path, ".m3u") { h.serveM3U(mount, w, r) } else { - targetURL := 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) + sendRedirect(w, r, streamURL(relayAddr, mount.Name)) } } @@ -349,6 +340,8 @@ func (h *HTTPRedirector) createHandler() http.Handler { mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { switch { 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) case r.URL.Path == "" || r.URL.Path == "/": statusPageHandler.ServeHTTP(w, r) @@ -357,8 +350,55 @@ func (h *HTTPRedirector) createHandler() http.Handler { } }) - // Instrument the resulting HTTP handler. - return logHandler(trackRequestsHandler(mux)) + // If a redirect map is present, run the redirect handler in + // 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. @@ -382,3 +422,18 @@ func addDefaultHeaders(w http.ResponseWriter) { w.Header().Set("Expires", "-1") 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) +}