github.com/ssetin/penguincast@v0.2.0/src/server/mount.go (about)

     1  // Package iceserver - icecast streaming server
     2  package iceserver
     3  
     4  import (
     5  	"bufio"
     6  	"encoding/base64"
     7  	"errors"
     8  	"fmt"
     9  	"io/ioutil"
    10  	"math"
    11  	"net/http"
    12  	"os"
    13  	"regexp"
    14  	"strconv"
    15  	"strings"
    16  	"sync"
    17  	"sync/atomic"
    18  	"time"
    19  
    20  	"golang.org/x/net/html/charset"
    21  	"golang.org/x/text/transform"
    22  )
    23  
    24  // MetaData ...
    25  type MetaData struct {
    26  	MetaInt      int
    27  	StreamTitle  string
    28  	meta         []byte
    29  	metaSizeByte int
    30  }
    31  
    32  // MountInfo ...
    33  type MountInfo struct {
    34  	Name      string
    35  	Listeners int32
    36  	UpTime    string
    37  	Buff      BufferInfo
    38  }
    39  
    40  // Mount ...
    41  type Mount struct {
    42  	Name         string `json:"Name"`
    43  	User         string `json:"User"`
    44  	Password     string `json:"Password"`
    45  	Description  string `json:"Description"`
    46  	BitRate      int    `json:"BitRate"`
    47  	ContentType  string `json:"ContentType"`
    48  	StreamURL    string `json:"StreamURL"`
    49  	Genre        string `json:"Genre"`
    50  	BurstSize    int    `json:"BurstSize"`
    51  	DumpFile     string `json:"DumpFile"`
    52  	MaxListeners int    `json:"MaxListeners"`
    53  
    54  	State struct {
    55  		Started     bool
    56  		StartedTime time.Time
    57  		MetaInfo    MetaData
    58  		Listeners   int32
    59  	} `json:"-"`
    60  
    61  	mux      sync.Mutex
    62  	Server   *IceServer `json:"-"`
    63  	buffer   BufferQueue
    64  	dumpFile *os.File
    65  }
    66  
    67  //Init ...
    68  func (m *Mount) Init(srv *IceServer) error {
    69  	m.Server = srv
    70  	m.Clear()
    71  	m.State.MetaInfo.MetaInt = m.BitRate * 1024 / 8 * 10
    72  
    73  	pool := m.Server.poolManager.Init(m.BitRate * 1024 / 8)
    74  	m.buffer.Init(m.BurstSize/(m.BitRate*1024/8)+2, pool)
    75  
    76  	if m.DumpFile > "" {
    77  		var err error
    78  		m.dumpFile, err = os.OpenFile(srv.Props.Paths.Log+m.DumpFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666)
    79  		if err != nil {
    80  			return err
    81  		}
    82  	}
    83  
    84  	return nil
    85  }
    86  
    87  //Close ...
    88  func (m *Mount) Close() {
    89  	if m.dumpFile != nil {
    90  		m.dumpFile.Close()
    91  	}
    92  }
    93  
    94  //Clear ...
    95  func (m *Mount) Clear() {
    96  	m.mux.Lock()
    97  	defer m.mux.Unlock()
    98  	m.State.Started = false
    99  	m.State.StartedTime = time.Time{}
   100  	m.zeroListeners()
   101  	m.State.MetaInfo.StreamTitle = ""
   102  	m.StreamURL = "http://" + m.Server.Props.Host + ":" + strconv.Itoa(m.Server.Props.Socket.Port) + "/" + m.Name
   103  }
   104  
   105  func (m *Mount) incListeners() {
   106  	atomic.AddInt32(&m.State.Listeners, 1)
   107  }
   108  
   109  func (m *Mount) decListeners() {
   110  	if atomic.LoadInt32(&m.State.Listeners) > 0 {
   111  		atomic.AddInt32(&m.State.Listeners, -1)
   112  	}
   113  }
   114  
   115  func (m *Mount) zeroListeners() {
   116  	atomic.StoreInt32(&m.State.Listeners, 0)
   117  }
   118  
   119  func (m *Mount) auth(w http.ResponseWriter, r *http.Request) error {
   120  	strAuth := r.Header.Get("authorization")
   121  
   122  	if strAuth == "" {
   123  		m.saySourceHello(w, r)
   124  		return errors.New("No authorization field")
   125  	}
   126  
   127  	s := strings.SplitN(strAuth, " ", 2)
   128  	if len(s) != 2 {
   129  		http.Error(w, "Not authorized", 401)
   130  		return errors.New("Not authorized")
   131  	}
   132  
   133  	b, err := base64.StdEncoding.DecodeString(s[1])
   134  	if err != nil {
   135  		http.Error(w, err.Error(), 401)
   136  		return err
   137  	}
   138  
   139  	pair := strings.SplitN(string(b), ":", 2)
   140  	if len(pair) != 2 {
   141  		http.Error(w, "Not authorized", 401)
   142  		return errors.New("Not authorized")
   143  	}
   144  
   145  	if m.Password != pair[1] && m.User != pair[0] {
   146  		http.Error(w, "Not authorized", 401)
   147  		return errors.New("Wrong user or password")
   148  	}
   149  
   150  	m.saySourceHello(w, r)
   151  
   152  	return nil
   153  }
   154  
   155  func (m *Mount) getParams(paramstr string) map[string]string {
   156  	var rex = regexp.MustCompile("(\\w+)=(\\w+)")
   157  	data := rex.FindAllStringSubmatch(paramstr, -1)
   158  	params := make(map[string]string)
   159  	for _, kv := range data {
   160  		k := kv[1]
   161  		v := kv[2]
   162  		params[k] = v
   163  	}
   164  	return params
   165  }
   166  
   167  func (m *Mount) sayListenerHello(w *bufio.ReadWriter, icymeta bool) {
   168  	w.WriteString("HTTP/1.0 200 OK\r\n")
   169  	w.WriteString("Server: ")
   170  	w.WriteString(m.Server.serverName)
   171  	w.WriteString(" ")
   172  	w.WriteString(m.Server.version)
   173  	w.WriteString("\r\nContent-Type: ")
   174  	w.WriteString(m.ContentType)
   175  	w.WriteString("\r\nConnection: Keep-Alive\r\n")
   176  	w.WriteString("X-Audiocast-Bitrate: ")
   177  	w.WriteString(strconv.Itoa(m.BitRate))
   178  	w.WriteString("\r\nX-Audiocast-Name: ")
   179  	w.WriteString(m.Name)
   180  	w.WriteString("\r\nX-Audiocast-Genre: ")
   181  	w.WriteString(m.Genre)
   182  	w.WriteString("\r\nX-Audiocast-Url: ")
   183  	w.WriteString(m.StreamURL)
   184  	w.WriteString("\r\nX-Audiocast-Public: 0\r\n")
   185  	w.WriteString("X-Audiocast-Description: ")
   186  	w.WriteString(m.Description)
   187  	w.WriteString("\r\n")
   188  	if icymeta {
   189  		w.WriteString("Icy-Metaint: ")
   190  		w.WriteString(strconv.Itoa(m.State.MetaInfo.MetaInt))
   191  		w.WriteString("\r\n")
   192  	}
   193  	w.WriteString("\r\n")
   194  	w.Flush()
   195  }
   196  
   197  func (m *Mount) saySourceHello(w http.ResponseWriter, r *http.Request) {
   198  	w.Header().Set("Server", m.Server.serverName+"/"+m.Server.version)
   199  	w.Header().Set("Connection", "Keep-Alive")
   200  	w.Header().Set("Allow", "GET, SOURCE")
   201  	w.Header().Set("Cache-Control", "no-cache")
   202  	w.Header().Set("Pragma", "no-cache")
   203  	w.Header().Set("Access-Control-Allow-Origin", "*")
   204  	w.Header().Set("Transfer-Encoding", "chunked")
   205  	w.WriteHeader(http.StatusOK)
   206  
   207  	flusher, _ := w.(http.Flusher)
   208  	flusher.Flush()
   209  }
   210  
   211  func (m *Mount) writeICEHeaders(w http.ResponseWriter, r *http.Request) {
   212  	var bitratestr string
   213  	bitratestr = r.Header.Get("ice-bitrate")
   214  	if bitratestr == "" {
   215  		audioinfo := r.Header.Get("ice-audio-info")
   216  		if len(audioinfo) > 3 {
   217  			params := m.getParams(audioinfo)
   218  			bitratestr = params["bitrate"]
   219  		}
   220  	}
   221  
   222  	brate, err := strconv.Atoi(bitratestr)
   223  	if err != nil {
   224  		m.BitRate = 0
   225  	} else {
   226  		m.BitRate = brate
   227  	}
   228  
   229  	m.Genre = r.Header.Get("ice-genre")
   230  	m.ContentType = r.Header.Get("content-type")
   231  	m.Description = r.Header.Get("ice-description")
   232  }
   233  
   234  func (m *Mount) updateMeta(w http.ResponseWriter, r *http.Request) {
   235  	err := m.auth(w, r)
   236  	if err != nil {
   237  		return
   238  	}
   239  
   240  	var metaSize byte
   241  	var mstr string
   242  	song := r.URL.Query().Get("song")
   243  	songReader := strings.NewReader(song)
   244  	enc, _, _ := charset.DetermineEncoding(([]byte)(song), "")
   245  	utf8Reader := transform.NewReader(songReader, enc.NewDecoder())
   246  	result, err := ioutil.ReadAll(utf8Reader)
   247  
   248  	if err != nil {
   249  		m.mux.Lock()
   250  		m.State.MetaInfo.StreamTitle = ""
   251  		m.mux.Unlock()
   252  		return
   253  	}
   254  
   255  	m.mux.Lock()
   256  	m.State.MetaInfo.StreamTitle = string(result[:])
   257  
   258  	if m.State.MetaInfo.StreamTitle > "" {
   259  		mstr = "StreamTitle='" + m.State.MetaInfo.StreamTitle + "';"
   260  	} else {
   261  		mstr += "StreamTitle='" + m.Description + "';"
   262  	}
   263  
   264  	metaSize = byte(math.Ceil(float64(len(mstr)) / 16.0))
   265  	m.State.MetaInfo.metaSizeByte = int(metaSize)*16 + 1
   266  	m.State.MetaInfo.meta = make([]byte, m.State.MetaInfo.metaSizeByte)
   267  	m.State.MetaInfo.meta[0] = metaSize
   268  
   269  	for idx := 0; idx < len(mstr); idx++ {
   270  		m.State.MetaInfo.meta[idx+1] = mstr[idx]
   271  	}
   272  	m.mux.Unlock()
   273  }
   274  
   275  func fmtDuration(d time.Duration) string {
   276  	d = d.Round(time.Second)
   277  	h := d / time.Hour
   278  	d -= h * time.Hour
   279  	m := d / time.Minute
   280  	d -= m * time.Minute
   281  	s := int(d.Seconds())
   282  	return fmt.Sprintf("%02d:%02d:%02d", h, m, s)
   283  }
   284  
   285  func (m *Mount) getMountsInfo() MountInfo {
   286  	var t MountInfo
   287  	t.Listeners = atomic.LoadInt32(&m.State.Listeners)
   288  	m.mux.Lock()
   289  	t.Name = m.Name
   290  	if m.State.Started {
   291  		t.UpTime = fmtDuration(time.Since(m.State.StartedTime))
   292  		t.Buff = m.buffer.Info()
   293  	}
   294  	m.mux.Unlock()
   295  	return t
   296  }
   297  
   298  // icy style metadata
   299  func (m *Mount) getIcyMeta() ([]byte, int) {
   300  	m.mux.Lock()
   301  	defer m.mux.Unlock()
   302  	return m.State.MetaInfo.meta, m.State.MetaInfo.metaSizeByte
   303  }