2020-04-13 12:15:12 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2020-04-13 21:48:49 +00:00
|
|
|
"os"
|
2020-04-13 12:15:12 +00:00
|
|
|
"sync"
|
2020-04-30 09:27:05 +00:00
|
|
|
"time"
|
2020-05-12 12:48:21 +00:00
|
|
|
"regexp"
|
2020-04-13 12:15:12 +00:00
|
|
|
)
|
|
|
|
|
2020-04-30 09:48:24 +00:00
|
|
|
const (
|
2020-05-03 21:42:51 +00:00
|
|
|
/* Help converting file size stat to supplied size in megabytes */
|
|
|
|
BytesInMegaByte = 1048576.0
|
|
|
|
|
|
|
|
/* Filename constants */
|
|
|
|
CgiBinDirStr = "cgi-bin"
|
|
|
|
GophermapFileStr = "gophermap"
|
2020-04-30 09:48:24 +00:00
|
|
|
)
|
|
|
|
|
2020-04-29 17:28:51 +00:00
|
|
|
type FileSystem struct {
|
2020-05-04 17:19:56 +00:00
|
|
|
/* Holds and helps manage our file cache, as well as managing
|
2020-05-08 15:52:17 +00:00
|
|
|
* access and responses to requests submitted a worker instance.
|
2020-05-04 17:19:56 +00:00
|
|
|
*/
|
|
|
|
|
2020-05-08 15:52:17 +00:00
|
|
|
CacheMap *FixedMap
|
|
|
|
CacheMutex sync.RWMutex
|
|
|
|
CacheFileMax int64
|
2020-05-12 12:48:21 +00:00
|
|
|
Remaps []*FileRemap
|
|
|
|
Restricted []*regexp.Regexp
|
2020-04-13 12:15:12 +00:00
|
|
|
}
|
|
|
|
|
2020-04-29 17:28:51 +00:00
|
|
|
func (fs *FileSystem) Init(size int, fileSizeMax float64) {
|
|
|
|
fs.CacheMap = NewFixedMap(size)
|
|
|
|
fs.CacheMutex = sync.RWMutex{}
|
|
|
|
fs.CacheFileMax = int64(BytesInMegaByte * fileSizeMax)
|
2020-05-07 19:52:25 +00:00
|
|
|
/* {,Reverse}Remap map is setup in `gophor.go`, no need to here */
|
|
|
|
}
|
|
|
|
|
2020-05-12 12:48:21 +00:00
|
|
|
func (fs *FileSystem) IsRestricted(path string) bool {
|
|
|
|
for _, regex := range fs.Restricted {
|
|
|
|
if regex.MatchString(path) {
|
|
|
|
return true
|
|
|
|
}
|
2020-05-07 19:52:25 +00:00
|
|
|
}
|
2020-05-12 12:48:21 +00:00
|
|
|
return false
|
2020-05-07 19:52:25 +00:00
|
|
|
}
|
|
|
|
|
2020-05-12 12:48:21 +00:00
|
|
|
func (fs *FileSystem) RemapRequestPath(requestPath *RequestPath) (*RequestPath, bool) {
|
|
|
|
for _, remap := range fs.Remaps {
|
|
|
|
/* No match :( keep lookin */
|
|
|
|
if !remap.Regex.MatchString(requestPath.Relative()) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Create new path from template and submatches */
|
|
|
|
newPath := make([]byte, 0)
|
2020-05-12 13:04:16 +00:00
|
|
|
for _, submatches := range remap.Regex.FindAllStringSubmatchIndex(requestPath.Relative(), -1) {
|
2020-05-12 12:48:21 +00:00
|
|
|
newPath = remap.Regex.ExpandString(newPath, remap.Template, requestPath.Relative(), submatches)
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Ignore empty replacement path */
|
|
|
|
if len(newPath) == 0 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Set this new path to the _actual_ path */
|
|
|
|
return requestPath.RemapPath(string(newPath)), true
|
2020-05-07 19:52:25 +00:00
|
|
|
}
|
2020-05-12 12:48:21 +00:00
|
|
|
|
|
|
|
return nil, false
|
2020-04-13 12:15:12 +00:00
|
|
|
}
|
|
|
|
|
2020-05-07 07:59:53 +00:00
|
|
|
func (fs *FileSystem) HandleRequest(responder *Responder) *GophorError {
|
2020-05-05 22:46:50 +00:00
|
|
|
/* Check if restricted file */
|
2020-05-12 12:48:21 +00:00
|
|
|
if fs.IsRestricted(responder.Request.Path.Relative()) {
|
2020-05-05 22:46:50 +00:00
|
|
|
return &GophorError{ IllegalPathErr, nil }
|
|
|
|
}
|
|
|
|
|
2020-05-12 12:48:21 +00:00
|
|
|
/* Try remap according to supplied regex */
|
|
|
|
remap, doneRemap := fs.RemapRequestPath(responder.Request.Path)
|
|
|
|
|
|
|
|
var err error
|
|
|
|
var stat os.FileInfo
|
|
|
|
if doneRemap {
|
|
|
|
/* Try get the remapped path */
|
|
|
|
stat, err = os.Stat(remap.Absolute())
|
|
|
|
if err == nil {
|
|
|
|
/* Remapped path exists, set this! */
|
|
|
|
responder.Request.Path = remap
|
|
|
|
} else {
|
|
|
|
/* Last ditch effort to grab generated file */
|
|
|
|
return fs.FetchGeneratedFile(responder, err)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
/* Just get regular supplied request path */
|
|
|
|
stat, err = os.Stat(responder.Request.Path.Absolute())
|
|
|
|
if err != nil {
|
|
|
|
/* Last ditch effort to grab generated file */
|
|
|
|
return fs.FetchGeneratedFile(responder, err)
|
2020-04-25 17:38:26 +00:00
|
|
|
}
|
2020-05-01 14:08:47 +00:00
|
|
|
}
|
2020-04-29 22:21:20 +00:00
|
|
|
|
2020-05-01 14:08:47 +00:00
|
|
|
switch {
|
2020-05-03 21:42:51 +00:00
|
|
|
/* Directory */
|
2020-05-01 14:08:47 +00:00
|
|
|
case stat.Mode() & os.ModeDir != 0:
|
2020-05-05 08:35:29 +00:00
|
|
|
/* Ignore anything under cgi-bin directory */
|
2020-05-08 15:52:17 +00:00
|
|
|
if responder.Request.Path.HasRelPrefix(CgiBinDirStr) {
|
2020-05-05 22:46:50 +00:00
|
|
|
return &GophorError{ IllegalPathErr, nil }
|
2020-05-03 21:42:51 +00:00
|
|
|
}
|
2020-04-29 17:28:51 +00:00
|
|
|
|
2020-04-30 12:58:29 +00:00
|
|
|
/* Check Gophermap exists */
|
2020-05-08 15:52:17 +00:00
|
|
|
gophermapPath := NewRequestPath(responder.Request.Path.RootDir(), responder.Request.Path.JoinRel(GophermapFileStr))
|
2020-05-05 22:46:50 +00:00
|
|
|
stat, err = os.Stat(gophermapPath.Absolute())
|
2020-04-30 12:58:29 +00:00
|
|
|
|
2020-04-29 17:28:51 +00:00
|
|
|
if err == nil {
|
2020-05-08 15:52:17 +00:00
|
|
|
/* Gophermap exists! If executable try return executed contents, else serve as regular gophermap. */
|
2020-05-07 07:59:53 +00:00
|
|
|
gophermapRequest := &Request{ gophermapPath, responder.Request.Parameters }
|
|
|
|
responder.Request = gophermapRequest
|
2020-05-05 22:46:50 +00:00
|
|
|
|
2020-05-03 21:42:51 +00:00
|
|
|
if stat.Mode().Perm() & 0100 != 0 {
|
2020-05-07 20:48:16 +00:00
|
|
|
return responder.SafeFlush(executeFile(responder))
|
2020-05-03 21:42:51 +00:00
|
|
|
} else {
|
2020-05-07 07:59:53 +00:00
|
|
|
return fs.FetchFile(responder)
|
2020-05-03 21:42:51 +00:00
|
|
|
}
|
2020-04-29 17:28:51 +00:00
|
|
|
} else {
|
|
|
|
/* No gophermap, serve directory listing */
|
2020-05-08 15:52:17 +00:00
|
|
|
return listDirAsGophermap(responder, map[string]bool{ gophermapPath.Relative(): true, CgiBinDirStr: true })
|
2020-04-30 12:58:29 +00:00
|
|
|
}
|
|
|
|
|
2020-04-29 17:28:51 +00:00
|
|
|
/* Regular file */
|
2020-05-03 21:42:51 +00:00
|
|
|
case stat.Mode() & os.ModeType == 0:
|
2020-05-08 15:52:17 +00:00
|
|
|
/* If cgi-bin, try return executed contents. Else, fetch regular file */
|
|
|
|
if responder.Request.Path.HasRelPrefix(CgiBinDirStr) {
|
2020-05-07 20:48:16 +00:00
|
|
|
return responder.SafeFlush(executeCgi(responder))
|
2020-05-03 21:42:51 +00:00
|
|
|
} else {
|
2020-05-07 07:59:53 +00:00
|
|
|
return fs.FetchFile(responder)
|
2020-05-03 21:42:51 +00:00
|
|
|
}
|
2020-04-29 17:28:51 +00:00
|
|
|
|
|
|
|
/* Unsupported type */
|
|
|
|
default:
|
2020-05-05 22:46:50 +00:00
|
|
|
return &GophorError{ FileTypeErr, nil }
|
2020-04-29 17:28:51 +00:00
|
|
|
}
|
2020-04-17 15:39:25 +00:00
|
|
|
}
|
|
|
|
|
2020-05-12 12:48:21 +00:00
|
|
|
func (fs *FileSystem) FetchGeneratedFile(responder *Responder, err error) *GophorError {
|
|
|
|
fs.CacheMutex.RLock()
|
|
|
|
file := fs.CacheMap.Get(responder.Request.Path.Absolute())
|
|
|
|
if file == nil {
|
|
|
|
/* Generated file at path not in cache map either, return */
|
|
|
|
fs.CacheMutex.RUnlock()
|
|
|
|
return &GophorError{ FileStatErr, err }
|
|
|
|
}
|
|
|
|
|
|
|
|
/* It's there! Get contents! */
|
|
|
|
file.Mutex.RLock()
|
|
|
|
gophorErr := file.WriteContents(responder)
|
|
|
|
file.Mutex.RUnlock()
|
|
|
|
|
|
|
|
fs.CacheMutex.RUnlock()
|
|
|
|
return gophorErr
|
|
|
|
}
|
|
|
|
|
2020-05-07 07:59:53 +00:00
|
|
|
func (fs *FileSystem) FetchFile(responder *Responder) *GophorError {
|
2020-04-15 11:43:02 +00:00
|
|
|
/* Get cache map read lock then check if file in cache map */
|
2020-04-29 17:28:51 +00:00
|
|
|
fs.CacheMutex.RLock()
|
2020-05-08 15:52:17 +00:00
|
|
|
file := fs.CacheMap.Get(responder.Request.Path.Absolute())
|
2020-04-24 11:09:54 +00:00
|
|
|
|
2020-04-19 19:44:48 +00:00
|
|
|
if file != nil {
|
2020-04-15 11:43:02 +00:00
|
|
|
/* File in cache -- before doing anything get file read lock */
|
2020-04-25 18:56:03 +00:00
|
|
|
file.Mutex.RLock()
|
2020-04-14 10:49:37 +00:00
|
|
|
|
2020-04-15 11:43:02 +00:00
|
|
|
/* Check file is marked as fresh */
|
2020-04-25 18:56:03 +00:00
|
|
|
if !file.Fresh {
|
2020-04-15 11:43:02 +00:00
|
|
|
/* File not fresh! Swap file read for write-lock */
|
2020-04-25 18:56:03 +00:00
|
|
|
file.Mutex.RUnlock()
|
|
|
|
file.Mutex.Lock()
|
2020-04-14 10:49:37 +00:00
|
|
|
|
2020-04-15 11:43:02 +00:00
|
|
|
/* Reload file contents from disk */
|
2020-05-05 22:46:50 +00:00
|
|
|
gophorErr := file.CacheContents()
|
2020-04-13 21:48:49 +00:00
|
|
|
if gophorErr != nil {
|
2020-04-14 10:49:37 +00:00
|
|
|
/* Error loading contents, unlock all mutex then return error */
|
2020-04-25 18:56:03 +00:00
|
|
|
file.Mutex.Unlock()
|
2020-04-29 17:28:51 +00:00
|
|
|
fs.CacheMutex.RUnlock()
|
2020-05-05 22:46:50 +00:00
|
|
|
return gophorErr
|
2020-04-13 21:48:49 +00:00
|
|
|
}
|
2020-04-14 10:49:37 +00:00
|
|
|
|
2020-04-15 11:43:02 +00:00
|
|
|
/* Updated! Swap back file write for read lock */
|
2020-04-25 18:56:03 +00:00
|
|
|
file.Mutex.Unlock()
|
|
|
|
file.Mutex.RLock()
|
2020-04-13 21:48:49 +00:00
|
|
|
}
|
|
|
|
} else {
|
2020-05-05 22:46:50 +00:00
|
|
|
/* Open file here, to check it exists, ready for file stat
|
|
|
|
* and in case file is too big we pass it as a raw response
|
2020-04-24 11:09:54 +00:00
|
|
|
*/
|
2020-05-08 15:52:17 +00:00
|
|
|
fd, err := os.Open(responder.Request.Path.Absolute())
|
2020-04-15 11:43:02 +00:00
|
|
|
if err != nil {
|
2020-04-17 17:12:36 +00:00
|
|
|
/* Error stat'ing file, unlock read mutex then return error */
|
2020-04-29 17:28:51 +00:00
|
|
|
fs.CacheMutex.RUnlock()
|
2020-05-05 22:46:50 +00:00
|
|
|
return &GophorError{ FileOpenErr, err }
|
|
|
|
}
|
|
|
|
|
|
|
|
/* We need a doctor, stat! */
|
|
|
|
stat, err := fd.Stat()
|
|
|
|
if err != nil {
|
|
|
|
/* Error stat'ing file, unlock read mutext then return */
|
|
|
|
fs.CacheMutex.RUnlock()
|
|
|
|
return &GophorError{ FileStatErr, err }
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Compare file size (in MB) to CacheFileSizeMax. If larger, just send file raw */
|
|
|
|
if stat.Size() > fs.CacheFileMax {
|
|
|
|
/* Unlock the read mutex, we don't need it where we're going... returning, we're returning. */
|
|
|
|
fs.CacheMutex.RUnlock()
|
2020-05-07 07:59:53 +00:00
|
|
|
return responder.WriteRaw(fd)
|
2020-04-15 11:43:02 +00:00
|
|
|
}
|
2020-04-13 21:48:49 +00:00
|
|
|
|
2020-05-03 21:42:51 +00:00
|
|
|
/* Create new file contents */
|
2020-04-29 17:28:51 +00:00
|
|
|
var contents FileContents
|
2020-05-08 15:52:17 +00:00
|
|
|
if responder.Request.Path.HasRelSuffix(GophermapFileStr) {
|
|
|
|
contents = &GophermapContents{ responder.Request.Path, nil }
|
2020-04-29 17:28:51 +00:00
|
|
|
} else {
|
2020-05-08 15:52:17 +00:00
|
|
|
contents = &RegularFileContents{ responder.Request.Path, nil }
|
2020-04-29 17:28:51 +00:00
|
|
|
}
|
2020-04-17 15:39:25 +00:00
|
|
|
|
|
|
|
/* Create new file wrapper around contents */
|
2020-05-04 17:19:56 +00:00
|
|
|
file = &File{ contents, sync.RWMutex{}, true, time.Now().UnixNano() }
|
2020-04-13 12:15:12 +00:00
|
|
|
|
2020-04-25 17:38:26 +00:00
|
|
|
/* File isn't in cache yet so no need to get file lock mutex */
|
2020-05-05 22:46:50 +00:00
|
|
|
gophorErr := file.CacheContents()
|
2020-04-13 21:48:49 +00:00
|
|
|
if gophorErr != nil {
|
2020-04-17 17:12:36 +00:00
|
|
|
/* Error loading contents, unlock read mutex then return error */
|
2020-04-29 17:28:51 +00:00
|
|
|
fs.CacheMutex.RUnlock()
|
2020-05-05 22:46:50 +00:00
|
|
|
return gophorErr
|
2020-04-15 11:43:02 +00:00
|
|
|
}
|
|
|
|
|
2020-04-19 20:14:36 +00:00
|
|
|
/* File not in cache -- Swap cache map read for write lock. */
|
2020-04-29 17:28:51 +00:00
|
|
|
fs.CacheMutex.RUnlock()
|
|
|
|
fs.CacheMutex.Lock()
|
2020-04-15 11:43:02 +00:00
|
|
|
|
2020-04-19 19:44:48 +00:00
|
|
|
/* Put file in the FixedMap */
|
2020-05-08 15:52:17 +00:00
|
|
|
fs.CacheMap.Put(responder.Request.Path.Absolute(), file)
|
2020-04-14 10:49:37 +00:00
|
|
|
|
2020-04-15 11:43:02 +00:00
|
|
|
/* Before unlocking cache mutex, lock file read for upcoming call to .Contents() */
|
2020-04-25 18:56:03 +00:00
|
|
|
file.Mutex.RLock()
|
2020-04-15 11:43:02 +00:00
|
|
|
|
2020-04-14 10:49:37 +00:00
|
|
|
/* Swap cache lock back to read */
|
2020-04-29 17:28:51 +00:00
|
|
|
fs.CacheMutex.Unlock()
|
|
|
|
fs.CacheMutex.RLock()
|
2020-04-13 12:15:12 +00:00
|
|
|
}
|
|
|
|
|
2020-05-08 15:52:17 +00:00
|
|
|
/* Write file contents via responder */
|
2020-05-07 07:59:53 +00:00
|
|
|
gophorErr := file.WriteContents(responder)
|
2020-04-25 18:56:03 +00:00
|
|
|
file.Mutex.RUnlock()
|
2020-04-14 10:49:37 +00:00
|
|
|
|
2020-04-15 11:43:02 +00:00
|
|
|
/* Finally we can unlock the cache map read lock, we are done :) */
|
2020-04-29 17:28:51 +00:00
|
|
|
fs.CacheMutex.RUnlock()
|
2020-04-13 12:15:12 +00:00
|
|
|
|
2020-05-05 22:46:50 +00:00
|
|
|
return gophorErr
|
2020-04-13 12:15:12 +00:00
|
|
|
}
|
2020-04-30 09:27:05 +00:00
|
|
|
|
|
|
|
type File struct {
|
2020-05-04 17:19:56 +00:00
|
|
|
/* Wraps around the cached contents of a file
|
2020-05-08 15:52:17 +00:00
|
|
|
* helping with management.
|
2020-05-04 17:19:56 +00:00
|
|
|
*/
|
|
|
|
|
|
|
|
Content FileContents
|
2020-04-30 09:27:05 +00:00
|
|
|
Mutex sync.RWMutex
|
|
|
|
Fresh bool
|
|
|
|
LastRefresh int64
|
|
|
|
}
|
|
|
|
|
2020-05-07 07:59:53 +00:00
|
|
|
func (f *File) WriteContents(responder *Responder) *GophorError {
|
|
|
|
return f.Content.Render(responder)
|
2020-04-30 09:27:05 +00:00
|
|
|
}
|
|
|
|
|
2020-05-05 22:46:50 +00:00
|
|
|
func (f *File) CacheContents() *GophorError {
|
2020-04-30 09:27:05 +00:00
|
|
|
/* Clear current file contents */
|
2020-05-04 17:19:56 +00:00
|
|
|
f.Content.Clear()
|
2020-04-30 09:27:05 +00:00
|
|
|
|
|
|
|
/* Reload the file */
|
2020-05-04 17:19:56 +00:00
|
|
|
gophorErr := f.Content.Load()
|
2020-04-30 09:27:05 +00:00
|
|
|
if gophorErr != nil {
|
|
|
|
return gophorErr
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Update lastRefresh, set fresh, unset deletion (not likely set) */
|
|
|
|
f.LastRefresh = time.Now().UnixNano()
|
2020-05-08 15:52:17 +00:00
|
|
|
f.Fresh = true
|
2020-04-30 09:27:05 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-05-08 15:52:17 +00:00
|
|
|
/* Start the file monitor! */
|
2020-04-30 09:27:05 +00:00
|
|
|
func startFileMonitor(sleepTime time.Duration) {
|
|
|
|
go func() {
|
|
|
|
for {
|
|
|
|
/* Sleep so we don't take up all the precious CPU time :) */
|
|
|
|
time.Sleep(sleepTime)
|
|
|
|
|
|
|
|
/* Check global file cache freshness */
|
|
|
|
checkCacheFreshness()
|
|
|
|
}
|
|
|
|
|
|
|
|
/* We shouldn't have reached here */
|
2020-05-02 21:34:49 +00:00
|
|
|
Config.SysLog.Fatal("", "FileCache monitor escaped run loop!\n")
|
2020-04-30 09:27:05 +00:00
|
|
|
}()
|
|
|
|
}
|
|
|
|
|
2020-05-08 15:52:17 +00:00
|
|
|
/* Check file cache for freshness, deleting files not-on disk */
|
2020-04-30 09:27:05 +00:00
|
|
|
func checkCacheFreshness() {
|
|
|
|
/* Before anything, get cache write lock (in case we have to delete) */
|
|
|
|
Config.FileSystem.CacheMutex.Lock()
|
|
|
|
|
|
|
|
/* Iterate through paths in cache map to query file last modified times */
|
|
|
|
for path := range Config.FileSystem.CacheMap.Map {
|
|
|
|
/* Get file pointer, no need for lock as we have write lock */
|
|
|
|
file := Config.FileSystem.CacheMap.Get(path)
|
|
|
|
|
|
|
|
/* If this is a generated file, we skip */
|
|
|
|
if isGeneratedType(file) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2020-05-08 15:52:17 +00:00
|
|
|
/* Check file still exists on disk, delete and continue if not */
|
2020-04-30 09:27:05 +00:00
|
|
|
stat, err := os.Stat(path)
|
|
|
|
if err != nil {
|
2020-05-02 21:34:49 +00:00
|
|
|
Config.SysLog.Error("", "Failed to stat file in cache: %s\n", path)
|
2020-04-30 09:27:05 +00:00
|
|
|
Config.FileSystem.CacheMap.Remove(path)
|
|
|
|
continue
|
|
|
|
}
|
2020-05-08 15:52:17 +00:00
|
|
|
|
|
|
|
/* Get file's last modified time */
|
2020-04-30 09:27:05 +00:00
|
|
|
timeModified := stat.ModTime().UnixNano()
|
|
|
|
|
2020-05-08 15:52:17 +00:00
|
|
|
/* If the file is marked as fresh, but file on disk is newer, mark as unfresh */
|
2020-04-30 09:27:05 +00:00
|
|
|
if file.Fresh && file.LastRefresh < timeModified {
|
|
|
|
file.Fresh = false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Done! We can release cache read lock */
|
|
|
|
Config.FileSystem.CacheMutex.Unlock()
|
|
|
|
}
|
|
|
|
|
2020-05-08 15:52:17 +00:00
|
|
|
/* Just a helper function to neaten-up checking if file contents is of generated type */
|
2020-04-30 09:27:05 +00:00
|
|
|
func isGeneratedType(file *File) bool {
|
2020-05-04 17:19:56 +00:00
|
|
|
switch file.Content.(type) {
|
2020-04-30 09:27:05 +00:00
|
|
|
case *GeneratedFileContents:
|
|
|
|
return true
|
|
|
|
default:
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|