Example #1
0
// Factory generates the appropriate authorization method by using input parameters
func Factory(path string) AuthenticatorFunc {
	// Check for debug mode, and if it's set, automatically use the Simple method
	if env.IsDebug() {
		log.Println("api: warning: authenticating user in debug mode")
		return simpleAuthenticate
	}

	// Check for request to emulated Subsonic API, which is authenticated using
	// its own, special method which outputs XML
	if strings.HasPrefix(path, "/subsonic") {
		return subsonicAuthenticate
	}

	// Check if path does not reside under the /api, meaning it is unauthenticated
	if !strings.HasPrefix(path, "/api") {
		return nilAuthenticate
	}

	// Strip any trailing slashes from the path
	path = strings.TrimRight(path, "/")

	// Check for request to API root (/api, /api/vX), which is unauthenticated
	if path == "/api" || (strings.HasPrefix(path, "/api/v") && len(path) == 7) {
		return nilAuthenticate
	}

	// Check for a login request: /api/vX/login, use bcrypt authenticator
	// Note: length check added to prevent this from happening on /api/VX/lastfm/login
	if len(path) == 13 && strings.HasPrefix(path, "/api/v") && strings.HasSuffix(path, "/login") {
		return bcryptAuthenticate
	}

	// All other situations, use the token authenticator
	return tokenAuthenticate
}
Example #2
0
// simpleAuthenticate uses the simple authentication method to log in to the API, returning
// a session user and a pair of client/server errors.
func simpleAuthenticate(req *http.Request) (*data.User, *data.Session, error, error) {
	// Verify that SimpleAuth was triggered in debug mode
	if !env.IsDebug() {
		return nil, nil, nil, errors.New("not in debug mode")
	}

	// Username for authentication
	var username string

	// Check for empty authorization header
	if req.Header.Get("Authorization") == "" {
		// If no header, check for credentials via querystring parameters
		query := req.URL.Query()
		username = query.Get("u")
	} else {
		// Fetch credentials from HTTP Basic auth
		tempUsername, _, err := basicCredentials(req.Header.Get("Authorization"))
		if err != nil {
			return nil, nil, err, nil
		}

		// Copy credentials
		username = tempUsername
	}

	// Check if username is blank
	if username == "" {
		return nil, nil, ErrNoUsername, nil
	}

	// Attempt to load user by username
	user := new(data.User)
	user.Username = username
	if err := user.Load(); err != nil {
		// Check for invalid user
		if err == sql.ErrNoRows {
			return nil, nil, errors.New("invalid username"), nil
		}

		// Server error
		return nil, nil, nil, err
	}

	// No errors, return session user, but no session because one does not exist yet
	return user, nil, nil, nil
}
Example #3
0
// apiRouter sets up the instance of negroni
func apiRouter(apiKillChan chan struct{}) {
	log.Println("api: starting...")

	// Initialize negroni
	n := negroni.New()

	// Set up render
	r := render.New(render.Options{
		// Output human-readable JSON/XML. GZIP will essentially negate the size increase, and this
		// makes the API much more developer-friendly
		IndentJSON: true,
		IndentXML:  true,
	})

	// GZIP all responses
	n.Use(gzip.Gzip(gzip.DefaultCompression))

	// Initial API setup
	n.Use(negroni.HandlerFunc(func(res http.ResponseWriter, req *http.Request, next http.HandlerFunc) {
		// Send a Server header with all responses
		res.Header().Set("Server", fmt.Sprintf("%s/%s (%s_%s)", App, Version, runtime.GOOS, runtime.GOARCH))

		// Store render in context for all API calls
		context.Set(req, api.CtxRender, r)

		// Wrap HTTP request and response with metrics instrumentation
		req.Body = httpRMetricsLogger{req.Body}
		metricsRes := httpWMetricsLogger{res}

		// On debug, log everything
		if env.IsDebug() {
			log.Println(req.Header)
			log.Println(req.URL)

			// Wrap response in debug logging
			next(httpWDebugLogger{metricsRes}, req)
			return
		}

		// Delegate to next middleware
		next(metricsRes, req)
		return
	}))

	// Authenticate all API calls
	n.Use(negroni.HandlerFunc(func(res http.ResponseWriter, req *http.Request, next http.HandlerFunc) {
		// Use factory to determine and invoke the proper authentication method for this path
		user, session, clientErr, serverErr := auth.Factory(req.URL.Path).Authenticate(req)

		// Check for client error
		if clientErr != nil {
			// Check for a Subsonic error, since these are rendered as XML
			if subErr, ok := clientErr.(*subsonic.Container); ok {
				r.XML(res, 200, subErr)
				return
			}

			// If debug mode, and no username or password, send a WWW-Authenticate header to prompt request
			// This allows for manual exploration of the API if needed
			if env.IsDebug() && (clientErr == auth.ErrNoUsername || clientErr == auth.ErrNoPassword) {
				res.Header().Set("WWW-Authenticate", "Basic")
			}

			r.JSON(res, 401, api.ErrorResponse{
				Error: &api.Error{
					Code:    401,
					Message: "authentication failed: " + clientErr.Error(),
				},
			})
			return
		}

		// Check for server error
		if serverErr != nil {
			log.Println(serverErr)

			// Check for a Subsonic error, since these are rendered as XML
			if subErr, ok := serverErr.(*subsonic.Container); ok {
				r.XML(res, 200, subErr)
				return
			}

			r.JSON(res, 500, api.ErrorResponse{
				Error: &api.Error{
					Code:    500,
					Message: "server error",
				},
			})
			return
		}

		// Successful login, map session user and session to gorilla context for this request
		context.Set(req, api.CtxUser, user)
		context.Set(req, api.CtxSession, session)

		// Print information about this API call
		log.Printf("api: [%s] %s %s?%s", req.RemoteAddr, req.Method, req.URL.Path, req.URL.Query().Encode())

		// Perform API call
		next(res, req)
	}))

	// Wait for graceful to signal termination
	gracefulChan := make(chan struct{}, 0)

	// Use gorilla mux with negroni, start server
	n.UseHandler(newRouter())
	go func() {
		// Load config
		conf, err := config.C.Load()
		if err != nil {
			log.Println(err)
			return
		}

		// Check for empty host
		if conf.Host == "" {
			log.Fatalf("api: no host specified in configuration")
		}

		// Start server, allowing up to 10 seconds after shutdown for clients to complete
		log.Println("api: binding to host", conf.Host)
		if err := graceful.ListenAndServe(&http.Server{Addr: conf.Host, Handler: n}, 10*time.Second); err != nil {
			// Check if address in use
			if strings.Contains(err.Error(), "address already in use") {
				log.Fatalf("api: cannot bind to %s, is wavepipe already running?", conf.Host)
			}

			// Ignore error on closing
			if !strings.Contains(err.Error(), "use of closed network connection") {
				// Log other errors
				log.Println(err)
			}
		}

		// Shutdown complete
		close(gracefulChan)
	}()

	// Trigger events via channel
	for {
		select {
		// Stop API
		case <-apiKillChan:
			// If testing, don't wait for graceful shutdown
			if !env.IsTest() {
				// Block and wait for graceful shutdown
				log.Println("api: waiting for remaining connections to close...")
				<-gracefulChan
			}

			// Inform manager that shutdown is complete
			log.Println("api: stopped!")
			apiKillChan <- struct{}{}
			return
		}
	}
}
Example #4
0
// newRouter sets up the web and API routes required by wavepipe
func newRouter() *mux.Router {
	// Create a router
	router := mux.NewRouter().StrictSlash(false)

	// HTTP handler for web UI
	webUI := func(res http.ResponseWriter, req *http.Request) {
		// Retrieve render
		r := context.Get(req, api.CtxRender).(*render.Render)

		// Get the asset name
		name := mux.Vars(req)["asset"]

		// If asset name empty, return the index
		if name == "" {
			name = "index.html"
		}

		// More information on debug
		if env.IsDebug() {
			log.Println("web: fetching resource: res/web/" + name)
		}

		// Retrieve asset
		asset, err := data.Asset("res/web/" + name)
		if err != nil {
			res.WriteHeader(404)
			return
		}

		// Render asset and return its type
		res.Header().Set("Content-Type", mime.TypeByExtension(path.Ext(name)))
		r.Data(res, 200, asset)
	}

	// Web UI and its assets
	router.HandleFunc("/", webUI).Methods("GET")
	router.HandleFunc("/res/{asset:.*}", webUI).Methods("GET")

	// Set up robots.txt to disallow crawling, since this is a dynamic service which users self-host
	router.HandleFunc("/robots.txt", func(res http.ResponseWriter, req *http.Request) {
		res.Write([]byte("# wavepipe media server\n" +
			"# https://github.com/mdlayher/wavepipe\n" +
			"User-agent: *\n" +
			"Disallow: /"))
	}).Methods("GET")

	// Set up current revision route, for easy identification of a wavepipe build
	router.HandleFunc("/revision", func(res http.ResponseWriter, req *http.Request) {
		res.Write([]byte(Revision))
	}).Methods("GET")

	// Set up API information route
	router.HandleFunc("/api", api.APIInfo).Methods("GET")

	// Set up API group routes, with API version parameter
	ar := router.PathPrefix("/api/{version}/").Subrouter()

	// Albums API
	ar.HandleFunc("/albums", api.GetAlbums).Methods("GET")
	ar.HandleFunc("/albums/{id}", api.GetAlbums).Methods("GET")

	// Art API
	ar.HandleFunc("/art", api.GetArt).Methods("GET")
	ar.HandleFunc("/art/{id}", api.GetArt).Methods("GET")

	// Artists API
	ar.HandleFunc("/artists", api.GetArtists).Methods("GET")
	ar.HandleFunc("/artists/{id}", api.GetArtists).Methods("GET")

	// Folders API
	ar.HandleFunc("/folders", api.GetFolders).Methods("GET")
	ar.HandleFunc("/folders/{id}", api.GetFolders).Methods("GET")

	// LastFM API
	ar.HandleFunc("/lastfm", api.PostLastFM).Methods("POST")
	ar.HandleFunc("/lastfm/{action}", api.PostLastFM).Methods("POST")
	ar.HandleFunc("/lastfm/{action}/{id}", api.PostLastFM).Methods("POST")

	// Login API
	ar.HandleFunc("/login", api.PostLogin).Methods("POST")

	// Logout API
	ar.HandleFunc("/logout", api.PostLogout).Methods("POST")

	// Search API
	ar.HandleFunc("/search", api.GetSearch).Methods("GET")
	ar.HandleFunc("/search/{query}", api.GetSearch).Methods("GET")

	// Songs API
	ar.HandleFunc("/songs", api.GetSongs).Methods("GET")
	ar.HandleFunc("/songs/{id}", api.GetSongs).Methods("GET")

	// Status API
	ar.HandleFunc("/status", api.GetStatus).Methods("GET")

	// Stream API
	ar.HandleFunc("/stream", api.GetStream).Methods("GET")
	ar.HandleFunc("/stream/{id}", api.GetStream).Methods("GET")

	// Transcode API
	ar.HandleFunc("/transcode", api.GetTranscode).Methods("GET")
	ar.HandleFunc("/transcode/{id}", api.GetTranscode).Methods("GET")

	// Users API
	ar.HandleFunc("/users", api.GetUsers).Methods("GET")
	ar.HandleFunc("/users/{id}", api.GetUsers).Methods("GET")
	ar.HandleFunc("/users", api.PostUsers).Methods("POST")
	ar.HandleFunc("/users/{id}", api.PutUsers).Methods("PUT", "PATCH")
	ar.HandleFunc("/users/{id}", api.DeleteUsers).Methods("DELETE")

	// Waveform API
	ar.HandleFunc("/waveform", api.GetWaveform).Methods("GET")
	ar.HandleFunc("/waveform/{id}", api.GetWaveform).Methods("GET")

	// Set up emulated Subsonic API routes
	sr := router.PathPrefix("/subsonic/rest").Subrouter()

	// Ping - used to check connectivity
	sr.HandleFunc("/ping.view", subsonic.Ping)

	// GetAlbumList2 - used to return a list of all albums by tags
	sr.HandleFunc("/getAlbumList2.view", subsonic.GetAlbumList2)

	// GetAlbum - used to retrieve information about one album
	sr.HandleFunc("/getAlbum.view", subsonic.GetAlbum)

	// GetCoverArt - used to retrieve cover art for an item
	sr.HandleFunc("/getCoverArt.view", subsonic.GetCoverArt)

	// GetIndexes - used to retrieve an index of artists with their IDs
	sr.HandleFunc("/getIndexes.view", subsonic.GetIndexes)

	// GetLicense - used to retrieve information about a Subsonic server's license
	sr.HandleFunc("/getLicense.view", subsonic.GetLicense)

	// GetMusicDirectory - used to retrieve folders and contained files
	sr.HandleFunc("/getMusicDirectory.view", subsonic.GetMusicDirectory)

	// GetMusicFolders - used to retrieve list of known music folders
	sr.HandleFunc("/getMusicFolders.view", subsonic.GetMusicFolders)

	// GetPlaylists - used to retrieve playlists from the server
	// (not currently implemented by wavepipe)
	sr.HandleFunc("/getPlaylists.view", subsonic.GetPlaylists)

	// GetRandomSongs - used to retrieve a number of random songs
	sr.HandleFunc("/getRandomSongs.view", subsonic.GetRandomSongs)

	// GetStarred - used to retrieve a list of favorite items
	// (not currently implemented by wavepipe)
	sr.HandleFunc("/getStarred.view", subsonic.GetStarred)

	// Stream - used to return a binary file stream
	sr.HandleFunc("/stream.view", subsonic.Stream)

	// On debug mode, enable pprof debug endpoints
	// Thanks: https://github.com/go-martini/martini/issues/228
	if env.IsDebug() {
		dr := router.PathPrefix("/debug/pprof").Subrouter()
		dr.HandleFunc("/", pprof.Index)
		dr.HandleFunc("/cmdline", pprof.Cmdline)
		dr.HandleFunc("/profile", pprof.Profile)
		dr.HandleFunc("/symbol", pprof.Symbol)
		dr.HandleFunc("/block", pprof.Handler("block").ServeHTTP)
		dr.HandleFunc("/heap", pprof.Handler("heap").ServeHTTP)
		dr.HandleFunc("/goroutine", pprof.Handler("goroutine").ServeHTTP)
		dr.HandleFunc("/threadcreate", pprof.Handler("threadcreate").ServeHTTP)
	}

	// Return configured router
	return router
}
Example #5
0
func main() {
	log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)

	// Parse command line flags
	flag.Parse()

	// Check if wavepipe was invoked as root (which is a really bad idea)
	user := common.System.User
	if user.Uid == "0" || user.Gid == "0" || user.Username == "root" {
		log.Println(core.App, ": WARNING, it is NOT advisable to run wavepipe as root!")
	}

	// Application entry point
	log.Println(core.App, ": starting...")

	// Check if running in debug mode, which will allow bypass of certain features such as
	// API authentication.  USE THIS FOR DEVELOPMENT ONLY!
	if env.IsDebug() {
		log.Println(core.App, ": WARNING, running in debug mode; authentication disabled!")
	}

	// Gracefully handle termination via UNIX signal
	sigChan := make(chan os.Signal, 1)

	// In test mode, wait for a short time, then invoke a signal shutdown
	if *testFlag {
		// Set an environment variable to enable mocking in other areas of the program
		if err := env.SetTest(true); err != nil {
			log.Println(err)
		}

		go func() {
			// Wait a few seconds, to allow reasonable startup time
			seconds := 10
			log.Println(core.App, ": started in test mode, stopping in", seconds, "seconds.")
			<-time.After(time.Duration(seconds) * time.Second)

			// Send interrupt
			sigChan <- os.Interrupt
		}()
	}

	// Invoke the manager, with graceful termination and core.Application exit code channels
	killChan := make(chan struct{})
	exitChan := make(chan int)
	go core.Manager(killChan, exitChan)

	// Trigger a shutdown if SIGINT or SIGTERM received
	signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
	for sig := range sigChan {
		log.Println(core.App, ": caught signal:", sig)
		killChan <- struct{}{}
		break
	}

	// Force terminate if signaled twice
	go func() {
		for sig := range sigChan {
			log.Println(core.App, ": caught signal:", sig, ", force halting now!")
			os.Exit(1)
		}
	}()

	// Graceful exit
	code := <-exitChan
	log.Println(core.App, ": graceful shutdown complete")
	os.Exit(code)
}