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