all repos — flounder @ 48c4084ba652171948827c35a6803c8c54aa98da

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		err := checkAuth(name, password)
196		if err == nil {
197			log.Println("logged in")
198			// redirect home
199		} else {
200			data := struct {
201				Error     string
202				PageTitle string
203			}{"Invalid login or password", c.SiteTitle}
204			err := t.ExecuteTemplate(w, "login.html", data)
205			if err != nil {
206				log.Println(err)
207				renderError(w, InternalServerErrorMsg, 500)
208				return
209			}
210		}
211		// create session
212		// redirect home
213		// verify login
214		// check for errors
215	}
216}
217
218const ok = "-0123456789abcdefghijklmnopqrstuvwxyz"
219
220func isOkUsername(s string) bool {
221	if len(s) < 1 {
222		return false
223	}
224	if len(s) > 31 {
225		return false
226	}
227	for _, char := range s {
228		if !strings.Contains(ok, strings.ToLower(string(char))) {
229			return false
230		}
231	}
232	return true
233}
234func registerHandler(w http.ResponseWriter, r *http.Request) {
235	if r.Method == "GET" {
236		data := struct {
237			Domain    string
238			Errors    []string
239			PageTitle string
240		}{c.RootDomain, nil, "Register"}
241		err := t.ExecuteTemplate(w, "register.html", data)
242		if err != nil {
243			log.Println(err)
244			renderError(w, InternalServerErrorMsg, 500)
245			return
246		}
247	} else if r.Method == "POST" {
248		r.ParseForm()
249		email := r.Form.Get("email")
250		password := r.Form.Get("password")
251		errors := []string{}
252		if !strings.Contains(email, "@") {
253			errors = append(errors, "Invalid Email")
254		}
255		if r.Form.Get("password") != r.Form.Get("password2") {
256			errors = append(errors, "Passwords don't match")
257		}
258		if len(password) < 6 {
259			errors = append(errors, "Password is too short")
260		}
261		username := strings.ToLower(r.Form.Get("username"))
262		if !isOkUsername(username) {
263			errors = append(errors, "Username is invalid: can only contain letters, numbers and hypens. Maximum 32 characters.")
264		}
265		hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 8) // TODO handle error
266		_, err = DB.Exec("insert into user (username, email, password_hash) values ($1, $2, $3)", username, email, string(hashedPassword))
267		if err != nil {
268			log.Println(err)
269			errors = append(errors, "Username or email is already used")
270		}
271		if len(errors) > 0 {
272			data := struct {
273				Domain    string
274				Errors    []string
275				PageTitle string
276			}{c.RootDomain, errors, "Register"}
277			t.ExecuteTemplate(w, "register.html", data)
278		} else {
279			data := struct {
280				Domain    string
281				Message   string
282				PageTitle string
283			}{c.RootDomain, "Registration complete! The server admin will approve your request before you can log in.", "Registration Complete"}
284			t.ExecuteTemplate(w, "message.html", data)
285		}
286	}
287}
288
289// Server a user's file
290func userFile(w http.ResponseWriter, r *http.Request) {
291	userName := strings.Split(r.Host, ".")[0]
292	fileName := path.Join(c.FilesDirectory, userName, filepath.Clean(r.URL.Path))
293	extension := path.Ext(fileName)
294	if r.URL.Path == "/static/style.css" {
295		http.ServeFile(w, r, path.Join(c.TemplatesDirectory, "static/style.css"))
296	}
297	if extension == ".gmi" || extension == ".gemini" {
298		// covert to html
299		stat, _ := os.Stat(fileName)
300		file, _ := os.Open(fileName)
301		htmlString := gmi.Parse(file).HTML()
302		reader := strings.NewReader(htmlString)
303		w.Header().Set("Content-Type", "text/html")
304		http.ServeContent(w, r, fileName, stat.ModTime(), reader)
305	} else {
306		http.ServeFile(w, r, fileName)
307	}
308}
309
310func runHTTPServer() {
311	log.Println("Running http server")
312	var err error
313	t, err = template.ParseGlob(path.Join(c.TemplatesDirectory, "*.html"))
314	if err != nil {
315		log.Fatal(err)
316	}
317	serveMux := http.NewServeMux()
318
319	serveMux.HandleFunc(c.RootDomain+"/", rootHandler)
320	serveMux.HandleFunc(c.RootDomain+"/my_site", mySiteHandler)
321	serveMux.HandleFunc(c.RootDomain+"/edit/", editFileHandler)
322	serveMux.HandleFunc(c.RootDomain+"/upload", uploadFilesHandler)
323	serveMux.HandleFunc(c.RootDomain+"/login", loginHandler)
324	serveMux.HandleFunc(c.RootDomain+"/register", registerHandler)
325	serveMux.HandleFunc(c.RootDomain+"/delete/", deleteFileHandler)
326
327	// TODO rate limit login https://github.com/ulule/limiter
328
329	wrapped := handlers.LoggingHandler(os.Stdout, serveMux)
330
331	// handle user files based on subdomain
332	serveMux.HandleFunc("/", userFile)
333	// login+register functions
334	srv := &http.Server{
335		ReadTimeout:  5 * time.Second,
336		WriteTimeout: 10 * time.Second,
337		IdleTimeout:  120 * time.Second,
338		Addr:         ":8080",
339		// TLSConfig:    tlsConfig,
340		Handler: wrapped,
341	}
342	log.Fatal(srv.ListenAndServe())
343}