all repos — flounder @ 5e4f552ce6ee50c36e1001575155af7208d477f3

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