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",
|
"address": "http://localhost:9999",
|
||||||
|
"dbtype": "sql",
|
||||||
"dbhost": "",
|
"dbhost": "",
|
||||||
"dbname": "pastebin.db",
|
"dbname": "pastebin.db",
|
||||||
"dbtable": "pastebin",
|
"dbtable": "pastebin",
|
||||||
"dbaccountstable": "accounts",
|
"dbaccountstable": "accounts",
|
||||||
"dbtype": "sqlite3",
|
|
||||||
"dbport": "",
|
"dbport": "",
|
||||||
"dbuser":"",
|
"dbuser":"",
|
||||||
"dbpassword":"",
|
"dbpassword":"",
|
||||||
"displayname": "MyCompany",
|
"displayname": "MyCompany",
|
||||||
"listenaddress": "localhost",
|
"listenaddress": "localhost",
|
||||||
"listenport": "9999",
|
"listenport": "9999",
|
||||||
"shorturllength": "5",
|
"shorturllength": "5"
|
||||||
"highlighter":"./highlighter-wrapper.py",
|
|
||||||
"googleAPIKey":"insert-if-you-want-goo.gl/addr"
|
|
||||||
}
|
}
|
||||||
|
|
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