all repos — flounder @ f7d80c2a661b1b8f7717b743082d263952667591

A small site builder for the Gemini protocol

http.go (view raw)

  1package main
  2
  3import (
  4	"database/sql"
  5	"git.sr.ht/~adnano/gmi"
  6	"github.com/gorilla/handlers"
  7	_ "github.com/mattn/go-sqlite3"
  8	"golang.org/x/crypto/bcrypt"
  9	"html/template"
 10	"io"
 11	"io/ioutil"
 12	"log"
 13	"net/http"
 14	"os"
 15	"path"
 16	"path/filepath"
 17	"strings"
 18	"time"
 19)
 20
 21var t *template.Template
 22var DB *sql.DB
 23
 24const InternalServerErrorMsg = "500: Internal Server Error"
 25
 26func renderError(w http.ResponseWriter, errorMsg string, statusCode int) {
 27	data := struct {
 28		PageTitle string
 29		ErrorMsg  string
 30	}{"Error!", errorMsg}
 31	err := t.ExecuteTemplate(w, "error.html", data)
 32	if err != nil { // shouldn't happen probably
 33		http.Error(w, errorMsg, statusCode)
 34	}
 35}
 36
 37func rootHandler(w http.ResponseWriter, r *http.Request) {
 38	// serve everything inside static directory
 39	if r.URL.Path != "/" {
 40		fileName := path.Join(c.TemplatesDirectory, "static", filepath.Clean(r.URL.Path))
 41		http.ServeFile(w, r, fileName)
 42		return
 43	}
 44	indexFiles, err := getIndexFiles()
 45	if err != nil {
 46		log.Println(err)
 47		renderError(w, InternalServerErrorMsg, 500)
 48		return
 49	}
 50	allUsers, err := getUsers()
 51	if err != nil {
 52		log.Println(err)
 53		renderError(w, InternalServerErrorMsg, 500)
 54		return
 55	}
 56	data := struct {
 57		Domain    string
 58		PageTitle string
 59		Files     []*File
 60		Users     []string
 61	}{c.RootDomain, c.SiteTitle, indexFiles, allUsers}
 62	err = t.ExecuteTemplate(w, "index.html", data)
 63	if err != nil {
 64		log.Println(err)
 65		renderError(w, InternalServerErrorMsg, 500)
 66		return
 67	}
 68}
 69
 70func editFileHandler(w http.ResponseWriter, r *http.Request) {
 71	authUser := "alex"
 72	fileName := filepath.Clean(r.URL.Path[len("/edit/"):])
 73	filePath := path.Join(c.FilesDirectory, authUser, fileName)
 74	if r.Method == "GET" {
 75		err := checkIfValidFile(filePath, nil)
 76		if err != nil {
 77			log.Println(err)
 78			renderError(w, err.Error(), 400)
 79			return
 80		}
 81		f, err := os.OpenFile(filePath, os.O_RDONLY|os.O_CREATE, 0644)
 82		defer f.Close()
 83		fileBytes, err := ioutil.ReadAll(f)
 84		if err != nil {
 85			log.Println(err)
 86			renderError(w, InternalServerErrorMsg, 500)
 87			return
 88		}
 89		data := struct {
 90			FileName  string
 91			FileText  string
 92			PageTitle string
 93		}{fileName, string(fileBytes), c.SiteTitle}
 94		err = t.ExecuteTemplate(w, "edit_file.html", data)
 95		if err != nil {
 96			log.Println(err)
 97			renderError(w, InternalServerErrorMsg, 500)
 98			return
 99		}
100	} else if r.Method == "POST" {
101		// get post body
102		r.ParseForm()
103		fileBytes := []byte(r.Form.Get("file_text"))
104		err := checkIfValidFile(filePath, fileBytes)
105		if err != nil {
106			log.Println(err)
107			renderError(w, err.Error(), 400)
108			return
109		}
110		err = ioutil.WriteFile(filePath, fileBytes, 0644)
111		if err != nil {
112			log.Println(err)
113			renderError(w, InternalServerErrorMsg, 500)
114			return
115		}
116		http.Redirect(w, r, "/my_site", 302)
117	}
118}
119
120func uploadFilesHandler(w http.ResponseWriter, r *http.Request) {
121	if r.Method == "POST" {
122		authUser := "alex"
123		r.ParseMultipartForm(10 << 20)
124		file, fileHeader, err := r.FormFile("file")
125		fileName := filepath.Clean(fileHeader.Filename)
126		defer file.Close()
127		if err != nil {
128			log.Println(err)
129			renderError(w, err.Error(), 400)
130			return
131		}
132		var dest []byte
133		file.Read(dest)
134		log.Println("asdfadf")
135		err = checkIfValidFile(fileName, dest)
136		if err != nil {
137			log.Println(err)
138			renderError(w, err.Error(), 400)
139			return
140		}
141		destPath := path.Join(c.FilesDirectory, authUser, fileName)
142
143		f, err := os.OpenFile(destPath, os.O_WRONLY|os.O_CREATE, 0644)
144		if err != nil {
145			log.Println(err)
146			renderError(w, InternalServerErrorMsg, 500)
147			return
148		}
149		defer f.Close()
150		io.Copy(f, file)
151	}
152	http.Redirect(w, r, "/my_site", 302)
153}
154
155func deleteFileHandler(w http.ResponseWriter, r *http.Request) {
156	authUser := "alex"
157	fileName := filepath.Clean(r.URL.Path[len("/delete/"):])
158	filePath := path.Join(c.FilesDirectory, authUser, fileName)
159	if r.Method == "POST" {
160		os.Remove(filePath) // suppress error
161	}
162	http.Redirect(w, r, "/my_site", 302)
163}
164
165func mySiteHandler(w http.ResponseWriter, r *http.Request) {
166	authUser := "alex"
167	// check auth
168	files, _ := getUserFiles(authUser)
169	data := struct {
170		Domain    string
171		PageTitle string
172		AuthUser  string
173		Files     []*File
174	}{c.RootDomain, c.SiteTitle, authUser, files}
175	_ = t.ExecuteTemplate(w, "my_site.html", data)
176}
177
178func loginHandler(w http.ResponseWriter, r *http.Request) {
179	if r.Method == "GET" {
180		// show page
181		data := struct {
182			Error     string
183			PageTitle string
184		}{"", "Login"}
185		err := t.ExecuteTemplate(w, "login.html", data)
186		if err != nil {
187			log.Println(err)
188			renderError(w, InternalServerErrorMsg, 500)
189			return
190		}
191	} else if r.Method == "POST" {
192		r.ParseForm()
193		name := r.Form.Get("username")
194		password := r.Form.Get("password")
195		row := DB.QueryRow("SELECT password_hash FROM user where username = $1", name)
196		var db_password []byte
197		_ = row.Scan(&db_password)
198		if bcrypt.CompareHashAndPassword(db_password, []byte(password)) == nil {
199			log.Println("logged in")
200			// create session
201			http.Redirect(w, r, "/", 302)
202		} else {
203			data := struct {
204				Error     string
205				PageTitle string
206			}{"Invalid login or password", c.SiteTitle}
207			err := t.ExecuteTemplate(w, "login.html", data)
208			if err != nil {
209				log.Println(err)
210				renderError(w, InternalServerErrorMsg, 500)
211				return
212			}
213		}
214	}
215}
216
217const ok = "-0123456789abcdefghijklmnopqrstuvwxyz"
218
219func isOkUsername(s string) bool {
220	if len(s) < 1 {
221		return false
222	}
223	if len(s) > 31 {
224		return false
225	}
226	for _, char := range s {
227		if !strings.Contains(ok, strings.ToLower(string(char))) {
228			return false
229		}
230	}
231	return true
232}
233func registerHandler(w http.ResponseWriter, r *http.Request) {
234	if r.Method == "GET" {
235		data := struct {
236			Domain    string
237			Errors    []string
238			PageTitle string
239		}{c.RootDomain, nil, "Register"}
240		err := t.ExecuteTemplate(w, "register.html", data)
241		if err != nil {
242			log.Println(err)
243			renderError(w, InternalServerErrorMsg, 500)
244			return
245		}
246	} else if r.Method == "POST" {
247		r.ParseForm()
248		email := r.Form.Get("email")
249		password := r.Form.Get("password")
250		errors := []string{}
251		if !strings.Contains(email, "@") {
252			errors = append(errors, "Invalid Email")
253		}
254		if r.Form.Get("password") != r.Form.Get("password2") {
255			errors = append(errors, "Passwords don't match")
256		}
257		if len(password) < 6 {
258			errors = append(errors, "Password is too short")
259		}
260		username := strings.ToLower(r.Form.Get("username"))
261		if !isOkUsername(username) {
262			errors = append(errors, "Username is invalid: can only contain letters, numbers and hypens. Maximum 32 characters.")
263		}
264		hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 8) // TODO handle error
265		_, err = DB.Exec("insert into user (username, email, password_hash) values ($1, $2, $3)", username, email, string(hashedPassword))
266		if err != nil {
267			log.Println(err)
268			errors = append(errors, "Username or email is already used")
269		}
270		if len(errors) > 0 {
271			data := struct {
272				Domain    string
273				Errors    []string
274				PageTitle string
275			}{c.RootDomain, errors, "Register"}
276			t.ExecuteTemplate(w, "register.html", data)
277		} else {
278			data := struct {
279				Domain    string
280				Message   string
281				PageTitle string
282			}{c.RootDomain, "Registration complete! The server admin will approve your request before you can log in.", "Registration Complete"}
283			t.ExecuteTemplate(w, "message.html", data)
284		}
285	}
286}
287
288// Server a user's file
289func userFile(w http.ResponseWriter, r *http.Request) {
290	userName := strings.Split(r.Host, ".")[0]
291	fileName := path.Join(c.FilesDirectory, userName, filepath.Clean(r.URL.Path))
292	extension := path.Ext(fileName)
293	if r.URL.Path == "/static/style.css" {
294		http.ServeFile(w, r, path.Join(c.TemplatesDirectory, "static/style.css"))
295	}
296	if extension == ".gmi" || extension == ".gemini" {
297		// covert to html
298		stat, _ := os.Stat(fileName)
299		file, _ := os.Open(fileName)
300		htmlString := gmi.Parse(file).HTML()
301		reader := strings.NewReader(htmlString)
302		w.Header().Set("Content-Type", "text/html")
303		http.ServeContent(w, r, fileName, stat.ModTime(), reader)
304	} else {
305		http.ServeFile(w, r, fileName)
306	}
307}
308
309func runHTTPServer() {
310	log.Println("Running http server")
311	var err error
312	t, err = template.ParseGlob(path.Join(c.TemplatesDirectory, "*.html"))
313	if err != nil {
314		log.Fatal(err)
315	}
316	serveMux := http.NewServeMux()
317
318	serveMux.HandleFunc(c.RootDomain+"/", rootHandler)
319	serveMux.HandleFunc(c.RootDomain+"/my_site", mySiteHandler)
320	serveMux.HandleFunc(c.RootDomain+"/edit/", editFileHandler)
321	serveMux.HandleFunc(c.RootDomain+"/upload", uploadFilesHandler)
322	serveMux.HandleFunc(c.RootDomain+"/login", loginHandler)
323	serveMux.HandleFunc(c.RootDomain+"/register", registerHandler)
324	serveMux.HandleFunc(c.RootDomain+"/delete/", deleteFileHandler)
325
326	// TODO rate limit login https://github.com/ulule/limiter
327
328	wrapped := handlers.LoggingHandler(os.Stdout, serveMux)
329
330	// handle user files based on subdomain
331	serveMux.HandleFunc("/", userFile)
332	// login+register functions
333	srv := &http.Server{
334		ReadTimeout:  5 * time.Second,
335		WriteTimeout: 10 * time.Second,
336		IdleTimeout:  120 * time.Second,
337		Addr:         ":8080",
338		// TLSConfig:    tlsConfig,
339		Handler: wrapped,
340	}
341	log.Fatal(srv.ListenAndServe())
342}