cblghrobots.txt6b47ba84 years agoHistory
442 lines · 14.0 KB · Go
RawDownload
package server import (	"context"	"errors"	"fmt"	"net/http"	"net/url"	"strconv"	"strings"	"syscall" 	"cerca/crypto"	"cerca/database"	"cerca/server/session"	"cerca/util"	"html/template" 	"github.com/carlmjohnson/requests") /* TODO (2022-01-03): include csrf token via gorilla, or w/e, when rendering */ type TemplateData struct {	Data     interface{}	LoggedIn bool // TODO (2022-01-09): put this in a middleware || template function or sth?	Title    string} type IndexData struct {	Threads []database.Thread} type GenericMessageData struct {	Title       string	Message     string	LinkMessage string	Link        string	LinkText    string} type RegisterData struct {	VerificationCode string	ErrorMessage     string} type RegisterSuccessData struct {	Keypair string} type LoginData struct {	FailedAttempt bool} type ThreadData struct {	Title string	Posts []database.Post} type RequestHandler struct {	db        *database.DB	session   *session.Session	allowlist []string // allowlist of domains valid for forum registration} var developing bool func dump(err error) {	if developing {		fmt.Println(err)	}} // returns true if logged in, and the userid of the logged in user.// returns false (and userid set to -1) if not logged infunc (h RequestHandler) IsLoggedIn(req *http.Request) (bool, int) {	ed := util.Describe("IsLoggedIn")	userid, err := h.session.Get(req)	err = ed.Eout(err, "getting userid from session cookie")	if err != nil {		dump(err)		return false, -1	} 	// make sure the user from the cookie actually exists	userExists, err := h.db.CheckUserExists(userid)	if err != nil {		dump(ed.Eout(err, "check userid in db"))		return false, -1	} else if !userExists {		return false, -1	}	return true, userid} var views = []string{"index", "head", "footer", "login-component", "login", "register", "register-success", "thread", "new-thread", "generic-message", "about"} // wrap the contents of `views` to the format expected by template.ParseFiles()func wrapViews() []string {	for i, item := range views {		views[i] = fmt.Sprintf("html/%s.html", item)	}	return views} var templates = template.Must(template.ParseFiles(wrapViews()...)) func (h RequestHandler) renderView(res http.ResponseWriter, viewName string, data TemplateData) {	if data.Title == "" {		data.Title = strings.ReplaceAll(viewName, "-", " ")	}	errTemp := templates.ExecuteTemplate(res, viewName+".html", data)	if errors.Is(errTemp, syscall.EPIPE) {		fmt.Println("had a broken pipe, continuing")	} else {		util.Check(errTemp, "rendering %s view", viewName)	}} func (h RequestHandler) ThreadRoute(res http.ResponseWriter, req *http.Request) {	ed := util.Describe("thread route")	parts := strings.Split(strings.TrimSpace(req.URL.Path), "/")	// invalid route, redirect to index	if len(parts) < 2 || parts[2] == "" {		IndexRedirect(res, req)		return	}	loggedIn, userid := h.IsLoggedIn(req) 	threadid, err := strconv.Atoi(parts[2])	ed.Check(err, "parse %s as id slug", parts[2]) 	if req.Method == "POST" && loggedIn {		// handle POST (=> add a reply, then show the thread)		content := req.PostFormValue("content")		// TODO (2022-01-09): make sure rendered content won't be empty after sanitizing:		// * run sanitize step && strings.TrimSpace and check length **before** doing AddPost		// TODO(2022-01-09): send errors back to thread's posting view		h.db.AddPost(content, threadid, userid)	}	// after handling a post, treat the request as if it was a get request	// TODO (2022-01-07):	// * handle error	thread := h.db.GetThread(threadid)	// markdownize content (but not title)	for i, post := range thread {		thread[i].Content = util.Markup(post.Content)	}	title := thread[0].ThreadTitle	view := TemplateData{ThreadData{title, thread}, loggedIn, title}	h.renderView(res, "thread", view)} func (h RequestHandler) ErrorRoute(res http.ResponseWriter, req *http.Request, status int) {	title := "Page not found"	data := GenericMessageData{		Title:   title,		Message: fmt.Sprintf("The visited page does not exist (anymore?). Error code %d.", status),	}	h.renderView(res, "generic-message", TemplateData{Data: data, Title: title})} func (h RequestHandler) IndexRoute(res http.ResponseWriter, req *http.Request) {	// handle 404	if req.URL.Path != "/" {		h.ErrorRoute(res, req, http.StatusNotFound)		return	}	loggedIn, _ := h.IsLoggedIn(req)	// show index listing	threads := h.db.ListThreads()	view := TemplateData{IndexData{threads}, loggedIn, "threads"}	h.renderView(res, "index", view)} func IndexRedirect(res http.ResponseWriter, req *http.Request) {	http.Redirect(res, req, "/", http.StatusSeeOther)} func (h RequestHandler) LogoutRoute(res http.ResponseWriter, req *http.Request) {	loggedIn, _ := h.IsLoggedIn(req)	if loggedIn {		h.session.Delete(res, req)	}	IndexRedirect(res, req)} func (h RequestHandler) LoginRoute(res http.ResponseWriter, req *http.Request) {	ed := util.Describe("LoginRoute")	loggedIn, _ := h.IsLoggedIn(req)	switch req.Method {	case "GET":		h.renderView(res, "login", TemplateData{LoginData{}, loggedIn, ""})	case "POST":		username := req.PostFormValue("username")		password := req.PostFormValue("password")		// * hash received password and compare to stored hash		passwordHash, userid, err := h.db.GetPasswordHash(username)		// make sure user exists		if err = ed.Eout(err, "getting password hash and uid"); err != nil {			fmt.Println(err)			h.renderView(res, "login", TemplateData{LoginData{FailedAttempt: true}, loggedIn, ""})			IndexRedirect(res, req)			return		}		if !crypto.ValidatePasswordHash(password, passwordHash) {			fmt.Println("incorrect password!")			h.renderView(res, "login", TemplateData{LoginData{FailedAttempt: true}, loggedIn, ""})			return		}		// save user id in cookie		err = h.session.Save(req, res, userid)		ed.Check(err, "saving session cookie")		IndexRedirect(res, req)	default:		fmt.Println("non get/post method, redirecting to index")		IndexRedirect(res, req)	}} // downloads the content at the verification link and compares it to the verification code. returns true if the verification link content contains the verification code somewherefunc hasVerificationCode(link, verification string) bool {	var linkBody string	err := requests.		URL(link).		ToString(&linkBody).		Fetch(context.Background())	if err != nil {		fmt.Println(util.Eout(err, "HasVerificationCode"))		return false	} 	return strings.Contains(strings.TrimSpace(linkBody), strings.TrimSpace(verification))} func (h RequestHandler) RegisterRoute(res http.ResponseWriter, req *http.Request) {	ed := util.Describe("register route")	loggedIn, _ := h.IsLoggedIn(req)	errMessage := ""	if loggedIn {		data := GenericMessageData{			Title:       "Register",			Message:     "You already have an account (you are logged in with it).",			Link:        "/",			LinkMessage: "Visit the",			LinkText:    "index",		}		h.renderView(res, "generic-message", TemplateData{data, loggedIn, "register"})		return	} 	renderErr := func(verificationCode, errMessage string) {		fmt.Println(errMessage)		h.renderView(res, "register", TemplateData{Data: RegisterData{verificationCode, errMessage}})	}	switch req.Method {	case "GET":		// try to get the verification code from the session (useful in case someone refreshed the page)		verificationCode, err := h.session.GetVerificationCode(req)		// we had an error getting the verification code, generate a code and set it on the session		if err != nil {			verificationCode = fmt.Sprintf("MRV%06d\n", crypto.GenerateVerificationCode())			err = h.session.SaveVerificationCode(req, res, verificationCode)			if err != nil {				errMessage = "Had troubles setting the verification code on session"				renderErr(verificationCode, errMessage)				return			}		}		h.renderView(res, "register", TemplateData{Data: RegisterData{verificationCode, ""}})	case "POST":		verificationCode, err := h.session.GetVerificationCode(req)		if err != nil {			errMessage = "There was no verification record for this browser session; missing data to compare against verification link content"			renderErr(verificationCode, errMessage)			return		}		username := req.PostFormValue("username")		password := req.PostFormValue("password")		// read verification code from form		verificationLink := req.PostFormValue("verificationlink")		// fmt.Printf("user: %s, verilink: %s\n", username, verificationLink)		u, err := url.Parse(verificationLink)		if err != nil {			errMessage = "Had troubles parsing the verification link, are you sure it was a proper url?"			renderErr(verificationCode, errMessage)			return		}		// check verification link domain against allowlist		if !util.Contains(h.allowlist, u.Host) {			fmt.Println(h.allowlist, u.Host, util.Contains(h.allowlist, u.Host))			errMessage = fmt.Sprintf("Verification link's host (%s) is not in the allowlist", u.Host)			renderErr(verificationCode, errMessage)			return		} 		// parse out verification code from verification link and compare against verification code in session		has := hasVerificationCode(verificationLink, verificationCode)		if !has {			errMessage = fmt.Sprintf("Verification code from link (%s) does not match", verificationLink)			renderErr(verificationCode, errMessage)			return		}		// make sure username is not registered already		exists, err := h.db.CheckUsernameExists(username)		if err != nil {			errMessage = "Database had a problem when checking username"			renderErr(verificationCode, errMessage)			return		}		if exists {			errMessage = fmt.Sprintf("Username %s appears to already exist, please pick another name", username)			renderErr(verificationCode, errMessage)			return		}		hash, err := crypto.HashPassword(password)		if err != nil {			fmt.Println(ed.Eout(err, "hash password"))			errMessage = "Database had a problem when hashing password"			renderErr(verificationCode, errMessage)			return		}		userid, err := h.db.CreateUser(username, hash)		if err != nil {			errMessage = "Error in db when creating user"			renderErr(verificationCode, errMessage)			return		}		// log the new user in		h.session.Save(req, res, userid)		// log where the registration is coming from, in the case of indirect invites && for curiosity		err = h.db.AddRegistration(userid, verificationLink)		if err = ed.Eout(err, "add registration"); err != nil {			dump(err)		}		// generate and pass public keypair		keypair, err := crypto.GenerateKeypair()		// record generated pubkey in database for eventual later use		err = h.db.AddPubkey(userid, keypair.Public)		if err = ed.Eout(err, "insert pubkey in db"); err != nil {			dump(err)		}		ed.Check(err, "generate keypair")		kpJson, err := keypair.Marshal()		ed.Check(err, "marshal keypair")		h.renderView(res, "register-success", TemplateData{RegisterSuccessData{string(kpJson)}, loggedIn, "registered successfully"})	default:		fmt.Println("non get/post method, redirecting to index")		IndexRedirect(res, req)	}} func (h RequestHandler) GenericRoute(res http.ResponseWriter, req *http.Request) {	data := GenericMessageData{		Title:       "GenericTitle",		Message:     "Generic message",		Link:        "/",		LinkMessage: "Generic link messsage",		LinkText:    "with link",	}	h.renderView(res, "generic-message", TemplateData{Data: data})} func (h RequestHandler) AboutRoute(res http.ResponseWriter, req *http.Request) {	loggedIn, _ := h.IsLoggedIn(req)	h.renderView(res, "about", TemplateData{LoggedIn: loggedIn})} func (h RequestHandler) RobotsRoute(res http.ResponseWriter, req *http.Request) {  fmt.Fprintln(res, "User-agent: *\nDisallow: /")} func (h RequestHandler) NewThreadRoute(res http.ResponseWriter, req *http.Request) {	loggedIn, userid := h.IsLoggedIn(req)	switch req.Method {	// Handle GET (=> want to start a new thread)	case "GET":		if !loggedIn {			title := "Not logged in"			data := GenericMessageData{				Title:       title,				Message:     "Only members of this forum may create new threads",				Link:        "/login",				LinkMessage: "If you are a member,",				LinkText:    "log in",			}			h.renderView(res, "generic-message", TemplateData{Data: data, Title: title})			return		}		h.renderView(res, "new-thread", TemplateData{LoggedIn: loggedIn, Title: "new thread"})	case "POST":		// Handle POST (=>		title := req.PostFormValue("title")		content := req.PostFormValue("content")		// TODO (2022-01-10): unstub topicid, once we have other topics :)		// the new thread was created: forward info to database		threadid, err := h.db.CreateThread(title, content, userid, 1)		if err != nil {			data := GenericMessageData{				Title:   "Error creating thread",				Message: "There was a database error when creating the thread, apologies.",			}			h.renderView(res, "generic-message", TemplateData{Data: data, Title: "new thread"})			return		}		// when data has been stored => redirect to thread		slug := fmt.Sprintf("thread/%d/%s/", threadid, util.SanitizeURL(title))		http.Redirect(res, req, "/"+slug, http.StatusSeeOther)	default:		fmt.Println("non get/post method, redirecting to index")		IndexRedirect(res, req)	}} func Serve(allowlist []string, sessionKey string, isdev bool) {	port := ":8272"	dbpath := "./data/forum.db"	if isdev {		developing = true		dbpath = "./data/forum.test.db"		port = ":8277"	} 	db := database.InitDB(dbpath)	handler := RequestHandler{&db, session.New(sessionKey, developing), allowlist}	/* note: be careful with trailing slashes; go's default handler is a bit sensitive */	// TODO (2022-01-10): introduce middleware to make sure there is never an issue with trailing slashes	http.HandleFunc("/about", handler.AboutRoute)	http.HandleFunc("/logout", handler.LogoutRoute)	http.HandleFunc("/login", handler.LoginRoute)	http.HandleFunc("/register", handler.RegisterRoute)	http.HandleFunc("/thread/new/", handler.NewThreadRoute)	http.HandleFunc("/thread/", handler.ThreadRoute)	http.HandleFunc("/robots.txt", handler.RobotsRoute)	http.HandleFunc("/", handler.IndexRoute) 	fileserver := http.FileServer(http.Dir("html/assets/"))	http.Handle("/assets/", http.StripPrefix("/assets/", fileserver)) 	fmt.Println("Serving forum on", port)	http.ListenAndServe(port, nil)}

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t