flounder

A simple gemini site builder
git clone git://git.alexwennerberg.com/flounder
Log | Files | Refs | README | LICENSE

http.go (24246B)


      1 package main
      2 
      3 import (
      4 	"bytes"
      5 	"fmt"
      6 	"html/template"
      7 	"io"
      8 	"io/ioutil"
      9 	"log"
     10 	"net"
     11 	"net/http"
     12 	"net/url"
     13 	"os"
     14 	"path"
     15 	"strings"
     16 	"time"
     17 
     18 	gmi "git.sr.ht/~adnano/go-gemini"
     19 	feeds "git.sr.ht/~aw/gorilla-feeds"
     20 	"github.com/gorilla/handlers"
     21 	"github.com/gorilla/sessions"
     22 	_ "github.com/mattn/go-sqlite3"
     23 	"golang.org/x/crypto/bcrypt"
     24 )
     25 
     26 var t *template.Template
     27 var SessionStore *sessions.CookieStore
     28 
     29 func renderDefaultError(w http.ResponseWriter, statusCode int) {
     30 	errorMsg := http.StatusText(statusCode)
     31 	renderError(w, errorMsg, statusCode)
     32 }
     33 
     34 func serverError(w http.ResponseWriter, err error) {
     35 	log.Print(err)
     36 	renderDefaultError(w, 500)
     37 }
     38 
     39 func renderError(w http.ResponseWriter, errorMsg string, statusCode int) {
     40 	data := struct {
     41 		StatusCode int
     42 		ErrorMsg   string
     43 		Config     Config
     44 	}{statusCode, errorMsg, c}
     45 	w.WriteHeader(statusCode)
     46 	err := t.ExecuteTemplate(w, "error.html", data)
     47 	if err != nil { // Shouldn't happen probably
     48 		http.Error(w, errorMsg, statusCode)
     49 	}
     50 }
     51 
     52 func rootHandler(w http.ResponseWriter, r *http.Request) {
     53 	// serve everything inside static directory
     54 	if r.URL.Path != "/" {
     55 		fileName := path.Join(c.TemplatesDirectory, "static", cleanPath(r.URL.Path))
     56 		_, err := os.Stat(fileName)
     57 		if err != nil {
     58 			renderDefaultError(w, http.StatusNotFound)
     59 			return
     60 		}
     61 		http.ServeFile(w, r, fileName) // TODO better error handling
     62 		return
     63 	}
     64 
     65 	user := getAuthUser(r)
     66 	indexFiles, err := getUpdatedFiles(user.IsAdmin, "")
     67 	if err != nil {
     68 		serverError(w, err)
     69 		return
     70 	}
     71 	allUsers, err := getActiveUserNames()
     72 	if err != nil {
     73 		serverError(w, err)
     74 		return
     75 	}
     76 	data := struct {
     77 		Config   Config
     78 		AuthUser AuthUser
     79 		Files    []*File
     80 		Users    []string
     81 	}{c, user, indexFiles, allUsers}
     82 	err = t.ExecuteTemplate(w, "index.html", data)
     83 	if err != nil {
     84 		serverError(w, err)
     85 		return
     86 	}
     87 }
     88 
     89 func updatesHandler(w http.ResponseWriter, r *http.Request) {
     90 	// user
     91 	authUser := getAuthUser(r)
     92 	var username string
     93 	if strings.HasSuffix(r.URL.Path, "atom.xml") {
     94 		username = cleanPath(r.URL.Path[len("/updates/") : len(r.URL.Path)-len("atom.xml")])[1:]
     95 		w.Header().Set("Content-Type", "application/atom+xml")
     96 		// build atom feed
     97 		files, err := getUpdatedFiles(authUser.IsAdmin, username)
     98 		if err != nil {
     99 			serverError(w, err)
    100 			return
    101 		}
    102 		baseURL := "//" + username + "." + c.Host
    103 		feed := feeds.Feed{
    104 			Title:  username + "'s updated files",
    105 			Author: &feeds.Author{Name: username},
    106 			Link:   &feeds.Link{Href: baseURL},
    107 		}
    108 		feed.Items = []*feeds.Item{}
    109 		for _, file := range files {
    110 			feed.Items = append(feed.Items, &feeds.Item{
    111 				Title:   file.Name,
    112 				Link:    &feeds.Link{Href: baseURL + "/" + file.Name},
    113 				Created: file.UpdatedTime, // actually updated. kinda funky
    114 			})
    115 		}
    116 		res, err := feed.ToAtom()
    117 		if err != nil {
    118 			serverError(w, err)
    119 			return
    120 		}
    121 		io.Copy(w, strings.NewReader(res))
    122 		return
    123 	} else {
    124 		username = cleanPath(r.URL.Path[len("/updates/"):])[1:]
    125 	}
    126 	files, err := getUpdatedFiles(authUser.IsAdmin, username)
    127 	if err != nil {
    128 		serverError(w, err)
    129 		return
    130 	}
    131 	data := struct {
    132 		Config   Config
    133 		AuthUser AuthUser
    134 		User     string
    135 		Files    []*File
    136 	}{c, authUser, username, files}
    137 	err = t.ExecuteTemplate(w, "updates.html", data)
    138 	if err != nil {
    139 		serverError(w, err)
    140 		return
    141 	}
    142 }
    143 
    144 func editFileHandler(w http.ResponseWriter, r *http.Request) {
    145 	user := getAuthUser(r)
    146 	if !user.LoggedIn {
    147 		renderDefaultError(w, http.StatusForbidden)
    148 		return
    149 	}
    150 	fileName := cleanPath(r.URL.Path[len("/edit/"):])
    151 	filePath := path.Join(c.FilesDirectory, user.Username, fileName)
    152 	isText := isTextFile(filePath)
    153 	alert := ""
    154 	var warnings []string
    155 	if r.Method == "POST" {
    156 		// get post body
    157 		alert = "saved"
    158 		r.ParseForm()
    159 		fileText := r.Form.Get("file_text")
    160 		// Web form by default gives us CR LF newlines.
    161 		// Unix files use just LF
    162 		fileText = strings.ReplaceAll(fileText, "\r\n", "\n")
    163 		fileBytes := []byte(fileText)
    164 		fileBytes = bytes.Trim(fileBytes, "\xef\xbb\xbf") // Remove BOM
    165 		err := checkIfValidFile(user.Username, filePath, fileBytes)
    166 		if err != nil {
    167 			log.Println(err)
    168 			renderError(w, err.Error(), http.StatusBadRequest)
    169 			return
    170 		}
    171 		sfl := getSchemedFlounderLinkLines(strings.NewReader(fileText))
    172 		if len(sfl) > 0 {
    173 			warnings = append(warnings, "Warning! Some of your links to pages use schemas. This means that they may break when viewed in Gemini or over HTTPS. Plase remove gemini: or https: from the start of these links:\n")
    174 			for _, l := range sfl {
    175 				warnings = append(warnings, l)
    176 			}
    177 		}
    178 		// create directories if dne
    179 		os.MkdirAll(path.Dir(filePath), os.ModePerm)
    180 		newName := cleanPath(r.Form.Get("rename"))
    181 		err = checkIfValidFile(user.Username, newName, fileBytes)
    182 		if err != nil {
    183 			log.Println(err)
    184 			renderError(w, err.Error(), http.StatusBadRequest)
    185 			return
    186 		}
    187 		if isText { // Cant edit binary files here
    188 			err = ioutil.WriteFile(filePath, fileBytes, 0644)
    189 			if err != nil {
    190 				log.Println(err)
    191 				renderError(w, err.Error(), http.StatusBadRequest)
    192 			}
    193 		}
    194 		if newName != fileName {
    195 			newPath := path.Join(c.FilesDirectory, user.Username, newName)
    196 			os.MkdirAll(path.Dir(newPath), os.ModePerm)
    197 			os.Rename(filePath, newPath)
    198 			fileName = newName
    199 			filePath = newPath
    200 			alert += " and renamed"
    201 		}
    202 	}
    203 
    204 	err := checkIfValidFile(user.Username, filePath, nil)
    205 	if err != nil {
    206 		log.Println(err)
    207 		renderError(w, err.Error(), http.StatusBadRequest)
    208 		return
    209 	}
    210 	// Create directories if dne
    211 	f, err := os.OpenFile(filePath, os.O_RDONLY, 0644)
    212 	var fileBytes []byte
    213 	if os.IsNotExist(err) || !isText {
    214 		fileBytes = []byte{}
    215 		err = nil
    216 	} else {
    217 		defer f.Close()
    218 		fileBytes, err = ioutil.ReadAll(f)
    219 	}
    220 	if err != nil {
    221 		serverError(w, err)
    222 		return
    223 	}
    224 	data := struct {
    225 		FileName string
    226 		FileText string
    227 		Config   Config
    228 		AuthUser AuthUser
    229 		Host     string
    230 		IsText   bool
    231 		IsGemini bool
    232 		IsGemlog bool
    233 		Alert    string
    234 		Warnings []string
    235 	}{fileName[1:], // remove starting slash
    236 		string(fileBytes), c, user, c.Host, isText, isGemini(fileName), strings.HasPrefix(fileName, "gemlog"), alert, warnings}
    237 	err = t.ExecuteTemplate(w, "edit_file.html", data)
    238 	if err != nil {
    239 		serverError(w, err)
    240 		return
    241 	}
    242 }
    243 
    244 func uploadFilesHandler(w http.ResponseWriter, r *http.Request) {
    245 	if r.Method == "POST" {
    246 		user := getAuthUser(r)
    247 		if !user.LoggedIn {
    248 			renderDefaultError(w, http.StatusForbidden)
    249 			return
    250 		}
    251 		r.ParseMultipartForm(10 << 6) // why does this not work
    252 		file, fileHeader, err := r.FormFile("file")
    253 		if err != nil {
    254 			log.Println(err)
    255 			renderError(w, "No file selected. Please go back and select a file.", http.StatusBadRequest)
    256 			return
    257 		}
    258 		fileName := cleanPath(fileHeader.Filename)
    259 		defer file.Close()
    260 		dest, _ := ioutil.ReadAll(file)
    261 		err = checkIfValidFile(user.Username, fileName, dest)
    262 		if err != nil {
    263 			log.Println(err)
    264 			renderError(w, err.Error(), http.StatusBadRequest)
    265 			return
    266 		}
    267 		destPath := path.Join(c.FilesDirectory, user.Username, fileName)
    268 
    269 		f, err := os.OpenFile(destPath, os.O_WRONLY|os.O_CREATE, 0644)
    270 		if err != nil {
    271 			serverError(w, err)
    272 			return
    273 		}
    274 		defer f.Close()
    275 		io.Copy(f, bytes.NewReader(dest))
    276 	}
    277 	http.Redirect(w, r, "/my_site", http.StatusSeeOther)
    278 }
    279 
    280 type AuthUser struct {
    281 	LoggedIn          bool
    282 	Username          string
    283 	IsAdmin           bool
    284 	ImpersonatingUser string // used if impersonating
    285 }
    286 
    287 func getAuthUser(r *http.Request) AuthUser {
    288 	session, _ := SessionStore.Get(r, "cookie-session")
    289 	user, ok := session.Values["auth_user"].(string)
    290 	impers, _ := session.Values["impersonating_user"].(string)
    291 	isAdmin, _ := session.Values["admin"].(bool)
    292 	return AuthUser{
    293 		LoggedIn:          ok,
    294 		Username:          user,
    295 		IsAdmin:           isAdmin,
    296 		ImpersonatingUser: impers,
    297 	}
    298 }
    299 
    300 func mySiteHandler(w http.ResponseWriter, r *http.Request) {
    301 	user := getAuthUser(r)
    302 	if !user.LoggedIn {
    303 		renderDefaultError(w, http.StatusForbidden)
    304 		return
    305 	}
    306 	// check auth
    307 	userFolder := getUserDirectory(user.Username)
    308 	files, _ := getMyFilesRecursive(userFolder, user.Username)
    309 	currentDate := time.Now().Format("2006-01-02")
    310 	data := struct {
    311 		Config      Config
    312 		Files       []File
    313 		AuthUser    AuthUser
    314 		CurrentDate string
    315 	}{c, files, user, currentDate}
    316 	_ = t.ExecuteTemplate(w, "my_site.html", data)
    317 }
    318 
    319 func myAccountHandler(w http.ResponseWriter, r *http.Request) {
    320 	user := getAuthUser(r)
    321 	authUser := user.Username
    322 	if !user.LoggedIn {
    323 		renderDefaultError(w, http.StatusForbidden)
    324 		return
    325 	}
    326 	me, _ := getUserByName(user.Username)
    327 	type pageData struct {
    328 		Config   Config
    329 		AuthUser AuthUser
    330 		MyUser   *User
    331 		Errors   []string
    332 	}
    333 	data := pageData{c, user, me, nil}
    334 
    335 	if r.Method == "GET" {
    336 		err := t.ExecuteTemplate(w, "me.html", data)
    337 		if err != nil {
    338 			serverError(w, err)
    339 			return
    340 		}
    341 	} else if r.Method == "POST" {
    342 		r.ParseForm()
    343 		newUsername := r.Form.Get("username")
    344 		errors := []string{}
    345 		newEmail := r.Form.Get("email")
    346 		newDomain := r.Form.Get("domain")
    347 		newUsername = strings.ToLower(newUsername)
    348 		var err error
    349 		_, exists := domains[newDomain]
    350 		if newDomain != me.Domain && !exists {
    351 			_, err = DB.Exec("update user set domain = ? where username = ?", newDomain, me.Username) // TODO use transaction
    352 			if err != nil {
    353 				errors = append(errors, err.Error())
    354 			} else {
    355 				certificates.Register(newDomain)
    356 				refreshDomainMap()
    357 				log.Printf("Changed domain for %s from %s to %s", authUser, me.Domain, newDomain)
    358 			}
    359 		}
    360 		if newEmail != me.Email {
    361 			_, err = DB.Exec("update user set email = ? where username = ?", newEmail, me.Username)
    362 			if err != nil {
    363 				// TODO better error not sql
    364 				errors = append(errors, err.Error())
    365 			} else {
    366 				log.Printf("Changed email for %s from %s to %s", authUser, me.Email, newEmail)
    367 			}
    368 		}
    369 		if newUsername != authUser {
    370 			// Rename User
    371 			err = renameUser(authUser, newUsername)
    372 			if err != nil {
    373 				log.Println(err)
    374 				errors = append(errors, "Could not rename user")
    375 			} else {
    376 				session, _ := SessionStore.Get(r, "cookie-session")
    377 				session.Values["auth_user"] = newUsername
    378 				session.Save(r, w)
    379 			}
    380 		}
    381 		// reset auth
    382 		user = getAuthUser(r)
    383 		data.Errors = errors
    384 		data.AuthUser = user
    385 		data.MyUser.Email = newEmail
    386 		data.MyUser.Domain = newDomain
    387 		_ = t.ExecuteTemplate(w, "me.html", data)
    388 	}
    389 }
    390 
    391 func archiveHandler(w http.ResponseWriter, r *http.Request) {
    392 	authUser := getAuthUser(r)
    393 	if !authUser.LoggedIn {
    394 		renderDefaultError(w, http.StatusForbidden)
    395 		return
    396 	}
    397 	if r.Method == "GET" {
    398 		userFolder := getUserDirectory(authUser.Username)
    399 		err := zipit(userFolder, w)
    400 		if err != nil {
    401 			serverError(w, err)
    402 			return
    403 		}
    404 
    405 	}
    406 }
    407 func loginHandler(w http.ResponseWriter, r *http.Request) {
    408 	if r.Method == "GET" {
    409 		// show page
    410 		data := struct {
    411 			Error  string
    412 			Config Config
    413 		}{"", c}
    414 		err := t.ExecuteTemplate(w, "login.html", data)
    415 		if err != nil {
    416 			serverError(w, err)
    417 			return
    418 		}
    419 	} else if r.Method == "POST" {
    420 		r.ParseForm()
    421 		name := strings.ToLower(r.Form.Get("username"))
    422 		password := r.Form.Get("password")
    423 		username, isAdmin, err := checkLogin(name, password)
    424 		if err == nil {
    425 			log.Println("logged in")
    426 			session, _ := SessionStore.Get(r, "cookie-session")
    427 			session.Values["auth_user"] = username
    428 			session.Values["admin"] = isAdmin
    429 			session.Save(r, w)
    430 			http.Redirect(w, r, "/my_site", http.StatusSeeOther)
    431 			return
    432 		} else {
    433 			data := struct {
    434 				Error  string
    435 				Config Config
    436 			}{err.Error(), c}
    437 			w.WriteHeader(401)
    438 			err := t.ExecuteTemplate(w, "login.html", data)
    439 			if err != nil {
    440 				serverError(w, err)
    441 				return
    442 			}
    443 		}
    444 	}
    445 }
    446 
    447 func logoutHandler(w http.ResponseWriter, r *http.Request) {
    448 	session, _ := SessionStore.Get(r, "cookie-session")
    449 	impers, ok := session.Values["impersonating_user"].(string)
    450 	if ok {
    451 		session.Values["auth_user"] = impers
    452 		session.Values["impersonating_user"] = nil // TODO expire this automatically
    453 		// session.Values["admin"] = nil // TODO fix admin
    454 	} else {
    455 		session.Options.MaxAge = -1
    456 	}
    457 	session.Save(r, w)
    458 	http.Redirect(w, r, "/", http.StatusSeeOther)
    459 }
    460 
    461 const ok = "-0123456789abcdefghijklmnopqrstuvwxyz"
    462 
    463 // TODO improve this
    464 var bannedUsernames = []string{"www", "proxy", "mtg", "lists", "grafana"}
    465 
    466 func registerHandler(w http.ResponseWriter, r *http.Request) {
    467 	if r.Method == "GET" {
    468 		data := struct {
    469 			Errors []string
    470 			Config Config
    471 		}{nil, c}
    472 		err := t.ExecuteTemplate(w, "register.html", data)
    473 		if err != nil {
    474 			serverError(w, err)
    475 			return
    476 		}
    477 	} else if r.Method == "POST" {
    478 		r.ParseForm()
    479 		email := strings.ToLower(r.Form.Get("email"))
    480 		password := r.Form.Get("password")
    481 		errors := []string{}
    482 		if r.Form.Get("password") != r.Form.Get("password2") {
    483 			errors = append(errors, "Passwords don't match")
    484 		}
    485 		if len(password) < 6 {
    486 			errors = append(errors, "Password is too short")
    487 		}
    488 		username := strings.ToLower(r.Form.Get("username"))
    489 		err := isOkUsername(username)
    490 		if err != nil {
    491 			errors = append(errors, err.Error())
    492 		}
    493 		_, err = os.Stat(getUserDirectory(username))
    494 		if !os.IsNotExist(err) {
    495 			// Don't allow user to create account if folder dne
    496 			errors = append(errors, "Invalid username")
    497 		}
    498 		hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 8) // TODO handle error
    499 		if err != nil {
    500 			serverError(w, err)
    501 			return
    502 		}
    503 		reference := r.Form.Get("reference")
    504 		if len(errors) == 0 {
    505 			_, err = DB.Exec("insert into user (username, email, password_hash, reference) values ($1, $2, $3, $4)", username, email, string(hashedPassword), reference)
    506 			if err != nil {
    507 				errors = append(errors, "Username or email is already used")
    508 			}
    509 		}
    510 		if len(errors) > 0 {
    511 			data := struct {
    512 				Config Config
    513 				Errors []string
    514 			}{c, errors}
    515 			w.WriteHeader(400)
    516 			t.ExecuteTemplate(w, "register.html", data)
    517 		} else {
    518 			data := struct {
    519 				Config  Config
    520 				Message string
    521 				Title   string
    522 			}{c, "Registration complete! The server admin will approve your request before you can log in.", "Registration Complete"}
    523 			t.ExecuteTemplate(w, "message.html", data)
    524 		}
    525 	}
    526 }
    527 
    528 func deleteFileHandler(w http.ResponseWriter, r *http.Request) {
    529 	user := getAuthUser(r)
    530 	if !user.LoggedIn {
    531 		renderDefaultError(w, http.StatusForbidden)
    532 		return
    533 	}
    534 	filePath := safeGetFilePath(user.Username, r.URL.Path[len("/delete/"):])
    535 	if r.Method == "POST" {
    536 		os.Remove(filePath) // TODO handle error
    537 	}
    538 	http.Redirect(w, r, "/my_site", http.StatusSeeOther)
    539 }
    540 
    541 func adminHandler(w http.ResponseWriter, r *http.Request) {
    542 	user := getAuthUser(r)
    543 	if !user.IsAdmin {
    544 		renderDefaultError(w, http.StatusForbidden)
    545 		return
    546 	}
    547 	allUsers, err := getUsers()
    548 	if err != nil {
    549 		log.Println(err)
    550 		renderDefaultError(w, http.StatusInternalServerError)
    551 		return
    552 	}
    553 	data := struct {
    554 		Users    []User
    555 		AuthUser AuthUser
    556 		Config   Config
    557 	}{allUsers, user, c}
    558 	err = t.ExecuteTemplate(w, "admin.html", data)
    559 	if err != nil {
    560 		serverError(w, err)
    561 		return
    562 	}
    563 }
    564 
    565 // Server a user's file
    566 // TODO replace with gemini proxy
    567 // Here be dragons
    568 func userFile(w http.ResponseWriter, r *http.Request) {
    569 	var userName string
    570 	custom := domains[r.Host]
    571 	if custom != "" {
    572 		userName = custom
    573 	} else {
    574 		userName = cleanPath(strings.Split(r.Host, ".")[0])[1:] // Clean probably unnecessary
    575 	}
    576 	unescaped, err := url.QueryUnescape(r.URL.Path)
    577 	if err != nil {
    578 		serverError(w, err)
    579 		return
    580 	}
    581 	p := cleanPath(unescaped)
    582 	fmt.Println(p)
    583 	var isDir bool
    584 	fullPath := path.Join(c.FilesDirectory, userName, p) // TODO rename filepath
    585 	stat, err := os.Stat(fullPath)
    586 	if stat != nil {
    587 		isDir = stat.IsDir()
    588 	}
    589 	if strings.HasSuffix(p, "index.gmi") {
    590 		http.Redirect(w, r, path.Dir(p), http.StatusMovedPermanently)
    591 		return
    592 	}
    593 
    594 	if strings.HasPrefix(p, "/"+HiddenFolder) {
    595 		renderDefaultError(w, http.StatusForbidden)
    596 		return
    597 	}
    598 	if r.URL.Path == "/gemlog/atom.xml" && os.IsNotExist(err) {
    599 		w.Header().Set("Content-Type", "application/atom+xml")
    600 		// TODO set always somehow
    601 		feed := generateFeedFromUser(userName)
    602 		atomString := feed.toAtomFeed()
    603 		io.Copy(w, strings.NewReader(atomString))
    604 		return
    605 	}
    606 
    607 	var geminiContent string
    608 	fullStat, err := os.Stat(path.Join(fullPath, "index.gmi"))
    609 	if isDir {
    610 		// redirect slash
    611 		if !strings.HasSuffix(r.URL.Path, "/") {
    612 			http.Redirect(w, r, p+"/", http.StatusSeeOther)
    613 		}
    614 		if os.IsNotExist(err) {
    615 			if p == "/gemlog" {
    616 				geminiContent = generateGemfeedPage(userName)
    617 			} else {
    618 				geminiContent = generateFolderPage(fullPath)
    619 			}
    620 		} else {
    621 			fullPath = path.Join(fullPath, "index.gmi")
    622 		}
    623 		if fullStat != nil {
    624 			stat = fullStat // wonky
    625 		}
    626 	}
    627 	if geminiContent == "" && os.IsNotExist(err) {
    628 		renderDefaultError(w, http.StatusNotFound)
    629 		return
    630 	}
    631 	// Dumb content negotiation
    632 	_, raw := r.URL.Query()["raw"]
    633 	acceptsGemini := strings.Contains(r.Header.Get("Accept"), "text/gemini")
    634 	if !raw && !acceptsGemini && (isGemini(fullPath) || geminiContent != "") {
    635 		var htmlDoc ConvertedGmiDoc
    636 		if geminiContent == "" {
    637 			file, _ := os.Open(fullPath)
    638 			parse, _ := gmi.ParseText(file)
    639 			htmlDoc = textToHTML(nil, parse)
    640 			defer file.Close()
    641 		} else {
    642 			parse, _ := gmi.ParseText(strings.NewReader(geminiContent))
    643 			htmlDoc = textToHTML(nil, parse)
    644 		}
    645 		hostname := strings.Split(r.Host, ":")[0]
    646 		uri := url.URL{
    647 			Scheme: "gemini",
    648 			Host:   hostname,
    649 			Path:   p,
    650 		}
    651 		if htmlDoc.Title == "" {
    652 			htmlDoc.Title = userName + p
    653 		}
    654 		data := struct {
    655 			SiteBody  template.HTML
    656 			PageTitle string
    657 			URI       *url.URL
    658 			GeminiURI *url.URL
    659 			Config    Config
    660 		}{template.HTML(htmlDoc.Content), htmlDoc.Title, &uri, &uri, c}
    661 		buff := bytes.NewBuffer([]byte{})
    662 		err = t.ExecuteTemplate(buff, "user_page.html", data)
    663 		if err != nil {
    664 			serverError(w, err)
    665 			return
    666 		}
    667 		breader := bytes.NewReader(buff.Bytes())
    668 		http.ServeContent(w, r, "", stat.ModTime(), breader)
    669 	} else {
    670 		http.ServeFile(w, r, fullPath)
    671 	}
    672 }
    673 
    674 func deleteAccountHandler(w http.ResponseWriter, r *http.Request) {
    675 	user := getAuthUser(r)
    676 	if r.Method == "POST" {
    677 		r.ParseForm()
    678 		validate := r.Form.Get("validate-delete")
    679 		if validate == user.Username {
    680 			err := deleteUser(user.Username)
    681 			if err != nil {
    682 				log.Println(err)
    683 				renderDefaultError(w, http.StatusInternalServerError)
    684 				return
    685 			}
    686 			logoutHandler(w, r)
    687 		} else {
    688 			http.Redirect(w, r, "/me", http.StatusSeeOther)
    689 		}
    690 	}
    691 }
    692 
    693 func resetPasswordHandler(w http.ResponseWriter, r *http.Request) {
    694 	user := getAuthUser(r)
    695 	data := struct {
    696 		Config   Config
    697 		AuthUser AuthUser
    698 		Error    string
    699 	}{c, user, ""}
    700 	if r.Method == "GET" {
    701 		err := t.ExecuteTemplate(w, "reset_pass.html", data)
    702 		if err != nil {
    703 			serverError(w, err)
    704 			return
    705 		}
    706 	} else if r.Method == "POST" {
    707 		r.ParseForm()
    708 		enteredCurrPass := r.Form.Get("password")
    709 		password1 := r.Form.Get("new_password1")
    710 		password2 := r.Form.Get("new_password2")
    711 		if password1 != password2 {
    712 			data.Error = "New passwords do not match"
    713 		} else if len(password1) < 6 {
    714 			data.Error = "Password is too short"
    715 		} else {
    716 			err := checkAuth(user.Username, enteredCurrPass)
    717 			if err == nil {
    718 				hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password1), 8)
    719 				if err != nil {
    720 					serverError(w, err)
    721 					return
    722 				}
    723 				_, err = DB.Exec("update user set password_hash = ? where username = ?", hashedPassword, user.Username)
    724 				if err != nil {
    725 					serverError(w, err)
    726 					return
    727 				}
    728 				log.Printf("User %s reset password", user.Username)
    729 				http.Redirect(w, r, "/me", http.StatusSeeOther)
    730 				return
    731 			} else {
    732 				data.Error = "That's not your current password"
    733 			}
    734 		}
    735 		err := t.ExecuteTemplate(w, "reset_pass.html", data)
    736 		if err != nil {
    737 			serverError(w, err)
    738 			return
    739 		}
    740 	}
    741 }
    742 
    743 // unused atm
    744 func proxyFinger(w http.ResponseWriter, r *http.Request) {
    745 	// Create connection
    746 	var output string
    747 	var errStr string
    748 	if r.Method == "POST" {
    749 		r.ParseForm()
    750 		query := r.Form.Get("query")
    751 		args := strings.Split(query, "@")
    752 		if len(args) != 2 {
    753 			errStr = "Not enough args"
    754 			goto render
    755 		}
    756 		// get data
    757 		conn, err := net.DialTimeout("tcp", args[1]+":79", time.Second*60)
    758 		if err != nil {
    759 			errStr = "Error calling service"
    760 			goto render
    761 		}
    762 		data := make([]byte, 16000) // 16kb
    763 		defer conn.Close()
    764 		conn.Write([]byte(args[0] + "\r\n"))
    765 		_, err = conn.Read(data)
    766 		if err != nil {
    767 			errStr = "Error calling service"
    768 			goto render
    769 		}
    770 		data = bytes.Trim(data, "\x00")
    771 		output = string(data)
    772 	}
    773 render:
    774 	if errStr != "" {
    775 		w.WriteHeader(500) // lazy
    776 	}
    777 	d := struct {
    778 		Output string
    779 		Error  string
    780 	}{output, errStr}
    781 	t.ExecuteTemplate(w, "fingerproxy.html", d)
    782 }
    783 
    784 func adminUserHandler(w http.ResponseWriter, r *http.Request) {
    785 	user := getAuthUser(r)
    786 	if r.Method == "POST" {
    787 		if !user.IsAdmin {
    788 			renderDefaultError(w, http.StatusForbidden)
    789 			return
    790 		}
    791 		components := strings.Split(r.URL.Path, "/")
    792 		if len(components) < 5 {
    793 			renderError(w, "Invalid action", http.StatusBadRequest)
    794 			return
    795 		}
    796 		userName := components[3]
    797 		action := components[4]
    798 		var err error
    799 		if action == "activate" {
    800 			err = activateUser(userName)
    801 		} else if action == "impersonate" {
    802 			if user.ImpersonatingUser != "" {
    803 				// Don't allow nested impersonation
    804 				renderError(w, "Cannot nest impersonation, log out from impersonated user first.", 400)
    805 				return
    806 			}
    807 			session, _ := SessionStore.Get(r, "cookie-session")
    808 			session.Values["auth_user"] = userName
    809 			session.Values["impersonating_user"] = user.Username
    810 			session.Save(r, w)
    811 			log.Printf("User %s impersonated %s", user.Username, userName)
    812 			http.Redirect(w, r, "/", http.StatusSeeOther)
    813 			return
    814 		}
    815 		if err != nil {
    816 			log.Println(err)
    817 			renderDefaultError(w, http.StatusInternalServerError)
    818 			return
    819 		}
    820 		http.Redirect(w, r, "/admin", http.StatusSeeOther)
    821 	}
    822 }
    823 
    824 func checkDomainHandler(w http.ResponseWriter, r *http.Request) {
    825 	domain := r.URL.Query().Get("domain")
    826 	if domain != "" && domains[domain] != "" {
    827 		w.Write([]byte(domain))
    828 		return
    829 	}
    830 	if domain == c.Host || strings.HasSuffix(domain, "."+c.Host) {
    831 		w.Write([]byte(domain))
    832 	}
    833 	http.Error(w, "Not Found", 404)
    834 }
    835 
    836 func proxyGemini(w http.ResponseWriter, r *http.Request) {
    837 	errorMsg := "proxy.flounder.online has been deprecated. Consider using another gemini proxy"
    838 	renderError(w, errorMsg, 410)
    839 }
    840 
    841 func runHTTPServer() {
    842 	log.Printf("Running http server with hostname %s on port %d.", c.Host, c.HttpPort)
    843 	var err error
    844 	t = template.New("main").Funcs(template.FuncMap{
    845 		"unixTime": time.Unix,
    846 		"parent":   path.Dir, "hasSuffix": strings.HasSuffix,
    847 		"safeGeminiURL": func(u string) template.URL {
    848 			if strings.HasPrefix(u, "gemini://") {
    849 				return template.URL(u)
    850 			}
    851 			return ""
    852 		}})
    853 	t, err = t.ParseGlob(path.Join(c.TemplatesDirectory, "*.html"))
    854 	if err != nil {
    855 		log.Fatal(err)
    856 	}
    857 	serveMux := http.NewServeMux()
    858 
    859 	s := strings.SplitN(c.Host, ":", 2)
    860 	hostname := s[0]
    861 
    862 	serveMux.HandleFunc(hostname+"/", rootHandler)
    863 	serveMux.HandleFunc(hostname+"/my_site", mySiteHandler)
    864 	serveMux.HandleFunc(hostname+"/me", myAccountHandler)
    865 	serveMux.HandleFunc(hostname+"/my_site/flounder-archive.zip", archiveHandler)
    866 	serveMux.HandleFunc(hostname+"/admin", adminHandler)
    867 	serveMux.HandleFunc(hostname+"/edit/", editFileHandler)
    868 	serveMux.HandleFunc(hostname+"/updates/", updatesHandler)
    869 	serveMux.HandleFunc(hostname+"/upload", uploadFilesHandler)
    870 	serveMux.Handle(hostname+"/login", limit(http.HandlerFunc(loginHandler)))
    871 	serveMux.Handle(hostname+"/register", limit(http.HandlerFunc(registerHandler)))
    872 	serveMux.HandleFunc(hostname+"/logout", logoutHandler)
    873 	serveMux.HandleFunc(hostname+"/delete/", deleteFileHandler)
    874 	serveMux.HandleFunc(hostname+"/delete-account", deleteAccountHandler)
    875 	serveMux.HandleFunc(hostname+"/reset-password", resetPasswordHandler)
    876 
    877 	// Used by Caddy
    878 	serveMux.HandleFunc(hostname+"/check-domain", checkDomainHandler)
    879 
    880 	// admin commands
    881 	serveMux.HandleFunc(hostname+"/admin/user/", adminUserHandler)
    882 
    883 	wrapped := handlers.CustomLoggingHandler(log.Writer(), serveMux, logFormatter)
    884 
    885 	// Deprecated
    886 	serveMux.HandleFunc("proxy."+hostname+"/", proxyGemini)
    887 	serveMux.HandleFunc("/", userFile)
    888 	srv := &http.Server{
    889 		ReadTimeout:  5 * time.Second,
    890 		WriteTimeout: 10 * time.Second,
    891 		IdleTimeout:  120 * time.Second,
    892 		Addr:         fmt.Sprintf(":%d", c.HttpPort),
    893 		Handler:      wrapped,
    894 	}
    895 	log.Fatal(srv.ListenAndServe())
    896 }