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