mster
This commit is contained in:
parent
7d2e1e2c25
commit
3c63282dd9
19
LICENSE.md
19
LICENSE.md
|
@ -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.
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
|
|
17
database.sql
17
database.sql
|
@ -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`)
|
||||
);
|
|
@ -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
|
||||
)
|
|
@ -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=
|
|
@ -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)
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
1185
pastebin.go
1185
pastebin.go
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue