all repos — flounder @ eb22734b0e46270fc4f4224a073142d01423997b

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