/* * DudelDu * * Copyright 2016 Matthias Ladkau. All rights reserved. * * This Source Code Form is subject to the terms of the MIT * License, If a copy of the MIT License was not distributed with this * file, You can obtain one at https://opensource.org/licenses/MIT. */ /* Package dudeldu is a simple audio streaming server using the SHOUTcast protocol. Server Server is the main server object which runs a shoutcast server instance. Using a WaitGroup a client can wait for the start and shutdown of the server. Incoming new connections are served with a ConnectionHandler method. The default implementation for this is the HandleRequest method of the DefaultRequestHandler object. DefaultRequestHandler DefaultRequestHandler is the default request handler implementation for the DudelDu server. DefaultRequestHandler has a customizable ServeRequest function. ServeRequest is called once a request was successfully decoded. The default implementation supports sending meta data while streaming audio. The metadata implementation is according to: http://www.smackfu.com/stuff/programming/shoutcast.html Playlists Playlists provide the data which is send to the client. A simple implementation will just read .mp3 files and send them in chunks (via the Frame() method) to the client. A request handler uses a PlaylistFactory to produce a Playlist for each new connection. */ package dudeldu import ( "encoding/base64" "regexp" ) /* requestAuthPattern is the pattern which is used to extract the request authentication (i case-insensitive / m multi-line mode: ^ and $ match begin/end line) */ var requestAuthPattern = regexp.MustCompile("(?im)^Authorization: Basic (\\S+).*$") /* checkAuth checks the authentication header of a client request. */ func (drh *DefaultRequestHandler) checkAuth(bufStr string, clientString string) (string, string, bool) { auth := "" res := requestAuthPattern.FindStringSubmatch(bufStr) origBufStr, hasAuth := drh.authPeers.Get(clientString) if len(res) > 1 { // Decode authentication b, err := base64.StdEncoding.DecodeString(res[1]) if err != nil { drh.logger.PrintDebug("Invalid request (cannot decode authentication): ", bufStr) return auth, bufStr, false } auth = string(b) // Authorize request if auth != drh.auth && drh.auth != "" { drh.logger.PrintDebug("Wrong authentication:", auth) return auth, bufStr, false } // Peer is now authorized store this so it can connect again drh.authPeers.Put(clientString, bufStr) } else if drh.auth != "" && !hasAuth { // No authorization drh.logger.PrintDebug("No authentication found") return auth, bufStr, false } else if bufStr == "" && hasAuth { // Workaround for strange clients like VLC which send first the // authentication then connect again on a different port and just // expect the stream bufStr = origBufStr.(string) // Get again the authentication res = requestAuthPattern.FindStringSubmatch(bufStr) if len(res) > 1 { if b, err := base64.StdEncoding.DecodeString(res[1]); err == nil { auth = string(b) } } } return auth, bufStr, true }
/* * DudelDu * * Copyright 2016 Matthias Ladkau. All rights reserved. * * This Source Code Form is subject to the terms of the MIT * License, If a copy of the MIT License was not distributed with this * file, You can obtain one at https://opensource.org/licenses/MIT. */ /* Package playlist contains the default playlist implementation. FilePlaylistFactory FilePlaylistFactory is a PlaylistFactory which reads its definition from a file. The definition file is expected to be a JSON encoded datastructure of the form: { <web path> : [ { "artist" : <artist> "title" : <title> "path" : <file path / url> } ] } The web path is the absolute path which may be requested by the streaming client (e.g. /foo/bar would be http://myserver:1234/foo/bar). The path is either a physical file or a web url reachable by the server process. The file ending determines the content type which is send to the client. */ package playlist import ( "bytes" "crypto/tls" "encoding/json" "io" "io/ioutil" "math/rand" "net/http" "net/url" "os" "path/filepath" "sync" "time" "devt.de/krotik/common/stringutil" "devt.de/krotik/dudeldu" ) /* FileExtContentTypes maps file extensions to content types */ var FileExtContentTypes = map[string]string{ ".mp3": "audio/mpeg", ".flac": "audio/flac", ".aac": "audio/x-aac", ".mp4a": "audio/mp4", ".mp4": "video/mp4", ".nsv": "video/nsv", ".ogg": "audio/ogg", ".spx": "audio/ogg", ".opus": "audio/ogg", ".oga": "audio/ogg", ".ogv": "video/ogg", ".weba": "audio/webm", ".webm": "video/webm", ".axa": "audio/annodex", ".axv": "video/annodex", } /* FrameSize is the frame size which is used by the playlists */ var FrameSize = dudeldu.FrameSize /* FilePlaylistFactory data structure */ type FilePlaylistFactory struct { data map[string][]map[string]string itemPathPrefix string } /* NewFilePlaylistFactory creates a new FilePlaylistFactory from a given definition file. */ func NewFilePlaylistFactory(path string, itemPathPrefix string) (*FilePlaylistFactory, error) { // Try to read the playlist file pl, err := ioutil.ReadFile(path) if err != nil { return nil, err } // Unmarshal json ret := &FilePlaylistFactory{ data: nil, itemPathPrefix: itemPathPrefix, } err = json.Unmarshal(pl, &ret.data) if err != nil { // Try again and strip out comments pl = stringutil.StripCStyleComments(pl) err = json.Unmarshal(pl, &ret.data) } if err != nil { return nil, err } return ret, nil } /* Playlist returns a playlist for a given path. */ func (fp *FilePlaylistFactory) Playlist(path string, shuffle bool) dudeldu.Playlist { if data, ok := fp.data[path]; ok { // Check if the playlist should be shuffled if shuffle { r := rand.New(rand.NewSource(time.Now().UnixNano())) shuffledData := make([]map[string]string, len(data), len(data)) for i, j := range r.Perm(len(data)) { shuffledData[i] = data[j] } data = shuffledData } return &FilePlaylist{path, fp.itemPathPrefix, 0, data, nil, false, &sync.Pool{New: func() interface{} { return make([]byte, FrameSize, FrameSize) }}} } return nil } /* FilePlaylist data structure */ type FilePlaylist struct { path string // Path of this playlist pathPrefix string // Prefix for all paths current int // Pointer to the current playing item data []map[string]string // Playlist items stream io.ReadCloser // Current open stream finished bool // Flag if this playlist has finished framePool *sync.Pool // Pool for byte arrays } /* currentItem returns the current playlist item */ func (fp *FilePlaylist) currentItem() map[string]string { if fp.current < len(fp.data) { return fp.data[fp.current] } return fp.data[len(fp.data)-1] } /* Name is the name of the playlist. */ func (fp *FilePlaylist) Name() string { return fp.path } /* ContentType returns the content type of this playlist e.g. audio/mpeg. */ func (fp *FilePlaylist) ContentType() string { ext := filepath.Ext(fp.currentItem()["path"]) if ctype, ok := FileExtContentTypes[ext]; ok { return ctype } return "audio" } /* Artist returns the artist which is currently playing. */ func (fp *FilePlaylist) Artist() string { return fp.currentItem()["artist"] } /* Title returns the title which is currently playing. */ func (fp *FilePlaylist) Title() string { return fp.currentItem()["title"] } /* Frame returns the current audio frame which is playing. */ func (fp *FilePlaylist) Frame() ([]byte, error) { var err error var frame []byte if fp.finished { return nil, dudeldu.ErrPlaylistEnd } if fp.stream == nil { // Make sure first file is loaded err = fp.nextFile() } if err == nil { // Get new byte array from a pool frame = fp.framePool.Get().([]byte) n := 0 nn := 0 for n < len(frame) && err == nil { nn, err = fp.stream.Read(frame[n:]) n += nn // Check if we need to read the next file if n < len(frame) || err == io.EOF { err = fp.nextFile() } } // Make sure the frame has no old data if it was only partially filled if n == 0 { // Special case we reached the end of the playlist frame = nil if err != nil { err = dudeldu.ErrPlaylistEnd } } else if n < len(frame) { // Resize frame if we have less data frame = frame[:n] } } if err == dudeldu.ErrPlaylistEnd { fp.finished = true } return frame, err } /* nextFile jumps to the next file for the playlist. */ func (fp *FilePlaylist) nextFile() error { var err error var stream io.ReadCloser // Except for the first call advance the current pointer if fp.stream != nil { fp.current++ fp.stream.Close() fp.stream = nil // Return special error if the end of the playlist has been reached if fp.current >= len(fp.data) { return dudeldu.ErrPlaylistEnd } } // Check if a file is already open if fp.stream == nil { item := fp.pathPrefix + fp.currentItem()["path"] if _, err = url.ParseRequestURI(item); err == nil { var resp *http.Response // We got an url - access it without SSL verification client := &http.Client{Transport: &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, }} if resp, err = client.Get(item); err == nil { buf := &StreamBuffer{} buf.ReadFrom(resp.Body) stream = buf } } else { // Open a new file stream, err = os.Open(item) } if err != nil { // Jump to the next file if there is an error fp.current++ return err } fp.stream = stream } return err } /* ReleaseFrame releases a frame which has been written to the client. */ func (fp *FilePlaylist) ReleaseFrame(frame []byte) { if len(frame) == FrameSize { fp.framePool.Put(frame) } } /* Finished returns if the playlist has finished playing. */ func (fp *FilePlaylist) Finished() bool { return fp.finished } /* Close any open files by this playlist and reset the current pointer. After this call the playlist can be played again. */ func (fp *FilePlaylist) Close() error { if fp.stream != nil { fp.stream.Close() fp.stream = nil } fp.current = 0 fp.finished = false return nil } /* StreamBuffer is a buffer which implements io.ReadCloser and can be used to stream one stream into another. The buffer detects a potential underflow and waits until enough bytes were read from the source stream. */ type StreamBuffer struct { bytes.Buffer // Buffer which is used to hold the data readFromOngoing bool } func (b *StreamBuffer) Read(p []byte) (int, error) { if b.readFromOngoing && b.Buffer.Len() < len(p) { // Prevent buffer underflow and wait until we got enough data for // the next read time.Sleep(10 * time.Millisecond) return b.Read(p) } n, err := b.Buffer.Read(p) // Return EOF if the buffer is empty if err == nil { if _, err = b.ReadByte(); err == nil { b.UnreadByte() } } return n, err } /* ReadFrom reads the source stream into the buffer. */ func (b *StreamBuffer) ReadFrom(r io.Reader) (int64, error) { b.readFromOngoing = true go func() { b.Buffer.ReadFrom(r) b.readFromOngoing = false }() return 0, nil } /* Close does nothing but must be there to implement io.ReadCloser. */ func (b *StreamBuffer) Close() error { // We are in memory so no need to close anything return nil }
/* * DudelDu * * Copyright 2016 Matthias Ladkau. All rights reserved. * * This Source Code Form is subject to the terms of the MIT * License, If a copy of the MIT License was not distributed with this * file, You can obtain one at https://opensource.org/licenses/MIT. */ package dudeldu import ( "bytes" "fmt" "io" "math" "net" "regexp" "strconv" "strings" "devt.de/krotik/common/datautil" ) /* MaxRequestSize is the maximum size for a request */ const MaxRequestSize = 1024 /* MetaDataInterval is the data interval in which meta data is send */ var MetaDataInterval uint64 = 65536 /* peerNoAuthTimeout is the time in seconds a peer can open new connections without sending new authentication information. */ const peerNoAuthTimeout = 10 /* MaxMetaDataSize is the maximum size for meta data (everything over is truncated) Must be a multiple of 16 which fits into one byte. Maximum: 16 * 255 = 4080 */ var MaxMetaDataSize = 4080 /* requestPathPattern is the pattern which is used to extract the requested path (i case-insensitive / m multi-line mode: ^ and $ match begin/end line) */ var requestPathPattern = regexp.MustCompile("(?im)get\\s+([^\\s]+).*") /* requestOffsetPattern is the pattern which is used to extract the requested offset (i case-insensitive / m multi-line mode: ^ and $ match begin/end line) */ var requestOffsetPattern = regexp.MustCompile("(?im)^Range: bytes=([0-9]+)-.*$") /* DefaultRequestHandler data structure */ type DefaultRequestHandler struct { PlaylistFactory PlaylistFactory // Factory for playlists ServeRequest func(c net.Conn, path string, metaDataSupport bool, offset int, auth string) // Function to serve requests loop bool // Flag if the playlist should be looped LoopTimes int // Number of loops -1 loops forever shuffle bool // Flag if the playlist should be shuffled auth string // Required (basic) authentication string - may be empty authPeers *datautil.MapCache // Peers which have been authenticated logger DebugLogger // Logger for debug output } /* NewDefaultRequestHandler creates a new default request handler object. */ func NewDefaultRequestHandler(pf PlaylistFactory, loop bool, shuffle bool, auth string) *DefaultRequestHandler { drh := &DefaultRequestHandler{ PlaylistFactory: pf, loop: loop, LoopTimes: -1, shuffle: shuffle, auth: auth, authPeers: datautil.NewMapCache(0, peerNoAuthTimeout), logger: nil, } drh.ServeRequest = drh.defaultServeRequest return drh } /* SetDebugLogger sets the debug logger for this request handler. */ func (drh *DefaultRequestHandler) SetDebugLogger(logger DebugLogger) { drh.logger = logger } /* HandleRequest handles requests from streaming clients. It tries to extract the path and if meta data is supported. Once a request has been successfully decoded ServeRequest is called. The connection is closed once HandleRequest finishes. */ func (drh *DefaultRequestHandler) HandleRequest(c net.Conn, nerr net.Error) { drh.logger.PrintDebug("Handling request from: ", c.RemoteAddr()) defer func() { c.Close() }() // Check if there was an error if nerr != nil { drh.logger.PrintDebug(nerr) return } buf, err := drh.decodeRequestHeader(c) if err != nil { drh.logger.PrintDebug(err) return } // Add ending sequence in case the client "forgets" bufStr := buf.String() + "\r\n\r\n" // Determine the remote string clientString := "-" if c.RemoteAddr() != nil { clientString, _, _ = net.SplitHostPort(c.RemoteAddr().String()) } drh.logger.PrintDebug("Client:", c.RemoteAddr(), " Request:", bufStr) if i := strings.Index(bufStr, "\r\n\r\n"); i >= 0 { var auth string var ok bool bufStr = strings.TrimSpace(bufStr[:i]) // Check authentication if auth, bufStr, ok = drh.checkAuth(bufStr, clientString); !ok { drh.writeUnauthorized(c) return } // Check if the client supports meta data metaDataSupport := false if strings.Contains(strings.ToLower(bufStr), "icy-metadata: 1") { metaDataSupport = true } // Extract offset offset := 0 res := requestOffsetPattern.FindStringSubmatch(bufStr) if len(res) > 1 { if o, err := strconv.Atoi(res[1]); err == nil { offset = o } } // Extract the path res = requestPathPattern.FindStringSubmatch(bufStr) if len(res) > 1 { // Now serve the request drh.ServeRequest(c, res[1], metaDataSupport, offset, auth) return } } drh.logger.PrintDebug("Invalid request: ", bufStr) } /* decodeRequestHeader decodes the header of an incoming request. */ func (drh *DefaultRequestHandler) decodeRequestHeader(c net.Conn) (*bytes.Buffer, error) { var buf bytes.Buffer rbuf := make([]byte, 512, 512) // Decode request n, err := c.Read(rbuf) for n > 0 || err != nil && err != io.EOF { // Do some error checking if err != nil { return nil, err } else if buf.Len() > MaxRequestSize { return nil, fmt.Errorf("Illegal request: Request is too long") } buf.Write(rbuf[:n]) if strings.Contains(string(rbuf), "\r\n\r\n") { break } n, err = c.Read(rbuf) } return &buf, nil } /* defaultServeRequest is called once a request was successfully decoded. */ func (drh *DefaultRequestHandler) defaultServeRequest(c net.Conn, path string, metaDataSupport bool, offset int, auth string) { var writtenBytes uint64 var currentPlaying string var err error drh.logger.PrintDebug("Serve request path:", path, " Metadata support:", metaDataSupport, " Offset:", offset) pl := drh.PlaylistFactory.Playlist(path, drh.shuffle) if pl == nil { // Stream was not found - no error checking here (don't care) drh.writeStreamNotFoundResponse(c) return } err = drh.writeStreamStartResponse(c, pl.Name(), pl.ContentType(), metaDataSupport) frameOffset := offset for { for !pl.Finished() { if drh.logger.IsDebugOutputEnabled() { playingString := fmt.Sprintf("%v - %v", pl.Title(), pl.Artist()) if playingString != currentPlaying { currentPlaying = playingString drh.logger.PrintDebug("Written bytes: ", writtenBytes) drh.logger.PrintDebug("Sending: ", currentPlaying) } } // Check if there were any errors if err != nil { drh.logger.PrintDebug(err) return } frameOffset, writtenBytes, err = drh.writeFrame(c, pl, frameOffset, writtenBytes, metaDataSupport) } // Handle looping - do not loop if close returns an error if pl.Close() != nil || !drh.loop { break } else if drh.LoopTimes != -1 { drh.LoopTimes-- if drh.LoopTimes == 0 { break } } } drh.logger.PrintDebug("Serve request path:", path, " complete") } /* prepareFrame prepares a frame before it can be written to a client. */ func (drh *DefaultRequestHandler) prepareFrame(c net.Conn, pl Playlist, frameOffset int, writtenBytes uint64, metaDataSupport bool) ([]byte, int, error) { frame, err := pl.Frame() // Handle offsets if frameOffset > 0 && err == nil { for frameOffset > len(frame) && err == nil { frameOffset -= len(frame) frame, err = pl.Frame() } if err == nil { frame = frame[frameOffset:] frameOffset = 0 if len(frame) == 0 { frame, err = pl.Frame() } } } if frame == nil { if !pl.Finished() { drh.logger.PrintDebug(fmt.Sprintf("Empty frame for: %v - %v (Error: %v)", pl.Title(), pl.Artist(), err)) } } else if err != nil { if err != ErrPlaylistEnd { drh.logger.PrintDebug(fmt.Sprintf("Error while retrieving playlist data: %v", err)) } err = nil } return frame, frameOffset, err } /* writeFrame writes a frame to a client. */ func (drh *DefaultRequestHandler) writeFrame(c net.Conn, pl Playlist, frameOffset int, writtenBytes uint64, metaDataSupport bool) (int, uint64, error) { frame, frameOffset, err := drh.prepareFrame(c, pl, frameOffset, writtenBytes, metaDataSupport) if frame == nil { return frameOffset, writtenBytes, err } // Check if meta data should be send if metaDataSupport && writtenBytes+uint64(len(frame)) >= MetaDataInterval { // Write rest data before sending meta data if preMetaDataLength := MetaDataInterval - writtenBytes; preMetaDataLength > 0 { if err == nil { _, err = c.Write(frame[:preMetaDataLength]) frame = frame[preMetaDataLength:] writtenBytes += preMetaDataLength } } if err == nil { // Write meta data - no error checking (next write should fail) drh.writeStreamMetaData(c, pl) // Write rest of the frame c.Write(frame) writtenBytes += uint64(len(frame)) } writtenBytes -= MetaDataInterval } else { // Just write the frame to the client if err == nil { clientWritten, _ := c.Write(frame) // Abort if the client does not accept more data if clientWritten == 0 && len(frame) > 0 { return frameOffset, writtenBytes, fmt.Errorf("Could not write to client - closing connection") } } pl.ReleaseFrame(frame) writtenBytes += uint64(len(frame)) } return frameOffset, writtenBytes, err } /* writeStreamMetaData writes meta data information into the stream. */ func (drh *DefaultRequestHandler) writeStreamMetaData(c net.Conn, playlist Playlist) { streamTitle := fmt.Sprintf("StreamTitle='%v - %v';", playlist.Title(), playlist.Artist()) // Truncate stream title if necessary if len(streamTitle) > MaxMetaDataSize { streamTitle = streamTitle[:MaxMetaDataSize-2] + "';" } // Calculate the meta data frame size as a multiple of 16 metaDataFrameSize := byte(math.Ceil(float64(len(streamTitle)) / 16.0)) // Write meta data to the client metaData := make([]byte, 16.0*metaDataFrameSize+1, 16.0*metaDataFrameSize+1) metaData[0] = metaDataFrameSize copy(metaData[1:], streamTitle) c.Write(metaData) } /* writeStreamStartResponse writes the start response to the client. */ func (drh *DefaultRequestHandler) writeStreamStartResponse(c net.Conn, name, contentType string, metaDataSupport bool) error { c.Write([]byte("ICY 200 OK\r\n")) c.Write([]byte(fmt.Sprintf("Content-Type: %v\r\n", contentType))) c.Write([]byte(fmt.Sprintf("icy-name: %v\r\n", name))) if metaDataSupport { c.Write([]byte("icy-metadata: 1\r\n")) c.Write([]byte(fmt.Sprintf("icy-metaint: %v\r\n", MetaDataInterval))) } _, err := c.Write([]byte("\r\n")) return err } /* writeStreamNotFoundResponse writes the not found response to the client. */ func (drh *DefaultRequestHandler) writeStreamNotFoundResponse(c net.Conn) error { _, err := c.Write([]byte("HTTP/1.1 404 Not found\r\n\r\n")) return err } /* writeUnauthorized writes the Unauthorized response to the client. */ func (drh *DefaultRequestHandler) writeUnauthorized(c net.Conn) error { _, err := c.Write([]byte("HTTP/1.1 401 Authorization Required\r\nWWW-Authenticate: Basic realm=\"DudelDu Streaming Server\"\r\n\r\n")) return err }
/* * DudelDu * * Copyright 2016 Matthias Ladkau. All rights reserved. * * This Source Code Form is subject to the terms of the MIT * License, If a copy of the MIT License was not distributed with this * file, You can obtain one at https://opensource.org/licenses/MIT. */ package dudeldu import ( "log" "net" "os" "os/signal" "sync" "syscall" "time" ) /* ProductVersion is the current version of DudelDu */ const ProductVersion = "1.3.1" /* ConnectionHandler is a function to handle new connections */ type ConnectionHandler func(net.Conn, net.Error) /* DebugLogger is the debug logging interface of the Server */ type DebugLogger interface { /* IsDebugOutputEnabled returns true if debug output is enabled. */ IsDebugOutputEnabled() bool /* PrintDebug will print debug output if `DebugOutput` is enabled. */ PrintDebug(v ...interface{}) } /* Server data structure */ type Server struct { Running bool // Flag indicating if the server is running Handler ConnectionHandler // Handler function for new connections DebugOutput bool // Enable additional debugging output LogPrint func(v ...interface{}) // Print logger method. signalling chan os.Signal // Channel for receiving signals tcpListener *net.TCPListener // TCP listener which accepts connections serving bool // Internal flag indicating if the socket should be served wgStatus *sync.WaitGroup // Optional wait group which should be notified once the server has started } /* NewServer creates a new DudelDu server. */ func NewServer(handler ConnectionHandler) *Server { return &Server{ Running: false, Handler: handler, DebugOutput: false, LogPrint: log.Print, } } /* IsDebugOutputEnabled returns true if debug output is enabled. */ func (ds *Server) IsDebugOutputEnabled() bool { return ds.DebugOutput } /* PrintDebug will print debug output if `DebugOutput` is enabled. */ func (ds *Server) PrintDebug(v ...interface{}) { if ds.DebugOutput { ds.LogPrint(v...) } } /* Run starts the DudelDu Server which can be stopped via ^C (Control-C). laddr should be the local address which should be given to net.Listen. wgStatus is an optional wait group which will be notified once the server is listening and once the server has shutdown. This function will not return unless the server is shutdown. */ func (ds *Server) Run(laddr string, wgStatus *sync.WaitGroup) error { // Create listener listener, err := net.Listen("tcp", laddr) if err != nil { if wgStatus != nil { wgStatus.Done() } return err } ds.tcpListener = listener.(*net.TCPListener) ds.wgStatus = wgStatus // Attach SIGINT handler - on unix and windows this is send // when the user presses ^C (Control-C). ds.signalling = make(chan os.Signal) signal.Notify(ds.signalling, syscall.SIGINT) // Put the serve call into a wait group so we can wait until shutdown // completed var wg sync.WaitGroup wg.Add(1) // Kick off the serve thread go func() { defer wg.Done() ds.Running = true ds.serv() }() for { // Listen for shutdown signal if ds.IsDebugOutputEnabled() { ds.PrintDebug("Listen for shutdown signal") } signal := <-ds.signalling if signal == syscall.SIGINT { // Shutdown the server ds.serving = false // Wait until the server has shut down wg.Wait() ds.Running = false break } } if wgStatus != nil { wgStatus.Done() } return nil } /* Shutdown sends a shutdown signal. */ func (ds *Server) Shutdown() { if ds.serving { ds.signalling <- syscall.SIGINT } } /* serv waits for new connections and assigns a handler to them. */ func (ds *Server) serv() { ds.serving = true for ds.serving { // Wait up to a second for a new connection ds.tcpListener.SetDeadline(time.Now().Add(time.Second)) newConn, err := ds.tcpListener.Accept() // Notify wgStatus if it was specified if ds.wgStatus != nil { ds.wgStatus.Done() ds.wgStatus = nil } netErr, ok := err.(net.Error) // Check if got an error and notify an error handler if newConn != nil || (ok && !(netErr.Timeout() || netErr.Temporary())) { go ds.Handler(newConn, netErr) } } ds.tcpListener.Close() }
/* * DudelDu * * Copyright 2016 Matthias Ladkau. All rights reserved. * * This Source Code Form is subject to the terms of the MIT * License, If a copy of the MIT License was not distributed with this * file, You can obtain one at https://opensource.org/licenses/MIT. */ /* DudelDu main entry point for the standalone server. Features: - Supports various streaming clients: VLC, ServeStream, ... and most Icecast clients. - Supports sending of meta data (sending artist and title to the streaming client). - Playlists are simple json files and data files are normal media (e.g. .mp3) files on disk. - Supports basic authentication. */ package main import ( "flag" "fmt" "log" "os" "devt.de/krotik/dudeldu" "devt.de/krotik/dudeldu/playlist" ) // Global variables // ================ /* ConfigFile is the config file which will be used to configure DudelDu */ var ConfigFile = "dudeldu.config.json" /* Known configuration options for DudelDu */ const ( ThreadPoolSize = "ThreadPoolSize" FrameQueueSize = "FrameQueueSize" ServerPort = "ServerPort" ServerHost = "ServerHost" PathPrefix = "PathPrefix" ) /* DefaultConfig is the defaut configuration */ var DefaultConfig = map[string]interface{}{ ThreadPoolSize: 10, FrameQueueSize: 10000, ServerPort: "9091", ServerHost: "127.0.0.1", PathPrefix: "", } type consolelogger func(v ...interface{}) /* Fatal/print logger methods. Using a custom type so we can test calls with unit tests. */ var fatal = consolelogger(log.Fatal) var print = consolelogger(func(a ...interface{}) { fmt.Fprint(os.Stderr, a...) fmt.Fprint(os.Stderr, "\n") }) var lookupEnv func(string) (string, bool) = os.LookupEnv /* DudelDu server instance (used by unit tests) */ var dds *dudeldu.Server /* Main entry point for DudelDu. */ func main() { var err error var plf dudeldu.PlaylistFactory print(fmt.Sprintf("DudelDu %v", dudeldu.ProductVersion)) auth := flag.String("auth", "", "Authentication as <user>:<pass>") serverHost := flag.String("host", DefaultConfig[ServerHost].(string), "Server hostname to listen on") serverPort := flag.String("port", DefaultConfig[ServerPort].(string), "Server port to listen on") threadPoolSize := flag.Int("tps", DefaultConfig[ThreadPoolSize].(int), "Thread pool size") frameQueueSize := flag.Int("fqs", DefaultConfig[FrameQueueSize].(int), "Frame queue size") pathPrefix := flag.String("pp", DefaultConfig[PathPrefix].(string), "Prefix all paths with a string") enableDebug := flag.Bool("debug", false, "Enable extra debugging output") loopPlaylist := flag.Bool("loop", false, "Loop playlists") shufflePlaylist := flag.Bool("shuffle", false, "Shuffle playlists") showHelp := flag.Bool("?", false, "Show this help message") flag.Usage = func() { print(fmt.Sprintf("Usage of %s [options] <playlist>", os.Args[0])) flag.PrintDefaults() print() print(fmt.Sprint("Authentication can also be defined via the environment variable: DUDELDU_AUTH=\"<user>:<pass>\"")) } flag.Parse() if len(flag.Args()) != 1 || *showHelp { flag.Usage() return } // Check for auth environment variable if envAuth, ok := lookupEnv("DUDELDU_AUTH"); ok && *auth == "" { *auth = envAuth } laddr := fmt.Sprintf("%v:%v", *serverHost, *serverPort) print(fmt.Sprintf("Serving playlist %v on %v", flag.Arg(0), laddr)) print(fmt.Sprintf("Thread pool size: %v", *threadPoolSize)) print(fmt.Sprintf("Frame queue size: %v", *frameQueueSize)) print(fmt.Sprintf("Loop playlist: %v", *loopPlaylist)) print(fmt.Sprintf("Shuffle playlist: %v", *shufflePlaylist)) print(fmt.Sprintf("Path prefix: %v", *pathPrefix)) if *auth != "" { print(fmt.Sprintf("Required authentication: %v", *auth)) } // Create server and listen plf, err = playlist.NewFilePlaylistFactory(flag.Arg(0), *pathPrefix) if err == nil { rh := dudeldu.NewDefaultRequestHandler(plf, *loopPlaylist, *shufflePlaylist, *auth) dds = dudeldu.NewServer(rh.HandleRequest) dds.DebugOutput = *enableDebug rh.SetDebugLogger(dds) defer print("Shutting down") err = dds.Run(laddr, nil) } if err != nil { fatal(err) } }