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