Skip to content
Snippets Groups Projects
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
}