Refactoring for pastes domain for code reusing and better structuring.

This commit is contained in:
Stanislav Nikitin 2019-04-12 23:29:42 +05:00
parent 19b5ef3d9f
commit 7281b9be65
No known key found for this signature in database
GPG Key ID: 106900B32F8192EE
7 changed files with 582 additions and 508 deletions

View File

@ -1,499 +0,0 @@
// Fast Paste Bin - uberfast and easy-to-use pastebin.
//
// Copyright (c) 2018, Stanislav N. aka pztrn and Fast Paste Bin
// developers.
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject
// to the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package pastes
import (
// stdlib
"bytes"
"net/http"
"regexp"
"strconv"
"strings"
"time"
// local
"gitlab.com/pztrn/fastpastebin/internal/captcha"
"gitlab.com/pztrn/fastpastebin/internal/pagination"
"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"
//"gitlab.com/dchest/captcha"
"github.com/labstack/echo"
)
var (
regexInts = regexp.MustCompile("[0-9]+")
)
// GET for "/paste/PASTE_ID" and "/paste/PASTE_ID/TIMESTAMP" (private pastes).
func pasteGET(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")
// 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)
}
if paste.IsExpired() {
c.Logger.Error().Msgf("Paste #%d is expired", pasteID)
errtpl := templater.GetErrorTemplate(ec, "Paste #"+strconv.Itoa(pasteID)+" not found")
return ec.HTML(http.StatusBadRequest, errtpl)
}
// 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())
errtpl := templater.GetErrorTemplate(ec, "Paste #"+strconv.Itoa(pasteID)+" not found")
return ec.HTML(http.StatusBadRequest, errtpl)
}
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))
errtpl := templater.GetErrorTemplate(ec, "Paste #"+strconv.Itoa(pasteID)+" not found")
return ec.HTML(http.StatusBadRequest, errtpl)
}
}
if paste.Private && paste.Password != "" {
// Check if cookie for this paste is defined. This means that user
// previously successfully entered a password.
cookie, err := ec.Cookie("PASTE-" + strconv.Itoa(pasteID))
if err != nil {
// No cookie, redirect to auth page.
c.Logger.Info().Msg("Tried to access passworded paste without autorization, redirecting to auth page...")
return ec.Redirect(http.StatusMovedPermanently, "/paste/"+pasteIDRaw+"/"+ec.Param("timestamp")+"/verify")
}
// Generate cookie value to check.
cookieValue := paste.GenerateCryptedCookieValue()
if cookieValue != cookie.Value {
c.Logger.Info().Msg("Invalid cookie, redirecting to auth page...")
return ec.Redirect(http.StatusMovedPermanently, "/paste/"+pasteIDRaw+"/"+ec.Param("timestamp")+"/verify")
}
// If all okay - do nothing :)
}
// 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 {
// 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])
// 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))
}
// POST for "/paste/" which will create new paste and redirect to
// "/pastes/CREATED_PASTE_ID".
func pastePOST(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")
}
params, err := ec.FormParams()
if err != nil {
errtpl := templater.GetErrorTemplate(ec, "Cannot create empty paste")
return ec.HTML(http.StatusBadRequest, errtpl)
}
c.Logger.Debug().Msgf("Received parameters: %+v", params)
// Do nothing if paste contents is empty.
if len(params["paste-contents"][0]) == 0 {
c.Logger.Debug().Msg("Empty paste submitted, ignoring")
errtpl := templater.GetErrorTemplate(ec, "Empty pastes aren't allowed.")
return ec.HTML(http.StatusBadRequest, errtpl)
}
if !strings.ContainsAny(params["paste-keep-for"][0], "Mmhd") && params["paste-keep-for"][0] != "forever" {
c.Logger.Debug().Msgf("'Keep paste for' field have invalid value: %s", params["paste-keep-for"][0])
errtpl := templater.GetErrorTemplate(ec, "Invalid 'Paste should be available for' parameter passed. Please do not try to hack us ;).")
return ec.HTML(http.StatusBadRequest, errtpl)
}
// Verify captcha.
if !captcha.Verify(params["paste-captcha-id"][0], params["paste-captcha-solution"][0]) {
c.Logger.Debug().Msgf("Invalid captcha solution for captcha ID '%s': %s", params["paste-captcha-id"][0], params["paste-captcha-solution"][0])
errtpl := templater.GetErrorTemplate(ec, "Invalid captcha solution.")
return ec.HTML(http.StatusBadRequest, errtpl)
}
paste := &structs.Paste{
Title: params["paste-title"][0],
Data: params["paste-contents"][0],
Language: params["paste-language"][0],
}
// Paste creation time in UTC.
createdAt := time.Now().UTC()
paste.CreatedAt = &createdAt
// Parse "keep for" field.
// Defaulting to "forever".
keepFor := 0
keepForUnit := 0
if params["paste-keep-for"][0] != "forever" {
keepForUnitRegex := regexp.MustCompile("[Mmhd]")
keepForRaw := regexInts.FindAllString(params["paste-keep-for"][0], 1)[0]
var err error
keepFor, err = strconv.Atoi(keepForRaw)
if err != nil {
if params["paste-keep-for"][0] == "forever" {
c.Logger.Debug().Msg("Keeping paste forever!")
keepFor = 0
} else {
c.Logger.Debug().Err(err).Msg("Failed to parse 'Keep for' integer")
errtpl := templater.GetErrorTemplate(ec, "Invalid 'Paste should be available for' parameter passed. Please do not try to hack us ;).")
return ec.HTML(http.StatusBadRequest, errtpl)
}
}
keepForUnitRaw := keepForUnitRegex.FindAllString(params["paste-keep-for"][0], 1)[0]
keepForUnit = structs.PASTE_KEEPS_CORELLATION[keepForUnitRaw]
}
paste.KeepFor = keepFor
paste.KeepForUnitType = keepForUnit
// Try to autodetect if it was selected.
if params["paste-language"][0] == "autodetect" {
lexer := lexers.Analyse(params["paste-language"][0])
if lexer != nil {
paste.Language = lexer.Config().Name
} else {
paste.Language = "text"
}
}
// Private paste?
paste.Private = false
privateCheckbox, privateCheckboxFound := params["paste-private"]
pastePassword, pastePasswordFound := params["paste-password"]
if privateCheckboxFound && privateCheckbox[0] == "on" || pastePasswordFound && pastePassword[0] != "" {
paste.Private = true
}
if pastePassword[0] != "" {
paste.CreatePassword(pastePassword[0])
}
id, err2 := c.Database.SavePaste(paste)
if err2 != nil {
c.Logger.Error().Msgf("Failed to save paste: %s", err2.Error())
errtpl := templater.GetErrorTemplate(ec, "Failed to save paste. Please, try again later.")
return ec.HTML(http.StatusBadRequest, errtpl)
}
newPasteIDAsString := strconv.FormatInt(id, 10)
c.Logger.Debug().Msgf("Paste saved, URL: /paste/" + newPasteIDAsString)
// Private pastes have it's timestamp in URL.
if paste.Private {
return ec.Redirect(http.StatusFound, "/paste/"+newPasteIDAsString+"/"+strconv.FormatInt(paste.CreatedAt.Unix(), 10))
}
return ec.Redirect(http.StatusFound, "/paste/"+newPasteIDAsString)
}
// GET for "/pastes/:id/raw", raw paste output.
func pasteRawGET(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)
}
// GET for "/pastes/", a list of publicly available pastes.
func pastesGET(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")
}
pageFromParamRaw := ec.Param("page")
var page = 1
if pageFromParamRaw != "" {
pageRaw := regexInts.FindAllString(pageFromParamRaw, 1)[0]
page, _ = strconv.Atoi(pageRaw)
}
c.Logger.Debug().Msgf("Requested page #%d", page)
// Get pastes IDs.
pastes, err3 := c.Database.GetPagedPastes(page)
c.Logger.Debug().Msgf("Got %d pastes", len(pastes))
var pastesString = "No pastes to show."
// Show "No pastes to show" on any error for now.
if err3 != nil {
c.Logger.Error().Msgf("Failed to get pastes list from database: %s", err3.Error())
noPastesToShowTpl := templater.GetErrorTemplate(ec, "No pastes to show.")
return ec.HTML(http.StatusOK, noPastesToShowTpl)
}
if len(pastes) > 0 {
pastesString = ""
for i := range pastes {
pasteDataMap := make(map[string]string)
pasteDataMap["pasteID"] = strconv.Itoa(pastes[i].ID)
pasteDataMap["pasteTitle"] = pastes[i].Title
pasteDataMap["pasteDate"] = pastes[i].CreatedAt.Format("2006-01-02 @ 15:04:05") + " UTC"
// Get max 4 lines of each paste.
pasteDataSplitted := strings.Split(pastes[i].Data, "\n")
var pasteData = ""
if len(pasteDataSplitted) < 4 {
pasteData = pastes[i].Data
} else {
pasteData = strings.Join(pasteDataSplitted[0:4], "\n")
}
pasteDataMap["pasteData"] = pasteData
pasteTpl := templater.GetRawTemplate(ec, "pastelist_paste.html", pasteDataMap)
pastesString += pasteTpl
}
}
// Pagination.
pages := c.Database.GetPastesPages()
c.Logger.Debug().Msgf("Total pages: %d, current: %d", pages, page)
paginationHTML := pagination.CreateHTML(page, pages, "/pastes/")
pasteListTpl := templater.GetTemplate(ec, "pastelist_list.html", map[string]string{"pastes": pastesString, "pagination": paginationHTML})
return ec.HTML(http.StatusOK, string(pasteListTpl))
}

View File

@ -25,10 +25,17 @@
package pastes package pastes
import ( import (
// stdlib
"regexp"
// local // local
"gitlab.com/pztrn/fastpastebin/internal/context" "gitlab.com/pztrn/fastpastebin/internal/context"
) )
var (
regexInts = regexp.MustCompile("[0-9]+")
)
var ( var (
c *context.Context c *context.Context
) )
@ -38,22 +45,22 @@ var (
func New(cc *context.Context) { func New(cc *context.Context) {
c = cc c = cc
////////////////////////////////////////////////////////////
// HTTP endpoints.
////////////////////////////////////////////////////////////
// New paste. // New paste.
c.Echo.POST("/paste/", pastePOST) c.Echo.POST("/paste/", pastePOSTWebInterface)
// Show public paste. // Show public paste.
c.Echo.GET("/paste/:id", pasteGET) c.Echo.GET("/paste/:id", pasteGETWebInterface)
// Show RAW representation of public paste. // Show RAW representation of public paste.
c.Echo.GET("/paste/:id/raw", pasteRawGET) c.Echo.GET("/paste/:id/raw", pasteRawGETWebInterface)
// Show private paste. // Show private paste.
c.Echo.GET("/paste/:id/:timestamp", pasteGET) c.Echo.GET("/paste/:id/:timestamp", pasteGETWebInterface)
// Show RAW representation of private paste. // Show RAW representation of private paste.
c.Echo.GET("/paste/:id/:timestamp/raw", pasteRawGET) c.Echo.GET("/paste/:id/:timestamp/raw", pasteRawGETWebInterface)
// Verify access to passworded paste. // Verify access to passworded paste.
c.Echo.GET("/paste/:id/:timestamp/verify", pastePasswordedVerifyGet) c.Echo.GET("/paste/:id/:timestamp/verify", pastePasswordedVerifyGet)
c.Echo.POST("/paste/:id/:timestamp/verify", pastePasswordedVerifyPost) c.Echo.POST("/paste/:id/:timestamp/verify", pastePasswordedVerifyPost)
// Pastes list. // Pastes list.
c.Echo.GET("/pastes/", pastesGET) c.Echo.GET("/pastes/", pastesGET)
c.Echo.GET("/pastes/:page", pastesGET) c.Echo.GET("/pastes/:page", pastesGET)

329
domains/pastes/paste_get.go Normal file
View File

@ -0,0 +1,329 @@
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)
}

View File

@ -0,0 +1,133 @@
package pastes
import (
// stdlib
"net/http"
"regexp"
"strconv"
"strings"
"time"
// local
"gitlab.com/pztrn/fastpastebin/internal/captcha"
"gitlab.com/pztrn/fastpastebin/internal/structs"
"gitlab.com/pztrn/fastpastebin/internal/templater"
// other
"github.com/alecthomas/chroma/lexers"
"github.com/labstack/echo"
)
// POST for "/paste/" which will create new paste and redirect to
// "/pastes/CREATED_PASTE_ID". This handler will do all the job for
// requests comes from browsers via web interface.
func pastePOSTWebInterface(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")
}
params, err := ec.FormParams()
if err != nil {
errtpl := templater.GetErrorTemplate(ec, "Cannot create empty paste")
return ec.HTML(http.StatusBadRequest, errtpl)
}
c.Logger.Debug().Msgf("Received parameters: %+v", params)
// Do nothing if paste contents is empty.
if len(params["paste-contents"][0]) == 0 {
c.Logger.Debug().Msg("Empty paste submitted, ignoring")
errtpl := templater.GetErrorTemplate(ec, "Empty pastes aren't allowed.")
return ec.HTML(http.StatusBadRequest, errtpl)
}
if !strings.ContainsAny(params["paste-keep-for"][0], "Mmhd") && params["paste-keep-for"][0] != "forever" {
c.Logger.Debug().Msgf("'Keep paste for' field have invalid value: %s", params["paste-keep-for"][0])
errtpl := templater.GetErrorTemplate(ec, "Invalid 'Paste should be available for' parameter passed. Please do not try to hack us ;).")
return ec.HTML(http.StatusBadRequest, errtpl)
}
// Verify captcha.
if !captcha.Verify(params["paste-captcha-id"][0], params["paste-captcha-solution"][0]) {
c.Logger.Debug().Msgf("Invalid captcha solution for captcha ID '%s': %s", params["paste-captcha-id"][0], params["paste-captcha-solution"][0])
errtpl := templater.GetErrorTemplate(ec, "Invalid captcha solution.")
return ec.HTML(http.StatusBadRequest, errtpl)
}
paste := &structs.Paste{
Title: params["paste-title"][0],
Data: params["paste-contents"][0],
Language: params["paste-language"][0],
}
// Paste creation time in UTC.
createdAt := time.Now().UTC()
paste.CreatedAt = &createdAt
// Parse "keep for" field.
// Defaulting to "forever".
keepFor := 0
keepForUnit := 0
if params["paste-keep-for"][0] != "forever" {
keepForUnitRegex := regexp.MustCompile("[Mmhd]")
keepForRaw := regexInts.FindAllString(params["paste-keep-for"][0], 1)[0]
var err error
keepFor, err = strconv.Atoi(keepForRaw)
if err != nil {
if params["paste-keep-for"][0] == "forever" {
c.Logger.Debug().Msg("Keeping paste forever!")
keepFor = 0
} else {
c.Logger.Debug().Err(err).Msg("Failed to parse 'Keep for' integer")
errtpl := templater.GetErrorTemplate(ec, "Invalid 'Paste should be available for' parameter passed. Please do not try to hack us ;).")
return ec.HTML(http.StatusBadRequest, errtpl)
}
}
keepForUnitRaw := keepForUnitRegex.FindAllString(params["paste-keep-for"][0], 1)[0]
keepForUnit = structs.PASTE_KEEPS_CORELLATION[keepForUnitRaw]
}
paste.KeepFor = keepFor
paste.KeepForUnitType = keepForUnit
// Try to autodetect if it was selected.
if params["paste-language"][0] == "autodetect" {
lexer := lexers.Analyse(params["paste-language"][0])
if lexer != nil {
paste.Language = lexer.Config().Name
} else {
paste.Language = "text"
}
}
// Private paste?
paste.Private = false
privateCheckbox, privateCheckboxFound := params["paste-private"]
pastePassword, pastePasswordFound := params["paste-password"]
if privateCheckboxFound && privateCheckbox[0] == "on" || pastePasswordFound && pastePassword[0] != "" {
paste.Private = true
}
if pastePassword[0] != "" {
paste.CreatePassword(pastePassword[0])
}
id, err2 := c.Database.SavePaste(paste)
if err2 != nil {
c.Logger.Error().Msgf("Failed to save paste: %s", err2.Error())
errtpl := templater.GetErrorTemplate(ec, "Failed to save paste. Please, try again later.")
return ec.HTML(http.StatusBadRequest, errtpl)
}
newPasteIDAsString := strconv.FormatInt(id, 10)
c.Logger.Debug().Msgf("Paste saved, URL: /paste/" + newPasteIDAsString)
// Private pastes have it's timestamp in URL.
if paste.Private {
return ec.Redirect(http.StatusFound, "/paste/"+newPasteIDAsString+"/"+strconv.FormatInt(paste.CreatedAt.Unix(), 10))
}
return ec.Redirect(http.StatusFound, "/paste/"+newPasteIDAsString)
}

View File

@ -0,0 +1,104 @@
// Fast Paste Bin - uberfast and easy-to-use pastebin.
//
// Copyright (c) 2018, Stanislav N. aka pztrn and Fast Paste Bin
// developers.
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject
// to the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package pastes
import (
// stdlib
"net/http"
"strconv"
"strings"
// local
"gitlab.com/pztrn/fastpastebin/internal/pagination"
"gitlab.com/pztrn/fastpastebin/internal/templater"
// other
"github.com/labstack/echo"
)
// GET for "/pastes/", a list of publicly available pastes.
// Web inteface version.
func pastesGET(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")
}
pageFromParamRaw := ec.Param("page")
var page = 1
if pageFromParamRaw != "" {
pageRaw := regexInts.FindAllString(pageFromParamRaw, 1)[0]
page, _ = strconv.Atoi(pageRaw)
}
c.Logger.Debug().Msgf("Requested page #%d", page)
// Get pastes IDs.
pastes, err3 := c.Database.GetPagedPastes(page)
c.Logger.Debug().Msgf("Got %d pastes", len(pastes))
var pastesString = "No pastes to show."
// Show "No pastes to show" on any error for now.
if err3 != nil {
c.Logger.Error().Msgf("Failed to get pastes list from database: %s", err3.Error())
noPastesToShowTpl := templater.GetErrorTemplate(ec, "No pastes to show.")
return ec.HTML(http.StatusOK, noPastesToShowTpl)
}
if len(pastes) > 0 {
pastesString = ""
for i := range pastes {
pasteDataMap := make(map[string]string)
pasteDataMap["pasteID"] = strconv.Itoa(pastes[i].ID)
pasteDataMap["pasteTitle"] = pastes[i].Title
pasteDataMap["pasteDate"] = pastes[i].CreatedAt.Format("2006-01-02 @ 15:04:05") + " UTC"
// Get max 4 lines of each paste.
pasteDataSplitted := strings.Split(pastes[i].Data, "\n")
var pasteData = ""
if len(pasteDataSplitted) < 4 {
pasteData = pastes[i].Data
} else {
pasteData = strings.Join(pasteDataSplitted[0:4], "\n")
}
pasteDataMap["pasteData"] = pasteData
pasteTpl := templater.GetRawTemplate(ec, "pastelist_paste.html", pasteDataMap)
pastesString += pasteTpl
}
}
// Pagination.
pages := c.Database.GetPastesPages()
c.Logger.Debug().Msgf("Total pages: %d, current: %d", pages, page)
paginationHTML := pagination.CreateHTML(page, pages, "/pastes/")
pasteListTpl := templater.GetTemplate(ec, "pastelist_list.html", map[string]string{"pastes": pastesString, "pagination": paginationHTML})
return ec.HTML(http.StatusOK, string(pasteListTpl))
}

View File

@ -26,7 +26,7 @@ package context
const ( const (
// Version . // Version .
Version = "0.3.0" Version = "0.3.1"
) )
// New creates new context. // New creates new context.