-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathmain.go
More file actions
347 lines (312 loc) · 17.4 KB
/
main.go
File metadata and controls
347 lines (312 loc) · 17.4 KB
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
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
package main
// NOTE: Before compiling, make sure you read the instructions regarding
// compilation with ImageMagick 6 vs. 7.
import (
"flag"
"html/template"
"net/http"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/gin-contrib/location"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/memstore"
"github.com/gin-gonic/gin"
"github.com/microcosm-cc/bluemonday"
"github.com/peterbourgon/diskv/v3"
_ "github.com/philippgille/gokv"
"github.com/philippgille/gokv/syncmap"
"github.com/vharitonsky/iniflags"
)
// Global variables
var (
// wLog, _ = syslog.Dial("", "", syslog.LOG_ERR | syslog.LOG_LOCAL0, "gOSWI") // write to syslog.
PathToStaticFiles, cacheDir string
GOSWIstore syncmap.Store // Stores tokens for password reset links.
imageCache *diskv.Diskv // As the name says: this is the image cache (gwyneth 20200726)
slideshow []string // Slideshow is a slice of strings representing all images for the splash-screen slideshow.
bluemondaySafeHTML = bluemonday.UGCPolicy() // Initialise bluemonday: this is the standard, we might do it a little more restrictive (gwyneth 20200815)
router *gin.Engine // Declared as global because it's supposed to be a singleton Gin router, available on all files here.
)
// Notification methods (optional, build with `systemd` to include the notification system).
// Since v0.9.3
// Is a valid `systemd` running?
var activeSystemd bool
// Enum of possible notification types sent by app to systemd.
type notificationType int8
const (
// Sent when app is launching and loading configuration, not yet ready
appReloading notificationType = iota // Configuration being loaded, initialisation being done...)
appReady // Application is now ready to accept requests.
appStopping // Application had a (planned) shutdown.
appStoppingError // Emergency shutdown, emits fatal error, possibly exiting with code 126.
)
// Configure the complex flagging system, which will also require loading from `config.ini`
// Note: flag.Tail() offers us all parameters at the end of the command line, we will use that to generate a list of images for the slideshow, but we cannot use that using pkg iniflags (gwyneth 20200711).
// main starts here.
func main() {
// Full configuration, which can be retrieved via flags, configure file, environment (TBD)...
// Note: this is now a singleton, assigned on configtype.go
config = Config{
"local": flag.String("local", "", "serve as webserver, example: 0.0.0.0:8000"),
"dsn": flag.String("dsn", "", "DSN for calling MySQL database"),
"templatePath": flag.String("templatePath", "", "Path to where the templates are stored (with trailing slash) - leave empty for autodetect"),
"ginMode": flag.String("ginMode", "release", "Default is 'release' (production-level logging) but you can set it to 'debug' (more logging)"),
"tlsCRT": flag.String("tlsCRT", "", "Absolute path for CRT certificate for TLS; leave empty for HTTP"),
"tlsKEY": flag.String("tlsKEY", "", "Absolute path for private key for TLS; leave empty for HTTP"),
"author": flag.String("author", "--nobody--", "Author name"),
"description": flag.String("description", "gOSWI", "Description for each page"),
"titleCommon": flag.String("titleCommon", "gOSWI", "Common part of the title for each page (usually the brand)"),
"cookieStore": flag.String("cookieStore", randomBase64String(64), "Secret random string required for the cookie store (will be generated randomly if unset)"),
"SMTPhost": flag.String("SMTPhost", "localhost", "Hostname of the SMTP server (for sending password reset tokens via email)"),
"gOSWIemail": flag.String("gOSWIemail", "manager@localhost", "Email address for the grid manager (must be valid and accepted by SMTPhost)"),
"gOSWIpassword": flag.String("gOSWIpassword", "", "Password for the grid manager (must be valid and accepted by SMTPhost)"),
"logo": flag.String("logo", "/assets/logos/gOSWI%20logo.svg", "Logo (SVG preferred); defaults to gOSWI logo"),
"logoTitle": flag.String("logoTitle", "gOSWI", "Title for the URL on the logo"),
"sidebarCollapsed": flag.String("sidebarCollapsed", "false", "true for a collapsed sidebar on startup"),
"slides": flag.String("slides", "", "Comma-separated list of URLs for slideshow images"),
"convertExt": flag.String("convertExt", ".png", "Filename extension or type for cached resources (depends on the converter actually supporting this particular extension; if not, conversion will fail)"),
"cache": flag.String("cache", "/cache/", "File path to the assets cache"),
"assetServer": flag.String("assetServer", "http://localhost:8003", "URL to OpenSimulator asset server (no trailing slash)"),
"ROBUSTserver": flag.String("ROBUSTserver", "http://localhost:8002", "URL to OpenSimulator ROBUST server (no trailing slash)"),
"gridstats": flag.String("gridstats", "/stats", "Relative path to where the Grid statistics are stored (default: /stats)"),
"ImageMagickCommand": flag.String("ImageMagickCommand", "", "Absolute path to ImageMagick command `imagick`; empty means search $PATH"),
"ImageMagickParams": flag.String("ImageMagickParams", "", "Parameters to be sent to ImageMagick command (EXPERIMENTAL!)"),
"NewRelicAppName": flag.String("NewRelicAppName", "", "Name of your New Relic application (empty: disabled)"),
"NewRelicLicenseKey": flag.String("NewRelicLicenseKey", "", "Your New Relic license key"),
"OTelServiceName": flag.String("OTelServiceName", "", "Name of your OpenTelemetry service (empty: disabled)"),
"OTelCollectorURL": flag.String("OTelCollectorURL", "", "URL for OpenTelemetry Collector (default: none)"),
"OTelInsecureMode": flag.String("OTelInsecureMode", "", "OpenTelemetry Insecure Mode (default: empty, i.e. secure mode)"),
}
notify(appReloading)
// figure out where the configuration is
_, callerFile, _, _ := runtime.Caller(0)
PathToStaticFiles = filepath.Dir(callerFile)
config.LogDebug("executable path is now ", PathToStaticFiles, " while the callerFile is ", callerFile)
// check if we have a config.ini on the same path as the binary; if not, try to get it to wherever PathToStaticFiles is pointing to
iniflags.SetConfigFile(filepath.Join(PathToStaticFiles, "/config.ini"))
// start parsing configuration
iniflags.Parse()
// initialise slideshow (all the URLs should be at the end of the commandline)
slideshow = strings.Split(*config["slides"], ",")
if len(slideshow) == 0 {
slideshow = append(
slideshow,
"https://source.unsplash.com/K4mSJ7kc0As/700x300",
"https://source.unsplash.com/Mv9hjnEUHR4/700x300",
"https://source.unsplash.com/oWTW-jNGl9I/700x300",
)
} else {
for i := range len(slideshow) {
slideshow[i] = strings.TrimSpace(slideshow[i]) // this will respect the order
}
}
config.LogTracef("%d slide(s) have been set to: %+v", len(slideshow), slideshow)
// cookieStore MUST be set to a random string! (gwyneth 20200628)
// we might also check for weak security strings on the configuration
if *config["cookieStore"] == "" {
config.LogFatal("[ERROR] Empty random string for 'cookieStore'; please set it either on the .INI file or pass it via a flag!\nAborting for security reasons.")
}
// prepare Gin router/render — first, set it to debug or release (release is default).
// Note: incidentally, this will actually also set the logging level.
if *config["ginMode"] == "debug" || *config["ginMode"] == "trace" {
gin.SetMode(gin.DebugMode)
} else {
gin.SetMode(gin.ReleaseMode)
}
router = gin.Default()
router.Delims("{{", "}}") // stick to default delims for Go templates
router.SetFuncMap(template.FuncMap{
"bitTest": bitTest,
})
// figure out where the templates are
if *config["templatePath"] != "" {
if !strings.HasSuffix(*config["templatePath"], "/") {
*config["templatePath"] += "/"
}
} else {
*config["templatePath"] = "/templates/"
}
systemdNotify("STATUS=loading templates")
router.LoadHTMLGlob(filepath.Join(PathToStaticFiles, *config["templatePath"], "*.tpl"))
//router.HTMLRender = createMyRender()
// router.Use(setUserStatus()) // this will allow us to 'see' if the user is authenticated or not
systemdNotify("STATUS=starting telemetry")
// If we have a telemetry option compiled in (New Relic, Open Telemetry...) we call it hre.
// Note that this is *not* a requirement, the default is a no-op.
// But if set, it will add some middleware to Gin.
initTelemetry()
store := memstore.NewStore([]byte(*config["cookieStore"])) // now using sessions (Gorilla sessions via Gin extension) stored in memory (gwyneth 20200812)
router.Use(sessions.Sessions("goswisession", store))
systemdNotify("STATUS=initialising various caches")
// Initialise the diskv storage on the cache directory (gwyneth 20200724)
imageCache = diskv.New(diskv.Options{
// BasePath: *config["cache"],
BasePath: PathToStaticFiles,
AdvancedTransform: imageCacheTransform, // currently defined on profile.go (gwyneth 20200724)
InverseTransform: imageCacheInverseTransform,
CacheSizeMax: 100 * 1024 * 1024, // possibly will become a config.ini option
})
// Prepare a directory for the cache (i.e. create it if it doesn't exist) (gwyneth 20200718)
// Note: in the future we might use diskv for the cache and pretty much ignore this
// Note 2: We *are* using diskv for the cache, but allegedly this is still 'needed'. (gwyneth 20200909)
cacheDir := filepath.Join(PathToStaticFiles, *config["cache"])
err := os.MkdirAll(cacheDir, os.ModePerm)
if err != nil {
config.LogWarn("Creating/accessing cache directory", cacheDir, "returned error:", err)
// we might not be able to use a cache if this doesn't work
// so we'll try to create a temporary cache instead
cacheDir = filepath.Join(os.TempDir(), *config["cache"])
err = os.MkdirAll(cacheDir, os.ModePerm)
if err != nil {
config.LogWarn("Creating temporary cache directory", cacheDir, "also returned error:", err)
}
}
systemdNotify("STATUS=setting up routing paths")
// Gin router configuration starts here
// Static stuff (will probably do it via nginx)
router.Static("/lib", filepath.Join(PathToStaticFiles, "/lib"))
router.Static("/assets", filepath.Join(PathToStaticFiles, "/assets"))
if *config["cache"] != "" {
router.Static("/cache", cacheDir)
config.LogInfo("Cache directory set up at", cacheDir)
} else {
config.LogError("Could not access or create cache directory with", cacheDir, "— this means there will be trouble ahead... error was (possibly)", err)
}
router.StaticFile("/favicon.ico", filepath.Join(PathToStaticFiles, "/assets/favicons/favicon.ico"))
router.StaticFile("/browserconfig.xml", filepath.Join(PathToStaticFiles, "/assets/favicons/browserconfig.xml"))
router.StaticFile("/site.webmanifest", filepath.Join(PathToStaticFiles, "/assets/favicons/site.webmanifest"))
router.GET("/", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.tpl", environment(c,
gin.H{
"titleCommon": *config["titleCommon"] + " - Home",
}))
})
router.GET("/welcome", GetStats)
router.GET("/about", func(c *gin.Context) {
c.HTML(http.StatusOK, "about.tpl", environment(c,
gin.H{
"titleCommon": *config["titleCommon"] + " - About",
}))
})
router.GET("/help", func(c *gin.Context) {
c.HTML(http.StatusOK, "help.tpl", environment(c,
gin.H{
"titleCommon": *config["titleCommon"] + " - Help",
}))
})
// the following are not implemented yet
router.GET("/economy", func(c *gin.Context) {
c.HTML(http.StatusNotFound, "404.tpl", environment(c,
gin.H{
"titleCommon": *config["titleCommon"] + " - Economy",
}))
})
router.GET("/search", func(c *gin.Context) {
c.HTML(http.StatusNotFound, "404.tpl", environment(c,
gin.H{
"titleCommon": *config["titleCommon"] + " - Search results",
}))
})
// adding routers for grid statistics; this requires a bit of tweaking, since:
// 1) we may have conflicting /stats directories (i.e. /stats for the reverse proxy server itself);
// 2) We need to handle both the case when someone wants the HTML page, and the cases when requiring a parameter for JSON, XML, YAML etc.;
// 3) Gin does not give us the headers by default, so we need to add middleware for that;
// 4) The middleware seems to be buggy and the default returns the IP address of the client, not of the server!
//
var locationMiddleware = location.New(location.Config{
Headers: location.Headers{Host: "X-Forwarded-Host"},
})
router.GET(*config["gridstats"]+"/:ResponseFormatType", locationMiddleware, OSSimpleStats) // plus middleware to get hostname
router.GET(*config["gridstats"], locationMiddleware, OSSimpleStats) // router without any specific format returns HTML (hopefully)
userRoutes := router.Group("/user")
{
userRoutes.POST("/register", ensureNotLoggedIn(), registerNewUser)
userRoutes.GET("/register", ensureNotLoggedIn(), func(c *gin.Context) {
// we show a 404 error for now
c.HTML(http.StatusOK, "404.tpl", environment(c,
gin.H{
"errorcode": http.StatusForbidden,
"errortext": "Access denied",
"errorbody": "Sorry, this grid is not accepting new registrations.",
"titleCommon": *config["titleCommon"] + " - Register new user",
"logintemplate": false,
}))
})
userRoutes.POST("/change-password", ensureLoggedIn(), changePassword)
userRoutes.GET("/change-password", ensureLoggedIn(), func(c *gin.Context) {
c.HTML(http.StatusOK, "change-password.tpl", environment(c, gin.H{
"titleCommon": *config["titleCommon"] + " - Change Password",
"logintemplate": true,
}))
})
userRoutes.POST("/reset-password", ensureNotLoggedIn(), resetPassword)
userRoutes.GET("/reset-password", ensureNotLoggedIn(), func(c *gin.Context) {
c.HTML(http.StatusOK, "reset-password.tpl", environment(c, gin.H{
"titleCommon": *config["titleCommon"] + " - Reset Password",
"logintemplate": true,
}))
})
userRoutes.GET("/token/:token", ensureNotLoggedIn(), checkTokenForPasswordReset)
userRoutes.POST("/login", ensureNotLoggedIn(), performLogin)
userRoutes.GET("/login", ensureNotLoggedIn(), func(c *gin.Context) {
c.HTML(http.StatusOK, "login.tpl", environment(c, gin.H{
"Debug": false, // probably unnecessary
"titleCommon": *config["titleCommon"] + "Welcome!",
"logintemplate": true,
}))
})
userRoutes.GET("/logout", ensureLoggedIn(), logout)
userRoutes.GET("/profile", ensureLoggedIn(), GetProfile)
userRoutes.POST("/profile", ensureLoggedIn(), saveProfile)
userRoutes.GET("/offline-messages", ensureLoggedIn(), getOfflineMessages)
userRoutes.GET("/feed-messages", ensureLoggedIn(), getFeedMessages)
}
router.GET("/mapdata", GetMapData)
router.GET("/avatar/:hash", Libravatar) // note that there might be further parameters beyond the hash (gwyneth 20200908)
router.NoRoute(func(c *gin.Context) {
c.HTML(http.StatusNotFound, "404.tpl", environment(c, gin.H{
"titleCommon": *config["titleCommon"] + " - 404",
}))
})
router.NoMethod(func(c *gin.Context) {
c.HTML(http.StatusNotFound, "404.tpl", environment(c, gin.H{
"titleCommon": *config["titleCommon"] + " - 404",
}))
})
// Ping handler (who knows, it might be useful in some contexts... such as Let's Encrypt certificates
router.GET("/ping", func(c *gin.Context) {
c.String(http.StatusOK, "pong")
})
// initialize our KV store (gwyneth 20200705)
// Note that the current implementation is a goroutine-safe, in-memory solution, without persistent storage —
// for now, we will not really need that, since it only stores relatively 'temporary' things, and, if all else fails,
// you can redo those things again (e.g. tokens for password reset)
GOSWIstore = syncmap.NewStore(syncmap.DefaultOptions)
defer GOSWIstore.Close() // according to the developer, stores should be closed when not in usage, since certain store implementations may require an explicit close to deallocate memory, free database resources, etc. (20200705)
// NOTE: ImageMagick is now initialised separately, with the build tag `imagick`.
// See imagick_compiled.go and the README.md. (gwyneth 20251027)
// Inform systemd (if running from systemd) that we've finished initialisation
// and configuration. and are now ready to start accepting requests.
notify(appReady)
// Deal with the way gOSWI was called, namely if it uses a default port, uses TLS (=HTTPS), etc.
if *config["local"] == "" {
if *config["tlsCRT"] != "" && *config["tlsKEY"] != "" {
err := router.RunTLS(":8033", *config["tlsCRT"], *config["tlsKEY"]) // if it works, it will never return
if err != nil {
config.LogWarnf("Could not run with TLS; either the certificate %q was not found, or the private key %q was not found, or either [maybe even both] are invalid.\n", *config["tlsCRT"], *config["tlsKEY"])
config.LogInfo("Running _without_ TLS on the usual port")
config.LogFatal(router.Run(":8033"))
}
} else {
config.LogInfo("Running with standard HTTP on the usual port, no TLS configuration detected")
config.LogFatal(router.Run(":8033"))
}
} else {
config.LogFatal(router.Run(*config["local"]))
}
// if we are here, router.Run() failed with an error
config.LogFatal("Boom, something went wrong! (or maybe this was merely stopped, I don't know)")
notify(appStoppingError)
}