forked from tildetown/town
Compare commits
36 Commits
Author | SHA1 | Date |
---|---|---|
vilmibm | ca33731826 | |
vilmibm | 81c9dede67 | |
vilmibm | 4958407856 | |
vilmibm | 9bb456bc17 | |
vilmibm | 69ad6a384a | |
vilmibm | 4284fb4048 | |
vilmibm | 100643d8fc | |
vilmibm | 0764534fed | |
vilmibm | 69666edefa | |
vilmibm | add129826a | |
vilmibm | 90808c1ce0 | |
vilmibm | 17d39483fb | |
vilmibm | 418e4a4a14 | |
vilmibm | 79dc987c61 | |
vilmibm | ba1a1319e3 | |
vilmibm | cbc868ae35 | |
vilmibm | 940779876c | |
vilmibm | f53b2721cb | |
vilmibm | b1ff57ba58 | |
vilmibm | be5020ad28 | |
vilmibm | 9bea4257c1 | |
vilmibm | 2a07a0e200 | |
vilmibm | 7255ee691e | |
vilmibm | 44686ad536 | |
vilmibm | 529e14158a | |
vilmibm | bf244101e6 | |
vilmibm | 6fa11aba8e | |
vilmibm | 5c2142f6e7 | |
vilmibm | 92faddd079 | |
vilmibm | 6950ba7109 | |
vilmibm | a3b13d21b3 | |
vilmibm | 5c6f4cce19 | |
vilmibm | 2a966bf842 | |
vilmibm | 880511a79a | |
vilmibm | 0884ba2ff6 | |
vilmibm | d0b79f3e7c |
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -1,7 +0,0 @@
|
|||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
fmt.Println("TODO")
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
|
||||
}
|
||||
|
||||
/*
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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() {
|
|
@ -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() {
|
|
@ -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
4
go.mod
|
@ -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
11
go.sum
|
@ -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=
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
);
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue