Initial commit.
This commit is contained in:
commit
40d64b8ecf
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
||||
* text=auto eol=lf
|
21
.gitignore
vendored
Normal file
21
.gitignore
vendored
Normal 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
8
.idea/.gitignore
vendored
Normal 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
7
.idea/discord.xml
Normal 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
9
.idea/mc-webserver.iml
Normal 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
8
.idea/modules.xml
Normal 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
12
.woodpecker/build.yml
Normal 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
29
LICENSE
Normal 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
52
Makefile
Normal 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
11
README.md
Normal 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
11
conf/cache.go
Normal 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
6
conf/config.go
Normal file
@ -0,0 +1,6 @@
|
||||
package conf
|
||||
|
||||
type ConfigYaml struct {
|
||||
Listen ListenYaml `yaml:"listen"`
|
||||
Serve ServeYaml `yaml:"serve"`
|
||||
}
|
8
conf/listen.go
Normal file
8
conf/listen.go
Normal 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
41
conf/serve.go
Normal 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
14
config.example.yml
Normal 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
10
go.mod
Normal 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
313
goinfo.go.html
Normal 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
54
pageHandler/get-router.go
Normal 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
174
pageHandler/go-info-page.go
Normal 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
304
pageHandler/page-handler.go
Normal 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)
|
||||
}
|
14
pageHandler/page-provider.go
Normal file
14
pageHandler/page-provider.go
Normal 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
18
pageHandler/pages.go
Normal 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
|
||||
}
|
28
pageHandler/pages/index/data.go
Normal file
28
pageHandler/pages/index/data.go
Normal 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"`
|
||||
}
|
193
pageHandler/pages/index/index-page.go
Normal file
193
pageHandler/pages/index/index-page.go
Normal 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
|
||||
}
|
||||
}
|
25
pageHandler/pages/index/mc.go
Normal file
25
pageHandler/pages/index/mc.go
Normal 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
|
||||
}
|
202
pageHandler/pages/index/template-marshal.go
Normal file
202
pageHandler/pages/index/template-marshal.go
Normal 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
|
||||
}
|
78
pageHandler/utils/content-range-value.go
Normal file
78
pageHandler/utils/content-range-value.go
Normal 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
48
pageHandler/utils/etag.go
Normal 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 ""
|
||||
}
|
47
pageHandler/utils/partial-range-writer.go
Normal file
47
pageHandler/utils/partial-range-writer.go
Normal 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
|
||||
}
|
153
pageHandler/utils/process-preconditions.go
Normal file
153
pageHandler/utils/process-preconditions.go
Normal 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
|
||||
}
|
45
pageHandler/utils/utils.go
Normal file
45
pageHandler/utils/utils.go
Normal 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
6
utils/info/conf-info.go
Normal file
@ -0,0 +1,6 @@
|
||||
package info
|
||||
|
||||
import "golang.captainalm.com/mc-webserver/conf"
|
||||
|
||||
var ListenSettings conf.ListenYaml
|
||||
var ServeSettings conf.ServeYaml
|
13
utils/info/product-info.go
Normal file
13
utils/info/product-info.go
Normal 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
|
||||
}
|
10
utils/io/buffered-writer.go
Normal file
10
utils/io/buffered-writer.go
Normal 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
|
||||
}
|
10
utils/io/counting-writer.go
Normal file
10
utils/io/counting-writer.go
Normal 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
31
utils/yaml/date-type.go
Normal 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
148
wappmcstat/main.go
Normal 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
86
wappmcstat/utils.go
Normal 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())
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user