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, }) }