all repos — flounder @ c119411f01c43e7e18898c5a283757e58e4cfe02

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