Compare commits

...

5 Commits

  1. 13
      Makefile
  2. 114
      client/main.go
  3. 171
      cmd/springer/main.go
  4. 67
      cmd/springerd/main.go
  5. 2
      modd.conf
  6. 91
      server/main.go

@ -1,5 +1,8 @@
bin/springer: server/main.go
go build -o bin/springer ./server
bin/springerd: cmd/springerd/main.go server/main.go
go build -o bin/springerd ./cmd/springerd
bin/springer: client/main.go
go build -o bin/springer ./cmd/springer
# Use zig as the cc to cross-compile mattn/sqlite for linux
#
@ -8,6 +11,6 @@ bin/springer: server/main.go
# this is basically magic
#
# to run locally:
# docker run -it -v $(pwd)/bin:/app ubuntu /app/springer-linux
bin/springer-linux: server/main.go
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 CC="zig cc -target x86_64-linux" CXX="zig cc -target x86_64-linux" go build -o bin/springer-linux ./server
# docker run -it -v $(pwd)/bin:/app ubuntu /app/springerd-linux
bin/springerd-linux: cmd/springerd/main.go server/main.go
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 CC="zig cc -target x86_64-linux" CXX="zig cc -target x86_64-linux" go build -o bin/springerd-linux ./cmd/springerd

@ -1,24 +1,16 @@
// TODO:
//
package main
package client
import (
"bytes"
"crypto/ed25519"
"encoding/hex"
"errors"
"fmt"
"io/ioutil"
"net/http"
"os"
"os/user"
"path/filepath"
"runtime"
"sync"
"time"
)
func validKey() (ed25519.PublicKey, ed25519.PrivateKey) {
func ValidKey() (ed25519.PublicKey, ed25519.PrivateKey) {
// A conforming key's final seven hex characters must be 83e followed by
// four characters that, interpreted as MMYY, express a valid month and
// year in the range 01/00 .. 12/99. Formally, the key must match this
@ -40,11 +32,12 @@ func validKey() (ed25519.PublicKey, ed25519.PrivateKey) {
var waitGroup sync.WaitGroup
var once sync.Once
fmt.Printf(" - looking for a key that ends in %s using %d routines\n",
fmt.Printf(" - looking for a key that ends in 8%s using %d routines\n",
keyEnd, nRoutines)
var publicKey ed25519.PublicKey
var privateKey ed25519.PrivateKey
start := time.Now()
waitGroup.Add(nRoutines)
for i := 0; i < nRoutines; i++ {
@ -59,7 +52,7 @@ func validKey() (ed25519.PublicKey, ed25519.PrivateKey) {
// bytes.Equal to keep the hot loop fast
if bytes.Equal(pub[29:32], target) && pub[28]&0x0F == 0x08 {
once.Do(func() {
fmt.Printf("found %x\n", pub)
fmt.Printf("found %x in %f minutes\n", pub, time.Since(start).Minutes())
publicKey = pub
privateKey = priv
})
@ -73,100 +66,3 @@ func validKey() (ed25519.PublicKey, ed25519.PrivateKey) {
return publicKey, privateKey
}
func fileExists(name string) bool {
if _, err := os.Stat(name); errors.Is(err, os.ErrNotExist) {
return false
}
return true
}
func getKeys() (ed25519.PublicKey, ed25519.PrivateKey) {
user, err := user.Current()
if err != nil {
panic(err)
}
configPath := os.Getenv("XDG_CONFIG_HOME")
if configPath == "" {
configPath = filepath.Join(user.HomeDir, ".config", "spring83")
}
if err = os.MkdirAll(configPath, os.ModePerm); err != nil {
panic(err)
}
pubfile := filepath.Join(configPath, "key.pub")
privfile := filepath.Join(configPath, "key.priv")
var pubkey ed25519.PublicKey
var privkey ed25519.PrivateKey
if fileExists(pubfile) && fileExists(privfile) {
pubkey, err = ioutil.ReadFile(pubfile)
if err != nil {
panic(err)
}
privkey, err = ioutil.ReadFile(privfile)
if err != nil {
panic(err)
}
} else {
fmt.Println("Generating valid key. This will take a minute")
pubkey, privkey = validKey()
os.WriteFile(pubfile, pubkey, 0666)
os.WriteFile(privfile, privkey, 0600)
}
return pubkey, privkey
}
func main() {
pubkey, privkey := getKeys()
client := &http.Client{}
body, err := ioutil.ReadAll(os.Stdin)
if err != nil {
panic(err)
}
// Prepoend a time element. Maybe we should check to see if it's already
// been provided?
timeElt := []byte(fmt.Sprintf("<time datetime=\"%s\">", time.Now().UTC().Format(time.RFC3339)))
body = append(timeElt, body...)
if len(body) == 0 {
panic(fmt.Errorf("input required"))
}
if len(body) > 2217 {
panic(fmt.Errorf("input body too long"))
}
// TODO: take the URL as a command line param
url := fmt.Sprintf("http://localhost:8000/%x", pubkey)
fmt.Printf("URL: %s\n", url)
req, err := http.NewRequest(http.MethodPut, url, bytes.NewBuffer(body))
if err != nil {
panic(err)
}
sig := ed25519.Sign(privkey, body)
fmt.Printf("Spring-83 Signature=%x\n", sig)
req.Header.Set("Authorization", fmt.Sprintf("Spring-83 Signature=%x", sig))
dt := time.Now().Format(time.RFC1123)
req.Header.Set("If-Unmodified-Since", dt)
resp, err := client.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
responseBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
panic(err)
}
fmt.Printf("%s: %s\n", resp.Status, responseBody)
}

@ -0,0 +1,171 @@
// TODO:
// * Board UI?
package main
import (
"bytes"
"crypto/ed25519"
"errors"
"flag"
"fmt"
"io/ioutil"
"net/http"
"os"
"os/user"
"path/filepath"
"time"
"github.com/llimllib/springer/client"
)
func fileExists(name string) bool {
if _, err := os.Stat(name); errors.Is(err, os.ErrNotExist) {
return false
}
return true
}
func getKeys(folder string) (ed25519.PublicKey, ed25519.PrivateKey) {
// get the expected public key file and private key file paths
var pubfile, privfile string
if len(folder) == 0 {
user, err := user.Current()
if err != nil {
panic(err)
}
configPath := os.Getenv("XDG_CONFIG_HOME")
if configPath == "" {
configPath = filepath.Join(user.HomeDir, ".config", "spring83")
}
if err = os.MkdirAll(configPath, os.ModePerm); err != nil {
panic(err)
}
pubfile = filepath.Join(configPath, "key.pub")
privfile = filepath.Join(configPath, "key.priv")
} else {
pubfile = filepath.Join(folder, "key.pub")
privfile = filepath.Join(folder, "key.priv")
}
// try to load the public and private key files as ed25519 keys
var pubkey ed25519.PublicKey
var privkey ed25519.PrivateKey
var err error
if fileExists(pubfile) && fileExists(privfile) {
pubkey, err = ioutil.ReadFile(pubfile)
if err != nil {
panic(err)
}
privkey, err = ioutil.ReadFile(privfile)
if err != nil {
panic(err)
}
} else {
fmt.Println("Generating valid key. This will take a minute")
pubkey, privkey = client.ValidKey()
os.WriteFile(pubfile, pubkey, 0666)
os.WriteFile(privfile, privkey, 0600)
}
return pubkey, privkey
}
func getBody(inputFile string) []byte {
var body []byte
var err error
if inputFile == "-" {
body, err = ioutil.ReadAll(os.Stdin)
if err != nil {
panic(err)
}
} else {
body, err = ioutil.ReadFile(inputFile)
if err != nil {
panic(err)
}
}
// Prepend a time element. Maybe we should check to see if it's already
// been provided?
timeElt := []byte(fmt.Sprintf("<time datetime=\"%s\">", time.Now().UTC().Format(time.RFC3339)))
body = append(timeElt, body...)
if len(body) == 0 {
panic(fmt.Errorf("input required"))
}
if len(body) > 2217 {
panic(fmt.Errorf("input body too long"))
}
return body
}
func sendBody(server string, body []byte, pubkey ed25519.PublicKey, privkey ed25519.PrivateKey) {
client := &http.Client{}
url := fmt.Sprintf("%s/%x", server, pubkey)
req, err := http.NewRequest(http.MethodPut, url, bytes.NewBuffer(body))
if err != nil {
panic(err)
}
sig := ed25519.Sign(privkey, body)
req.Header.Set("Authorization", fmt.Sprintf("Spring-83 Signature=%x", sig))
dt := time.Now().Format(time.RFC1123)
req.Header.Set("If-Unmodified-Since", dt)
resp, err := client.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
responseBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
panic(err)
}
if resp.StatusCode != 200 {
fmt.Printf("%s: %s\n", resp.Status, responseBody)
}
}
func usage() {
fmt.Print(`springer [-file=filename] [-key=keyfolder] server
Send a spring83 board to a server
flags:
-file=filename
if present, a file to send to the server instead of accepting bytes on
stdin
-key=keyfolder
a folder the program should use for finding your public and private
keys. It will expect there to be two files, one called "key.pub" and
another called "key.priv" which are your public and private keys,
respectively
`)
os.Exit(1)
}
func main() {
inputFile := flag.String("file", "-", "The file to send to the server")
keyFolder := flag.String("key", "", "A folder to check for key.pub and key.priv")
flag.Usage = usage
flag.Parse()
if len(flag.Args()) < 1 {
usage()
}
server := flag.Args()[0]
pubkey, privkey := getKeys(*keyFolder)
sendBody(server, getBody(*inputFile), pubkey, privkey)
}

@ -0,0 +1,67 @@
package main
import (
"fmt"
"net/http"
"os"
"time"
_ "github.com/mattn/go-sqlite3"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/llimllib/springer/server"
)
// getenv returns the environment variable given by the key if present,
// otherwise it returns adefault
func getenv(key string, adefault string) string {
val, ok := os.LookupEnv(key)
if !ok {
return adefault
}
return val
}
func initLog() zerolog.Logger {
logLevel := getenv("LOG_LEVEL", "info")
level, err := zerolog.ParseLevel(logLevel)
if err != nil {
log.Panic().Err(err).Msg("")
}
log := zerolog.New(os.Stderr).With().Timestamp().Logger().Level(level)
if getenv("PRETTY_LOGGING", "") != "" {
log = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339})
}
return log
}
func main() {
log := initLog()
db := server.InitDB(log)
server.InitRuntimeValues(log)
server.InitCleaner(db, log)
spring83 := server.NewSpring83Server(db, log)
host := getenv("HOST", "")
port := getenv("PORT", "8000")
addr := fmt.Sprintf("%s:%s", host, port)
timeoutMsg := "Request timed out"
srv := &http.Server{
Addr: addr,
ReadTimeout: 1 * time.Second,
WriteTimeout: 1 * time.Second,
IdleTimeout: 30 * time.Second,
ReadHeaderTimeout: 2 * time.Second,
Handler: server.RequestLogger(log, http.TimeoutHandler(spring83, 2*time.Second, timeoutMsg)),
}
log.Info().Str("addr", addr).Msg("starting helloserver on")
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Err(err).Msg("Error received from server")
}
}

@ -1,3 +1,3 @@
server/main.go server/templates/** {
daemon: go run ./server
daemon: go run ./cmd/springerd
}

@ -9,7 +9,7 @@
// * scan for <link rel="next"...> links as specified in the spec
// * add unique request ID
package main
package server
import (
"bytes"
@ -37,7 +37,6 @@ import (
_ "github.com/mattn/go-sqlite3"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
const MAX_KEY_64 = (1 << 64) - 1
@ -62,23 +61,13 @@ var (
vcsTime string
)
// getenv returns the environment variable given by the key if present,
// otherwise it returns adefault
func getenv(key string, adefault string) string {
val, ok := os.LookupEnv(key)
if !ok {
return adefault
}
return val
}
func must(err error, log zerolog.Logger) {
if err != nil {
log.Fatal().Err(err).Msg("")
}
}
func initDB(log zerolog.Logger) *sql.DB {
func InitDB(log zerolog.Logger) *sql.DB {
dbName := "./spring83.db"
// if the db doesn't exist, create it
@ -108,19 +97,16 @@ func initDB(log zerolog.Logger) *sql.DB {
return db
}
func initLog() zerolog.Logger {
logLevel := getenv("LOG_LEVEL", "info")
level, err := zerolog.ParseLevel(logLevel)
if err != nil {
log.Panic().Err(err).Msg("")
}
log := zerolog.New(os.Stderr).With().Timestamp().Logger().Level(level)
if getenv("PRETTY_LOGGING", "") != "" {
log = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339})
}
return log
// initCleaner starts a goroutine that will delete any expired entries every 5 minutes
func InitCleaner(db *sql.DB, log zerolog.Logger) {
removeExpiredBoards(db, log)
ticker := time.NewTicker(5 * time.Minute)
go func() {
for {
<-ticker.C
removeExpiredBoards(db, log)
}
}()
}
type responseRecorder struct {
@ -133,7 +119,7 @@ func (rec *responseRecorder) WriteHeader(code int) {
rec.ResponseWriter.WriteHeader(code)
}
func requestLogger(log zerolog.Logger, next http.Handler) http.Handler {
func RequestLogger(log zerolog.Logger, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rec := &responseRecorder{w, http.StatusOK}
t := time.Now()
@ -154,7 +140,7 @@ func requestLogger(log zerolog.Logger, next http.Handler) http.Handler {
})
}
func initRuntimeValues(log zerolog.Logger) {
func InitRuntimeValues(log zerolog.Logger) {
if bi, ok := debug.ReadBuildInfo(); !ok {
log.Fatal().Msg("Unable to get build info")
} else {
@ -179,57 +165,17 @@ func removeExpiredBoards(db *sql.DB, log zerolog.Logger) {
WHERE expiry_datetime < ?
`, dt)
if err != nil {
log.Error().Err(err).Msg("Error inserting board")
log.Error().Err(err).Msg("Error deleting board")
}
rows, err := res.RowsAffected()
if err != nil {
log.Error().Err(err).Msg("Error inserting board")
log.Error().Err(err).Msg("Error getting deleted board rows affected")
}
log.Info().Int64("deleted", rows).Dur("duration", time.Since(t)).Msg("entries cleaned")
}
// initCleaner starts a goroutine that will delete any expired entries every 5 minutes
func initCleaner(db *sql.DB, log zerolog.Logger) {
removeExpiredBoards(db, log)
ticker := time.NewTicker(5 * time.Minute)
go func() {
for {
<-ticker.C
removeExpiredBoards(db, log)
}
}()
}
func main() {
log := initLog()
db := initDB(log)
initRuntimeValues(log)
initCleaner(db, log)
server := newSpring83Server(db, log)
host := getenv("HOST", "")
port := getenv("PORT", "8000")
addr := fmt.Sprintf("%s:%s", host, port)
timeoutMsg := "Request timed out"
srv := &http.Server{
Addr: addr,
ReadTimeout: 1 * time.Second,
WriteTimeout: 1 * time.Second,
IdleTimeout: 30 * time.Second,
ReadHeaderTimeout: 2 * time.Second,
Handler: requestLogger(log, http.TimeoutHandler(server, 2*time.Second, timeoutMsg)),
}
log.Info().Str("addr", addr).Msg("starting helloserver on")
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Err(err).Msg("Error received from server")
}
}
func readTemplate(name string, fsys fs.FS) (string, error) {
h, err := fs.ReadFile(fsys, name)
if err != nil {
@ -265,7 +211,7 @@ type Spring83Server struct {
homeTemplate *template.Template
}
func newSpring83Server(db *sql.DB, log zerolog.Logger) *Spring83Server {
func NewSpring83Server(db *sql.DB, log zerolog.Logger) *Spring83Server {
return &Spring83Server{
db: db,
log: log,
@ -540,7 +486,8 @@ func (s *Spring83Server) publishBoard(w http.ResponseWriter, r *http.Request) {
board=?,
creation_datetime=?,
expiry_datetime=?
`, keyStr, body, bodyTimeISO, expiry, body, bodyTimeISO, expiry)
WHERE key=?
`, keyStr, body, bodyTimeISO, expiry, body, bodyTimeISO, expiry, keyStr)
if err != nil {
s.log.Error().Err(err).Msg("Error inserting board")

Loading…
Cancel
Save