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