diff --git a/.gitignore b/.gitignore index fb3dc3d..21465f2 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ main dsn *.pem api_key.pem +admin_hash.pem diff --git a/bootstrap/routes.go b/bootstrap/routes.go index ca46cb5..8341e60 100644 --- a/bootstrap/routes.go +++ b/bootstrap/routes.go @@ -18,38 +18,19 @@ along with this program. If not, see .Rawley Fow */ import ( - "net/http" - "strings" - "github.com/gin-gonic/gin" "gitlab.com/rawleyifowler/site-rework/controllers" + "gitlab.com/rawleyifowler/site-rework/utils" ) // Initializes all routes from the controller package func InitializeRoutes(router *gin.Engine) { - router.NoRoute(ServePage("not_found.tmpl")) + router.NoRoute(utils.ServePage("not_found.tmpl")) blogGroup := router.Group("/blog") controllers.RegisterBlogGroup(blogGroup) - router.GET("/", ServePage("index.tmpl")) - router.GET("/resume", ServePage("resume.tmpl")) - router.GET("/contact", ServePage("contact.tmpl")) -} - -// Returns a handler function to serve a page based on the template that is inputted -func ServePage(temp string) gin.HandlerFunc { - title := strings.Split(temp, ".")[0] - if title == "index" { - // Index is the home page so we don't care - title = "" - } else { - // Else title it accordingly - title = " | " + title - } - return func(c *gin.Context) { - c.HTML( - http.StatusOK, - temp, - struct{ Title string }{Title: title}, - ) - } + adminGroup := router.Group("/admin") + controllers.RegisterAdminGroup(adminGroup) + router.GET("/", utils.ServePage("index.tmpl")) + router.GET("/resume", utils.ServePage("resume.tmpl")) + router.GET("/contact", utils.ServePage("contact.tmpl")) } diff --git a/controllers/admin.go b/controllers/admin.go new file mode 100644 index 0000000..6d09ec0 --- /dev/null +++ b/controllers/admin.go @@ -0,0 +1,87 @@ +package controllers + +import ( + "crypto/sha256" + "net/http" + + "github.com/gin-gonic/gin" + "gitlab.com/rawleyifowler/site-rework/models" + "gitlab.com/rawleyifowler/site-rework/utils" +) + +var ( + key string = utils.LoadApiKey("api_key.pem") + admin_hash string = utils.LoadAdminHash("admin_hash.pem") + att map[string]uint32 = make(map[string]uint32) +) + +// Ensure is logged in +func AdminOnly() gin.HandlerFunc { + return func(c *gin.Context) { + k, err := c.Cookie("admin_key") + if err != nil { + c.Redirect(http.StatusPermanentRedirect, "/admin/") + } + if k == key { + c.Next() + } + } +} + +func RegisterAdminGroup(r *gin.RouterGroup) { + r.GET("/", utils.ServePage("login.tmpl")) + r.POST("/login", HandleLogin) + r.GET("/post", AdminOnly(), utils.ServePage("create_post.tmpl")) + r.POST("/post", AdminOnly(), CreatePost) +} + +func HandleLogin(c *gin.Context) { + if att[c.ClientIP()] > 5 { + c.Redirect(http.StatusPermanentRedirect, "/admin/") + } + err := c.Request.ParseForm() + if err != nil { + c.Redirect(http.StatusPermanentRedirect, "/admin/") + } + f := c.Request.Form + for _, v := range []string{"username", "password"} { + if !f.Has(v) { + c.AbortWithStatus(http.StatusNotAcceptable) + } + } + str := f.Get("username") + f.Get("password") + s := sha256.New() + s.Sum([]byte(str)) + var h []byte + s.Write(h) + if string(h) == admin_hash { + c.SetCookie("admin_key", key, 1, "/", "rawley.xyz", true, true) + c.Redirect(http.StatusTemporaryRedirect, "/admin/post") + } else { + att[c.ClientIP()]++ + c.Redirect(http.StatusTemporaryRedirect, "/admin/") + } +} + +func CreatePost(c *gin.Context) { + err := c.Request.ParseForm() + if err != nil { + c.AbortWithStatus(http.StatusBadRequest) + } + f := c.Request.Form + for _, v := range []string{"title", "content", "url"} { + if !f.Has(v) { + c.AbortWithStatus(http.StatusNotAcceptable) + } + } + b := AddBlogPost(&models.BlogPost{ + Title: f.Get("title"), + Content: f.Get("content"), + Url: f.Get("url"), + }) + if b { + c.Redirect(http.StatusTemporaryRedirect, "/blog/post/"+f.Get("url")) + } else { + c.Redirect(http.StatusTemporaryRedirect, "/admin/post") + } +} diff --git a/controllers/blog.go b/controllers/blog.go index 6eb95fa..889b72e 100644 --- a/controllers/blog.go +++ b/controllers/blog.go @@ -16,8 +16,6 @@ along with this program. If not, see .Rawley Fow */ import ( - "encoding/json" - "io/ioutil" "net/http" "strconv" @@ -34,7 +32,6 @@ type CommentDto struct { var ( db *gorm.DB - apiKey string recentPosters map[string]uint captchaVals [2]int ) @@ -44,8 +41,6 @@ func RegisterBlogGroup(r *gin.RouterGroup) { recentPosters = make(map[string]uint) // Load dsn and initialize database dsn := utils.LoadDSN("dsn") - // Load and initialize api key - apiKey = utils.LoadApiKey("api_key.pem") var err error db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{}) if err != nil { @@ -54,7 +49,7 @@ func RegisterBlogGroup(r *gin.RouterGroup) { utils.PerformMigrations(db) r.GET("/", RenderBlogPage) r.GET("/post/:url", RenderIndividualBlogPost) - r.POST("/post", CreateBlogPost) + r.POST("/post") r.POST("/post/comment", CreateComment) // Clear the commenters cache every 15 minutes go utils.TimedClearMap(&recentPosters, 180000*5) @@ -86,40 +81,9 @@ func RenderIndividualBlogPost(c *gin.Context) { } } -func CreateBlogPost(c *gin.Context) { - reqKey, err := c.Request.Cookie("APK") - if err != nil { - c.Status(http.StatusForbidden) - return - } - if reqKey.Value != apiKey { - c.Status(http.StatusForbidden) - return - } - raw, err := ioutil.ReadAll(c.Request.Body) - if err != nil { - c.Status(http.StatusNotAcceptable) - return - } - var data models.BlogPost - // Write the data from the request to the data variable - if json.Unmarshal([]byte(raw), &data) != nil { - c.Status(http.StatusNotAcceptable) - return - } - // Create the blog post in the database - err = db.Create(&data).Error - if err != nil { - c.JSON(http.StatusNotAcceptable, "{ \"error\": "+err.Error()+" }") - } else { - c.Status(http.StatusAccepted) - } -} - func CreateComment(c *gin.Context) { if c.Request.ParseForm() != nil { - c.Status(http.StatusNotAcceptable) - return + c.AbortWithStatus(http.StatusNotAcceptable) } a := []string{c.Request.Form.Get("author"), c.Request.Form.Get("content"), @@ -128,7 +92,7 @@ func CreateComment(c *gin.Context) { // If the length of the comment is great enough, and the comment already exists we can safely assume it is spam. if len(a[1]) > 20 && len(*GetCommentsByContent(a[1], a[2])) > 0 { c.HTML(http.StatusNotAcceptable, "comment_post_failed.tmpl", CommentDto{Url: a[2]}) - return + c.Abort() } i, err := strconv.ParseInt(a[3], 10, 32) if err != nil || int(i) != (captchaVals[0]+captchaVals[1]) { @@ -160,6 +124,11 @@ func GetNumberOfRecentPosts(c *gin.Context) uint { return recentPosters[c.ClientIP()] } +func AddBlogPost(bp *models.BlogPost) bool { + err := db.Create(bp).Error + return err == nil +} + func DeleteBlogPostById(id string) bool { err := db.Model(&models.BlogPost{}).Delete(&models.BlogPost{Url: id}).Error return err == nil diff --git a/e b/e new file mode 100644 index 0000000..ff07b46 --- /dev/null +++ b/e @@ -0,0 +1,106 @@ + + +2022/03/02 07:29:33 [Recovery] 2022/03/02 - 07:29:33 panic recovered: +GET /admin/post HTTP/1.1 +Host: localhost:8080 +Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 +Accept-Encoding: gzip, deflate +Accept-Language: en-US,en;q=0.5 +Cache-Control: max-age=0 +Connection: keep-alive +Sec-Fetch-Dest: document +Sec-Fetch-Mode: navigate +Sec-Fetch-Site: none +Sec-Fetch-User: ?1 +Sec-Gpc: 1 +Upgrade-Insecure-Requests: 1 +User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0 + + +Cannot redirect with status code 406 +/home/rf/.go/pkg/mod/github.com/gin-gonic/gin@v1.7.7/render/redirect.go:22 (0x7ff947) + Redirect.Render: panic(fmt.Sprintf("Cannot redirect with status code %d", r.Code)) +/home/rf/.go/pkg/mod/github.com/gin-gonic/gin@v1.7.7/context.go:927 (0x806ea6) + (*Context).Render: if err := r.Render(c.Writer); err != nil { +/home/rf/.go/pkg/mod/github.com/gin-gonic/gin@v1.7.7/context.go:1008 (0x8cab1d) + (*Context).Redirect: c.Render(-1, render.Redirect{ +/home/rf/Projects/personal/site-rework/controllers/admin.go:23 (0x8caabd) + AdminOnly.func1: c.Redirect(http.StatusNotAcceptable, "/admin/") +/home/rf/.go/pkg/mod/github.com/gin-gonic/gin@v1.7.7/context.go:168 (0x80f321) + (*Context).Next: c.handlers[c.index](c) +/home/rf/.go/pkg/mod/github.com/gin-gonic/gin@v1.7.7/recovery.go:99 (0x80f30c) + CustomRecoveryWithWriter.func1: c.Next() +/home/rf/.go/pkg/mod/github.com/gin-gonic/gin@v1.7.7/context.go:168 (0x80f321) + (*Context).Next: c.handlers[c.index](c) +/home/rf/.go/pkg/mod/github.com/gin-gonic/gin@v1.7.7/recovery.go:99 (0x80f30c) + CustomRecoveryWithWriter.func1: c.Next() +/home/rf/.go/pkg/mod/github.com/gin-gonic/gin@v1.7.7/context.go:168 (0x80e586) + (*Context).Next: c.handlers[c.index](c) +/home/rf/.go/pkg/mod/github.com/gin-gonic/gin@v1.7.7/logger.go:241 (0x80e569) + LoggerWithConfig.func1: c.Next() +/home/rf/.go/pkg/mod/github.com/gin-gonic/gin@v1.7.7/context.go:168 (0x80dad0) + (*Context).Next: c.handlers[c.index](c) +/home/rf/.go/pkg/mod/github.com/gin-gonic/gin@v1.7.7/gin.go:555 (0x80d738) + (*Engine).handleHTTPRequest: c.Next() +/home/rf/.go/pkg/mod/github.com/gin-gonic/gin@v1.7.7/gin.go:511 (0x80d271) + (*Engine).ServeHTTP: engine.handleHTTPRequest(c) +/usr/lib/go/src/net/http/server.go:2879 (0x6b66da) + serverHandler.ServeHTTP: handler.ServeHTTP(rw, req) +/usr/lib/go/src/net/http/server.go:1930 (0x6b1d87) + (*conn).serve: serverHandler{c.server}.ServeHTTP(w, w.req) +/usr/lib/go/src/runtime/asm_amd64.s:1581 (0x464d80) + goexit: BYTE $0x90 // NOP + + + +2022/03/02 07:29:33 [Recovery] 2022/03/02 - 07:29:33 panic recovered: +GET /admin/post HTTP/1.1 +Host: localhost:8080 +Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 +Accept-Encoding: gzip, deflate +Accept-Language: en-US,en;q=0.5 +Cache-Control: max-age=0 +Connection: keep-alive +Sec-Fetch-Dest: document +Sec-Fetch-Mode: navigate +Sec-Fetch-Site: none +Sec-Fetch-User: ?1 +Sec-Gpc: 1 +Upgrade-Insecure-Requests: 1 +User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0 + + +Cannot redirect with status code 406 +/home/rf/.go/pkg/mod/github.com/gin-gonic/gin@v1.7.7/render/redirect.go:22 (0x7ff947) + Redirect.Render: panic(fmt.Sprintf("Cannot redirect with status code %d", r.Code)) +/home/rf/.go/pkg/mod/github.com/gin-gonic/gin@v1.7.7/context.go:927 (0x806ea6) + (*Context).Render: if err := r.Render(c.Writer); err != nil { +/home/rf/.go/pkg/mod/github.com/gin-gonic/gin@v1.7.7/context.go:1008 (0x8cab1d) + (*Context).Redirect: c.Render(-1, render.Redirect{ +/home/rf/Projects/personal/site-rework/controllers/admin.go:23 (0x8caabd) + AdminOnly.func1: c.Redirect(http.StatusNotAcceptable, "/admin/") +/home/rf/.go/pkg/mod/github.com/gin-gonic/gin@v1.7.7/context.go:168 (0x80f321) + (*Context).Next: c.handlers[c.index](c) +/home/rf/.go/pkg/mod/github.com/gin-gonic/gin@v1.7.7/recovery.go:99 (0x80f30c) + CustomRecoveryWithWriter.func1: c.Next() +/home/rf/.go/pkg/mod/github.com/gin-gonic/gin@v1.7.7/context.go:168 (0x80f321) + (*Context).Next: c.handlers[c.index](c) +/home/rf/.go/pkg/mod/github.com/gin-gonic/gin@v1.7.7/recovery.go:99 (0x80f30c) + CustomRecoveryWithWriter.func1: c.Next() +/home/rf/.go/pkg/mod/github.com/gin-gonic/gin@v1.7.7/context.go:168 (0x80e586) + (*Context).Next: c.handlers[c.index](c) +/home/rf/.go/pkg/mod/github.com/gin-gonic/gin@v1.7.7/logger.go:241 (0x80e569) + LoggerWithConfig.func1: c.Next() +/home/rf/.go/pkg/mod/github.com/gin-gonic/gin@v1.7.7/context.go:168 (0x80dad0) + (*Context).Next: c.handlers[c.index](c) +/home/rf/.go/pkg/mod/github.com/gin-gonic/gin@v1.7.7/gin.go:555 (0x80d738) + (*Engine).handleHTTPRequest: c.Next() +/home/rf/.go/pkg/mod/github.com/gin-gonic/gin@v1.7.7/gin.go:511 (0x80d271) + (*Engine).ServeHTTP: engine.handleHTTPRequest(c) +/usr/lib/go/src/net/http/server.go:2879 (0x6b66da) + serverHandler.ServeHTTP: handler.ServeHTTP(rw, req) +/usr/lib/go/src/net/http/server.go:1930 (0x6b1d87) + (*conn).serve: serverHandler{c.server}.ServeHTTP(w, w.req) +/usr/lib/go/src/runtime/asm_amd64.s:1581 (0x464d80) + goexit: BYTE $0x90 // NOP + diff --git a/templates/create_post.tmpl b/templates/create_post.tmpl new file mode 100644 index 0000000..1b7d72b --- /dev/null +++ b/templates/create_post.tmpl @@ -0,0 +1,10 @@ +{{ template "header.tmpl" . }} +

Create post

+
+ + +