// bcryptAuthenticate uses the bcrypt authentication method to log in to the API, returning // a session user and a pair of client/server errors func bcryptAuthenticate(req *http.Request) (*data.User, *data.Session, error, error) { // Username and password for authentication var username string var password string // Check for empty authorization header if req.Header.Get("Authorization") == "" { // If no header, check for credentials via POST parameters username = req.PostFormValue("username") password = req.PostFormValue("password") } else { // Fetch credentials from HTTP Basic auth tempUsername, tempPassword, err := basicCredentials(req.Header.Get("Authorization")) if err != nil { return nil, nil, err, nil } // Copy credentials username = tempUsername password = tempPassword } // Check if either credential is blank if username == "" { return nil, nil, ErrNoUsername, nil } else if password == "" { return nil, nil, ErrNoPassword, 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, ErrInvalidUsername, nil } // Server error return nil, nil, nil, err } // Compare input password with bcrypt password, checking for errors err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) if err != nil && err == bcrypt.ErrMismatchedHashAndPassword { // Mismatch password return nil, nil, ErrInvalidPassword, nil } else if err != nil && err != bcrypt.ErrMismatchedHashAndPassword { // Return 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 }
// tokenAuthenticate uses the token authentication method to log in to the API, returning // a session user and a pair of client/server errors func tokenAuthenticate(req *http.Request) (*data.User, *data.Session, error, error) { // Token for authentication var token string // Check for empty authorization header if req.Header.Get("Authorization") == "" { // If no header, check for credentials via querystring parameters token = req.URL.Query().Get("s") } else { // Fetch credentials from HTTP Basic auth tempToken, _, err := basicCredentials(req.Header.Get("Authorization")) if err != nil { return nil, nil, err, nil } // Copy credentials token = tempToken } // Check if token is blank if token == "" { return nil, nil, ErrNoToken, nil } // Attempt to load session by key session := new(data.Session) session.Key = token if err := session.Load(); err != nil { // Check for invalid user if err == sql.ErrNoRows { return nil, nil, ErrInvalidToken, nil } // Server error return nil, nil, nil, err } // Attempt to load associated user by user ID from session user := new(data.User) user.ID = session.UserID if err := user.Load(); err != nil { // Server error return nil, nil, nil, err } // Update session expiration date by 1 week session.Expire = time.Now().Add(7 * 24 * time.Hour).Unix() if err := session.Update(); err != nil { return nil, nil, nil, err } // No errors, return session user and session return user, session, nil, nil }
// 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 }
// PostLastFM allows access to the Last.fm API, enabling wavepipe to set a user's currently-playing // track, as well as to enable scrobbling. func PostLastFM(w http.ResponseWriter, r *http.Request) { // Retrieve render ren := context.Get(r, CtxRender).(*render.Render) // Attempt to retrieve user from context user := new(data.User) if tempUser := context.Get(r, CtxUser); tempUser != nil { user = tempUser.(*data.User) } else { // No user stored in context log.Println("api: no user stored in request context!") ren.JSON(w, 500, serverErr) return } // Output struct for Last.fm response out := LastFMResponse{} // Check API version if version, ok := mux.Vars(r)["version"]; ok { // Check if this API call is supported in the advertised version if !apiVersionSet.Has(version) { ren.JSON(w, 400, errRes(400, "unsupported API version: "+version)) return } } // Do not allow guests and below to use Last.fm functionality if user.RoleID < data.RoleUser { ren.JSON(w, 403, permissionErr) return } // Check API action action, ok := mux.Vars(r)["action"] if !ok { ren.JSON(w, 400, errRes(400, "no string action provided")) return } // Check for valid action if !set.New(lfmLogin, lfmNowPlaying, lfmScrobble).Has(action) { ren.JSON(w, 400, errRes(400, "invalid string action provided")) return } // Instantiate Last.fm package lfm := lastfm.New(lfmAPIKey, lfmAPISecret) // Authenticate to the Last.fm API if action == lfmLogin { // Retrieve username from POST body username := r.PostFormValue("username") if username == "" { ren.JSON(w, 400, errRes(400, lfmLogin+": no username provided")) return } // Retrieve password from POST body password := r.PostFormValue("password") if password == "" { ren.JSON(w, 400, errRes(400, lfmLogin+": no password provided")) return } // Send a login request to Last.fm if err := lfm.Login(username, password); err != nil { ren.JSON(w, 401, errRes(401, lfmLogin+": last.fm authentication failed")) return } // Retrieve the API token for this user with wavepipe token, err := lfm.GetToken() if err != nil { log.Println(err) ren.JSON(w, 500, serverErr) return } // Store the user's Last.fm token in the database user.LastFMToken = token if err := user.Update(); err != nil { log.Println(err) ren.JSON(w, 500, serverErr) return } // Return the token authorization URL for the user out.URL = lfm.GetAuthTokenUrl(token) log.Println(lfmLogin, ": generated new token for user:"******"" { ren.JSON(w, 401, errRes(401, action+": user must authenticate to last.fm")) return } // Send a login request to Last.fm using token if err := lfm.LoginWithToken(user.LastFMToken); err != nil { // Check if token has not been authorized if strings.HasPrefix(err.Error(), "LastfmError[14]") { // Generate error output, but add the token authorization URL out.URL = lfm.GetAuthTokenUrl(user.LastFMToken) ren.JSON(w, 401, errRes(401, action+": last.fm token not yet authorized")) return } // All other failures ren.JSON(w, 401, errRes(401, action+": last.fm authentication failed")) return } // Check for an ID parameter pID, ok := mux.Vars(r)["id"] if !ok { ren.JSON(w, 400, errRes(400, action+": no integer song ID provided")) return } // Verify valid integer ID id, err := strconv.Atoi(pID) if err != nil { ren.JSON(w, 400, errRes(400, action+": invalid integer song ID")) return } // Load the song by ID song := &data.Song{ID: id} if err := song.Load(); err != nil { // Check for invalid ID if err == sql.ErrNoRows { ren.JSON(w, 404, errRes(404, action+": song ID not found")) return } // All other errors log.Println(err) ren.JSON(w, 500, serverErr) return } // Log the current action log.Printf("%s : %s : [#%05d] %s - %s", action, user.Username, song.ID, song.Artist, song.Title) // Create the track entity required by Last.fm from the song track := lastfm.P{ "artist": song.Artist, "album": song.Album, "track": song.Title, "timestamp": time.Now().Unix(), } // Check for optional timestamp parameter, which could be useful for sending scrobbles at // past times, etc if pTS := r.URL.Query().Get("timestamp"); pTS != "" { // Verify valid integer timestamp ts, err := strconv.Atoi(pTS) if err != nil || ts < 0 { ren.JSON(w, 400, errRes(400, action+": invalid integer timestamp")) return } // Override previously set timestamp with this one track["timestamp"] = ts } // Send a now playing request to the Last.fm API if action == lfmNowPlaying { // Perform the action if _, err := lfm.Track.UpdateNowPlaying(track); err != nil { log.Println(err) ren.JSON(w, 500, serverErr) return } // HTTP 200 OK with JSON out.Error = nil ren.JSON(w, 200, out) return } // Send a scrobble request to the Last.fm API if action == lfmScrobble { // Perform the action if _, err := lfm.Track.Scrobble(track); err != nil { log.Println(err) ren.JSON(w, 500, serverErr) return } // HTTP 200 OK with JSON out.Error = nil ren.JSON(w, 200, out) return } // Invalid action, meaning programmer error, HTTP 500 panic("no such Last.fm action: " + action) }
// subsonicAuthenticate uses the Subsonic authentication method to log in to the API, returning // only a pair of client/server errors func subsonicAuthenticate(req *http.Request) (*data.User, *data.Session, error, error) { // Check for required credentials via querystring query := req.URL.Query() username := query.Get("u") password := query.Get("p") // Check if username or password is blank if username == "" || password == "" { return nil, nil, subsonic.ErrBadCredentials, nil } // Check for Subsonic version version := query.Get("v") if version == "" { return nil, nil, subsonic.ErrMissingParameter, nil } // TODO: reevaluate this strategy in the future, but for now, we will use a user's wavepipe session // TODO: key as their Subsonic password. This will mean that the username and session key are passed on // TODO: every request, but also means that no more database schema must be added for Subsonic authentication. // Check for "enc:" prefix, specifying a hex-encoded password if strings.HasPrefix(password, "enc:") { // Decode hex string out, err := hex.DecodeString(password[4:]) if err != nil { return nil, nil, nil, err } password = string(out) } // Attempt to load session by key passed via Subsonic password parameter session := new(data.Session) session.Key = password if err := session.Load(); err != nil { // Check for invalid user if err == sql.ErrNoRows { return nil, nil, subsonic.ErrBadCredentials, nil } // Server error return nil, nil, nil, err } // Attempt to load associated user by username from Subsonic username parameter user := new(data.User) user.Username = username if err := user.Load(); err != nil { // Server error return nil, nil, nil, err } // Update session expiration date by 1 week session.Expire = time.Now().Add(7 * 24 * time.Hour).Unix() if err := session.Update(); err != nil { return nil, nil, nil, err } // No errors, return no user or session because the emulated Subsonic API is read-only return nil, nil, nil, nil }