Initial commit.

This commit is contained in:
Captain ALM 2023-08-13 20:19:47 +01:00
commit 40d64b8ecf
Signed by: alfred
GPG Key ID: 4E4ADD02609997B1
38 changed files with 2248 additions and 0 deletions

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
* text=auto eol=lf

21
.gitignore vendored Normal file
View File

@ -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_

8
.idea/.gitignore vendored Normal file
View File

@ -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

7
.idea/discord.xml Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DiscordProjectSettings">
<option name="show" value="PROJECT_FILES" />
<option name="description" value="" />
</component>
</project>

9
.idea/mc-webserver.iml Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/mc-webserver.iml" filepath="$PROJECT_DIR$/.idea/mc-webserver.iml" />
</modules>
</component>
</project>

12
.woodpecker/build.yml Normal file
View File

@ -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

29
LICENSE Normal file
View File

@ -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.

52
Makefile Normal file
View File

@ -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_

11
README.md Normal file
View File

@ -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)

11
conf/cache.go Normal file
View File

@ -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"`
}

6
conf/config.go Normal file
View File

@ -0,0 +1,6 @@
package conf
type ConfigYaml struct {
Listen ListenYaml `yaml:"listen"`
Serve ServeYaml `yaml:"serve"`
}

8
conf/listen.go Normal file
View File

@ -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"`
}

41
conf/serve.go Normal file
View File

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

14
config.example.yml Normal file
View File

@ -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

10
go.mod Normal file
View File

@ -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
)

313
goinfo.go.html Normal file
View File

@ -0,0 +1,313 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="noindex, nofollow, nositelinkssearchbox">
<title>Go Info</title>
<style>
.full-heading {
margin: auto;
width: 80%;
border: black 1px solid;
text-align: center;
background-color: mediumslateblue;
}
table, th, td {
margin: auto;
text-align: left;
border: black 1px solid;
border-collapse: collapse;
word-break: break-word;
-ms-word-wrap: break-word;
word-wrap: break-word;
}
table, td {
background-color: lightgray;
}
table {
width: 80%;
}
th {
background-color: lightsteelblue;
width: 25%;
}
td {
width: 75%;
}
</style>
</head>
<body>
<p>
<div class="full-heading">
<h1>{{ .GoVersion }} - {{ .ProductName }}</h1>
</div>
</p>
<p>
{{ if .FullOutput }}
<div class="full-heading">
<b>
<a href="?">Less Output</a>
</b>
</div>
{{ else }}
<div class="full-heading">
<b>
<a href="?full">More Output</a>
</b>
</div>
{{ end }}
</p>
<p>
<table>
<tr>
<th>Product Name</th>
<td>{{ .ProductName }}</td>
</tr>
<tr>
<th>Product Description</th>
<td>{{ .ProductDescription }}</td>
</tr>
<tr>
<th>Product License</th>
<td>BSD 3-Clause License</td>
</tr>
<tr>
<th>Product Location</th>
<td>{{ .ProductLocation }}</td>
</tr>
<tr>
<th>Build Commit</th>
<td>#{{ .BuildVersion }}</td>
</tr>
<tr>
<th>Build Date</th>
<td>{{ .BuildDate }}</td>
</tr>
<tr>
<th>Working Directory</th>
<td>{{ .WorkingDirectory }}</td>
</tr>
{{ if .FullOutput }}
<tr>
<th>Process ID</th>
<td>{{ .ProcessID }}</td>
</tr>
<tr>
<th>Parent Process ID</th>
<td>{{ .ParentProcessID }}</td>
</tr>
{{ end }}
</table>
</p>
<p>
<table>
<tr>
<th>Go Version</th>
<td>{{ .GoVersion }}</td>
</tr>
<tr>
<th>Go Toolchain</th>
<td>{{ .Compiler }}</td>
</tr>
<tr>
<th>GOROOT</th>
<td>{{ .GoRoot }}</td>
</tr>
<tr>
<th>GOMAXPROCS</th>
<td>{{ .GoMaxProcs }}</td>
</tr>
<tr>
<th>Go Routine Count</th>
<td>{{ .GoRoutineNum }}</td>
</tr>
<tr>
<th>Go c go call Count</th>
<td>{{ .GoCGoCallNum }}</td>
</tr>
</table>
</p>
<p>
<table>
<tr>
<th>Hostname</th>
<td>{{ .Hostname }}</td>
</tr>
<tr>
<th>Operating System</th>
<td>{{ .GoOS }}</td>
</tr>
<tr>
<th>Architecture</th>
<td>{{ .GoArch }}</td>
</tr>
<tr>
<th>Number of Cores</th>
<td>{{ .NumCPU }}</td>
</tr>
<tr>
<th>Memory Page Size</th>
<td>{{ .PageSize }}</td>
</tr>
</table>
</p>
<p>
<table>
<tr>
<th>Listen Type</th>
<td>{{ .ListenSettings.WebNetwork }}</td>
</tr>
<tr>
<th>Listening Address</th>
<td>{{ .ListenSettings.Web }}</td>
</tr>
<tr>
<th>Listening Method</th>
<td>{{ .ListenSettings.WebMethod }}</td>
</tr>
<tr>
<th>Identifying</th>
<td>{{ .ListenSettings.Identify }}</td>
</tr>
{{ if and .FullOutput .ListenSettings.Identify }}
<tr>
<th>Server</th>
<td>Clerie Gilbert</td>
</tr>
<tr>
<th>Powered By</th>
<td>Love</td>
</tr>
<tr>
<th>Friendly</th>
<td>True</td>
</tr>
{{ end }}
</table>
</p>
<p>
<table>
<tr>
<th>Template Storage Path</th>
<td>{{ .ServeSettings.GetDataStoragePath }}</td>
</tr>
<tr>
<th>Served Domains</th>
<td>{{ .ServeSettings.GetDomainString }}</td>
</tr>
<tr>
<th>Range Supported</th>
<td>{{ .ServeSettings.RangeSupported }}</td>
</tr>
</table>
</p>
{{ if .FullOutput }}
<p>
<table>
<tr>
<th>Enable Template Caching</th>
<td>{{ .ServeSettings.CacheSettings.EnableTemplateCaching }}</td>
</tr>
<tr>
<th>Enable Template Cache Purge</th>
<td>{{ .ServeSettings.CacheSettings.EnableTemplateCachePurge }}</td>
</tr>
<tr>
<th>Enable Content Caching</th>
<td>{{ .ServeSettings.CacheSettings.EnableContentsCaching }}</td>
</tr>
<tr>
<th>Enable Content Cache Purge</th>
<td>{{ .ServeSettings.CacheSettings.EnableContentsCachePurge }}</td>
</tr>
<tr>
<th>Max Age</th>
<td>{{ .ServeSettings.CacheSettings.MaxAge }}</td>
</tr>
<tr>
<th>Enable Last Modified Precondition Support</th>
<td>{{ .ServeSettings.CacheSettings.NotModifiedResponseUsingLastModified }}</td>
</tr>
<tr>
<th>Enable ETag Precondition Support</th>
<td>{{ .ServeSettings.CacheSettings.NotModifiedResponseUsingETags }}</td>
</tr>
</table>
</p>
<p>
<table>
<tr>
<th>Environment Variables</th>
</tr>
{{ range .Environment }}
<tr>
<td>{{ . }}</td>
</tr>
{{ end }}
</table>
</p>
{{ end }}
<p>
<table>
<tr>
<th>Number of Registered Pages</th>
<td>{{ len .RegisteredPages }}</td>
</tr>
</table>
</p>
{{ if and .FullOutput (not (eq (len .RegisteredPages) 0)) }}
<p>
<table>
<tr>
<th>Registered Pages</th>
</tr>
{{ range .RegisteredPages }}
<tr>
<td>{{ . }}</td>
</tr>
{{ end }}
</table>
</p>
{{ end }}
</p>
<p>
<table>
<tr>
<th>Number of Cached Pages</th>
<td>{{ len .CachedPages }}</td>
</tr>
</table>
</p>
{{ if and .FullOutput (not (eq (len .CachedPages) 0)) }}
<p>
<table>
<tr>
<th>Cached Pages</th>
</tr>
{{ range .CachedPages }}
<tr>
<td>{{ . }}</td>
</tr>
{{ end }}
</table>
</p>
{{ end }}
</p>
<p>
{{ if .FullOutput }}
<div class="full-heading">
<b>
<a href="?">Less Output</a>
</b>
</div>
{{ else }}
<div class="full-heading">
<b>
<a href="?full">More Output</a>
</b>
</div>
{{ end }}
</p>
</body>
</html>

54
pageHandler/get-router.go Normal file
View File

@ -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)
})
}

174
pageHandler/go-info-page.go Normal file
View File

@ -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()
}
}

304
pageHandler/page-handler.go Normal file
View File

@ -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)
}

View File

@ -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()
}

18
pageHandler/pages.go Normal file
View File

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

View File

@ -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"`
}

View File

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

View File

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

View File

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

View File

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

48
pageHandler/utils/etag.go Normal file
View File

@ -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 ""
}

View File

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

View File

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

View File

@ -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")
}
}

6
utils/info/conf-info.go Normal file
View File

@ -0,0 +1,6 @@
package info
import "golang.captainalm.com/mc-webserver/conf"
var ListenSettings conf.ListenYaml
var ServeSettings conf.ServeYaml

View File

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

View File

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

View File

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

31
utils/yaml/date-type.go Normal file
View File

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

148
wappmcstat/main.go Normal file
View File

@ -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)
}

86
wappmcstat/utils.go Normal file
View File

@ -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())
}
}
}