Commit b331773a authored by ale's avatar ale

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
......@@ -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))
}
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)
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment