summaryrefslogtreecommitdiff
path: root/client/fsm.go
blob: 7f16dce5c2683de0650c2c5680f1710b174e9d41 (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
package client

import (
	"fmt"

	"github.com/eduvpn/eduvpn-common/i18nerr"
	"github.com/eduvpn/eduvpn-common/internal/fsm"
	"github.com/eduvpn/eduvpn-common/internal/log"
)

type (
	// FSMStateID is an alias to the fsm state ID type
	FSMStateID = fsm.StateID
	// FSMStates is an alias to the fsm states type
	FSMStates = fsm.States
	// FSMState is an alias to the fsm state type
	FSMState = fsm.State
	// FSMTransition is an alias to the fsm transition type
	FSMTransition = fsm.Transition
)

const (
	// StateDeregistered is the state where we are deregistered
	StateDeregistered FSMStateID = iota

	// StateMain is the main state
	StateMain

	// StateAddingServer is the state where a server is being added
	StateAddingServer

	// StateOAuthStarted means the state where the OAuth procedure is triggered
	StateOAuthStarted

	// StateGettingConfig is the state a VPN config is being obtained
	StateGettingConfig

	// StateAskLocation is the state where a secure internet location is being asked
	StateAskLocation

	// StateAskProfile is the state where a profile is being asked for
	StateAskProfile

	// StateGotConfig is the state where a config is obtained
	StateGotConfig

	// StateConnecting is the state where the VPN is connecting
	StateConnecting

	// StateConnected is the state where the VPN is connected
	StateConnected

	// StateDisconnecting is the state where the VPN is disconnecting
	StateDisconnecting

	// StateDisconnected is the state where the VPN is disconnected
	StateDisconnected
)

// GetStateName gets the State name for state `s`
func GetStateName(s FSMStateID) string {
	switch s {
	case StateDeregistered:
		return "Deregistered"
	case StateMain:
		return "Main"
	case StateAddingServer:
		return "AddingServer"
	case StateOAuthStarted:
		return "OAuthStarted"
	case StateGettingConfig:
		return "GettingConfig"
	case StateAskLocation:
		return "AskLocation"
	case StateAskProfile:
		return "AskProfile"
	case StateGotConfig:
		return "GotConfig"
	case StateConnecting:
		return "Connecting"
	case StateConnected:
		return "Connected"
	case StateDisconnecting:
		return "Disconnecting"
	case StateDisconnected:
		return "Disconnected"
	default:
		panic(fmt.Sprintf("unknown conversion of state: %d to string", s))
	}
}

func newFSM(
	callback func(FSMStateID, FSMStateID, interface{}) bool,
) fsm.FSM {
	states := FSMStates{
		StateDeregistered: FSMState{
			Transitions: []FSMTransition{
				{To: StateMain, Description: "Register"},
			},
		},
		StateMain: FSMState{
			Transitions: []FSMTransition{
				{To: StateDeregistered, Description: "Deregister"},
				{To: StateAddingServer, Description: "Add a server"},
				{To: StateGettingConfig, Description: "Get a VPN config"},
				{To: StateConnected, Description: "Already connected"},
			},
		},
		StateAddingServer: FSMState{
			Transitions: []FSMTransition{
				{To: StateOAuthStarted, Description: "Authorize"},
			},
		},
		StateOAuthStarted: FSMState{
			Transitions: []FSMTransition{
				{To: StateMain, Description: "Authorized"},
				{To: StateDisconnected, Description: "Cancel, was disconnected"},
				{To: StateGotConfig, Description: "Cancel, was got config"},
			},
		},
		StateGettingConfig: FSMState{
			Transitions: []FSMTransition{
				{To: StateAskLocation, Description: "Invalid location"},
				{To: StateAskProfile, Description: "Invalid or no profile"},
				{To: StateDisconnected, Description: "Go back to disconnected"},
				{To: StateGotConfig, Description: "Successfully got a configuration"},
				{To: StateOAuthStarted, Description: "Authorize"},
			},
		},
		StateAskLocation: FSMState{
			Transitions: []FSMTransition{
				{To: StateGettingConfig, Description: "Location chosen"},
			},
		},
		StateAskProfile: FSMState{
			Transitions: []FSMTransition{
				{To: StateGettingConfig, Description: "Profile chosen"},
			},
		},
		StateGotConfig: FSMState{
			Transitions: []FSMTransition{
				{To: StateGettingConfig, Description: "Get a VPN config again"},
				{To: StateConnecting, Description: "VPN is connecting"},
				{To: StateOAuthStarted, Description: "Renew"},
			},
		},
		StateConnecting: FSMState{
			Transitions: []FSMTransition{
				{To: StateConnected, Description: "VPN is connected"},
				{To: StateDisconnecting, Description: "Cancel connecting"},
			},
		},
		StateConnected: FSMState{
			Transitions: []FSMTransition{
				{To: StateDisconnecting, Description: "VPN is disconnecting"},
			},
		},
		StateDisconnecting: FSMState{
			Transitions: []FSMTransition{
				{To: StateDisconnected, Description: "VPN is disconnected"},
				{To: StateConnected, Description: "Cancel disconnecting"},
			},
		},
		StateDisconnected: FSMState{
			Transitions: []FSMTransition{
				{To: StateConnecting, Description: "Connect with existing config"},
				{To: StateGettingConfig, Description: "Connect with a new config"},
				{To: StateOAuthStarted, Description: "Renew"},
			},
		},
	}

	return fsm.NewFSM(StateMain, states, callback, GetStateName)
}

// SetState sets the state for the client FSM to `state`
func (c *Client) SetState(state FSMStateID) error {
	c.mu.Lock()
	defer c.mu.Unlock()
	curr := c.FSM.Current
	_, err := c.FSM.GoTransition(state)
	if err != nil {
		// self-transitions are only debug errors
		if c.FSM.InState(state) {
			log.Logger.Debugf("attempt an invalid self-transition: %s", c.FSM.GetStateName(state))
			return nil
		}
		return i18nerr.WrapInternalf(err, "Failed internal state transition requested by the client from: '%s' to '%s'", GetStateName(curr), GetStateName(state))
	}
	return nil
}

// InState returns whether or not the client is in state `state`
func (c *Client) InState(state FSMStateID) bool {
	c.mu.Lock()
	defer c.mu.Unlock()
	return c.FSM.InState(state)
}