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().Err(err1).Int("paste ID", pasteID).Msg("Failed to get paste") return nil, pasteNotFound } // Check if paste is expired. if paste.IsExpired() { c.Logger.Error().Int("paste ID", pasteID).Msg("Paste is expired") 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().Int("paste ID", pasteID).Int64("paste timestamp", pasteTs).Int64("provided timestamp", timestamp).Msg("Incorrect timestamp provided for private paste") 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().Int("paste ID", pasteID).Msg("Trying to get paste data") // 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).Int("paste ID", pasteID).Int64("provided timestamp", tsProvided).Msg("Invalid timestamp provided for getting private paste") 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().Int("paste ID", pasteID).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().Err(err3).Msg("Failed to tokenize paste data") } // 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().Err(err4).Msg("Failed to format paste data") } 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().Err(err1).Int("paste ID", pasteID).Msg("Failed to get paste data") 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().Int("paste ID", pasteID).Msg("Requesting paste") // Get paste. paste, err1 := c.Database.GetPaste(pasteID) if err1 != nil { c.Logger.Error().Err(err1).Int("paste ID", pasteID).Msg("Failed to get paste") 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().Int("paste ID", pasteID).Msg("Requesting paste data") // Get paste. paste, err1 := c.Database.GetPaste(pasteID) if err1 != nil { c.Logger.Error().Err(err1).Int("paste ID", pasteID).Msg("Failed to get paste from database") return ec.HTML(http.StatusBadRequest, "Paste #"+pasteIDRaw+" does not exist.") } if paste.IsExpired() { c.Logger.Error().Int("paste ID", pasteID).Msg("Paste is expired") 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().Err(err2).Int("paste ID", pasteID).Str("provided timestamp", tsProvidedStr).Msg("Invalid timestamp provided for getting private paste") return ec.String(http.StatusBadRequest, "Paste #"+pasteIDRaw+" not found") } pasteTs := paste.CreatedAt.Unix() if tsProvided != pasteTs { c.Logger.Error().Int("paste ID", pasteID).Int64("provided timestamp", tsProvided).Int64("paste timestamp", pasteTs).Msg("Incorrect timestamp provided for private paste") 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().Int("paste ID", pasteID).Msg("Cannot render paste as raw: passworded paste. Patches welcome!") return ec.String(http.StatusBadRequest, "Paste #"+pasteIDRaw+" not found") } return ec.String(http.StatusOK, paste.Data) }