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