all repos — flounder @ 161962dc4b3ea854aebdda56410ee5a1524e426f

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