Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add password protected pastes #26

Merged
merged 1 commit into from
Jul 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 18 additions & 18 deletions docker-compose-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ services:
networks:
- go-pb_dev
volumes:
- postgres-data:/var/lib/postgresql/data
- postgres-data-dev:/var/lib/postgresql/data
- ./scripts/db-healthcheck.sh:/db-healthcheck.sh
environment:
- PGDATA=/var/lib/postgresql/data/pgdata
Expand Down Expand Up @@ -50,22 +50,22 @@ services:
ports:
- 8888:8080

app:
image: iliafrenkel/go-pb:latest
container_name: app
depends_on:
db-server:
condition: service_healthy
restart: unless-stopped
networks:
- go-pb_dev
environment:
- GOPB_API_DB_CONN_STRING=host=db-server user=iliaf password=iliaf dbname=iliaf port=5432 sslmode=disable
- GOPB_WEB_LOGO=bighead.svg
- GOPB_WEB_HOST=0.0.0.0
- GOPB_API_TOKEN_SECRET=very#secret#api#token#donotshare
- GOPB_WEB_COOKIE_AUTH_KEY=verysecretcookieauthkeydontshare
ports:
- 8080:8080
# app:
# image: iliafrenkel/go-pb:latest
# container_name: app
# depends_on:
# db-server:
# condition: service_healthy
# restart: unless-stopped
# networks:
# - go-pb_dev
# environment:
# - GOPB_API_DB_CONN_STRING=host=db-server user=iliaf password=iliaf dbname=iliaf port=5432 sslmode=disable
# - GOPB_WEB_LOGO=bighead.svg
# - GOPB_WEB_HOST=0.0.0.0
# - GOPB_API_TOKEN_SECRET=very#secret#api#token#donotshare
# - GOPB_WEB_COOKIE_AUTH_KEY=verysecretcookieauthkeydontshare
# ports:
# - 8080:8080
##
###############################################################################
4 changes: 4 additions & 0 deletions src/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ type PasteForm struct {
UserID int64 `json:"user_id"`
}

type PastePassword struct {
Password string `json:"password" form:"password" binding:"required"`
}

// PasteService is the interface that defines methods for working with Pastes.
//
// Implementations should define the underlying storage such as database,
Expand Down
87 changes: 84 additions & 3 deletions src/api/http/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/iliafrenkel/go-pb/src/api"
"github.com/iliafrenkel/go-pb/src/api/base62"
"golang.org/x/crypto/bcrypt"
)

// APIServerOptions defines various parameters needed to run the ApiServer
Expand Down Expand Up @@ -69,6 +70,7 @@ type APIServer struct {
//
// The routes are:
// GET /paste/{id} - get paste by ID
// POST /paste/{id} - get password protected paste by ID
// POST /paste - create new paste
// DELETE /paste/{id} - delete paste by ID
// GET /paste/list/{id} - get a list of pastes by UserID
Expand Down Expand Up @@ -112,6 +114,7 @@ func New(pSvc api.PasteService, uSvc api.UserService, opts APIServerOptions) *AP
paste := handler.Router.Group("/paste")
{
paste.GET("/:id", handler.handlePasteGet)
paste.POST("/:id", handler.verifyJSONMiddleware(new(api.PastePassword)), handler.handlePasteGetWithPassword)
paste.POST("", handler.verifyJSONMiddleware(new(api.PasteForm)), handler.handlePasteCreate)
paste.DELETE("/:id", handler.handlePasteDelete)
paste.GET("/list/:id", handler.handlePasteList)
Expand Down Expand Up @@ -277,10 +280,12 @@ func (h *APIServer) verifyJSONMiddleware(data interface{}) gin.HandlerFunc {
}

// handlePasteGet is an HTTP handler for the GET /paste/{id} route, it returns
// the paste as a JSON string or 404 Not Found.
// the paste as a JSON string or 404 Not Found. If paste is password protected
// we return 401 Unauthorised and the caller has to POST to /paste/{id} to with
// the password to get the paste.
func (h *APIServer) handlePasteGet(c *gin.Context) {
// We expect the id parameter as base62 encoded string, we try to decode
// it into a uint64 paste id and return 404 if we can't.
// it into a int64 paste id and return 404 if we can't.
id, err := base62.Decode(c.Param("id"))
if err != nil {
c.JSON(http.StatusNotFound, api.HTTPError{
Expand All @@ -290,7 +295,9 @@ func (h *APIServer) handlePasteGet(c *gin.Context) {
return
}

p, err := h.PasteService.Get(int64(id))
p, err := h.PasteService.Get(id)
// Service returned an error and we don't know what to do here. We log the
// error and send 500 InternalServerError back to the caller.
if err != nil {
log.Println("handlePasteGet: unexpected error: ", err.Error())
c.JSON(http.StatusInternalServerError, api.HTTPError{
Expand All @@ -299,14 +306,25 @@ func (h *APIServer) handlePasteGet(c *gin.Context) {
})
return
}
// Service call was successful but returned nil. Respond with 404 NoFound.
if p == nil {
c.JSON(http.StatusNotFound, api.HTTPError{
Code: http.StatusNotFound,
Message: "Paste not found",
})
return
}
// Check if the paste is password protected. If yes, we return 401
// Unauthorized and the caller must POST back with the password.
if p.Password != "" {
c.JSON(http.StatusUnauthorized, api.HTTPError{
Code: http.StatusUnauthorized,
Message: "Paste is password protected",
})
return
}

// All is good, send the paste back to the caller.
c.JSON(http.StatusOK, p)

// We "burn" the paste if DeleteAfterRead flag is set.
Expand All @@ -315,6 +333,69 @@ func (h *APIServer) handlePasteGet(c *gin.Context) {
}
}

func (h *APIServer) handlePasteGetWithPassword(c *gin.Context) {
// We expect the id parameter as base62 encoded string, we try to decode
// it into a int64 paste id and return 404 if we can't.
id, err := base62.Decode(c.Param("id"))
if err != nil {
c.JSON(http.StatusNotFound, api.HTTPError{
Code: http.StatusNotFound,
Message: "Paste not found",
})
return
}
// Get the password from the context, the verifyJSONMiddlerware should've
// prepared it for us.
var pwd string
if data, ok := c.Get("payload"); !ok {
log.Println("handlePasteGetWithPassword: unexpected error: ", err.Error())
c.JSON(http.StatusInternalServerError, api.HTTPError{
Code: http.StatusInternalServerError,
Message: fmt.Sprintf("%s: %s", http.StatusText(http.StatusInternalServerError), err.Error()),
})
return
} else {
pwd = data.(*api.PastePassword).Password
}
// Get the paste
p, err := h.PasteService.Get(id)
// Service returned an error and we don't know what to do here. We log the
// error and send 500 InternalServerError back to the caller.
if err != nil {
log.Println("handlePasteGet: unexpected error: ", err.Error())
c.JSON(http.StatusInternalServerError, api.HTTPError{
Code: http.StatusInternalServerError,
Message: fmt.Sprintf("%s: %s", http.StatusText(http.StatusInternalServerError), err.Error()),
})
return
}
// Service call was successful but returned nil. Respond with 404 NoFound.
if p == nil {
c.JSON(http.StatusNotFound, api.HTTPError{
Code: http.StatusNotFound,
Message: "Paste not found",
})
return
}
// Verify the password
if err := bcrypt.CompareHashAndPassword([]byte(p.Password), []byte(pwd)); err != nil {
c.JSON(http.StatusUnauthorized, api.HTTPError{
Code: http.StatusUnauthorized,
Message: "Paste password is incorrect",
})
return
}

// All is good, send the paste back to the caller.
c.JSON(http.StatusOK, p)

// We "burn" the paste if DeleteAfterRead flag is set.
if p.DeleteAfterRead {
h.PasteService.Delete(p.ID)
}

}

// handlePasteCreate is an HTTP handler for the POST /paste route. It expects
// the new paste as a JSON sting in the body of the request. Returns newly
// created paste as a JSON string and the 'Location' header set to the new
Expand Down
10 changes: 9 additions & 1 deletion src/api/paste/memory/memory.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"time"

"github.com/iliafrenkel/go-pb/src/api"
"golang.org/x/crypto/bcrypt"
)

// PasteService stores all the pastes in memory and implements the
Expand Down Expand Up @@ -81,7 +82,14 @@ func (s *PasteService) Create(p api.PasteForm) (*api.Paste, error) {
return nil, fmt.Errorf("unknown duration format: %s", p.Expires)
}
}
// Create new paste with a randomly generated ID
// Create new paste with a randomly generated ID and a hashed password.
if p.Password != "" {
hash, err := bcrypt.GenerateFromPassword([]byte(p.Password), bcrypt.DefaultCost)
if err != nil {
return nil, err
}
p.Password = string(hash)
}
newPaste := api.Paste{
ID: rand.Int63(),
Title: p.Title,
Expand Down
10 changes: 9 additions & 1 deletion src/api/paste/sqldb/sqldb.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"time"

"github.com/iliafrenkel/go-pb/src/api"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)

Expand Down Expand Up @@ -104,7 +105,14 @@ func (s *PasteService) Create(p api.PasteForm) (*api.Paste, error) {
return nil, fmt.Errorf("unknown duration format: %s", p.Expires)
}
}
// Create new paste with a randomly generated ID
// Create new paste with a randomly generated ID and a hashed password.
if p.Password != "" {
hash, err := bcrypt.GenerateFromPassword([]byte(p.Password), bcrypt.DefaultCost)
if err != nil {
return nil, err
}
p.Password = string(hash)
}
newPaste := api.Paste{
ID: rand.Int63(),
Title: p.Title,
Expand Down
Loading