Compare commits

...

36 Commits
trunk ... trunk

Author SHA1 Message Date
vilmibm ca33731826 no, c 2023-11-04 05:11:36 +00:00
vilmibm 81c9dede67 tweak 2023-11-04 05:09:33 +00:00
vilmibm 4958407856 add Makefile 2023-11-04 05:08:46 +00:00
vilmibm 9bb456bc17 add newline to keyfile if needed 2023-11-01 20:59:49 +00:00
vilmibm 69ad6a384a idk 2023-11-01 20:36:53 +00:00
vilmibm 4284fb4048 logger 2023-11-01 03:42:43 +00:00
vilmibm 100643d8fc further the rearranging 2023-11-01 03:25:26 +00:00
vilmibm 0764534fed rearrange things, add locking writer 2023-11-01 02:44:22 +00:00
vilmibm 69666edefa success note 2023-10-26 22:36:48 +00:00
vilmibm add129826a assume prompts are required 2023-10-26 22:32:30 +00:00
vilmibm 90808c1ce0 bug fixes, ready for external qa 2023-10-25 19:24:01 +00:00
vilmibm 17d39483fb fix logging 2023-10-25 17:52:04 +00:00
vilmibm 418e4a4a14 logging 2023-10-25 17:42:46 +00:00
vilmibm 79dc987c61 minor 2023-10-25 08:44:20 +00:00
vilmibm ba1a1319e3 sigh 2023-10-25 08:42:31 +00:00
vilmibm cbc868ae35 oops 2023-10-25 08:34:58 +00:00
vilmibm 940779876c oops 2023-10-25 08:34:32 +00:00
vilmibm f53b2721cb use helper 2023-10-25 08:33:25 +00:00
vilmibm b1ff57ba58 fill in emailtouser 2023-10-25 08:24:23 +00:00
vilmibm be5020ad28 WIP retooling bc of permission issue 2023-10-25 04:39:13 +00:00
vilmibm 9bea4257c1 notes 2023-10-25 03:52:54 +00:00
vilmibm 2a07a0e200 mark code used 2023-10-25 03:03:36 +00:00
vilmibm 7255ee691e minor 2023-10-25 02:48:01 +00:00
vilmibm 44686ad536 add new appendkeyfile helper 2023-10-25 02:41:16 +00:00
vilmibm 529e14158a forgot to add these 2023-10-25 02:41:02 +00:00
vilmibm bf244101e6 finish code validation, start on key handling 2023-10-25 01:40:37 +00:00
vilmibm 6fa11aba8e code processing, TODOs 2023-10-24 19:23:35 +00:00
vilmibm 5c2142f6e7 auth code db methods 2023-10-24 19:17:16 +00:00
vilmibm 92faddd079 refactor prompting, start on redeemCode 2023-10-24 19:10:13 +00:00
vilmibm 6950ba7109 send auth code email 2023-10-24 18:58:34 +00:00
vilmibm a3b13d21b3 creating auth codes in db 2023-10-24 06:22:21 +00:00
vilmibm 5c6f4cce19 ref 2023-10-24 05:16:51 +00:00
vilmibm 2a966bf842 ask db about users 2023-10-24 05:15:04 +00:00
vilmibm 880511a79a add color and email prompt 2023-10-21 04:58:30 +00:00
vilmibm 0884ba2ff6 fix glaring upsetting error in signup copy 2023-10-18 00:48:47 +00:00
vilmibm d0b79f3e7c start on bare bones help SSH command 2023-10-18 00:48:37 +00:00
26 changed files with 806 additions and 75 deletions

13
.gitignore vendored
View File

@ -1,10 +1,15 @@
*.swp
bin/
cmd/launcher/launcher
cmd/request/request
cmd/contrib/contrib
cmd/visit/visit
cmd/signup/signup
cmd/review/review
cmd/welcome/welcome
cmd/createkeyfile/createkeyfile
cmd/registeruser/registeruser
cmd/stats/stats
external/cmd/signup/signup
external/cmd/welcome/welcome
external/cmd/help/help
external/cmd/helpers/emailtouser/emailtouser
external/cmd/helpers/createkeyfile/createkeyfile
external/cmd/helpers/registeruser/registeruser
external/cmd/helpers/appendkeyfile/appendkeyfile

59
Makefile 100644
View File

@ -0,0 +1,59 @@
all: cmds external
install: all
cp bin/launcher /usr/local/bin/town
cp bin/stats /town/bin/
cp bin/contrib /town/bin/
cp bin/request /town/bin/
cp bin/appendkeyfile /town/bin/
cp bin/createkeyfile /town/bin/
cp bin/emailtouser /town/bin/
cp bin/registeruser /town/bin/
cp bin/help /town/bin/external/
cp bin/welcome /town/bin/external/
cp bin/signup /town/bin/external/
clean:
rm -rf bin
external: bin/help bin/welcome bin/signup exthelpers
bin/help: external/cmd/help/main.go bin
go build -o bin/help ./external/cmd/help
bin/welcome: external/cmd/welcome/main.go bin
go build -o bin/welcome ./external/cmd/welcome
bin/signup: external/cmd/signup/main.go bin
go build -o bin/signup ./external/cmd/signup
exthelpers: bin/appendkeyfile bin/createkeyfile bin/emailtouser bin/registeruser
bin/appendkeyfile: external/cmd/helpers/appendkeyfile/main.go bin
go build -o bin/appendkeyfile ./external/cmd/helpers/appendkeyfile
bin/createkeyfile: external/cmd/helpers/createkeyfile/main.go bin
go build -o bin/createkeyfile ./external/cmd/helpers/createkeyfile
bin/emailtouser: external/cmd/helpers/emailtouser/main.go bin
go build -o bin/emailtouser ./external/cmd/helpers/emailtouser
bin/registeruser: external/cmd/helpers/registeruser/main.go bin
go build -o bin/registeruser ./external/cmd/helpers/registeruser
cmds: bin/launcher bin/stats bin/contrib bin/request
bin/launcher: cmd/launcher/main.go bin
go build -o bin/launcher ./cmd/launcher
bin/stats: cmd/stats/main.go bin
go build -o bin/stats ./cmd/stats
bin/contrib: cmd/contrib/main.go bin
go build -o bin/contrib ./cmd/contrib
bin/request: cmd/request/main.go bin
go build -o bin/request ./cmd/request
bin:
mkdir -p bin

View File

@ -1,7 +0,0 @@
package main
import "fmt"
func main() {
fmt.Println("TODO")
}

View File

@ -31,17 +31,17 @@ See you on the server,
func loadPassword() (string, error) {
f, err := os.Open("/town/docs/smtp.pw")
if err != nil {
return "", err
return "", fmt.Errorf("could not open smtp password file: %w", err)
}
pw := make([]byte, 100)
n, err := f.Read(pw)
if err != nil {
return "", err
return "", fmt.Errorf("could not read smtp password file: %w", err)
}
if n == 0 {
return "", errors.New("read nothing")
return "", errors.New("smtp password file was empty")
}
return string(pw[0:n]), nil

47
codes/codes.go 100644
View File

@ -0,0 +1,47 @@
package codes
import (
"crypto/rand"
"encoding/base64"
"math/big"
"strings"
)
const codeLen = 32
func NewCode(email string) string {
charset := "abcdefghijklmnopqrztuvwxyz"
charset += strings.ToUpper(charset)
charset += "0123456789"
charset += "`~!@#$%^&*()-=_+[]{}|;:,./<>?"
code := []byte{}
max := big.NewInt(int64(len(charset)))
for len(code) < codeLen {
ix, err := rand.Int(rand.Reader, max)
if err != nil {
// TODO this is bad but I'm just kind of hoping it doesn't happen...often
panic(err)
}
code = append(code, charset[ix.Int64()])
}
code = append(code, ' ')
eb := []byte(email)
for x := 0; x < len(eb); x++ {
code = append(code, eb[x])
}
return base64.StdEncoding.EncodeToString(code)
}
func Decode(code string) ([]string, error) {
decoded, err := base64.StdEncoding.DecodeString(code)
if err != nil {
return nil, err
}
return strings.Split(string(decoded), " "), nil
}

1
external/README.md vendored 100644
View File

@ -0,0 +1 @@
this folder contains the things that external people can access via ssh: join@tilde.town, welcome@tilde.town, and help@tilde.town

60
external/cmd/help/email.go vendored 100644
View File

@ -0,0 +1,60 @@
package main
import (
"errors"
"fmt"
"os"
"git.tilde.town/tildetown/town/email"
)
const emailText = `hello!
You (hopefully) requested to add a new public key to your tilde.town account.
If you didn't, feel free to ignore this email (or report it to an admin).
If you did, here is your auth code: %s
To use this code, please open a terminal and run:
ssh help@tilde.town
Follow the instructions there to add your new key and restore access to your account.
best,
~vilmibm`
func loadPassword() (string, error) {
f, err := os.Open("/town/docs/smtp_help.pw")
if err != nil {
return "", fmt.Errorf("could not open smtp password file: %w", err)
}
pw := make([]byte, 100)
n, err := f.Read(pw)
if err != nil {
return "", fmt.Errorf("could not read smtp password file: %w", err)
}
if n == 0 {
return "", errors.New("smtp password file was empty")
}
return string(pw[0:n]), nil
}
func sendAuthCodeEmail(ac AuthCode) error {
pw, err := loadPassword()
if err != nil {
return err
}
body := fmt.Sprintf(emailText, ac.Code)
mailer := email.NewExternalMailer(pw)
return mailer.Send(
ac.Email,
"Adding a new tilde.town public key",
body)
}

404
external/cmd/help/main.go vendored 100644
View File

@ -0,0 +1,404 @@
package main
import (
"bytes"
"database/sql"
"errors"
"fmt"
"log"
"os"
"os/exec"
"strconv"
"strings"
"git.tilde.town/tildetown/town/codes"
"git.tilde.town/tildetown/town/external/lockingwriter"
"git.tilde.town/tildetown/town/sshkey"
"github.com/charmbracelet/lipgloss"
_ "github.com/mattn/go-sqlite3"
"github.com/mattn/go-tty"
)
// TODO consider local-only help command for renaming, email mgmt, deleting account
// TODO put colorscheme, prompting stuff into own packages for use in the other commands. would be good to get off of survey.
// TODO use new lockingwriter for logging in the other external commands
func connectDB() (*sql.DB, error) {
db, err := sql.Open("sqlite3", "/town/var/codes/codes.db?mode=rw")
if err != nil {
return nil, err
}
return db, nil
}
type colorScheme struct {
Header func(string) string
Subtitle func(string) string
Prompt func(string) string
Email func(string) string
Option func(string) string
Error func(string) string
}
func newColorScheme() colorScheme {
s2r := func(s lipgloss.Style) func(string) string {
return s.Render
}
c := func(s string) lipgloss.Color {
return lipgloss.Color(s)
}
s := lipgloss.NewStyle
return colorScheme{
Header: s2r(s().Bold(true).Foreground(c("#E0B0FF"))),
Subtitle: s2r(s().Italic(true).Foreground(c("gray"))),
Email: s2r(s().Bold(true).Underline(true)),
Prompt: s2r(s().Bold(true).Foreground(c("#00752d"))),
Option: s2r(s().Bold(true).Foreground(c("#38747a"))),
Error: s2r(s().Bold(true).Foreground(c("#f43124"))),
}
}
type Prompter struct {
cs colorScheme
tty *tty.TTY
}
func NewPrompter(tty *tty.TTY, cs colorScheme) *Prompter {
return &Prompter{
cs: cs,
tty: tty,
}
}
func (p *Prompter) String(prompt string) (string, error) {
// TODO assumes blank is no bueno
var err error
var answer string
for answer == "" {
fmt.Println("")
fmt.Println(p.cs.Prompt(prompt))
fmt.Println(p.cs.Subtitle("(press enter to submit)"))
answer, err = p.tty.ReadString()
if err != nil {
return "", fmt.Errorf("couldn't collect input: %w", err)
}
}
return answer, nil
}
func (p *Prompter) Select(prompt string, opts []string) (int, error) {
fmt.Println()
fmt.Println(p.cs.Prompt(prompt))
fmt.Println(p.cs.Subtitle("(pick an option using the corresponding number)"))
chosen := -1
for chosen < 0 {
fmt.Println()
for ix, o := range opts {
fmt.Printf("%s: %s\n", p.cs.Option(fmt.Sprintf("%d", ix+1)), o)
}
r, err := p.tty.ReadRune()
if err != nil {
return -1, fmt.Errorf("could not collect answer for '%s': %w", prompt, err)
}
c, err := strconv.Atoi(string(r))
if err != nil {
fmt.Println()
fmt.Printf("I could not understand '%s'. Try again, please.\n", string(r))
continue
}
if c > len(opts) || c == 0 {
fmt.Println()
fmt.Printf("%s is not an option. Try again, please.\n", string(r))
continue
}
chosen = c - 1
}
fmt.Println("")
return chosen, nil
}
func _main(cs colorScheme) error {
lw := lockingwriter.New()
l := log.New(lw, "help: ", log.Ldate|log.Ltime|log.LUTC|log.Lshortfile|log.Lmsgprefix)
db, err := connectDB()
if err != nil {
return fmt.Errorf("could not connect to database. please let root@tilde.town know about this.")
}
fmt.Println(cs.Header("Hi, you have reached the tilde town help desk."))
fmt.Println()
fmt.Println("Please check out the options below.")
fmt.Printf("If none of them apply to your case, you can email %s. \n", cs.Email("root@tilde.town"))
tty, err := tty.Open()
if err != nil {
return fmt.Errorf("could not open tty: %w", err)
}
defer tty.Close()
p := NewPrompter(tty, cs)
options := []string{
"I need to request that a new SSH key be added to my account.",
"I have a code from my e-mail to redeem for a new SSH key",
"I just want out of here",
}
c, err := p.Select("What do you need help with?", options)
defer func() {
}()
switch c {
case 0:
return collectEmail(l, db, cs, p)
case 1:
return redeemCode(l, db, cs, p)
case 2:
return nil
}
return nil
}
func emailToUsername(email string) (string, error) {
cmd := exec.Command("sudo", "/town/bin/emailtouser", email)
stderrBuff := bytes.NewBuffer([]byte{})
stdoutBuff := bytes.NewBuffer([]byte{})
cmd.Stderr = stderrBuff
cmd.Stdout = stdoutBuff
if err := cmd.Run(); err != nil {
return "", fmt.Errorf("emailtouser failed with '%s': %w", stderrBuff.String(), err)
}
return strings.TrimSpace(stdoutBuff.String()), nil
}
func collectEmail(l *log.Logger, db *sql.DB, cs colorScheme, p *Prompter) error {
fmt.Println(cs.Header("We can send a authorization code to an email associated with your town account."))
email, err := p.String("email to send reset code to?")
if err != nil {
return err
}
fmt.Println()
fmt.Println(cs.Header("thanks!"))
fmt.Println()
fmt.Printf("If %s is associated with a town account we'll email an authorization code.\n", cs.Email(email))
mustHave := []string{"@", "."}
found := 0
for _, s := range mustHave {
if strings.Contains(email, s) {
found++
}
}
if found != len(mustHave) {
l.Printf("corrupt email '%s'", email)
return nil
}
if _, err = emailToUsername(email); err != nil {
l.Printf("no user for '%s'", email)
return nil
}
code := codes.NewCode(email)
ac := &AuthCode{
Code: code,
Email: email,
}
if err = ac.Insert(db); err != nil {
l.Printf("database error: %s", err.Error())
return errors.New("the database was sad")
}
if err = sendAuthCodeEmail(*ac); err != nil {
l.Printf("mail send error: %s", err.Error())
return errors.New("email sending failed")
}
return nil
}
func redeemCode(l *log.Logger, db *sql.DB, cs colorScheme, p *Prompter) error {
fmt.Println(cs.Header("redeem an auth code and add a new public key"))
c, err := p.String("paste your auth code:")
if err != nil {
l.Printf("failed to prompt: %s", err.Error())
fmt.Println(cs.Error("sorry, I couldn't read that."))
return nil
}
parts, err := codes.Decode(c)
if err != nil {
l.Printf("failed to decode auth code: %s", err.Error())
fmt.Println(cs.Error("sorry, that doesn't look like an auth code..."))
return nil
}
code := &AuthCode{
Code: c,
Email: parts[1],
}
err = code.Hydrate(db)
if err != nil {
l.Printf("hydrate failed: %s", err.Error())
return errors.New("the database is sad")
}
if code.Used {
fmt.Println(cs.Error("That code has already been redeemed. You'll have to request a new one."))
return nil
}
username, err := emailToUsername(code.Email)
if err != nil {
l.Printf("could not find user: %s", err.Error())
fmt.Println(cs.Error("That code doesn't seem to match an account."))
return nil
}
fmt.Println()
fmt.Printf("hi, ~%s", username)
key, err := p.String("paste your new public key:")
if err != nil {
l.Printf("failed to prompt: %s", err.Error())
fmt.Println(cs.Error("sorry, I couldn't read that."))
return nil
}
valid, err := sshkey.ValidKey(key)
if err != nil {
return fmt.Errorf("failed to validate key: %w", err)
}
if !valid {
errMsg := fmt.Sprintf("that key is invalid: %s", err.Error())
fmt.Println(cs.Error(errMsg))
return nil
}
cmd := exec.Command("sudo", "--user", username, "/town/bin/appendkeyfile")
cmd.Stdin = bytes.NewBufferString(key + "\n")
stdoutBuff := bytes.NewBuffer([]byte{})
cmd.Stdout = stdoutBuff
stderrBuff := bytes.NewBuffer([]byte{})
cmd.Stderr = stderrBuff
if err = cmd.Run(); err != nil {
l.Printf("appendkeyfile failed with '%s', '%s': %s", stderrBuff.String(), stdoutBuff.String(), err.Error())
return errors.New("adding to keys file failed")
}
err = code.MarkUsed(db)
if err != nil {
l.Printf("failed to mark used: %s", err.Error())
return errors.New("database was sad")
}
fmt.Println()
fmt.Println("new key added! you should be able to use it to log in.")
return nil
}
func main() {
cs := newColorScheme()
err := _main(cs)
defer func() {
fmt.Println()
fmt.Println(cs.Header("bye~"))
}()
if err != nil {
fmt.Println(
cs.Error(fmt.Sprintf("sorry, something went wrong: %s", err.Error())))
fmt.Println("Please let an admin know by emailing a copy of this error to root@tilde.town")
os.Exit(1)
}
}
type AuthCode struct {
ID int64
Code string
Email string
Used bool
}
func (c *AuthCode) Insert(db *sql.DB) error {
stmt, err := db.Prepare(`
INSERT INTO auth_codes (code, email)
VALUES (?, ?)`)
if err != nil {
return err
}
defer stmt.Close()
result, err := stmt.Exec(c.Code, c.Email)
if err != nil {
return err
}
liid, err := result.LastInsertId()
if err != nil {
return err
}
c.ID = liid
return nil
}
func (c *AuthCode) Hydrate(db *sql.DB) error {
stmt, err := db.Prepare(`
SELECT id, used
FROM auth_codes
WHERE code = ? AND email = ?`)
if err != nil {
return err
}
defer stmt.Close()
return stmt.QueryRow(c.Code, c.Email).Scan(&c.ID, &c.Used)
}
func (c *AuthCode) MarkUsed(db *sql.DB) error {
if c.ID == 0 {
return errors.New("not hydrated")
}
stmt, err := db.Prepare(`
UPDATE auth_codes SET used = 1 WHERE id = ?`)
if err != nil {
return err
}
defer stmt.Close()
result, err := stmt.Exec(c.ID)
if err != nil {
return err
}
var rowsAffected int64
if rowsAffected, err = result.RowsAffected(); err != nil {
return err
}
if rowsAffected == 0 {
return errors.New("no rows affected")
}
return nil
}

View File

@ -0,0 +1,59 @@
package main
/*
The purpose of this command is to be run via sudo as an arbitrary user by the "help" user. It is invoked as part of the "i need to add a new public key" flow from "ssh help@tilde.town".
It's based on the createkeyfile helper and heavily copy pasta'd. They should probably share code or be a single command but I wanted to keep things simple for now.
*/
import (
"fmt"
"os"
"os/user"
"path"
)
const keyfileName = "authorized_keys2"
func quit(msg string, code int) {
// TODO print to stderr
fmt.Println(msg)
os.Exit(code)
}
func main() {
u, err := user.Current()
if err != nil {
quit(err.Error(), 2)
}
sshPath := path.Join("/home", u.Username, ".ssh")
keyfilePath := path.Join(sshPath, keyfileName)
f, err := os.OpenFile(keyfilePath, os.O_APPEND|os.O_WRONLY, 0600)
if err != nil {
quit(fmt.Sprintf("failed to open %s: %s", keyfilePath, err.Error()), 5)
}
defer f.Close()
stdin := make([]byte, 90000) // arbitrary limit
n, err := os.Stdin.Read(stdin)
if err != nil {
quit(err.Error(), 6)
} else if n == 0 {
quit("nothing passed on STDIN", 7)
}
stdin = stdin[0:n]
n, err = f.Write(stdin)
if err != nil {
quit(err.Error(), 9)
} else if n == 0 {
quit("wrote nothing to keyfile", 10)
}
}

View File

@ -76,12 +76,13 @@ func main() {
quit(fmt.Sprintf("file contents look wrong: %s", string(stdin)), 8)
}
n, err = f.Write(stdin)
_, err = f.Write(stdin)
if err != nil {
quit(err.Error(), 9)
} else if n == 0 {
quit("wrote nothing to keyfile", 10)
}
_, err = f.WriteString("\n")
}
/*

View File

@ -0,0 +1,40 @@
package main
import (
"errors"
"fmt"
"os"
"git.tilde.town/tildetown/town/towndb"
)
func _main(args []string) error {
if len(args) < 2 {
return errors.New("need email")
}
email := args[1]
db, err := towndb.ConnectDB()
if err != nil {
return err
}
user, err := towndb.UserForEmail(db, email)
if err != nil {
return err
}
if user == nil {
return errors.New("email does not correspond to user")
}
fmt.Print(user.Username)
return nil
}
func main() {
if err := _main(os.Args); err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err.Error())
os.Exit(1)
}
}

View File

@ -134,7 +134,6 @@ func main() {
}
func _main(l *log.Logger, db *sql.DB) error {
l.Println("starting a session")
pages := tview.NewPages()
mainFlex := tview.NewFlex()
input := tview.NewTextArea()
@ -305,7 +304,6 @@ func _main(l *log.Logger, db *sql.DB) error {
/nod
and pressing enter will cause you to nod. some other verbs: /quit /look`))
case "quit":
l.Println("got /quit")
app.Stop()
case "look":
fmt.Fprintln(msgScroll, "")
@ -314,8 +312,6 @@ func _main(l *log.Logger, db *sql.DB) error {
if !sm.Advance() {
fmt.Fprintln(msgScroll, "you nod, but nothing happens.")
fmt.Fprintln(msgScroll)
} else {
l.Println("advancing scene")
}
}
return
@ -334,10 +330,7 @@ func _main(l *log.Logger, db *sql.DB) error {
msgScroll.ScrollToEnd()
}
defer func() {
l.Println("exiting")
db.Close()
}()
defer db.Close()
app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
switch event.Key() {

View File

@ -7,6 +7,7 @@ import (
"fmt"
"os"
"os/exec"
"strings"
"time"
"git.tilde.town/tildetown/town/invites"
@ -142,7 +143,7 @@ This program is going to exit and you are now free to ssh to town as yourself:
if your public key isn't found by ssh, you'll need to explain to ssh how to find it with:
ssh -i "replace with path to public key file" %[1]s@tilde.town
ssh -i "replace with path to private key file" %[1]s@tilde.town
for help with ssh, see: https://tilde.town/wiki/getting-started/ssh.html
@ -205,12 +206,16 @@ func createUser(data newUserData) (err error) {
}
func keyfileText(data newUserData) string {
pkey := data.PubKey
if !strings.HasSuffix(pkey, "\n") {
pkey += "\n"
}
header := `########## GREETINGS! ##########
# Hi! This file was automatically generated by tilde.town when
# This file was automatically generated by tilde.town when
# your account was created. You can edit it if you want, but we
# recommend adding stuff to ~/.ssh/authorized_keys instead.`
return fmt.Sprintf("%s\n%s", header, data.PubKey)
return fmt.Sprintf("%s\n%s", header, pkey)
}
func main() {

View File

@ -0,0 +1,64 @@
package lockingwriter
import (
"os"
"time"
"github.com/gofrs/flock"
)
// for now this package defines a writer for use with log.New(). It it intended to be used from the external ssh applications. This logger uses a file lock to allow all the various ssh applications to log to the same file.
// the correct way to do this is to send log events to a daemon
// but i'm trying to cut a corner
type LockingWriter struct {
path string
}
const (
fp = "/town/var/log/external.log"
lp = "/town/var/log/log.lock"
)
func New() *LockingWriter {
f, err := os.OpenFile(fp, os.O_EXCL|os.O_CREATE|os.O_WRONLY, 0660)
if err != nil && !os.IsExist(err) {
panic(err)
}
f.Close()
return &LockingWriter{
path: fp,
}
}
func (l *LockingWriter) Write(p []byte) (n int, err error) {
fl := flock.New(lp)
var locked bool
for !locked {
locked, err = fl.TryLock()
if err != nil {
return
}
time.Sleep(time.Second)
}
var f *os.File
f, err = os.OpenFile(l.path, os.O_APPEND|os.O_WRONLY, 0660)
if err != nil {
return
}
defer f.Close()
defer fl.Unlock()
n, err = f.Write(p)
if err != nil {
panic(err)
}
return
//return f.Write(p)
}

4
go.mod
View File

@ -8,7 +8,9 @@ require (
github.com/charmbracelet/glamour v0.5.0
github.com/charmbracelet/lipgloss v0.6.0
github.com/gdamore/tcell/v2 v2.5.3
github.com/gofrs/flock v0.8.1
github.com/mattn/go-sqlite3 v1.14.16
github.com/mattn/go-tty v0.0.5
github.com/rivo/tview v0.0.0-20230130130022-4a1b7a76c01c
github.com/spf13/cobra v1.5.0
gopkg.in/yaml.v3 v3.0.1
@ -23,7 +25,7 @@ require (
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-colorable v0.1.2 // indirect
github.com/mattn/go-colorable v0.1.4 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect

11
go.sum
View File

@ -24,6 +24,8 @@ github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdk
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell/v2 v2.5.3 h1:b9XQrT6QGbgI7JvZOJXFNczOQeIYbo8BfeSMzt2sAV0=
github.com/gdamore/tcell/v2 v2.5.3/go.mod h1:wSkrPaXoiIWZqW/g7Px4xc79di6FTcpB8tvaKJ6uGBo=
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
@ -34,12 +36,15 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
@ -47,6 +52,8 @@ github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-tty v0.0.5 h1:s09uXI7yDbXzzTTfw3zonKFzwGkyYlgU3OMjqA0ddz4=
github.com/mattn/go-tty v0.0.5/go.mod h1:u5GGXBtZU6RQoKV8gY5W6UhMudbR5vXnUe7j3pxse28=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/microcosm-cc/bluemonday v1.0.17 h1:Z1a//hgsQ4yjC+8zEkV8IWySkXnsxmdSY642CTFQb5Y=
@ -84,6 +91,8 @@ github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGj
golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

View File

@ -1,20 +1,16 @@
package invites
import (
"crypto/rand"
"database/sql"
"encoding/base64"
"errors"
"math/big"
"strings"
"time"
"git.tilde.town/tildetown/town/codes"
_ "github.com/mattn/go-sqlite3"
)
const (
dsn = "/town/var/invites/invites.db?mode=rw"
codeLen = 32
dsn = "/town/var/invites/invites.db?mode=rw"
)
type Invite struct {
@ -33,7 +29,7 @@ func (i *Invite) Insert(db *sql.DB) error {
return err
}
i.Code = generateCode(i.Email)
i.Code = codes.NewCode(i.Email)
_, err = stmt.Exec(i.Code, i.Email)
if err != nil {
@ -53,44 +49,6 @@ func ConnectDB() (*sql.DB, error) {
return db, nil
}
func generateCode(email string) string {
charset := "abcdefghijklmnopqrztuvwxyz"
charset += strings.ToUpper(charset)
charset += "0123456789"
charset += "`~!@#$%^&*()-=_+[]{}|;:,./<>?"
code := []byte{}
max := big.NewInt(int64(len(charset)))
for len(code) < codeLen {
ix, err := rand.Int(rand.Reader, max)
if err != nil {
// TODO this is bad but I'm just kind of hoping it doesn't happen...often
panic(err)
}
code = append(code, charset[ix.Int64()])
}
code = append(code, ' ')
eb := []byte(email)
for x := 0; x < len(eb); x++ {
code = append(code, eb[x])
}
return base64.StdEncoding.EncodeToString(code)
}
func Decode(code string) ([]string, error) {
decoded, err := base64.StdEncoding.DecodeString(code)
if err != nil {
return nil, err
}
return strings.Split(string(decoded), " "), nil
}
func Get(db *sql.DB, code string) (*Invite, error) {
inv := &Invite{
Code: code,

View File

@ -0,0 +1,7 @@
CREATE TABLE IF NOT EXISTS auth_codes (
id INTEGER PRIMARY KEY,
created TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M', 'now', 'localtime')),
code TEXT,
email TEXT,
used INTEGER DEFAULT 0
);

View File

@ -2,6 +2,7 @@ package towndb
import (
"database/sql"
"errors"
"time"
_ "github.com/mattn/go-sqlite3"
@ -142,6 +143,29 @@ func (u *TownUser) Insert(db *sql.DB) (err error) {
return tx.Commit()
}
// UserForEmail returns the user associated with an email or nil if no matching user is found
func UserForEmail(db *sql.DB, address string) (*TownUser, error) {
stmt, err := db.Prepare(`
SELECT u.id, u.username FROM users u
JOIN emails e ON e.userid = u.id
WHERE e.address = ?
`)
if err != nil {
return nil, err
}
defer stmt.Close()
row := stmt.QueryRow(address)
u := &TownUser{}
if err = row.Scan(&u.ID, &u.Username); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
return nil, err
}
return u, nil
}
func ConnectDB() (*sql.DB, error) {
db, err := sql.Open("sqlite3", dsn)
if err != nil {