From 40d64b8ecf24da1ced726755785cd30c854e9b58 Mon Sep 17 00:00:00 2001
From: Captain ALM
Date: Sun, 13 Aug 2023 20:19:47 +0100
Subject: [PATCH] Initial commit.
---
.gitattributes | 1 +
.gitignore | 21 ++
.idea/.gitignore | 8 +
.idea/discord.xml | 7 +
.idea/mc-webserver.iml | 9 +
.idea/modules.xml | 8 +
.woodpecker/build.yml | 12 +
LICENSE | 29 ++
Makefile | 52 ++++
README.md | 11 +
conf/cache.go | 11 +
conf/config.go | 6 +
conf/listen.go | 8 +
conf/serve.go | 41 +++
config.example.yml | 14 +
go.mod | 10 +
goinfo.go.html | 313 ++++++++++++++++++++
pageHandler/get-router.go | 54 ++++
pageHandler/go-info-page.go | 174 +++++++++++
pageHandler/page-handler.go | 304 +++++++++++++++++++
pageHandler/page-provider.go | 14 +
pageHandler/pages.go | 18 ++
pageHandler/pages/index/data.go | 28 ++
pageHandler/pages/index/index-page.go | 193 ++++++++++++
pageHandler/pages/index/mc.go | 25 ++
pageHandler/pages/index/template-marshal.go | 202 +++++++++++++
pageHandler/utils/content-range-value.go | 78 +++++
pageHandler/utils/etag.go | 48 +++
pageHandler/utils/partial-range-writer.go | 47 +++
pageHandler/utils/process-preconditions.go | 153 ++++++++++
pageHandler/utils/utils.go | 45 +++
utils/info/conf-info.go | 6 +
utils/info/product-info.go | 13 +
utils/io/buffered-writer.go | 10 +
utils/io/counting-writer.go | 10 +
utils/yaml/date-type.go | 31 ++
wappmcstat/main.go | 148 +++++++++
wappmcstat/utils.go | 86 ++++++
38 files changed, 2248 insertions(+)
create mode 100644 .gitattributes
create mode 100644 .gitignore
create mode 100644 .idea/.gitignore
create mode 100644 .idea/discord.xml
create mode 100644 .idea/mc-webserver.iml
create mode 100644 .idea/modules.xml
create mode 100644 .woodpecker/build.yml
create mode 100644 LICENSE
create mode 100644 Makefile
create mode 100644 README.md
create mode 100644 conf/cache.go
create mode 100644 conf/config.go
create mode 100644 conf/listen.go
create mode 100644 conf/serve.go
create mode 100644 config.example.yml
create mode 100644 go.mod
create mode 100644 goinfo.go.html
create mode 100644 pageHandler/get-router.go
create mode 100644 pageHandler/go-info-page.go
create mode 100644 pageHandler/page-handler.go
create mode 100644 pageHandler/page-provider.go
create mode 100644 pageHandler/pages.go
create mode 100644 pageHandler/pages/index/data.go
create mode 100644 pageHandler/pages/index/index-page.go
create mode 100644 pageHandler/pages/index/mc.go
create mode 100644 pageHandler/pages/index/template-marshal.go
create mode 100644 pageHandler/utils/content-range-value.go
create mode 100644 pageHandler/utils/etag.go
create mode 100644 pageHandler/utils/partial-range-writer.go
create mode 100644 pageHandler/utils/process-preconditions.go
create mode 100644 pageHandler/utils/utils.go
create mode 100644 utils/info/conf-info.go
create mode 100644 utils/info/product-info.go
create mode 100644 utils/io/buffered-writer.go
create mode 100644 utils/io/counting-writer.go
create mode 100644 utils/yaml/date-type.go
create mode 100644 wappmcstat/main.go
create mode 100644 wappmcstat/utils.go
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..6313b56
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+* text=auto eol=lf
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c5a21c3
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,21 @@
+# Environment variables file
+.env
+
+# Distributable directory
+dist/
+
+# Test data and logs folders
+.data/
+.idea/dataSources.xml
+
+# CDN link
+cdn/
+cdn
+cdn_/
+cdn_
+
+# Config Link
+cnf/
+cnf
+cnf_/
+cnf_
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..13566b8
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,8 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/discord.xml b/.idea/discord.xml
new file mode 100644
index 0000000..d8e9561
--- /dev/null
+++ b/.idea/discord.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/mc-webserver.iml b/.idea/mc-webserver.iml
new file mode 100644
index 0000000..5e764c4
--- /dev/null
+++ b/.idea/mc-webserver.iml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..5998cf7
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.woodpecker/build.yml b/.woodpecker/build.yml
new file mode 100644
index 0000000..9cb0961
--- /dev/null
+++ b/.woodpecker/build.yml
@@ -0,0 +1,12 @@
+platform: linux/amd64
+
+pipeline:
+ format:
+ image: golang
+ commands:
+ - files=$(gofmt -l .) && echo "$files" && [ -z "$files" ]
+ build:
+ image: golang
+ commands:
+ - make build
+
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..7fbe392
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,29 @@
+BSD 3-Clause License
+
+Copyright (c) 2022, Captain ALM
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..cafd211
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,52 @@
+SHELL := /bin/bash
+PRODUCT_NAME := wappmcstat
+BIN := dist/${PRODUCT_NAME}
+DNAME := ${PRODUCT_NAME}_
+ENTRY_POINT := ./cmd/${PRODUCT_NAME}
+HASH := $(shell git rev-parse --short HEAD)
+COMMIT_DATE := $(shell git show -s --format=%ci ${HASH})
+BUILD_DATE := $(shell date '+%Y-%m-%d %H:%M:%S')
+VERSION := ${HASH}
+LD_FLAGS := -s -w -X 'main.buildVersion=${VERSION}' -X 'main.buildDate=${BUILD_DATE}' -X 'main.buildName=${PRODUCT_NAME}'
+COMP_BIN := go
+
+ifeq ($(OS),Windows_NT)
+ BIN := $(BIN).exe
+ DNAME := $(DNAME).exe
+endif
+
+.PHONY: build dev test clean deploy d
+
+build:
+ mkdir -p dist/
+ ${COMP_BIN} build -o "${BIN}" -ldflags="${LD_FLAGS}" ${ENTRY_POINT}
+
+dev:
+ mkdir -p dist/
+ ${COMP_BIN} build -tags debug -o "${BIN}" -ldflags="${LD_FLAGS}" ${ENTRY_POINT}
+ ./${BIN}
+
+test:
+ ${COMP_BIN} test
+
+clean:
+ ${COMP_BIN} clean
+ rm -r -f dist/
+
+deploy: build
+ sudo systemctl stop wappcityuni
+ sudo cp "${BIN}" /usr/local/bin
+ sudo cp *.go.html cnf
+ sudo cp *.go.yml cnf
+ sudo cp *.css cdn
+ sudo cp *.js cdn
+ sudo systemctl start wappcityuni
+
+d: build
+ sudo systemctl stop wappcityuni_
+ sudo cp "${BIN}" "/usr/local/bin/${DNAME}"
+ sudo cp *.go.html cnf_
+ sudo cp *.go.yml cnf_
+ sudo cp *.css cdn_
+ sudo cp *.js cdn_
+ sudo systemctl start wappcityuni_
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..7087cc9
--- /dev/null
+++ b/README.md
@@ -0,0 +1,11 @@
+# Captain ALM Minecraft Statistics WebServer
+
+[![Build Status](https://ci.mrmelon54.com/api/badges/alfred/mc-webserver/status.svg)](https://ci.mrmelon54.com/alfred/mc-webserver)
+
+This provides my template and cache supporting web / application server for minecraft server status.
+
+Maintainer:
+[Captain ALM](https://code.mrmelon54.com/alfred)
+
+License:
+[BSD 3-Clause](https://code.mrmelon54.com/alfred/mc-webserver/src/branch/master/LICENSE)
diff --git a/conf/cache.go b/conf/cache.go
new file mode 100644
index 0000000..8f8b254
--- /dev/null
+++ b/conf/cache.go
@@ -0,0 +1,11 @@
+package conf
+
+type CacheSettingsYaml struct {
+ EnableTemplateCaching bool `yaml:"enableTemplateCaching"`
+ EnableTemplateCachePurge bool `yaml:"enableTemplateCachePurge"`
+ EnableContentsCaching bool `yaml:"enableContentsCaching"`
+ EnableContentsCachePurge bool `yaml:"enableContentsCachePurge"`
+ MaxAge uint `yaml:"maxAge"`
+ NotModifiedResponseUsingLastModified bool `yaml:"notModifiedUsingLastModified"`
+ NotModifiedResponseUsingETags bool `yaml:"notModifiedUsingETags"`
+}
diff --git a/conf/config.go b/conf/config.go
new file mode 100644
index 0000000..8ac8c65
--- /dev/null
+++ b/conf/config.go
@@ -0,0 +1,6 @@
+package conf
+
+type ConfigYaml struct {
+ Listen ListenYaml `yaml:"listen"`
+ Serve ServeYaml `yaml:"serve"`
+}
diff --git a/conf/listen.go b/conf/listen.go
new file mode 100644
index 0000000..04ad4ce
--- /dev/null
+++ b/conf/listen.go
@@ -0,0 +1,8 @@
+package conf
+
+type ListenYaml struct {
+ Web string `yaml:"web"`
+ WebMethod string `yaml:"webMethod"`
+ WebNetwork string `yaml:"webNetwork"`
+ Identify bool `yaml:"identify"`
+}
diff --git a/conf/serve.go b/conf/serve.go
new file mode 100644
index 0000000..a034205
--- /dev/null
+++ b/conf/serve.go
@@ -0,0 +1,41 @@
+package conf
+
+import (
+ "os"
+ "path"
+ "path/filepath"
+ "strings"
+)
+
+type ServeYaml struct {
+ DataStorage string `yaml:"dataStorage"`
+ Domains []string `yaml:"domains"`
+ RangeSupported bool `yaml:"rangeSupported"`
+ EnableGoInfoPage bool `yaml:"enableGoInfoPage"`
+ CacheSettings CacheSettingsYaml `yaml:"cacheSettings"`
+}
+
+func (sy ServeYaml) GetDomainString() string {
+ if len(sy.Domains) == 0 {
+ return "all"
+ } else {
+ return strings.Join(sy.Domains, " ")
+ }
+}
+
+func (sy ServeYaml) GetDataStoragePath() string {
+ if sy.DataStorage == "" || !filepath.IsAbs(sy.DataStorage) {
+ wd, err := os.Getwd()
+ if err != nil {
+ return ""
+ } else {
+ if sy.DataStorage == "" {
+ return wd
+ } else {
+ return path.Join(wd, sy.DataStorage)
+ }
+ }
+ } else {
+ return sy.DataStorage
+ }
+}
diff --git a/config.example.yml b/config.example.yml
new file mode 100644
index 0000000..e49631e
--- /dev/null
+++ b/config.example.yml
@@ -0,0 +1,14 @@
+listen:
+ webNetwork: "tcp4"
+ web: ":8080"
+ webMethod: "http"
+ identify: true
+serve:
+ rangeSupported: true
+ enableGoInfoPage: true
+ cacheSettings:
+ enableContentsCaching: true
+ enableContentsCachePurge: true
+ maxAge: 3600
+ notModifiedUsingLastModified: true
+ notModifiedUsingETags: true
\ No newline at end of file
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..516707f
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,10 @@
+module golang.captainalm.com/mc-webserver
+
+go 1.18
+
+require (
+ github.com/gorilla/mux v1.8.0
+ github.com/joho/godotenv v1.4.0
+ gopkg.in/yaml.v3 v3.0.1
+ github.com/mcstatus-io/mcutil v1.3.1
+)
diff --git a/goinfo.go.html b/goinfo.go.html
new file mode 100644
index 0000000..740966a
--- /dev/null
+++ b/goinfo.go.html
@@ -0,0 +1,313 @@
+
+
+
+
+
+
+ Go Info
+
+
+
+
+
+
{{ .GoVersion }} - {{ .ProductName }}
+
+
+
+ {{ if .FullOutput }}
+
+{{ else }}
+
+{{ end }}
+
+
+
+
+ Product Name |
+ {{ .ProductName }} |
+
+
+ Product Description |
+ {{ .ProductDescription }} |
+
+
+ Product License |
+ BSD 3-Clause License |
+
+
+ Product Location |
+ {{ .ProductLocation }} |
+
+
+ Build Commit |
+ #{{ .BuildVersion }} |
+
+
+ Build Date |
+ {{ .BuildDate }} |
+
+
+ Working Directory |
+ {{ .WorkingDirectory }} |
+
+ {{ if .FullOutput }}
+
+ Process ID |
+ {{ .ProcessID }} |
+
+
+ Parent Process ID |
+ {{ .ParentProcessID }} |
+
+ {{ end }}
+
+
+
+
+
+ Go Version |
+ {{ .GoVersion }} |
+
+
+ Go Toolchain |
+ {{ .Compiler }} |
+
+
+ GOROOT |
+ {{ .GoRoot }} |
+
+
+ GOMAXPROCS |
+ {{ .GoMaxProcs }} |
+
+
+ Go Routine Count |
+ {{ .GoRoutineNum }} |
+
+
+ Go c go call Count |
+ {{ .GoCGoCallNum }} |
+
+
+
+
+
+
+ Hostname |
+ {{ .Hostname }} |
+
+
+ Operating System |
+ {{ .GoOS }} |
+
+
+ Architecture |
+ {{ .GoArch }} |
+
+
+ Number of Cores |
+ {{ .NumCPU }} |
+
+
+ Memory Page Size |
+ {{ .PageSize }} |
+
+
+
+
+
+
+ Listen Type |
+ {{ .ListenSettings.WebNetwork }} |
+
+
+ Listening Address |
+ {{ .ListenSettings.Web }} |
+
+
+ Listening Method |
+ {{ .ListenSettings.WebMethod }} |
+
+
+ Identifying |
+ {{ .ListenSettings.Identify }} |
+
+ {{ if and .FullOutput .ListenSettings.Identify }}
+
+ Server |
+ Clerie Gilbert |
+
+
+ Powered By |
+ Love |
+
+
+ Friendly |
+ True |
+
+ {{ end }}
+
+
+
+
+
+ Template Storage Path |
+ {{ .ServeSettings.GetDataStoragePath }} |
+
+
+ Served Domains |
+ {{ .ServeSettings.GetDomainString }} |
+
+
+ Range Supported |
+ {{ .ServeSettings.RangeSupported }} |
+
+
+
+{{ if .FullOutput }}
+
+
+
+ Enable Template Caching |
+ {{ .ServeSettings.CacheSettings.EnableTemplateCaching }} |
+
+
+ Enable Template Cache Purge |
+ {{ .ServeSettings.CacheSettings.EnableTemplateCachePurge }} |
+
+
+ Enable Content Caching |
+ {{ .ServeSettings.CacheSettings.EnableContentsCaching }} |
+
+
+ Enable Content Cache Purge |
+ {{ .ServeSettings.CacheSettings.EnableContentsCachePurge }} |
+
+
+ Max Age |
+ {{ .ServeSettings.CacheSettings.MaxAge }} |
+
+
+ Enable Last Modified Precondition Support |
+ {{ .ServeSettings.CacheSettings.NotModifiedResponseUsingLastModified }} |
+
+
+ Enable ETag Precondition Support |
+ {{ .ServeSettings.CacheSettings.NotModifiedResponseUsingETags }} |
+
+
+
+
+
+
+ Environment Variables |
+
+ {{ range .Environment }}
+
+ {{ . }} |
+
+ {{ end }}
+
+
+{{ end }}
+
+
+
+ Number of Registered Pages |
+ {{ len .RegisteredPages }} |
+
+
+
+{{ if and .FullOutput (not (eq (len .RegisteredPages) 0)) }}
+
+
+
+ Registered Pages |
+
+ {{ range .RegisteredPages }}
+
+ {{ . }} |
+
+ {{ end }}
+
+
+{{ end }}
+
+
+
+
+ Number of Cached Pages |
+ {{ len .CachedPages }} |
+
+
+
+{{ if and .FullOutput (not (eq (len .CachedPages) 0)) }}
+
+
+
+ Cached Pages |
+
+ {{ range .CachedPages }}
+
+ {{ . }} |
+
+ {{ end }}
+
+
+{{ end }}
+
+
+ {{ if .FullOutput }}
+
+{{ else }}
+
+{{ end }}
+
+
+
\ No newline at end of file
diff --git a/pageHandler/get-router.go b/pageHandler/get-router.go
new file mode 100644
index 0000000..61fdc1f
--- /dev/null
+++ b/pageHandler/get-router.go
@@ -0,0 +1,54 @@
+package pageHandler
+
+import (
+ "github.com/gorilla/mux"
+ "golang.captainalm.com/mc-webserver/conf"
+ "golang.captainalm.com/mc-webserver/pageHandler/utils"
+ "net/http"
+)
+
+var theRouter *mux.Router
+var thePageHandler *PageHandler
+
+func GetRouter(config conf.ConfigYaml) http.Handler {
+ if theRouter == nil {
+ theRouter = mux.NewRouter()
+ if thePageHandler == nil {
+ thePageHandler = NewPageHandler(config.Serve)
+ }
+ if len(config.Serve.Domains) == 0 {
+ theRouter.PathPrefix("/").HandlerFunc(thePageHandler.ServeHTTP)
+ } else {
+ for _, domain := range config.Serve.Domains {
+ theRouter.Host(domain).HandlerFunc(thePageHandler.ServeHTTP)
+ }
+ theRouter.PathPrefix("/").HandlerFunc(domainNotAllowed)
+ }
+ if config.Listen.Identify {
+ theRouter.Use(headerMiddleware)
+ }
+ }
+ return theRouter
+}
+
+func domainNotAllowed(rw http.ResponseWriter, req *http.Request) {
+ if req.Method == http.MethodGet || req.Method == http.MethodHead {
+ utils.WriteResponseHeaderCanWriteBody(req.Method, rw, http.StatusNotFound, "Domain Not Allowed")
+ } else {
+ rw.Header().Set("Allow", http.MethodOptions+", "+http.MethodGet+", "+http.MethodHead)
+ if req.Method == http.MethodOptions {
+ utils.WriteResponseHeaderCanWriteBody(req.Method, rw, http.StatusOK, "")
+ } else {
+ utils.WriteResponseHeaderCanWriteBody(req.Method, rw, http.StatusMethodNotAllowed, "")
+ }
+ }
+}
+
+func headerMiddleware(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Server", "Clerie Gilbert")
+ w.Header().Set("X-Powered-By", "Love")
+ w.Header().Set("X-Friendly", "True")
+ next.ServeHTTP(w, r)
+ })
+}
diff --git a/pageHandler/go-info-page.go b/pageHandler/go-info-page.go
new file mode 100644
index 0000000..524a3fb
--- /dev/null
+++ b/pageHandler/go-info-page.go
@@ -0,0 +1,174 @@
+package pageHandler
+
+import (
+ "golang.captainalm.com/mc-webserver/conf"
+ "golang.captainalm.com/mc-webserver/utils/info"
+ "golang.captainalm.com/mc-webserver/utils/io"
+ "html/template"
+ "net/url"
+ "os"
+ "path"
+ "runtime"
+ "sync"
+ "time"
+)
+
+const templateName = "goinfo.go.html"
+
+func newGoInfoPage(handlerIn *PageHandler, dataStore string, cacheTemplates bool) *goInfoPage {
+ var ptm *sync.Mutex
+ if cacheTemplates {
+ ptm = &sync.Mutex{}
+ }
+ pageToReturn := &goInfoPage{
+ Handler: handlerIn,
+ DataStore: dataStore,
+ PageTemplateMutex: ptm,
+ }
+ return pageToReturn
+}
+
+type goInfoPage struct {
+ Handler *PageHandler
+ DataStore string
+ PageTemplateMutex *sync.Mutex
+ PageTemplate *template.Template
+}
+
+func (gipg *goInfoPage) GetCacheIDExtension(urlParameters url.Values) string {
+ if urlParameters.Has("full") {
+ return "full"
+ } else {
+ return ""
+ }
+}
+
+type goInfoTemplateMarshal struct {
+ FullOutput bool
+ RegisteredPages []string
+ CachedPages []string
+ ProcessID int
+ ParentProcessID int
+ ProductLocation string
+ ProductName string
+ ProductDescription string
+ BuildVersion string
+ BuildDate string
+ WorkingDirectory string
+ Hostname string
+ PageSize int
+ GoVersion string
+ GoRoutineNum int
+ GoCGoCallNum int64
+ NumCPU int
+ GoRoot string
+ GoMaxProcs int
+ Compiler string
+ GoArch string
+ GoOS string
+ ListenSettings conf.ListenYaml
+ ServeSettings conf.ServeYaml
+ Environment []string
+}
+
+func (gipg *goInfoPage) GetPath() string {
+ return "/goinfo.go"
+}
+
+func (gipg *goInfoPage) GetLastModified() time.Time {
+ return time.Now()
+}
+
+func (gipg *goInfoPage) GetContents(urlParameters url.Values) (contentType string, contents []byte, canCache bool) {
+ theTemplate, err := gipg.getPageTemplate()
+ if err != nil {
+ return "text/plain", []byte("Cannot Get Info.\r\n" + err.Error()), false
+ }
+ var regPages []string
+ var cacPages []string
+ env := make([]string, 0)
+ if urlParameters.Has("full") {
+ regPages = gipg.Handler.GetRegisteredPages()
+ cacPages = gipg.Handler.GetCachedPages()
+ env = os.Environ()
+ } else {
+ regPages = make([]string, len(gipg.Handler.PageProviders))
+ cacPages = make([]string, gipg.Handler.GetNumberOfCachedPages())
+ }
+ theBuffer := &io.BufferedWriter{}
+ err = theTemplate.ExecuteTemplate(theBuffer, templateName, &goInfoTemplateMarshal{
+ FullOutput: urlParameters.Has("full"),
+ RegisteredPages: regPages,
+ CachedPages: cacPages,
+ ProcessID: os.Getpid(),
+ ParentProcessID: os.Getppid(),
+ ProductLocation: getStringOrError(os.Executable),
+ ProductName: info.BuildName,
+ ProductDescription: info.BuildDescription,
+ BuildVersion: info.BuildVersion,
+ BuildDate: info.BuildDate,
+ WorkingDirectory: getStringOrError(os.Getwd),
+ Hostname: getStringOrError(os.Hostname),
+ PageSize: os.Getpagesize(),
+ GoVersion: runtime.Version(),
+ GoRoutineNum: runtime.NumGoroutine(),
+ GoCGoCallNum: runtime.NumCgoCall(),
+ NumCPU: runtime.NumCPU(),
+ GoRoot: runtime.GOROOT(),
+ GoMaxProcs: runtime.GOMAXPROCS(0),
+ Compiler: runtime.Compiler,
+ GoArch: runtime.GOARCH,
+ GoOS: runtime.GOOS,
+ ListenSettings: info.ListenSettings,
+ ServeSettings: info.ServeSettings,
+ Environment: env,
+ })
+ if err != nil {
+ return "text/plain", []byte("Cannot Get Info.\r\n" + err.Error()), false
+ }
+ return "text/html", theBuffer.Data, false
+}
+
+func (gipg *goInfoPage) PurgeTemplate() {
+ if gipg.PageTemplateMutex != nil {
+ gipg.PageTemplateMutex.Lock()
+ gipg.PageTemplate = nil
+ gipg.PageTemplateMutex.Unlock()
+ }
+}
+
+func (gipg *goInfoPage) getPageTemplate() (*template.Template, error) {
+ if gipg.PageTemplateMutex != nil {
+ gipg.PageTemplateMutex.Lock()
+ defer gipg.PageTemplateMutex.Unlock()
+ }
+ if gipg.PageTemplate == nil {
+ thePath := templateName
+ if gipg.DataStore != "" {
+ thePath = path.Join(gipg.DataStore, thePath)
+ }
+ loadedData, err := os.ReadFile(thePath)
+ if err != nil {
+ return nil, err
+ }
+ tmpl, err := template.New(templateName).Parse(string(loadedData))
+ if err != nil {
+ return nil, err
+ }
+ if gipg.PageTemplateMutex != nil {
+ gipg.PageTemplate = tmpl
+ }
+ return tmpl, nil
+ } else {
+ return gipg.PageTemplate, nil
+ }
+}
+
+func getStringOrError(funcIn func() (string, error)) string {
+ toReturn, err := funcIn()
+ if err == nil {
+ return toReturn
+ } else {
+ return "Error: " + err.Error()
+ }
+}
diff --git a/pageHandler/page-handler.go b/pageHandler/page-handler.go
new file mode 100644
index 0000000..fcb7b6c
--- /dev/null
+++ b/pageHandler/page-handler.go
@@ -0,0 +1,304 @@
+package pageHandler
+
+import (
+ "golang.captainalm.com/mc-webserver/conf"
+ "golang.captainalm.com/mc-webserver/pageHandler/utils"
+ "io"
+ "mime/multipart"
+ "net/http"
+ "net/textproto"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+)
+
+const indexName = "index.go"
+
+type PageHandler struct {
+ PageContentsCache map[string]*CachedPage
+ PageProviders map[string]PageProvider
+ pageContentsCacheRWMutex *sync.RWMutex
+ RangeSupported bool
+ CacheSettings conf.CacheSettingsYaml
+}
+
+type CachedPage struct {
+ Content []byte
+ ContentType string
+ LastMod time.Time
+}
+
+func NewPageHandler(config conf.ServeYaml) *PageHandler {
+ var thePCCMap map[string]*CachedPage
+ var theMutex *sync.RWMutex
+ if config.CacheSettings.EnableContentsCaching {
+ thePCCMap = make(map[string]*CachedPage)
+ theMutex = &sync.RWMutex{}
+ }
+ toReturn := &PageHandler{
+ PageContentsCache: thePCCMap,
+ pageContentsCacheRWMutex: theMutex,
+ RangeSupported: config.RangeSupported,
+ CacheSettings: config.CacheSettings,
+ }
+ if config.EnableGoInfoPage {
+ toReturn.PageProviders = GetProviders(config.CacheSettings.EnableTemplateCaching, config.DataStorage, toReturn)
+ } else {
+ toReturn.PageProviders = GetProviders(config.CacheSettings.EnableTemplateCaching, config.DataStorage, nil)
+ }
+ return toReturn
+}
+
+func (ph *PageHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
+ actualPagePath := ""
+ if strings.HasSuffix(request.URL.Path, "/") {
+ if strings.HasSuffix(request.URL.Path, ".go/") {
+ actualPagePath = strings.TrimRight(request.URL.Path, "/")
+ } else {
+ actualPagePath = request.URL.Path + indexName
+ }
+ } else {
+ actualPagePath = request.URL.Path
+ }
+
+ var currentProvider PageProvider
+ canCache := false
+ actualQueries := ""
+ queryValues := request.URL.Query()
+ var pageContent []byte
+ pageContentType := ""
+ var lastMod time.Time
+
+ if currentProvider = ph.PageProviders[actualPagePath]; currentProvider != nil {
+ actualQueries = currentProvider.GetCacheIDExtension(queryValues)
+
+ if ph.CacheSettings.EnableContentsCaching {
+ cached := ph.getPageFromCache(request.URL.Path, actualQueries)
+ if cached != nil {
+ pageContent = cached.Content
+ pageContentType = cached.ContentType
+ lastMod = cached.LastMod
+ }
+ }
+
+ if pageContentType == "" {
+ pageContentType, pageContent, canCache = currentProvider.GetContents(queryValues)
+ lastMod = currentProvider.GetLastModified()
+ if pageContentType != "" && canCache && ph.CacheSettings.EnableContentsCaching {
+ ph.setPageToCache(request.URL.Path, actualQueries, &CachedPage{
+ Content: pageContent,
+ ContentType: pageContentType,
+ LastMod: lastMod,
+ })
+ }
+ }
+ }
+
+ allowedMethods := ph.getAllowedMethodsForPath(request.URL.Path)
+ allowed := false
+ if request.Method != http.MethodOptions {
+ for _, method := range allowedMethods {
+ if method == request.Method {
+ allowed = true
+ break
+ }
+ }
+ }
+
+ if allowed {
+
+ if pageContentType == "" {
+ utils.WriteResponseHeaderCanWriteBody(request.Method, writer, http.StatusNotFound, "Page Not Found")
+ } else {
+
+ switch request.Method {
+ case http.MethodGet, http.MethodHead:
+
+ writer.Header().Set("Content-Type", pageContentType)
+ writer.Header().Set("Content-Length", strconv.Itoa(len(pageContent)))
+ utils.SetLastModifiedHeader(writer.Header(), lastMod)
+ utils.SetCacheHeaderWithAge(writer.Header(), ph.CacheSettings.MaxAge, lastMod)
+ theETag := utils.GetValueForETagUsingByteArray(pageContent)
+ writer.Header().Set("ETag", theETag)
+
+ if utils.ProcessSupportedPreconditionsForNext(writer, request, lastMod, theETag, ph.CacheSettings.NotModifiedResponseUsingLastModified, ph.CacheSettings.NotModifiedResponseUsingETags) {
+
+ httpRangeParts := utils.ProcessRangePreconditions(int64(len(pageContent)), writer, request, lastMod, theETag, ph.RangeSupported)
+ if httpRangeParts != nil {
+ if len(httpRangeParts) <= 1 {
+ var theWriter io.Writer = writer
+ if len(httpRangeParts) == 1 {
+ theWriter = utils.NewPartialRangeWriter(theWriter, httpRangeParts[0])
+ }
+ _, _ = theWriter.Write(pageContent)
+ } else {
+ multWriter := multipart.NewWriter(writer)
+ writer.Header().Set("Content-Type", "multipart/byteranges; boundary="+multWriter.Boundary())
+ for _, currentPart := range httpRangeParts {
+ mimePart, err := multWriter.CreatePart(textproto.MIMEHeader{
+ "Content-Range": {currentPart.ToField(int64(len(pageContent)))},
+ "Content-Type": {"text/plain; charset=utf-8"},
+ })
+ if err != nil {
+ break
+ }
+ _, err = mimePart.Write(pageContent[currentPart.Start : currentPart.Start+currentPart.Length])
+ if err != nil {
+ break
+ }
+ }
+ _ = multWriter.Close()
+ }
+ }
+ }
+ case http.MethodDelete:
+ ph.PurgeTemplateCache(actualPagePath, request.URL.Path == "/")
+ ph.PurgeContentsCache(request.URL.Path, actualQueries)
+ utils.SetNeverCacheHeader(writer.Header())
+ utils.WriteResponseHeaderCanWriteBody(request.Method, writer, http.StatusOK, "")
+ }
+ }
+ } else {
+
+ theAllowHeaderContents := ""
+ for _, method := range allowedMethods {
+ theAllowHeaderContents += method + ", "
+ }
+
+ writer.Header().Set("Allow", strings.TrimSuffix(theAllowHeaderContents, ", "))
+ if request.Method == http.MethodOptions {
+ utils.WriteResponseHeaderCanWriteBody(request.Method, writer, http.StatusOK, "")
+ } else {
+ utils.WriteResponseHeaderCanWriteBody(request.Method, writer, http.StatusMethodNotAllowed, "")
+ }
+ }
+}
+
+func (ph *PageHandler) PurgeContentsCache(path string, query string) {
+ if ph.CacheSettings.EnableContentsCaching && ph.CacheSettings.EnableContentsCachePurge {
+ if path == "/" {
+ ph.pageContentsCacheRWMutex.Lock()
+ ph.PageContentsCache = make(map[string]*CachedPage)
+ ph.pageContentsCacheRWMutex.Unlock()
+ } else {
+ if strings.HasSuffix(path, ".go/") {
+ ph.pageContentsCacheRWMutex.RLock()
+ toDelete := make([]string, len(ph.PageContentsCache))
+ theSize := 0
+ for cPath := range ph.PageContentsCache {
+ dPath := strings.Split(cPath, "?")[0]
+ if dPath == path || dPath == path[:len(path)-1] {
+ toDelete[theSize] = cPath
+ theSize++
+ }
+ }
+ ph.pageContentsCacheRWMutex.RUnlock()
+ ph.pageContentsCacheRWMutex.Lock()
+ for i := 0; i < theSize; i++ {
+ delete(ph.PageContentsCache, toDelete[i])
+ }
+ ph.pageContentsCacheRWMutex.Unlock()
+ return
+ } else if strings.HasSuffix(path, "/") {
+ path += indexName
+ }
+ ph.pageContentsCacheRWMutex.Lock()
+ if query == "" {
+ delete(ph.PageContentsCache, path)
+ } else {
+ delete(ph.PageContentsCache, path+"?"+query)
+ }
+ ph.pageContentsCacheRWMutex.Unlock()
+ }
+ }
+}
+
+func (ph *PageHandler) PurgeTemplateCache(path string, all bool) {
+ if ph.CacheSettings.EnableTemplateCaching && ph.CacheSettings.EnableTemplateCachePurge {
+ if all {
+ for _, pageProvider := range ph.PageProviders {
+ pageProvider.PurgeTemplate()
+ }
+ } else {
+ if pageProvider, ok := ph.PageProviders[path]; ok {
+ pageProvider.PurgeTemplate()
+ }
+ }
+ }
+}
+func (ph *PageHandler) getPageFromCache(pathIn string, cleanedQueries string) *CachedPage {
+ ph.pageContentsCacheRWMutex.RLock()
+ defer ph.pageContentsCacheRWMutex.RUnlock()
+ if strings.HasSuffix(pathIn, ".go/") {
+ return ph.PageContentsCache[strings.TrimRight(pathIn, "/")]
+ } else if strings.HasSuffix(pathIn, "/") {
+ pathIn += indexName
+ }
+ if cleanedQueries == "" {
+ return ph.PageContentsCache[pathIn]
+ } else {
+ return ph.PageContentsCache[pathIn+"?"+cleanedQueries]
+ }
+}
+
+func (ph *PageHandler) setPageToCache(pathIn string, cleanedQueries string, newPage *CachedPage) {
+ ph.pageContentsCacheRWMutex.Lock()
+ defer ph.pageContentsCacheRWMutex.Unlock()
+ if strings.HasSuffix(pathIn, ".go/") {
+ ph.PageContentsCache[strings.TrimRight(pathIn, "/")] = newPage
+ return
+ } else if strings.HasSuffix(pathIn, "/") {
+ pathIn += indexName
+ }
+ if cleanedQueries == "" {
+ ph.PageContentsCache[pathIn] = newPage
+ } else {
+ ph.PageContentsCache[pathIn+"?"+cleanedQueries] = newPage
+ }
+}
+
+func (ph *PageHandler) getAllowedMethodsForPath(pathIn string) []string {
+ if pathIn == "/" || strings.HasSuffix(pathIn, ".go/") {
+ if (ph.CacheSettings.EnableTemplateCaching && ph.CacheSettings.EnableTemplateCachePurge) ||
+ (ph.CacheSettings.EnableContentsCaching && ph.CacheSettings.EnableContentsCachePurge) {
+ return []string{http.MethodHead, http.MethodGet, http.MethodOptions, http.MethodDelete}
+ } else {
+ return []string{http.MethodHead, http.MethodGet, http.MethodOptions}
+ }
+ } else {
+ if ph.CacheSettings.EnableContentsCaching && ph.CacheSettings.EnableContentsCachePurge {
+ return []string{http.MethodHead, http.MethodGet, http.MethodOptions, http.MethodDelete}
+ } else {
+ return []string{http.MethodHead, http.MethodGet, http.MethodOptions}
+ }
+ }
+}
+
+func (ph *PageHandler) GetRegisteredPages() []string {
+ pages := make([]string, len(ph.PageProviders))
+ index := 0
+ for s := range ph.PageProviders {
+ pages[index] = s
+ index++
+ }
+ return pages
+}
+
+func (ph *PageHandler) GetCachedPages() []string {
+ ph.pageContentsCacheRWMutex.RLock()
+ defer ph.pageContentsCacheRWMutex.RUnlock()
+ pages := make([]string, len(ph.PageContentsCache))
+ index := 0
+ for s := range ph.PageContentsCache {
+ pages[index] = s
+ index++
+ }
+ return pages
+}
+
+func (ph *PageHandler) GetNumberOfCachedPages() int {
+ ph.pageContentsCacheRWMutex.RLock()
+ defer ph.pageContentsCacheRWMutex.RUnlock()
+ return len(ph.PageContentsCache)
+}
diff --git a/pageHandler/page-provider.go b/pageHandler/page-provider.go
new file mode 100644
index 0000000..8cd6b6f
--- /dev/null
+++ b/pageHandler/page-provider.go
@@ -0,0 +1,14 @@
+package pageHandler
+
+import (
+ "net/url"
+ "time"
+)
+
+type PageProvider interface {
+ GetPath() string
+ GetLastModified() time.Time
+ GetCacheIDExtension(urlParameters url.Values) string
+ GetContents(urlParameters url.Values) (contentType string, contents []byte, canCache bool)
+ PurgeTemplate()
+}
diff --git a/pageHandler/pages.go b/pageHandler/pages.go
new file mode 100644
index 0000000..24a9e8b
--- /dev/null
+++ b/pageHandler/pages.go
@@ -0,0 +1,18 @@
+package pageHandler
+
+import "golang.captainalm.com/mc-webserver/pageHandler/pages/index"
+
+var providers map[string]PageProvider
+
+func GetProviders(cacheTemplates bool, dataStorage string, pageHandler *PageHandler) map[string]PageProvider {
+ if providers == nil {
+ providers = make(map[string]PageProvider)
+ if pageHandler != nil {
+ infoPage := newGoInfoPage(pageHandler, dataStorage, cacheTemplates)
+ providers[infoPage.GetPath()] = infoPage //Go Information Page
+ }
+ indexPage := index.NewPage(dataStorage, cacheTemplates)
+ providers[indexPage.GetPath()] = indexPage
+ }
+ return providers
+}
diff --git a/pageHandler/pages/index/data.go b/pageHandler/pages/index/data.go
new file mode 100644
index 0000000..76fe93b
--- /dev/null
+++ b/pageHandler/pages/index/data.go
@@ -0,0 +1,28 @@
+package index
+
+import (
+ "html/template"
+ "time"
+)
+
+type DataYaml struct {
+ PageTitle string `yaml:"pageTitle"`
+ ServerDescription template.HTML `yaml:"serverDescription"`
+ MCAddress string `yaml:"mcAddress"`
+ MCPort uint16 `yaml:"mcPort"`
+ MCType string `yaml:"mcType"`
+ MCProtocolVersion int `yaml:"mcProtocolVersion"`
+ MCClientGUID int64 `yaml:"mcClientGUID"`
+ MCTimeout time.Duration `yaml:"mcTimeout"`
+ AllowDisplayState bool `yaml:"allowDisplayState"`
+ AllowDisplayVersion bool `yaml:"allowDisplayVersion"`
+ AllowDisplayActualAddress bool `yaml:"allowDisplayActualAddress"`
+ AllowPlayerCountDisplay bool `yaml:"allowPlayerCountDisplay"`
+ AllowPlayerListing bool `yaml:"allowPlayerListing"`
+ AllowMOTDDisplay bool `yaml:"allowMOTDDisplay"`
+ AllowFaviconDisplay bool `yaml:"allowFaviconDisplay"`
+ AllowSecureProfileModeDisplay bool `yaml:"allowSecureProfileModeDisplay"`
+ AllowPreviewChatModeDisplay bool `yaml:"allowPreviewChatModeDisplay"`
+ AllowDisplayModded bool `yaml:"allowDisplayModded"`
+ AllowModListing bool `yaml:"allowModListing"`
+}
diff --git a/pageHandler/pages/index/index-page.go b/pageHandler/pages/index/index-page.go
new file mode 100644
index 0000000..398b9bd
--- /dev/null
+++ b/pageHandler/pages/index/index-page.go
@@ -0,0 +1,193 @@
+package index
+
+import (
+ "golang.captainalm.com/mc-webserver/utils/io"
+ "gopkg.in/yaml.v3"
+ "html/template"
+ "net/url"
+ "os"
+ "path"
+ "strings"
+ "sync"
+ "time"
+)
+
+const templateName = "index.go.html"
+const yamlName = "index.go.yml"
+
+func NewPage(dataStore string, cacheTemplates bool) *Page {
+ var ptm *sync.Mutex
+ var sdm *sync.Mutex
+ if cacheTemplates {
+ ptm = &sync.Mutex{}
+ sdm = &sync.Mutex{}
+ }
+ pageToReturn := &Page{
+ DataStore: dataStore,
+ StoredDataMutex: sdm,
+ PageTemplateMutex: ptm,
+ }
+ return pageToReturn
+}
+
+type Page struct {
+ DataStore string
+ StoredDataMutex *sync.Mutex
+ StoredData *DataYaml
+ LastModifiedData time.Time
+ PageTemplateMutex *sync.Mutex
+ PageTemplate *template.Template
+ LastModifiedTemplate time.Time
+}
+
+func (p *Page) GetPath() string {
+ return "/index.go"
+}
+
+func (p *Page) GetLastModified() time.Time {
+ if p.LastModifiedData.After(p.LastModifiedTemplate) {
+ return p.LastModifiedData
+ } else {
+ return p.LastModifiedTemplate
+ }
+}
+
+func (p *Page) GetCacheIDExtension(urlParameters url.Values) string {
+ toReturn := p.getNonThemedCleanQuery(urlParameters)
+ if toReturn != "" {
+ toReturn += "&"
+ }
+ if urlParameters.Has("light") {
+ toReturn += "light"
+ }
+ return strings.TrimRight(toReturn, "&")
+}
+
+func (p *Page) getNonThemedCleanQuery(urlParameters url.Values) string {
+ toReturn := ""
+ if urlParameters.Has("players") {
+ toReturn += "players&"
+ }
+ if urlParameters.Has("mods") {
+ toReturn += "mods&"
+ }
+ if urlParameters.Has("extended") {
+ toReturn += "extended"
+ }
+ return strings.TrimRight(toReturn, "&")
+}
+
+func (p *Page) GetContents(urlParameters url.Values) (contentType string, contents []byte, canCache bool) {
+ theTemplate, err := p.getPageTemplate()
+ if err != nil {
+ return "text/plain", []byte("Cannot Get Index.\r\n" + err.Error()), false
+ }
+ theData, err := p.getPageData()
+ if err != nil {
+ return "text/plain", []byte("Cannot Get Data.\r\n" + err.Error()), false
+ }
+ theMarshal := &Marshal{
+ Data: *theData,
+ Light: urlParameters.Has("light"),
+ PlayersShown: urlParameters.Has("players"),
+ ModsShown: urlParameters.Has("mods"),
+ ExtendedShown: urlParameters.Has("extended"),
+ Parameters: template.URL(p.getNonThemedCleanQuery(urlParameters)),
+ Online: true,
+ }
+ theMC, err := theMarshal.NewMC()
+ theMarshal.Queried = theMC
+ if err != nil {
+ theMarshal.Online = false
+ }
+ theBuffer := &io.BufferedWriter{}
+ err = theTemplate.ExecuteTemplate(theBuffer, templateName, theMarshal)
+ if err != nil {
+ return "text/plain", []byte("Cannot Get Page.\r\n" + err.Error()), false
+ }
+ return "text/html", theBuffer.Data, true
+}
+
+func (p *Page) PurgeTemplate() {
+ if p.PageTemplateMutex != nil {
+ p.PageTemplateMutex.Lock()
+ p.PageTemplate = nil
+ p.PageTemplateMutex.Unlock()
+ }
+ if p.StoredDataMutex != nil {
+ p.StoredDataMutex.Lock()
+ p.StoredData = nil
+ p.StoredDataMutex.Unlock()
+ }
+}
+
+func (p *Page) getPageTemplate() (*template.Template, error) {
+ if p.PageTemplateMutex != nil {
+ p.PageTemplateMutex.Lock()
+ defer p.PageTemplateMutex.Unlock()
+ }
+ if p.PageTemplate == nil {
+ thePath := templateName
+ if p.DataStore != "" {
+ thePath = path.Join(p.DataStore, thePath)
+ }
+ stat, err := os.Stat(thePath)
+ if err != nil {
+ return nil, err
+ }
+ p.LastModifiedTemplate = stat.ModTime()
+ loadedData, err := os.ReadFile(thePath)
+ if err != nil {
+ return nil, err
+ }
+ tmpl, err := template.New(templateName).Parse(string(loadedData))
+ if err != nil {
+ return nil, err
+ }
+ if p.PageTemplateMutex != nil {
+ p.PageTemplate = tmpl
+ }
+ return tmpl, nil
+ } else {
+ return p.PageTemplate, nil
+ }
+}
+
+func (p *Page) getPageData() (*DataYaml, error) {
+ if p.StoredDataMutex != nil {
+ p.StoredDataMutex.Lock()
+ defer p.StoredDataMutex.Unlock()
+ }
+ if p.StoredData == nil {
+ thePath := yamlName
+ if p.DataStore != "" {
+ thePath = path.Join(p.DataStore, thePath)
+ }
+ stat, err := os.Stat(thePath)
+ if err != nil {
+ return nil, err
+ }
+ p.LastModifiedData = stat.ModTime()
+ fileHandle, err := os.Open(thePath)
+ if err != nil {
+ return nil, err
+ }
+ dataYaml := &DataYaml{}
+ decoder := yaml.NewDecoder(fileHandle)
+ err = decoder.Decode(dataYaml)
+ if err != nil {
+ _ = fileHandle.Close()
+ return nil, err
+ }
+ err = fileHandle.Close()
+ if err != nil {
+ return nil, err
+ }
+ if p.StoredDataMutex != nil {
+ p.StoredData = dataYaml
+ }
+ return dataYaml, nil
+ } else {
+ return p.StoredData, nil
+ }
+}
diff --git a/pageHandler/pages/index/mc.go b/pageHandler/pages/index/mc.go
new file mode 100644
index 0000000..0543435
--- /dev/null
+++ b/pageHandler/pages/index/mc.go
@@ -0,0 +1,25 @@
+package index
+
+import (
+ "html/template"
+)
+
+type MC struct {
+ Version *string
+ ProtocolVersion *int64
+ Address string
+ Port uint16
+ Port6 *uint16
+ PlayerCount *int64
+ MaxPlayers *int64
+ Players []string
+ MOTD string
+ ActualHost *string
+ ActualPort *uint16
+ Favicon *template.HTML
+ Edition *string
+ ModCount int64
+ Mods []string
+ SecureProfilesEnforced *bool
+ PreviewChatEnforced *bool
+}
diff --git a/pageHandler/pages/index/template-marshal.go b/pageHandler/pages/index/template-marshal.go
new file mode 100644
index 0000000..e368d4e
--- /dev/null
+++ b/pageHandler/pages/index/template-marshal.go
@@ -0,0 +1,202 @@
+package index
+
+import (
+ "errors"
+ "github.com/mcstatus-io/mcutil"
+ "github.com/mcstatus-io/mcutil/options"
+ "github.com/mcstatus-io/mcutil/response"
+ "html/template"
+ "strings"
+)
+
+type Marshal struct {
+ Data DataYaml
+ Queried MC
+ PlayersShown bool
+ ModsShown bool
+ ExtendedShown bool
+ Parameters template.URL
+ Light bool
+ Online bool
+}
+
+func (m Marshal) NewMC() (MC, error) {
+ switch strings.ToLower(m.Data.MCType) {
+ case "java":
+ r, err := mcutil.Status(m.Data.MCAddress, m.Data.MCPort, options.JavaStatus{
+ EnableSRV: m.ExtendedShown && m.Data.AllowDisplayActualAddress,
+ Timeout: m.Data.MCTimeout,
+ ProtocolVersion: m.Data.MCProtocolVersion,
+ })
+ if err != nil {
+ return MC{}, err
+ }
+ r2, err := mcutil.StatusRaw(m.Data.MCAddress, m.Data.MCPort, options.JavaStatus{
+ Timeout: m.Data.MCTimeout,
+ ProtocolVersion: m.Data.MCProtocolVersion,
+ })
+ if err != nil {
+ return MC{}, err
+ }
+ return MC{
+ Version: &r.Version.NameClean,
+ ProtocolVersion: &r.Version.Protocol,
+ Address: m.Data.MCAddress,
+ Port: m.Data.MCPort,
+ Port6: nil,
+ PlayerCount: r.Players.Online,
+ MaxPlayers: r.Players.Max,
+ Players: CollectPlayers(r.Players.Sample),
+ MOTD: r.MOTD.Clean,
+ ActualHost: CollectSRVHost(r.SRVResult),
+ ActualPort: CollectSRVPort(r.SRVResult),
+ Favicon: CollectFavicon(r.Favicon),
+ Edition: CollectModEdition(r.ModInfo),
+ ModCount: CollectModCount(r.ModInfo),
+ Mods: CollectMods(r.ModInfo),
+ SecureProfilesEnforced: CollectSecureProfileEnforcement(r2),
+ PreviewChatEnforced: CollectPreviewChatEnforcement(r2),
+ }, nil
+ case "legacy", "legacyjava", "javalegacy", "legacy java", "java legacy", "legacy_java", "java_legacy":
+ r, err := mcutil.StatusLegacy(m.Data.MCAddress, m.Data.MCPort, options.JavaStatusLegacy{
+ EnableSRV: m.ExtendedShown && m.Data.AllowDisplayActualAddress,
+ Timeout: m.Data.MCTimeout,
+ ProtocolVersion: m.Data.MCProtocolVersion,
+ })
+ if err != nil {
+ return MC{}, err
+ }
+ return MC{
+ Version: &r.Version.NameClean,
+ ProtocolVersion: &r.Version.Protocol,
+ Address: m.Data.MCAddress,
+ Port: m.Data.MCPort,
+ Port6: nil,
+ PlayerCount: &r.Players.Online,
+ MaxPlayers: &r.Players.Max,
+ Players: nil,
+ MOTD: r.MOTD.Clean,
+ ActualHost: CollectSRVHost(r.SRVResult),
+ ActualPort: CollectSRVPort(r.SRVResult),
+ Favicon: nil,
+ Edition: nil,
+ ModCount: 0,
+ Mods: nil,
+ SecureProfilesEnforced: nil,
+ PreviewChatEnforced: nil,
+ }, nil
+ case "bedrock":
+ r, err := mcutil.StatusBedrock(m.Data.MCAddress, m.Data.MCPort, options.BedrockStatus{
+ EnableSRV: m.ExtendedShown && m.Data.AllowDisplayActualAddress,
+ Timeout: m.Data.MCTimeout,
+ ClientGUID: m.Data.MCClientGUID,
+ })
+ if err != nil {
+ return MC{}, err
+ }
+ return MC{
+ Version: r.Version,
+ ProtocolVersion: r.ProtocolVersion,
+ Address: m.Data.MCAddress,
+ Port: m.CollectIPv4Port(r.PortIPv4),
+ Port6: r.PortIPv6,
+ PlayerCount: r.OnlinePlayers,
+ MaxPlayers: r.MaxPlayers,
+ Players: nil,
+ MOTD: r.MOTD.Clean,
+ ActualHost: CollectSRVHost(r.SRVResult),
+ ActualPort: CollectSRVPort(r.SRVResult),
+ Favicon: nil,
+ Edition: r.Edition,
+ ModCount: 0,
+ Mods: nil,
+ SecureProfilesEnforced: nil,
+ PreviewChatEnforced: nil,
+ }, nil
+ default:
+ return MC{}, errors.New("Invalid MCType")
+ }
+}
+
+func CollectPlayers(sampleArray []response.SamplePlayer) []string {
+ if sampleArray == nil {
+ return nil
+ }
+ toReturn := make([]string, len(sampleArray))
+ for i := 0; i < len(sampleArray); i++ {
+ toReturn[i] = sampleArray[i].NameClean
+ }
+ return toReturn
+}
+
+func CollectSRVHost(srv *response.SRVRecord) *string {
+ if srv == nil {
+ return nil
+ }
+ return &srv.Host
+}
+
+func CollectSRVPort(srv *response.SRVRecord) *uint16 {
+ if srv == nil {
+ return nil
+ }
+ return &srv.Port
+}
+
+func CollectFavicon(favicon *string) *template.HTML {
+ if favicon == nil {
+ return nil
+ }
+ toReturn := template.HTML(*favicon)
+ return &toReturn
+}
+
+func CollectModEdition(mod *response.ModInfo) *string {
+ if mod == nil {
+ return nil
+ }
+ return &mod.Type
+}
+
+func CollectModCount(mod *response.ModInfo) int64 {
+ if mod == nil {
+ return 0
+ }
+ return int64(len(mod.Mods))
+}
+
+func CollectMods(mod *response.ModInfo) []string {
+ if mod == nil {
+ return nil
+ }
+ toReturn := make([]string, len(mod.Mods))
+ for i := 0; i < len(mod.Mods); i++ {
+ toReturn[i] = mod.Mods[i].ID + " (" + mod.Mods[i].Version + ")"
+ }
+ return toReturn
+}
+
+func CollectSecureProfileEnforcement(data map[string]interface{}) *bool {
+ val, ok := data["enforcesSecureChat"]
+ if ok {
+ toReturn := val.(bool)
+ return &toReturn
+ }
+ return nil
+}
+
+func CollectPreviewChatEnforcement(data map[string]interface{}) *bool {
+ val, ok := data["previewsChat"]
+ if ok {
+ toReturn := val.(bool)
+ return &toReturn
+ }
+ return nil
+}
+
+func (m Marshal) CollectIPv4Port(port *uint16) uint16 {
+ if port == nil {
+ return m.Data.MCPort
+ }
+ return *port
+}
diff --git a/pageHandler/utils/content-range-value.go b/pageHandler/utils/content-range-value.go
new file mode 100644
index 0000000..b0944b8
--- /dev/null
+++ b/pageHandler/utils/content-range-value.go
@@ -0,0 +1,78 @@
+package utils
+
+import (
+ "strconv"
+ "strings"
+)
+
+type ContentRangeValue struct {
+ Start, Length int64
+}
+
+func (rstrc ContentRangeValue) ToField(maxLength int64) string {
+ return "bytes " + strconv.FormatInt(rstrc.Start, 10) + "-" + strconv.FormatInt(rstrc.Start+rstrc.Length-1, 10) + "/" + strconv.FormatInt(maxLength, 10)
+}
+
+func GetRanges(rangeStringIn string, maxLength int64) []ContentRangeValue {
+ actualRangeString := strings.TrimPrefix(rangeStringIn, "bytes=")
+ if strings.ContainsAny(actualRangeString, ",") {
+ seperated := strings.Split(actualRangeString, ",")
+ toReturn := make([]ContentRangeValue, len(seperated))
+ pos := 0
+ for _, s := range seperated {
+ if cRange, ok := GetRange(s, maxLength); ok {
+ toReturn[pos] = cRange
+ pos += 1
+ }
+ }
+ if pos == 0 {
+ return nil
+ }
+ return toReturn[:pos]
+ }
+ if cRange, ok := GetRange(actualRangeString, maxLength); ok {
+ return []ContentRangeValue{cRange}
+ }
+ return nil
+}
+
+func GetRange(rangePartIn string, maxLength int64) (ContentRangeValue, bool) {
+ before, after, done := strings.Cut(rangePartIn, "-")
+ before = strings.Trim(before, " ")
+ after = strings.Trim(after, " ")
+ if !done {
+ return ContentRangeValue{}, false
+ }
+ var parsedAfter, parsedBefore int64 = -1, -1
+ if after != "" {
+ if parsed, err := strconv.ParseInt(after, 10, 64); err == nil {
+ parsedAfter = parsed
+ } else {
+ return ContentRangeValue{}, false
+ }
+ }
+ if before != "" {
+ if parsed, err := strconv.ParseInt(before, 10, 64); err == nil {
+ parsedBefore = parsed
+ } else {
+ return ContentRangeValue{}, false
+ }
+ }
+ if parsedBefore >= 0 && parsedAfter > parsedBefore && parsedAfter < maxLength {
+ return ContentRangeValue{
+ Start: parsedBefore,
+ Length: parsedAfter - parsedBefore + 1,
+ }, true
+ } else if parsedAfter < 0 && parsedBefore >= 0 && parsedBefore < maxLength {
+ return ContentRangeValue{
+ Start: parsedBefore,
+ Length: maxLength - parsedBefore,
+ }, true
+ } else if parsedBefore < 0 && parsedAfter >= 1 && maxLength-parsedAfter >= 0 {
+ return ContentRangeValue{
+ Start: maxLength - parsedAfter,
+ Length: parsedAfter,
+ }, true
+ }
+ return ContentRangeValue{}, false
+}
diff --git a/pageHandler/utils/etag.go b/pageHandler/utils/etag.go
new file mode 100644
index 0000000..a5b4781
--- /dev/null
+++ b/pageHandler/utils/etag.go
@@ -0,0 +1,48 @@
+package utils
+
+import (
+ "crypto"
+ "encoding/hex"
+ "strings"
+)
+
+func GetValueForETagUsingByteArray(b []byte) string {
+ theHash := crypto.SHA1.New()
+ _, _ = theHash.Write(b)
+ theSum := theHash.Sum(nil)
+ theHash.Reset()
+ return "\"" + hex.EncodeToString(theSum) + "\""
+}
+
+func GetETagValues(stringIn string) []string {
+ if strings.ContainsAny(stringIn, ",") {
+ seperated := strings.Split(stringIn, ",")
+ toReturn := make([]string, len(seperated))
+ pos := 0
+ for _, s := range seperated {
+ cETag := GetETagValue(s)
+ if cETag != "" {
+ toReturn[pos] = cETag
+ pos += 1
+ }
+ }
+ if pos == 0 {
+ return nil
+ }
+ return toReturn[:pos]
+ }
+ toReturn := []string{GetETagValue(stringIn)}
+ if toReturn[0] == "" {
+ return nil
+ }
+ return toReturn
+}
+
+func GetETagValue(stringIn string) string {
+ startIndex := strings.IndexAny(stringIn, "\"") + 1
+ endIndex := strings.LastIndexAny(stringIn, "\"")
+ if endIndex > startIndex {
+ return stringIn[startIndex:endIndex]
+ }
+ return ""
+}
diff --git a/pageHandler/utils/partial-range-writer.go b/pageHandler/utils/partial-range-writer.go
new file mode 100644
index 0000000..2f82753
--- /dev/null
+++ b/pageHandler/utils/partial-range-writer.go
@@ -0,0 +1,47 @@
+package utils
+
+import "io"
+
+func NewPartialRangeWriter(writerIn io.Writer, httpRangeIn ContentRangeValue) io.Writer {
+ return &PartialRangeWriter{
+ passedWriter: writerIn,
+ passedWriterIndex: 0,
+ httpRange: httpRangeIn,
+ exclusiveLastIndex: httpRangeIn.Start + httpRangeIn.Length,
+ }
+}
+
+type PartialRangeWriter struct {
+ passedWriter io.Writer
+ passedWriterIndex int64
+ exclusiveLastIndex int64
+ httpRange ContentRangeValue
+}
+
+func (prw *PartialRangeWriter) Write(p []byte) (n int, err error) {
+ var pOffsetIndex int64 = -1
+ if prw.passedWriterIndex >= prw.httpRange.Start && prw.passedWriterIndex < prw.exclusiveLastIndex {
+ pOffsetIndex = 0
+ } else if prw.passedWriterIndex+int64(len(p)) > prw.httpRange.Start && prw.passedWriterIndex < prw.exclusiveLastIndex {
+ pOffsetIndex = prw.httpRange.Start - prw.passedWriterIndex
+ prw.passedWriterIndex += pOffsetIndex
+ } else {
+ prw.passedWriterIndex += int64(len(p))
+ }
+ if pOffsetIndex >= 0 {
+ if prw.passedWriterIndex+(int64(len(p))-pOffsetIndex) <= prw.exclusiveLastIndex {
+ written, err := prw.passedWriter.Write(p[pOffsetIndex:])
+ prw.passedWriterIndex += int64(written)
+ if err != nil {
+ return written, err
+ }
+ } else {
+ written, err := prw.passedWriter.Write(p[pOffsetIndex : prw.exclusiveLastIndex-prw.passedWriterIndex+pOffsetIndex])
+ prw.passedWriterIndex += int64(written)
+ if err != nil {
+ return written, err
+ }
+ }
+ }
+ return n, nil
+}
diff --git a/pageHandler/utils/process-preconditions.go b/pageHandler/utils/process-preconditions.go
new file mode 100644
index 0000000..59221f7
--- /dev/null
+++ b/pageHandler/utils/process-preconditions.go
@@ -0,0 +1,153 @@
+package utils
+
+import (
+ "golang.captainalm.com/mc-webserver/utils/io"
+ "mime/multipart"
+ "net/http"
+ "net/textproto"
+ "strconv"
+ "strings"
+ "time"
+)
+
+func ProcessSupportedPreconditionsForNext(rw http.ResponseWriter, req *http.Request, modT time.Time, etag string, noBypassModify bool, noBypassMatch bool) bool {
+ theStrippedETag := GetETagValue(etag)
+ if noBypassMatch && theStrippedETag != "" && req.Header.Get("If-None-Match") != "" {
+ etagVals := GetETagValues(req.Header.Get("If-None-Match"))
+ conditionSuccess := false
+ for _, s := range etagVals {
+ if s == theStrippedETag {
+ conditionSuccess = true
+ break
+ }
+ }
+ if conditionSuccess {
+ WriteResponseHeaderCanWriteBody(req.Method, rw, http.StatusNotModified, "")
+ return false
+ }
+ }
+
+ if noBypassMatch && theStrippedETag != "" && req.Header.Get("If-Match") != "" {
+ etagVals := GetETagValues(req.Header.Get("If-Match"))
+ conditionFailed := true
+ for _, s := range etagVals {
+ if s == theStrippedETag {
+ conditionFailed = false
+ break
+ }
+ }
+ if conditionFailed {
+ SwitchToNonCachingHeaders(rw.Header())
+ rw.Header().Del("Content-Type")
+ rw.Header().Del("Content-Length")
+ WriteResponseHeaderCanWriteBody(req.Method, rw, http.StatusPreconditionFailed, "")
+ return false
+ }
+ }
+
+ if noBypassModify && !modT.IsZero() && req.Header.Get("If-Modified-Since") != "" {
+ parse, err := time.Parse(http.TimeFormat, req.Header.Get("If-Modified-Since"))
+ if err == nil && modT.Before(parse) || strings.EqualFold(modT.Format(http.TimeFormat), req.Header.Get("If-Modified-Since")) {
+ WriteResponseHeaderCanWriteBody(req.Method, rw, http.StatusNotModified, "")
+ return false
+ }
+ }
+
+ if noBypassModify && !modT.IsZero() && req.Header.Get("If-Unmodified-Since") != "" {
+ parse, err := time.Parse(http.TimeFormat, req.Header.Get("If-Unmodified-Since"))
+ if err == nil && modT.After(parse) {
+ SwitchToNonCachingHeaders(rw.Header())
+ rw.Header().Del("Content-Type")
+ rw.Header().Del("Content-Length")
+ WriteResponseHeaderCanWriteBody(req.Method, rw, http.StatusPreconditionFailed, "")
+ return false
+ }
+ }
+
+ return true
+}
+
+func ProcessRangePreconditions(maxLength int64, rw http.ResponseWriter, req *http.Request, modT time.Time, etag string, supported bool) []ContentRangeValue {
+ canDoRange := supported
+ theStrippedETag := GetETagValue(etag)
+ modTStr := modT.Format(http.TimeFormat)
+
+ if canDoRange {
+ rw.Header().Set("Accept-Ranges", "bytes")
+ }
+
+ if canDoRange && !modT.IsZero() && strings.HasSuffix(req.Header.Get("If-Range"), "GMT") {
+ newModT, err := time.Parse(http.TimeFormat, modTStr)
+ parse, err := time.Parse(http.TimeFormat, req.Header.Get("If-Range"))
+ if err == nil && !newModT.Equal(parse) {
+ canDoRange = false
+ }
+ } else if canDoRange && theStrippedETag != "" && req.Header.Get("If-Range") != "" {
+ if GetETagValue(req.Header.Get("If-Range")) != theStrippedETag {
+ canDoRange = false
+ }
+ }
+
+ if canDoRange && strings.HasPrefix(req.Header.Get("Range"), "bytes=") {
+ if theRanges := GetRanges(req.Header.Get("Range"), maxLength); len(theRanges) != 0 {
+ if len(theRanges) == 1 {
+ rw.Header().Set("Content-Length", strconv.FormatInt(theRanges[0].Length, 10))
+ rw.Header().Set("Content-Range", theRanges[0].ToField(maxLength))
+ } else {
+ theSize := GetMultipartLength(theRanges, rw.Header().Get("Content-Type"), maxLength)
+ rw.Header().Set("Content-Length", strconv.FormatInt(theSize, 10))
+ }
+ if WriteResponseHeaderCanWriteBody(req.Method, rw, http.StatusPartialContent, "") {
+ return theRanges
+ } else {
+ return nil
+ }
+ } else {
+ SwitchToNonCachingHeaders(rw.Header())
+ rw.Header().Del("Content-Type")
+ rw.Header().Del("Content-Length")
+ rw.Header().Set("Content-Range", "bytes */"+strconv.FormatInt(maxLength, 10))
+ WriteResponseHeaderCanWriteBody(req.Method, rw, http.StatusRequestedRangeNotSatisfiable, "")
+ return nil
+ }
+ }
+ if WriteResponseHeaderCanWriteBody(req.Method, rw, http.StatusOK, "") {
+ return make([]ContentRangeValue, 0)
+ }
+ return nil
+}
+
+func GetMultipartLength(parts []ContentRangeValue, contentType string, maxLength int64) int64 {
+ cWriter := &io.CountingWriter{Length: 0}
+ var returnLength int64 = 0
+ multWriter := multipart.NewWriter(cWriter)
+ for _, currentPart := range parts {
+ _, _ = multWriter.CreatePart(textproto.MIMEHeader{
+ "Content-Range": {currentPart.ToField(maxLength)},
+ "Content-Type": {contentType},
+ })
+ returnLength += currentPart.Length
+ }
+ _ = multWriter.Close()
+ returnLength += cWriter.Length
+ return returnLength
+}
+
+func WriteResponseHeaderCanWriteBody(method string, rw http.ResponseWriter, statusCode int, message string) bool {
+ hasBody := method != http.MethodHead && method != http.MethodOptions
+ if hasBody && message != "" {
+ rw.Header().Set("Content-Type", "text/plain; charset=utf-8")
+ rw.Header().Set("X-Content-Type-Options", "nosniff")
+ rw.Header().Set("Content-Length", strconv.Itoa(len(message)+2))
+ SetNeverCacheHeader(rw.Header())
+ }
+ rw.WriteHeader(statusCode)
+ if hasBody {
+ if message != "" {
+ _, _ = rw.Write([]byte(message + "\r\n"))
+ return false
+ }
+ return true
+ }
+ return false
+}
diff --git a/pageHandler/utils/utils.go b/pageHandler/utils/utils.go
new file mode 100644
index 0000000..551caa4
--- /dev/null
+++ b/pageHandler/utils/utils.go
@@ -0,0 +1,45 @@
+package utils
+
+import (
+ "net/http"
+ "strconv"
+ "time"
+)
+
+func SetNeverCacheHeader(header http.Header) {
+ header.Set("Cache-Control", "max-age=0, no-cache, no-store, must-revalidate")
+ header.Set("Pragma", "no-cache")
+}
+
+func SetLastModifiedHeader(header http.Header, modTime time.Time) {
+ if !modTime.IsZero() {
+ header.Set("Last-Modified", modTime.UTC().Format(http.TimeFormat))
+ }
+}
+
+func SetCacheHeaderWithAge(header http.Header, maxAge uint, modifiedTime time.Time) {
+ header.Set("Cache-Control", "max-age="+strconv.Itoa(int(maxAge))+", must-revalidate")
+ if maxAge > 0 {
+ checkerSecondsBetween := int64(time.Now().UTC().Sub(modifiedTime.UTC()).Seconds())
+ if checkerSecondsBetween < 0 {
+ checkerSecondsBetween *= -1
+ }
+ header.Set("Age", strconv.FormatUint(uint64(checkerSecondsBetween)%uint64(maxAge), 10))
+ }
+}
+
+func SwitchToNonCachingHeaders(header http.Header) {
+ SetNeverCacheHeader(header)
+ if header.Get("Last-Modified") != "" {
+ header.Del("Last-Modified")
+ }
+ if header.Get("Age") != "" {
+ header.Del("Age")
+ }
+ if header.Get("Expires") != "" {
+ header.Del("Expires")
+ }
+ if header.Get("ETag") != "" {
+ header.Del("ETag")
+ }
+}
diff --git a/utils/info/conf-info.go b/utils/info/conf-info.go
new file mode 100644
index 0000000..53f4ed1
--- /dev/null
+++ b/utils/info/conf-info.go
@@ -0,0 +1,6 @@
+package info
+
+import "golang.captainalm.com/mc-webserver/conf"
+
+var ListenSettings conf.ListenYaml
+var ServeSettings conf.ServeYaml
diff --git a/utils/info/product-info.go b/utils/info/product-info.go
new file mode 100644
index 0000000..fcf8ad4
--- /dev/null
+++ b/utils/info/product-info.go
@@ -0,0 +1,13 @@
+package info
+
+var BuildName string
+var BuildDescription string
+var BuildVersion string
+var BuildDate string
+
+func SetupProductInfo(buildName string, buildDescription string, buildVersion string, buildDate string) {
+ BuildName = buildName
+ BuildDescription = buildDescription
+ BuildVersion = buildVersion
+ BuildDate = buildDate
+}
diff --git a/utils/io/buffered-writer.go b/utils/io/buffered-writer.go
new file mode 100644
index 0000000..b9f1d9e
--- /dev/null
+++ b/utils/io/buffered-writer.go
@@ -0,0 +1,10 @@
+package io
+
+type BufferedWriter struct {
+ Data []byte
+}
+
+func (c *BufferedWriter) Write(p []byte) (n int, err error) {
+ c.Data = append(c.Data, p...)
+ return len(p), nil
+}
diff --git a/utils/io/counting-writer.go b/utils/io/counting-writer.go
new file mode 100644
index 0000000..152a5b9
--- /dev/null
+++ b/utils/io/counting-writer.go
@@ -0,0 +1,10 @@
+package io
+
+type CountingWriter struct {
+ Length int64
+}
+
+func (c *CountingWriter) Write(p []byte) (n int, err error) {
+ c.Length += int64(len(p))
+ return len(p), nil
+}
diff --git a/utils/yaml/date-type.go b/utils/yaml/date-type.go
new file mode 100644
index 0000000..910cd12
--- /dev/null
+++ b/utils/yaml/date-type.go
@@ -0,0 +1,31 @@
+package yaml
+
+import (
+ "gopkg.in/yaml.v3"
+ "strings"
+ "time"
+)
+
+const dateFormat = "02/01/2006"
+
+type DateType struct {
+ time.Time
+}
+
+func (dt *DateType) MarshalYAML() (interface{}, error) {
+ return dt.Time.Format(dateFormat), nil
+}
+
+func (dt *DateType) UnmarshalYAML(value *yaml.Node) error {
+ var stringIn string
+ err := value.Decode(&stringIn)
+ if err != nil {
+ return nil
+ }
+ pt, err := time.Parse(dateFormat, strings.TrimSpace(stringIn))
+ if err != nil {
+ return err
+ }
+ dt.Time = pt
+ return nil
+}
diff --git a/wappmcstat/main.go b/wappmcstat/main.go
new file mode 100644
index 0000000..18c8a40
--- /dev/null
+++ b/wappmcstat/main.go
@@ -0,0 +1,148 @@
+package main
+
+import (
+ "fmt"
+ "github.com/joho/godotenv"
+ "golang.captainalm.com/mc-webserver/conf"
+ "golang.captainalm.com/mc-webserver/pageHandler"
+ "golang.captainalm.com/mc-webserver/utils/info"
+ "gopkg.in/yaml.v3"
+ "log"
+ "net"
+ "net/http"
+ "os"
+ "os/signal"
+ "path"
+ "path/filepath"
+ "strings"
+ "sync"
+ "syscall"
+ "time"
+)
+
+var (
+ buildName = ""
+ buildDescription = "Minecraft Status Web APP"
+ buildVersion = "develop"
+ buildDate = ""
+)
+
+func main() {
+ log.Printf("[Main] Starting up %s (%s) #%s (%s)\n", buildDescription, buildName, buildVersion, buildDate)
+ y := time.Now()
+ info.SetupProductInfo(buildName, buildDescription, buildVersion, buildDate)
+
+ //Hold main thread till safe shutdown exit:
+ wg := &sync.WaitGroup{}
+ wg.Add(1)
+
+ //Get working directory:
+ cwdDir, err := os.Getwd()
+ if err != nil {
+ log.Println(err)
+ }
+
+ //Load environment file:
+ err = godotenv.Load()
+ if err != nil {
+ log.Fatalln("Error loading .env file")
+ }
+
+ //Data directory processing:
+ dataDir := os.Getenv("DIR_DATA")
+ if dataDir == "" {
+ dataDir = path.Join(cwdDir, ".data")
+ }
+
+ check(os.MkdirAll(dataDir, 0777))
+
+ //Config file processing:
+ configLocation := os.Getenv("CONFIG_FILE")
+ if configLocation == "" {
+ configLocation = path.Join(dataDir, "config.yml")
+ } else {
+ if !filepath.IsAbs(configLocation) {
+ configLocation = path.Join(dataDir, configLocation)
+ }
+ }
+
+ //Config loading:
+ configFile, err := os.Open(configLocation)
+ if err != nil {
+ log.Fatalln("Failed to open config.yml")
+ }
+
+ var configYml conf.ConfigYaml
+ groupsDecoder := yaml.NewDecoder(configFile)
+ err = groupsDecoder.Decode(&configYml)
+ if err != nil {
+ log.Fatalln("Failed to parse config.yml:", err)
+ }
+ err = configFile.Close()
+ if err != nil {
+ log.Println("Failed to close config file.")
+ }
+
+ //Server definitions:
+ var webServer *http.Server
+ var fcgiListen net.Listener
+ info.ListenSettings = configYml.Listen
+ info.ServeSettings = configYml.Serve
+ switch strings.ToLower(configYml.Listen.WebMethod) {
+ case "http":
+ webServer = &http.Server{Handler: pageHandler.GetRouter(configYml)}
+ go runBackgroundHttp(webServer, getListener(configYml, cwdDir), false)
+ case "fcgi":
+ fcgiListen = getListener(configYml, cwdDir)
+ if fcgiListen == nil {
+ log.Fatalln("Listener Nil")
+ } else {
+ go runBackgroundFCgi(pageHandler.GetRouter(configYml), fcgiListen)
+ }
+ default:
+ log.Fatalln("Unknown Web Method.")
+ }
+
+ //=====================
+ // Safe shutdown
+ sigs := make(chan os.Signal, 1)
+ signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
+
+ //Startup complete:
+ z := time.Now().Sub(y)
+ log.Printf("[Main] Took '%s' to fully initialize modules\n", z.String())
+
+ go func() {
+ <-sigs
+ fmt.Printf("\n")
+
+ log.Printf("[Main] Attempting safe shutdown\n")
+ a := time.Now()
+
+ if webServer != nil {
+ log.Printf("[Main] Shutting down HTTP server...\n")
+ err := webServer.Close()
+ if err != nil {
+ log.Println(err)
+ }
+ }
+
+ if fcgiListen != nil {
+ log.Printf("[Main] Shutting down FCGI server...\n")
+ err := fcgiListen.Close()
+ if err != nil {
+ log.Println(err)
+ }
+ }
+
+ log.Printf("[Main] Signalling program exit...\n")
+ b := time.Now().Sub(a)
+ log.Printf("[Main] Took '%s' to fully shutdown modules\n", b.String())
+ wg.Done()
+ }()
+ //
+ //=====================
+ wg.Wait()
+ log.Println("[Main] Goodbye")
+ //os.Exit(0)
+}
diff --git a/wappmcstat/utils.go b/wappmcstat/utils.go
new file mode 100644
index 0000000..b5fdd2a
--- /dev/null
+++ b/wappmcstat/utils.go
@@ -0,0 +1,86 @@
+package main
+
+import (
+ "golang.captainalm.com/mc-webserver/conf"
+ "log"
+ "net"
+ "net/http"
+ "net/http/fcgi"
+ "os"
+ "path"
+ "path/filepath"
+ "strings"
+)
+
+func check(err error) {
+ if err != nil {
+ panic(err)
+ }
+}
+
+func getListener(config conf.ConfigYaml, cwd string) net.Listener {
+ split := strings.Split(strings.ToLower(config.Listen.WebNetwork), ":")
+ if len(split) == 0 {
+ log.Fatalln("Invalid Web Network")
+ return nil
+ } else {
+ var theListener net.Listener
+ var theError error
+ log.Println("[Main] Socket Network Type: " + split[0])
+ log.Printf("[Main] Starting up %s server on %s...\n", config.Listen.WebMethod, config.Listen.Web)
+ switch split[0] {
+ case "tcp", "tcp4", "tcp6":
+ theListener, theError = net.Listen(strings.ToLower(config.Listen.WebNetwork), config.Listen.Web)
+ case "unix", "unixgram", "unixpacket":
+ socketPath := config.Listen.Web
+ if !filepath.IsAbs(socketPath) {
+ if !filepath.IsAbs(cwd) {
+ log.Fatalln("Web Path Not Absolute And No Working Directory.")
+ return nil
+ }
+ socketPath = path.Join(cwd, socketPath)
+ }
+ log.Println("[Main] Removing old socket.")
+ if err := os.RemoveAll(socketPath); err != nil {
+ log.Fatalln("Could Not Remove Old Socket.")
+ return nil
+ }
+ theListener, theError = net.Listen(strings.ToLower(config.Listen.WebNetwork), config.Listen.Web)
+ default:
+ log.Fatalln("Unknown Web Network.")
+ return nil
+ }
+ if theError != nil {
+ log.Fatalln("Failed to listen due to:", theError)
+ return nil
+ }
+ return theListener
+ }
+}
+
+func runBackgroundHttp(s *http.Server, l net.Listener, tlsEnabled bool) {
+ var err error
+ if tlsEnabled {
+ err = s.ServeTLS(l, "", "")
+ } else {
+ err = s.Serve(l)
+ }
+ if err != nil {
+ if err == http.ErrServerClosed {
+ log.Println("The http server shutdown successfully")
+ } else {
+ log.Fatalf("[Http] Error trying to host the http server: %s\n", err.Error())
+ }
+ }
+}
+
+func runBackgroundFCgi(h http.Handler, l net.Listener) {
+ err := fcgi.Serve(l, h)
+ if err != nil {
+ if err == net.ErrClosed {
+ log.Println("The fcgi server shutdown successfully")
+ } else {
+ log.Fatalf("[Http] Error trying to host the fcgi server: %s\n", err.Error())
+ }
+ }
+}