all repos — flounder @ 1dff307d9086d5015da568e89a51ffcc07d44a42

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