Files
2026-05-24 19:52:45 -07:00

310 lines
8.4 KiB
Go

package main
import (
"bytes"
"compress/gzip"
"context"
"crypto/md5"
"encoding/json"
"fmt"
"html"
"io"
"log"
"mime"
"net/http"
"os"
"os/signal"
"path"
"strings"
"sync"
"syscall"
"text/template"
"time"
)
type ManifestArticle struct {
ID string `json:"id"`
BannerMain string `json:"banner_main"`
BannerList string `json:"banner_list"`
BannerDescription string `json:"banner_description"`
Date time.Time `json:"date"`
Title string `json:"title"`
Description string `json:"description"`
Author string `json:"author"`
Tags []string `json:"tags"`
}
type ManifestNode struct {
ID string `json:"id"`
Icon string `json:"icon"`
Name string `json:"name"`
Description string `json:"description"`
}
type Manifest struct {
Links []ManifestNode `json:"links"`
Authors []ManifestNode `json:"authors"`
Tags []ManifestNode `json:"tags"`
Articles []ManifestArticle `json:"articles"`
}
type CacheInfo struct {
HeaderETag string
HeaderContentType string
ContentPlainText []byte
ContentCompressed []byte
}
var (
MANIFEST Manifest
MANIFEST_RAW string
HTTP_ADDRESS = os.Getenv("HTTP_ADDRESS")
CACHE_VERSION = fmt.Sprintf("%X", time.Now().Unix())
)
func init() {
// Default Values
if HTTP_ADDRESS == "" {
HTTP_ADDRESS = "127.0.0.1:8080"
}
// Manifest Parsing
data, err := os.ReadFile("public/blog/manifest.json")
if err != nil {
log.Fatalln("Cannot Read Manifest JSON:", err)
}
if err := json.Unmarshal(data, &MANIFEST); err != nil {
log.Fatalln("Cannot Parse Manifest JSON:", err)
}
MANIFEST_RAW = string(data)
}
func main() {
// Startup Services
var stopCtx, stop = context.WithCancel(context.Background())
var stopWg sync.WaitGroup
go SetupHTTP(stopCtx, &stopWg)
// Await Shutdown Signal
cancel := make(chan os.Signal, 1)
signal.Notify(cancel, syscall.SIGINT, syscall.SIGTERM)
<-cancel
stop()
// Begin Shutdown Process
timeout, finish := context.WithTimeout(context.Background(), time.Minute)
defer finish()
go func() {
<-timeout.Done()
if timeout.Err() == context.DeadlineExceeded {
log.Fatalln("[main] Cleanup timeout! Exiting now.")
}
}()
stopWg.Wait()
log.Println("[main] All done, bye bye!")
os.Exit(0)
}
func SetupHTTP(stop context.Context, await *sync.WaitGroup) {
mux := http.NewServeMux()
loc := map[string]any{
"Version": CACHE_VERSION,
}
// Webpage Endpoints //
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" {
ServeTemplate(w, r, "templates/home.html", loc)
return
}
if r.Method == http.MethodGet {
ServeTemplate(w, r, "templates/404.html", loc)
return
}
w.WriteHeader(http.StatusMethodNotAllowed)
})
mux.HandleFunc("/blog/{slug...}", func(w http.ResponseWriter, r *http.Request) {
// Although this is a SPA, we still need to render
// the OG meta tags for SEO / Social Media Sites
var article *ManifestArticle
for _, art := range MANIFEST.Articles {
if strings.EqualFold(art.ID, r.PathValue("slug")) {
article = &art
break
}
}
ServeTemplate(w, r, "templates/blog.html", map[string]any{
"Version": CACHE_VERSION,
"ManifestRaw": html.EscapeString(MANIFEST_RAW),
"Manifest": MANIFEST,
"Article": article,
})
})
// Secret Endpoints //
mux.HandleFunc("/secret/sprites", func(w http.ResponseWriter, r *http.Request) {
ServeTemplate(w, r, "templates/secret_sprites.html", loc)
})
mux.HandleFunc("/secret/picross", func(w http.ResponseWriter, r *http.Request) {
ServeTemplate(w, r, "templates/secret_picross_board.html", loc)
})
mux.HandleFunc("/secret/picross/award", func(w http.ResponseWriter, r *http.Request) {
ServeTemplate(w, r, "templates/secret_picross_award.html", loc)
})
// Special Endpoints //
mux.HandleFunc("/blog/rss.xml", func(w http.ResponseWriter, r *http.Request) {
ServeTemplate(w, r, "templates/rss.xml", map[string]any{
"Version": CACHE_VERSION,
"Manifest": MANIFEST,
"Date": time.Now().UTC().Format(time.RFC1123),
"RFC1123": time.RFC1123,
})
})
mux.HandleFunc("/sitemap.xml", func(w http.ResponseWriter, r *http.Request) {
ServeTemplate(w, r, "templates/sitemap.xml", map[string]any{
"Version": CACHE_VERSION,
"Manifest": MANIFEST,
"Date": time.Now().UTC().Format("2006-01-02"),
"Locations": []string{
"https://panca.kz/",
"https://panca.kz/blog",
},
})
})
mux.HandleFunc("/qr", func(w http.ResponseWriter, r *http.Request) {
redirects := []string{
"https://www.youtube.com/watch?v=dQw4w9WgXcQ", // Risk Astley - Never Gonna Give You Up
"https://www.youtube.com/watch?v=DnmHG3mJv1o", // Masayoshi Takanaka - Tropic Birds
"https://www.youtube.com/watch?v=ZZCt92zLmLI", // Blur - Intermission
"https://www.twitch.tv/robcdee", // awesome sauce
"https://piapro.net/intl/en.html", // mikudayooo
}
index := int(time.Now().Unix()/60) % len(redirects) // rotate every minute
http.Redirect(w, r, redirects[index], http.StatusTemporaryRedirect)
})
// Resource Endpoints //
mux.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
ServeFile(w, r, "favicon.ico")
})
mux.HandleFunc("/robots.txt", func(w http.ResponseWriter, r *http.Request) {
ServeFile(w, r, "robots.txt")
})
mux.HandleFunc("/public/{path...}", func(w http.ResponseWriter, r *http.Request) {
ServeFile(w, r, path.Clean(r.PathValue("path")))
})
// Create HTTP Server //
svr := http.Server{
Handler: mux,
Addr: HTTP_ADDRESS,
MaxHeaderBytes: 4096,
IdleTimeout: 5 * time.Second,
ReadHeaderTimeout: 5 * time.Second,
WriteTimeout: 30 * time.Second,
ReadTimeout: 30 * time.Second,
}
await.Add(1)
go func() {
defer await.Done()
<-stop.Done()
svr.Shutdown(context.Background())
log.Println("[http] Cleaned up HTTP")
}()
log.Printf("[http] Listening @ %s\n", svr.Addr)
if err := svr.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalln("[http] Listen Error:", err)
}
}
func ServeTemplate(w http.ResponseWriter, r *http.Request, filepath string, locals any) {
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
clientCacheKey := r.Header.Get("If-None-Match")
clientCompress := strings.Contains(r.Header.Get("Accept-Encoding"), "gzip")
// Render Template //
html := bytes.Buffer{}
tmpl, err := template.ParseFiles(filepath)
if err != nil {
log.Printf("[http] Cannot parse template '%s': %s\n", filepath, err)
return
}
if err := tmpl.Execute(&html, locals); err != nil {
log.Printf("[http] Cannot render template '%s': %s \n", filepath, err)
w.WriteHeader(http.StatusInternalServerError)
return
}
// Cache Validation //
serverCacheKey := fmt.Sprintf("%x", md5.Sum(html.Bytes()))
serverFileType := mime.TypeByExtension(path.Ext(filepath))
if strings.EqualFold(clientCacheKey, serverCacheKey) {
w.WriteHeader(http.StatusNotModified)
return
}
// Stream Contents //
w.Header().Set("ETag", serverCacheKey)
w.Header().Set("Content-Type", serverFileType)
// w.Header().Set("Cache-Control", "public, max-age=3600, must-revalidate")
if clientCompress {
w.Header().Set("Content-Encoding", "gzip")
pack := gzip.NewWriter(w)
pack.Write(html.Bytes())
pack.Close()
} else {
w.Write(html.Bytes())
}
}
func ServeFile(w http.ResponseWriter, r *http.Request, filepath string) {
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
// Open File //
file, err := os.Open("./public/" + filepath)
if err != nil {
if os.IsNotExist(err) {
w.WriteHeader(http.StatusNotFound)
return
}
log.Printf("[http] Cannot open file '%s': %s\n", filepath, err)
w.WriteHeader(http.StatusInternalServerError)
return
}
defer file.Close()
// Skip Compression on Large/Media Files //
serverFileType := mime.TypeByExtension(path.Ext(filepath))
clientCompress := strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") &&
!strings.Contains(serverFileType, "image") &&
!strings.Contains(serverFileType, "audio") &&
!strings.Contains(serverFileType, "video")
// Stream Contents //
w.Header().Set("Content-Type", serverFileType)
w.Header().Set("X-Robots-Tag", "noindex")
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
if clientCompress {
w.Header().Set("Content-Encoding", "gzip")
pack := gzip.NewWriter(w)
io.Copy(pack, file)
pack.Close()
} else {
io.Copy(w, file)
}
}