config.go 8.11 KiB
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
}