init: technical take home for gitlab operate
This commit is contained in:
197
main.go
Normal file
197
main.go
Normal file
@@ -0,0 +1,197 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Config represents the HTTP server configuration
|
||||
type Config struct {
|
||||
Port int `json:"port"`
|
||||
Host string `json:"host"`
|
||||
RootPath string `json:"root_path"`
|
||||
AuthSecret string `json:"auth_secret"`
|
||||
}
|
||||
|
||||
// Server containers contextual dependencies for the HTTP server
|
||||
type Server struct {
|
||||
config *Config
|
||||
logger *slog.Logger
|
||||
mux *http.ServeMux
|
||||
}
|
||||
|
||||
// NewServer creates a new instance with the provided configuration
|
||||
func NewServer(config *Config, logger *slog.Logger) *Server {
|
||||
return &Server{
|
||||
config: config,
|
||||
logger: logger,
|
||||
mux: http.NewServeMux(),
|
||||
}
|
||||
}
|
||||
|
||||
// SetupRoutes configures routes on the Server
|
||||
func (s *Server) SetupRoutes() {
|
||||
// Public file server
|
||||
fs := http.FileServer(http.Dir(s.config.RootPath))
|
||||
s.mux.Handle("/", fs)
|
||||
|
||||
// Protected API endpoint
|
||||
s.mux.Handle("/api/", s.authMiddleware(http.HandlerFunc(s.apiHandler)))
|
||||
}
|
||||
|
||||
// Start logs and starts the underlying HTTP server
|
||||
func (s *Server) Start() error {
|
||||
addr := fmt.Sprintf("%s:%d", s.config.Host, s.config.Port)
|
||||
s.logger.Info("starting HTTP server", "address", addr)
|
||||
return http.ListenAndServe(addr, s.mux)
|
||||
}
|
||||
|
||||
func main() {
|
||||
// setup structured logger
|
||||
jsonHandler := slog.NewJSONHandler(os.Stdout, nil)
|
||||
log := slog.New(jsonHandler)
|
||||
|
||||
// create and parse cli args for config file
|
||||
var configFile string
|
||||
flag.StringVar(&configFile, "config", "", "Configuration file for HTTP server.")
|
||||
flag.Parse()
|
||||
log.Debug("parsed command line flags")
|
||||
|
||||
// load configuration
|
||||
loader := &ConfigLoader{
|
||||
readFile: os.ReadFile,
|
||||
getEnv: os.Getenv,
|
||||
}
|
||||
config, err := loader.Load(configFile)
|
||||
if err != nil {
|
||||
log.Error("failed to load configuration", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
log.Info("server configuration loaded",
|
||||
"host", config.Host,
|
||||
"port", config.Port,
|
||||
"root_path", config.RootPath,
|
||||
"auth_enabled", config.AuthSecret != "")
|
||||
|
||||
// create and start server
|
||||
server := NewServer(config, log)
|
||||
server.SetupRoutes()
|
||||
|
||||
if err := server.Start(); err != nil {
|
||||
log.Error("server failed", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// ConfigLoader simplifies loading configuration from file and environment variables
|
||||
type ConfigLoader struct {
|
||||
readFile func(string) ([]byte, error)
|
||||
getEnv func(string) string
|
||||
}
|
||||
|
||||
// Load conditionally loads variables from the file and env
|
||||
func (cl *ConfigLoader) Load(configFile string) (*Config, error) {
|
||||
config := &Config{
|
||||
Port: 8080, // default port
|
||||
Host: "0.0.0.0", // default host
|
||||
RootPath: ".", // default root path
|
||||
}
|
||||
|
||||
if configFile != "" {
|
||||
if err := cl.loadFromFile(configFile, config); err != nil {
|
||||
return nil, fmt.Errorf("failed to load config from file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := cl.loadFromEnv(config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// loadFromFile reads and unmarshals the configuration
|
||||
func (cl *ConfigLoader) loadFromFile(filename string, config *Config) error {
|
||||
data, err := cl.readFile(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return json.Unmarshal(data, config)
|
||||
}
|
||||
|
||||
// loadFromEnv loads configuration from environment variables
|
||||
func (cl *ConfigLoader) loadFromEnv(config *Config) error {
|
||||
if port := cl.getEnv("SERVER_PORT"); port != "" {
|
||||
p, err := strconv.Atoi(port)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid SERVER_PORT: %w", err)
|
||||
}
|
||||
config.Port = p
|
||||
}
|
||||
|
||||
if host := cl.getEnv("SERVER_HOST"); host != "" {
|
||||
config.Host = host
|
||||
}
|
||||
|
||||
if rootPath := cl.getEnv("SERVER_ROOT_PATH"); rootPath != "" {
|
||||
config.RootPath = rootPath
|
||||
}
|
||||
|
||||
if authSecret := cl.getEnv("SERVER_AUTH_SECRET"); authSecret != "" {
|
||||
config.AuthSecret = authSecret
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateToken checks if the provided token matches the secret
|
||||
func validateToken(authHeader, secret string) bool {
|
||||
if !strings.HasPrefix(authHeader, "Bearer ") {
|
||||
return false
|
||||
}
|
||||
token := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
return token == secret
|
||||
}
|
||||
|
||||
// authMiddleware validates authentication using Bearer token
|
||||
func (s *Server) authMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Skip authentication if no secret is configured
|
||||
if s.config.AuthSecret == "" {
|
||||
s.logger.Warn("authentication bypassed - no secret configured")
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate token and respond accordingly
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if validateToken(authHeader, s.config.AuthSecret) {
|
||||
s.logger.Info("authentication successful", "path", r.URL.Path)
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Fallback on authentication failure
|
||||
s.logger.Warn("authentication failed", "path", r.URL.Path, "remote_addr", r.RemoteAddr)
|
||||
w.Header().Set("WWW-Authenticate", `Bearer realm="Restricted"`)
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
})
|
||||
}
|
||||
|
||||
// apiHandler handles authenticated API requests
|
||||
func (s *Server) apiHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"message": "authenticated request successful",
|
||||
"path": r.URL.Path,
|
||||
"method": r.Method,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user