From 3c83c64f1ad07dfd6d308f6839bddae0f51b7bdc Mon Sep 17 00:00:00 2001 From: dailypush Date: Thu, 30 Mar 2023 11:53:31 -0400 Subject: [PATCH] Dockerfile, Error handling, comments, docker-compose (#2) --- Makefile | 43 ++++++++++++++++++++++++++-------- config.example.yml | 8 +++---- docker-compose.yml | 36 ++++++++++++++++++++++++++++ dockerfile | 40 ++++++++++++++++++++++++++++++++ src/config.go | 36 ++++++++++++++++++++++++++-- src/main.go | 19 ++++++++------- src/redis.go | 26 ++++++++++++++------- src/routes.go | 8 +++++++ src/util.go | 58 +++++++++++++++++++++++++--------------------- 9 files changed, 214 insertions(+), 60 deletions(-) create mode 100644 docker-compose.yml create mode 100644 dockerfile diff --git a/Makefile b/Makefile index dfab5e5..388aa40 100644 --- a/Makefile +++ b/Makefile @@ -1,14 +1,37 @@ -build: - go build -o bin/main src/*.go +# Variables +BINARY := bin/main +SOURCES := $(wildcard src/*.go) -build-linux: - GOOS=linux go build -o bin/main src/*.go +# Build for the current platform +build: $(BINARY) -build-windows: - GOOS=windows go build -o bin/main src/*.go +$(BINARY): $(SOURCES) + go build -o $(BINARY) $(SOURCES) -run: - go run src/*.go +# Build for Linux +build-linux: GOOS := linux +build-linux: EXTENSION := +build-linux: build-cross -flush-cache: - go run src/*.go --flush-cache \ No newline at end of file +# Build for Windows +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 diff --git a/config.example.yml b/config.example.yml index 2679170..6f5626e 100644 --- a/config.example.yml +++ b/config.example.yml @@ -1,8 +1,8 @@ -environment: production -host: 127.0.0.1 +environment: development +host: 0.0.0.0 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: java_status_duration: 1m bedrock_status_duration: 1m - icon_duration: 15m \ No newline at end of file + icon_duration: 24h \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..c4accb1 --- /dev/null +++ b/docker-compose.yml @@ -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: diff --git a/dockerfile b/dockerfile new file mode 100644 index 0000000..61bd2d2 --- /dev/null +++ b/dockerfile @@ -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"] diff --git a/src/config.go b/src/config.go index 6c58565..bfd70db 100644 --- a/src/config.go +++ b/src/config.go @@ -1,18 +1,22 @@ package main import ( + "errors" "os" + "strconv" "time" "gopkg.in/yaml.v3" ) +// CacheConfig contains cache-related settings. type CacheConfig struct { JavaStatusDuration time.Duration `yaml:"java_status_duration" json:"java_status_duration"` BedrockStatusDuration time.Duration `yaml:"bedrock_status_duration" json:"bedrock_status_duration"` IconDuration time.Duration `yaml:"icon_duration" json:"icon_duration"` } +// Config represents the application configuration. type Config struct { Environment string `yaml:"environment"` Host string `yaml:"host"` @@ -21,12 +25,40 @@ type Config struct { 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 { data, err := os.ReadFile(file) - if err != nil { 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 } diff --git a/src/main.go b/src/main.go index 7bfcd76..e48f58b 100644 --- a/src/main.go +++ b/src/main.go @@ -15,8 +15,7 @@ var ( app *fiber.App = fiber.New(fiber.Config{ DisableStartupMessage: true, 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) }, }) @@ -25,24 +24,26 @@ var ( ) func init() { + // Read config file 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 { - log.Fatal(err) + log.Fatalf("Failed to retrieve EULA blocked servers: %v", err) } - log.Println("Successfully retrieved EULA blocked servers") + // Connect to Redis if the config is set if config.Redis != 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") } + // Set up middleware app.Use(recover.New(recover.Config{ EnableStackTrace: true, })) @@ -65,5 +66,7 @@ func main() { defer r.Close() 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) + } } diff --git a/src/redis.go b/src/redis.go index 2c384f0..1a8bbc0 100644 --- a/src/redis.go +++ b/src/redis.go @@ -2,38 +2,45 @@ package main import ( "context" + "errors" "time" "github.com/go-redis/redis/v8" ) +const defaultTimeout = 5 * time.Second + +// Redis is a wrapper around the Redis client. type Redis struct { Client *redis.Client } +// Connect establishes a connection to the Redis server using the configuration. 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 { return err } r.Client = redis.NewClient(opts) - ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) - + ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout) defer cancel() 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) { if r.Client == nil { return nil, 0, nil } - ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) - + ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout) defer cancel() p := r.Client.Pipeline() @@ -54,30 +61,31 @@ func (r *Redis) Get(key string) ([]byte, time.Duration, error) { 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 { if r.Client == nil { return nil } - ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) - + ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout) defer cancel() 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 { if r.Client == nil { return nil } - ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) - + ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout) defer cancel() return r.Client.Incr(ctx, key).Err() } +// Close closes the Redis client connection. func (r *Redis) Close() error { if r.Client == nil { return nil diff --git a/src/routes.go b/src/routes.go index 94e137b..84df19c 100644 --- a/src/routes.go +++ b/src/routes.go @@ -18,20 +18,24 @@ func init() { app.Use(NotFoundHandler) } +// StatisticsResponse is the structure for the response of the statistics route. type StatisticsResponse struct { Cache CacheConfig `json:"cache"` } +// PingHandler responds with a 200 OK status for simple health checks. func PingHandler(ctx *fiber.Ctx) error { return ctx.SendStatus(http.StatusOK) } +// StatisticsHandler returns the cache configuration in the response. func StatisticsHandler(ctx *fiber.Ctx) error { return ctx.JSON(StatisticsResponse{ Cache: config.Cache, }) } +// JavaStatusHandler returns the status of the Java edition Minecraft server specified in the address parameter. func JavaStatusHandler(ctx *fiber.Ctx) error { host, port, err := ParseAddress(ctx.Params("address"), 25565) @@ -58,6 +62,7 @@ func JavaStatusHandler(ctx *fiber.Ctx) error { 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 { host, port, err := ParseAddress(ctx.Params("address"), 19132) @@ -84,6 +89,7 @@ func BedrockStatusHandler(ctx *fiber.Ctx) error { return ctx.JSON(response) } +// IconHandler returns the server icon for the specified Java edition Minecraft server. func IconHandler(ctx *fiber.Ctx) error { host, port, err := ParseAddress(ctx.Params("address"), 25565) @@ -106,10 +112,12 @@ func IconHandler(ctx *fiber.Ctx) error { return ctx.Type("png").Send(icon) } +// DefaultIconHandler returns the default server icon. func DefaultIconHandler(ctx *fiber.Ctx) error { 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 { return ctx.SendStatus(http.StatusNotFound) } diff --git a/src/util.go b/src/util.go index 5a78c25..72dae8d 100644 --- a/src/util.go +++ b/src/util.go @@ -15,19 +15,20 @@ import ( var ( //go:embed icon.png - defaultIcon []byte - blockedServers *MutexArray[string] = nil - ipAddressRegExp *regexp.Regexp = regexp.MustCompile(`^\d{1,3}(\.\d{1,3}){3}$`) + defaultIcon []byte + blockedServers *MutexArray = nil + ipAddressRegex *regexp.Regexp = regexp.MustCompile(`^\d{1,3}(\.\d{1,3}){3}$`) ) -type MutexArray[K comparable] struct { - List []K +// MutexArray is a thread-safe array for storing and checking values. +type MutexArray struct { + List []interface{} 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() - defer m.Mutex.Unlock() for _, v := range m.List { @@ -39,9 +40,11 @@ func (m *MutexArray[K]) Has(value K) bool { return false } + + +// GetBlockedServerList fetches the list of blocked servers from Mojang's session server. func GetBlockedServerList() error { resp, err := http.Get("https://sessionserver.mojang.com/blockedservers") - if err != nil { return err } @@ -53,42 +56,43 @@ func GetBlockedServerList() error { defer resp.Body.Close() body, err := io.ReadAll(resp.Body) - if err != nil { return err } - blockedServers = &MutexArray[string]{ - List: strings.Split(string(body), "\n"), + // Convert []string to []interface{} + 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{}, } return nil } + + +// IsBlockedAddress checks if the given address is in the blocked servers list. func IsBlockedAddress(address string) bool { split := strings.Split(strings.ToLower(address), ".") - isIPAddress := ipAddressRegExp.MatchString(address) + isIPAddress := ipAddressRegex.MatchString(address) for k := range split { - newAddress := "" + var newAddress string switch k { case 0: - { - newAddress = strings.Join(split, ".") - - break - } + newAddress = strings.Join(split, ".") default: - { - if isIPAddress { - newAddress = fmt.Sprintf("%s.*", strings.Join(split[0:len(split)-k], ".")) - } else { - newAddress = fmt.Sprintf("*.%s", strings.Join(split[k:], ".")) - } - - break + if isIPAddress { + newAddress = fmt.Sprintf("%s.*", strings.Join(split[0:len(split)-k], ".")) + } else { + newAddress = fmt.Sprintf("*.%s", strings.Join(split[k:], ".")) } } @@ -103,6 +107,7 @@ func IsBlockedAddress(address string) bool { 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) { 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) - if err != nil { return "", 0, err }