From 811c9bb2f7358ff094fe13deb6d961088baa2d8f Mon Sep 17 00:00:00 2001
From: Alex <alex@scerba.org>
Date: Sat, 4 May 2024 17:28:37 -0400
Subject: Add Go webserver.

---
 cmd/http/errors.go |  23 ++++++++++++
 cmd/http/handle.go | 103 +++++++++++++++++++++++++++++++++++++++++++++++++++
 cmd/http/load.go   | 106 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 cmd/http/main.go   |  83 +++++++++++++++++++++++++++++++++++++++++
 cmd/http/middle.go |  32 ++++++++++++++++
 cmd/http/render.go |  36 ++++++++++++++++++
 6 files changed, 383 insertions(+)
 create mode 100644 cmd/http/errors.go
 create mode 100644 cmd/http/handle.go
 create mode 100644 cmd/http/load.go
 create mode 100644 cmd/http/main.go
 create mode 100644 cmd/http/middle.go
 create mode 100644 cmd/http/render.go

(limited to 'cmd/http')

diff --git a/cmd/http/errors.go b/cmd/http/errors.go
new file mode 100644
index 0000000..9406a9a
--- /dev/null
+++ b/cmd/http/errors.go
@@ -0,0 +1,23 @@
+package main
+
+import (
+	"fmt"
+	"net/http"
+	"runtime/debug"
+)
+
+func (app *application) serverError(w http.ResponseWriter, err error) {
+	trace := fmt.Sprintf("%s\n%s", err.Error(), debug.Stack())
+	app.errorLog.Output(2, trace)
+
+	http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+}
+
+func (app *application) clientError(w http.ResponseWriter, status int) {
+	app.errorLog.Printf("Clent error: %d\n", status)
+	http.Error(w, http.StatusText(status), status)
+}
+
+func (app *application) notFound(w http.ResponseWriter) {
+	app.clientError(w, http.StatusNotFound)
+}
diff --git a/cmd/http/handle.go b/cmd/http/handle.go
new file mode 100644
index 0000000..a8b1da1
--- /dev/null
+++ b/cmd/http/handle.go
@@ -0,0 +1,103 @@
+package main
+
+import (
+	"net/http"
+	"strings"
+)
+
+func (app *application) home(w http.ResponseWriter, r *http.Request) {
+	w.Header().Set("Content-Type", "text/html; charset=utf-8")
+
+	path := strings.Split(r.URL.Path, "/")
+	if path[1] != "" {
+		app.notFound(w)
+	} else {
+		/* 		p, err := app.aggregate("html/projects")
+		   		if err != nil {
+		   			app.serverError(w, err)
+		   		} */
+
+		err := renderTemplate(w, "index", nil)
+		if err != nil {
+			app.serverError(w, err)
+		}
+	}
+}
+
+func (app *application) faq(w http.ResponseWriter, r *http.Request) {
+	w.Header().Set("Content-Type", "text/html; charset=utf-8")
+
+	path := strings.Split(r.URL.Path, "/")
+	if path[1] != "faq" {
+		app.notFound(w)
+	} else {
+		err := renderTemplate(w, "faq", nil)
+		if err != nil {
+			app.serverError(w, err)
+		}
+	}
+}
+
+func (app *application) about(w http.ResponseWriter, r *http.Request) {
+	w.Header().Set("Content-Type", "text/html; charset=utf-8")
+
+	path := strings.Split(r.URL.Path, "/")
+	if path[1] != "about" {
+		app.notFound(w)
+	} else {
+		err := renderTemplate(w, "about", nil)
+		if err != nil {
+			app.serverError(w, err)
+		}
+	}
+}
+
+func (app *application) gallery(w http.ResponseWriter, r *http.Request) {
+	w.Header().Set("Content-Type", "text/html; charset=utf-8")
+
+	path := strings.Split(r.URL.Path, "/")
+	if path[1] != "gallery" {
+		app.notFound(w)
+	} else {
+		err := renderTemplate(w, "gallery", nil)
+		if err != nil {
+			app.serverError(w, err)
+		}
+	}
+}
+
+func (app *application) blog(w http.ResponseWriter, r *http.Request) {
+	w.Header().Set("Content-Type", "text/html; charset=utf-8")
+
+	path := strings.Split(r.URL.Path, "/")
+	if path[1] != "blog" {
+		app.notFound(w)
+	} else {
+		err := renderTemplate(w, "blog", nil)
+		if err != nil {
+			app.serverError(w, err)
+		}
+	}
+	/*
+		 	if len(path) > 4 {
+				app.notFound(w)
+			} else if len(path) == 4 && path[3] == "" {
+				http.Redirect(w, r, "/"+path[1]+"/"+path[2], http.StatusFound)
+			} else {
+				post, err := app.readFile("html" + strings.TrimSuffix(r.URL.Path, "/") + ".tmpl.html")
+				if err != nil {
+					app.notFound(w)
+					return
+				}
+
+				var posts []*Post
+				posts = append(posts, post)
+				p := &Posts{Contents: posts}
+
+				err = renderTemplate(w, path[1]+"/"+path[2], p)
+				if err != nil {
+					app.serverError(w, err)
+				}
+			}
+	*/
+}
diff --git a/cmd/http/load.go b/cmd/http/load.go
new file mode 100644
index 0000000..d63d7ce
--- /dev/null
+++ b/cmd/http/load.go
@@ -0,0 +1,106 @@
+package main
+
+import (
+	"os"
+	"regexp"
+	"sort"
+	"strings"
+)
+
+// Post struct contains necessary data for a post
+type Post struct {
+	FileName string
+	Title    string
+	Date     string
+	Tags     []string
+	Image    string
+}
+
+// Posts stuct contains a collection of type Post
+type Posts struct {
+	Contents []*Post
+}
+
+// Read all found files and load them into a stuct
+func (app *application) aggregate(location string) (p *Posts, err error) {
+	var posts []*Post
+
+	files, err := os.ReadDir(location)
+	if err != nil {
+		return nil, err
+	}
+
+	// Loop over every file in the directory and read the contents.
+	for _, file := range files {
+		if !file.IsDir() && strings.HasSuffix(file.Name(), ".tmpl.html") {
+			newPost, err := app.readFile(location + "/" + file.Name())
+			if err != nil {
+				return nil, err
+			}
+
+			posts = append(posts, newPost)
+		}
+	}
+
+	sort.Slice(posts, func(i, j int) bool {
+		return posts[i].Date > posts[j].Date
+	})
+
+	return &Posts{Contents: posts}, nil
+}
+
+func (app *application) readFile(location string) (p *Post, err error) {
+	fileContent, err := os.ReadFile(location)
+	if err != nil {
+		return nil, err
+	}
+
+	var post *Post = new(Post)
+
+	fileName := strings.TrimSuffix(strings.Split(location, "/")[2], ".tmpl.html")
+
+	// title
+	title := strings.ReplaceAll(fileName, "_", " ")
+
+	// date
+	datePattern := regexp.MustCompile(`{{define "uploaded-on"}}(\d{4}-\d{2}-\d{2}){{end}}`)
+	dateMatching := datePattern.FindStringSubmatch(string(fileContent))
+
+	var date string
+	if len(dateMatching) > 1 {
+		date = dateMatching[1]
+	} else {
+		date = ""
+	}
+
+	// tags
+	tagsPattern := regexp.MustCompile(`{{define "keywords"}}([\w\s]+){{end}}`)
+	matchingTags := tagsPattern.FindStringSubmatch(string(fileContent))
+
+	var tags []string
+	if len(matchingTags) > 1 {
+		tags = strings.Fields(matchingTags[1])
+	} else {
+		tags = []string{}
+	}
+
+	// thumbnail image
+	imagePattern := regexp.MustCompile(`<img src="(.+)" class="mainImage"( alt="(.+)")* />`)
+	matchingImage := imagePattern.FindStringSubmatch(string(fileContent))
+
+	var image string
+	if len(matchingImage) > 1 {
+		image = matchingImage[0]
+	} else {
+		image = ""
+	}
+
+	post.FileName = fileName
+	post.Title = title
+	post.Date = date
+	post.Tags = tags
+	post.Image = image
+
+	return post, nil
+
+}
diff --git a/cmd/http/main.go b/cmd/http/main.go
new file mode 100644
index 0000000..828b9d8
--- /dev/null
+++ b/cmd/http/main.go
@@ -0,0 +1,83 @@
+package main
+
+import (
+	"flag"
+	"log"
+	"net/http"
+	"os"
+	"strings"
+)
+
+var (
+	fullchain = "/etc/letsencrypt/live/alexscerba.com/fullchain.pem"
+	privkey   = "/etc/letsencrypt/live/alexscerba.com/privkey.pem"
+)
+
+type application struct {
+	errorLog *log.Logger
+	infoLog  *log.Logger
+}
+
+func (app *application) httpsRedirect(w http.ResponseWriter, req *http.Request) {
+	// remove/add not default ports from req.Host
+	target := "https://" + req.Host + req.URL.Path
+	if len(req.URL.RawQuery) > 0 {
+		target += "?" + req.URL.RawQuery
+	}
+	app.infoLog.Printf("redirect to: %s", target)
+	http.Redirect(w, req, target,
+		// see comments below and consider the codes 308, 302, or 301
+		http.StatusMovedPermanently)
+}
+
+func (app *application) wwwRedirect(h http.Handler) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		if !strings.HasPrefix(r.Host, "www.") {
+			http.Redirect(w, r, "https://www."+r.Host+r.RequestURI, 302)
+			return
+		}
+
+		h.ServeHTTP(w, r)
+	})
+}
+
+func main() {
+	addr := flag.String("addr", ":4002", "HTTP Network Address")
+	flag.Parse() // required before flag is used
+
+	infoLog := log.New(os.Stdout, "INFO\t", log.Ldate|log.Ltime)
+	errorLog := log.New(os.Stderr, "ERROR\t", log.Ldate|log.Ltime|log.Lshortfile)
+
+	app := &application{
+		errorLog: errorLog,
+		infoLog:  infoLog,
+	}
+
+	mux := http.NewServeMux()
+
+	fs := http.FileServer(http.Dir("./static"))
+	mux.Handle("/static/", http.StripPrefix("/static/", fs))
+
+	mux.HandleFunc("/faq", app.faq)
+	mux.HandleFunc("/faq/", app.faq)
+	mux.HandleFunc("/about", app.about)
+	mux.HandleFunc("/about/", app.about)
+	mux.HandleFunc("/gallery", app.gallery)
+	mux.HandleFunc("/gallery/", app.gallery)
+	mux.HandleFunc("/blog", app.blog)
+	mux.HandleFunc("/blog/", app.blog)
+	mux.HandleFunc("/", app.home)
+
+	if *addr == ":443" {
+		www := app.wwwRedirect(mux)
+
+		infoLog.Printf("Starting TLS server on %s...\n", *addr)
+		go http.ListenAndServe(":80", www)
+		err := http.ListenAndServeTLS(*addr, fullchain, privkey, gzipHandler(www))
+		log.Fatal(err)
+	} else {
+		infoLog.Printf("Starting server on %s...\n", *addr)
+		err := http.ListenAndServe(*addr, gzipHandler(mux))
+		log.Fatal(err)
+	}
+}
diff --git a/cmd/http/middle.go b/cmd/http/middle.go
new file mode 100644
index 0000000..29b49a6
--- /dev/null
+++ b/cmd/http/middle.go
@@ -0,0 +1,32 @@
+package main
+
+import (
+	"compress/gzip"
+	"io"
+	"net/http"
+	"strings"
+)
+
+type gzipResponseWriter struct {
+	io.Writer
+	http.ResponseWriter
+}
+
+func (grw gzipResponseWriter) Write(data []byte) (int, error) {
+	return grw.Writer.Write(data)
+}
+
+func gzipHandler(next http.Handler) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
+			next.ServeHTTP(w, r)
+			return
+		}
+
+		w.Header().Set("Content-Encoding", "gzip")
+		gzipWriter := gzip.NewWriter(w)
+		defer gzipWriter.Close()
+		gzippedResponseWriter := gzipResponseWriter{Writer: gzipWriter, ResponseWriter: w}
+		next.ServeHTTP(gzippedResponseWriter, r)
+	})
+}
diff --git a/cmd/http/render.go b/cmd/http/render.go
new file mode 100644
index 0000000..585ba31
--- /dev/null
+++ b/cmd/http/render.go
@@ -0,0 +1,36 @@
+package main
+
+import (
+	"net/http"
+	//"strings"
+	"text/template"
+)
+
+func renderTemplate(w http.ResponseWriter, page string, p *Posts) (err error) {
+	t, err := template.ParseFiles("html/master.tmpl.html", "html/"+page+".tmpl.html")
+	if err != nil {
+		return err
+	}
+
+	//splitPath := strings.Split(page, "/")
+
+	//data := make(map[string]interface{})
+
+	// If were loading the index, set page to 'Index' and pass through all posts.
+	// Otherwise, set page to 'Projects' and pass through the first post (should only be one
+	// coming in)
+	/* if splitPath[0] == "index" {
+		data["Page"] = "Index"
+		data["Posts"] = p
+	} else {
+		data["Page"] = "Project"
+		data["Post"] = p.Contents[0]
+	} */
+
+	err = t.Execute(w, nil)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
-- 
cgit v1.2.3