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