diff --git a/main.go b/main.go new file mode 100755 index 0000000..0f58e9f --- /dev/null +++ b/main.go @@ -0,0 +1,134 @@ +package main + +import ( + "html/template" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" +) + +func main() { + router := gin.Default() + router.LoadHTMLGlob("templates/*") + + router.GET("/", func(c *gin.Context) { + c.Redirect(302, "/"+randomAlliterateCombo()) + }) + router.GET("/:page", func(c *gin.Context) { + page := c.Param("page") + c.Redirect(302, "/"+page+"/edit") + }) + router.GET("/:page/:command", handlePageRequest) + router.POST("/update", handlePageUpdate) + router.POST("/prime", handlePrime) + router.POST("/lock", handleLock) + + router.Run(":8050") +} + +func handlePageRequest(c *gin.Context) { + page := c.Param("page") + command := c.Param("command") + version := c.DefaultQuery("version", "ajksldfjl") + p := Open(page) + if p.IsPrimedForSelfDestruct && !p.IsLocked { + p.Update("*This page has now self-destructed.*\n\n" + p.Text.GetCurrent()) + p.Erase() + } + if command == "erase" && !p.IsLocked { + p.Erase() + c.Redirect(302, "/"+page+"/edit") + } + rawText := p.Text.GetCurrent() + rawHTML := p.RenderedPage + + // Check to see if an old version is requested + versionInt, versionErr := strconv.Atoi(version) + if versionErr == nil && versionInt > 0 { + versionText, err := p.Text.GetPreviousByTimestamp(int64(versionInt)) + if err == nil { + rawText = versionText + rawHTML = MarkdownToHtml(rawText) + } + } + c.HTML(http.StatusOK, "index.html", gin.H{ + "EditPage": command == "edit", + "ViewPage": command == "view", + "ListPage": command == "list", + "HistoryPage": command == "history", + "Page": p.Name, + "RenderedPage": template.HTML([]byte(rawHTML)), + "RawPage": rawText, + "Versions": p.Text.GetSnapshots(), + "IsLocked": p.IsLocked, + "IsEncrypted": p.IsEncrypted, + }) +} + +func handlePageUpdate(c *gin.Context) { + type QueryJSON struct { + Page string `json:"page"` + NewText string `json:"new_text"` + } + var json QueryJSON + if c.BindJSON(&json) != nil { + c.String(http.StatusBadRequest, "Problem binding keys") + return + } + log.Trace("Update: %v", json) + p := Open(json.Page) + if !p.IsLocked { + p.Update(json.NewText) + p.Save() + c.JSON(http.StatusOK, gin.H{"success": true, "message": "Saved"}) + } else { + c.JSON(http.StatusOK, gin.H{"success": false, "message": "Locked"}) + } +} + +func handlePrime(c *gin.Context) { + type QueryJSON struct { + Page string `json:"page"` + } + var json QueryJSON + if c.BindJSON(&json) != nil { + c.String(http.StatusBadRequest, "Problem binding keys") + return + } + log.Trace("Update: %v", json) + p := Open(json.Page) + p.IsPrimedForSelfDestruct = true + p.Save() + c.JSON(http.StatusOK, gin.H{"success": true}) +} + +func handleLock(c *gin.Context) { + type QueryJSON struct { + Page string `json:"page"` + Passphrase string `json:"passphrase"` + } + + var json QueryJSON + if c.BindJSON(&json) != nil { + c.String(http.StatusBadRequest, "Problem binding keys") + return + } + p := Open(json.Page) + var message string + if p.IsLocked { + err2 := CheckPasswordHash(json.Passphrase, p.PassphraseToUnlock) + if err2 != nil { + c.JSON(http.StatusOK, gin.H{"success": false, "message": "Can't unlock"}) + return + } + p.IsLocked = false + message = "Unlocked" + } else { + p.IsLocked = true + p.PassphraseToUnlock = HashPassword(json.Passphrase) + message = "Locked" + } + p.Save() + c.JSON(http.StatusOK, gin.H{"success": true, "message": message}) +} diff --git a/page.go b/page.go new file mode 100755 index 0000000..0b99bba --- /dev/null +++ b/page.go @@ -0,0 +1,95 @@ +package main + +import ( + "encoding/base32" + "encoding/json" + "io/ioutil" + "os" + "path" + + "github.com/microcosm-cc/bluemonday" + "github.com/russross/blackfriday" + "github.com/schollz/versionedtext" +) + +var pathToData = "data" + +func init() { + os.MkdirAll(pathToData, 0755) +} + +type Page struct { + Name string + Text versionedtext.VersionedText + RenderedPage string + IsLocked bool + PassphraseToUnlock string + IsEncrypted bool + IsPrimedForSelfDestruct bool +} + +func Open(name string) (p *Page) { + p = new(Page) + p.Name = name + p.Text = versionedtext.NewVersionedText("") + p.Render() + bJSON, err := ioutil.ReadFile(path.Join(pathToData, encodeToBase32(name)+".json")) + if err != nil { + return + } + err = json.Unmarshal(bJSON, &p) + if err != nil { + p = new(Page) + } + return p +} + +func (p *Page) Update(newText string) error { + p.Text.Update(newText) + p.Render() + return p.Save() +} + +func (p *Page) Render() { + if p.IsEncrypted { + p.RenderedPage = "" + p.Text.GetCurrent() + "" + return + } + p.RenderedPage = MarkdownToHtml(p.Text.GetCurrent()) +} + +func MarkdownToHtml(s string) string { + unsafe := blackfriday.MarkdownCommon([]byte(s)) + pClean := bluemonday.UGCPolicy() + pClean.AllowElements("img") + pClean.AllowAttrs("alt").OnElements("img") + pClean.AllowAttrs("src").OnElements("img") + pClean.AllowAttrs("class").OnElements("a") + pClean.AllowAttrs("href").OnElements("a") + pClean.AllowAttrs("id").OnElements("a") + pClean.AllowDataURIImages() + html := pClean.SanitizeBytes(unsafe) + return string(html) +} + +func (p *Page) Save() error { + bJSON, err := json.MarshalIndent(p, "", " ") + if err != nil { + return err + } + return ioutil.WriteFile(path.Join(pathToData, encodeToBase32(p.Name)+".json"), bJSON, 0755) +} + +func (p *Page) Erase() error { + return os.Remove(path.Join(pathToData, encodeToBase32(p.Name)+".json")) +} + +func encodeToBase32(s string) string { + return base32.StdEncoding.EncodeToString([]byte(s)) +} + +func decodeFromBase32(s string) (s2 string, err error) { + bString, err := base32.StdEncoding.DecodeString(s) + s2 = string(bString) + return +} diff --git a/page_test.go b/page_test.go new file mode 100755 index 0000000..bf767b1 --- /dev/null +++ b/page_test.go @@ -0,0 +1,31 @@ +package main + +import ( + // "fmt" + "os" + "strings" + "testing" +) + +func TestGeneral(t *testing.T) { + defer os.RemoveAll("data") + p := Open("testpage") + err := p.Update("**bold**") + if err != nil { + t.Error(err) + } + if strings.TrimSpace(p.RenderedPage) != "

bold

" { + t.Errorf("Did not render: '%s'", p.RenderedPage) + } + err = p.Update("**bold** and *italic*") + if err != nil { + t.Error(err) + } + p.Save() + + p2 := Open("testpage") + if strings.TrimSpace(p2.RenderedPage) != "

bold and italic

" { + t.Errorf("Did not render: '%s'", p2.RenderedPage) + } + +} diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..54c3ca3 --- /dev/null +++ b/utils.go @@ -0,0 +1,203 @@ +package main + +import ( + "crypto/sha256" + "encoding/binary" + "encoding/hex" + "io/ioutil" + "math/rand" + "net" + "strings" + "time" + + "golang.org/x/crypto/bcrypt" + + "github.com/jcelliott/lumber" + "github.com/schollz/cryptopasta" + "github.com/sergi/go-diff/diffmatchpatch" +) + +var animals []string +var adjectives []string +var aboutPageText string + +var log *lumber.ConsoleLogger + +func init() { + rand.Seed(time.Now().Unix()) + animalsText, _ := ioutil.ReadFile("./static/text/animals") + animals = strings.Split(string(animalsText), ",") + adjectivesText, _ := ioutil.ReadFile("./static/text/adjectives") + adjectives = strings.Split(string(adjectivesText), "\n") + log = lumber.NewConsoleLogger(lumber.TRACE) +} + +func randomAnimal() string { + return strings.Replace(strings.Title(animals[rand.Intn(len(animals)-1)]), " ", "", -1) +} + +func randomAdjective() string { + return strings.Replace(strings.Title(adjectives[rand.Intn(len(adjectives)-1)]), " ", "", -1) +} + +func randomAlliterateCombo() (combo string) { + combo = "" + // // first determine which names are taken from program data + // takenNames := []string{} + // err := db.View(func(tx *bolt.Tx) error { + // // Assume bucket exists and has keys + // b := tx.Bucket([]byte("programdata")) + // c := b.Cursor() + // for k, v := c.First(); k != nil; k, v = c.Next() { + // takenNames = append(takenNames, strings.ToLower(string(v))) + // } + // return nil + // }) + // if err != nil { + // panic(err) + // } + // fmt.Println(takenNames) + // generate random alliteration thats not been used + for { + animal := randomAnimal() + adjective := randomAdjective() + if animal[0] == adjective[0] { //&& stringInSlice(strings.ToLower(adjective+animal), takenNames) == false { + combo = adjective + animal + break + } + } + return +} + +// is there a string in a slice? +func stringInSlice(s string, strings []string) bool { + for _, k := range strings { + if s == k { + return true + } + } + return false +} + +// itob returns an 8-byte big endian representation of v. +func itob(v int) []byte { + b := make([]byte, 8) + binary.BigEndian.PutUint64(b, uint64(v)) + return b +} + +func contentType(filename string) string { + switch { + case strings.Contains(filename, ".css"): + return "text/css" + case strings.Contains(filename, ".jpg"): + return "image/jpeg" + case strings.Contains(filename, ".png"): + return "image/png" + case strings.Contains(filename, ".js"): + return "application/javascript" + } + return "text/html" +} + +func diffRebuildtexts(diffs []diffmatchpatch.Diff) []string { + text := []string{"", ""} + for _, myDiff := range diffs { + if myDiff.Type != diffmatchpatch.DiffInsert { + text[0] += myDiff.Text + } + if myDiff.Type != diffmatchpatch.DiffDelete { + text[1] += myDiff.Text + } + } + return text +} + +func timeTrack(start time.Time, name string) { + elapsed := time.Since(start) + log.Debug("%s took %s", name, elapsed) +} + +var src = rand.NewSource(time.Now().UnixNano()) + +const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" +const ( + letterIdxBits = 6 // 6 bits to represent a letter index + letterIdxMask = 1<= 0; { + if remain == 0 { + cache, remain = src.Int63(), letterIdxMax + } + if idx := int(cache & letterIdxMask); idx < len(letterBytes) { + b[i] = letterBytes[idx] + i-- + } + cache >>= letterIdxBits + remain-- + } + + return string(b) +} + +// GetLocalIP returns the local ip address +func GetLocalIP() string { + addrs, err := net.InterfaceAddrs() + if err != nil { + return "" + } + bestIP := "" + for _, address := range addrs { + // check the address type and if it is not a loopback the display it + if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { + if ipnet.IP.To4() != nil { + return ipnet.IP.String() + } + } + } + return bestIP +} + +// HashPassword generates a bcrypt hash of the password using work factor 14. +// https://github.com/gtank/cryptopasta/blob/master/hash.go +func HashPassword(password string) string { + hash, _ := bcrypt.GenerateFromPassword([]byte(password), 14) + return hex.EncodeToString(hash) +} + +// CheckPassword securely compares a bcrypt hashed password with its possible +// plaintext equivalent. Returns nil on success, or an error on failure. +// https://github.com/gtank/cryptopasta/blob/master/hash.go +func CheckPasswordHash(password, hashedString string) error { + hash, err := hex.DecodeString(hashedString) + if err != nil { + return err + } + return bcrypt.CompareHashAndPassword(hash, []byte(password)) +} + +func EncryptString(toEncrypt string, password string) (string, error) { + key := sha256.Sum256([]byte(password)) + encrypted, err := cryptopasta.Encrypt([]byte(toEncrypt), &key) + if err != nil { + return "", err + } + + return hex.EncodeToString(encrypted), nil +} + +func DecryptString(toDecrypt string, password string) (string, error) { + key := sha256.Sum256([]byte(password)) + contentData, err := hex.DecodeString(toDecrypt) + if err != nil { + return "", err + } + bDecrypted, err := cryptopasta.Decrypt(contentData, &key) + return string(bDecrypted), err +} diff --git a/utils_test.go b/utils_test.go new file mode 100755 index 0000000..51728ea --- /dev/null +++ b/utils_test.go @@ -0,0 +1,38 @@ +package main + +import ( + "testing" +) + +func TestHashing(t *testing.T) { + p := HashPassword("1234") + log.Debug(p) + err := CheckPasswordHash("1234", p) + if err != nil { + t.Errorf("Should be correct password") + } + err = CheckPasswordHash("1234lkjklj", p) + if err == nil { + t.Errorf("Should NOT be correct password") + } +} + +func TestEncryption(t *testing.T) { + s, err := EncryptString("some string", "some password") + if err != nil { + t.Errorf("What") + } + log.Debug(s) + d, err := DecryptString(s, "some wrong password") + if err == nil { + t.Errorf("Should throw error for bad password") + } + d, err = DecryptString(s, "some password") + if err != nil { + t.Errorf("Should not throw password") + } + if d != "some string" { + t.Errorf("Problem decoding") + } + +}