all repos — flounder @ 055fcbe201d0fd4c1dd9b1c966359cdc9a11b31f

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