This repository has been archived by the owner on Mar 29, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
828 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.