flounder

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

http.go (24377B) - raw


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