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/gorilla/sessions"
8 _ "github.com/mattn/go-sqlite3"
9 "golang.org/x/crypto/bcrypt"
10 "html/template"
11 "io"
12 "io/ioutil"
13 "log"
14 "net/http"
15 "os"
16 "path"
17 "path/filepath"
18 "strings"
19 "time"
20)
21
22var t *template.Template
23var DB *sql.DB
24var SessionStore *sessions.CookieStore
25
26const InternalServerErrorMsg = "500: Internal Server Error"
27
28func renderError(w http.ResponseWriter, errorMsg string, statusCode int) {
29 data := struct {
30 PageTitle string
31 ErrorMsg string
32 }{"Error!", errorMsg}
33 err := t.ExecuteTemplate(w, "error.html", data)
34 if err != nil { // shouldn't happen probably
35 http.Error(w, errorMsg, statusCode)
36 }
37}
38
39func rootHandler(w http.ResponseWriter, r *http.Request) {
40 // serve everything inside static directory
41 if r.URL.Path != "/" {
42 fileName := path.Join(c.TemplatesDirectory, "static", filepath.Clean(r.URL.Path))
43 http.ServeFile(w, r, fileName)
44 return
45 }
46 _, authd := getAuthUser(r)
47 indexFiles, err := getIndexFiles()
48 if err != nil {
49 log.Println(err)
50 renderError(w, InternalServerErrorMsg, 500)
51 return
52 }
53 allUsers, err := getUsers()
54 if err != nil {
55 log.Println(err)
56 renderError(w, InternalServerErrorMsg, 500)
57 return
58 }
59 data := struct {
60 Host string
61 PageTitle string
62 Files []*File
63 Users []string
64 LoggedIn bool
65 }{c.Host, c.SiteTitle, indexFiles, allUsers, authd}
66 err = t.ExecuteTemplate(w, "index.html", data)
67 if err != nil {
68 log.Println(err)
69 renderError(w, InternalServerErrorMsg, 500)
70 return
71 }
72}
73
74func editFileHandler(w http.ResponseWriter, r *http.Request) {
75 session, _ := SessionStore.Get(r, "cookie-session")
76 authUser, ok := session.Values["auth_user"].(string)
77 if !ok {
78 renderError(w, "Forbidden", 403)
79 return
80 }
81 fileName := filepath.Clean(r.URL.Path[len("/edit/"):])
82 filePath := path.Join(c.FilesDirectory, authUser, fileName)
83 if r.Method == "GET" {
84 err := checkIfValidFile(filePath, nil)
85 if err != nil {
86 log.Println(err)
87 renderError(w, err.Error(), 400)
88 return
89 }
90 f, err := os.OpenFile(filePath, os.O_RDONLY|os.O_CREATE, 0644)
91 defer f.Close()
92 fileBytes, err := ioutil.ReadAll(f)
93 if err != nil {
94 log.Println(err)
95 renderError(w, InternalServerErrorMsg, 500)
96 return
97 }
98 data := struct {
99 FileName string
100 FileText string
101 PageTitle string
102 }{fileName, string(fileBytes), c.SiteTitle}
103 err = t.ExecuteTemplate(w, "edit_file.html", data)
104 if err != nil {
105 log.Println(err)
106 renderError(w, InternalServerErrorMsg, 500)
107 return
108 }
109 } else if r.Method == "POST" {
110 // get post body
111 r.ParseForm()
112 fileBytes := []byte(r.Form.Get("file_text"))
113 err := checkIfValidFile(filePath, fileBytes)
114 if err != nil {
115 log.Println(err)
116 renderError(w, err.Error(), 400)
117 return
118 }
119 err = ioutil.WriteFile(filePath, fileBytes, 0644)
120 if err != nil {
121 log.Println(err)
122 renderError(w, InternalServerErrorMsg, 500)
123 return
124 }
125 http.Redirect(w, r, "/my_site", 302)
126 }
127}
128
129func uploadFilesHandler(w http.ResponseWriter, r *http.Request) {
130 if r.Method == "POST" {
131 session, _ := SessionStore.Get(r, "cookie-session")
132 authUser, ok := session.Values["auth_user"].(string)
133 if !ok {
134 renderError(w, "Forbidden", 403)
135 return
136 }
137 r.ParseMultipartForm(10 << 20)
138 file, fileHeader, err := r.FormFile("file")
139 fileName := filepath.Clean(fileHeader.Filename)
140 defer file.Close()
141 if err != nil {
142 log.Println(err)
143 renderError(w, err.Error(), 400)
144 return
145 }
146 var dest []byte
147 file.Read(dest)
148 log.Println("asdfadf")
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, file)
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, approved FROM user where username = $1", name)
225 var db_password []byte
226 var active bool
227 _ = row.Scan(&db_password, &active)
228 if !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 _, err = DB.Exec("insert into user (username, email, password_hash) values ($1, $2, $3)", username, email, string(hashedPassword))
313 if err != nil {
314 log.Println(err)
315 errors = append(errors, "Username or email is already used")
316 }
317 if len(errors) > 0 {
318 data := struct {
319 Host string
320 Errors []string
321 PageTitle string
322 }{c.Host, errors, "Register"}
323 t.ExecuteTemplate(w, "register.html", data)
324 } else {
325 data := struct {
326 Host string
327 Message string
328 PageTitle string
329 }{c.Host, "Registration complete! The server admin will approve your request before you can log in.", "Registration Complete"}
330 t.ExecuteTemplate(w, "message.html", data)
331 }
332 }
333}
334
335// Server a user's file
336func userFile(w http.ResponseWriter, r *http.Request) {
337 userName := strings.Split(r.Host, ".")[0]
338 p := filepath.Clean(r.URL.Path)
339 if p == "/" {
340 p = "index.gmi"
341 }
342 fileName := path.Join(c.FilesDirectory, userName, p)
343 extension := path.Ext(fileName)
344 if r.URL.Path == "/style.css" {
345 http.ServeFile(w, r, path.Join(c.TemplatesDirectory, "static/style.css"))
346 }
347 if extension == ".gmi" || extension == ".gemini" {
348 _, err := os.Stat(fileName)
349 if err != nil {
350 renderError(w, "404: file not found", 404)
351 return
352 }
353 file, _ := os.Open(fileName)
354
355 htmlString := gmi.Parse(file).HTML()
356 data := struct {
357 SiteBody template.HTML
358 PageTitle string
359 }{template.HTML(htmlString), userName}
360 t.ExecuteTemplate(w, "user_page.html", data)
361 } else {
362 http.ServeFile(w, r, fileName)
363 }
364}
365
366func runHTTPServer() {
367 log.Printf("Running http server on %s", c.Host)
368 var err error
369 t, err = template.ParseGlob(path.Join(c.TemplatesDirectory, "*.html"))
370 if err != nil {
371 log.Fatal(err)
372 }
373 serveMux := http.NewServeMux()
374
375 s := strings.SplitN(c.Host, ":", 2)
376 hostname := s[0]
377 var port string
378 if len(s) > 1 {
379 port = s[1]
380 } else {
381 port = "443"
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(os.Stdout, 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: ":" + port,
404 // TLSConfig: tlsConfig,
405 Handler: wrapped,
406 }
407 log.Fatal(srv.ListenAndServeTLS(c.TLSCertFile, c.TLSKeyFile))
408}