all repos — flounder @ 5eec59dca9972e3f10e4bd24bb8ce0127b9ea6be

A small site builder for the Gemini protocol

main.go (view raw)

  1package main
  2
  3import (
  4	"crypto/rand"
  5	"database/sql"
  6	"flag"
  7	"fmt"
  8	"github.com/gorilla/sessions"
  9	"io"
 10	"io/ioutil"
 11	"log"
 12	"os"
 13	"path"
 14	"path/filepath"
 15	"sort"
 16	"sync"
 17	"time"
 18)
 19
 20var c Config // global var to hold static configuration
 21
 22type File struct {
 23	Creator     string
 24	Name        string
 25	UpdatedTime time.Time
 26	TimeAgo     string
 27}
 28
 29type User struct {
 30	Username  string
 31	Email     string
 32	Active    bool
 33	Admin     bool
 34	CreatedAt int // timestamp
 35}
 36
 37func getActiveUserNames() ([]string, error) {
 38	rows, err := DB.Query(`SELECT username from user WHERE active is true`)
 39	if err != nil {
 40		return nil, err
 41	}
 42	var users []string
 43	for rows.Next() {
 44		var user string
 45		err = rows.Scan(&user)
 46		if err != nil {
 47			return nil, err
 48		}
 49		users = append(users, user)
 50	}
 51	return users, nil
 52}
 53
 54func getUsers() ([]User, error) {
 55	rows, err := DB.Query(`SELECT username, email, active, admin, created_at from user ORDER BY created_at DESC`)
 56	if err != nil {
 57		return nil, err
 58	}
 59	var users []User
 60	for rows.Next() {
 61		var user User
 62		err = rows.Scan(&user.Username, &user.Email, &user.Active, &user.Admin, &user.CreatedAt)
 63		if err != nil {
 64			return nil, err
 65		}
 66		users = append(users, user)
 67	}
 68	return users, nil
 69}
 70
 71func getIndexFiles() ([]*File, error) { // cache this function
 72	result := []*File{}
 73	err := filepath.Walk(c.FilesDirectory, func(thepath string, info os.FileInfo, err error) error {
 74		if err != nil {
 75			log.Printf("Failure accessing a path %q: %v\n", thepath, err)
 76			return err // think about
 77		}
 78		// make this do what it should
 79		if !info.IsDir() {
 80			creatorFolder, _ := path.Split(thepath)
 81			updatedTime := info.ModTime()
 82			result = append(result, &File{
 83				Name:        info.Name(),
 84				Creator:     path.Base(creatorFolder),
 85				UpdatedTime: updatedTime,
 86				TimeAgo:     timeago(&updatedTime),
 87			})
 88		}
 89		return nil
 90	})
 91	if err != nil {
 92		return nil, err
 93	}
 94	sort.Slice(result, func(i, j int) bool {
 95		return result[i].UpdatedTime.After(result[j].UpdatedTime)
 96	})
 97	if len(result) > 50 {
 98		result = result[:50]
 99	}
100	return result, nil
101} // todo clean up paths
102
103func getUserFiles(user string) ([]*File, error) {
104	result := []*File{}
105	files, err := ioutil.ReadDir(path.Join(c.FilesDirectory, user))
106	if err != nil {
107		return nil, err
108	}
109	for _, file := range files {
110		result = append(result, &File{
111			Name:        file.Name(),
112			Creator:     user,
113			UpdatedTime: file.ModTime(),
114		})
115	}
116	return result, nil
117}
118
119func createTablesIfDNE() {
120	_, err := DB.Exec(`CREATE TABLE IF NOT EXISTS user (
121  id INTEGER PRIMARY KEY NOT NULL,
122  username TEXT NOT NULL UNIQUE,
123  email TEXT NOT NULL UNIQUE,
124  password_hash TEXT NOT NULL,
125  active boolean NOT NULL DEFAULT false,
126  admin boolean NOT NULL DEFAULT false,
127  created_at INTEGER DEFAULT (strftime('%s', 'now'))
128);
129
130CREATE TABLE IF NOT EXISTS cookie_key (
131  value TEXT NOT NULL
132);`)
133	if err != nil {
134		log.Fatal(err)
135	}
136}
137
138// Generate a cryptographically secure key for the cookie store
139func generateCookieKeyIfDNE() []byte {
140	rows, err := DB.Query("SELECT value FROM cookie_key LIMIT 1")
141	defer rows.Close()
142	if err != nil {
143		log.Fatal(err)
144	}
145	if rows.Next() {
146		var cookie []byte
147		err := rows.Scan(&cookie)
148		if err != nil {
149			log.Fatal(err)
150		}
151		return cookie
152	} else {
153		k := make([]byte, 32)
154		_, err := io.ReadFull(rand.Reader, k)
155		if err != nil {
156			log.Fatal(err)
157		}
158		_, err = DB.Exec("insert into cookie_key values ($1)", k)
159		if err != nil {
160			log.Fatal(err)
161		}
162		return k
163	}
164}
165
166func main() {
167	configPath := flag.String("c", "flounder.toml", "path to config file") // doesnt work atm
168	if len(os.Args) < 2 {
169		fmt.Println("expected 'admin' or 'serve' subcommand")
170		os.Exit(1)
171	}
172	flag.Parse()
173
174	var err error
175	c, err = getConfig(*configPath)
176	if err != nil {
177		log.Fatal(err)
178	}
179	logFile, err := os.OpenFile(c.LogFile, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644)
180	if err != nil {
181		panic(err)
182	}
183	mw := io.MultiWriter(os.Stdout, logFile)
184	log.SetOutput(mw)
185
186	// Generate self signed cert if does not exist. This is not suitable for production.
187	_, err1 := os.Stat(c.TLSCertFile)
188	_, err2 := os.Stat(c.TLSKeyFile)
189	if os.IsNotExist(err1) || os.IsNotExist(err2) {
190		log.Println("Keyfile or certfile does not exist.")
191	}
192
193	// Generate session cookie key if does not exist
194	DB, err = sql.Open("sqlite3", c.DBFile)
195	if err != nil {
196		log.Fatal(err)
197	}
198
199	createTablesIfDNE()
200	cookie := generateCookieKeyIfDNE()
201	SessionStore = sessions.NewCookieStore(cookie)
202
203	switch os.Args[1] {
204	case "serve":
205		wg := new(sync.WaitGroup)
206		wg.Add(2)
207		go func() {
208			runHTTPServer()
209			wg.Done()
210		}()
211		go func() {
212			runGeminiServer()
213			wg.Done()
214		}()
215		wg.Wait()
216	case "admin":
217		runAdminCommand()
218	}
219}