lens @ 5168a9476f0e83264ecafc85bc9145e8bdcbb8dc

ref: Move net/http

I was young and naive, fasthttp does not fit my needs and make
development slower. I'll move to net/http since it has a wider support
and will spare some time on implementation detail things (like CSRF). It
will allow me to reduce a bit of the code since there may be lib for
handling cookie encryption and auth in general.
diff --git a/cmd/server/main.go b/cmd/server/main.go
index 385c54f554a135737ec038c895fb6f32db917e2a..39987e5834e607f801155dab453d6ca7cffc8b0f 100644
--- a/cmd/server/main.go
+++ b/cmd/server/main.go
@@ -4,13 +4,13 @@ import (
 	"context"
 	"encoding/hex"
 	"errors"
+	"net/http"
 	"os"
 	"os/signal"
 
-	"github.com/fasthttp/router"
+	"github.com/gorilla/mux"
 	"github.com/sirupsen/logrus"
 	flag "github.com/spf13/pflag"
-	"github.com/valyala/fasthttp"
 	"gorm.io/driver/mysql"
 	"gorm.io/driver/postgres"
 	"gorm.io/driver/sqlite"
@@ -71,8 +71,8 @@ 	if err != nil {
 		panic("failed to decode key database: " + err.Error())
 	}
 
-	r := router.New()
-	r.GET("/static/{filepath:*}", ext.FileServer(static.Static))
+	r := mux.NewRouter()
+	r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.FS(static.Static))))
 
 	// repository
 	var (
@@ -122,7 +122,7 @@ 	)
 
 	// worker
 	var (
-		serverWorker = worker.NewServerWorker(&fasthttp.Server{Handler: r.Handler, NoDefaultContentType: true})
+		serverWorker = worker.NewServerWorker(&http.Server{Handler: r, Addr: "0.0.0.0:8080"})
 		fileWorker   = worker.NewWorkerFromChanProcessor[string](
 			fileScanner,
 			scheduler,
diff --git a/go.mod b/go.mod
index 8b4538dd8cc256279703e1f2a210d72e4aed034d..91dd7fa6fe880f5bcc4c8e787ec6d438b695edab 100644
--- a/go.mod
+++ b/go.mod
@@ -4,12 +4,11 @@ go 1.19
 
 require (
 	github.com/barasher/go-exiftool v1.10.0
-	github.com/fasthttp/router v1.4.19
 	github.com/google/go-cmp v0.5.9
+	github.com/gorilla/mux v1.8.0
 	github.com/h2non/bimg v1.1.9
 	github.com/sirupsen/logrus v1.9.2
 	github.com/spf13/pflag v1.0.5
-	github.com/valyala/fasthttp v1.47.0
 	github.com/valyala/quicktemplate v1.7.0
 	golang.org/x/crypto v0.8.0
 	gorm.io/driver/mysql v1.5.1
@@ -20,16 +19,13 @@ 	gorm.io/gorm v1.25.1
 )
 
 require (
-	github.com/andybalholm/brotli v1.0.5 // indirect
 	github.com/go-sql-driver/mysql v1.7.1 // indirect
 	github.com/jackc/pgpassfile v1.0.0 // indirect
 	github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
 	github.com/jackc/pgx/v5 v5.3.1 // indirect
 	github.com/jinzhu/inflection v1.0.0 // indirect
 	github.com/jinzhu/now v1.1.5 // indirect
-	github.com/klauspost/compress v1.16.5 // indirect
 	github.com/mattn/go-sqlite3 v1.14.16 // indirect
-	github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
 	github.com/valyala/bytebufferpool v1.0.0 // indirect
 	golang.org/x/mod v0.10.0 // indirect
 	golang.org/x/sys v0.8.0 // indirect
diff --git a/go.sum b/go.sum
index c0456bcef960c20fd37c2cb31634cc5a5b5e1c35..03ce51a72b270cd86a03b06252ae5e751efefc18 100644
--- a/go.sum
+++ b/go.sum
@@ -1,14 +1,10 @@
 github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
 github.com/andybalholm/brotli v1.0.3/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
-github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
-github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
 github.com/barasher/go-exiftool v1.10.0 h1:f5JY5jc42M7tzR6tbL9508S2IXdIcG9QyieEXNMpIhs=
 github.com/barasher/go-exiftool v1.10.0/go.mod h1:F9s/a3uHSM8YniVfwF+sbQUtP8Gmh9nyzigNF+8vsWo=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/fasthttp/router v1.4.19 h1:RLE539IU/S4kfb4MP56zgP0TIBU9kEg0ID9GpWO0vqk=
-github.com/fasthttp/router v1.4.19/go.mod h1:+Fh3YOd8x1+he6ZS+d2iUDBH9MGGZ1xQFUor0DE9rKE=
 github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
 github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
 github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
@@ -18,6 +14,8 @@ github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
 github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
+github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
 github.com/h2non/bimg v1.1.9 h1:WH20Nxko9l/HFm4kZCA3Phbgu2cbHvYzxwxn9YROEGg=
 github.com/h2non/bimg v1.1.9/go.mod h1:R3+UiYwkK4rQl6KVFTOFJHitgLbZXBZNFh2cv3AEbp8=
 github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
@@ -33,16 +31,12 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
 github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
 github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
 github.com/klauspost/compress v1.13.5/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
-github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI=
-github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
 github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
 github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
 github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
 github.com/microsoft/go-mssqldb v0.17.0 h1:Fto83dMZPnYv1Zwx5vHHxpNraeEaUlQ/hhHLgZiaenE=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk=
-github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
 github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y=
 github.com/sirupsen/logrus v1.9.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
 github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
@@ -54,8 +48,6 @@ github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
 github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
 github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
 github.com/valyala/fasthttp v1.30.0/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus=
-github.com/valyala/fasthttp v1.47.0 h1:y7moDoxYzMooFpT5aHgNgVOQDrS3qlkfiP9mDtGGK9c=
-github.com/valyala/fasthttp v1.47.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
 github.com/valyala/quicktemplate v1.7.0 h1:LUPTJmlVcb46OOUY3IeD9DojFpAVbsG+5WFTcjMJzCM=
 github.com/valyala/quicktemplate v1.7.0/go.mod h1:sqKJnoaOF88V07vkO+9FL8fb9uZg/VPSJnLYn+LmLk8=
 github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
diff --git a/pkg/ext/fileserver.go b/pkg/ext/fileserver.go
deleted file mode 100644
index 87c1ae8f2d507f8c1d69832d741e24a2c76daeca..0000000000000000000000000000000000000000
--- a/pkg/ext/fileserver.go
+++ /dev/null
@@ -1,33 +0,0 @@
-package ext
-
-import (
-	"io/fs"
-	"mime"
-	"path/filepath"
-
-	"github.com/valyala/fasthttp"
-)
-
-type FileSystem interface {
-	Open(name string) (fs.File, error)
-}
-
-// This is a VERY simple file server. It does not take a lot into consideration
-// and it should only be used to return small predictable files, like in the
-// static folder.
-func FileServer(rootFS FileSystem) fasthttp.RequestHandler {
-	return func(ctx *fasthttp.RequestCtx) {
-		path := ctx.UserValue("filepath").(string)
-
-		f, err := rootFS.Open(path)
-		if err != nil {
-			InternalServerError(ctx, err)
-			return
-		}
-		defer f.Close()
-
-		m := mime.TypeByExtension(filepath.Ext(path))
-		ctx.SetContentType(m)
-		ctx.SetBodyStream(f, -1)
-	}
-}
diff --git a/pkg/ext/middleware.go b/pkg/ext/middleware.go
index bcc6c5feb3fa1e30f54afc64d96dfc2c6ad5ca2d..fe2d185be75a28df5f686dabb664b237f85d56ae 100644
--- a/pkg/ext/middleware.go
+++ b/pkg/ext/middleware.go
@@ -1,22 +1,22 @@
 package ext
 
 import (
+	"context"
 	"encoding/base64"
+	"errors"
+	"net/http"
 	"time"
 
 	"github.com/sirupsen/logrus"
-	"github.com/valyala/fasthttp"
 
 	"git.sr.ht/~gabrielgio/img/pkg/database/repository"
 	"git.sr.ht/~gabrielgio/img/pkg/service"
 )
 
-func HTML(next fasthttp.RequestHandler) fasthttp.RequestHandler {
-	return func(ctx *fasthttp.RequestCtx) {
-		if len(ctx.Request.Header.ContentType()) > 0 {
-			ctx.Response.Header.SetContentType("text/html")
-		}
-		next(ctx)
+func HTML(next http.HandlerFunc) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Set("Content-Type", "text/html")
+		next(w, r)
 	}
 }
 
@@ -30,17 +30,15 @@ 		entry: log,
 	}
 }
 
-func (l *LogMiddleware) HTTP(next fasthttp.RequestHandler) fasthttp.RequestHandler {
-	return func(ctx *fasthttp.RequestCtx) {
+func (l *LogMiddleware) HTTP(next http.HandlerFunc) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
 		start := time.Now()
-		next(ctx)
+		next(w, r)
 		elapsed := time.Since(start)
 		l.entry.
 			WithField("time", elapsed).
-			WithField("code", ctx.Response.StatusCode()).
-			WithField("path", string(ctx.Path())).
-			WithField("bytes", len(ctx.Response.Body())).
-			Info(string(ctx.Request.Header.Method()))
+			WithField("path", r.URL.Path).
+			Info(r.Method)
 	}
 }
 
@@ -56,23 +54,23 @@ 		entry: log.WithField("context", "auth"),
 	}
 }
 
-func (a *AuthMiddleware) LoggedIn(next fasthttp.RequestHandler) fasthttp.RequestHandler {
-	return func(ctx *fasthttp.RequestCtx) {
-		path := string(ctx.Path())
+func (a *AuthMiddleware) LoggedIn(next http.HandlerFunc) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		path := string(r.URL.Path)
 		if path == "/login" || path == "/initial" {
-			next(ctx)
+			next(w, r)
 			return
 		}
 
 		redirectLogin := "/login?redirect=" + path
-		authBase64 := ctx.Request.Header.Cookie("auth")
-		if authBase64 == nil {
+		authBase64, err := r.Cookie("auth")
+		if errors.Is(err, http.ErrNoCookie) {
 			a.entry.Info("No auth provided")
-			ctx.Redirect(redirectLogin, 307)
+			http.Redirect(w, r, redirectLogin, http.StatusTemporaryRedirect)
 			return
 		}
 
-		auth, err := base64.StdEncoding.DecodeString(string(authBase64))
+		auth, err := base64.StdEncoding.DecodeString(authBase64.Value)
 		if err != nil {
 			a.entry.Error(err)
 			return
@@ -81,20 +79,20 @@
 		token, err := service.ReadToken(auth, a.key)
 		if err != nil {
 			a.entry.Error(err)
-			ctx.Redirect(redirectLogin, 307)
+			http.Redirect(w, r, redirectLogin, http.StatusTemporaryRedirect)
 			return
 		}
-		ctx.SetUserValue("token", token)
+		r = r.WithContext(context.WithValue(r.Context(), "token", token))
 		a.entry.
 			WithField("userID", token.UserID).
 			WithField("username", token.Username).
 			Info("user recognized")
-		next(ctx)
+		next(w, r)
 	}
 }
 
-func GetTokenFromCtx(ctx *fasthttp.RequestCtx) *service.Token {
-	tokenValue := ctx.UserValue("token")
+func GetTokenFromCtx(w http.ResponseWriter, r *http.Request) *service.Token {
+	tokenValue := r.Context().Value("token")
 	if token, ok := tokenValue.(*service.Token); ok {
 		return token
 	}
@@ -111,31 +109,31 @@ 		userRepository: userRepository,
 	}
 }
 
-func (i *InitialSetupMiddleware) Check(next fasthttp.RequestHandler) fasthttp.RequestHandler {
-	return func(ctx *fasthttp.RequestCtx) {
+func (i *InitialSetupMiddleware) Check(next http.HandlerFunc) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
 		// if user has been set to context it is logged in already
-		_, ok := ctx.UserValue("token").(*service.Token)
-		if ok {
-			next(ctx)
+		token := GetTokenFromCtx(w, r)
+		if token == nil {
+			next(w, r)
 			return
 		}
 
-		path := string(ctx.Path())
+		path := r.URL.Path
 		if path == "/initial" {
-			next(ctx)
+			next(w, r)
 			return
 		}
 
-		exists, err := i.userRepository.Any(ctx)
+		exists, err := i.userRepository.Any(r.Context())
 		if err != nil {
-			InternalServerError(ctx, err)
+			InternalServerError(w, err)
 			return
 		}
 
 		if exists {
-			next(ctx)
+			next(w, r)
 			return
 		}
-		ctx.Redirect("/initial", 307)
+		http.Redirect(w, r, "/initial", http.StatusTemporaryRedirect)
 	}
 }
diff --git a/pkg/ext/responses.go b/pkg/ext/responses.go
index dbad5b20204f2c8f3d665bf9a7cd7094bdddf328..ba58dd5d54d2a7d27a5a3a0c241240d3ec747614 100644
--- a/pkg/ext/responses.go
+++ b/pkg/ext/responses.go
@@ -1,39 +1,21 @@
 package ext
 
 import (
-	"bytes"
 	"fmt"
-
-	"github.com/valyala/fasthttp"
+	"net/http"
 
 	"git.sr.ht/~gabrielgio/img/templates"
 )
 
-var (
-	ContentTypeHTML = []byte("text/html")
-)
-
-func NotFoundHTML(ctx *fasthttp.RequestCtx) {
-	templates.WritePageTemplate(ctx, &templates.ErrorPage{
+func NotFound(w http.ResponseWriter, r *http.Request) {
+	templates.WritePageTemplate(w, &templates.ErrorPage{
 		Err: "Not Found",
 	})
 }
 
-func NotFound(ctx *fasthttp.RequestCtx) {
-	ctx.Response.SetStatusCode(404)
-	ct := ctx.Response.Header.ContentType()
-	if bytes.Equal(ct, ContentTypeHTML) {
-		NotFoundHTML(ctx)
-	}
-}
-
-func InternalServerError(ctx *fasthttp.RequestCtx, err error) {
-	ctx.Response.SetStatusCode(500)
-	templates.WritePageTemplate(ctx, &templates.ErrorPage{
+func InternalServerError(w http.ResponseWriter, err error) {
+	w.WriteHeader(http.StatusInternalServerError)
+	templates.WritePageTemplate(w, &templates.ErrorPage{
 		Err: fmt.Sprintf("Internal Server Error:\n%s", err.Error()),
 	})
 }
-
-func NoContent(ctx *fasthttp.RequestCtx) {
-	ctx.Response.SetStatusCode(204)
-}
diff --git a/pkg/ext/router.go b/pkg/ext/router.go
index 74f0a95f468cb3a484bdfa17a27e6085c35729a8..8b9a31090bef2036a018bb43cae4e6e81fd6ad0a 100644
--- a/pkg/ext/router.go
+++ b/pkg/ext/router.go
@@ -1,51 +1,52 @@
 package ext
 
 import (
-	"github.com/fasthttp/router"
-	"github.com/valyala/fasthttp"
+	"net/http"
+
+	"github.com/gorilla/mux"
 )
 
 type (
 	Router struct {
 		middlewares []Middleware
-		fastRouter  *router.Router
+		router      *mux.Router
 	}
-	Middleware          func(next fasthttp.RequestHandler) fasthttp.RequestHandler
-	ErrorRequestHandler func(ctx *fasthttp.RequestCtx) error
+	Middleware          func(next http.HandlerFunc) http.HandlerFunc
+	ErrorRequestHandler func(w http.ResponseWriter, r *http.Request) error
 )
 
-func NewRouter(nestedRouter *router.Router) *Router {
+func NewRouter(nestedRouter *mux.Router) *Router {
 	return &Router{
-		fastRouter: nestedRouter,
+		router: nestedRouter,
 	}
 }
 
-func (self *Router) AddMiddleware(middleware Middleware) {
-	self.middlewares = append(self.middlewares, middleware)
+func (r *Router) AddMiddleware(middleware Middleware) {
+	r.middlewares = append(r.middlewares, middleware)
 }
 
-func wrapError(next ErrorRequestHandler) fasthttp.RequestHandler {
-	return func(ctx *fasthttp.RequestCtx) {
-		if err := next(ctx); err != nil {
-			ctx.Response.SetStatusCode(500)
-			InternalServerError(ctx, err)
+func wrapError(next ErrorRequestHandler) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		if err := next(w, r); err != nil {
+			w.WriteHeader(http.StatusInternalServerError)
+			InternalServerError(w, err)
 		}
 	}
 }
 
-func (self *Router) run(next ErrorRequestHandler) fasthttp.RequestHandler {
-	return func(ctx *fasthttp.RequestCtx) {
+func (r *Router) run(next ErrorRequestHandler) http.HandlerFunc {
+	return func(w http.ResponseWriter, re *http.Request) {
 		req := wrapError(next)
-		for _, r := range self.middlewares {
+		for _, r := range r.middlewares {
 			req = r(req)
 		}
-		req(ctx)
+		req(w, re)
 	}
 }
 
-func (self *Router) GET(path string, handler ErrorRequestHandler) {
-	self.fastRouter.GET(path, self.run(handler))
+func (r *Router) GET(path string, handler ErrorRequestHandler) {
+	r.router.HandleFunc(path, r.run(handler)).Methods("GET")
 }
-func (self *Router) POST(path string, handler ErrorRequestHandler) {
-	self.fastRouter.POST(path, self.run(handler))
+func (r *Router) POST(path string, handler ErrorRequestHandler) {
+	r.router.HandleFunc(path, r.run(handler)).Methods("POSt")
 }
diff --git a/pkg/view/auth.go b/pkg/view/auth.go
index 631cfb30815cde5ec8694a09073da6a597b8e0a4..2a4b95e77f23249aaa99b9c644ae39e6926029db 100644
--- a/pkg/view/auth.go
+++ b/pkg/view/auth.go
@@ -2,8 +2,7 @@ package view
 
 import (
 	"encoding/base64"
-
-	"github.com/valyala/fasthttp"
+	"net/http"
 
 	"git.sr.ht/~gabrielgio/img/pkg/ext"
 	"git.sr.ht/~gabrielgio/img/pkg/service"
@@ -20,71 +19,77 @@ 		userController: userController,
 	}
 }
 
-func (v *AuthView) LoginView(ctx *fasthttp.RequestCtx) error {
-	templates.WritePageTemplate(ctx, &templates.LoginPage{})
+func (v *AuthView) LoginView(w http.ResponseWriter, r *http.Request) error {
+	templates.WritePageTemplate(w, &templates.LoginPage{})
 	return nil
 }
 
-func (v *AuthView) Logout(ctx *fasthttp.RequestCtx) error {
-	cook := fasthttp.Cookie{}
-	cook.SetKey("auth")
-	cook.SetValue("")
-	cook.SetMaxAge(-1)
-	cook.SetHTTPOnly(true)
-	cook.SetSameSite(fasthttp.CookieSameSiteDefaultMode)
-	ctx.Response.Header.SetCookie(&cook)
+func (v *AuthView) Logout(w http.ResponseWriter, r *http.Request) error {
+	cook := http.Cookie{
+		Name:     "auth",
+		Value:    "",
+		MaxAge:   -1,
+		HttpOnly: true,
+		SameSite: http.SameSiteDefaultMode,
+	}
+	http.SetCookie(w, &cook)
 
-	ctx.Redirect("/", 307)
+	http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
 	return nil
 }
 
-func (v *AuthView) Login(ctx *fasthttp.RequestCtx) error {
-	username := ctx.FormValue("username")
-	password := ctx.FormValue("password")
+func (v *AuthView) Login(w http.ResponseWriter, r *http.Request) error {
+	var (
+		username = []byte(r.FormValue("username"))
+		password = []byte(r.FormValue("password"))
+	)
 
-	auth, err := v.userController.Login(ctx, username, password)
+	auth, err := v.userController.Login(r.Context(), username, password)
 	if err != nil {
 		return err
 	}
 
 	base64Auth := base64.StdEncoding.EncodeToString(auth)
 
-	cook := fasthttp.Cookie{}
-	cook.SetKey("auth")
-	cook.SetValue(base64Auth)
-	cook.SetHTTPOnly(true)
-	cook.SetSameSite(fasthttp.CookieSameSiteDefaultMode)
-	ctx.Response.Header.SetCookie(&cook)
+	cook := http.Cookie{
+		Name:     "auth",
+		Value:    base64Auth,
+		HttpOnly: true,
+		SameSite: http.SameSiteDefaultMode,
+	}
+	http.SetCookie(w, &cook)
 
-	redirect := string(ctx.FormValue("redirect"))
+	redirect := r.FormValue("redirect")
 	if redirect == "" {
-		ctx.Redirect("/", 307)
+		http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
 	} else {
-		ctx.Redirect(redirect, 307)
+		http.Redirect(w, r, redirect, http.StatusTemporaryRedirect)
 	}
 	return nil
 }
 
-func Index(ctx *fasthttp.RequestCtx) {
-	ctx.Redirect("/login", 307)
+func Index(w http.ResponseWriter, r *http.Request) {
+	http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
 }
 
-func (v *AuthView) InitialRegisterView(ctx *fasthttp.RequestCtx) error {
-	templates.WritePageTemplate(ctx, &templates.RegisterPage{})
+func (v *AuthView) InitialRegisterView(w http.ResponseWriter, r *http.Request) error {
+	templates.WritePageTemplate(w, &templates.RegisterPage{})
 	return nil
 }
 
-func (v *AuthView) InitialRegister(ctx *fasthttp.RequestCtx) error {
-	username := ctx.FormValue("username")
-	password := ctx.FormValue("password")
-	path := ctx.FormValue("path")
+func (v *AuthView) InitialRegister(w http.ResponseWriter, r *http.Request) error {
+	var (
+		username = []byte(r.FormValue("username"))
+		password = []byte(r.FormValue("password"))
+		path     = []byte(r.FormValue("path"))
+	)
 
-	err := v.userController.InitialRegister(ctx, username, password, path)
+	err := v.userController.InitialRegister(r.Context(), username, password, path)
 	if err != nil {
 		return err
 	}
 
-	ctx.Redirect("/login", 307)
+	http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
 	return nil
 }
 
diff --git a/pkg/view/filesystem.go b/pkg/view/filesystem.go
index 6a011171eef0c61a0385600ec638b4dcc18f672c..d49ad4ff02e0b62f151c8570ed4598b73d504cde 100644
--- a/pkg/view/filesystem.go
+++ b/pkg/view/filesystem.go
@@ -1,7 +1,7 @@
 package view
 
 import (
-	"github.com/valyala/fasthttp"
+	"net/http"
 
 	"git.sr.ht/~gabrielgio/img/pkg/database/repository"
 	"git.sr.ht/~gabrielgio/img/pkg/ext"
@@ -31,21 +31,23 @@ 		settings:  settingsRepository,
 	}
 }
 
-func (self *FileSystemView) Index(ctx *fasthttp.RequestCtx) error {
-	pathValue := string(ctx.FormValue("path"))
-	token := ext.GetTokenFromCtx(ctx)
+func (self *FileSystemView) Index(w http.ResponseWriter, r *http.Request) error {
+	var (
+		pathValue = r.FormValue("path")
+		token     = ext.GetTokenFromCtx(w, r)
+	)
 
-	page, err := self.fsService.GetPage(ctx, token.UserID, pathValue)
+	page, err := self.fsService.GetPage(r.Context(), token.UserID, pathValue)
 	if err != nil {
 		return err
 	}
 
-	settings, err := self.settings.Load(ctx)
+	settings, err := self.settings.Load(r.Context())
 	if err != nil {
 		return err
 	}
 
-	templates.WritePageTemplate(ctx, &templates.FilePage{
+	templates.WritePageTemplate(w, &templates.FilePage{
 		Page:      page,
 		ShowMode:  settings.ShowMode,
 		ShowOwner: settings.ShowOwner,
diff --git a/pkg/view/media.go b/pkg/view/media.go
index 6e34fd6c4505d418f9a57c57f20d9156866a9b2f..c7d84ec9e8346627ef2b13fab1bc7e0ec949a2d5 100644
--- a/pkg/view/media.go
+++ b/pkg/view/media.go
@@ -1,9 +1,8 @@
 package view
 
 import (
+	"net/http"
 	"strconv"
-
-	"github.com/valyala/fasthttp"
 
 	"git.sr.ht/~gabrielgio/img/pkg/database/repository"
 	"git.sr.ht/~gabrielgio/img/pkg/ext"
@@ -18,12 +17,12 @@ 		settingsRepository repository.SettingsRepository
 	}
 )
 
-func getPagination(ctx *fasthttp.RequestCtx) *repository.Pagination {
+func getPagination(w http.ResponseWriter, r *http.Request) *repository.Pagination {
 	var (
 		size    int
 		page    int
-		sizeStr = string(ctx.FormValue("size"))
-		pageStr = string(ctx.FormValue("page"))
+		sizeStr = r.FormValue("size")
+		pageStr = r.FormValue("page")
 	)
 
 	if sizeStr == "" {
@@ -60,22 +59,22 @@ 		settingsRepository: settingsRepository,
 	}
 }
 
-func (self *MediaView) Index(ctx *fasthttp.RequestCtx) error {
-	p := getPagination(ctx)
-	token := ext.GetTokenFromCtx(ctx)
+func (self *MediaView) Index(w http.ResponseWriter, r *http.Request) error {
+	p := getPagination(w, r)
+	token := ext.GetTokenFromCtx(w, r)
 
-	userPath, err := self.userRepository.GetPathFromUserID(ctx, token.UserID)
+	userPath, err := self.userRepository.GetPathFromUserID(r.Context(), token.UserID)
 	if err != nil {
 		return err
 	}
 
 	p.Path = userPath
-	medias, err := self.mediaRepository.List(ctx, p)
+	medias, err := self.mediaRepository.List(r.Context(), p)
 	if err != nil {
 		return err
 	}
 
-	settings, err := self.settingsRepository.Load(ctx)
+	settings, err := self.settingsRepository.Load(r.Context())
 	if err != nil {
 		return err
 	}
@@ -89,43 +88,43 @@ 		},
 		Settings: settings,
 	}
 
-	templates.WritePageTemplate(ctx, page)
+	templates.WritePageTemplate(w, page)
 
 	return nil
 }
 
-func (self *MediaView) GetImage(ctx *fasthttp.RequestCtx) error {
-	pathHash := string(ctx.FormValue("path_hash"))
+func (self *MediaView) GetImage(w http.ResponseWriter, r *http.Request) error {
+	pathHash := r.FormValue("path_hash")
 
-	media, err := self.mediaRepository.Get(ctx, pathHash)
+	media, err := self.mediaRepository.Get(r.Context(), pathHash)
 	if err != nil {
 		return err
 	}
 
-	ctx.Response.Header.SetContentType(media.MIMEType)
-	fasthttp.ServeFileUncompressed(ctx, media.Path)
+	w.Header().Set("Content-Type", media.MIMEType)
+	http.ServeFile(w, r, media.Path)
 	return nil
 }
 
-func (self *MediaView) GetThumbnail(ctx *fasthttp.RequestCtx) error {
-	pathHash := string(ctx.FormValue("path_hash"))
+func (self *MediaView) GetThumbnail(w http.ResponseWriter, r *http.Request) error {
+	pathHash := r.FormValue("path_hash")
 
-	path, err := self.mediaRepository.GetThumbnailPath(ctx, pathHash)
+	path, err := self.mediaRepository.GetThumbnailPath(r.Context(), pathHash)
 	if err != nil {
-		ctx.Redirect("/media/image?path_hash="+pathHash, 307)
+		http.Redirect(w, r, "/media/image?path_hash="+pathHash, http.StatusTemporaryRedirect)
 		// nolint: nilerr
 		return nil
 	}
 
-	ctx.Request.Header.SetContentType("image/jpeg")
-	fasthttp.ServeFileUncompressed(ctx, path)
+	w.Header().Set("Content-Type", "image/jpeg")
+	http.ServeFile(w, r, path)
 	return nil
 }
 
 func (self *MediaView) SetMyselfIn(r *ext.Router) {
-	r.GET("/media", self.Index)
-	r.POST("/media", self.Index)
+	r.GET("/media/", self.Index)
+	r.POST("/media/", self.Index)
 
-	r.GET("/media/image", self.GetImage)
-	r.GET("/media/thumbnail", self.GetThumbnail)
+	r.GET("/media/image/", self.GetImage)
+	r.GET("/media/thumbnail/", self.GetThumbnail)
 }
diff --git a/pkg/view/settings.go b/pkg/view/settings.go
index 5131362e2ba129a8791a42a73ce732e91322f5c2..bf2dca6d27190c1ada6765d2ee61588edb8e85f0 100644
--- a/pkg/view/settings.go
+++ b/pkg/view/settings.go
@@ -1,9 +1,8 @@
 package view
 
 import (
+	"net/http"
 	"strconv"
-
-	"github.com/valyala/fasthttp"
 
 	"git.sr.ht/~gabrielgio/img/pkg/database/repository"
 	"git.sr.ht/~gabrielgio/img/pkg/ext"
@@ -29,18 +28,18 @@ 		userController:     userController,
 	}
 }
 
-func (self *SettingsView) Index(ctx *fasthttp.RequestCtx) error {
-	s, err := self.settingsRepository.Load(ctx)
+func (self *SettingsView) Index(w http.ResponseWriter, r *http.Request) error {
+	s, err := self.settingsRepository.Load(r.Context())
 	if err != nil {
 		return err
 	}
 
-	users, err := self.userController.List(ctx)
+	users, err := self.userController.List(r.Context())
 	if err != nil {
 		return err
 	}
 
-	templates.WritePageTemplate(ctx, &templates.SettingsPage{
+	templates.WritePageTemplate(w, &templates.SettingsPage{
 		Settings: s,
 		Users:    users,
 	})
@@ -48,22 +47,22 @@
 	return nil
 }
 
-func (self *SettingsView) User(ctx *fasthttp.RequestCtx) error {
-	id := string(ctx.FormValue("userId"))
+func (self *SettingsView) User(w http.ResponseWriter, r *http.Request) error {
+	id := r.FormValue("userId")
 	idValue, err := ParseUint(id)
 	if err != nil {
 		return err
 	}
 
 	if idValue == nil {
-		templates.WritePageTemplate(ctx, &templates.UserPage{})
+		templates.WritePageTemplate(w, &templates.UserPage{})
 	} else {
-		user, err := self.userController.Get(ctx, *idValue)
+		user, err := self.userController.Get(r.Context(), *idValue)
 		if err != nil {
 			return err
 		}
 
-		templates.WritePageTemplate(ctx, &templates.UserPage{
+		templates.WritePageTemplate(w, &templates.UserPage{
 			ID:       idValue,
 			Username: user.Username,
 			Path:     user.Path,
@@ -74,13 +73,13 @@
 	return nil
 }
 
-func (self *SettingsView) UpsertUser(ctx *fasthttp.RequestCtx) error {
+func (self *SettingsView) UpsertUser(w http.ResponseWriter, r *http.Request) error {
 	var (
-		username = string(ctx.FormValue("username"))
-		password = ctx.FormValue("password")
-		path     = string(ctx.FormValue("path"))
-		isAdmin  = string(ctx.FormValue("isAdmin")) == "on"
-		id       = string(ctx.FormValue("userId"))
+		username = r.FormValue("username")
+		password = []byte(r.FormValue("password"))
+		path     = r.FormValue("path")
+		isAdmin  = r.FormValue("isAdmin") == "on"
+		id       = r.FormValue("userId")
 	)
 
 	idValue, err := ParseUint(id)
@@ -88,18 +87,18 @@ 	if err != nil {
 		return err
 	}
 
-	err = self.userController.Upsert(ctx, idValue, username, "", password, isAdmin, path)
+	err = self.userController.Upsert(r.Context(), idValue, username, "", password, isAdmin, path)
 	if err != nil {
 		return err
 	}
 
-	ctx.Redirect("/settings", 307)
+	http.Redirect(w, r, "/settings", http.StatusTemporaryRedirect)
 	return nil
 }
 
-func (self *SettingsView) Delete(ctx *fasthttp.RequestCtx) error {
+func (self *SettingsView) Delete(w http.ResponseWriter, r *http.Request) error {
 	var (
-		id = string(ctx.FormValue("userId"))
+		id = r.FormValue("userId")
 	)
 
 	idValue, err := ParseUint(id)
@@ -108,24 +107,24 @@ 		return err
 	}
 
 	if idValue != nil {
-		err = self.userController.Delete(ctx, *idValue)
+		err = self.userController.Delete(r.Context(), *idValue)
 		if err != nil {
 			return err
 		}
 	}
 
-	ctx.Redirect("/settings", 307)
+	http.Redirect(w, r, "/settings", http.StatusTemporaryRedirect)
 	return nil
 }
 
-func (self *SettingsView) Save(ctx *fasthttp.RequestCtx) error {
+func (self *SettingsView) Save(w http.ResponseWriter, r *http.Request) error {
 	var (
-		showMode             = string(ctx.FormValue("showMode")) == "on"
-		showOwner            = string(ctx.FormValue("showOwner")) == "on"
-		preloadVideoMetadata = string(ctx.FormValue("preloadVideoMetadata")) == "on"
+		showMode             = r.FormValue("showMode") == "on"
+		showOwner            = r.FormValue("showOwner") == "on"
+		preloadVideoMetadata = r.FormValue("preloadVideoMetadata") == "on"
 	)
 
-	err := self.settingsRepository.Save(ctx, &repository.Settings{
+	err := self.settingsRepository.Save(r.Context(), &repository.Settings{
 		ShowMode:             showMode,
 		ShowOwner:            showOwner,
 		PreloadVideoMetadata: preloadVideoMetadata,
@@ -134,7 +133,7 @@ 	if err != nil {
 		return err
 	}
 
-	return self.Index(ctx)
+	return self.Index(w, r)
 }
 
 func (self *SettingsView) SetMyselfIn(r *ext.Router) {
diff --git a/pkg/worker/httpserver.go b/pkg/worker/httpserver.go
index 181cf73f4d173437b0b37312b1a4999ad4221324..dc8f255f1953dfec7011e4611dd54ddfb3d22cfa 100644
--- a/pkg/worker/httpserver.go
+++ b/pkg/worker/httpserver.go
@@ -2,29 +2,24 @@ package worker
 
 import (
 	"context"
-
-	"github.com/valyala/fasthttp"
+	"net/http"
 )
 
 type ServerWorker struct {
-	server *fasthttp.Server
+	server *http.Server
 }
 
 func (self *ServerWorker) Start(ctx context.Context) error {
 	go func() {
 		// nolint: errcheck
-		self.server.ListenAndServe("0.0.0.0:8080")
+		self.server.ListenAndServe()
 	}()
 
 	<-ctx.Done()
-	return self.Shutdown()
+	return self.server.Shutdown(ctx)
 }
 
-func (self *ServerWorker) Shutdown() error {
-	return self.server.Shutdown()
-}
-
-func NewServerWorker(server *fasthttp.Server) *ServerWorker {
+func NewServerWorker(server *http.Server) *ServerWorker {
 	return &ServerWorker{
 		server: server,
 	}
diff --git a/templates/media.qtpl b/templates/media.qtpl
index 621789945412ff643221945ab5ea59d590722f66..ae58e6181411735d7552404a7f6c5c443cb0dcae 100644
--- a/templates/media.qtpl
+++ b/templates/media.qtpl
@@ -22,19 +22,19 @@ <div class="columns is-multiline">
 {% for _, media := range p.Medias %}
     <div class="card-image">
        {% if media.IsVideo() %}
-       <video class="image is-fit" controls muted="true" poster="/media/thumbnail?path_hash={%s media.PathHash %}" preload="{%s p.PreloadAttr() %}">
-           <source src="/media/image?path_hash={%s media.PathHash %}" type="{%s media.MIMEType %}">
+       <video class="image is-fit" controls muted="true" poster="/media/thumbnail/?path_hash={%s media.PathHash %}" preload="{%s p.PreloadAttr() %}">
+           <source src="/media/image/?path_hash={%s media.PathHash %}" type="{%s media.MIMEType %}">
        </video>
        {% else %}
         <figure class="image is-fit">
-            <img src="/media/thumbnail?path_hash={%s media.PathHash %}">
+            <img src="/media/thumbnail/?path_hash={%s media.PathHash %}">
         </figure>
         {% endif %}
     </div>
 {% endfor %}
 </div>
 <div class="row">
-    <a href="/media?page={%d p.Next.Page %}" class="button is-pulled-right">next</a>
+    <a href="/media/?page={%d p.Next.Page %}" class="button is-pulled-right">next</a>
 </div>
 {% endfunc %}