diff options
| author | jwijenbergh <jeroenwijenbergh@protonmail.com> | 2022-03-31 11:50:38 +0200 |
|---|---|---|
| committer | jwijenbergh <jeroenwijenbergh@protonmail.com> | 2022-03-31 11:50:38 +0200 |
| commit | 0d860b20a8b6b61d937124ee1955074b12c3f8e6 (patch) | |
| tree | 506c74a1709fcf648d6850eb9486257e70ce1e5a /src | |
| parent | 6258542936e54074784cbc1bf910bd0503312d39 (diff) | |
Initial approach to creating a fsm with states and substates
Diffstat (limited to 'src')
| -rw-r--r-- | src/fsm.go | 206 | ||||
| -rw-r--r-- | src/oauth.go | 29 | ||||
| -rw-r--r-- | src/server.go | 5 | ||||
| -rw-r--r-- | src/state.go | 40 |
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 |
