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