Beispiel #1
0
// 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
}
Beispiel #2
0
// 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
}
Beispiel #3
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
}
Beispiel #4
0
// 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)
}
Beispiel #5
0
// 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
}