summaryrefslogtreecommitdiff
path: root/src/fsm.go
blob: f73ce4c933827bb8f6a26d716c7337b8837903fc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
package eduvpn

import (
	"errors"
	"fmt"
	"os"
)

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) InState(check FSMStateID) bool {
	fsm := FindFSMState(check, eduvpn.FSM)

	if fsm == nil {
		return false
	}

	return fsm.Current == check
}

func (eduvpn *VPNState) writeGraph() {
	graph := eduvpn.GenerateGraph()

	f, err := os.Create("debug.graph")

	if err != nil {
		eduvpn.Log(LOG_INFO, fmt.Sprintf("Failed to write debug fsm graph with error %v", err))
	}

	defer f.Close()

	f.WriteString(graph)
}

func (eduvpn *VPNState) GoTransition(newState FSMStateID, data string) bool {
	fsm, ok := eduvpn.findTransition(newState)

	if ok {
		oldState := fsm.Current
		fsm.Current = newState
		if eduvpn.Debug {
			eduvpn.writeGraph()
		}
		eduvpn.StateCallback(oldState.String(), newState.String(), data)
	}

	return ok
}

func getGraphviz(fsm *FSM, graph string) string {
	if fsm == nil {
		return graph
	}

	for name, state := range fsm.States {
		for _, transition := range state.Transition {
			graph += "\n" + "cluster_" + name.String() + " -> cluster_" + transition.String()
		}

		graph += "\nsubgraph cluster_" + name.String() + "{\n"
		if (state.Locked) {
			graph += "style=\"dotted\"\n"
		} else {
			graph += "style=\"\"\n"
		}
		if (fsm.Current == name) {
			graph += "color=\"blue\"\n"
			graph += "fontcolor=\"blue\"\n"
		} else {
			graph += "color=\"\"\n"
			graph += "fontcolor=\"\"\n"
		}
		graph += "label=" + name.String()
		graph = getGraphviz(state.Sub, graph)
		graph += "\n}"
	}
	return graph
}

func (eduvpn *VPNState) GenerateGraph() string {
	graph := "digraph fsm {\n"
	graph += "nodesep=2"
	graph = getGraphviz(eduvpn.FSM, graph)
	graph += "\n}"

	return graph
}

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,
	}
}