Dockerfile, Error handling, comments, docker-compose (#2)

This commit is contained in:
dailypush 2023-03-30 11:53:31 -04:00 committed by GitHub
parent 4488a9db70
commit 3c83c64f1a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 214 additions and 60 deletions

View File

@ -1,14 +1,37 @@
build: # Variables
go build -o bin/main src/*.go BINARY := bin/main
SOURCES := $(wildcard src/*.go)
build-linux: # Build for the current platform
GOOS=linux go build -o bin/main src/*.go build: $(BINARY)
build-windows: $(BINARY): $(SOURCES)
GOOS=windows go build -o bin/main src/*.go go build -o $(BINARY) $(SOURCES)
run: # Build for Linux
go run src/*.go build-linux: GOOS := linux
build-linux: EXTENSION :=
build-linux: build-cross
flush-cache: # Build for Windows
go run src/*.go --flush-cache build-windows: GOOS := windows
build-windows: EXTENSION := .exe
build-windows: build-cross
# Cross-compile for a specific platform (used by build-linux and build-windows)
build-cross: export GOOS := $(GOOS)
build-cross: BINARY := $(BINARY)$(EXTENSION)
build-cross: $(BINARY)
# Run the application
run: build
./$(BINARY)
# Flush cache
flush-cache: build
./$(BINARY) --flush-cache
# Clean up generated files
.PHONY: clean
clean:
rm -f $(BINARY) $(BINARY).exe

View File

@ -1,8 +1,8 @@
environment: production environment: development
host: 127.0.0.1 host: 0.0.0.0
port: 3001 port: 3001
redis: redis://username:password@127.0.0.1:6379/0 redis: ${REDIS_URL} # Use an environment variable to define the Redis URL
cache: cache:
java_status_duration: 1m java_status_duration: 1m
bedrock_status_duration: 1m bedrock_status_duration: 1m
icon_duration: 15m icon_duration: 24h

36
docker-compose.yml Normal file
View File

@ -0,0 +1,36 @@
version: "3.9"
services:
ping-server:
build: .
image: ping-server
ports:
- "3001:3001"
depends_on:
- redis
networks:
- app_network
environment:
REDIS_URL: "redis://redis:6379" # Define the Redis URL using the hostname of the Redis service
volumes:
- type: bind
source: ./config.example.yml
target: /app/config.yml
read_only: true
redis:
image: "redis:latest"
command: ["redis-server", "--appendonly", "yes"]
ports:
- "6379:6379"
networks:
- app_network
volumes:
- redis_data:/data
networks:
app_network:
driver: bridge
volumes:
redis_data:

40
dockerfile Normal file
View File

@ -0,0 +1,40 @@
# Build stage
FROM golang:1.18 AS build
WORKDIR /app
# Install required dependencies
COPY go.mod go.sum ./
RUN go mod download
# Copy the source code
COPY . .
# Build the executable with CGO disabled
RUN CGO_ENABLED=0 go build -o bin/main src/*.go
RUN mv config.example.yml ./bin/config.yml
#######################
# Test stage
FROM build as test
RUN go test -v ./...
#######################
# Runtime stage
FROM gcr.io/distroless/base
# Set the working directory
WORKDIR /app
# Copy the binary from the build stage
COPY --from=build /app/bin/main /app/main
COPY --from=build /app/bin/config.yml /app/config.yml
# Expose the port the app runs on (replace with your desired port)
EXPOSE 3001
# Run the app
CMD ["./main"]

View File

@ -1,18 +1,22 @@
package main package main
import ( import (
"errors"
"os" "os"
"strconv"
"time" "time"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
// CacheConfig contains cache-related settings.
type CacheConfig struct { type CacheConfig struct {
JavaStatusDuration time.Duration `yaml:"java_status_duration" json:"java_status_duration"` JavaStatusDuration time.Duration `yaml:"java_status_duration" json:"java_status_duration"`
BedrockStatusDuration time.Duration `yaml:"bedrock_status_duration" json:"bedrock_status_duration"` BedrockStatusDuration time.Duration `yaml:"bedrock_status_duration" json:"bedrock_status_duration"`
IconDuration time.Duration `yaml:"icon_duration" json:"icon_duration"` IconDuration time.Duration `yaml:"icon_duration" json:"icon_duration"`
} }
// Config represents the application configuration.
type Config struct { type Config struct {
Environment string `yaml:"environment"` Environment string `yaml:"environment"`
Host string `yaml:"host"` Host string `yaml:"host"`
@ -21,12 +25,40 @@ type Config struct {
Cache CacheConfig `yaml:"cache"` Cache CacheConfig `yaml:"cache"`
} }
// ReadFile reads the configuration from the given file and overrides values using environment variables.
func (c *Config) ReadFile(file string) error { func (c *Config) ReadFile(file string) error {
data, err := os.ReadFile(file) data, err := os.ReadFile(file)
if err != nil { if err != nil {
return err return err
} }
return yaml.Unmarshal(data, c) if err := yaml.Unmarshal(data, c); err != nil {
return err
}
return c.overrideWithEnvVars()
}
// overrideWithEnvVars overrides configuration values using environment variables.
func (c *Config) overrideWithEnvVars() error {
if env := os.Getenv("ENVIRONMENT"); env != "" {
c.Environment = env
}
if host := os.Getenv("HOST"); host != "" {
c.Host = host
}
if port := os.Getenv("PORT"); port != "" {
portInt, err := strconv.Atoi(port)
if err != nil {
return errors.New("invalid port value in environment variable")
}
c.Port = uint16(portInt)
}
if redisURL := os.Getenv("REDIS_URL"); redisURL != "" {
c.Redis = &redisURL
}
return nil
} }

View File

@ -15,8 +15,7 @@ var (
app *fiber.App = fiber.New(fiber.Config{ app *fiber.App = fiber.New(fiber.Config{
DisableStartupMessage: true, DisableStartupMessage: true,
ErrorHandler: func(ctx *fiber.Ctx, err error) error { ErrorHandler: func(ctx *fiber.Ctx, err error) error {
log.Println(ctx.Request().URI(), err) log.Printf("Error: %v - URI: %v\n", err, ctx.Request().URI())
return ctx.SendStatus(http.StatusInternalServerError) return ctx.SendStatus(http.StatusInternalServerError)
}, },
}) })
@ -25,24 +24,26 @@ var (
) )
func init() { func init() {
// Read config file
if err := config.ReadFile("config.yml"); err != nil { if err := config.ReadFile("config.yml"); err != nil {
log.Fatal(err) log.Fatalf("Failed to read config file: %v", err)
} }
// Get the blocked server list
if err := GetBlockedServerList(); err != nil { if err := GetBlockedServerList(); err != nil {
log.Fatal(err) log.Fatalf("Failed to retrieve EULA blocked servers: %v", err)
} }
log.Println("Successfully retrieved EULA blocked servers") log.Println("Successfully retrieved EULA blocked servers")
// Connect to Redis if the config is set
if config.Redis != nil { if config.Redis != nil {
if err := r.Connect(); err != nil { if err := r.Connect(); err != nil {
log.Fatal(err) log.Fatalf("Failed to connect to Redis: %v", err)
} }
log.Println("Successfully connected to Redis") log.Println("Successfully connected to Redis")
} }
// Set up middleware
app.Use(recover.New(recover.Config{ app.Use(recover.New(recover.Config{
EnableStackTrace: true, EnableStackTrace: true,
})) }))
@ -65,5 +66,7 @@ func main() {
defer r.Close() defer r.Close()
log.Printf("Listening on %s:%d\n", config.Host, config.Port) log.Printf("Listening on %s:%d\n", config.Host, config.Port)
log.Fatal(app.Listen(fmt.Sprintf("%s:%d", config.Host, config.Port))) if err := app.Listen(fmt.Sprintf("%s:%d", config.Host, config.Port)); err != nil {
log.Fatalf("Failed to start server: %v", err)
}
} }

View File

@ -2,38 +2,45 @@ package main
import ( import (
"context" "context"
"errors"
"time" "time"
"github.com/go-redis/redis/v8" "github.com/go-redis/redis/v8"
) )
const defaultTimeout = 5 * time.Second
// Redis is a wrapper around the Redis client.
type Redis struct { type Redis struct {
Client *redis.Client Client *redis.Client
} }
// Connect establishes a connection to the Redis server using the configuration.
func (r *Redis) Connect() error { func (r *Redis) Connect() error {
opts, err := redis.ParseURL(*config.Redis) if config.Redis == nil {
return errors.New("missing Redis configuration")
}
opts, err := redis.ParseURL(*config.Redis)
if err != nil { if err != nil {
return err return err
} }
r.Client = redis.NewClient(opts) r.Client = redis.NewClient(opts)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
defer cancel() defer cancel()
return r.Client.Ping(ctx).Err() return r.Client.Ping(ctx).Err()
} }
// Get retrieves the value and TTL for a given key.
func (r *Redis) Get(key string) ([]byte, time.Duration, error) { func (r *Redis) Get(key string) ([]byte, time.Duration, error) {
if r.Client == nil { if r.Client == nil {
return nil, 0, nil return nil, 0, nil
} }
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
defer cancel() defer cancel()
p := r.Client.Pipeline() p := r.Client.Pipeline()
@ -54,30 +61,31 @@ func (r *Redis) Get(key string) ([]byte, time.Duration, error) {
return data, ttl.Val(), err return data, ttl.Val(), err
} }
// Set sets the value and TTL for a given key.
func (r *Redis) Set(key string, value interface{}, ttl time.Duration) error { func (r *Redis) Set(key string, value interface{}, ttl time.Duration) error {
if r.Client == nil { if r.Client == nil {
return nil return nil
} }
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
defer cancel() defer cancel()
return r.Client.Set(ctx, key, value, ttl).Err() return r.Client.Set(ctx, key, value, ttl).Err()
} }
// Increment increments the integer value of a key by 1.
func (r *Redis) Increment(key string) error { func (r *Redis) Increment(key string) error {
if r.Client == nil { if r.Client == nil {
return nil return nil
} }
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
defer cancel() defer cancel()
return r.Client.Incr(ctx, key).Err() return r.Client.Incr(ctx, key).Err()
} }
// Close closes the Redis client connection.
func (r *Redis) Close() error { func (r *Redis) Close() error {
if r.Client == nil { if r.Client == nil {
return nil return nil

View File

@ -18,20 +18,24 @@ func init() {
app.Use(NotFoundHandler) app.Use(NotFoundHandler)
} }
// StatisticsResponse is the structure for the response of the statistics route.
type StatisticsResponse struct { type StatisticsResponse struct {
Cache CacheConfig `json:"cache"` Cache CacheConfig `json:"cache"`
} }
// PingHandler responds with a 200 OK status for simple health checks.
func PingHandler(ctx *fiber.Ctx) error { func PingHandler(ctx *fiber.Ctx) error {
return ctx.SendStatus(http.StatusOK) return ctx.SendStatus(http.StatusOK)
} }
// StatisticsHandler returns the cache configuration in the response.
func StatisticsHandler(ctx *fiber.Ctx) error { func StatisticsHandler(ctx *fiber.Ctx) error {
return ctx.JSON(StatisticsResponse{ return ctx.JSON(StatisticsResponse{
Cache: config.Cache, Cache: config.Cache,
}) })
} }
// JavaStatusHandler returns the status of the Java edition Minecraft server specified in the address parameter.
func JavaStatusHandler(ctx *fiber.Ctx) error { func JavaStatusHandler(ctx *fiber.Ctx) error {
host, port, err := ParseAddress(ctx.Params("address"), 25565) host, port, err := ParseAddress(ctx.Params("address"), 25565)
@ -58,6 +62,7 @@ func JavaStatusHandler(ctx *fiber.Ctx) error {
return ctx.JSON(response) return ctx.JSON(response)
} }
// BedrockStatusHandler returns the status of the Bedrock edition Minecraft server specified in the address parameter.
func BedrockStatusHandler(ctx *fiber.Ctx) error { func BedrockStatusHandler(ctx *fiber.Ctx) error {
host, port, err := ParseAddress(ctx.Params("address"), 19132) host, port, err := ParseAddress(ctx.Params("address"), 19132)
@ -84,6 +89,7 @@ func BedrockStatusHandler(ctx *fiber.Ctx) error {
return ctx.JSON(response) return ctx.JSON(response)
} }
// IconHandler returns the server icon for the specified Java edition Minecraft server.
func IconHandler(ctx *fiber.Ctx) error { func IconHandler(ctx *fiber.Ctx) error {
host, port, err := ParseAddress(ctx.Params("address"), 25565) host, port, err := ParseAddress(ctx.Params("address"), 25565)
@ -106,10 +112,12 @@ func IconHandler(ctx *fiber.Ctx) error {
return ctx.Type("png").Send(icon) return ctx.Type("png").Send(icon)
} }
// DefaultIconHandler returns the default server icon.
func DefaultIconHandler(ctx *fiber.Ctx) error { func DefaultIconHandler(ctx *fiber.Ctx) error {
return ctx.Type("png").Send(defaultIcon) return ctx.Type("png").Send(defaultIcon)
} }
// NotFoundHandler handles requests to routes that do not exist and returns a 404 Not Found status.
func NotFoundHandler(ctx *fiber.Ctx) error { func NotFoundHandler(ctx *fiber.Ctx) error {
return ctx.SendStatus(http.StatusNotFound) return ctx.SendStatus(http.StatusNotFound)
} }

View File

@ -16,18 +16,19 @@ import (
var ( var (
//go:embed icon.png //go:embed icon.png
defaultIcon []byte defaultIcon []byte
blockedServers *MutexArray[string] = nil blockedServers *MutexArray = nil
ipAddressRegExp *regexp.Regexp = regexp.MustCompile(`^\d{1,3}(\.\d{1,3}){3}$`) ipAddressRegex *regexp.Regexp = regexp.MustCompile(`^\d{1,3}(\.\d{1,3}){3}$`)
) )
type MutexArray[K comparable] struct { // MutexArray is a thread-safe array for storing and checking values.
List []K type MutexArray struct {
List []interface{}
Mutex *sync.Mutex Mutex *sync.Mutex
} }
func (m *MutexArray[K]) Has(value K) bool { // Has checks if the given value is present in the array.
func (m *MutexArray) Has(value interface{}) bool {
m.Mutex.Lock() m.Mutex.Lock()
defer m.Mutex.Unlock() defer m.Mutex.Unlock()
for _, v := range m.List { for _, v := range m.List {
@ -39,9 +40,11 @@ func (m *MutexArray[K]) Has(value K) bool {
return false return false
} }
// GetBlockedServerList fetches the list of blocked servers from Mojang's session server.
func GetBlockedServerList() error { func GetBlockedServerList() error {
resp, err := http.Get("https://sessionserver.mojang.com/blockedservers") resp, err := http.Get("https://sessionserver.mojang.com/blockedservers")
if err != nil { if err != nil {
return err return err
} }
@ -53,43 +56,44 @@ func GetBlockedServerList() error {
defer resp.Body.Close() defer resp.Body.Close()
body, err := io.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return err return err
} }
blockedServers = &MutexArray[string]{ // Convert []string to []interface{}
List: strings.Split(string(body), "\n"), strSlice := strings.Split(string(body), "\n")
interfaceSlice := make([]interface{}, len(strSlice))
for i, v := range strSlice {
interfaceSlice[i] = v
}
blockedServers = &MutexArray{
List: interfaceSlice,
Mutex: &sync.Mutex{}, Mutex: &sync.Mutex{},
} }
return nil return nil
} }
// IsBlockedAddress checks if the given address is in the blocked servers list.
func IsBlockedAddress(address string) bool { func IsBlockedAddress(address string) bool {
split := strings.Split(strings.ToLower(address), ".") split := strings.Split(strings.ToLower(address), ".")
isIPAddress := ipAddressRegExp.MatchString(address) isIPAddress := ipAddressRegex.MatchString(address)
for k := range split { for k := range split {
newAddress := "" var newAddress string
switch k { switch k {
case 0: case 0:
{
newAddress = strings.Join(split, ".") newAddress = strings.Join(split, ".")
break
}
default: default:
{
if isIPAddress { if isIPAddress {
newAddress = fmt.Sprintf("%s.*", strings.Join(split[0:len(split)-k], ".")) newAddress = fmt.Sprintf("%s.*", strings.Join(split[0:len(split)-k], "."))
} else { } else {
newAddress = fmt.Sprintf("*.%s", strings.Join(split[k:], ".")) newAddress = fmt.Sprintf("*.%s", strings.Join(split[k:], "."))
} }
break
}
} }
newAddressBytes := sha1.Sum([]byte(newAddress)) newAddressBytes := sha1.Sum([]byte(newAddress))
@ -103,6 +107,7 @@ func IsBlockedAddress(address string) bool {
return false return false
} }
// ParseAddress extracts the hostname and port from the given address string, and returns the default port if none is provided.
func ParseAddress(address string, defaultPort uint16) (string, uint16, error) { func ParseAddress(address string, defaultPort uint16) (string, uint16, error) {
result := strings.SplitN(address, ":", 2) result := strings.SplitN(address, ":", 2)
@ -115,7 +120,6 @@ func ParseAddress(address string, defaultPort uint16) (string, uint16, error) {
} }
port, err := strconv.ParseUint(result[1], 10, 16) port, err := strconv.ParseUint(result[1], 10, 16)
if err != nil { if err != nil {
return "", 0, err return "", 0, err
} }