Files
pastebin/yasuc.go

280 lines
7.1 KiB
Go

// yasuc - yet another sprunge.us clone (command line pastebin)
//
// Copyright (C) Tom Jakubowski <tom@crystae.net>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package main
import (
"crypto/sha256"
"database/sql"
"encoding/hex"
"flag"
"fmt"
"html/template"
"log"
"math/rand"
"net/http"
"os"
"time"
_ "modernc.org/sqlite"
)
const (
maxPasteSize = 4 * 1024 * 1024
shortIDLength = 7
)
const usageText = `
<!doctype html>
<html>
<head>
</head>
<body>
<pre>
yasuc(1) YASUC yasuc(1)
NAME
yasuc - command line pastebin.
SYNOPSIS
&lt;command&gt; | curl -F 'sprunge=&lt;-' {{.BaseURL}}
DESCRIPTION
A command line pastebin. Pastes are immutable and created with simple HTTP
POST requests. The path of a paste's URL is a short unique identifier.
EXAMPLE
$ echo 'hello world' | curl -F 'sprunge=&lt;-' {{.BaseURL}}
{{.BaseURL}}/Ly34kx3
$ firefox {{.BaseURL}}/Ly34kx3
COPYRIGHT
Copyright © Tom Jakubowski. License AGPLv3: GNU Affero GPL Version 3
<a href="https://www.gnu.org/licenses/agpl-3.0.en.html">https://www.gnu.org/licenses/agpl-3.0.en.html</a>.
This is free software; you are free to change and redistribute it. There
is NO WARRANTY, to the extent permitted by law. For copying conditions and
source, see <a href="https://github.com/tomjakubowski/yasuc">https://github.com/tomjakubowski/yasuc</a>.
SEE ALSO
<a href="https://github.com/tomjakubowski/yasuc">http://github.com/tomjakubowski/yasuc</a>
</pre>
</body>
</html>
`
type pasteTooLarge struct{}
func (e pasteTooLarge) Error() string {
return fmt.Sprintf("paste too large (maximum size %d bytes)", maxPasteSize)
}
// generateShortID gera um ID único de 7 caracteres
func generateShortID() string {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
b := make([]byte, shortIDLength)
for i := range b {
b[i] = charset[rand.Intn(len(charset))]
}
return string(b)
}
// generateUniqueShortID gera um ID único que não existe no banco
func generateUniqueShortID(db *sql.DB) (string, error) {
maxAttempts := 100
for i := 0; i < maxAttempts; i++ {
id := generateShortID()
// Verificar se o ID já existe
var exists int
err := db.QueryRow("SELECT 1 FROM pastes WHERE short_id = ? LIMIT 1", id).Scan(&exists)
if err == sql.ErrNoRows {
return id, nil
}
if err != nil {
return "", err
}
}
return "", fmt.Errorf("não foi possível gerar um ID único após %d tentativas", maxAttempts)
}
func stashPaste(db *sql.DB, pasteStr string) (key string, err error) {
if len(pasteStr) > maxPasteSize {
err = pasteTooLarge{}
return
}
// Gerar ID curto único
shortID, err := generateUniqueShortID(db)
if err != nil {
return "", err
}
// Calcular hash SHA-256 para verificação de duplicatas
paste := []byte(pasteStr)
hash := sha256.Sum256(paste)
hashStr := hex.EncodeToString(hash[:])
// Verificar se já existe um paste com o mesmo conteúdo
var existingID string
err = db.QueryRow("SELECT short_id FROM pastes WHERE content_hash = ? LIMIT 1", hashStr).Scan(&existingID)
if err == nil {
// Paste já existe, retornar o ID existente
return existingID, nil
} else if err != sql.ErrNoRows {
return "", err
}
// Inserir novo paste
_, err = db.Exec("INSERT INTO pastes (short_id, content, content_hash, created_at) VALUES (?, ?, ?, ?)",
shortID, pasteStr, hashStr, time.Now().Unix())
if err != nil {
return "", err
}
return shortID, nil
}
type pasteNotFound struct{}
func (e pasteNotFound) Error() string {
return "not found"
}
func fetchPaste(db *sql.DB, key string) (paste string, err error) {
var content string
err = db.QueryRow("SELECT content FROM pastes WHERE short_id = ?", key).Scan(&content)
if err != nil {
if err == sql.ErrNoRows {
return "", pasteNotFound{}
}
return "", err
}
return content, nil
}
type handler struct {
db *sql.DB
}
func (h *handler) alles(w http.ResponseWriter, req *http.Request) {
if req.URL.Path != "/" {
paste, err := fetchPaste(h.db, req.URL.Path[1:])
if err != nil {
if _, ok := err.(pasteNotFound); ok {
http.Error(w, "not found", http.StatusNotFound)
} else {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
return
}
fmt.Fprintf(w, "%s", paste)
return
}
if req.Method == http.MethodPost {
body := req.FormValue("sprunge")
// sprunge.us proceeds if there's no form data in 'sprunge', and just makes
// an empty paste.
key, err := stashPaste(h.db, body)
if err != nil {
if _, ok := err.(pasteTooLarge); ok {
http.Error(w, err.Error(), http.StatusNotAcceptable)
} else {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
return
}
baseURL := fmt.Sprintf("https://%s", req.Host)
fmt.Fprintf(w, "%s/%s\n", baseURL, key)
return
}
// usage message
tmpl, err := template.New("usage").Parse(usageText)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
baseURL := fmt.Sprintf("https://%s", req.Host)
data := struct{ BaseURL string }{baseURL}
_ = tmpl.Execute(w, data)
}
func newHandler(db *sql.DB) http.Handler {
h := handler{db}
mux := http.NewServeMux()
mux.HandleFunc("/", h.alles)
return mux
}
func initDatabase(db *sql.DB) error {
// Create the pastes table if it doesn't exist
_, err := db.Exec(`
CREATE TABLE IF NOT EXISTS pastes (
short_id TEXT PRIMARY KEY,
content TEXT NOT NULL,
content_hash TEXT NOT NULL,
created_at INTEGER NOT NULL
)
`)
if err != nil {
return err
}
// Create indexes for better performance
_, err = db.Exec(`CREATE INDEX IF NOT EXISTS idx_pastes_content_hash ON pastes(content_hash)`)
if err != nil {
return err
}
_, err = db.Exec(`CREATE INDEX IF NOT EXISTS idx_pastes_created_at ON pastes(created_at)`)
return err
}
func main() {
// Initialize random seed
rand.Seed(time.Now().UnixNano())
var dbPath, addr string
var port int
flag.StringVar(&dbPath, "db", "", "location of database file")
flag.StringVar(&addr, "addr", "", "bind address")
flag.IntVar(&port, "port", 9001, "bind port")
flag.Parse()
if dbPath == "" {
fmt.Fprintf(os.Stderr, "db option is required\n")
os.Exit(1)
}
// Open SQLite database
db, err := sql.Open("sqlite", dbPath)
if err != nil {
log.Fatal(err)
}
defer func() { _ = db.Close() }()
// Initialize database schema
err = initDatabase(db)
if err != nil {
log.Fatal(err)
}
http.Handle("/", newHandler(db))
sockAddr := fmt.Sprintf("%s:%d", addr, port)
log.Fatal(http.ListenAndServe(sockAddr, nil))
}