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