all repos — flounder @ 375e32af23a03e8d06704af706a817cf5beb5129

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