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 }