-
Notifications
You must be signed in to change notification settings - Fork 12
/
Copy pathmain.go
316 lines (274 loc) · 9.55 KB
/
main.go
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
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
package main
import (
"embed"
"encoding/json"
"flag"
"fmt"
"io"
"log"
"net/http"
"os"
"os/signal"
"strings"
"sync/atomic"
"syscall"
)
type SfUI struct {
MaxWsTerminals int `yaml:"max_ws_terminals"` // Max terminals that can be allocated per client
MaxSharedDesktopConn int `yaml:"max_shared_desktop_conn"` // Max no of clients that can connect to a shared desktop
WSPingInterval int `yaml:"ws_ping_interval"` // Intervals at which the client pings the terminals WS connection
WSTimeout int `yaml:"ws_timeout"` // Timeout (in minutes) applied to terminal and desktop ws connections
ServerBindAddress string `yaml:"server_bind_address"` // Address to which the current app binds
Debug bool `yaml:"debug"` // Print debug information
StartXpraCommand string `yaml:"start_xpra_command"` // Command used to start xpra
StartVNCCommand string `yaml:"start_vnc_command"` // Command used to start VNC
StartFileBrowserCommand string `yaml:"start_filebrowser_command"` // Command used to start filebrowser
VNCPort uint16 `yaml:"vnc_port"`
FileBrowserPort uint16 `yaml:"filebrowser_port"`
CompiledClientConfig []byte // Ui related config that has to be sent to client
SfEndpoints []string `yaml:"sf_endpoints"` // Sf Endpoints To Use
NoEndpoints int32 // No of available endpoints
SfUIOrigin string `yaml:"sf_ui_origin"` // Where SFUI is deployed, for CSRF prevention, ex: https://web.segfault.net
UseXForwardedForHeader bool `yaml:"use_x_forwarded_for_header"` // Use the X-Forwared-For HTTP header, usefull when behind a reverse proxy
DisableOriginCheck bool `yaml:"disable_origin_check"` // Disable Origin Checking
DisableDesktop bool `yaml:"disable_desktop"` // Disable websocket based GUI desktop access
ClientInactivityTimeout int `yaml:"client_inactivity_timeout"` // Minutes after which the clients master SSH connection is killed
ValidSecret func(s string) bool // Secret Validator
EndpointSelector *atomic.Int32 // Helps select a endpoint in RR fashion
NoOfEndpoints int32 // No of available endpoints
SegfaultSSHUsername string `yaml:"segfault_ssh_username"`
SegfaultSSHPassword string `yaml:"segfault_ssh_password"`
SegfaultUseSSHKey bool `yaml:"segfault_use_ssh_key"` // whether to use a ssh key
SegfaultSSHKeyPath string `yaml:"segfault_ssh_key_path"` // absolute path to the ssh key
MaintenanceSecret string `yaml:"maintenance_secret"` // secret used to restrict access to certain maintenance apis
EnableMetricLogging bool `yaml:"enable_metric_logging"` // collect metrics from sfui
MetricLoggerQueueSize int `yaml:"metric_logger_queue_size"`
ElasticServerHost string `yaml:"elastic_server_host"`
ElasticIndexName string `yaml:"elastic_index_name"`
ElasticUsername string `yaml:"elastic_username"`
ElasticPassword string `yaml:"elastic_password"`
OpenObserveCompatible bool `yaml:"open_observe_compatible"`
GeoIpDBPath string `yaml:"geo_ip_db_path"`
}
var buildTime string
var buildHash string
var SfuiVersion string = "0.2.0"
//go:embed ui/dist/sf-ui
var staticfiles embed.FS
func main() {
if ActionInvoked := handleCmdLineFlags(); ActionInvoked {
return
}
sfui := ReadConfig()
log.Printf("SFUI [Version : %s] [Built on : %s]\n", SfuiVersion, buildTime)
rlErr := obtainRunLock()
if rlErr != nil {
log.Println(rlErr)
return
}
// release runLock in cleanUp()
sfui.handleSignals()
sfui.InitRouter()
if sfui.EnableMetricLogging {
gerr := GeoIpInit(sfui.GeoIpDBPath)
if gerr != nil {
log.Println(gerr)
}
MLogger.StartLogger(sfui.MetricLoggerQueueSize, 1,
sfui.ElasticServerHost, sfui.ElasticIndexName,
sfui.ElasticUsername, sfui.ElasticPassword, sfui.OpenObserveCompatible)
}
BanDB.Init()
log.Printf("Listening on http://%s ....\n", sfui.ServerBindAddress)
http.ListenAndServe(sfui.ServerBindAddress, http.HandlerFunc(sfui.requestHandler))
}
func (sfui *SfUI) handleSignals() {
sigs := make(chan os.Signal, 1)
// catch all signals
signal.Notify(sigs)
go func() {
for sig := range sigs {
switch sig {
case syscall.SIGINT:
fallthrough
case syscall.SIGTERM:
fallthrough
case syscall.SIGHUP:
sfui.cleanUp()
os.Exit(0)
}
}
}()
}
func handleCmdLineFlags() (ActionInvoked bool) {
// Handle CmdLine Flags
var install bool
var uninstall bool
flag.BoolVar(&install, "install", false, "install SFUI")
flag.BoolVar(&uninstall, "uninstall", false, "uninstall SFUI")
flag.Parse()
if install {
ierr := InstallService()
if ierr != nil {
log.Println(ierr.Error())
}
ActionInvoked = true
}
if uninstall {
uierr := UnInstallService()
if uierr != nil {
log.Println(uierr.Error())
}
ActionInvoked = true
}
return ActionInvoked
}
func (sfui *SfUI) cleanUp() {
sfui.DisableClientAccess()
log.Println("Disconnecting all clients...")
sfui.RemoveAllClients()
log.Println("Flushing Log Queue...")
if sfui.EnableMetricLogging {
MLogger.FlushQueue()
GeoIpClose()
}
BanDB.Save()
releaseRunLock()
}
func (sfui *SfUI) handleLogin(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json")
data, err := io.ReadAll(io.LimitReader(r.Body, 2048))
if err == nil {
clientIp := sfui.getClientAddr(r)
isBanned, reason := BanDB.IsBanned(clientIp)
if isBanned {
w.WriteHeader(http.StatusUnavailableForLegalReasons)
w.Write([]byte(fmt.Sprintf(`{"status":"Banned", "reason" : "%s"}`, reason)))
return
}
loginReq := TermRequest{}
if json.Unmarshal(data, &loginReq) == nil {
loginReq.ClientIp = clientIp
if loginReq.NewInstance {
secret := sfui.getEndpointNameRR() + "-"
secret += sfui.generateSecret(&loginReq)
w.WriteHeader(http.StatusOK)
termRes := TermResponse{
Status: "OK",
Secret: secret,
}
response, _ := json.Marshal(termRes)
w.Write(response)
if sfui.EnableMetricLogging {
go MLogger.AddLogEntry(&Metric{
Type: "NewAccount",
Referrer: r.Header.Get("Referer"),
Country: GetCountryByIp(loginReq.ClientIp),
UserUid: getClientId(loginReq.ClientIp),
TimeZone: r.Header.Get("TimeZone"),
})
}
return
}
if sfui.ValidSecret(loginReq.Secret) {
client, cerr := sfui.GetClient(loginReq.Secret)
isDuplicate := false
if cerr == nil {
// 1 active and non matching tab ids - Duplicate
// 2 active and matching tab ids - Non Duplicate
if client.TabId != nil && client.ClientActive != nil {
winIdMatches := (*client.TabId == loginReq.TabId)
if client.ClientActive.Load() && !winIdMatches {
isDuplicate = true
}
// 3 inactive and matching tab ids - Non Duplicate
// 4 inactive and non matching tab ids - Non Duplicate, set new tab id
if !client.ClientActive.Load() && !winIdMatches {
client.SetTabId(loginReq.TabId)
}
}
} else {
// start a new client
go func() {
client, cerr := sfui.GetExistingClientOrMakeNew(loginReq.Secret, loginReq.ClientIp)
if cerr == nil {
client.SetTabId(loginReq.TabId)
}
}()
if sfui.EnableMetricLogging {
go MLogger.AddLogEntry(&Metric{
Type: "Login",
Referrer: r.Header.Get("Referer"),
Country: GetCountryByIp(loginReq.ClientIp),
UserUid: getClientId(loginReq.ClientIp),
TimeZone: r.Header.Get("TimeZone"),
})
}
}
w.WriteHeader(http.StatusOK)
termRes := TermResponse{
Status: "OK",
IsDuplicate: isDuplicate,
}
response, _ := json.Marshal(termRes)
w.Write(response)
return
}
}
}
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(`{"status":"Internal Server Error"}`))
}
func (sfui *SfUI) handleLogout(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json")
data, err := io.ReadAll(io.LimitReader(r.Body, 2048))
if err == nil {
logoutReq := TermRequest{}
if json.Unmarshal(data, &logoutReq) == nil {
if sfui.ValidSecret(logoutReq.Secret) {
// Remove the client connection
client, err := sfui.GetClient(logoutReq.Secret)
if err == nil { // Client exists
sfui.RemoveClient(&client)
}
w.WriteHeader(http.StatusOK)
termRes := TermResponse{
Status: "OK",
}
response, _ := json.Marshal(termRes)
w.Write(response)
return
}
}
}
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(`{"status":"Internal Server Error"}`))
}
// Split secret into endpoint name and actual-secret
// return the endpoint FQDN based on the name (ex: 8lgm -> return 8lgm.segfault.net)
// defaults to first available endpoint FQDN if name is not found.
func (sfui *SfUI) getEndpointAndSecret(secret string) (EndpointAddress string, ActualSecret string) {
secretParts := strings.Split(secret, "-") // secret is in the form "endpointname-randomsecretXXXXX"
if len(secretParts) > 1 {
endpointName := secretParts[0]
for _, address := range sfui.SfEndpoints {
if strings.Contains(address, endpointName) {
return address, secretParts[1]
}
}
}
return sfui.SfEndpoints[0], secret
}
func (sfui *SfUI) getEndpointNameRR() string {
selected := sfui.EndpointSelector.Load()
if selected > sfui.NoOfEndpoints-1 {
sfui.EndpointSelector.Store(0)
selected = 0
}
sfui.EndpointSelector.Add(1)
eparts := strings.Split(sfui.SfEndpoints[selected], ".")
if len(eparts) > 0 {
return eparts[0]
}
return ""
}