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