package icecast import ( "bytes" "encoding/xml" "flag" "io" "log" "net" "net/url" "os" "path/filepath" "git.autistici.org/ale/autoradio" pb "git.autistici.org/ale/autoradio/proto" ) var ( // Icecast tunables. icecastQueueSize = flag.Int("icecast-queue-size", 1<<20, "Icecast queue size (bytes)") icecastClientTimeout = flag.Int("icecast-client-timeout", 30, "Icecast client timeout (s)") icecastHeaderTimeout = flag.Int("icecast-header-timeout", 15, "Icecast header timeout (s)") icecastSourceTimeout = flag.Int("icecast-source-timeout", 60, "Icecast source timeout (s)") icecastBurstSize = flag.Int("icecast-burst-size", 131072, "Icecast connection burst size (bytes)") preamble = "<!-- Automatically generated, do not edit -->\n\n" ) type iceLimitsConfig struct { Clients int `xml:"clients"` Sources int `xml:"sources"` QueueSize int `xml:"queue-size"` ClientTimeout int `xml:"client-timeout"` HeaderTimeout int `xml:"header-timeout"` SourceTimeout int `xml:"source-timeout"` BurstSize int `xml:"burst-size"` } type iceAuthenticationConfig struct { SourcePassword string `xml:"source-password"` AdminUser string `xml:"admin-user"` AdminPassword string `xml:"admin-password"` } type iceListenConfig struct { BindAddress string `xml:"bind-address"` Port int `xml:"port"` ShoutcastCompat int `xml:"shoutcast-compat"` } type icePathsConfig struct { Basedir string `xml:"basedir"` Logdir string `xml:"logdir"` Webroot string `xml:"webroot"` Adminroot string `xml:"adminroot"` } type iceLoggingConfig struct { Accesslog string `xml:"accesslog"` Errorlog string `xml:"errorlog"` Loglevel int `xml:"loglevel"` Logsize int `xml:"logsize"` } type iceSecurityConfig struct { Chroot int `xml:"chroot"` } type iceRelayConfig struct { Server string `xml:"server"` Port string `xml:"port"` Mount string `xml:"mount"` LocalMount string `xml:"local-mount"` Username string `xml:"username"` Password string `xml:"password"` OnDemand int `xml:"on-demand"` RelayShoutcastMetadata int `xml:"relay-shoutcast-metadata"` } type iceMountConfig struct { Name string `xml:"mount-name"` Username string `xml:"username"` Password string `xml:"password"` FallbackMount string `xml:"fallback-mount,omitempty"` FallbackOverride int `xml:"fallback-override,omitempty"` Hidden int `xml:"hidden"` OnConnect string `xml:"on-connect,omitempty"` OnDisconnect string `xml:"on-disconnect,omitempty"` // Public int `xml:"no-yp"` // MaxListeners int `xml:"max-listeners"` } // Configuration of the local Icecast daemon. This is a write-only // object, meant for serialization to XML. We keep around a single // copy of it and just update Relays and Mounts every time. type icecastConfig struct { XMLName xml.Name Limits iceLimitsConfig `xml:"limits"` Auth iceAuthenticationConfig `xml:"authentication"` Hostname string `xml:"hostname"` Fileserve int `xml:"fileserve"` Paths icePathsConfig `xml:"paths"` Logging iceLoggingConfig `xml:"logging"` Security iceSecurityConfig `xml:"security"` Listen []iceListenConfig `xml:"listen-socket"` Relays []*iceRelayConfig `xml:"relay"` Mounts []*iceMountConfig `xml:"mount"` } var ( randomSourcePassword string hostname = "unknown" ) func init() { randomSourcePassword = autoradio.GeneratePassword() if h, err := os.Hostname(); err == nil { hostname = h } } // Create an Icecast configuration suitable for a Debian-based system // install of the 'icecast2' package. Things to note about the // generated config: // // - It binds to the IcecastPort (defined in api.go) on all // interfaces. // // - A random admin password is generated once on each node, and saved // to a file for persistence. It is not really meant to be used by the // operator. // // - We don't use the global source password, but icecast is happier // if it's set, so we just use a random password every time. // func newIcecastConfig(mounts []*pb.Mount, port, maxClients int, adminPw string, isMaster bool, masterAddr string) *icecastConfig { c := icecastConfig{ XMLName: xml.Name{Local: "icecast"}, Limits: iceLimitsConfig{ Clients: maxClients, Sources: maxClients / 2, QueueSize: *icecastQueueSize, ClientTimeout: *icecastClientTimeout, HeaderTimeout: *icecastHeaderTimeout, SourceTimeout: *icecastSourceTimeout, BurstSize: *icecastBurstSize, }, Auth: iceAuthenticationConfig{ AdminUser: "admin", AdminPassword: adminPw, SourcePassword: randomSourcePassword, }, Hostname: hostname, Fileserve: 1, Paths: icePathsConfig{ Basedir: "/usr/share/icecast2", Logdir: "/var/log/icecast2", Webroot: "/usr/share/icecast2/web", Adminroot: "/usr/share/icecast2/admin", }, Logging: iceLoggingConfig{ Accesslog: "access.log", Errorlog: "error.log", Loglevel: 3, Logsize: 10000, }, Security: iceSecurityConfig{0}, Listen: []iceListenConfig{ {"0.0.0.0", port, 0}, }, } for _, m := range mounts { switch { case m.IsRelay(): r, err := relayConfig(m) if err != nil { log.Printf("error in relay config for %s: %v", m.Path, err) } else { c.Relays = append(c.Relays, r) } case isMaster: c.Mounts = append(c.Mounts, masterMountConfig(m)) default: r, err := slaveMountConfig(m, masterAddr) if err != nil { log.Printf("error in slave config for %s: %v", m.Path, err) } else { c.Relays = append(c.Relays, r) } } } return &c } // Encode the configuration to XML. func (c *icecastConfig) Encode() ([]byte, error) { var buf bytes.Buffer output, err := xml.MarshalIndent(c, "", " ") if err != nil { return nil, err } if _, err := io.WriteString(&buf, preamble); err != nil { return nil, err } if _, err := buf.Write(output); err != nil { return nil, err } return buf.Bytes(), nil } func relayConfig(m *pb.Mount) (*iceRelayConfig, error) { u, err := url.Parse(m.RelayUrl) if err != nil { // A failure here is almost invisible and not very // useful, but at least we can prevent garbling the // resulting icecast config. log.Printf("can't parse relay url for %s: %s", m.Path, err) return nil, err } host, port, err := net.SplitHostPort(u.Host) if err != nil { host = u.Host port = "80" } rc := iceRelayConfig{ Mount: u.Path, LocalMount: autoradio.MountPathToIcecastPath(m.Path), Server: host, Port: port, OnDemand: 1, RelayShoutcastMetadata: 1, } if u.User != nil { rc.Username = u.User.Username() if p, ok := u.User.Password(); ok { rc.Password = p } } return &rc, nil } func slaveMountConfig(m *pb.Mount, masterAddr string) (*iceRelayConfig, error) { masterHost, masterPort, err := net.SplitHostPort(masterAddr) if err != nil { return nil, err } path := autoradio.MountPathToIcecastPath(m.Path) return &iceRelayConfig{ Mount: path, LocalMount: path, Server: masterHost, Port: masterPort, Username: m.SourceUsername, Password: m.SourcePassword, OnDemand: 1, RelayShoutcastMetadata: 1, }, nil } func masterMountConfig(m *pb.Mount) *iceMountConfig { mc := iceMountConfig{ Name: autoradio.MountPathToIcecastPath(m.Path), Username: m.SourceUsername, Password: m.SourcePassword, FallbackOverride: 1, // MaxListeners: 1000, // NoYp: 1, } // When no explicit fallback URL is specified, use the locally // installed silence audo file. In order to serve the right // format, we guess the file extension for the silence // fallback by looking at the extension of the mount itself. switch { case m.FallbackPath != "": mc.FallbackMount = m.FallbackPath case filepath.Ext(m.Path) == ".ogg": mc.FallbackMount = "/silence.ogg" default: mc.FallbackMount = "/silence.mp3" } return &mc }