From 7281b9be653072389f62734c4b0712557c75e3e0 Mon Sep 17 00:00:00 2001 From: "Stanislav N. aka pztrn" Date: Fri, 12 Apr 2019 23:29:42 +0500 Subject: [PATCH] Refactoring for pastes domain for code reusing and better structuring. --- domains/pastes/api_http.go | 499 ------------------------ domains/pastes/exported.go | 23 +- domains/pastes/paste_get.go | 329 ++++++++++++++++ domains/pastes/paste_post.go | 133 +++++++ domains/pastes/pastes_get.go | 104 +++++ internal/context/exported.go | 2 +- internal/structs/{model.go => paste.go} | 0 7 files changed, 582 insertions(+), 508 deletions(-) delete mode 100644 domains/pastes/api_http.go create mode 100644 domains/pastes/paste_get.go create mode 100644 domains/pastes/paste_post.go create mode 100644 domains/pastes/pastes_get.go rename internal/structs/{model.go => paste.go} (100%) diff --git a/domains/pastes/api_http.go b/domains/pastes/api_http.go deleted file mode 100644 index 3328903..0000000 --- a/domains/pastes/api_http.go +++ /dev/null @@ -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"] = "Private" - pasteData["pasteTs"] = strconv.FormatInt(paste.CreatedAt.Unix(), 10) + "/" - } else { - pasteData["pasteType"] = "Public" - 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)) -} diff --git a/domains/pastes/exported.go b/domains/pastes/exported.go index c216253..37e5a72 100644 --- a/domains/pastes/exported.go +++ b/domains/pastes/exported.go @@ -25,10 +25,17 @@ package pastes import ( + // stdlib + "regexp" + // local "gitlab.com/pztrn/fastpastebin/internal/context" ) +var ( + regexInts = regexp.MustCompile("[0-9]+") +) + var ( c *context.Context ) @@ -38,22 +45,22 @@ var ( func New(cc *context.Context) { c = cc + //////////////////////////////////////////////////////////// + // HTTP endpoints. + //////////////////////////////////////////////////////////// // New paste. - c.Echo.POST("/paste/", pastePOST) - + c.Echo.POST("/paste/", pastePOSTWebInterface) // Show public paste. - c.Echo.GET("/paste/:id", pasteGET) + c.Echo.GET("/paste/:id", pasteGETWebInterface) // Show RAW representation of public paste. - c.Echo.GET("/paste/:id/raw", pasteRawGET) - + c.Echo.GET("/paste/:id/raw", pasteRawGETWebInterface) // Show private paste. - c.Echo.GET("/paste/:id/:timestamp", pasteGET) + c.Echo.GET("/paste/:id/:timestamp", pasteGETWebInterface) // 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. c.Echo.GET("/paste/:id/:timestamp/verify", pastePasswordedVerifyGet) c.Echo.POST("/paste/:id/:timestamp/verify", pastePasswordedVerifyPost) - // Pastes list. c.Echo.GET("/pastes/", pastesGET) c.Echo.GET("/pastes/:page", pastesGET) diff --git a/domains/pastes/paste_get.go b/domains/pastes/paste_get.go new file mode 100644 index 0000000..1b0bdf7 --- /dev/null +++ b/domains/pastes/paste_get.go @@ -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"] = "Private" + pasteData["pasteTs"] = strconv.FormatInt(paste.CreatedAt.Unix(), 10) + "/" + } else { + pasteData["pasteType"] = "Public" + 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) +} diff --git a/domains/pastes/paste_post.go b/domains/pastes/paste_post.go new file mode 100644 index 0000000..a3870a3 --- /dev/null +++ b/domains/pastes/paste_post.go @@ -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) +} diff --git a/domains/pastes/pastes_get.go b/domains/pastes/pastes_get.go new file mode 100644 index 0000000..a908b9b --- /dev/null +++ b/domains/pastes/pastes_get.go @@ -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)) +} diff --git a/internal/context/exported.go b/internal/context/exported.go index 0fb13d7..9c3cf22 100644 --- a/internal/context/exported.go +++ b/internal/context/exported.go @@ -26,7 +26,7 @@ package context const ( // Version . - Version = "0.3.0" + Version = "0.3.1" ) // New creates new context. diff --git a/internal/structs/model.go b/internal/structs/paste.go similarity index 100% rename from internal/structs/model.go rename to internal/structs/paste.go