maybe 0.1
This commit is contained in:
commit
7420c88f84
|
@ -0,0 +1,8 @@
|
|||
|
||||
all: honk
|
||||
|
||||
honk: *.go
|
||||
go build -o honk
|
||||
|
||||
clean:
|
||||
rm -f honk
|
|
@ -0,0 +1,38 @@
|
|||
honk honk
|
||||
|
||||
-- features
|
||||
|
||||
Take control of your honks and join the federation in the fight against the
|
||||
evil empire.
|
||||
|
||||
Send honks. Receive honks. And not just honks.
|
||||
Bonk, donk, tonk, all your favorite activities are here.
|
||||
|
||||
Purple color scheme.
|
||||
|
||||
The button to submit a new honk says "it's gonna be honked".
|
||||
|
||||
Ein Honk is a stupid person auf deutsch.
|
||||
|
||||
-- requirements
|
||||
|
||||
github.com/gorilla/mux
|
||||
golang.org/x/crypto
|
||||
golang.org/x/net
|
||||
golang.org/x/text
|
||||
humungus.tedunangst.com/r/go-sqlite3
|
||||
|
||||
It should be sufficient to type make after unpacking a release under go/src.
|
||||
Mind your gopath.
|
||||
|
||||
Even on a fast machine, building from source can take several seconds.
|
||||
|
||||
Busy honk instances may require megabytes of memory.
|
||||
|
||||
-- setup
|
||||
|
||||
./honk init
|
||||
|
||||
./honk
|
||||
|
||||
honk expects to be fronted by a TLS terminating reverse proxy.
|
|
@ -0,0 +1,629 @@
|
|||
//
|
||||
// Copyright (c) 2019 Ted Unangst <tedu@tedunangst.com>
|
||||
//
|
||||
// Permission to use, copy, modify, and distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
func NewJunk() map[string]interface{} {
|
||||
return make(map[string]interface{})
|
||||
}
|
||||
|
||||
func WriteJunk(w io.Writer, j map[string]interface{}) error {
|
||||
e := json.NewEncoder(w)
|
||||
e.SetEscapeHTML(false)
|
||||
e.SetIndent("", " ")
|
||||
err := e.Encode(j)
|
||||
return err
|
||||
}
|
||||
|
||||
func ReadJunk(r io.Reader) (map[string]interface{}, error) {
|
||||
decoder := json.NewDecoder(r)
|
||||
var j map[string]interface{}
|
||||
err := decoder.Decode(&j)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return j, nil
|
||||
}
|
||||
|
||||
var theonetruename = `application/ld+json; profile="https://www.w3.org/ns/activitystreams"`
|
||||
var falsenames = []string{
|
||||
`application/ld+json`,
|
||||
`application/activity+json`,
|
||||
}
|
||||
var itiswhatitis = "https://www.w3.org/ns/activitystreams"
|
||||
var thewholeworld = "https://www.w3.org/ns/activitystreams#Public"
|
||||
|
||||
func friendorfoe(ct string) bool {
|
||||
ct = strings.ToLower(ct)
|
||||
for _, at := range falsenames {
|
||||
if strings.HasPrefix(ct, at) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func PostJunk(keyname string, key *rsa.PrivateKey, url string, j map[string]interface{}) error {
|
||||
client := http.DefaultClient
|
||||
var buf bytes.Buffer
|
||||
WriteJunk(&buf, j)
|
||||
req, err := http.NewRequest("POST", url, &buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
zig(keyname, key, req, buf.Bytes())
|
||||
req.Header.Set("Content-Type", theonetruename)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode != 200 && resp.StatusCode != 202 {
|
||||
resp.Body.Close()
|
||||
return fmt.Errorf("http post status: %d", resp.StatusCode)
|
||||
}
|
||||
log.Printf("successful post: %s %d", url, resp.StatusCode)
|
||||
return nil
|
||||
}
|
||||
|
||||
type gzCloser struct {
|
||||
r *gzip.Reader
|
||||
under io.ReadCloser
|
||||
}
|
||||
|
||||
func (gz *gzCloser) Read(p []byte) (int, error) {
|
||||
return gz.r.Read(p)
|
||||
}
|
||||
|
||||
func (gz *gzCloser) Close() error {
|
||||
defer gz.under.Close()
|
||||
return gz.r.Close()
|
||||
}
|
||||
|
||||
func GetJunk(url string) (map[string]interface{}, error) {
|
||||
client := http.DefaultClient
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Accept", theonetruename)
|
||||
req.Header.Set("Accept-Encoding", "gzip")
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
resp.Body.Close()
|
||||
return nil, fmt.Errorf("http get status: %d", resp.StatusCode)
|
||||
}
|
||||
if strings.EqualFold(resp.Header.Get("Content-Encoding"), "gzip") {
|
||||
gz, err := gzip.NewReader(resp.Body)
|
||||
if err != nil {
|
||||
resp.Body.Close()
|
||||
return nil, err
|
||||
}
|
||||
resp.Body = &gzCloser{r: gz, under: resp.Body}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
j, err := ReadJunk(resp.Body)
|
||||
return j, err
|
||||
}
|
||||
|
||||
func jsonfindinterface(ii interface{}, keys []string) interface{} {
|
||||
for _, key := range keys {
|
||||
idx, err := strconv.Atoi(key)
|
||||
if err == nil {
|
||||
m := ii.([]interface{})
|
||||
if idx >= len(m) {
|
||||
return nil
|
||||
}
|
||||
ii = m[idx]
|
||||
} else {
|
||||
m := ii.(map[string]interface{})
|
||||
ii = m[key]
|
||||
if ii == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return ii
|
||||
}
|
||||
func jsonfindstring(j interface{}, keys []string) (string, bool) {
|
||||
s, ok := jsonfindinterface(j, keys).(string)
|
||||
return s, ok
|
||||
}
|
||||
func jsonfindarray(j interface{}, keys []string) ([]interface{}, bool) {
|
||||
a, ok := jsonfindinterface(j, keys).([]interface{})
|
||||
return a, ok
|
||||
}
|
||||
func jsonfindmap(j interface{}, keys []string) (map[string]interface{}, bool) {
|
||||
m, ok := jsonfindinterface(j, keys).(map[string]interface{})
|
||||
return m, ok
|
||||
}
|
||||
func jsongetstring(j interface{}, key string) (string, bool) {
|
||||
return jsonfindstring(j, []string{key})
|
||||
}
|
||||
func jsongetarray(j interface{}, key string) ([]interface{}, bool) {
|
||||
return jsonfindarray(j, []string{key})
|
||||
}
|
||||
func jsongetmap(j interface{}, key string) (map[string]interface{}, bool) {
|
||||
return jsonfindmap(j, []string{key})
|
||||
}
|
||||
|
||||
func sha256string(s string) string {
|
||||
hasher := sha256.New()
|
||||
io.WriteString(hasher, s)
|
||||
sum := hasher.Sum(nil)
|
||||
return fmt.Sprintf("%x", sum)
|
||||
}
|
||||
|
||||
func savedonk(url string, name, media string) *Donk {
|
||||
log.Printf("saving donk: %s", url)
|
||||
var donk Donk
|
||||
row := stmtFindFile.QueryRow(url)
|
||||
err := row.Scan(&donk.FileID)
|
||||
if err == nil {
|
||||
return &donk
|
||||
}
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
log.Printf("err querying: %s", err)
|
||||
}
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
log.Printf("errer fetching %s: %s", url, err)
|
||||
return nil
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
return nil
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
io.Copy(&buf, resp.Body)
|
||||
|
||||
xid := xfiltrate()
|
||||
|
||||
res, err := stmtSaveFile.Exec(xid, name, url, media, buf.Bytes())
|
||||
if err != nil {
|
||||
log.Printf("error saving file %s: %s", url, err)
|
||||
return nil
|
||||
}
|
||||
donk.FileID, _ = res.LastInsertId()
|
||||
return &donk
|
||||
}
|
||||
|
||||
func needxonk(userid int64, x *Honk) bool {
|
||||
row := stmtFindXonk.QueryRow(userid, x.XID, x.What)
|
||||
err := row.Scan(&x.ID)
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
if err != sql.ErrNoRows {
|
||||
log.Printf("err querying xonk: %s", err)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func savexonk(x *Honk) {
|
||||
if x.What == "eradicate" {
|
||||
log.Printf("eradicating %s by %s", x.RID, x.Honker)
|
||||
_, err := stmtDeleteHonk.Exec(x.RID, x.Honker)
|
||||
if err != nil {
|
||||
log.Printf("error eradicating: %s", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
dt := x.Date.UTC().Format(dbtimeformat)
|
||||
aud := strings.Join(x.Audience, " ")
|
||||
res, err := stmtSaveHonk.Exec(x.UserID, x.What, x.Honker, x.XID, x.RID, dt, x.URL, aud, x.Noise)
|
||||
if err != nil {
|
||||
log.Printf("err saving xonk: %s", err)
|
||||
return
|
||||
}
|
||||
x.ID, _ = res.LastInsertId()
|
||||
for _, d := range x.Donks {
|
||||
_, err = stmtSaveDonk.Exec(x.ID, d.FileID)
|
||||
if err != nil {
|
||||
log.Printf("err saving donk: %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var boxofboxes = make(map[string]string)
|
||||
var boxlock sync.Mutex
|
||||
|
||||
func getboxes(ident string) (string, string, error) {
|
||||
boxlock.Lock()
|
||||
defer boxlock.Unlock()
|
||||
b, ok := boxofboxes[ident]
|
||||
if ok {
|
||||
if b == "" {
|
||||
return "", "", fmt.Errorf("error?")
|
||||
}
|
||||
m := strings.Split(b, "\n")
|
||||
return m[0], m[1], nil
|
||||
}
|
||||
j, err := GetJunk(ident)
|
||||
if err != nil {
|
||||
boxofboxes[ident] = ""
|
||||
return "", "", err
|
||||
}
|
||||
inbox, _ := jsongetstring(j, "inbox")
|
||||
outbox, _ := jsongetstring(j, "outbox")
|
||||
boxofboxes[ident] = inbox + "\n" + outbox
|
||||
return inbox, outbox, err
|
||||
}
|
||||
|
||||
func peeppeep() {
|
||||
user, _ := butwhatabout("")
|
||||
honkers := gethonkers(user.ID)
|
||||
for _, f := range honkers {
|
||||
if f.Flavor != "peep" {
|
||||
continue
|
||||
}
|
||||
log.Printf("getting updates: %s", f.XID)
|
||||
_, outbox, err := getboxes(f.XID)
|
||||
if err != nil {
|
||||
log.Printf("error getting outbox: %s", err)
|
||||
continue
|
||||
}
|
||||
log.Printf("getting outbox")
|
||||
j, err := GetJunk(outbox)
|
||||
if err != nil {
|
||||
log.Printf("err: %s", err)
|
||||
continue
|
||||
}
|
||||
t, _ := jsongetstring(j, "type")
|
||||
if t == "OrderedCollection" {
|
||||
items, _ := jsongetarray(j, "orderedItems")
|
||||
if items == nil {
|
||||
page1, _ := jsongetstring(j, "first")
|
||||
j, err = GetJunk(page1)
|
||||
if err != nil {
|
||||
log.Printf("err: %s", err)
|
||||
continue
|
||||
}
|
||||
items, _ = jsongetarray(j, "orderedItems")
|
||||
}
|
||||
|
||||
for _, item := range items {
|
||||
xonk := xonkxonk(item)
|
||||
if xonk != nil && needxonk(user.ID, xonk) {
|
||||
xonk.UserID = user.ID
|
||||
savexonk(xonk)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newphone(a []string, obj map[string]interface{}) []string {
|
||||
for _, addr := range []string{"to", "cc", "attributedTo"} {
|
||||
who, _ := jsongetstring(obj, addr)
|
||||
if who != "" {
|
||||
a = append(a, who)
|
||||
}
|
||||
whos, _ := jsongetarray(obj, addr)
|
||||
for _, w := range whos {
|
||||
who, _ := w.(string)
|
||||
if who != "" {
|
||||
a = append(a, who)
|
||||
}
|
||||
}
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
func oneofakind(a []string) []string {
|
||||
var x []string
|
||||
for n, s := range a {
|
||||
for i := n + 1; i < len(a); i++ {
|
||||
if a[i] == s {
|
||||
a[i] = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, s := range a {
|
||||
if s != "" {
|
||||
x = append(x, s)
|
||||
}
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
func xonkxonk(item interface{}) *Honk {
|
||||
|
||||
// id, _ := jsongetstring(item, "id")
|
||||
what, _ := jsongetstring(item, "type")
|
||||
dt, _ := jsongetstring(item, "published")
|
||||
|
||||
var audience []string
|
||||
var err error
|
||||
var xid, rid, url, content string
|
||||
var obj map[string]interface{}
|
||||
switch what {
|
||||
case "Announce":
|
||||
xid, _ = jsongetstring(item, "object")
|
||||
log.Printf("getting bonk: %s", xid)
|
||||
obj, err = GetJunk(xid)
|
||||
if err != nil {
|
||||
log.Printf("error regetting: %s", err)
|
||||
}
|
||||
what = "bonk"
|
||||
case "Create":
|
||||
obj, _ = jsongetmap(item, "object")
|
||||
what = "honk"
|
||||
case "Delete":
|
||||
obj, _ = jsongetmap(item, "object")
|
||||
what = "eradicate"
|
||||
default:
|
||||
log.Printf("unknown activity: %s", what)
|
||||
return nil
|
||||
}
|
||||
who, _ := jsongetstring(item, "actor")
|
||||
|
||||
var xonk Honk
|
||||
if obj != nil {
|
||||
ot, _ := jsongetstring(obj, "type")
|
||||
url, _ = jsongetstring(obj, "url")
|
||||
if ot == "Note" {
|
||||
audience = newphone(audience, obj)
|
||||
xid, _ = jsongetstring(obj, "id")
|
||||
content, _ = jsongetstring(obj, "content")
|
||||
rid, _ = jsongetstring(obj, "inReplyTo")
|
||||
if what == "honk" && rid != "" {
|
||||
what = "tonk"
|
||||
}
|
||||
}
|
||||
if ot == "Tombstone" {
|
||||
rid, _ = jsongetstring(obj, "id")
|
||||
}
|
||||
atts, _ := jsongetarray(obj, "attachment")
|
||||
for _, att := range atts {
|
||||
at, _ := jsongetstring(att, "type")
|
||||
mt, _ := jsongetstring(att, "mediaType")
|
||||
u, _ := jsongetstring(att, "url")
|
||||
name, _ := jsongetstring(att, "name")
|
||||
if at == "Document" {
|
||||
mt = strings.ToLower(mt)
|
||||
log.Printf("attachment: %s %s", mt, u)
|
||||
if mt == "image/jpeg" || mt == "image/png" ||
|
||||
mt == "image/gif" {
|
||||
donk := savedonk(u, name, mt)
|
||||
if donk != nil {
|
||||
xonk.Donks = append(xonk.Donks, donk)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
audience = append(audience, who)
|
||||
|
||||
audience = oneofakind(audience)
|
||||
|
||||
xonk.What = what
|
||||
xonk.Honker = who
|
||||
xonk.XID = xid
|
||||
xonk.RID = rid
|
||||
xonk.Date, _ = time.Parse(time.RFC3339, dt)
|
||||
xonk.URL = url
|
||||
xonk.Noise = content
|
||||
xonk.Audience = audience
|
||||
|
||||
return &xonk
|
||||
}
|
||||
|
||||
func rubadubdub(user *WhatAbout, req map[string]interface{}) {
|
||||
xid, _ := jsongetstring(req, "id")
|
||||
reqactor, _ := jsongetstring(req, "actor")
|
||||
j := NewJunk()
|
||||
j["@context"] = itiswhatitis
|
||||
j["id"] = user.URL + "/dub/" + xid
|
||||
j["type"] = "Accept"
|
||||
j["actor"] = user.URL
|
||||
j["to"] = reqactor
|
||||
j["published"] = time.Now().UTC().Format(time.RFC3339)
|
||||
j["object"] = req
|
||||
|
||||
WriteJunk(os.Stdout, j)
|
||||
|
||||
actor, _ := jsongetstring(req, "actor")
|
||||
inbox, _, err := getboxes(actor)
|
||||
if err != nil {
|
||||
log.Printf("can't get dub box: %s", err)
|
||||
return
|
||||
}
|
||||
keyname, key := ziggy(user)
|
||||
err = PostJunk(keyname, key, inbox, j)
|
||||
if err != nil {
|
||||
log.Printf("can't rub a dub: %s", err)
|
||||
return
|
||||
}
|
||||
stmtSaveDub.Exec(user.ID, actor, actor, "dub")
|
||||
}
|
||||
|
||||
func subsub(user *WhatAbout, xid string) {
|
||||
|
||||
j := NewJunk()
|
||||
j["@context"] = itiswhatitis
|
||||
j["id"] = user.URL + "/sub/" + xid
|
||||
j["type"] = "Follow"
|
||||
j["actor"] = user.URL
|
||||
j["to"] = xid
|
||||
j["object"] = xid
|
||||
j["published"] = time.Now().UTC().Format(time.RFC3339)
|
||||
|
||||
inbox, _, err := getboxes(xid)
|
||||
if err != nil {
|
||||
log.Printf("can't send follow: %s", err)
|
||||
return
|
||||
}
|
||||
WriteJunk(os.Stdout, j)
|
||||
keyname, key := ziggy(user)
|
||||
err = PostJunk(keyname, key, inbox, j)
|
||||
if err != nil {
|
||||
log.Printf("failed to subsub: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func jonkjonk(user *WhatAbout, h *Honk) (map[string]interface{}, map[string]interface{}) {
|
||||
dt := h.Date.Format(time.RFC3339)
|
||||
var jo map[string]interface{}
|
||||
j := NewJunk()
|
||||
j["id"] = user.URL + "/" + h.What + "/" + h.XID
|
||||
j["actor"] = user.URL
|
||||
j["published"] = dt
|
||||
j["to"] = h.Audience[0]
|
||||
if len(h.Audience) > 1 {
|
||||
j["cc"] = h.Audience[1:]
|
||||
}
|
||||
|
||||
switch h.What {
|
||||
case "tonk":
|
||||
fallthrough
|
||||
case "honk":
|
||||
j["type"] = "Create"
|
||||
jo = NewJunk()
|
||||
jo["id"] = user.URL + "/h/" + h.XID
|
||||
jo["type"] = "Note"
|
||||
jo["published"] = dt
|
||||
jo["url"] = user.URL + "/h/" + h.XID
|
||||
jo["attributedTo"] = user.URL
|
||||
if h.RID != "" {
|
||||
jo["inReplyTo"] = h.RID
|
||||
}
|
||||
jo["to"] = h.Audience[0]
|
||||
if len(h.Audience) > 1 {
|
||||
jo["cc"] = h.Audience[1:]
|
||||
}
|
||||
jo["content"] = h.Noise
|
||||
g := bunchofgrapes(h.Noise)
|
||||
if len(g) > 0 {
|
||||
var tags []interface{}
|
||||
for _, m := range g {
|
||||
t := NewJunk()
|
||||
t["type"] = "Mention"
|
||||
t["name"] = m.who
|
||||
t["href"] = m.where
|
||||
tags = append(tags, t)
|
||||
}
|
||||
jo["tag"] = tags
|
||||
}
|
||||
var atts []interface{}
|
||||
for _, d := range h.Donks {
|
||||
jd := NewJunk()
|
||||
jd["mediaType"] = d.Media
|
||||
jd["name"] = d.Name
|
||||
jd["type"] = "Document"
|
||||
jd["url"] = d.URL
|
||||
atts = append(atts, jd)
|
||||
}
|
||||
if len(atts) > 0 {
|
||||
jo["attachment"] = atts
|
||||
}
|
||||
j["object"] = jo
|
||||
case "bonk":
|
||||
j["type"] = "Announce"
|
||||
j["object"] = h.XID
|
||||
}
|
||||
|
||||
return j, jo
|
||||
}
|
||||
|
||||
func honkworldwide(user *WhatAbout, honk *Honk) {
|
||||
aud := append([]string{}, honk.Audience...)
|
||||
for i, a := range aud {
|
||||
if a == thewholeworld || a == user.URL {
|
||||
aud[i] = ""
|
||||
}
|
||||
}
|
||||
keyname, key := ziggy(user)
|
||||
jonk, _ := jonkjonk(user, honk)
|
||||
jonk["@context"] = itiswhatitis
|
||||
for _, f := range getdubs(user.ID) {
|
||||
inbox, _, err := getboxes(f.XID)
|
||||
if err != nil {
|
||||
log.Printf("error getting inbox %s: %s", f.XID, err)
|
||||
continue
|
||||
}
|
||||
err = PostJunk(keyname, key, inbox, jonk)
|
||||
if err != nil {
|
||||
log.Printf("failed to post json to %s: %s", inbox, err)
|
||||
}
|
||||
for i, a := range aud {
|
||||
if a == f.XID {
|
||||
aud[i] = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, a := range aud {
|
||||
if a != "" && !strings.HasSuffix(a, "/followers") {
|
||||
inbox, _, err := getboxes(a)
|
||||
if err != nil {
|
||||
log.Printf("error getting inbox %s: %s", a, err)
|
||||
continue
|
||||
}
|
||||
err = PostJunk(keyname, key, inbox, jonk)
|
||||
if err != nil {
|
||||
log.Printf("failed to post json to %s: %s", inbox, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func asjonker(user *WhatAbout) map[string]interface{} {
|
||||
whatabout := obfusbreak(user.About)
|
||||
|
||||
j := NewJunk()
|
||||
j["@context"] = itiswhatitis
|
||||
j["id"] = user.URL
|
||||
j["type"] = "Person"
|
||||
j["inbox"] = user.URL + "/inbox"
|
||||
j["outbox"] = user.URL + "/outbox"
|
||||
j["name"] = user.Display
|
||||
j["preferredUsername"] = user.Name
|
||||
j["summary"] = whatabout
|
||||
j["url"] = user.URL
|
||||
a := NewJunk()
|
||||
a["type"] = "icon"
|
||||
a["mediaType"] = "image/png"
|
||||
a["url"] = fmt.Sprintf("https://%s/a?a=%s", serverName, url.QueryEscape(user.URL))
|
||||
j["icon"] = a
|
||||
k := NewJunk()
|
||||
k["id"] = user.URL + "#key"
|
||||
k["owner"] = user.URL
|
||||
k["publicKeyPem"] = user.Key
|
||||
j["publicKey"] = k
|
||||
|
||||
return j
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha512"
|
||||
"image"
|
||||
"image/png"
|
||||
)
|
||||
|
||||
func avatar(name string) []byte {
|
||||
h := sha512.New()
|
||||
h.Write([]byte(name))
|
||||
s := h.Sum(nil)
|
||||
img := image.NewNRGBA(image.Rect(0, 0, 64, 64))
|
||||
for i := 0; i < 64; i++ {
|
||||
for j := 0; j < 64; j++ {
|
||||
p := i*img.Stride + j*4
|
||||
xx := i/16*16 + j/16
|
||||
x := s[xx]
|
||||
if x < 64 {
|
||||
img.Pix[p+0] = 32
|
||||
img.Pix[p+1] = 0
|
||||
img.Pix[p+2] = 64
|
||||
img.Pix[p+3] = 255
|
||||
} else if x < 128 {
|
||||
img.Pix[p+0] = 32
|
||||
img.Pix[p+1] = 0
|
||||
img.Pix[p+2] = 92
|
||||
img.Pix[p+3] = 255
|
||||
} else if x < 192 {
|
||||
img.Pix[p+0] = 64
|
||||
img.Pix[p+1] = 0
|
||||
img.Pix[p+2] = 128
|
||||
img.Pix[p+3] = 255
|
||||
} else {
|
||||
img.Pix[p+0] = 96
|
||||
img.Pix[p+1] = 0
|
||||
img.Pix[p+2] = 160
|
||||
img.Pix[p+3] = 255
|
||||
}
|
||||
}
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
png.Encode(&buf, img)
|
||||
return buf.Bytes()
|
||||
}
|
|
@ -0,0 +1,191 @@
|
|||
//
|
||||
// Copyright (c) 2019 Ted Unangst <tedu@tedunangst.com>
|
||||
//
|
||||
// Permission to use, copy, modify, and distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"log"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
var permittedtags = []string{"div", "h1", "h2", "h3", "h4", "h5", "h6",
|
||||
"table", "thead", "tbody", "th", "tr", "td",
|
||||
"p", "br", "pre", "code", "blockquote", "strong", "em", "b", "i", "s", "sup",
|
||||
"ol", "ul", "li"}
|
||||
var permittedattr = []string{"colspan", "rowspan"}
|
||||
var bannedtags = []string{"script", "style"}
|
||||
|
||||
func init() {
|
||||
sort.Strings(permittedtags)
|
||||
sort.Strings(permittedattr)
|
||||
sort.Strings(bannedtags)
|
||||
}
|
||||
|
||||
func contains(array []string, tag string) bool {
|
||||
idx := sort.SearchStrings(array, tag)
|
||||
return idx < len(array) && array[idx] == tag
|
||||
}
|
||||
|
||||
func getattr(node *html.Node, attr string) string {
|
||||
for _, a := range node.Attr {
|
||||
if a.Key == attr {
|
||||
return a.Val
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func hasclass(node *html.Node, class string) bool {
|
||||
return strings.Contains(" "+getattr(node, "class")+" ", " "+class+" ")
|
||||
}
|
||||
|
||||
func writetag(w io.Writer, node *html.Node) {
|
||||
io.WriteString(w, "<")
|
||||
io.WriteString(w, node.Data)
|
||||
for _, attr := range node.Attr {
|
||||
if contains(permittedattr, attr.Key) {
|
||||
fmt.Fprintf(w, ` %s="%s"`, attr.Key, html.EscapeString(attr.Val))
|
||||
}
|
||||
}
|
||||
io.WriteString(w, ">")
|
||||
}
|
||||
|
||||
func render(w io.Writer, node *html.Node) {
|
||||
switch node.Type {
|
||||
case html.ElementNode:
|
||||
tag := node.Data
|
||||
switch {
|
||||
case tag == "a":
|
||||
href := getattr(node, "href")
|
||||
hrefurl, err := url.Parse(href)
|
||||
if err != nil {
|
||||
href = "#BROKEN-" + href
|
||||
} else {
|
||||
href = hrefurl.String()
|
||||
}
|
||||
fmt.Fprintf(w, `<a href="%s" rel=noreferrer>`, html.EscapeString(href))
|
||||
case tag == "img":
|
||||
div := replaceimg(node)
|
||||
if div != "skip" {
|
||||
io.WriteString(w, div)
|
||||
}
|
||||
case tag == "span":
|
||||
case tag == "iframe":
|
||||
src := html.EscapeString(getattr(node, "src"))
|
||||
fmt.Fprintf(w, `<iframe src="<a href="%s">%s</a>">`, src, src)
|
||||
case contains(permittedtags, tag):
|
||||
writetag(w, node)
|
||||
case contains(bannedtags, tag):
|
||||
return
|
||||
}
|
||||
case html.TextNode:
|
||||
io.WriteString(w, html.EscapeString(node.Data))
|
||||
}
|
||||
for c := node.FirstChild; c != nil; c = c.NextSibling {
|
||||
render(w, c)
|
||||
}
|
||||
if node.Type == html.ElementNode {
|
||||
tag := node.Data
|
||||
if tag == "a" || (contains(permittedtags, tag) && tag != "br") {
|
||||
fmt.Fprintf(w, "</%s>", tag)
|
||||
}
|
||||
if tag == "p" || tag == "div" {
|
||||
io.WriteString(w, "\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func replaceimg(node *html.Node) string {
|
||||
src := getattr(node, "src")
|
||||
alt := getattr(node, "alt")
|
||||
//title := getattr(node, "title")
|
||||
if hasclass(node, "Emoji") && alt != "" {
|
||||
return html.EscapeString(alt)
|
||||
}
|
||||
return html.EscapeString(fmt.Sprintf(`<img src="%s">`, src))
|
||||
}
|
||||
|
||||
func cleannode(node *html.Node) template.HTML {
|
||||
var buf strings.Builder
|
||||
render(&buf, node)
|
||||
return template.HTML(buf.String())
|
||||
}
|
||||
|
||||
func cleanstring(shtml string) template.HTML {
|
||||
reader := strings.NewReader(shtml)
|
||||
body, err := html.Parse(reader)
|
||||
if err != nil {
|
||||
log.Printf("error parsing html: %s", err)
|
||||
return ""
|
||||
}
|
||||
return cleannode(body)
|
||||
}
|
||||
|
||||
func textonly(w io.Writer, node *html.Node) {
|
||||
switch node.Type {
|
||||
case html.ElementNode:
|
||||
tag := node.Data
|
||||
switch {
|
||||
case tag == "a":
|
||||
href := getattr(node, "href")
|
||||
fmt.Fprintf(w, `<a href="%s">`, href)
|
||||
case tag == "img":
|
||||
io.WriteString(w, "<img>")
|
||||
case contains(bannedtags, tag):
|
||||
return
|
||||
}
|
||||
case html.TextNode:
|
||||
io.WriteString(w, node.Data)
|
||||
}
|
||||
for c := node.FirstChild; c != nil; c = c.NextSibling {
|
||||
textonly(w, c)
|
||||
}
|
||||
if node.Type == html.ElementNode {
|
||||
tag := node.Data
|
||||
if tag == "a" {
|
||||
fmt.Fprintf(w, "</%s>", tag)
|
||||
}
|
||||
if tag == "p" || tag == "div" {
|
||||
io.WriteString(w, "\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var re_whitespaceeater = regexp.MustCompile("[ \t\r]*\n[ \t\r]*")
|
||||
var re_blanklineeater = regexp.MustCompile("\n\n+")
|
||||
var re_tabeater = regexp.MustCompile("[ \t]+")
|
||||
|
||||
func htmltotext(shtml template.HTML) string {
|
||||
reader := strings.NewReader(string(shtml))
|
||||
body, _ := html.Parse(reader)
|
||||
var buf strings.Builder
|
||||
textonly(&buf, body)
|
||||
rv := buf.String()
|
||||
rv = re_whitespaceeater.ReplaceAllLiteralString(rv, "\n")
|
||||
rv = re_blanklineeater.ReplaceAllLiteralString(rv, "\n\n")
|
||||
rv = re_tabeater.ReplaceAllLiteralString(rv, " ")
|
||||
for len(rv) > 0 && rv[0] == '\n' {
|
||||
rv = rv[1:]
|
||||
}
|
||||
return rv
|
||||
}
|
|
@ -0,0 +1,148 @@
|
|||
//
|
||||
// Copyright (c) 2019 Ted Unangst <tedu@tedunangst.com>
|
||||
//
|
||||
// Permission to use, copy, modify, and distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"math"
|
||||
)
|
||||
|
||||
func lineate(s uint8) float64 {
|
||||
x := float64(s)
|
||||
x /= 255.0
|
||||
if x < 0.04045 {
|
||||
x /= 12.92
|
||||
} else {
|
||||
x += 0.055
|
||||
x /= 1.055
|
||||
x = math.Pow(x, 2.4)
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
func delineate(x float64) uint8 {
|
||||
if x > 0.0031308 {
|
||||
x = math.Pow(x, 1/2.4)
|
||||
x *= 1.055
|
||||
x -= 0.055
|
||||
} else {
|
||||
x *= 12.92
|
||||
}
|
||||
x *= 255.0
|
||||
return uint8(x)
|
||||
}
|
||||
|
||||
func blend(d []byte, s1, s2, s3, s4 int) byte {
|
||||
l1 := lineate(d[s1])
|
||||
l2 := lineate(d[s2])
|
||||
l3 := lineate(d[s3])
|
||||
l4 := lineate(d[s4])
|
||||
return delineate((l1 + l2 + l3 + l4) / 4.0)
|
||||
}
|
||||
|
||||
func squish(d []byte, s1, s2, s3, s4 int) byte {
|
||||
return uint8((uint32(s1) + uint32(s2)) / 2)
|
||||
}
|
||||
|
||||
func vacuumwrap(img image.Image, format string) ([]byte, string, error) {
|
||||
maxdimension := 2048
|
||||
for img.Bounds().Max.X > maxdimension || img.Bounds().Max.Y > maxdimension {
|
||||
switch oldimg := img.(type) {
|
||||
case *image.NRGBA:
|
||||
w, h := oldimg.Rect.Max.X/2, oldimg.Rect.Max.Y/2
|
||||
newimg := image.NewNRGBA(image.Rectangle{Max: image.Point{X: w, Y: h}})
|
||||
for j := 0; j < h; j++ {
|
||||
for i := 0; i < w; i++ {
|
||||
p := newimg.Stride*j + i*4
|
||||
q1 := oldimg.Stride*(j*2+0) + i*4*2
|
||||
q2 := oldimg.Stride*(j*2+1) + i*4*2
|
||||
newimg.Pix[p+0] = blend(oldimg.Pix, q1+0, q1+4, q2+0, q2+4)
|
||||
newimg.Pix[p+1] = blend(oldimg.Pix, q1+1, q1+5, q2+1, q2+5)
|
||||
newimg.Pix[p+2] = blend(oldimg.Pix, q1+2, q1+6, q2+2, q2+6)
|
||||
newimg.Pix[p+3] = squish(oldimg.Pix, q1+3, q1+7, q2+3, q2+7)
|
||||
}
|
||||
}
|
||||
img = newimg
|
||||
case *image.YCbCr:
|
||||
w, h := oldimg.Rect.Max.X/2, oldimg.Rect.Max.Y/2
|
||||
newimg := image.NewYCbCr(image.Rectangle{Max: image.Point{X: w, Y: h}},
|
||||
oldimg.SubsampleRatio)
|
||||
for j := 0; j < h; j++ {
|
||||
for i := 0; i < w; i++ {
|
||||
p := newimg.YStride*j + i
|
||||
q1 := oldimg.YStride*(j*2+0) + i*2
|
||||
q2 := oldimg.YStride*(j*2+1) + i*2
|
||||
newimg.Y[p+0] = blend(oldimg.Y, q1+0, q1+1, q2+0, q2+1)
|
||||
}
|
||||
}
|
||||
switch newimg.SubsampleRatio {
|
||||
case image.YCbCrSubsampleRatio444:
|
||||
w, h = w, h
|
||||
case image.YCbCrSubsampleRatio422:
|
||||
w, h = w/2, h
|
||||
case image.YCbCrSubsampleRatio420:
|
||||
w, h = w/2, h/2
|
||||
case image.YCbCrSubsampleRatio440:
|
||||
w, h = w, h/2
|
||||
case image.YCbCrSubsampleRatio411:
|
||||
w, h = w/4, h
|
||||
case image.YCbCrSubsampleRatio410:
|
||||
w, h = w/4, h/2
|
||||
}
|
||||
for j := 0; j < h; j++ {
|
||||
for i := 0; i < w; i++ {
|
||||
p := newimg.CStride*j + i
|
||||
q1 := oldimg.CStride*(j*2+0) + i*2
|
||||
q2 := oldimg.CStride*(j*2+1) + i*2
|
||||
newimg.Cb[p+0] = blend(oldimg.Cb, q1+0, q1+1, q2+0, q2+1)
|
||||
newimg.Cr[p+0] = blend(oldimg.Cr, q1+0, q1+1, q2+0, q2+1)
|
||||
}
|
||||
}
|
||||
img = newimg
|
||||
default:
|
||||
return nil, "", fmt.Errorf("can't support image format")
|
||||
}
|
||||
}
|
||||
maxsize := 512 * 1024
|
||||
quality := 80
|
||||
var buf bytes.Buffer
|
||||
for {
|
||||
switch format {
|
||||
case "png":
|
||||
png.Encode(&buf, img)
|
||||
case "jpeg":
|
||||
jpeg.Encode(&buf, img, &jpeg.Options{Quality: quality})
|
||||
default:
|
||||
return nil, "", fmt.Errorf("can't encode format: %s", format)
|
||||
}
|
||||
if buf.Len() > maxsize && quality > 30 {
|
||||
switch format {
|
||||
case "png":
|
||||
format = "jpeg"
|
||||
case "jpeg":
|
||||
quality -= 10
|
||||
}
|
||||
buf.Reset()
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
return buf.Bytes(), format, nil
|
||||
}
|
|
@ -0,0 +1,331 @@
|
|||
//
|
||||
// Copyright (c) 2019 Ted Unangst <tedu@tedunangst.com>
|
||||
//
|
||||
// Permission to use, copy, modify, and distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/sha512"
|
||||
"crypto/subtle"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"hash"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
type keytype struct{}
|
||||
|
||||
var thekey keytype
|
||||
|
||||
func LoginChecker(handler http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
userinfo, ok := checkauthcookie(r)
|
||||
if ok {
|
||||
ctx := context.WithValue(r.Context(), thekey, userinfo)
|
||||
r = r.WithContext(ctx)
|
||||
}
|
||||
handler.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func LoginRequired(handler http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ok := GetUserInfo(r) != nil
|
||||
if !ok {
|
||||
loginredirect(w, r)
|
||||
return
|
||||
}
|
||||
handler.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func GetUserInfo(r *http.Request) *UserInfo {
|
||||
userinfo, ok := r.Context().Value(thekey).(*UserInfo)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return userinfo
|
||||
}
|
||||
|
||||
func calculateCSRF(salt, action, auth string) string {
|
||||
hasher := sha512.New512_256()
|
||||
zero := []byte{0}
|
||||
hasher.Write(zero)
|
||||
hasher.Write([]byte(auth))
|
||||
hasher.Write(zero)
|
||||
hasher.Write([]byte(csrfkey))
|
||||
hasher.Write(zero)
|
||||
hasher.Write([]byte(salt))
|
||||
hasher.Write(zero)
|
||||
hasher.Write([]byte(action))
|
||||
hasher.Write(zero)
|
||||
hash := hexsum(hasher)
|
||||
|
||||
return salt + hash
|
||||
}
|
||||
|
||||
func GetCSRF(action string, r *http.Request) string {
|
||||
auth := getauthcookie(r)
|
||||
if auth == "" {
|
||||
return ""
|
||||
}
|
||||
hasher := sha512.New512_256()
|
||||
io.CopyN(hasher, rand.Reader, 32)
|
||||
salt := hexsum(hasher)
|
||||
|
||||
return calculateCSRF(salt, action, auth)
|
||||
}
|
||||
|
||||
func CheckCSRF(action string, r *http.Request) bool {
|
||||
auth := getauthcookie(r)
|
||||
if auth == "" {
|
||||
return false
|
||||
}
|
||||
csrf := r.FormValue("CSRF")
|
||||
if len(csrf) != authlen*2 {
|
||||
return false
|
||||
}
|
||||
salt := csrf[0:authlen]
|
||||
rv := calculateCSRF(salt, action, auth)
|
||||
ok := subtle.ConstantTimeCompare([]byte(rv), []byte(csrf)) == 1
|
||||
return ok
|
||||
}
|
||||
|
||||
func CSRFWrap(action string, handler http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ok := CheckCSRF(action, r)
|
||||
if !ok {
|
||||
http.Error(w, "invalid csrf", 403)
|
||||
return
|
||||
}
|
||||
handler.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func loginredirect(w http.ResponseWriter, r *http.Request) {
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "auth",
|
||||
Value: "",
|
||||
MaxAge: -1,
|
||||
Secure: securecookies,
|
||||
HttpOnly: true,
|
||||
})
|
||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
var authregex = regexp.MustCompile("^[[:alnum:]]+$")
|
||||
var authlen = 32
|
||||
|
||||
var stmtUserName, stmtUserAuth, stmtSaveAuth, stmtDeleteAuth *sql.Stmt
|
||||
var csrfkey string
|
||||
var securecookies bool
|
||||
|
||||
func LoginInit(db *sql.DB) {
|
||||
var err error
|
||||
stmtUserName, err = db.Prepare("select userid, hash from users where username = ?")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
var userinfo UserInfo
|
||||
t := reflect.TypeOf(userinfo)
|
||||
var fields []string
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
f := t.Field(i)
|
||||
fields = append(fields, strings.ToLower(f.Name))
|
||||
}
|
||||
stmtUserAuth, err = db.Prepare(fmt.Sprintf("select %s from users where userid = (select userid from auth where hash = ?)", strings.Join(fields, ", ")))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
stmtSaveAuth, err = db.Prepare("insert into auth (userid, hash) values (?, ?)")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
stmtDeleteAuth, err = db.Prepare("delete from auth where userid = ?")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
debug := false
|
||||
getconfig("debug", &debug)
|
||||
securecookies = !debug
|
||||
getconfig("csrfkey", &csrfkey)
|
||||
}
|
||||
|
||||
var authinprogress = make(map[string]bool)
|
||||
var authprogressmtx sync.Mutex
|
||||
|
||||
func rateandwait(username string) bool {
|
||||
authprogressmtx.Lock()
|
||||
defer authprogressmtx.Unlock()
|
||||
if authinprogress[username] {
|
||||
return false
|
||||
}
|
||||
authinprogress[username] = true
|
||||
go func(name string) {
|
||||
time.Sleep(1 * time.Second / 2)
|
||||
authprogressmtx.Lock()
|
||||
authinprogress[name] = false
|
||||
authprogressmtx.Unlock()
|
||||
}(username)
|
||||
return true
|
||||
}
|
||||
|
||||
func getauthcookie(r *http.Request) string {
|
||||
cookie, err := r.Cookie("auth")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
auth := cookie.Value
|
||||
if !(len(auth) == authlen && authregex.MatchString(auth)) {
|
||||
log.Printf("login: bad auth: %s", auth)
|
||||
return ""
|
||||
}
|
||||
return auth
|
||||
}
|
||||
|
||||
func checkauthcookie(r *http.Request) (*UserInfo, bool) {
|
||||
auth := getauthcookie(r)
|
||||
if auth == "" {
|
||||
return nil, false
|
||||
}
|
||||
hasher := sha512.New512_256()
|
||||
hasher.Write([]byte(auth))
|
||||
authhash := hexsum(hasher)
|
||||
row := stmtUserAuth.QueryRow(authhash)
|
||||
var userinfo UserInfo
|
||||
v := reflect.ValueOf(&userinfo).Elem()
|
||||
var ptrs []interface{}
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
f := v.Field(i)
|
||||
ptrs = append(ptrs, f.Addr().Interface())
|
||||
}
|
||||
err := row.Scan(ptrs...)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
log.Printf("login: no auth found")
|
||||
} else {
|
||||
log.Printf("login: error scanning auth row: %s", err)
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
return &userinfo, true
|
||||
}
|
||||
|
||||
func loaduser(username string) (int64, string, bool) {
|
||||
row := stmtUserName.QueryRow(username)
|
||||
var userid int64
|
||||
var hash string
|
||||
err := row.Scan(&userid, &hash)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
log.Printf("login: no username found")
|
||||
} else {
|
||||
log.Printf("login: error loading username: %s", err)
|
||||
}
|
||||
return -1, "", false
|
||||
}
|
||||
return userid, hash, true
|
||||
}
|
||||
|
||||
var userregex = regexp.MustCompile("^[[:alnum:]]+$")
|
||||
var userlen = 32
|
||||
var passlen = 128
|
||||
|
||||
func hexsum(h hash.Hash) string {
|
||||
return fmt.Sprintf("%x", h.Sum(nil))[0:authlen]
|
||||
}
|
||||
|
||||
func dologin(w http.ResponseWriter, r *http.Request) {
|
||||
username := r.FormValue("username")
|
||||
password := r.FormValue("password")
|
||||
|
||||
if len(username) == 0 || len(username) > userlen ||
|
||||
!userregex.MatchString(username) || len(password) == 0 ||
|
||||
len(password) > passlen {
|
||||
log.Printf("login: invalid password attempt")
|
||||
loginredirect(w, r)
|
||||
return
|
||||
}
|
||||
userid, hash, ok := loaduser(username)
|
||||
if !ok {
|
||||
loginredirect(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if !rateandwait(username) {
|
||||
loginredirect(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||
if err != nil {
|
||||
log.Printf("login: incorrect password")
|
||||
loginredirect(w, r)
|
||||
return
|
||||
}
|
||||
hasher := sha512.New512_256()
|
||||
io.CopyN(hasher, rand.Reader, 32)
|
||||
hash = hexsum(hasher)
|
||||
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "auth",
|
||||
Value: hash,
|
||||
MaxAge: 3600 * 24 * 30,
|
||||
Secure: securecookies,
|
||||
HttpOnly: true,
|
||||
})
|
||||
|
||||
hasher.Reset()
|
||||
hasher.Write([]byte(hash))
|
||||
authhash := hexsum(hasher)
|
||||
|
||||
_, err = stmtSaveAuth.Exec(userid, authhash)
|
||||
if err != nil {
|
||||
log.Printf("error saving auth: %s", err)
|
||||
}
|
||||
|
||||
log.Printf("login: successful login")
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func dologout(w http.ResponseWriter, r *http.Request) {
|
||||
userinfo, ok := checkauthcookie(r)
|
||||
if ok && CheckCSRF("logout", r) {
|
||||
_, err := stmtDeleteAuth.Exec(userinfo.UserID)
|
||||
if err != nil {
|
||||
log.Printf("login: error deleting old auth: %s", err)
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "auth",
|
||||
Value: "",
|
||||
MaxAge: -1,
|
||||
Secure: securecookies,
|
||||
HttpOnly: true,
|
||||
})
|
||||
}
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
//
|
||||
// Copyright (c) 2018 Ted Unangst <tedu@tedunangst.com>
|
||||
//
|
||||
// Permission to use, copy, modify, and distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"io"
|
||||
)
|
||||
|
||||
type Rss struct {
|
||||
XMLName xml.Name `xml:"rss"`
|
||||
Version string `xml:"version,attr"`
|
||||
Feed *RssFeed
|
||||
}
|
||||
|
||||
type RssFeed struct {
|
||||
XMLName xml.Name `xml:"channel"`
|
||||
Title string `xml:"title"`
|
||||
Link string `xml:"link"`
|
||||
Description string `xml:"description"`
|
||||
FeedImage *RssFeedImage
|
||||
Items []*RssItem
|
||||
}
|
||||
|
||||
type RssFeedImage struct {
|
||||
XMLName xml.Name `xml:"image"`
|
||||
URL string `xml:"url"`
|
||||
Title string `xml:"title"`
|
||||
Link string `xml:"link"`
|
||||
}
|
||||
|
||||
type RssItem struct {
|
||||
XMLName xml.Name `xml:"item"`
|
||||
Title string `xml:"title"`
|
||||
Description RssCData `xml:"description"`
|
||||
Link string `xml:"link"`
|
||||
PubDate string `xml:"pubDate"`
|
||||
}
|
||||
|
||||
type RssCData struct {
|
||||
Data string `xml:",cdata"`
|
||||
}
|
||||
|
||||
func (fd *RssFeed) Write(w io.Writer) error {
|
||||
r := Rss{Version: "2.0", Feed: fd}
|
||||
io.WriteString(w, xml.Header)
|
||||
enc := xml.NewEncoder(w)
|
||||
enc.Indent("", " ")
|
||||
err := enc.Encode(r)
|
||||
io.WriteString(w, "\n")
|
||||
return err
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
|
||||
CREATE TABLE honks (honkid integer primary key, userid integer, what text, honker text, xid text, rid text, dt text, url text, noise text);
|
||||
CREATE TABLE donks (honkid integer, fileid integer);
|
||||
CREATE TABLE files(fileid integer primary key, xid text, name text, url text, media text, content blob);
|
||||
CREATE TABLE honkers (honkerid integer primary key, userid integer, name text, xid text, flavor text, pubkey text);
|
||||
|
||||
create index idx_honksxid on honks(xid);
|
||||
create index idx_honkshonker on honks(honker);
|
||||
create index idx_honkerxid on honkers(xid);
|
||||
create index idx_filesxid on files(xid);
|
||||
create index idx_filesurl on files(url);
|
||||
|
||||
CREATE TABLE config (key text, value text);
|
||||
|
||||
CREATE TABLE users (userid integer primary key, username text, hash text, displayname text, about text, pubkey text, seckey text);
|
||||
CREATE TABLE auth (authid integer primary key, userid integer, hash text);
|
||||
CREATE INDEX idxusers_username on users(username);
|
||||
CREATE INDEX idxauth_userid on auth(userid);
|
||||
CREATE INDEX idxauth_hash on auth(hash);
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
//
|
||||
// Copyright (c) 2019 Ted Unangst <tedu@tedunangst.com>
|
||||
//
|
||||
// Permission to use, copy, modify, and distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"html/template"
|
||||
"io"
|
||||
"log"
|
||||
)
|
||||
|
||||
type Template struct {
|
||||
names []string
|
||||
templates *template.Template
|
||||
reload bool
|
||||
}
|
||||
|
||||
func mapmaker(values ...interface{}) (map[string]interface{}, error) {
|
||||
if len(values)%2 != 0 {
|
||||
return nil, errors.New("need arguments in pairs")
|
||||
}
|
||||
dict := make(map[string]interface{}, len(values)/2)
|
||||
for i := 0; i < len(values); i += 2 {
|
||||
key, ok := values[i].(string)
|
||||
if !ok {
|
||||
return nil, errors.New("key must be string")
|
||||
}
|
||||
dict[key] = values[i+1]
|
||||
}
|
||||
return dict, nil
|
||||
}
|
||||
|
||||
func loadtemplates(filenames ...string) (*template.Template, error) {
|
||||
templates := template.New("")
|
||||
templates.Funcs(template.FuncMap{
|
||||
"map": mapmaker,
|
||||
})
|
||||
templates, err := templates.ParseFiles(filenames...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return templates, nil
|
||||
}
|
||||
|
||||
func (t *Template) ExecuteTemplate(w io.Writer, name string, data interface{}) error {
|
||||
if t.reload {
|
||||
templates, err := loadtemplates(t.names...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return templates.ExecuteTemplate(w, name, data)
|
||||
}
|
||||
return t.templates.ExecuteTemplate(w, name, data)
|
||||
}
|
||||
|
||||
func ParseTemplates(reload bool, filenames ...string) *Template {
|
||||
t := new(Template)
|
||||
t.names = filenames
|
||||
t.reload = reload
|
||||
templates, err := loadtemplates(filenames...)
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
if !reload {
|
||||
t.templates = templates
|
||||
}
|
||||
return t
|
||||
}
|
|
@ -0,0 +1,227 @@
|
|||
//
|
||||
// Copyright (c) 2019 Ted Unangst <tedu@tedunangst.com>
|
||||
//
|
||||
// Permission to use, copy, modify, and distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/rand"
|
||||
"crypto/sha512"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
_ "humungus.tedunangst.com/r/go-sqlite3"
|
||||
)
|
||||
|
||||
var savedstyleparam string
|
||||
|
||||
func getstyleparam() string {
|
||||
if savedstyleparam != "" {
|
||||
return savedstyleparam
|
||||
}
|
||||
data, _ := ioutil.ReadFile("views/style.css")
|
||||
hasher := sha512.New()
|
||||
hasher.Write(data)
|
||||
return fmt.Sprintf("?v=%.8x", hasher.Sum(nil))
|
||||
}
|
||||
|
||||
var dbtimeformat = "2006-01-02 15:04:05"
|
||||
|
||||
var alreadyopendb *sql.DB
|
||||
var dbname = "honk.db"
|
||||
var stmtConfig *sql.Stmt
|
||||
|
||||
func initdb() {
|
||||
schema, err := ioutil.ReadFile("schema.sql")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
_, err = os.Stat(dbname)
|
||||
if err == nil {
|
||||
log.Fatalf("%s already exists", dbname)
|
||||
}
|
||||
db, err := sql.Open("sqlite3", dbname)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
os.Remove(dbname)
|
||||
os.Exit(1)
|
||||
}()
|
||||
c := make(chan os.Signal)
|
||||
signal.Notify(c, os.Interrupt)
|
||||
go func() {
|
||||
<-c
|
||||
fmt.Printf("\x1b[?12;25h\x1b[0m")
|
||||
fmt.Printf("\n")
|
||||
os.Remove(dbname)
|
||||
os.Exit(1)
|
||||
}()
|
||||
for _, line := range strings.Split(string(schema), ";") {
|
||||
_, err = db.Exec(line)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
defer db.Close()
|
||||
r := bufio.NewReader(os.Stdin)
|
||||
fmt.Printf("username: ")
|
||||
name, err := r.ReadString('\n')
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
name = name[:len(name)-1]
|
||||
if len(name) < 1 {
|
||||
log.Print("that's way too short")
|
||||
return
|
||||
}
|
||||
fmt.Printf("password: \x1b[?25l\x1b[%d;%dm \x1b[16D", 30, 40)
|
||||
pass, err := r.ReadString('\n')
|
||||
fmt.Printf("\x1b[0m\x1b[?12;25h")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return
|
||||
}
|
||||
pass = pass[:len(pass)-1]
|
||||
if len(pass) < 6 {
|
||||
log.Print("that's way too short")
|
||||
return
|
||||
}
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(pass), 12)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
_, err = db.Exec("insert into users (username, hash) values (?, ?)", name, hash)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("listen address: ")
|
||||
addr, err := r.ReadString('\n')
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
addr = addr[:len(addr)-1]
|
||||
if len(addr) < 1 {
|
||||
log.Print("that's way too short")
|
||||
return
|
||||
}
|
||||
_, err = db.Exec("insert into config (key, value) values (?, ?)", "listenaddr", addr)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("server name: ")
|
||||
addr, err = r.ReadString('\n')
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
addr = addr[:len(addr)-1]
|
||||
if len(addr) < 1 {
|
||||
log.Print("that's way too short")
|
||||
return
|
||||
}
|
||||
_, err = db.Exec("insert into config (key, value) values (?, ?)", "servername", addr)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
var randbytes [16]byte
|
||||
rand.Read(randbytes[:])
|
||||
key := fmt.Sprintf("%x", randbytes)
|
||||
_, err = db.Exec("insert into config (key, value) values (?, ?)", "csrfkey", key)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
err = finishusersetup()
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
db.Exec("insert into config (key, value) values (?, ?)", "debug", 1)
|
||||
db.Close()
|
||||
fmt.Printf("done.\n")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func opendatabase() *sql.DB {
|
||||
if alreadyopendb != nil {
|
||||
return alreadyopendb
|
||||
}
|
||||
var err error
|
||||
_, err = os.Stat(dbname)
|
||||
if err != nil {
|
||||
log.Fatalf("unable to open database: %s", err)
|
||||
}
|
||||
db, err := sql.Open("sqlite3", dbname)
|
||||
if err != nil {
|
||||
log.Fatalf("unable to open database: %s", err)
|
||||
}
|
||||
stmtConfig, err = db.Prepare("select value from config where key = ?")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
alreadyopendb = db
|
||||
return db
|
||||
}
|
||||
|
||||
func getconfig(key string, value interface{}) error {
|
||||
row := stmtConfig.QueryRow(key)
|
||||
err := row.Scan(value)
|
||||
if err == sql.ErrNoRows {
|
||||
err = nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func openListener() (net.Listener, error) {
|
||||
var listenAddr string
|
||||
err := getconfig("listenaddr", &listenAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if listenAddr == "" {
|
||||
return nil, fmt.Errorf("must have listenaddr")
|
||||
}
|
||||
proto := "tcp"
|
||||
if listenAddr[0] == '/' {
|
||||
proto = "unix"
|
||||
err := os.Remove(listenAddr)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
log.Printf("unable to unlink socket: %s", err)
|
||||
}
|
||||
}
|
||||
listener, err := net.Listen(proto, listenAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if proto == "unix" {
|
||||
os.Chmod(listenAddr, 0777)
|
||||
}
|
||||
return listener, nil
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>honk</title>
|
||||
<link href="/style.css{{ .StyleParam }}" rel="stylesheet">
|
||||
<link href="/icon.png" rel="icon">
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<span><a href="/">honk</a></span>
|
||||
{{ if .ShowRSS }}
|
||||
<span><a href="/rss">rss</a></span>
|
||||
{{ end }}
|
||||
{{ if .UserInfo }}
|
||||
<span><a href="/u/{{ .UserInfo.Username }}">{{ .UserInfo.Username }}</a></span>
|
||||
<span><a href="/honkers">honkers</a></span>
|
||||
<span><a href="/logout?CSRF={{ .LogoutCSRF }}">logout</a></span>
|
||||
{{ else }}
|
||||
<span><a href="/login">login</a></span>
|
||||
{{ end }}
|
||||
</div>
|
|
@ -0,0 +1,27 @@
|
|||
{{ template "header.html" . }}
|
||||
<div class="center">
|
||||
<div class="info">
|
||||
<p>{{ .ServerMessage }}
|
||||
{{ if .HonkCSRF }}
|
||||
{{ template "honkform.html" . }}
|
||||
{{ end }}
|
||||
</div>
|
||||
<div>
|
||||
{{ $BonkCSRF := .HonkCSRF }}
|
||||
{{ range .Honks }}
|
||||
{{ template "honk.html" map "Honk" . "Bonk" $BonkCSRF }}
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ if $BonkCSRF }}
|
||||
<script>
|
||||
function post(url, data) {
|
||||
var x = new XMLHttpRequest()
|
||||
x.open("POST", url)
|
||||
x.setRequestHeader("Content-Type", "application/x-www-form-urlencoded")
|
||||
x.send(data)
|
||||
}
|
||||
function bonk(xid) {
|
||||
post("/bonk", "CSRF={{ $BonkCSRF }}&xid=" + xid)
|
||||
}
|
||||
</script>
|
||||
{{ end }}
|
|
@ -0,0 +1,14 @@
|
|||
<div class="honk {{ if eq .Honk.What "tonked" }} tonk {{ end }}">
|
||||
{{ with .Honk }}
|
||||
<div class="title"><img alt="avatar" src="/a?a={{ .Honker}}"><p><a href="{{ .Honker }}" rel=noreferrer>{{ .Username }}</a> <span class="clip">{{ .What }} {{ .Date.Format "02 Jan 2006 15:04" }} <a href="{{ .URL }}" rel=noreferrer>{{ .URL }}</a></span></div>
|
||||
<div class="noise"><p>{{ .HTML }}</div>
|
||||
{{ range .Donks }}
|
||||
<p><a href="/d/{{ .XID }}"><img src="/d/{{ .XID }}" title="{{ .URL }}"></a>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ if .Bonk }}
|
||||
<p>
|
||||
<button onclick="bonk('{{ .Honk.XID }}'); return false;"><a href="/bonk">bonk</a></button>
|
||||
<button style="margin-left: 4em;" onclick="showhonkform('{{ .Honk.XID }}', '{{ .Honk.Username }}'); return false;"><a href="/newhonk">tonk</a></button>
|
||||
{{ end }}
|
||||
</div>
|
|
@ -0,0 +1,25 @@
|
|||
{{ template "header.html" . }}
|
||||
<div class="center">
|
||||
<div class="info">
|
||||
<p>
|
||||
<form action="/savehonker" method="POST">
|
||||
<span class="title">add new honker</span>
|
||||
<input type="hidden" name="CSRF" value="{{ .HonkerCSRF }}">
|
||||
<p><input tabindex=1 type="text" name="name" value="" autocomplete=off> - name
|
||||
<p><input tabindex=1 type="text" name="url" value="" autocomplete=off> - url
|
||||
<p><span><label for="peep">just peeping:</label>
|
||||
<input tabindex=1 type="checkbox" id="peep" name="peep" value="peep"><span></span></span>
|
||||
<p><input tabindex=1 type="submit" name="add honker" value="add honker">
|
||||
</form>
|
||||
</div>
|
||||
{{ range .Honkers }}
|
||||
<div class="honk" id="honker{{ .ID }}">
|
||||
<p>
|
||||
<span class="linktitle">{{ .Name }}</span>
|
||||
<p>url: {{ .XID }}
|
||||
<p><a href="/h/{{ .Name }}">honks</a>
|
||||
<p>flavor: {{ .Flavor }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,30 @@
|
|||
<p>
|
||||
<button onclick="showhonkform(); return false"><a href="/newhonk">it's honking time</a></button>
|
||||
<form id="honkform" action="/honk" method="POST" enctype="multipart/form-data" style="display: none">
|
||||
<p></p>
|
||||
<input type="hidden" name="rid" value="">
|
||||
<input type="hidden" name="CSRF" value="{{ .HonkCSRF }}">
|
||||
<textarea name="noise" id="honknoise"></textarea>
|
||||
<p>
|
||||
<input type="submit" value="it's gonna be honked">
|
||||
<label id="donker" style="margin-left:4em;">attach: <input onchange="updatedonker();" type="file" name="donk"><span></span></label>
|
||||
</form>
|
||||
<script>
|
||||
function showhonkform(rid, hname) {
|
||||
var el = document.getElementById("honkform")
|
||||
el.style = "display: block"
|
||||
if (rid) {
|
||||
el.children[0].innerHTML = "tonking " + rid
|
||||
el.children[1].value = rid
|
||||
el.children[3].value = "@" + hname
|
||||
} else {
|
||||
el.children[0].innerHTML = ""
|
||||
el.children[1].value = ""
|
||||
}
|
||||
el.scrollIntoView()
|
||||
}
|
||||
function updatedonker() {
|
||||
var el = document.getElementById("donker")
|
||||
el.children[1].textContent = el.children[0].value
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,24 @@
|
|||
{{ template "header.html" . }}
|
||||
<div class="center">
|
||||
{{ if .Name }}
|
||||
<div class="info">
|
||||
<p>{{ .Name }} <span style="margin-left:1em;"><a href="/u/{{ .Name }}/rss">rss</a></span>
|
||||
{{ if .HonkCSRF }}
|
||||
<div>
|
||||
<form id="aboutform" action="/saveuser" method="POST">
|
||||
<input type="hidden" name="CSRF" value="{{ .UserCSRF }}">
|
||||
<textarea name="whatabout">{{ .RawWhatAbout }}</textarea>
|
||||
<p>
|
||||
<input type="submit" value="update">
|
||||
</form>
|
||||
</div>
|
||||
{{ else }}
|
||||
<p>{{ .WhatAbout }}
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
<div>
|
||||
{{ range .Honks }}
|
||||
{{ template "honk.html" map "Honk" . }}
|
||||
{{ end }}
|
||||
</div>
|
|
@ -0,0 +1,11 @@
|
|||
{{ template "header.html" . }}
|
||||
<div class="center">
|
||||
<div class="info">
|
||||
<form action="/dologin" method="POST">
|
||||
<p><span class="title">login</span>
|
||||
<p><input tabindex=1 type="text" name="username" autocomplete=off> - username
|
||||
<p><input tabindex=1 type="password" name="password"> - password
|
||||
<p><input tabindex=1 type="submit" name="login" value="login">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,146 @@
|
|||
body {
|
||||
background: #305;
|
||||
color: #dde;
|
||||
font-size: 1em;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
a {
|
||||
color: #dde;
|
||||
}
|
||||
form {
|
||||
font-family: monospace;
|
||||
}
|
||||
p {
|
||||
margin-top 1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
input {
|
||||
font-family: monospace;
|
||||
background: #305;
|
||||
color: #dde;
|
||||
font-size: 1.0em;
|
||||
line-height: 1.2em;
|
||||
padding: 0.5em;
|
||||
}
|
||||
.header {
|
||||
max-width: 1200px;
|
||||
margin: 1em auto;
|
||||
font-size: 1.5em;
|
||||
text-align: right;
|
||||
}
|
||||
.header span {
|
||||
margin-right: 2em;
|
||||
}
|
||||
.center {
|
||||
max-width: 1200px;
|
||||
margin: auto;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
.info {
|
||||
background: #002;
|
||||
border: 2px solid #dde;
|
||||
margin-bottom: 1em;
|
||||
padding: 0em 1em 0em 1em;
|
||||
}
|
||||
.info div {
|
||||
margin-top 1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
button, form input[type=submit] {
|
||||
font-size: 0.8em;
|
||||
font-family: monospace;
|
||||
color: #dde;
|
||||
background: #305;
|
||||
border: 1px solid #dde;
|
||||
padding: 0.5em;
|
||||
}
|
||||
button a {
|
||||
text-decoration: none;
|
||||
}
|
||||
.info form {
|
||||
margin-top: 1em;
|
||||
}
|
||||
.info textarea {
|
||||
padding: 0.5em;
|
||||
font-size: 1em;
|
||||
background: #305;
|
||||
color: #dde;
|
||||
width: 700px;
|
||||
height: 8em;
|
||||
margin-bottom: 0.5em;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@media screen and (max-width: 1024px) {
|
||||
.info textarea {
|
||||
width: 500px;
|
||||
}
|
||||
}
|
||||
.info input[type="checkbox"] {
|
||||
position: fixed;
|
||||
top: -9999px;
|
||||
}
|
||||
.info input[type="checkbox"] + span:after {
|
||||
content: "no";
|
||||
}
|
||||
.info input[type="checkbox"]:checked + span:after {
|
||||
content: "yes";
|
||||
}
|
||||
.info input[type="checkbox"]:focus + span:after {
|
||||
outline: 1px solid #dde;
|
||||
}
|
||||
.info input[type=file] {
|
||||
display: none;
|
||||
}
|
||||
.info label {
|
||||
border: 1px solid #dde;
|
||||
font-size: 0.8em;
|
||||
padding: 0.5em;
|
||||
font-size: 0.8em;
|
||||
background: #305;
|
||||
}
|
||||
|
||||
.honk {
|
||||
width: 90%;
|
||||
margin: auto;
|
||||
background: #002;
|
||||
border: 2px solid #dde;
|
||||
border-radius: 1em;
|
||||
margin-bottom: 1em;
|
||||
padding-left: 1em;
|
||||
padding-right: 1em;
|
||||
padding-top: 0;
|
||||
}
|
||||
.tonk {
|
||||
}
|
||||
.tonk .noise {
|
||||
color: #aab;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
.tonk .noise a {
|
||||
color: #aab;
|
||||
}
|
||||
.honk a {
|
||||
color: #dde;
|
||||
}
|
||||
.honk .title .clip a {
|
||||
color: #88a;
|
||||
}
|
||||
.honk .title {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 0.8em;
|
||||
margin-top: 1em;
|
||||
}
|
||||
.honk .title img {
|
||||
float: left;
|
||||
margin-right: 1em;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
.honk .title p {
|
||||
margin-top: 0px;
|
||||
}
|
||||
img {
|
||||
max-width: 100%
|
||||
}
|
|
@ -0,0 +1,203 @@
|
|||
//
|
||||
// Copyright (c) 2019 Ted Unangst <tedu@tedunangst.com>
|
||||
//
|
||||
// Permission to use, copy, modify, and distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func sb64(data []byte) string {
|
||||
var sb strings.Builder
|
||||
b64 := base64.NewEncoder(base64.StdEncoding, &sb)
|
||||
b64.Write(data)
|
||||
b64.Close()
|
||||
return sb.String()
|
||||
|
||||
}
|
||||
func b64s(s string) []byte {
|
||||
var buf bytes.Buffer
|
||||
b64 := base64.NewDecoder(base64.StdEncoding, strings.NewReader(s))
|
||||
io.Copy(&buf, b64)
|
||||
return buf.Bytes()
|
||||
}
|
||||
func sb64sha256(content []byte) string {
|
||||
h := sha256.New()
|
||||
h.Write(content)
|
||||
return sb64(h.Sum(nil))
|
||||
}
|
||||
|
||||
func zig(keyname string, key *rsa.PrivateKey, req *http.Request, content []byte) {
|
||||
headers := []string{"(request-target)", "date", "host", "content-length", "digest"}
|
||||
var stuff []string
|
||||
for _, h := range headers {
|
||||
var s string
|
||||
switch h {
|
||||
case "(request-target)":
|
||||
s = strings.ToLower(req.Method) + " " + req.URL.RequestURI()
|
||||
case "date":
|
||||
s = req.Header.Get(h)
|
||||
if s == "" {
|
||||
s = time.Now().UTC().Format(http.TimeFormat)
|
||||
req.Header.Set(h, s)
|
||||
}
|
||||
case "host":
|
||||
s = req.Header.Get(h)
|
||||
if s == "" {
|
||||
s = req.URL.Hostname()
|
||||
req.Header.Set(h, s)
|
||||
}
|
||||
case "content-length":
|
||||
s = req.Header.Get(h)
|
||||
if s == "" {
|
||||
s = strconv.Itoa(len(content))
|
||||
req.Header.Set(h, s)
|
||||
req.ContentLength = int64(len(content))
|
||||
}
|
||||
case "digest":
|
||||
s = req.Header.Get(h)
|
||||
if s == "" {
|
||||
s = "SHA-256=" + sb64sha256(content)
|
||||
req.Header.Set(h, s)
|
||||
}
|
||||
}
|
||||
stuff = append(stuff, h+": "+s)
|
||||
}
|
||||
|
||||
h := sha256.New()
|
||||
h.Write([]byte(strings.Join(stuff, "\n")))
|
||||
sig, _ := rsa.SignPKCS1v15(rand.Reader, key, crypto.SHA256, h.Sum(nil))
|
||||
bsig := sb64(sig)
|
||||
|
||||
sighdr := fmt.Sprintf(`keyId="%s",algorithm="%s",headers="%s",signature="%s"`,
|
||||
keyname, "rsa-sha256", strings.Join(headers, " "), bsig)
|
||||
req.Header.Set("Signature", sighdr)
|
||||
}
|
||||
|
||||
var re_sighdrval = regexp.MustCompile(`(.*)="(.*)"`)
|
||||
|
||||
func zag(req *http.Request, content []byte) (string, error) {
|
||||
sighdr := req.Header.Get("Signature")
|
||||
|
||||
var keyname, algo, heads, bsig string
|
||||
for _, v := range strings.Split(sighdr, ",") {
|
||||
m := re_sighdrval.FindStringSubmatch(v)
|
||||
if len(m) != 3 {
|
||||
return "", fmt.Errorf("bad scan: %s from %s\n", v, sighdr)
|
||||
}
|
||||
switch m[1] {
|
||||
case "keyId":
|
||||
keyname = m[2]
|
||||
case "algorithm":
|
||||
algo = m[2]
|
||||
case "headers":
|
||||
heads = m[2]
|
||||
case "signature":
|
||||
bsig = m[2]
|
||||
default:
|
||||
return "", fmt.Errorf("bad sig val: %s", m[1])
|
||||
}
|
||||
}
|
||||
if keyname == "" || algo == "" || heads == "" || bsig == "" {
|
||||
return "", fmt.Errorf("missing a sig value")
|
||||
}
|
||||
|
||||
key := zaggy(keyname)
|
||||
if key == nil {
|
||||
return "", fmt.Errorf("no key for %s", keyname)
|
||||
}
|
||||
headers := strings.Split(heads, " ")
|
||||
var stuff []string
|
||||
for _, h := range headers {
|
||||
var s string
|
||||
switch h {
|
||||
case "(request-target)":
|
||||
s = strings.ToLower(req.Method) + " " + req.URL.RequestURI()
|
||||
case "host":
|
||||
s = req.Host
|
||||
default:
|
||||
s = req.Header.Get(h)
|
||||
}
|
||||
stuff = append(stuff, h+": "+s)
|
||||
}
|
||||
|
||||
h := sha256.New()
|
||||
h.Write([]byte(strings.Join(stuff, "\n")))
|
||||
sig := b64s(bsig)
|
||||
err := rsa.VerifyPKCS1v15(key, crypto.SHA256, h.Sum(nil), sig)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return keyname, nil
|
||||
}
|
||||
|
||||
func pez(s string) (pri *rsa.PrivateKey, pub *rsa.PublicKey, err error) {
|
||||
block, _ := pem.Decode([]byte(s))
|
||||
if block == nil {
|
||||
err = fmt.Errorf("no pem data")
|
||||
return
|
||||
}
|
||||
switch block.Type {
|
||||
case "PUBLIC KEY":
|
||||
var k interface{}
|
||||
k, err = x509.ParsePKIXPublicKey(block.Bytes)
|
||||
if k != nil {
|
||||
pub, _ = k.(*rsa.PublicKey)
|
||||
}
|
||||
case "RSA PUBLIC KEY":
|
||||
pub, err = x509.ParsePKCS1PublicKey(block.Bytes)
|
||||
case "RSA PRIVATE KEY":
|
||||
pri, err = x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err == nil {
|
||||
pub = &pri.PublicKey
|
||||
}
|
||||
default:
|
||||
err = fmt.Errorf("unknown key type")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func zem(i interface{}) (string, error) {
|
||||
var b pem.Block
|
||||
var err error
|
||||
switch k := i.(type) {
|
||||
case *rsa.PrivateKey:
|
||||
b.Type = "RSA PRIVATE KEY"
|
||||
b.Bytes = x509.MarshalPKCS1PrivateKey(k)
|
||||
case *rsa.PublicKey:
|
||||
b.Type = "PUBLIC KEY"
|
||||
b.Bytes, err = x509.MarshalPKIXPublicKey(k)
|
||||
default:
|
||||
err = fmt.Errorf("unknown key type: %s", k)
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(pem.EncodeToMemory(&b)), nil
|
||||
}
|
Loading…
Reference in New Issue