This commit is contained in:
a 2022-08-03 05:02:53 -05:00
parent 7d2e1e2c25
commit 3c63282dd9
20 changed files with 1499 additions and 1335 deletions

View File

@ -1,19 +0,0 @@
Copyright (c) 2016 Eliot Whalan <ewhal@pantsu.cat>
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.

BIN
Pastebin

Binary file not shown.

596
cmd/server/main.go Normal file
View File

@ -0,0 +1,596 @@
package main
import (
"context"
"encoding/json"
"fmt"
"html"
"html/template"
"io"
"log"
"net/http"
"os"
"time"
"git.tuxpa.in/a/nat/lib/store"
"git.tuxpa.in/a/nat/lib/store/sqlike"
"git.tuxpa.in/a/nat/lib/styler"
"database/sql"
"github.com/go-chi/chi/v5"
"github.com/gorilla/securecookie"
// bcrypt for password hashing
"golang.org/x/crypto/bcrypt"
)
// Configuration struct,
type Configuration struct {
Address string `json:"address"` // Url to to the pastebin
ListenAddress string `json:"listenaddress"` // Address that pastebin will bind on
ListenPort string `json:"listenport"` // Port that pastebin will listen on
ShortUrlLength int `json:"shorturllength,string"` // Length of the generated short urls
}
// This struct is used for generating pages.
type Page struct {
Body template.HTML
Expiry string
GoogleAPIKey string
Lang string
LangsFirst map[string]string
LangsLast map[string]string
PasteTitle string
Style string
SupportedStyles map[string]string
Title string
UrlAddress string
UrlClone string
UrlDownload string
UrlHome string
UrlRaw string
WrapperErr string
UserKey string
}
// Template pages,
var templates = template.Must(template.ParseFiles("static/index.html",
"static/syntax.html",
"static/register.html",
"static/pastes.html",
"static/login.html"),
)
// Global variables, *shrug*
var configuration Configuration
var dbHandle *sql.DB
var debug bool
var debugLogger *log.Logger
type Server struct {
store store.Store
styler *styler.Styler
}
// generate new random cookie keys
var cookieHandler = securecookie.New(
securecookie.GenerateRandomKey(64),
securecookie.GenerateRandomKey(32),
)
//
// Functions below,
//
// This struct is used for indata when a request is being made to the pastebin.
type Request struct {
DelKey string `json:"delkey"` // The delkey that is used to delete paste
Expiry int64 `json:"expiry,string"` // An expiry date
Id string `json:"id"` // The id of the paste
Lang string `json:"lang"` // The language of the paste
Paste string `json:"paste"` // The actual pase
Style string `json:"style"` // The style of the paste
Title string `json:"title"` // The title of the paste
UserKey string `json:"key"` // The title of the paste
WebReq bool `json:"webreq"` // If its a webrequest or not
}
// getSupportedLangs reads supported lexers from the highlighter-wrapper (which
// in turn gets available lexers from pygments). It then puts them into two
// maps, depending on if it's a "prioritized" lexers. If it's prioritized or not
// is determined by if its listed in the assets/prio-lexers. The description is
// the key and the actual lexer is the value. The maps are used by the
// html-template. The function doesn't return anything since the maps are
// defined globally (shrug).
// printHelp prints a description of the program.
// Exit code will depend on how the function is called.
func printHelp(err int) {
fmt.Printf("\n Description, \n")
fmt.Printf(" - pastebin")
fmt.Printf(" support for syntax highlightnig (trough python-pygments).\n")
fmt.Printf(" Usage, \n")
fmt.Printf(" - %s [--help] \n\n", os.Args[0])
fmt.Printf(" Where, \n")
fmt.Printf(" - help shows this *incredibly* useful help.\n")
os.Exit(err)
}
// checkArgs parses the command line in a very simple manner.
func checkArgs() {
if len(os.Args[1:]) >= 1 {
for _, arg := range os.Args[1:] {
switch arg {
case "-h", "--help":
printHelp(0)
case "-d", "--debug":
debug = true
default:
printHelp(1)
}
}
}
}
// DelHandler handles the deletion of pastes.
// If pasteId and DelKey consist the paste will be removed.
func (s *Server) DelHandler(w http.ResponseWriter, r *http.Request) {
var inData Request
decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&inData)
inData.Id = chi.URLParam(r, "pasteId")
// Escape user input,
inData.DelKey = html.EscapeString(inData.DelKey)
inData.Id = html.EscapeString(inData.Id)
err = s.store.DelPaste(r.Context(), inData.Id, inData.DelKey)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
b := store.Response{Status: "Deleted paste " + inData.Id}
err = json.NewEncoder(w).Encode(b)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
// SaveHandler will handle the actual save of each paste.
// Returns with a store.Response struct.
func (s *Server) SaveHandler(w http.ResponseWriter, r *http.Request) {
var inData Request
decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&inData)
// Return error if we can't decode the json-data,
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if len(inData.Paste) > 1500000 {
http.Error(w, "Paste too long.", 500)
return
}
// Return error if we don't have any data at all
if inData.Paste == "" {
http.Error(w, "Empty paste.", 500)
return
}
if len(inData.Title) > 50 {
http.Error(w, "Title to long.", 500)
return
}
p, err := s.store.SavePaste(r.Context(), inData.Title, inData.Paste, time.Second*time.Duration(inData.Expiry), inData.UserKey)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(p)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
// high calls the highlighter-wrapper and runs the paste through it.
// Takes the arguments,
// paste, the actual paste data as a string,
// lang, the pygments lexer to use as a string,
// style, the pygments style to use as a string
// Returns two strings, first is the output from the pygments html-formatter,
// the second is a custom message
// checkPasteExpiry checks if a paste is overdue.
// It takes the pasteId as sting and the expiry date as an int64 as arguments.
// If the paste is overdue it gets deleted and false is returned.
func (s *Server) checkPasteExpiry(pasteId string, expiretime time.Time) bool {
if expiretime.IsZero() {
} else {
// Current time,
now := time.Now()
// Human friendly strings for logging,
// If expiry is greater than current time, delete paste,
if now.After(expiretime) {
err := s.store.ForceDelPaste(context.TODO(), pasteId)
if err != nil {
log.Printf("failed to delete paste: %s, %w\n", pasteId)
}
return false
}
}
return true
}
func (s *Server) APIHandler(w http.ResponseWriter, r *http.Request) {
pasteId := chi.URLParam(r, "pasteId")
var inData Request
decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&inData)
//if err != nil {
// http.Error(w, err.Error(), http.StatusInternalServerError)
// return
//}
// Get the actual paste data,
p, err := s.store.GetPaste(r.Context(), pasteId)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if inData.WebReq {
// Run it through the highgligther.,
p.Paste, p.Extra, p.Lang, p.Style, err = s.styler.Highlight(p.Paste, inData.Lang, inData.Style)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(p)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
// pasteHandler generates the html paste pages
func (s *Server) pasteHandler(w http.ResponseWriter, r *http.Request) {
pasteId := chi.URLParam(r, "pasteId")
lang := chi.URLParam(r, "lang")
style := chi.URLParam(r, "style")
// Get the actual paste data,
p, err := s.store.GetPaste(r.Context(), pasteId)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
// Run it through the highgligther.,
p.Paste, p.Extra, p.Lang, p.Style, err = s.styler.Highlight(p.Paste, lang, style)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
l := s.styler.Legacy()
// Construct page struct
page := &Page{
Body: template.HTML(p.Paste),
Expiry: p.Expiry,
Lang: p.Lang,
LangsFirst: l[0],
LangsLast: l[1],
Style: p.Style,
SupportedStyles: l[2],
Title: p.Title,
UrlClone: configuration.Address + "/clone/" + pasteId,
UrlDownload: configuration.Address + "/download/" + pasteId,
UrlHome: configuration.Address,
UrlRaw: configuration.Address + "/raw/" + pasteId,
WrapperErr: p.Extra,
}
err = templates.ExecuteTemplate(w, "syntax.html", page)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
// CloneHandler handles generating the clone pages
func (s *Server) CloneHandler(w http.ResponseWriter, r *http.Request) {
paste := chi.URLParam(r, "pasteId")
p, err := s.store.GetPaste(r.Context(), paste)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
user, _ := s.store.GetUserKey(r.Context(), paste)
page := &Page{
Body: template.HTML(p.Paste),
PasteTitle: "Copy of " + p.Title,
Title: "Copy of " + p.Title,
UserKey: user,
}
err = templates.ExecuteTemplate(w, "index.html", page)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
// DownloadHandler forces downloads of selected pastes
func (s *Server) DownloadHandler(w http.ResponseWriter, r *http.Request) {
pasteId := chi.URLParam(r, "pasteId")
p, err := s.store.GetPaste(r.Context(), pasteId)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Set header to an attachment so browser will automatically download it
w.Header().Set("Content-Disposition", "attachment; filename="+p.Paste)
w.Header().Set("Content-Type", r.Header.Get("Content-Type"))
io.WriteString(w, p.Paste)
}
// RawHandler displays the pastes in text/plain format
func (s *Server) RawHandler(w http.ResponseWriter, r *http.Request) {
pasteId := chi.URLParam(r, "pasteId")
p, err := s.store.GetPaste(r.Context(), pasteId)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/plain; charset=UTF-8; imeanit=yes")
// Simply write string to browser
io.WriteString(w, p.Paste)
}
// loginHandler
func (s *Server) loginHandlerGet(w http.ResponseWriter, r *http.Request) {
err := templates.ExecuteTemplate(w, "login.html", "")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func (s *Server) loginHandlerPost(w http.ResponseWriter, r *http.Request) {
email := r.FormValue("email")
password := r.FormValue("password")
email_escaped := html.EscapeString(email)
hashedPassword, err := s.store.HasAccount(r.Context(), email_escaped)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if len(hashedPassword) == 0 {
http.Redirect(w, r, "/register", 302)
return
}
// compare bcrypt hash to userinput password
err = bcrypt.CompareHashAndPassword(hashedPassword, []byte(password))
if err == nil {
// prepare cookie
value := map[string]string{
"email": email,
}
// encode variables into cookie
if encoded, err := cookieHandler.Encode("session", value); err == nil {
cookie := &http.Cookie{
Name: "session",
Value: encoded,
Path: "/",
}
// set user cookie
http.SetCookie(w, cookie)
}
// Redirect to home page
http.Redirect(w, r, "/", 302)
}
// Redirect to login page
http.Redirect(w, r, "/login", 302)
}
func (s *Server) pastesHandler(w http.ResponseWriter, r *http.Request) {
key, err := s.getUserKey(r)
b, err := s.store.GetUserPastes(r.Context(), key)
err = templates.ExecuteTemplate(w, "pastes.html", &b)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
// loggedIn returns true if cookie exists
func (s *Server) getUserKey(r *http.Request) (string, error) {
cookie, err := r.Cookie("session")
cookieValue := make(map[string]string)
if err != nil {
return "", err
}
err = cookieHandler.Decode("session", cookie.Value, &cookieValue)
if err != nil {
return "", err
}
email := cookieValue["email"]
// Query database if id exists and if it does call generateName again
user_key, err := s.store.GetUserKey(r.Context(), email)
switch {
case err == sql.ErrNoRows:
return "", nil
case err != nil:
return "", err
default:
}
return user_key, nil
}
// registerHandler
func (s *Server) registerHandlerGet(w http.ResponseWriter, r *http.Request) {
err := templates.ExecuteTemplate(w, "register.html", "")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func (s *Server) registerHandlerPost(w http.ResponseWriter, r *http.Request) {
email := r.FormValue("email")
pass := r.FormValue("password")
email_escaped := html.EscapeString(email)
bts, err := s.store.HasAccount(r.Context(), email_escaped)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
email_taken := true
if len(bts) == 0 {
email_taken = false
}
if email_taken {
http.Redirect(w, r, "/register", 302)
return
}
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
err = s.store.RegisterUser(r.Context(), email_escaped, hashedPassword)
if err != nil {
log.Printf("failed register user %v\n", err)
}
http.Redirect(w, r, "/login", 302)
}
// logoutHandler destroys cookie data and redirects to root
func (s *Server) logoutHandler(w http.ResponseWriter, r *http.Request) {
cookie := &http.Cookie{
Name: "session",
Value: "",
Path: "/",
MaxAge: -1,
}
http.SetCookie(w, cookie)
http.Redirect(w, r, "/", 301)
}
// RootHandler handles generating the root page
func (s *Server) RootHandler(w http.ResponseWriter, r *http.Request) {
userkey, err := s.getUserKey(r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
l := s.styler.Legacy()
p := &Page{
LangsFirst: l[0],
LangsLast: l[1],
Title: "nat",
UrlAddress: configuration.Address,
UserKey: userkey,
}
err = templates.ExecuteTemplate(w, "index.html", p)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func serveCss(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "static/pastebin.css")
}
func main() {
// Check args,
checkArgs()
// Load config,
file, err := os.Open("config.json")
if err != nil {
os.Exit(1)
}
// Try to parse json,
err = json.NewDecoder(file).Decode(&configuration)
if err != nil {
panic(err)
}
srv := &Server{
store: sqlike.MustNew(),
styler: styler.New(),
}
router := chi.NewRouter()
router.Get("/", srv.RootHandler)
router.Get("/p/{pasteId}", srv.pasteHandler)
router.Get("/p/{pasteId}/{lang}", srv.pasteHandler)
router.Get("/p/{pasteId}/{lang}/{style}", srv.pasteHandler)
// Api
router.Post("/api", srv.SaveHandler)
router.Post("/api/{pasteId}", srv.APIHandler)
router.Get("/api/{pasteId}", srv.APIHandler)
router.Delete("/api/{pasteId}", srv.DelHandler)
router.Get("/raw/{pasteId}", srv.RawHandler)
router.Get("/clone/{pasteId}", srv.CloneHandler)
router.Get("/login", srv.loginHandlerGet)
router.Post("/login", srv.loginHandlerPost)
router.HandleFunc("/logout", srv.logoutHandler)
router.Get("/register", srv.registerHandlerGet)
router.Post("/register", srv.registerHandlerPost)
router.Get("/pastes", srv.pastesHandler)
router.Get("/download/{pasteId}", srv.DownloadHandler)
router.Get("/assets/pastebin.css", serveCss)
http_srv := &http.Server{
Handler: router,
Addr: configuration.ListenAddress + ":" + configuration.ListenPort,
WriteTimeout: 15 * time.Second,
ReadTimeout: 15 * time.Second,
}
log.Println("starting http server on", configuration.ListenAddress, configuration.ListenPort)
err = http_srv.ListenAndServe()
if err != nil {
panic(err)
}
}

View File

@ -1,17 +1,15 @@
{
"address": "http://localhost:9999",
"dbtype": "sql",
"dbhost": "",
"dbname": "pastebin.db",
"dbtable": "pastebin",
"dbaccountstable": "accounts",
"dbtype": "sqlite3",
"dbport": "",
"dbuser":"",
"dbpassword":"",
"displayname": "MyCompany",
"listenaddress": "localhost",
"listenport": "9999",
"shorturllength": "5",
"highlighter":"./highlighter-wrapper.py",
"googleAPIKey":"insert-if-you-want-goo.gl/addr"
"shorturllength": "5"
}

View File

@ -1,17 +0,0 @@
CREATE TABLE `pastebin` (
`id` varchar(30) NOT NULL,
`title` varchar(50) default NULL,
`hash` char(40) default NULL,
`data` longtext,
`delkey` char(40) default NULL,
`expiry` int,
`userid` varchar(255),
PRIMARY KEY (`id`)
);
CREATE TABLE `accounts` (
`email` varchar(255) NOT NULL,
`password` varchar(255) NOT NULL,
`key` varchar(255) NOT NULL,
PRIMARY KEY (`key`)
);

14
go.mod Normal file
View File

@ -0,0 +1,14 @@
module git.tuxpa.in/a/nat
go 1.18
require (
github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5
github.com/go-chi/chi/v5 v5.0.7
github.com/go-sql-driver/mysql v1.6.0
github.com/gorilla/mux v1.8.0
github.com/gorilla/securecookie v1.1.1
github.com/lib/pq v1.10.6
github.com/mattn/go-sqlite3 v1.14.14
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa
)

16
go.sum Normal file
View File

@ -0,0 +1,16 @@
github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5 h1:RAV05c0xOkJ3dZGS0JFybxFKZ2WMLabgx3uXnd7rpGs=
github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4=
github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8=
github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs=
github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-sqlite3 v1.14.14 h1:qZgc/Rwetq+MtyE18WhzjokPD93dNqLGNT3QJuLvBGw=
github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=

View File

@ -1,110 +0,0 @@
#!/usr/bin/env python
try:
import pygments
except ImportError:
print(" Please install python pygments module")
from pygments import highlight
from pygments.lexers import get_lexer_by_name, guess_lexer
from pygments.formatters import HtmlFormatter
import sys
def render(code, lang, theme):
guess = ""
lang_org = lang
try:
lexer = get_lexer_by_name(lang)
except:
try:
guess = 1
lexer = guess_lexer(code)
lang = lexer.aliases[0]
except:
if lang == "autodetect":
out = "Could not autodetect language (returning plain text).\n"
else:
out = "Given language was not found :: '"+lang+"' (returning plain text).\n"
lexer = get_lexer_by_name("text")
html_format = HtmlFormatter(style=theme, noclasses="true", linenos="true", encoding="utf-8")
return highlight(code, lexer, html_format),out
if guess:
out = "Lexer guessed :: "+lang
if lang != lang_org and lang_org != "autodetect":
out += " (although given language was "+lang_org+") "
else:
out = "Successfully used lexer for given language :: "+lang
try:
html_format = HtmlFormatter(style=theme, noclasses="true", linenos="true", encoding="utf-8")
except:
html_format = HtmlFormatter(noclasses="true", linenos="true", encoding="utf-8")
return highlight(code, lexer, html_format),out
def usage(err=0):
print("\n Description, \n")
print(" - This is a small wrapper for the pygments html-formatter.")
print(" It will read data on stdin and simply print it on stdout")
print("\n Usage, \n")
print(" - %s [lang] [style] < FILE" % sys.argv[0])
print(" - %s getlexers" % sys.argv[0])
print(" - %s getstyles" % sys.argv[0])
print("\n Where, \n")
print(" - lang is the language of your code")
print(" - style is the 'theme' for the formatter")
print(" - getlexers will print available lexers (displayname;lexer-name)")
print(" - getstyles will print available styles \n")
sys.exit(err)
def get_styles():
item = pygments.styles.get_all_styles()
for items in item:
print(items)
sys.exit(0)
def get_lexers():
item = pygments.lexers.get_all_lexers()
for items in item:
print(items[0]+";"+items[1][0])
sys.exit(0)
# " Main "
code = ""
if len(sys.argv) >= 2:
for arg in sys.argv:
if arg == '--help' or arg == '-h':
usage()
if arg == 'getlexers':
get_lexers()
if arg == 'getstyles':
get_styles()
if len(sys.argv) == 3:
lang = sys.argv[1]
theme = sys.argv[2]
else:
usage(1);
if not sys.stdin.isatty():
for line in sys.stdin:
code += line
out, stderr = render(code, lang, theme)
print(out)
sys.stderr.write(stderr)
else:
print("err : No data on stdin.")
sys.exit(1)

286
lib/idgen/idgen.go Normal file
View File

@ -0,0 +1,286 @@
package idgen
import (
randc "crypto/rand"
"errors"
"fmt"
"math"
randm "math/rand"
"sync"
"time"
)
const DefaultABC = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-"
// Abc represents a shuffled alphabet used to generate the Ids and provides methods to
// encode data.
type Abc struct {
alphabet []rune
}
// Shortid type represents a short Id generator working with a given alphabet.
type Shortid struct {
abc Abc
worker uint
epoch time.Time // ids can be generated for 34 years since this date
ms uint // ms since epoch for the last id
count uint // request count within the same ms
mx sync.Mutex // locks access to ms and count
}
var shortid *Shortid
func init() {
shortid = MustNew(0, DefaultABC, 1)
}
func GetDefault() *Shortid {
return shortid
}
// SetDefault overwrites the default generator.
// should not be used concurrently with generation
func SetDefault(sid *Shortid) {
shortid = sid
}
// Generate generates an Id using the default generator.
func Generate() (string, error) {
return shortid.Generate()
}
// MustGenerate acts just like Generate, but panics instead of returning errors.
func MustGenerate() string {
id, err := Generate()
if err == nil {
return id
}
panic(err)
}
// New constructs an instance of the short Id generator for the given worker number [0,31], alphabet
// (64 unique symbols) and seed value (to shuffle the alphabet). The worker number should be
// different for multiple or distributed processes generating Ids into the same data space. The
// seed, on contrary, should be identical.
func New(worker uint8, alphabet string, seed uint64) (*Shortid, error) {
if worker > 31 {
return nil, errors.New("expected worker in the range [0,31]")
}
abc, err := NewAbc(alphabet, seed)
if err == nil {
sid := &Shortid{
abc: abc,
worker: uint(worker),
epoch: time.Date(2016, time.January, 1, 0, 0, 0, 0, time.UTC),
ms: 0,
count: 0,
}
return sid, nil
}
return nil, err
}
// MustNew acts just like New, but panics instead of returning errors.
func MustNew(worker uint8, alphabet string, seed uint64) *Shortid {
sid, err := New(worker, alphabet, seed)
if err == nil {
return sid
}
panic(err)
}
// Generate generates a new short Id.
func (sid *Shortid) Generate() (string, error) {
return sid.generateInternal(nil, sid.epoch)
}
// MustGenerate acts just like Generate, but panics instead of returning errors.
func (sid *Shortid) MustGenerate() string {
id, err := sid.Generate()
if err == nil {
return id
}
panic(err)
}
func (sid *Shortid) generateInternal(tm *time.Time, epoch time.Time) (string, error) {
ms, count := sid.getMsAndCounter(tm, epoch)
idrunes := make([]rune, 9)
if tmp, err := sid.abc.Encode(ms, 8, 5); err == nil {
copy(idrunes, tmp) // first 8 symbols
} else {
return "", err
}
if tmp, err := sid.abc.Encode(sid.worker, 1, 5); err == nil {
idrunes[8] = tmp[0]
} else {
return "", err
}
if count > 0 {
if countrunes, err := sid.abc.Encode(count, 0, 6); err == nil {
// only extend if really need it
idrunes = append(idrunes, countrunes...)
} else {
return "", err
}
}
return string(idrunes), nil
}
func (sid *Shortid) getMsAndCounter(tm *time.Time, epoch time.Time) (uint, uint) {
sid.mx.Lock()
defer sid.mx.Unlock()
var ms uint
if tm != nil {
ms = uint(tm.Sub(epoch).Nanoseconds() / 1000000)
} else {
ms = uint(time.Now().Sub(epoch).Nanoseconds() / 1000000)
}
if ms == sid.ms {
sid.count++
} else {
sid.count = 0
sid.ms = ms
}
return sid.ms, sid.count
}
// String returns a string representation of the short Id generator.
func (sid *Shortid) String() string {
return fmt.Sprintf("Shortid(worker=%v, epoch=%v, abc=%v)", sid.worker, sid.epoch, sid.abc)
}
// Abc returns the instance of alphabet used for representing the Ids.
func (sid *Shortid) Abc() Abc {
return sid.abc
}
// Epoch returns the value of epoch used as the beginning of millisecond counting (normally
// 2016-01-01 00:00:00 local time)
func (sid *Shortid) Epoch() time.Time {
return sid.epoch
}
// Worker returns the value of worker for this short Id generator.
func (sid *Shortid) Worker() uint {
return sid.worker
}
// NewAbc constructs a new instance of shuffled alphabet to be used for Id representation.
func NewAbc(alphabet string, seed uint64) (Abc, error) {
runes := []rune(alphabet)
if len(runes) != len(DefaultABC) {
return Abc{}, fmt.Errorf("alphabet must contain %v unique characters", len(DefaultABC))
}
if nonUnique(runes) {
return Abc{}, errors.New("alphabet must contain unique characters only")
}
abc := Abc{alphabet: nil}
abc.shuffle(alphabet, seed)
return abc, nil
}
// MustNewAbc acts just like NewAbc, but panics instead of returning errors.
func MustNewAbc(alphabet string, seed uint64) Abc {
res, err := NewAbc(alphabet, seed)
if err == nil {
return res
}
panic(err)
}
func nonUnique(runes []rune) bool {
found := make(map[rune]struct{})
for _, r := range runes {
if _, seen := found[r]; !seen {
found[r] = struct{}{}
}
}
return len(found) < len(runes)
}
func (abc *Abc) shuffle(alphabet string, seed uint64) {
source := []rune(alphabet)
for len(source) > 1 {
seed = (seed*9301 + 49297) % 233280
i := int(seed * uint64(len(source)) / 233280)
abc.alphabet = append(abc.alphabet, source[i])
source = append(source[:i], source[i+1:]...)
}
abc.alphabet = append(abc.alphabet, source[0])
}
// Encode encodes a given value into a slice of runes of length nsymbols. In case nsymbols==0, the
// length of the result is automatically computed from data. Even if fewer symbols is required to
// encode the data than nsymbols, all positions are used encoding 0 where required to guarantee
// uniqueness in case further data is added to the sequence. The value of digits [4,6] represents
// represents n in 2^n, which defines how much randomness flows into the algorithm: 4 -- every value
// can be represented by 4 symbols in the alphabet (permitting at most 16 values), 5 -- every value
// can be represented by 2 symbols in the alphabet (permitting at most 32 values), 6 -- every value
// is represented by exactly 1 symbol with no randomness (permitting 64 values).
func (abc *Abc) Encode(val, nsymbols, digits uint) ([]rune, error) {
if digits < 4 || 6 < digits {
return nil, fmt.Errorf("allowed digits range [4,6], found %v", digits)
}
var computedSize uint = 1
if val >= 1 {
computedSize = uint(math.Log2(float64(val)))/digits + 1
}
if nsymbols == 0 {
nsymbols = computedSize
} else if nsymbols < computedSize {
return nil, fmt.Errorf("cannot accommodate data, need %v digits, got %v", computedSize, nsymbols)
}
mask := 1<<digits - 1
random := make([]int, int(nsymbols))
// no random component if digits == 6
if digits < 6 {
copy(random, maskedRandomInts(len(random), 0x3f-mask))
}
res := make([]rune, int(nsymbols))
for i := range res {
shift := digits * uint(i)
index := (int(val>>shift) & mask) | random[i]
res[i] = abc.alphabet[index]
}
return res, nil
}
// MustEncode acts just like Encode, but panics instead of returning errors.
func (abc *Abc) MustEncode(val, size, digits uint) []rune {
res, err := abc.Encode(val, size, digits)
if err == nil {
return res
}
panic(err)
}
func maskedRandomInts(size, mask int) []int {
ints := make([]int, size)
bytes := make([]byte, size)
if _, err := randc.Read(bytes); err == nil {
for i, b := range bytes {
ints[i] = int(b) & mask
}
} else {
for i := range ints {
ints[i] = randm.Intn(0xff) & mask
}
}
return ints
}
// String returns a string representation of the Abc instance.
func (abc Abc) String() string {
return fmt.Sprintf("Abc{alphabet='%v')", abc.Alphabet())
}
// Alphabet returns the alphabet used as an immutable string.
func (abc Abc) Alphabet() string {
return string(abc.alphabet)
}

328
lib/store/sqlike/sqlike.go Normal file
View File

@ -0,0 +1,328 @@
package sqlike
import (
"context"
"crypto/sha1"
"database/sql"
"encoding/base64"
"errors"
"fmt"
"html"
"strconv"
"time"
"git.tuxpa.in/a/nat/lib/idgen"
"git.tuxpa.in/a/nat/lib/store"
"github.com/dchest/uniuri"
_ "github.com/go-sql-driver/mysql"
_ "github.com/lib/pq"
_ "github.com/mattn/go-sqlite3"
)
type SqlikeConfig struct {
DBHost string `json:"dbhost"` // Name of your database host
DBName string `json:"dbname"` // Name of your database
DBPassword string `json:"dbpassword"` // The password for the database user
DBPlaceHolder [7]string // ? / $[i] Depending on db driver.
DBPort string `json:"dbport"` // Port of the database
DBTable string `json:"dbtable"` // Name of the table in the database
DBAccountsTable string `json:"dbaccountstable"` // Name of the table in the database
DBType string `json:"dbtype"` // Type of database
DBUser string `json:"dbuser"` // The database user
DisplayName string `json:"displayname"` // Name of your pastebin
}
var _ store.Store = (*Sqlike)(nil)
type Sqlike struct {
config SqlikeConfig
handle *sql.DB
}
func MustNew(config ...SqlikeConfig) *Sqlike {
o, err := New(config...)
if err != nil {
panic(err)
}
return o
}
func New(config ...SqlikeConfig) (*Sqlike, error) {
s := &Sqlike{}
// default settings
s.config.DBType = "sqlite3"
s.config.DBHost = "db.sqlite"
s.config.DBName = "pastebin"
s.config.DBAccountsTable = "accounts"
if len(config) > 0 {
s.config = config[0]
}
if err := s.connect(); err != nil {
return nil, err
}
return s, nil
}
func (s *Sqlike) connect() error {
var dbinfo string
for i := 0; i < 7; i++ {
s.config.DBPlaceHolder[i] = "?"
}
switch s.config.DBType {
case "sqlite3":
dbinfo = s.config.DBName
case "postgres":
dbinfo = fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
s.config.DBHost,
s.config.DBPort,
s.config.DBUser,
s.config.DBPassword,
s.config.DBName)
for i := 0; i < 7; i++ {
s.config.DBPlaceHolder[i] = "$" + strconv.Itoa(i+1)
}
case "mysql":
dbinfo = s.config.DBUser + ":" + s.config.DBPassword + "@tcp(" + s.config.DBHost + ":" + s.config.DBPort + ")/" + s.config.DBName
case "":
return errors.New(" Database error : dbtype not specified in sqlike config")
default:
return errors.New(" Database error : Specified dbtype (" +
s.config.DBType + ") not supported.")
}
db, err := sql.Open(s.config.DBType, dbinfo)
if err != nil {
return err
}
var dummy string
err = db.QueryRow("select id from " + s.config.DBTable + " where id='dummyid'").Scan(&dummy)
switch {
case err == sql.ErrNoRows:
case err != nil:
return err
}
s.handle = db
return nil
}
func shaPaste(paste string) string {
hasher := sha1.New()
hasher.Write([]byte(paste))
sha := base64.URLEncoding.EncodeToString(hasher.Sum(nil))
return sha
}
func (s *Sqlike) SavePaste(ctx context.Context, title string, data string, expiry time.Duration, userKey string) (*store.Response, error) {
var id, hash, delkey string
// Escape user input,
data = html.EscapeString(data)
title = html.EscapeString(title)
userKey = html.EscapeString(userKey)
// Hash paste data and query database to see if paste exists
sha := shaPaste(data)
err := s.handle.QueryRow("select id, title, hash, data, delkey from "+
s.config.DBTable+" where hash="+
s.config.DBPlaceHolder[0], sha).Scan(&id,
&title, &hash, &data, &delkey)
switch {
case err == sql.ErrNoRows:
case err != nil:
return nil, err
default:
return &store.Response{
Status: "Paste data already exists ...",
Id: id,
Title: title,
Sha1: hash,
Size: len(data)}, nil
}
// Generate id,
id = idgen.MustGenerate()
expiretime := time.Now().Add(expiry)
// Set the generated id as title if not given,
if title == "" {
title = id
}
delKey := uniuri.NewLen(40)
// This is needed since mysql/postgres uses different placeholders,
var dbQuery string
for i := 0; i < 7; i++ {
dbQuery += s.config.DBPlaceHolder[i] + ","
}
dbQuery = dbQuery[:len(dbQuery)-1]
stmt, err := s.handle.Prepare("INSERT INTO " + s.config.DBTable + " (id,title,hash,data,delkey,expiry,userid)values(" + dbQuery + ")")
if err != nil {
return nil, err
}
_, err = stmt.Exec(id, title, sha, data, delKey, expiretime, userKey)
if err != nil {
return nil, err
}
stmt.Close()
if err != nil {
return nil, err
}
return &store.Response{
Status: "Successfully saved paste.",
Id: id,
Title: title,
Sha1: hash,
Size: len(data),
DelKey: delKey}, nil
}
func (s *Sqlike) GetUserPastes(ctx context.Context, userKey string) (*store.Pastes, error) {
pst := &store.Pastes{}
rows, err := s.handle.Query("select id, title, delkey, data from "+
s.config.DBTable+" where userid="+
s.config.DBPlaceHolder[0], userKey)
switch {
case err == sql.ErrNoRows:
case err != nil:
return nil, err
default:
defer rows.Close()
for rows.Next() {
var id, title, delKey, data string
rows.Scan(&id, &title, &delKey, &data)
res := store.Response{
Id: id,
Title: title,
Size: len(data),
DelKey: delKey}
pst.Response = append(pst.Response, res)
}
}
return pst, nil
}
func (s *Sqlike) GetUserKey(ctx context.Context, email string) (string, error) {
var user_key string
err := s.handle.QueryRowContext(ctx, "select key from "+s.config.DBAccountsTable+
" where email="+s.config.DBPlaceHolder[0], email).
Scan(&user_key)
if err != nil {
return "", err
}
return user_key, nil
}
func (s *Sqlike) GetPaste(ctx context.Context, pasteId string) (*store.Response, error) {
var title, paste string
var expiry int64
err := s.handle.QueryRowContext(ctx, "select title, data, expiry from "+
s.config.DBTable+" where id="+s.config.DBPlaceHolder[0],
pasteId).Scan(&title, &paste, &expiry)
switch {
case err == sql.ErrNoRows:
return &store.Response{Status: "Requested paste doesn't exist."}, nil
case err != nil:
return nil, err
}
expiretime := time.Unix(expiry, 0)
if expiry == 0 {
expiretime = time.Time{}
}
// Check if paste is overdue,
ok := time.Now().After(expiretime)
if err != nil || !ok {
return &store.Response{Status: "Requested paste doesn't exist."}, nil
}
// Unescape the saved data,
paste = html.UnescapeString(paste)
title = html.UnescapeString(title)
expiryS := "Never"
if expiry != 0 {
expiryS = time.Unix(expiry, 0).Format("2006-01-02 15:04:05")
}
r := &store.Response{
Status: "Success",
Id: pasteId,
Title: title,
Paste: paste,
Size: len(paste),
Expiry: expiryS}
return r, nil
}
func (s *Sqlike) ForceDelPaste(ctx context.Context, pasteId string) error {
stmt, err := s.handle.PrepareContext(ctx, "delete from pastebin where id="+
s.config.DBPlaceHolder[0])
if err != nil {
return err
}
defer stmt.Close()
// Execute it,
_, err = stmt.ExecContext(ctx, pasteId)
if err != nil {
return err
}
return nil
}
func (s *Sqlike) RegisterUser(ctx context.Context, email string, hashpass []byte) error {
var dbQuery string
for i := 0; i < 3; i++ {
dbQuery += s.config.DBPlaceHolder[i] + ","
}
dbQuery = dbQuery[:len(dbQuery)-1]
stmt, err := s.handle.PrepareContext(ctx, "INSERT into "+s.config.DBAccountsTable+"(email, password, key) values("+dbQuery+")")
defer stmt.Close()
if err != nil {
return err
}
key := idgen.MustGenerate()
_, err = stmt.ExecContext(ctx, email, hashpass, key)
if err != nil {
return err
}
return nil
}
func (s *Sqlike) DelPaste(ctx context.Context, pasteId, delKey string) error {
stmt, err := s.handle.PrepareContext(ctx, "delete from pastebin where delkey="+
s.config.DBPlaceHolder[0]+" and id="+
s.config.DBPlaceHolder[1])
if err != nil {
return err
}
defer stmt.Close()
res, err := stmt.ExecContext(ctx, delKey, pasteId)
_, err = res.RowsAffected()
if err == sql.ErrNoRows {
return nil
}
if err != nil {
return err
}
return nil
}
func (s *Sqlike) HasAccount(ctx context.Context, email string) ([]byte, error) {
var hashedPassword []byte
err := s.handle.QueryRowContext(ctx, "select password from "+s.config.DBAccountsTable+
" where email="+s.config.DBPlaceHolder[0], email).
Scan(&hashedPassword)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
return hashedPassword, nil
}

39
lib/store/store.go Normal file
View File

@ -0,0 +1,39 @@
package store
import (
"context"
"time"
)
// A request to the store will be this json struct.
type Response struct {
DelKey string `json:"delkey"` // The id to use when delete a paste
Expiry string `json:"expiry"` // The date when post expires
Extra string `json:"extra"` // Extra output from the highlight-wrapper
Id string `json:"id"` // The id of the paste
Lang string `json:"lang"` // Specified language
Paste string `json:"paste"` // The eactual paste data
Sha1 string `json:"sha1"` // The sha1 of the paste
Size int `json:"size"` // The length of the paste
Status string `json:"status"` // A custom status message
Style string `json:"style"` // Specified style
Title string `json:"title"` // The title of the paste
}
type Pastes struct {
Response []Response
}
type Store interface {
GetUserKey(ctx context.Context, email string) (string, error)
GetUserPastes(ctx context.Context, userKey string) (*Pastes, error)
GetPaste(ctx context.Context, pasteId string) (*Response, error)
SavePaste(ctx context.Context, title string, data string, expiry time.Duration, userKey string) (*Response, error)
ForceDelPaste(ctx context.Context, pasteId string) error
DelPaste(ctx context.Context, pasteId, delKey string) error
HasAccount(ctx context.Context, email string) ([]byte, error)
RegisterUser(ctx context.Context, email string, hashpass []byte) error
}

218
lib/styler/styler.go Normal file
View File

@ -0,0 +1,218 @@
package styler
import (
"bufio"
"bytes"
"fmt"
"log"
"os"
"os/exec"
"strings"
"unicode/utf8"
)
type Highlighter string
const (
HIGHLIGHTER_NONE Highlighter = "none"
HIGHLIGHTER_PYTHON Highlighter = "python"
HIGHLIGHTER_GO Highlighter = "go"
)
func (h Highlighter) Cmd() string {
switch h {
case HIGHLIGHTER_PYTHON:
return ".highlighers/highlighter-wrapper.py"
case HIGHLIGHTER_GO:
return ".highlighers/highligher.gobin"
case HIGHLIGHTER_NONE:
fallthrough
default:
return ""
}
}
type Styler struct {
listOfLangsFirst map[string]string
listOfLangsLast map[string]string
listOfStyles map[string]string
config StylerConfig
}
func (s *Styler) Legacy() [3]map[string]string {
return [3]map[string]string{
s.listOfLangsFirst,
s.listOfLangsLast,
s.listOfStyles,
}
}
type StylerConfig struct {
Highlighter Highlighter `json:"highlighter"` // The name of the highlighter.
}
func New(config ...StylerConfig) *Styler {
s := &Styler{
listOfLangsFirst: make(map[string]string),
listOfLangsLast: make(map[string]string),
listOfStyles: make(map[string]string),
}
// default settings
s.config.Highlighter = "none"
if len(config) > 0 {
s.config = config[0]
}
return s
}
func isBad(s string) bool {
for i := 0; i < len(s); i++ {
b := s[i]
if ('a' <= b && b <= 'z') ||
('A' <= b && b <= 'Z') ||
('0' <= b && b <= '9') ||
b == ' ' {
continue
} else {
return false
}
}
return true
}
func sanitize(s string) string {
if !utf8.ValidString(s) {
return s
}
if len(s) < 30 {
return s
}
if !isBad(s) {
return s
}
return ""
}
func (s *Styler) Highlight(paste string, lang string, style string) (string, string, string, string, error) {
var supported_lang, supported_styles bool
lang = sanitize(lang)
style = sanitize(style)
lang, supported_lang = s.listOfLangsFirst[lang]
style, supported_styles = s.listOfStyles[style]
lang = sanitize(lang)
style = sanitize(style)
if lang == "" {
lang = "autodetect"
}
if !supported_lang && lang != "autodetect" {
lang = "text"
}
// Same with the styles,
if !supported_styles {
style = "autodetect"
}
switch s.config.Highlighter {
case HIGHLIGHTER_PYTHON:
if _, err := os.Stat(s.config.Highlighter.Cmd()); os.IsNotExist(err) {
return "", "", "", "", err
}
cmd := exec.Command(s.config.Highlighter.Cmd(), lang, style)
cmd.Stdin = strings.NewReader(paste)
var stdout bytes.Buffer
cmd.Stdout = &stdout
var stderr bytes.Buffer
cmd.Stderr = &stderr
err := cmd.Run()
if err != nil {
return "", "", "", "", err
}
return stdout.String(), stderr.String(), lang, style, nil
case "none":
fallthrough
default:
return paste, "", lang, style, nil
}
}
func (s *Styler) loadSupportedStyles() {
switch s.config.Highlighter {
case HIGHLIGHTER_PYTHON:
arg := "getstyles"
out, err := exec.Command(s.config.Highlighter.Cmd(), arg).Output()
if err != nil {
log.Fatal(err)
}
// Loop lexers and add them to respectively map,
for _, line := range strings.Split(string(out), "\n") {
if line == "" {
continue
}
s.listOfStyles[line] = strings.Title(line)
}
case "none":
fallthrough
default:
return
}
}
func (st *Styler) loadSupportedLangs() error {
switch st.config.Highlighter {
case HIGHLIGHTER_PYTHON:
var prioLexers map[string]string
// Initialize maps,
prioLexers = make(map[string]string)
// Get prioritized lexers and put them in a separate map,
file, err := os.Open("static/prio-lexers")
if err != nil {
return err
}
scanner := bufio.NewScanner(file)
for scanner.Scan() {
prioLexers[scanner.Text()] = "1"
}
if err := scanner.Err(); err != nil {
log.Fatal(err)
}
file.Close()
arg := "getlexers"
out, err := exec.Command(st.config.Highlighter.Cmd(), arg).Output()
if err != nil {
return fmt.Errorf("%w: %s", err, string(out))
}
// Loop lexers and add them to respectively map,
for _, line := range strings.Split(string(out), "\n") {
if line == "" {
continue
}
s := strings.Split(line, ";")
if len(s) != 2 {
os.Exit(1)
}
s[0] = strings.Title(s[0])
if prioLexers[s[0]] == "1" {
st.listOfLangsFirst[s[0]] = s[1]
} else {
st.listOfLangsLast[s[0]] = s[1]
}
}
return nil
case "none":
fallthrough
default:
return nil
}
}

File diff suppressed because it is too large Load Diff