config.go 8.11 KB
Newer Older
ale's avatar
ale committed
1
package icecast
2 3 4 5

import (
	"bytes"
	"encoding/xml"
6
	"flag"
7
	"io"
8 9 10
	"log"
	"net"
	"net/url"
ale's avatar
ale committed
11
	"os"
12
	"path/filepath"
ale's avatar
ale committed
13

ale's avatar
ale committed
14
	"git.autistici.org/ale/autoradio"
ale's avatar
ale committed
15
	pb "git.autistici.org/ale/autoradio/proto"
16 17 18
)

var (
19 20 21 22 23 24
	// 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)")
25

ale's avatar
ale committed
26 27
	preamble = "<!-- Automatically generated, do not edit -->\n\n"
)
28

29
type iceLimitsConfig struct {
30 31
	Clients       int `xml:"clients"`
	Sources       int `xml:"sources"`
32 33 34 35
	QueueSize     int `xml:"queue-size"`
	ClientTimeout int `xml:"client-timeout"`
	HeaderTimeout int `xml:"header-timeout"`
	SourceTimeout int `xml:"source-timeout"`
36
	BurstSize     int `xml:"burst-size"`
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
}

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"`
ale's avatar
ale committed
71
	Port                   string `xml:"port"`
72 73 74 75 76 77 78 79 80
	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 {
81 82 83
	Name             string `xml:"mount-name"`
	Username         string `xml:"username"`
	Password         string `xml:"password"`
84 85 86
	FallbackMount    string `xml:"fallback-mount,omitempty"`
	FallbackOverride int    `xml:"fallback-override,omitempty"`
	Hidden           int    `xml:"hidden"`
87 88 89 90
	OnConnect        string `xml:"on-connect,omitempty"`
	OnDisconnect     string `xml:"on-disconnect,omitempty"`
	// Public           int    `xml:"no-yp"`
	// MaxListeners     int    `xml:"max-listeners"`
91 92
}

ale's avatar
ale committed
93 94 95
// 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.
96
type icecastConfig struct {
97 98 99 100 101 102 103 104 105
	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"`
ale's avatar
ale committed
106 107 108 109 110 111 112 113 114 115 116 117 118 119
	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
	}
120 121
}

ale's avatar
ale committed
122 123 124 125 126 127 128
// 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.
//
ale's avatar
ale committed
129 130 131
// - 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.
ale's avatar
ale committed
132
//
ale's avatar
ale committed
133 134 135 136 137
// - 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{
ale's avatar
ale committed
138
		XMLName: xml.Name{Local: "icecast"},
139
		Limits: iceLimitsConfig{
140 141 142 143 144 145 146
			Clients:       maxClients,
			Sources:       maxClients / 2,
			QueueSize:     *icecastQueueSize,
			ClientTimeout: *icecastClientTimeout,
			HeaderTimeout: *icecastHeaderTimeout,
			SourceTimeout: *icecastSourceTimeout,
			BurstSize:     *icecastBurstSize,
147 148 149 150
		},
		Auth: iceAuthenticationConfig{
			AdminUser:      "admin",
			AdminPassword:  adminPw,
ale's avatar
ale committed
151
			SourcePassword: randomSourcePassword,
152
		},
ale's avatar
ale committed
153
		Hostname:  hostname,
154 155 156 157 158 159 160 161 162 163 164 165 166 167 168
		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{
ale's avatar
ale committed
169
			{"0.0.0.0", port, 0},
170 171
		},
	}
ale's avatar
ale committed
172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194

	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
195 196
}

ale's avatar
ale committed
197
// Encode the configuration to XML.
198
func (c *icecastConfig) Encode() ([]byte, error) {
199 200 201 202 203 204
	var buf bytes.Buffer

	output, err := xml.MarshalIndent(c, "", "  ")
	if err != nil {
		return nil, err
	}
ale's avatar
ale committed
205 206 207
	if _, err := io.WriteString(&buf, preamble); err != nil {
		return nil, err
	}
208 209 210 211 212 213
	if _, err := buf.Write(output); err != nil {
		return nil, err
	}
	return buf.Bytes(), nil
}

ale's avatar
ale committed
214
func relayConfig(m *pb.Mount) (*iceRelayConfig, error) {
215 216 217 218 219
	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.
ale's avatar
ale committed
220 221
		log.Printf("can't parse relay url for %s: %s", m.Path, err)
		return nil, err
222
	}
ale's avatar
ale committed
223
	host, port, err := net.SplitHostPort(u.Host)
224
	if err != nil {
ale's avatar
ale committed
225
		host = u.Host
226 227 228 229 230
		port = "80"
	}

	rc := iceRelayConfig{
		Mount:                  u.Path,
ale's avatar
ale committed
231 232 233
		LocalMount:             autoradio.MountPathToIcecastPath(m.Path),
		Server:                 host,
		Port:                   port,
234 235 236 237 238 239 240 241 242
		OnDemand:               1,
		RelayShoutcastMetadata: 1,
	}
	if u.User != nil {
		rc.Username = u.User.Username()
		if p, ok := u.User.Password(); ok {
			rc.Password = p
		}
	}
ale's avatar
ale committed
243
	return &rc, nil
244 245
}

ale's avatar
ale committed
246 247 248 249 250 251 252
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{
ale's avatar
ale committed
253 254
		Mount:                  path,
		LocalMount:             path,
ale's avatar
ale committed
255 256 257 258
		Server:                 masterHost,
		Port:                   masterPort,
		Username:               m.SourceUsername,
		Password:               m.SourcePassword,
259 260
		OnDemand:               1,
		RelayShoutcastMetadata: 1,
ale's avatar
ale committed
261
	}, nil
262 263
}

ale's avatar
ale committed
264 265
func masterMountConfig(m *pb.Mount) *iceMountConfig {
	mc := iceMountConfig{
266 267 268 269
		Name:             autoradio.MountPathToIcecastPath(m.Path),
		Username:         m.SourceUsername,
		Password:         m.SourcePassword,
		FallbackOverride: 1,
ale's avatar
ale committed
270 271
		// MaxListeners: 1000,
		// NoYp:   1,
272
	}
273 274 275 276 277 278 279

	// 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 != "":
ale's avatar
ale committed
280
		mc.FallbackMount = m.FallbackPath
281 282 283 284
	case filepath.Ext(m.Path) == ".ogg":
		mc.FallbackMount = "/silence.ogg"
	default:
		mc.FallbackMount = "/silence.mp3"
ale's avatar
ale committed
285
	}
286

ale's avatar
ale committed
287
	return &mc
288
}