135 lines
3.4 KiB
Go
Executable File
135 lines
3.4 KiB
Go
Executable File
package main
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"flag"
|
|
"html/template"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"golang.org/x/crypto/acme/autocert"
|
|
)
|
|
|
|
var (
|
|
dev = flag.Bool("dev", false, "Development mode (HTTP on port 8080)")
|
|
domain = flag.String("domain", "inou.com", "Domain for TLS certificate")
|
|
certDir = flag.String("certs", "/tank/inou/certs", "Certificate cache directory")
|
|
tmplDir = flag.String("templates", "templates", "Templates directory")
|
|
staticDir = flag.String("static", "static", "Static files directory")
|
|
)
|
|
|
|
var templates *template.Template
|
|
|
|
func main() {
|
|
flag.Parse()
|
|
loadTemplates()
|
|
|
|
mux := http.NewServeMux()
|
|
|
|
mux.HandleFunc("/", handleLanding)
|
|
mux.HandleFunc("/login", handleLogin)
|
|
mux.HandleFunc("/dashboard", handleDashboard)
|
|
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir(*staticDir))))
|
|
|
|
handler := securityHeaders(mux)
|
|
|
|
if *dev {
|
|
log.Println("Development mode: http://localhost:8080")
|
|
log.Fatal(http.ListenAndServe(":8080", handler))
|
|
} else {
|
|
go redirectHTTP()
|
|
serveTLS(handler)
|
|
}
|
|
}
|
|
|
|
func loadTemplates() {
|
|
pattern := filepath.Join(*tmplDir, "*.tmpl")
|
|
var err error
|
|
templates, err = template.ParseGlob(pattern)
|
|
if err != nil {
|
|
log.Fatalf("Failed to load templates: %v", err)
|
|
}
|
|
log.Printf("Loaded templates: %s", pattern)
|
|
}
|
|
|
|
func render(w http.ResponseWriter, name string, data any) {
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
err := templates.ExecuteTemplate(w, name, data)
|
|
if err != nil {
|
|
log.Printf("Template error: %v", err)
|
|
http.Error(w, "Internal error", http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
func securityHeaders(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("X-Content-Type-Options", "nosniff")
|
|
w.Header().Set("X-Frame-Options", "DENY")
|
|
w.Header().Set("X-XSS-Protection", "1; mode=block")
|
|
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
|
|
w.Header().Del("Server")
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
func redirectHTTP() {
|
|
srv := &http.Server{
|
|
Addr: ":80",
|
|
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
target := "https://" + r.Host + r.URL.Path
|
|
if r.URL.RawQuery != "" {
|
|
target += "?" + r.URL.RawQuery
|
|
}
|
|
http.Redirect(w, r, target, http.StatusMovedPermanently)
|
|
}),
|
|
}
|
|
log.Println("HTTP redirect: :80 → :443")
|
|
if err := srv.ListenAndServe(); err != nil {
|
|
log.Printf("HTTP redirect server error: %v", err)
|
|
}
|
|
}
|
|
|
|
func serveTLS(handler http.Handler) {
|
|
if err := os.MkdirAll(*certDir, 0700); err != nil {
|
|
log.Fatalf("Failed to create cert directory: %v", err)
|
|
}
|
|
|
|
m := &autocert.Manager{
|
|
Cache: autocert.DirCache(*certDir),
|
|
Prompt: autocert.AcceptTOS,
|
|
HostPolicy: autocert.HostWhitelist(*domain),
|
|
}
|
|
|
|
srv := &http.Server{
|
|
Addr: ":443",
|
|
Handler: handler,
|
|
TLSConfig: &tls.Config{GetCertificate: m.GetCertificate},
|
|
}
|
|
|
|
log.Printf("HTTPS server: https://%s", *domain)
|
|
log.Fatal(srv.ListenAndServeTLS("", ""))
|
|
}
|
|
|
|
// Handlers
|
|
|
|
func handleLanding(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/" {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
render(w, "landing.tmpl", nil)
|
|
}
|
|
|
|
func handleLogin(w http.ResponseWriter, r *http.Request) {
|
|
render(w, "login.tmpl", nil)
|
|
}
|
|
|
|
func handleDashboard(w http.ResponseWriter, r *http.Request) {
|
|
data := map[string]any{
|
|
"Email": "johan@jongsma.me",
|
|
}
|
|
render(w, "dashboard.tmpl", data)
|
|
}
|