330 lines
11 KiB
Go
330 lines
11 KiB
Go
package pastes
|
|
|
|
import (
|
|
// stdlib
|
|
"bytes"
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
|
|
// local
|
|
"gitlab.com/pztrn/fastpastebin/internal/structs"
|
|
"gitlab.com/pztrn/fastpastebin/internal/templater"
|
|
|
|
// other
|
|
"github.com/alecthomas/chroma"
|
|
"github.com/alecthomas/chroma/formatters"
|
|
htmlfmt "github.com/alecthomas/chroma/formatters/html"
|
|
"github.com/alecthomas/chroma/lexers"
|
|
"github.com/alecthomas/chroma/styles"
|
|
"github.com/labstack/echo"
|
|
)
|
|
|
|
const (
|
|
pasteCookieInvalid = "PASTE_COOKIE_INVALID"
|
|
pasteExpired = "PASTE_EXPIRED"
|
|
pasteNotFound = "PASTE_NOT_FOUND"
|
|
pasteTimestampInvalid = "PASTE_TIMESTAMP_INVALID"
|
|
)
|
|
|
|
// Actual getting paste data and returns it's content without formatting.
|
|
// This function will return paste's structure and optional error string
|
|
// that defined in constants above.
|
|
// Actually required only paste ID, all other parameters are optional
|
|
// for some cases, e.g. public paste won't check for timestamp and cookie
|
|
// value (they both will be ignored), but private will.
|
|
func pasteGetData(pasteID int, timestamp int64, cookieValue string) (*structs.Paste, string) {
|
|
// We should check if database connection available.
|
|
//dbConn := c.Database.GetDatabaseConnection()
|
|
//if c.Config.Database.Type != "flatfiles" && dbConn == nil {
|
|
// return ec.Redirect(http.StatusFound, "/database_not_available")
|
|
//}
|
|
|
|
// Get paste.
|
|
paste, err1 := c.Database.GetPaste(pasteID)
|
|
if err1 != nil {
|
|
c.Logger.Error().Msgf("Failed to get paste #%d: %s", pasteID, err1.Error())
|
|
return nil, pasteNotFound
|
|
}
|
|
|
|
// Check if paste is expired.
|
|
if paste.IsExpired() {
|
|
c.Logger.Error().Msgf("Paste #%d is expired", pasteID)
|
|
return nil, pasteExpired
|
|
}
|
|
|
|
// Check if we have a private paste and it's parameters are correct.
|
|
if paste.Private {
|
|
pasteTs := paste.CreatedAt.Unix()
|
|
if timestamp != pasteTs {
|
|
c.Logger.Error().Msgf("Incorrect timestamp '%d' provided for private paste #%d, waiting for %d", timestamp, pasteID, pasteTs)
|
|
return nil, pasteTimestampInvalid
|
|
}
|
|
}
|
|
|
|
// If we have a private paste requested and password for that paste
|
|
// was defined - check additional things that required to view this
|
|
// paste.
|
|
if paste.Private && paste.Password != "" {
|
|
// Generate cookie value to check.
|
|
pasteCookieValue := paste.GenerateCryptedCookieValue()
|
|
|
|
if cookieValue != pasteCookieValue {
|
|
return nil, pasteCookieInvalid
|
|
}
|
|
}
|
|
|
|
return paste, ""
|
|
}
|
|
|
|
// GET for "/api/paste/PASTE_ID" and "/api/paste/PASTE_ID/TIMESTAMP".
|
|
func pasteGETApi(ec echo.Context) error {
|
|
return nil
|
|
}
|
|
|
|
// GET for "/paste/PASTE_ID" and "/paste/PASTE_ID/TIMESTAMP" (private pastes).
|
|
// Web interface version.
|
|
func pasteGETWebInterface(ec echo.Context) error {
|
|
pasteIDRaw := ec.Param("id")
|
|
// We already get numbers from string, so we will not check strconv.Atoi()
|
|
// error.
|
|
pasteID, _ := strconv.Atoi(regexInts.FindAllString(pasteIDRaw, 1)[0])
|
|
pasteIDStr := strconv.Itoa(pasteID)
|
|
c.Logger.Debug().Msgf("Trying to get paste #%+v", pasteID)
|
|
|
|
// Check if we have timestamp passed.
|
|
// If passed timestamp is invalid (isn't a real UNIX timestamp) we
|
|
// will show 404 Not Found error and spam about that in logs.
|
|
var timestamp int64
|
|
tsProvidedStr := ec.Param("timestamp")
|
|
if tsProvidedStr != "" {
|
|
tsProvided, err := strconv.ParseInt(tsProvidedStr, 10, 64)
|
|
if err != nil {
|
|
c.Logger.Error().Err(err).Msgf("Invalid timestamp '%s' provided for getting private paste #%d", tsProvidedStr, pasteID)
|
|
errtpl := templater.GetErrorTemplate(ec, "Paste #"+pasteIDStr+" not found")
|
|
return ec.HTML(http.StatusBadRequest, errtpl)
|
|
} else {
|
|
timestamp = tsProvided
|
|
}
|
|
}
|
|
|
|
// Check if we have "PASTE-PASTEID" cookie defined. It is required
|
|
// for private pastes.
|
|
var cookieValue string
|
|
cookie, err1 := ec.Cookie("PASTE-" + pasteIDStr)
|
|
if err1 == nil {
|
|
cookieValue = cookie.Value
|
|
}
|
|
|
|
paste, error := pasteGetData(pasteID, timestamp, cookieValue)
|
|
|
|
// For these cases we should return 404 Not Found page.
|
|
if error == pasteExpired || error == pasteNotFound || error == pasteTimestampInvalid {
|
|
errtpl := templater.GetErrorTemplate(ec, "Paste #"+pasteIDRaw+" not found")
|
|
return ec.HTML(http.StatusNotFound, errtpl)
|
|
}
|
|
|
|
// If passed cookie value was invalid - go to paste authorization
|
|
// page.
|
|
if error == pasteCookieInvalid {
|
|
c.Logger.Info().Msg("Invalid cookie, redirecting to auth page...")
|
|
return ec.Redirect(http.StatusMovedPermanently, "/paste/"+pasteIDStr+"/"+ec.Param("timestamp")+"/verify")
|
|
}
|
|
|
|
// Format paste data map.
|
|
pasteData := make(map[string]string)
|
|
pasteData["pasteTitle"] = paste.Title
|
|
pasteData["pasteID"] = strconv.Itoa(paste.ID)
|
|
pasteData["pasteDate"] = paste.CreatedAt.Format("2006-01-02 @ 15:04:05") + " UTC"
|
|
pasteData["pasteLanguage"] = paste.Language
|
|
|
|
pasteExpirationString := "Never"
|
|
if paste.KeepFor != 0 && paste.KeepForUnitType != 0 {
|
|
pasteExpirationString = paste.GetExpirationTime().Format("2006-01-02 @ 15:04:05") + " UTC"
|
|
}
|
|
pasteData["pasteExpiration"] = pasteExpirationString
|
|
|
|
if paste.Private {
|
|
pasteData["pasteType"] = "<span class='has-text-danger'>Private</span>"
|
|
pasteData["pasteTs"] = strconv.FormatInt(paste.CreatedAt.Unix(), 10) + "/"
|
|
} else {
|
|
pasteData["pasteType"] = "<span class='has-text-success'>Public</span>"
|
|
pasteData["pasteTs"] = ""
|
|
}
|
|
|
|
// Highlight.
|
|
// Get lexer.
|
|
lexer := lexers.Get(paste.Language)
|
|
if lexer == nil {
|
|
lexer = lexers.Fallback
|
|
}
|
|
// Tokenize paste data.
|
|
lexered, err3 := lexer.Tokenise(nil, paste.Data)
|
|
if err3 != nil {
|
|
c.Logger.Error().Msgf("Failed to tokenize paste data: %s", err3.Error())
|
|
}
|
|
// Get style for HTML output.
|
|
style := styles.Get("monokai")
|
|
if style == nil {
|
|
style = styles.Fallback
|
|
}
|
|
// Get HTML formatter.
|
|
formatter := chroma.Formatter(htmlfmt.New(htmlfmt.WithLineNumbers(), htmlfmt.LineNumbersInTable()))
|
|
if formatter == nil {
|
|
formatter = formatters.Fallback
|
|
}
|
|
// Create buffer and format into it.
|
|
buf := new(bytes.Buffer)
|
|
err4 := formatter.Format(buf, style, lexered)
|
|
if err4 != nil {
|
|
c.Logger.Error().Msgf("Failed to format paste data: %s", err4.Error())
|
|
}
|
|
pasteData["pastedata"] = buf.String()
|
|
|
|
// Get template and format it.
|
|
pasteHTML := templater.GetTemplate(ec, "paste.html", pasteData)
|
|
|
|
return ec.HTML(http.StatusOK, pasteHTML)
|
|
}
|
|
|
|
// GET for "/paste/PASTE_ID/TIMESTAMP/verify" - a password verify page.
|
|
func pastePasswordedVerifyGet(ec echo.Context) error {
|
|
pasteIDRaw := ec.Param("id")
|
|
timestampRaw := ec.Param("timestamp")
|
|
// We already get numbers from string, so we will not check strconv.Atoi()
|
|
// error.
|
|
pasteID, _ := strconv.Atoi(regexInts.FindAllString(pasteIDRaw, 1)[0])
|
|
|
|
// Get paste.
|
|
paste, err1 := c.Database.GetPaste(pasteID)
|
|
if err1 != nil {
|
|
c.Logger.Error().Msgf("Failed to get paste #%d: %s", pasteID, err1.Error())
|
|
errtpl := templater.GetErrorTemplate(ec, "Paste #"+pasteIDRaw+" not found")
|
|
return ec.HTML(http.StatusBadRequest, errtpl)
|
|
}
|
|
|
|
// Check for auth cookie. If present - redirect to paste.
|
|
cookie, err := ec.Cookie("PASTE-" + strconv.Itoa(pasteID))
|
|
if err == nil {
|
|
// No cookie, redirect to auth page.
|
|
c.Logger.Debug().Msg("Paste cookie found, checking it...")
|
|
|
|
// Generate cookie value to check.
|
|
cookieValue := paste.GenerateCryptedCookieValue()
|
|
|
|
if cookieValue == cookie.Value {
|
|
c.Logger.Info().Msg("Valid cookie, redirecting to paste page...")
|
|
return ec.Redirect(http.StatusMovedPermanently, "/paste/"+pasteIDRaw+"/"+ec.Param("timestamp"))
|
|
}
|
|
|
|
c.Logger.Debug().Msg("Invalid cookie, showing auth page")
|
|
}
|
|
|
|
// HTML data.
|
|
htmlData := make(map[string]string)
|
|
htmlData["pasteID"] = strconv.Itoa(pasteID)
|
|
htmlData["pasteTimestamp"] = timestampRaw
|
|
|
|
verifyHTML := templater.GetTemplate(ec, "passworded_paste_verify.html", htmlData)
|
|
|
|
return ec.HTML(http.StatusOK, verifyHTML)
|
|
}
|
|
|
|
// POST for "/paste/PASTE_ID/TIMESTAMP/verify" - a password verify page.
|
|
func pastePasswordedVerifyPost(ec echo.Context) error {
|
|
// We should check if database connection available.
|
|
dbConn := c.Database.GetDatabaseConnection()
|
|
if c.Config.Database.Type != "flatfiles" && dbConn == nil {
|
|
return ec.Redirect(http.StatusFound, "/database_not_available")
|
|
}
|
|
|
|
pasteIDRaw := ec.Param("id")
|
|
timestampRaw := ec.Param("timestamp")
|
|
// We already get numbers from string, so we will not check strconv.Atoi()
|
|
// error.
|
|
pasteID, _ := strconv.Atoi(regexInts.FindAllString(pasteIDRaw, 1)[0])
|
|
c.Logger.Debug().Msgf("Requesting paste #%+v", pasteID)
|
|
|
|
// Get paste.
|
|
paste, err1 := c.Database.GetPaste(pasteID)
|
|
if err1 != nil {
|
|
c.Logger.Error().Msgf("Failed to get paste #%d: %s", pasteID, err1.Error())
|
|
errtpl := templater.GetErrorTemplate(ec, "Paste #"+strconv.Itoa(pasteID)+" not found")
|
|
return ec.HTML(http.StatusBadRequest, errtpl)
|
|
}
|
|
|
|
params, err2 := ec.FormParams()
|
|
if err2 != nil {
|
|
c.Logger.Debug().Msg("No form parameters passed")
|
|
errtpl := templater.GetErrorTemplate(ec, "Paste #"+strconv.Itoa(pasteID)+" not found")
|
|
return ec.HTML(http.StatusBadRequest, errtpl)
|
|
}
|
|
|
|
if paste.VerifyPassword(params["paste-password"][0]) {
|
|
// Set cookie that this paste's password is verified and paste
|
|
// can be viewed.
|
|
cookie := new(http.Cookie)
|
|
cookie.Name = "PASTE-" + strconv.Itoa(pasteID)
|
|
cookie.Value = paste.GenerateCryptedCookieValue()
|
|
cookie.Expires = time.Now().Add(24 * time.Hour)
|
|
ec.SetCookie(cookie)
|
|
|
|
return ec.Redirect(http.StatusFound, "/paste/"+strconv.Itoa(pasteID)+"/"+timestampRaw)
|
|
}
|
|
|
|
errtpl := templater.GetErrorTemplate(ec, "Invalid password. Please, try again.")
|
|
return ec.HTML(http.StatusBadRequest, string(errtpl))
|
|
}
|
|
|
|
// GET for "/pastes/:id/raw", raw paste output.
|
|
// Web interface version.
|
|
func pasteRawGETWebInterface(ec echo.Context) error {
|
|
// We should check if database connection available.
|
|
dbConn := c.Database.GetDatabaseConnection()
|
|
if c.Config.Database.Type != "flatfiles" && dbConn == nil {
|
|
return ec.Redirect(http.StatusFound, "/database_not_available/raw")
|
|
}
|
|
|
|
pasteIDRaw := ec.Param("id")
|
|
// We already get numbers from string, so we will not check strconv.Atoi()
|
|
// error.
|
|
pasteID, _ := strconv.Atoi(regexInts.FindAllString(pasteIDRaw, 1)[0])
|
|
c.Logger.Debug().Msgf("Requesting paste #%+v", pasteID)
|
|
|
|
// Get paste.
|
|
paste, err1 := c.Database.GetPaste(pasteID)
|
|
if err1 != nil {
|
|
c.Logger.Error().Msgf("Failed to get paste #%d from database: %s", pasteID, err1.Error())
|
|
return ec.HTML(http.StatusBadRequest, "Paste #"+pasteIDRaw+" does not exist.")
|
|
}
|
|
|
|
if paste.IsExpired() {
|
|
c.Logger.Error().Msgf("Paste #%d is expired", pasteID)
|
|
return ec.HTML(http.StatusBadRequest, "Paste #"+pasteIDRaw+" does not exist.")
|
|
}
|
|
|
|
// Check if we have a private paste and it's parameters are correct.
|
|
if paste.Private {
|
|
tsProvidedStr := ec.Param("timestamp")
|
|
tsProvided, err2 := strconv.ParseInt(tsProvidedStr, 10, 64)
|
|
if err2 != nil {
|
|
c.Logger.Error().Msgf("Invalid timestamp '%s' provided for getting private paste #%d: %s", tsProvidedStr, pasteID, err2.Error())
|
|
return ec.String(http.StatusBadRequest, "Paste #"+pasteIDRaw+" not found")
|
|
}
|
|
pasteTs := paste.CreatedAt.Unix()
|
|
if tsProvided != pasteTs {
|
|
c.Logger.Error().Msgf("Incorrect timestamp '%v' provided for private paste #%d, waiting for %v", tsProvidedStr, pasteID, strconv.FormatInt(pasteTs, 10))
|
|
return ec.String(http.StatusBadRequest, "Paste #"+pasteIDRaw+" not found")
|
|
}
|
|
}
|
|
|
|
// ToDo: figure out how to handle passworded pastes here.
|
|
// Return error for now.
|
|
if paste.Password != "" {
|
|
c.Logger.Error().Msgf("Cannot render paste #%d as raw: passworded paste. Patches welcome!", pasteID)
|
|
return ec.String(http.StatusBadRequest, "Paste #"+pasteIDRaw+" not found")
|
|
}
|
|
|
|
return ec.String(http.StatusOK, paste.Data)
|
|
}
|