summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorjwijenbergh <jeroenwijenbergh@protonmail.com>2022-03-31 11:50:38 +0200
committerjwijenbergh <jeroenwijenbergh@protonmail.com>2022-03-31 11:50:38 +0200
commit0d860b20a8b6b61d937124ee1955074b12c3f8e6 (patch)
tree506c74a1709fcf648d6850eb9486257e70ce1e5a /src
parent6258542936e54074784cbc1bf910bd0503312d39 (diff)
Initial approach to creating a fsm with states and substates
Diffstat (limited to 'src')
-rw-r--r--src/fsm.go206
-rw-r--r--src/oauth.go29
-rw-r--r--src/server.go5
-rw-r--r--src/state.go40
4 files changed, 263 insertions, 17 deletions
diff --git a/src/fsm.go b/src/fsm.go
new file mode 100644
index 0000000..9978fde
--- /dev/null
+++ b/src/fsm.go
@@ -0,0 +1,206 @@
+package eduvpn
+
+import (
+ "errors"
+)
+
+type FSMStateID int8
+
+const (
+ // Registered means the app is registered with the wrapper
+ APP_REGISTERED FSMStateID = iota
+
+ // Deregistered means the app is not registered with the wrapper
+ APP_DEREGISTERED
+
+ // We have the states where a server is chosen or not
+ // When no server is chosen, we have no substate
+ CONFIG_NOSERVER
+
+ // When a server is chosen we have the remaining states as substates
+ CONFIG_CHOSENSERVER
+
+ // The states for when the server is authenticated
+ // The SERVER_AUTHENTICATED is the parent state
+ // While SERVER_CONNECTED and SERVER_DISCONNECTED are substatse
+ SERVER_AUTHENTICATED
+ SERVER_CONNECTED
+ SERVER_DISCONNECTED
+
+ // The states for when the server is not authenticated
+ // The SERVER_NOT_AUTHENTICATED is the parent state
+ // While SERVER_INITIALIZED, SERVER_OAUTH_STARTED and SERVER_OAUTH_FINISHED are substates
+ SERVER_NOT_AUTHENTICATED
+ SERVER_INITIALIZED
+ SERVER_OAUTH_STARTED
+ SERVER_OAUTH_FINISHED
+)
+
+func (s FSMStateID) String() string {
+ switch s {
+ case APP_REGISTERED:
+ return "APP_REGISTERED"
+ case APP_DEREGISTERED:
+ return "APP_DEREGISTERED"
+ case CONFIG_NOSERVER:
+ return "CONFIG_NOSERVER"
+ case CONFIG_CHOSENSERVER:
+ return "CONFIG_CHOSENSERVER"
+ case SERVER_AUTHENTICATED:
+ return "SERVER_AUTHENTICATED"
+ case SERVER_CONNECTED:
+ return "SERVER_CONNECTED"
+ case SERVER_DISCONNECTED:
+ return "SERVER_DISCONNECTED"
+ case SERVER_NOT_AUTHENTICATED:
+ return "SERVER_NOT_AUTHENTICATED"
+ case SERVER_INITIALIZED:
+ return "SERVER_INITIALIZED"
+ case SERVER_OAUTH_STARTED:
+ return "SERVER_OAUTH_STARTED"
+ case SERVER_OAUTH_FINISHED:
+ return "SERVER_OAUTH_FINISHED"
+ default:
+ panic("unknown conversion of state to string")
+ }
+}
+
+type (
+ FSMStates map[FSMStateID]*FSMState
+ FSMTransitions []FSMStateID
+)
+
+type FSMState struct {
+ Sub *FSM
+ Transition FSMTransitions
+
+ // When Locked=True it cannot go to the parent state and transition away
+ Locked bool
+}
+
+type FSM struct {
+ States FSMStates
+ Current FSMStateID
+}
+
+func (fsmState *FSMState) hasTransition(check FSMStateID) bool {
+ for _, state := range fsmState.Transition {
+ if state == check {
+ return true
+ }
+ }
+ return false
+}
+
+func (eduvpn *VPNState) getCurrentState() (*FSMState, error) {
+ state, hasState := eduvpn.FSM.States[eduvpn.FSM.Current]
+
+ if !hasState {
+ return nil, errors.New("Cannot get current state")
+ }
+
+ return state, nil
+}
+
+func FindFSMState(state FSMStateID, fsm *FSM) *FSM {
+ if fsm == nil {
+ return nil
+ }
+
+ // Check if the state is in the current fsm
+ retrievedState, hasState := fsm.States[state]
+
+ // Otherwise we need to go to the sub states
+ if !hasState || retrievedState == nil {
+ return FindFSMState(state, fsm.States[fsm.Current].Sub)
+ } else {
+ return fsm
+ }
+}
+
+func (eduvpn *VPNState) IsInFSMState(check FSMStateID) bool {
+ return eduvpn.FSM.Current == check
+}
+
+func (eduvpn *VPNState) findTransition(check FSMStateID) (*FSM, bool) {
+ fsm := FindFSMState(check, eduvpn.FSM)
+
+ if fsm == nil {
+ return nil, false
+ }
+
+ subStates := fsm.States[fsm.Current].Sub
+
+ if subStates != nil {
+ if subStates.States[subStates.Current].Locked {
+ return nil, false
+ }
+ }
+
+ for _, val := range fsm.States[fsm.Current].Transition {
+ if val == check {
+ return fsm, true
+ }
+ }
+
+ return nil, false
+}
+
+func (eduvpn *VPNState) HasTransition(check FSMStateID) bool {
+ fsm, ok := eduvpn.findTransition(check)
+
+ return ok && fsm != nil
+}
+
+func (eduvpn *VPNState) GoTransition(newState FSMStateID, data string) bool {
+ fsm, ok := eduvpn.findTransition(newState)
+
+ if ok {
+ oldState := fsm.Current
+ fsm.Current = newState
+ eduvpn.StateCallback(oldState.String(), newState.String(), data)
+ }
+
+ return ok
+}
+
+func (eduvpn *VPNState) InitializeFSM() {
+ // The states when a server is authenticated
+ serverAuthenticated := &FSMState{Sub: &FSM{States: FSMStates{
+ SERVER_DISCONNECTED: {Transition: FSMTransitions{SERVER_CONNECTED}},
+ SERVER_CONNECTED: {Transition: FSMTransitions{SERVER_DISCONNECTED}},
+ }, Current: SERVER_DISCONNECTED}, Transition: FSMTransitions{SERVER_NOT_AUTHENTICATED}}
+
+ // The states when a server is not authenticated
+ serverNotAuthenticated := &FSMState{Sub: &FSM{States: FSMStates{
+ // In this state we cannot exit to the parent state
+ // As the parent state can go to authenticated
+ SERVER_INITIALIZED: {Transition: FSMTransitions{SERVER_OAUTH_STARTED}, Locked: true},
+
+ // The state that indicates oauth is in progress
+ SERVER_OAUTH_STARTED: {Transition: FSMTransitions{SERVER_OAUTH_FINISHED}, Locked: true},
+ SERVER_OAUTH_FINISHED: {Transition: FSMTransitions{SERVER_OAUTH_STARTED}},
+ }, Current: SERVER_INITIALIZED}, Transition: FSMTransitions{SERVER_AUTHENTICATED}}
+
+ // The states of the server, it has authenticated and not authenticated ass sub states
+ serverStates := &FSMState{Sub: &FSM{States: FSMStates{
+ SERVER_AUTHENTICATED: serverAuthenticated,
+ SERVER_NOT_AUTHENTICATED: serverNotAuthenticated,
+ }, Current: SERVER_NOT_AUTHENTICATED}, Transition: FSMTransitions{CONFIG_NOSERVER}}
+
+ // The state when a server is registered
+ registeredState := &FSMState{Sub: &FSM{States: FSMStates{
+ // When no server has been chosen, we have no sub states
+ CONFIG_NOSERVER: {Transition: FSMTransitions{CONFIG_CHOSENSERVER}},
+ // A server has been chosen, it has substates such as oauth, connected and disconnected
+ CONFIG_CHOSENSERVER: serverStates,
+ }, Current: CONFIG_NOSERVER}, Transition: FSMTransitions{APP_DEREGISTERED}}
+
+ deregisteredState := &FSMState{Transition: FSMTransitions{APP_REGISTERED}}
+
+ eduvpn.FSM = &FSM{
+ States: FSMStates{
+ APP_REGISTERED: registeredState, APP_DEREGISTERED: deregisteredState,
+ }, Current: APP_DEREGISTERED,
+ }
+}
diff --git a/src/oauth.go b/src/oauth.go
index 45daf10..8656979 100644
--- a/src/oauth.go
+++ b/src/oauth.go
@@ -5,6 +5,7 @@ import (
"crypto/sha256"
"encoding/base64"
"encoding/json"
+ "errors"
"fmt"
"net/http"
"net/url"
@@ -225,17 +226,20 @@ func (oauth *OAuth) Callback(w http.ResponseWriter, req *http.Request) {
// Initializes the OAuth for eduvpn.
// It needs a vpn state that was gotten from `Register`
// It returns the authurl for the browser and an error if present
-func (eduvpn *VPNState) InitializeOAuth() (string, error) {
+func (eduvpn *VPNState) InitializeOAuth() error {
+ if !eduvpn.HasTransition(SERVER_OAUTH_STARTED) {
+ return errors.New("Failed starting oauth, invalid state")
+ }
// Generate the state
state, stateErr := genState()
if stateErr != nil {
- return "", &OAuthFailedInitializeError{Err: stateErr}
+ return &OAuthFailedInitializeError{Err: stateErr}
}
// Generate the verifier and challenge
verifier, verifierErr := genVerifier()
if verifierErr != nil {
- return "", &OAuthFailedInitializeError{Err: verifierErr}
+ return &OAuthFailedInitializeError{Err: verifierErr}
}
challenge := genChallengeS256(verifier)
@@ -258,33 +262,38 @@ func (eduvpn *VPNState) InitializeOAuth() (string, error) {
// Fill the struct with the necessary fields filled for the next call to getting the HTTP client
oauthSession := &OAuthExchangeSession{ClientID: eduvpn.Name, State: state, Verifier: verifier}
eduvpn.Server.OAuth = &OAuth{TokenURL: eduvpn.Server.Endpoints.API.V3.Token, Session: oauthSession}
- return authURL, nil
+ eduvpn.GoTransition(SERVER_OAUTH_STARTED, authURL)
+ return nil
}
// Error definitions
func (eduvpn *VPNState) FinishOAuth() error {
+ if !eduvpn.HasTransition(SERVER_OAUTH_FINISHED) {
+ return errors.New("invalid state to finish oauth")
+ }
oauth := eduvpn.Server.OAuth
- if oauth == nil {
- panic("invalid oauth state")
+ tokenErr := oauth.getTokensWithCallback()
+ if tokenErr != nil {
+ return tokenErr
}
- return oauth.getTokensWithCallback()
+ eduvpn.GoTransition(SERVER_OAUTH_FINISHED, "")
+ eduvpn.GoTransition(SERVER_AUTHENTICATED, "")
+ return nil
}
func (state *VPNState) LoginOAuth() error {
- authURL, authInitializeErr := state.InitializeOAuth()
+ authInitializeErr := state.InitializeOAuth()
if authInitializeErr != nil {
return authInitializeErr
}
- go state.StateCallback("Registered", "OAuthInitialized", authURL)
oauthErr := state.FinishOAuth()
if oauthErr != nil {
return oauthErr
}
- state.StateCallback("OAuthInitialized", "OAuthFinished", "finished oauth")
state.WriteConfig()
return nil
}
diff --git a/src/server.go b/src/server.go
index d512049..f829610 100644
--- a/src/server.go
+++ b/src/server.go
@@ -42,17 +42,22 @@ type ServerEndpoints struct {
}
func (server *Server) Initialize(url string) error {
+ if !GetVPNState().HasTransition(CONFIG_CHOSENSERVER) {
+ return errors.New("cannot choose a server")
+ }
server.BaseURL = url
endpointsErr := server.GetEndpoints()
if endpointsErr != nil {
return endpointsErr
}
+ GetVPNState().GoTransition(CONFIG_CHOSENSERVER, "Chosen server")
return nil
}
// FIXME: Check validity of tokens
func (server *Server) IsAuthenticated() bool {
return server.OAuth != nil
+ // return GetVPNState().HasTransition(SERVER_NOT_AUTHENTICATED)
}
func (server *Server) GetEndpoints() error {
diff --git a/src/state.go b/src/state.go
index a48d13d..aa31513 100644
--- a/src/state.go
+++ b/src/state.go
@@ -1,5 +1,9 @@
package eduvpn
+import (
+ "errors"
+)
+
type VPNState struct {
// Info passed by the client
ConfigDirectory string `json:"-"`
@@ -14,34 +18,46 @@ type VPNState struct {
// The file we keep open for logging
LogFile *FileLogger `json:"-"`
+
+ FSM *FSM `json:"-"`
}
func (state *VPNState) Register(name string, directory string, stateCallback func(string, string, string)) error {
+ if state.FSM == nil {
+ state.InitializeFSM()
+ }
+ if !state.HasTransition(APP_REGISTERED) {
+ return errors.New("app already registered")
+ }
state.Name = name
state.ConfigDirectory = directory
state.StateCallback = stateCallback
// Initialize the logger
- state.InitLog(LOG_WARNING)
-
- state.Log(LOG_INFO, "App registered")
+ // state.InitLog(LOG_WARNING)
- state.StateCallback("Start", "Registered", "app registered")
+ // state.Log(LOG_INFO, "App registered")
// Try to load the previous configuration
if state.LoadConfig() != nil {
// This error can be safely ignored, as when the config does not load, the struct will not be filled
- state.Log(LOG_INFO, "Previous configuration not found")
+ // state.Log(LOG_INFO, "Previous configuration not found")
}
+ state.GoTransition(APP_REGISTERED, "HALLO")
return nil
}
-func (state *VPNState) Deregister() {
+func (state *VPNState) Deregister() error {
+ if !state.HasTransition(APP_DEREGISTERED) {
+ return errors.New("app cannot deregister")
+ }
// Close the log file
state.CloseLog()
// Re-initialize everything
state = &VPNState{}
+ state.GoTransition(APP_DEREGISTERED, "")
+ return nil
}
func (state *VPNState) Connect(url string) (string, error) {
@@ -62,7 +78,17 @@ func (state *VPNState) Connect(url string) (string, error) {
}
}
- return state.Server.GetConfig()
+ config, configErr := state.Server.GetConfig()
+
+ if configErr != nil {
+ return "", configErr
+ }
+
+ if !state.HasTransition(SERVER_CONNECTED) {
+ return "", errors.New("cannot connect to server, invalid state")
+ }
+
+ return config, nil
}
var VPNStateInstance *VPNState