Add initial code
This commit is contained in:
commit
70601daac0
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
bin/
|
||||||
|
config.yml
|
||||||
|
.DS_Store
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2022 Jacob Gunther
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
16
Makefile
Normal file
16
Makefile
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
build:
|
||||||
|
go build -o bin/main src/*.go
|
||||||
|
|
||||||
|
build-linux:
|
||||||
|
GOOS=linux go build -o bin/main src/*.go
|
||||||
|
|
||||||
|
build-windows:
|
||||||
|
GOOS=windows go build -o bin/main src/*.go
|
||||||
|
|
||||||
|
run:
|
||||||
|
go run src/*.go
|
||||||
|
|
||||||
|
flush-cache:
|
||||||
|
go run src/*.go --flush-cache
|
||||||
|
|
||||||
|
all: build
|
||||||
32
README.md
Normal file
32
README.md
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
# API Server
|
||||||
|
The REST server that powers the API for mcstatus.io. This repository is open source to allow developers to run their own Minecraft server status API server.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- [Go](https://go.dev/)
|
||||||
|
- [Redis](https://redis.io/)
|
||||||
|
- [GNU Make](https://www.gnu.org/software/make/) (*optional*)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Clone the repository to a folder
|
||||||
|
- `git clone https://github.com/mcstatus-io/api-server.git`
|
||||||
|
2. Move the working directory into the folder
|
||||||
|
- `cd api-server`
|
||||||
|
3. Install all required dependencies
|
||||||
|
- `go get ...`
|
||||||
|
4. Build the executable
|
||||||
|
- Using GNU make
|
||||||
|
- `make build`
|
||||||
|
- Without GNU make
|
||||||
|
- `go build -o .\bin\main.exe .\src\*.go` (Windows)
|
||||||
|
- `go build -o bin/main src/*.go` (Unix)
|
||||||
|
5. Copy the `config.example.yml` file to `config.yml` and edit the details
|
||||||
|
6. Start the API server
|
||||||
|
- `.\bin\main.exe` (Windows)
|
||||||
|
- `bin/main` (Unix)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[MIT License](https://github.com/mcstatus-io/api-server/blob/main/LICENSE)
|
||||||
|
|
||||||
7
config.example.yml
Normal file
7
config.example.yml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
environment: development
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 3001
|
||||||
|
redis: redis://127.0.0.1:6379/0
|
||||||
|
cache_enable: false
|
||||||
|
status_cache_ttl: 5m
|
||||||
|
favicon_cache_ttl: 24h
|
||||||
33
go.mod
Normal file
33
go.mod
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
module main
|
||||||
|
|
||||||
|
go 1.18
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/PassTheMayo/mcstatus/v4 v4.0.0
|
||||||
|
github.com/go-yaml/yaml v2.1.0+incompatible
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/buaazp/fasthttprouter v0.1.1
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/andybalholm/brotli v1.0.4 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
|
github.com/go-redis/redis/v8 v8.11.5
|
||||||
|
github.com/klauspost/compress v1.15.6 // indirect
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
|
github.com/valyala/fasthttp v1.37.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/fatih/color v1.13.0 // indirect
|
||||||
|
github.com/kr/pretty v0.3.0 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.9 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||||
|
github.com/rogpeppe/go-internal v1.8.1 // indirect
|
||||||
|
golang.org/x/net v0.0.0-20220622184535-263ec571b305 // indirect
|
||||||
|
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664 // indirect
|
||||||
|
)
|
||||||
73
go.sum
Normal file
73
go.sum
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
github.com/PassTheMayo/mcstatus/v4 v4.0.0 h1:SgIAwpLkH2Vt7KDZZ7tAsAufyI9XRy9YdgMUJVAw1o0=
|
||||||
|
github.com/PassTheMayo/mcstatus/v4 v4.0.0/go.mod h1:62Iagw0yGRyufsvEqPI4T8fJpIf6UUh14junH8ZR2LY=
|
||||||
|
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
|
||||||
|
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||||
|
github.com/buaazp/fasthttprouter v0.1.1 h1:4oAnN0C3xZjylvZJdP35cxfclyn4TYkW6Y+DSvS+h8Q=
|
||||||
|
github.com/buaazp/fasthttprouter v0.1.1/go.mod h1:h/Ap5oRVLeItGKTVBb+heQPks+HdIUtGmI4H5WCYijM=
|
||||||
|
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
||||||
|
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
|
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
|
||||||
|
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||||
|
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
|
||||||
|
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
|
||||||
|
github.com/go-yaml/yaml v2.1.0+incompatible h1:RYi2hDdss1u4YE7GwixGzWwVo47T8UQwnTLB6vQiq+o=
|
||||||
|
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
|
||||||
|
github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||||
|
github.com/klauspost/compress v1.15.6 h1:6D9PcO8QWu0JyaQ2zUMmu16T1T+zjjEpP91guRsvDfY=
|
||||||
|
github.com/klauspost/compress v1.15.6/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
|
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||||
|
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U=
|
||||||
|
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||||
|
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||||
|
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||||
|
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||||
|
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||||
|
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||||
|
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
|
||||||
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||||
|
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||||
|
github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg=
|
||||||
|
github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
|
github.com/valyala/fasthttp v1.37.0 h1:7WHCyI7EAkQMVmrfBhWTCOaeROb1aCBiTopx63LkMbE=
|
||||||
|
github.com/valyala/fasthttp v1.37.0/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I=
|
||||||
|
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||||
|
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||||
|
golang.org/x/net v0.0.0-20220622184535-263ec571b305 h1:dAgbJ2SP4jD6XYfMNLVj0BF21jo2PjChrtGaAvF5M3I=
|
||||||
|
golang.org/x/net v0.0.0-20220622184535-263ec571b305/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664 h1:wEZYwx+kK+KlZ0hpvP2Ls1Xr4+RWnlzGFwPP0aiDjIU=
|
||||||
|
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
28
src/config.go
Normal file
28
src/config.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-yaml/yaml"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Configuration struct {
|
||||||
|
Environment string `yaml:"environment"`
|
||||||
|
Host string `yaml:"host"`
|
||||||
|
Port uint16 `yaml:"port"`
|
||||||
|
Redis string `yaml:"redis"`
|
||||||
|
CacheEnable bool `yaml:"cache_enable"`
|
||||||
|
StatusCacheTTL time.Duration `yaml:"status_cache_ttl"`
|
||||||
|
FaviconCacheTTL time.Duration `yaml:"favicon_cache_ttl"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Configuration) ReadFile(path string) error {
|
||||||
|
data, err := ioutil.ReadFile(path)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return yaml.Unmarshal(data, c)
|
||||||
|
}
|
||||||
BIN
src/default-icon.png
Normal file
BIN
src/default-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
125
src/main.go
Normal file
125
src/main.go
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/buaazp/fasthttprouter"
|
||||||
|
"github.com/valyala/fasthttp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
config *Configuration = &Configuration{}
|
||||||
|
r *Redis = &Redis{}
|
||||||
|
blockedServers []string = nil
|
||||||
|
blockedServersMutex *sync.Mutex = &sync.Mutex{}
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
if err := config.ReadFile("config.yml"); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.Connect(config.Redis); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Successfully connected to Redis")
|
||||||
|
|
||||||
|
if Contains(os.Args, "--flush-cache") {
|
||||||
|
keys, err := r.Keys("java:*")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = r.Delete(keys...); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
keys, err = r.Keys("bedrock:*")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = r.Delete(keys...); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
keys, err = r.Keys("favicon:*")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = r.Delete(keys...); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Successfully flushed all cache keys")
|
||||||
|
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if instanceID := os.Getenv("INSTANCE_ID"); len(instanceID) > 0 {
|
||||||
|
value, err := strconv.ParseUint(instanceID, 10, 16)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
config.Port += uint16(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := GetBlockedServerList(); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func middleware(next fasthttp.RequestHandler) fasthttp.RequestHandler {
|
||||||
|
return func(ctx *fasthttp.RequestCtx) {
|
||||||
|
if config.Environment == "development" {
|
||||||
|
log.Printf("GET %s - %s \"%s\"", ctx.Request.URI().Path(), ctx.RemoteAddr(), ctx.UserAgent())
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Response.Header.Set("Access-Control-Allow-Headers", "*")
|
||||||
|
ctx.Response.Header.Set("Access-Control-Allow-Methods", "HEAD,GET,POST,OPTIONS")
|
||||||
|
ctx.Response.Header.Set("Access-Control-Allow-Origin", "*")
|
||||||
|
ctx.Response.Header.Set("Access-Control-Expose-Headers", "X-Cache-Hit,X-Cache-Time-Remaining,X-Server-Status,Content-Disposition")
|
||||||
|
|
||||||
|
next(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
defer r.Close()
|
||||||
|
|
||||||
|
router := fasthttprouter.New()
|
||||||
|
router.GET("/ping", PingHandler)
|
||||||
|
router.GET("/status/java/:address", JavaStatusHandler)
|
||||||
|
router.GET("/status/bedrock/:address", BedrockStatusHandler)
|
||||||
|
router.GET("/favicon/:address", FaviconNoExtensionHandler)
|
||||||
|
router.GET("/favicon/:address/*filename", FaviconHandler)
|
||||||
|
|
||||||
|
router.PanicHandler = func(rc *fasthttp.RequestCtx, i interface{}) {
|
||||||
|
log.Println(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
router.NotFound = func(ctx *fasthttp.RequestCtx) {
|
||||||
|
WriteError(ctx, nil, http.StatusNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Listening on %s:%d\n", config.Host, config.Port)
|
||||||
|
log.Fatal(fasthttp.ListenAndServe(fmt.Sprintf("%s:%d", config.Host, config.Port), middleware(router.Handler)))
|
||||||
|
|
||||||
|
s := make(chan os.Signal)
|
||||||
|
signal.Notify(s, os.Interrupt, syscall.SIGTERM)
|
||||||
|
<-s
|
||||||
|
}
|
||||||
155
src/redis.go
Normal file
155
src/redis.go
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-redis/redis/v8"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Redis struct {
|
||||||
|
Conn *redis.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Redis) Connect(uri string) error {
|
||||||
|
opts, err := redis.ParseURL(uri)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
conn := redis.NewClient(opts)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||||
|
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err = conn.Ping(ctx).Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Conn = conn
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Redis) TTL(key string) (time.Duration, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||||
|
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
val, err := r.Conn.TTL(ctx, key).Result()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, redis.Nil) {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Redis) Exists(key string) (bool, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||||
|
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
val, err := r.Conn.Exists(ctx, key).Result()
|
||||||
|
|
||||||
|
return val == 1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Redis) Get(key string) (string, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||||
|
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
val, err := r.Conn.Get(ctx, key).Result()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, redis.Nil) {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Redis) GetBytes(key string) ([]byte, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||||
|
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
result := r.Conn.Get(ctx, key)
|
||||||
|
|
||||||
|
if err := result.Err(); err != nil {
|
||||||
|
if errors.Is(err, redis.Nil) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Redis) Set(key string, value interface{}, ttl time.Duration) error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||||
|
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
return r.Conn.Set(ctx, key, value, ttl).Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Redis) GetValueAndTTL(key string) (bool, string, time.Duration, error) {
|
||||||
|
exists, err := r.Exists(key)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return false, "", 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
return false, "", 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
value, err := r.Get(key)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return false, "", 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ttl, err := r.TTL(key)
|
||||||
|
|
||||||
|
return true, value, ttl, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Redis) Keys(pattern string) ([]string, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||||
|
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
res := r.Conn.Keys(ctx, pattern)
|
||||||
|
|
||||||
|
if err := res.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.Result()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Redis) Delete(keys ...string) error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||||
|
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
return r.Conn.Del(ctx, keys...).Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Redis) Close() error {
|
||||||
|
return r.Conn.Close()
|
||||||
|
}
|
||||||
308
src/routes.go
Normal file
308
src/routes.go
Normal file
@ -0,0 +1,308 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/valyala/fasthttp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func PingHandler(ctx *fasthttp.RequestCtx) {
|
||||||
|
ctx.SetBodyString(http.StatusText(http.StatusOK))
|
||||||
|
}
|
||||||
|
|
||||||
|
func JavaStatusHandler(ctx *fasthttp.RequestCtx) {
|
||||||
|
host, port, err := ParseAddress(ctx.UserValue("address").(string), 25565)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
WriteError(ctx, nil, http.StatusBadRequest, "Invalid/malformed address")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheEnabled, cacheKey, err := IsCacheEnabled(ctx, "java", host, port)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
WriteError(ctx, err, http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if cacheEnabled {
|
||||||
|
exists, cache, ttl, err := r.GetValueAndTTL(cacheKey)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
WriteError(ctx, err, http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
ctx.Response.Header.Set("X-Cache-Hit", "TRUE")
|
||||||
|
ctx.Response.Header.Set("X-Cache-Time-Remaining", strconv.Itoa(int(ttl.Seconds())))
|
||||||
|
ctx.SetContentType("application/json")
|
||||||
|
ctx.SetBodyString(cache)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Response.Header.Set("X-Cache-Hit", "FALSE")
|
||||||
|
|
||||||
|
status := GetJavaStatus(host, port)
|
||||||
|
|
||||||
|
if status.Online && status.Response.Favicon != nil {
|
||||||
|
data, err := base64.StdEncoding.DecodeString(strings.Replace(*status.Response.Favicon, "data:image/png;base64,", "", 1))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
WriteError(ctx, err, http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.Set(fmt.Sprintf("favicon:%s-%d", host, port), data, config.FaviconCacheTTL); err != nil {
|
||||||
|
WriteError(ctx, err, http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(status)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
WriteError(ctx, err, http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = r.Set(cacheKey, data, config.StatusCacheTTL); err != nil {
|
||||||
|
WriteError(ctx, err, http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.SetContentType("application/json")
|
||||||
|
ctx.SetBody(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BedrockStatusHandler(ctx *fasthttp.RequestCtx) {
|
||||||
|
host, port, err := ParseAddress(ctx.UserValue("address").(string), 19132)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
WriteError(ctx, nil, http.StatusBadRequest, "Invalid/malformed address")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheEnabled, cacheKey, err := IsCacheEnabled(ctx, "bedrock", host, port)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
WriteError(ctx, err, http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if cacheEnabled {
|
||||||
|
exists, cache, ttl, err := r.GetValueAndTTL(cacheKey)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
WriteError(ctx, err, http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
ctx.Response.Header.Set("X-Cache-Hit", "TRUE")
|
||||||
|
ctx.Response.Header.Set("X-Cache-Time-Remaining", strconv.Itoa(int(ttl.Seconds())))
|
||||||
|
ctx.SetContentType("application/json")
|
||||||
|
ctx.SetBodyString(cache)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Response.Header.Set("X-Cache-Hit", "FALSE")
|
||||||
|
|
||||||
|
data, err := json.Marshal(GetBedrockStatus(host, port))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
WriteError(ctx, err, http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = r.Set(cacheKey, data, config.StatusCacheTTL); err != nil {
|
||||||
|
WriteError(ctx, err, http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.SetContentType("application/json")
|
||||||
|
ctx.SetBody(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func FaviconNoExtensionHandler(ctx *fasthttp.RequestCtx) {
|
||||||
|
host, port, err := ParseAddress(ctx.UserValue("address").(string), 25565)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
WriteError(ctx, nil, http.StatusBadRequest, "Invalid/malformed address")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheKey := fmt.Sprintf("favicon:%s-%d", host, port)
|
||||||
|
|
||||||
|
exists, err := r.Exists(cacheKey)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
WriteError(ctx, err, http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
value, err := r.GetBytes(cacheKey)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
WriteError(ctx, err, http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Response.Header.Set("X-Cache-Hit", "TRUE")
|
||||||
|
ctx.Response.Header.Set("X-Server-Status", "online-cache")
|
||||||
|
ctx.SetContentType("image/png")
|
||||||
|
ctx.SetBody(value)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Response.Header.Set("X-Cache-Hit", "FALSE")
|
||||||
|
|
||||||
|
status := GetJavaStatus(host, port)
|
||||||
|
|
||||||
|
if !status.Online {
|
||||||
|
ctx.Response.Header.Set("X-Server-Status", "offline")
|
||||||
|
ctx.SetContentType("image/png")
|
||||||
|
ctx.SetBody(defaultIconBytes)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if status.Response.Favicon == nil {
|
||||||
|
ctx.Response.Header.Set("X-Server-Status", "online-no-icon")
|
||||||
|
ctx.SetContentType("image/png")
|
||||||
|
ctx.SetBody(defaultIconBytes)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := base64.StdEncoding.DecodeString(strings.Replace(*status.Response.Favicon, "data:image/png;base64,", "", 1))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
WriteError(ctx, err, http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.Set(cacheKey, data, config.FaviconCacheTTL); err != nil {
|
||||||
|
WriteError(ctx, err, http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Response.Header.Set("X-Server-Status", "online")
|
||||||
|
ctx.SetContentType("image/png")
|
||||||
|
ctx.SetBody(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func FaviconHandler(ctx *fasthttp.RequestCtx) {
|
||||||
|
filename := ctx.UserValue("filename").(string)
|
||||||
|
|
||||||
|
if !strings.HasSuffix(filename, ".png") {
|
||||||
|
WriteError(ctx, nil, http.StatusBadRequest, "Filename must end with .png")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
host, port, err := ParseAddress(ctx.UserValue("address").(string), 25565)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
WriteError(ctx, nil, http.StatusBadRequest, "Invalid/malformed address")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheKey := fmt.Sprintf("favicon:%s-%d", host, port)
|
||||||
|
|
||||||
|
exists, err := r.Exists(cacheKey)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
WriteError(ctx, err, http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
value, err := r.GetBytes(cacheKey)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
WriteError(ctx, err, http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Response.Header.Set("X-Cache-Hit", "TRUE")
|
||||||
|
ctx.Response.Header.Set("X-Server-Status", "online-cache")
|
||||||
|
ctx.Response.Header.Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", filename))
|
||||||
|
ctx.SetContentType("image/png")
|
||||||
|
ctx.SetBody(value)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Response.Header.Set("X-Cache-Hit", "FALSE")
|
||||||
|
|
||||||
|
status := GetJavaStatus(host, port)
|
||||||
|
|
||||||
|
if !status.Online {
|
||||||
|
ctx.Response.Header.Set("X-Server-Status", "offline")
|
||||||
|
ctx.Response.Header.Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", filename))
|
||||||
|
ctx.SetContentType("image/png")
|
||||||
|
ctx.SetBody(defaultIconBytes)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if status.Response.Favicon == nil {
|
||||||
|
ctx.Response.Header.Set("X-Server-Status", "online-no-icon")
|
||||||
|
ctx.Response.Header.Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", filename))
|
||||||
|
ctx.SetContentType("image/png")
|
||||||
|
ctx.SetBody(defaultIconBytes)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := base64.StdEncoding.DecodeString(strings.Replace(*status.Response.Favicon, "data:image/png;base64,", "", 1))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
WriteError(ctx, err, http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.Set(cacheKey, data, config.FaviconCacheTTL); err != nil {
|
||||||
|
WriteError(ctx, err, http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Response.Header.Set("X-Server-Status", "online")
|
||||||
|
ctx.Response.Header.Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", filename))
|
||||||
|
ctx.SetContentType("image/png")
|
||||||
|
ctx.SetBody(data)
|
||||||
|
}
|
||||||
251
src/status.go
Normal file
251
src/status.go
Normal file
@ -0,0 +1,251 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "github.com/PassTheMayo/mcstatus/v4"
|
||||||
|
|
||||||
|
type StatusResponse[T JavaStatus | BedrockStatus] struct {
|
||||||
|
Online bool `json:"online"`
|
||||||
|
Host string `json:"host"`
|
||||||
|
Port uint16 `json:"port"`
|
||||||
|
EULABlocked bool `json:"eula_blocked"`
|
||||||
|
Response *T `json:"response"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type JavaStatus struct {
|
||||||
|
Version *Version `json:"version"`
|
||||||
|
Players Players `json:"players"`
|
||||||
|
MOTD MOTD `json:"motd"`
|
||||||
|
Favicon *string `json:"favicon"`
|
||||||
|
ModInfo *ModInfo `json:"mod_info"`
|
||||||
|
SRVRecord *SRVRecord `json:"srv_record"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Players struct {
|
||||||
|
Online int `json:"online"`
|
||||||
|
Max int `json:"max"`
|
||||||
|
Sample []SamplePlayer `json:"sample"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SamplePlayer struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Clean string `json:"clean"`
|
||||||
|
HTML string `json:"html"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ModInfo struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Mods []Mod `json:"mods"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Mod struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BedrockStatus struct {
|
||||||
|
ServerGUID int64 `json:"server_guid"`
|
||||||
|
Edition *string `json:"edition"`
|
||||||
|
MOTD *MOTD `json:"motd"`
|
||||||
|
ProtocolVersion *int64 `json:"protocol_version"`
|
||||||
|
Version *string `json:"version"`
|
||||||
|
OnlinePlayers *int64 `json:"online_players"`
|
||||||
|
MaxPlayers *int64 `json:"max_players"`
|
||||||
|
ServerID *string `json:"server_id"`
|
||||||
|
Gamemode *string `json:"gamemode"`
|
||||||
|
GamemodeID *int64 `json:"gamemode_id"`
|
||||||
|
PortIPv4 *uint16 `json:"port_ipv4"`
|
||||||
|
PortIPv6 *uint16 `json:"port_ipv6"`
|
||||||
|
SRVRecord *SRVRecord `json:"srv_record"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MOTD struct {
|
||||||
|
Raw string `json:"raw"`
|
||||||
|
Clean string `json:"clean"`
|
||||||
|
HTML string `json:"html"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Version struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Protocol int `json:"protocol"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SRVRecord struct {
|
||||||
|
Host string `json:"host"`
|
||||||
|
Port uint16 `json:"port"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetJavaStatus(host string, port uint16) (resp StatusResponse[JavaStatus]) {
|
||||||
|
status, err := mcstatus.Status(host, port)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
statusLegacy, err := mcstatus.StatusLegacy(host, port)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
resp = StatusResponse[JavaStatus]{
|
||||||
|
Online: false,
|
||||||
|
Host: host,
|
||||||
|
Port: port,
|
||||||
|
EULABlocked: IsBlockedAddress(host),
|
||||||
|
Response: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp = StatusResponse[JavaStatus]{
|
||||||
|
Online: true,
|
||||||
|
Host: host,
|
||||||
|
Port: port,
|
||||||
|
EULABlocked: IsBlockedAddress(host),
|
||||||
|
Response: &JavaStatus{
|
||||||
|
Version: nil,
|
||||||
|
Players: Players{
|
||||||
|
Online: statusLegacy.Players.Online,
|
||||||
|
Max: statusLegacy.Players.Max,
|
||||||
|
Sample: make([]SamplePlayer, 0),
|
||||||
|
},
|
||||||
|
MOTD: MOTD{
|
||||||
|
Raw: statusLegacy.MOTD.Raw,
|
||||||
|
Clean: statusLegacy.MOTD.Clean,
|
||||||
|
HTML: statusLegacy.MOTD.HTML,
|
||||||
|
},
|
||||||
|
Favicon: nil,
|
||||||
|
ModInfo: nil,
|
||||||
|
SRVRecord: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if statusLegacy.Version != nil {
|
||||||
|
resp.Response.Version = &Version{
|
||||||
|
Name: statusLegacy.Version.Name,
|
||||||
|
Protocol: statusLegacy.Version.Protocol,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if statusLegacy.SRVResult != nil {
|
||||||
|
resp.Response.SRVRecord = &SRVRecord{
|
||||||
|
Host: statusLegacy.SRVResult.Host,
|
||||||
|
Port: statusLegacy.SRVResult.Port,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
samplePlayers := make([]SamplePlayer, 0)
|
||||||
|
|
||||||
|
for _, player := range status.Players.Sample {
|
||||||
|
samplePlayers = append(samplePlayers, SamplePlayer{
|
||||||
|
ID: player.ID,
|
||||||
|
Name: player.Name,
|
||||||
|
Clean: player.Clean,
|
||||||
|
HTML: player.HTML,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
resp = StatusResponse[JavaStatus]{
|
||||||
|
Online: true,
|
||||||
|
Host: host,
|
||||||
|
Port: port,
|
||||||
|
EULABlocked: IsBlockedAddress(host),
|
||||||
|
Response: &JavaStatus{
|
||||||
|
Version: &Version{
|
||||||
|
Name: status.Version.Name,
|
||||||
|
Protocol: status.Version.Protocol,
|
||||||
|
},
|
||||||
|
Players: Players{
|
||||||
|
Online: status.Players.Online,
|
||||||
|
Max: status.Players.Max,
|
||||||
|
Sample: samplePlayers,
|
||||||
|
},
|
||||||
|
MOTD: MOTD{
|
||||||
|
Raw: status.MOTD.Raw,
|
||||||
|
Clean: status.MOTD.Clean,
|
||||||
|
HTML: status.MOTD.HTML,
|
||||||
|
},
|
||||||
|
Favicon: status.Favicon,
|
||||||
|
ModInfo: nil,
|
||||||
|
SRVRecord: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if status.ModInfo != nil {
|
||||||
|
mods := make([]Mod, 0)
|
||||||
|
|
||||||
|
for _, mod := range status.ModInfo.Mods {
|
||||||
|
mods = append(mods, Mod{
|
||||||
|
ID: mod.ID,
|
||||||
|
Version: mod.Version,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Response.ModInfo = &ModInfo{
|
||||||
|
Type: status.ModInfo.Type,
|
||||||
|
Mods: mods,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if status.SRVResult != nil {
|
||||||
|
resp.Response.SRVRecord = &SRVRecord{
|
||||||
|
Host: status.SRVResult.Host,
|
||||||
|
Port: status.SRVResult.Port,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetBedrockStatus(host string, port uint16) (resp StatusResponse[BedrockStatus]) {
|
||||||
|
status, err := mcstatus.StatusBedrock(host, port)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
resp = StatusResponse[BedrockStatus]{
|
||||||
|
Online: false,
|
||||||
|
Host: host,
|
||||||
|
Port: port,
|
||||||
|
EULABlocked: IsBlockedAddress(host),
|
||||||
|
Response: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp = StatusResponse[BedrockStatus]{
|
||||||
|
Online: true,
|
||||||
|
Host: host,
|
||||||
|
Port: port,
|
||||||
|
EULABlocked: IsBlockedAddress(host),
|
||||||
|
Response: &BedrockStatus{
|
||||||
|
ServerGUID: status.ServerGUID,
|
||||||
|
Edition: status.Edition,
|
||||||
|
MOTD: nil,
|
||||||
|
ProtocolVersion: status.ProtocolVersion,
|
||||||
|
Version: status.Version,
|
||||||
|
OnlinePlayers: status.OnlinePlayers,
|
||||||
|
MaxPlayers: status.MaxPlayers,
|
||||||
|
ServerID: status.ServerID,
|
||||||
|
Gamemode: status.Gamemode,
|
||||||
|
GamemodeID: status.GamemodeID,
|
||||||
|
PortIPv4: status.PortIPv4,
|
||||||
|
PortIPv6: status.PortIPv6,
|
||||||
|
SRVRecord: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if status.MOTD != nil {
|
||||||
|
resp.Response.MOTD = &MOTD{
|
||||||
|
Raw: status.MOTD.Raw,
|
||||||
|
Clean: status.MOTD.Clean,
|
||||||
|
HTML: status.MOTD.HTML,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if status.SRVResult != nil {
|
||||||
|
resp.Response.SRVRecord = &SRVRecord{
|
||||||
|
Host: status.SRVResult.Host,
|
||||||
|
Port: status.SRVResult.Port,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
161
src/util.go
Normal file
161
src/util.go
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha1"
|
||||||
|
_ "embed"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/valyala/fasthttp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
//go:embed default-icon.png
|
||||||
|
defaultIconBytes []byte
|
||||||
|
ipAddressRegExp = regexp.MustCompile("^\\d{1,3}(\\.\\d{1,3}){3}$")
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrNoAddressMatch = errors.New("address does not match any known format")
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetBlockedServerList() error {
|
||||||
|
resp, err := http.Get("https://sessionserver.mojang.com/blockedservers")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
blockedServersMutex.Lock()
|
||||||
|
blockedServers = strings.Split(string(body), "\n")
|
||||||
|
blockedServersMutex.Unlock()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsBlockedAddress(address string) bool {
|
||||||
|
split := strings.Split(strings.ToLower(address), ".")
|
||||||
|
|
||||||
|
isIPAddress := ipAddressRegExp.MatchString(address)
|
||||||
|
|
||||||
|
for k := range split {
|
||||||
|
newAddress := ""
|
||||||
|
|
||||||
|
switch k {
|
||||||
|
case 0:
|
||||||
|
{
|
||||||
|
newAddress = strings.Join(split, ".")
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
if isIPAddress {
|
||||||
|
newAddress = fmt.Sprintf("%s.*", strings.Join(split[0:len(split)-k], "."))
|
||||||
|
} else {
|
||||||
|
newAddress = fmt.Sprintf("*.%s", strings.Join(split[k:], "."))
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newAddressBytes := sha1.Sum([]byte(newAddress))
|
||||||
|
newAddressHash := hex.EncodeToString(newAddressBytes[:])
|
||||||
|
|
||||||
|
blockedServersMutex.Lock()
|
||||||
|
|
||||||
|
if Contains(blockedServers, newAddressHash) {
|
||||||
|
blockedServersMutex.Unlock()
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
blockedServersMutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseAddress(address string, defaultPort uint16) (string, uint16, error) {
|
||||||
|
result := strings.SplitN(address, ":", 2)
|
||||||
|
|
||||||
|
if len(result) < 1 {
|
||||||
|
return "", 0, ErrNoAddressMatch
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(result) < 2 {
|
||||||
|
return result[0], defaultPort, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
port, err := strconv.ParseUint(result[1], 10, 16)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result[0], uint16(port), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func WriteError(ctx *fasthttp.RequestCtx, err error, statusCode int, body ...string) {
|
||||||
|
ctx.SetStatusCode(statusCode)
|
||||||
|
|
||||||
|
if len(body) > 0 {
|
||||||
|
ctx.SetBodyString(body[0])
|
||||||
|
} else {
|
||||||
|
ctx.SetBodyString(http.StatusText(statusCode))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsCacheEnabled(ctx *fasthttp.RequestCtx, cacheType, host string, port uint16) (bool, string, error) {
|
||||||
|
key := fmt.Sprintf("%s:%s-%d", cacheType, host, port)
|
||||||
|
|
||||||
|
if authKey := ctx.Request.Header.Peek("Authorization"); len(authKey) > 0 {
|
||||||
|
exists, err := r.Exists(fmt.Sprintf("auth_key:%s", authKey))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
WriteError(ctx, err, http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return config.CacheEnable, key, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
return false, key, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return config.CacheEnable, key, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Contains[T comparable](arr []T, x T) bool {
|
||||||
|
for _, v := range arr {
|
||||||
|
if v == x {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user