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 }
235 if bcrypt.CompareHashAndPassword(db_password, []byte(password)) == nil {
236 log.Println("logged in")
237 session, _ := SessionStore.Get(r, "cookie-session")
238 session.Values["auth_user"] = name
239 session.Save(r, w)
240 http.Redirect(w, r, "/", 302)
241 } else {
242 data := struct {
243 Error string
244 PageTitle string
245 }{"Invalid login or password", c.SiteTitle}
246 err := t.ExecuteTemplate(w, "login.html", data)
247 if err != nil {
248 log.Println(err)
249 renderError(w, InternalServerErrorMsg, 500)
250 return
251 }
252 }
253 }
254}
255
256func logoutHandler(w http.ResponseWriter, r *http.Request) {
257 session, _ := SessionStore.Get(r, "cookie-session")
258 session.Options.MaxAge = -1
259 session.Save(r, w)
260 http.Redirect(w, r, "/", 302)
261}
262
263const ok = "-0123456789abcdefghijklmnopqrstuvwxyz"
264
265func isOkUsername(s string) bool {
266 if len(s) < 1 {
267 return false
268 }
269 if len(s) > 31 {
270 return false
271 }
272 for _, char := range s {
273 if !strings.Contains(ok, strings.ToLower(string(char))) {
274 return false
275 }
276 }
277 return true
278}
279func registerHandler(w http.ResponseWriter, r *http.Request) {
280 if r.Method == "GET" {
281 data := struct {
282 Host string
283 Errors []string
284 PageTitle string
285 }{c.Host, nil, "Register"}
286 err := t.ExecuteTemplate(w, "register.html", data)
287 if err != nil {
288 log.Println(err)
289 renderError(w, InternalServerErrorMsg, 500)
290 return
291 }
292 } else if r.Method == "POST" {
293 r.ParseForm()
294 email := r.Form.Get("email")
295 password := r.Form.Get("password")
296 errors := []string{}
297 if !strings.Contains(email, "@") {
298 errors = append(errors, "Invalid Email")
299 }
300 if r.Form.Get("password") != r.Form.Get("password2") {
301 errors = append(errors, "Passwords don't match")
302 }
303 if len(password) < 6 {
304 errors = append(errors, "Password is too short")
305 }
306 username := strings.ToLower(r.Form.Get("username"))
307 if !isOkUsername(username) {
308 errors = append(errors, "Username is invalid: can only contain letters, numbers and hypens. Maximum 32 characters.")
309 }
310 hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 8) // TODO handle error
311 _, err = DB.Exec("insert into user (username, email, password_hash) values ($1, $2, $3)", username, email, string(hashedPassword))
312 if err != nil {
313 log.Println(err)
314 errors = append(errors, "Username or email is already used")
315 }
316 if len(errors) > 0 {
317 data := struct {
318 Host string
319 Errors []string
320 PageTitle string
321 }{c.Host, errors, "Register"}
322 t.ExecuteTemplate(w, "register.html", data)
323 } else {
324 os.Mkdir(path.Join(c.FilesDirectory, username), os.ModePerm)
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 fileName := path.Join(c.FilesDirectory, userName, filepath.Clean(r.URL.Path))
339 extension := path.Ext(fileName)
340 if r.URL.Path == "/static/style.css" {
341 http.ServeFile(w, r, path.Join(c.TemplatesDirectory, "static/style.css"))
342 }
343 if extension == ".gmi" || extension == ".gemini" {
344 // covert to html
345 stat, _ := os.Stat(fileName)
346 file, _ := os.Open(fileName)
347 htmlString := gmi.Parse(file).HTML()
348 reader := strings.NewReader(htmlString)
349 w.Header().Set("Content-Type", "text/html")
350 http.ServeContent(w, r, fileName, stat.ModTime(), reader)
351 } else {
352 http.ServeFile(w, r, fileName)
353 }
354}
355
356func runHTTPServer() {
357 log.Printf("Running http server on %s", c.Host)
358 var err error
359 t, err = template.ParseGlob(path.Join(c.TemplatesDirectory, "*.html"))
360 if err != nil {
361 log.Fatal(err)
362 }
363 serveMux := http.NewServeMux()
364
365 s := strings.SplitN(c.Host, ":", 2)
366 hostname := s[0]
367 var port string
368 if len(s) > 1 {
369 port = s[1]
370 } else {
371 port = "443"
372 }
373 serveMux.HandleFunc(hostname+"/", rootHandler)
374 serveMux.HandleFunc(hostname+"/my_site", mySiteHandler)
375 serveMux.HandleFunc(hostname+"/edit/", editFileHandler)
376 serveMux.HandleFunc(hostname+"/upload", uploadFilesHandler)
377 serveMux.HandleFunc(hostname+"/login", loginHandler)
378 serveMux.HandleFunc(hostname+"/logout", logoutHandler)
379 serveMux.HandleFunc(hostname+"/register", registerHandler)
380 serveMux.HandleFunc(hostname+"/delete/", deleteFileHandler)
381
382 // TODO rate limit login https://github.com/ulule/limiter
383
384 wrapped := handlers.LoggingHandler(os.Stdout, serveMux)
385
386 // handle user files based on subdomain
387 serveMux.HandleFunc("/", userFile)
388 // login+register functions
389 srv := &http.Server{
390 ReadTimeout: 5 * time.Second,
391 WriteTimeout: 10 * time.Second,
392 IdleTimeout: 120 * time.Second,
393 Addr: ":" + port,
394 // TLSConfig: tlsConfig,
395 Handler: wrapped,
396 }
397 log.Fatal(srv.ListenAndServeTLS(c.TLSCertFile, c.TLSKeyFile))
398}