Skip to content
This repository has been archived by the owner on Mar 29, 2024. It is now read-only.

Commit

Permalink
Add SPN account handling and API
Browse files Browse the repository at this point in the history
  • Loading branch information
dhaavi committed Nov 23, 2021
1 parent 5a08823 commit 86e6737
Show file tree
Hide file tree
Showing 10 changed files with 828 additions and 4 deletions.
39 changes: 39 additions & 0 deletions access/account/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package account

import (
"errors"
"net/http"
)

const (
AuthHeaderDevice = "Device-17"
AuthHeaderToken = "Token-17"
AuthHeaderNextToken = "Next-Token-17"
AuthHeaderNextTokenDeprecated = "Next_token_17"
)

type AuthToken struct {
Device string
Token string
}

func GetAuthTokenFromRequest(request *http.Request) (*AuthToken, error) {
device := request.Header.Get(AuthHeaderDevice)
if device == "" {
return nil, errors.New("device ID is missing")
}
token := request.Header.Get(AuthHeaderToken)
if token == "" {
return nil, errors.New("token is missing")
}

return &AuthToken{
Device: device,
Token: token,
}, nil
}

func (at *AuthToken) ApplyTo(request *http.Request) {
request.Header.Set(AuthHeaderDevice, at.Device)
request.Header.Set(AuthHeaderToken, at.Token)
}
12 changes: 12 additions & 0 deletions access/account/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package account

const (
CAAuthenticateURL = "/authenticate"
CAProfileURL = "/profile"
CAGetTokensURL = "/tokens"
)

const (
CHAuthenticateURL = "/v1/authenticate"
CHUserProfileURL = "/v1/user_profile"
)
76 changes: 76 additions & 0 deletions access/account/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package account

import "time"

// User, Subscription and Charge states.
const (
// UserStateNone is only used within Portmaster for saving information for
// logging into the same device.
UserStateNone = ""
UserStateFresh = "fresh"
UserStateQueued = "queued"
UserStateApproved = "approved"
UserStateSuspended = "suspended"

SubscriptionStatePending = "pending"
SubscriptionStateActive = "active"
SubscriptionStateCancelled = "cancelled"
SubscriptionStateExpired = "expired"

ChargeStatePending = "pending"
ChargeStateCompleted = "completed"
ChargeStateDead = "dead"
)

// Agent and Hub return statuses.
const (
// StatusInvalidAuth [401 Unauthorized] is returned when the credentials are
// invalid or the user was logged out.
StatusInvalidAuth = 401
// StatusInvalidDevice [404 Not Found] is returned when the device trying to
// log into does not exist.
StatusInvalidDevice = 404
// StatusReachedDeviceLimit [409 Conflict] is returned when the device limit is reached.
StatusReachedDeviceLimit = 409
// StatusDeviceInactive [423 Locked] is returned when the device is locked.
StatusDeviceInactive = 423
// StatusNotLoggedIn [412 Precondition] is returned by the Portmaster, if an action required to be logged in, but the user is not logged in.
StatusNotLoggedIn = 412
)

// User describes an SPN user account.
type User struct {
Username string `json:"username"`
State string `json:"state"`
Balance int `json:"balance"`
Device *Device `json:"device"`
Subscription *Subscription `json:"subscription"`
CurrentPlan *Plan `json:"current_plan"`
NextPlan *Plan `json:"next_plan"`
}

// MayUseSPN return whether the user may currently use the SPN.
func (u *User) MayUseSPN() bool {
return u.State == UserStateApproved &&
time.Now().Before(u.Subscription.EndsAt)
}

// Device describes a device of an SPN user.
type Device struct {
Name string `json:"name"`
ID string `json:"id"`
}

// Subscription describes an SPN subscription.
type Subscription struct {
EndsAt time.Time `json:"ends_at"`
State string `json:"state"`
}

// Plan describes an SPN subscription plan.
type Plan struct {
Name string `json:"name"`
Amount int `json:"amount"`
Months int `json:"months"`
Renewable bool `json:"renewable"`
}
136 changes: 136 additions & 0 deletions access/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package access

import (
"fmt"
"net/http"

"github.com/safing/portbase/api"
"github.com/safing/portbase/database/record"
"github.com/safing/spn/access/account"
)

func registerAPIEndpoints() error {
if err := api.RegisterEndpoint(api.Endpoint{
Path: `spn/account/login`,
Read: api.PermitAdmin,
BelongsTo: module,
HandlerFunc: handleLogin,
Name: "SPN Login",
Description: "Log into your SPN account.",
}); err != nil {
return err
}

if err := api.RegisterEndpoint(api.Endpoint{
Path: `spn/account/logout`,
Read: api.PermitAdmin,
BelongsTo: module,
ActionFunc: handleLogout,
Name: "SPN Logout",
Description: "Logout from your SPN account.",
Parameters: []api.Parameter{
{
Method: "GET",
Field: "purge",
Value: "",
Description: "If set, account data is purged. Otherwise, the username and device ID are kept in order to log into the same device when logging in with the same user again.",
},
},
}); err != nil {
return err
}

if err := api.RegisterEndpoint(api.Endpoint{
Path: `spn/account/user/profile`,
Read: api.PermitUser,
BelongsTo: module,
RecordFunc: handleGetUserProfile,
Name: "SPN User Profile",
Description: "Get the user profile of the logged in SPN account.",
Parameters: []api.Parameter{
{
Method: "GET",
Field: "refresh",
Value: "",
Description: "If set, the user profile is freshly fetched from the account server.",
},
},
}); err != nil {
return err
}

return nil
}

func handleLogin(w http.ResponseWriter, r *http.Request) {
// Check if we are already authenticated.
user, err := GetUser()
if err == nil && user.State != account.UserStateNone {
http.Error(
w,
fmt.Sprintf("Already logged in as %s (Device: %s)", user.Username, user.Device.Name),
http.StatusConflict,
)
return
}

// Get username and password.
username, password, ok := r.BasicAuth()
// Request, if omitted.
if !ok || username == "" || password == "" {
w.Header().Set("WWW-Authenticate", "Basic realm=SPN Login")
http.Error(w, "Login with your SPN account.", http.StatusUnauthorized)
return
}

// Process login.
user, code, err := login(module.Ctx, username, password)
if err != nil {
if code == 0 {
http.Error(w, "Internal error: "+err.Error(), http.StatusInternalServerError)
} else {
http.Error(w, err.Error(), code)
}
return
}

// Return success.
w.Write([]byte(
fmt.Sprintf("Now logged in as %s (Device: %s)", user.Username, user.Device.Name),
))
return
}

func handleLogout(ar *api.Request) (msg string, err error) {
_, purge := ar.URLVars["purge"]
err = logout(purge)
switch {
case err != nil:
return "", err
case purge:
return "Logged out and user data purged.", nil
default:
return "Logged out.", nil
}
}

func handleGetUserProfile(ar *api.Request) (r record.Record, err error) {
// Check if we are already authenticated.
user, err := GetUser()
if err != nil || user.State == account.UserStateNone {
return nil, api.ErrorWithStatus(
ErrNotLoggedIn,
account.StatusNotLoggedIn,
)
}

// Should we refresh the user profile?
if _, ok := ar.URLVars["refresh"]; ok {
user, _, err = getUserProfile(module.Ctx)
if err != nil {
return nil, err
}
}

return user, nil
}
Loading

0 comments on commit 86e6737

Please sign in to comment.