Small temp redis-based pastebin server.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

295 lines
7.5 KiB

package main
import (
"fmt"
"html/template"
"net/http"
"net/url"
"strings"
"time"
"github.com/gorilla/mux"
"gitlab.com/Artemix/paste/storage"
"gitlab.com/Artemix/validator-go"
"gitlab.com/Artemix/validator-go/rules"
)
// region Data Structures and types
// Handlers is the base request handlers struct, including dependencies
type Handlers struct {
BackendService storage.Backend
Templates *template.Template
Renderers map[string]Renderer
}
// Content is the response data structure
type Content struct {
Error string
Value string
ViewMode ViewMode
Code string
OriginalCommand PasteSubmitCommand
}
type ViewMode struct {
Value string
}
func (v ViewMode) Is(value string) bool {
return v.Value == value
}
func mode(viewMode string) ViewMode {
return ViewMode{Value: viewMode}
}
// PasteSubmitCommand is the paste upload command
type PasteSubmitCommand struct {
Paste string
TimeStr string
ExpirationTime time.Duration
}
// PasteSubmitSchema is the schema used to validate form submission
var PasteSubmitSchema = map[string][]validator.Rule{
"paste": {rules.Required{}},
"time": {rules.Required{}, rules.Choice{Choices: []string{"1h", "6h", "12h", "24h"}}},
}
// GenericError is representing generic HTTP errors
type GenericError struct {
Title string
Description string
}
var genericErrors = map[int]GenericError{
http.StatusBadRequest: {"Bad request", "You sent a request the server cannot handle."},
http.StatusNotFound: {"Page not found", "No page could be found at the given URL"},
http.StatusInternalServerError: {"Internal server error", "The server crashed during the request."},
}
// endregion
// region Router
// GetRouter builds a HTTP router based on Gorilla/Mux, to handle
// HTTP traffic
func (h Handlers) GetRouter() *mux.Router {
h.Renderers = map[string]Renderer{
"show": h.viewAsCode,
"raw": h.viewAsRaw,
"markdown": h.viewAsMarkdown,
}
r := mux.NewRouter()
r.Use(LogRequest)
r.NotFoundHandler = http.HandlerFunc(h.handle404)
r.HandleFunc("/", h.handleNewPaste).
Methods(http.MethodPost)
r.Use(OpenCORSMiddleware("/"))
r.HandleFunc("/", h.handleHome).
Methods(http.MethodGet)
r.HandleFunc("/about", h.handleAbout).Methods(http.MethodGet)
r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("./static"))))
r.HandleFunc("/-/{key}", h.handlePasteRetrieval).Methods(http.MethodGet)
return r
}
// endregion
// region Middlewares
// OpenCORSMiddleware accepts requests from everywhere, on the
// OPTIONS and POST methods only.
func OpenCORSMiddleware(route string) mux.MiddlewareFunc {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
if request.Method == http.MethodPost && request.RequestURI == route {
for key, value := range map[string]string{
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "OPTIONS, POST",
} {
response.Header().Set(key, value)
}
}
next.ServeHTTP(response, request)
})
}
}
// endregion
// region Static pages
func (h Handlers) handleHome(w http.ResponseWriter, req *http.Request) {
ErrIf(h.Templates.ExecuteTemplate(w, "homepage", nil))
}
func (h Handlers) handleAbout(w http.ResponseWriter, req *http.Request) {
ErrIf(h.Templates.ExecuteTemplate(w, "about", nil))
}
func (h Handlers) handle404(w http.ResponseWriter, req *http.Request) {
h.GenericHTTPError(404, nil)(w, req)
}
// endregion
// region Form validators
// ValidateAndTransformPasteSubmitCommand validates the form based on the
// PasteSubmit schema, And fills a PasteSubmit command with associated values
func ValidateAndTransformPasteSubmitCommand(form url.Values) (
PasteSubmitCommand, validator.Errors) {
form.Set("paste", strings.TrimSpace(form.Get("paste")))
values, validationErrors := validator.Validator{Schema: PasteSubmitSchema}.Validate(form)
command := PasteSubmitCommand{
Paste: values.Get("paste"),
TimeStr: values.Get("time"),
ExpirationTime: 0,
}
if "" != command.TimeStr {
keepaliveTime := time.Hour
switch command.TimeStr {
case "6h":
keepaliveTime *= 6
case "12h":
keepaliveTime *= 12
case "24h":
keepaliveTime *= 24
}
command.ExpirationTime = keepaliveTime
}
return command, validationErrors
}
// endregion
// region Paste ephemeral handling
func (h Handlers) handleNewPaste(w http.ResponseWriter, req *http.Request) {
err := req.ParseForm()
if nil != err {
h.PasteSubmitError(400, err, &Content{
Error: "Form submit error",
})(w, req)
return
}
command, validationErrors := ValidateAndTransformPasteSubmitCommand(req.Form)
if nil != validationErrors &&
0 < len(validationErrors) {
h.PasteSubmitError(400, validationErrors, &Content{
Error: "You must provide a document and a valid time limit",
OriginalCommand: command,
})(w, req)
return
}
key := GetNewKey()
err = h.BackendService.
Persist(key, command.Paste, command.ExpirationTime)
if nil != err {
h.GenericHTTPError(500, err.Error())(w, req)
return
}
http.Redirect(w, req, "/-/"+key, http.StatusFound)
_, err = fmt.Fprint(w, key)
ErrIf(err)
}
func (h Handlers) handlePasteRetrieval(w http.ResponseWriter, req *http.Request) {
key := mux.Vars(req)["key"]
res, err := h.BackendService.Retrieve(key)
viewMode := req.URL.Query().Get("view")
if "" == viewMode {
viewMode = "show"
}
if (!rules.Choice{Choices: []string{"show", "raw", "markdown"}}.Validate(viewMode)) {
h.GenericHTTPError(400, fmt.Sprintf("Unknown view mode %s", viewMode))(w, req)
} else if nil != err {
h.GenericHTTPError(500, err.Error())(w, req)
} else if res == "" {
h.GenericHTTPError(404, nil)(w, req)
} else {
ErrIf(h.Renderers[viewMode](w, key, res))
}
}
type Renderer func(w http.ResponseWriter, key string, value string) error
func (h Handlers) viewAsCode(w http.ResponseWriter, key string, value string) error {
return h.Templates.ExecuteTemplate(w, "show", &Content{
Value: value,
Code: key,
ViewMode: mode("show"),
})
}
func (h Handlers) viewAsMarkdown(w http.ResponseWriter, key string, value string) error {
return h.Templates.ExecuteTemplate(w, "markdown", &Content{
Value: value,
Code: key,
ViewMode: mode("markdown"),
})
}
func (h Handlers) viewAsRaw(w http.ResponseWriter, key string, value string) error {
w.Header().Set("Content-Type", "text/plain; charset=utf8")
_, err := fmt.Fprint(w, value)
return err
}
// endregion
// region Error handlers
// GenericHTTPError takes a status code and an error, and
// - logs the error using the global ErrIf method
// - writes the generic template `http-error` with the status code and message
func (h *Handlers) GenericHTTPError(
statusCode int,
error interface{},
) func(w http.ResponseWriter, req *http.Request) {
ErrIf(error)
return func(w http.ResponseWriter, req *http.Request) {
w.WriteHeader(statusCode)
ErrIf(h.Templates.ExecuteTemplate(w, "http-error", map[string]interface{}{
"Error": genericErrors[statusCode],
}))
}
}
// PasteSubmitError takes a status code and a content dataset, and
// - logs the content.Error value using the global ErrIf method
// - writes the homepage template with the content
func (h *Handlers) PasteSubmitError(
statusCode int,
original interface{},
content *Content,
) func(w http.ResponseWriter, req *http.Request) {
ErrIf(original)
return func(w http.ResponseWriter, req *http.Request) {
w.WriteHeader(statusCode)
ErrIf(h.Templates.ExecuteTemplate(w, "homepage", content))
}
}
// endregion