You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
277 lines
6.7 KiB
Go
277 lines
6.7 KiB
Go
package core
|
|
|
|
import (
|
|
"os"
|
|
"regexp"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/grufwub/go-bufpools"
|
|
"github.com/grufwub/go-errors"
|
|
"github.com/grufwub/go-filecache"
|
|
"github.com/grufwub/go-logger"
|
|
)
|
|
|
|
const (
|
|
// Version holds the current version string
|
|
Version = "v3.2.0-beta"
|
|
)
|
|
|
|
var (
|
|
// Root stores the server's root directory
|
|
Root string
|
|
|
|
// Bind stores the server's bound IP
|
|
Bind string
|
|
|
|
// Hostname stores the host's outward hostname
|
|
Hostname string
|
|
|
|
// Port stores the internal port the host is binded to
|
|
Port string
|
|
|
|
// AccessLog is the global Access SLogger
|
|
AccessLog *logger.SLogger
|
|
|
|
// SystemLog is the global System SLogger
|
|
SystemLog *logger.SLogger
|
|
|
|
// FileCache is the global FileCache object
|
|
FileCache *filecache.FileCache
|
|
|
|
// File system related globals
|
|
monitorSleepTime time.Duration
|
|
fileSizeMax int64
|
|
|
|
// Global listener
|
|
serverListener *Listener
|
|
|
|
// Client connection related globals
|
|
connReadDeadline time.Duration
|
|
connWriteDeadline time.Duration
|
|
|
|
// Server protocol
|
|
protocol string
|
|
|
|
// CGI related globals
|
|
cgiPath *Path
|
|
cgiEnv []string
|
|
|
|
// Global OS signal channel
|
|
sigChannel chan os.Signal
|
|
|
|
// Buffer pools
|
|
connRequestBufferPool *bufpools.BufferPool
|
|
connBufferedReaderPool *bufpools.BufferedReaderPool
|
|
connBufferedWriterPool *bufpools.BufferedWriterPool
|
|
fileBufferedReaderPool *bufpools.BufferedReaderPool
|
|
fileBufferPool *bufpools.BufferPool
|
|
pathBuilderPool *sync.Pool
|
|
|
|
// Compiled regex globals
|
|
cgiDirRegex *regexp.Regexp
|
|
restrictedPaths []*regexp.Regexp
|
|
hiddenPaths []*regexp.Regexp
|
|
requestRemaps []*RequestRemap
|
|
|
|
// WithinCGIDir returns whether a path is within the server's specified CGI scripts directory
|
|
WithinCGIDir func(*Path) bool
|
|
|
|
// appendCgiEnv is the global function set by implementor to append protocol specific CGI information
|
|
appendCgiEnv func(*Client, *Request, []string) []string
|
|
|
|
// IsRestrictedPath is the global function to check against restricted paths
|
|
IsRestrictedPath func(*Path) bool
|
|
|
|
// IsHiddenPath is the global function to check against hidden paths
|
|
IsHiddenPath func(*Path) bool
|
|
|
|
// RemapRequest is the global function to remap a request
|
|
RemapRequest func(*Request) bool
|
|
|
|
// Global client-handling filesystem functions
|
|
newFileContent func(*Path) FileContent
|
|
handleDirectory func(*Client, *os.File, *Path) error
|
|
handleLargeFile func(*Client, *os.File, *Path) error
|
|
)
|
|
|
|
// Start begins operation of the server
|
|
func Start(serve func(*Client)) {
|
|
// Start the FileCache freshness monitor
|
|
SystemLog.Infof("Starting cache monitor with freq: %s", monitorSleepTime.String())
|
|
go FileCache.StartMonitor(monitorSleepTime)
|
|
|
|
// Start the listener
|
|
SystemLog.Infof("Listening on %s://%s:%s (%s:%s)", protocol, Hostname, Port, Bind, Port)
|
|
go func() {
|
|
for {
|
|
client, err := serverListener.Accept()
|
|
if err != nil {
|
|
SystemLog.Error(err.Error())
|
|
}
|
|
|
|
// Serve client then close in separate goroutine
|
|
go func() {
|
|
serve(client)
|
|
client.Conn().Close()
|
|
}()
|
|
}
|
|
}()
|
|
|
|
// Listen for OS signals and terminate if necessary
|
|
sig := <-sigChannel
|
|
SystemLog.Infof("Signal received: %s. Shutting down...", sig.String())
|
|
os.Exit(0)
|
|
}
|
|
|
|
// HandleClient handles a Client, attempting to serve their request from the filesystem whether a regular file, gophermap, dir listing or CGI script
|
|
func HandleClient(client *Client, request *Request) error {
|
|
// If restricted, return error
|
|
if IsRestrictedPath(request.Path()) {
|
|
return ErrRestrictedPath.Extend(request.Path().Selector())
|
|
}
|
|
|
|
// Try remap if necessary. If remapped, log!
|
|
if ok := RemapRequest(request); ok {
|
|
client.LogInfo("Remapped request: %s %s", request.Path().Selector(), request.Query())
|
|
}
|
|
|
|
// If within CGI dir, attempt to execute this!
|
|
if WithinCGIDir(request.Path()) {
|
|
return TryExecuteCGIScript(client, request)
|
|
}
|
|
|
|
// First check for file on disk
|
|
file, err := OpenFile(request.Path())
|
|
if err != nil {
|
|
// Get read-lock, defer unlock
|
|
FileCache.RLock()
|
|
defer FileCache.RUnlock()
|
|
|
|
// Don't throw in the towel yet! Check for generated file in cache
|
|
cached, ok := FileCache.Get(request.Path().Absolute())
|
|
if !ok {
|
|
return err
|
|
}
|
|
|
|
// We got a generated file! Get content
|
|
content := cached.Content().(FileContent)
|
|
|
|
// Send content to client
|
|
return content.WriteToClient(client, request.Path())
|
|
}
|
|
defer file.Close()
|
|
|
|
// Get stat
|
|
stat, err := file.Stat()
|
|
if err != nil {
|
|
return errors.With(err).WrapWithin(ErrFileStat)
|
|
}
|
|
|
|
switch {
|
|
// Directory
|
|
case stat.Mode().IsDir():
|
|
return handleDirectory(client, file, request.Path())
|
|
|
|
// Regular file
|
|
case stat.Mode().IsRegular():
|
|
return FetchFile(client, file, stat, request.Path())
|
|
|
|
// Unsupported type
|
|
default:
|
|
return ErrFileType
|
|
}
|
|
}
|
|
|
|
// FetchFile attempts to fetch a file from the cache, using the supplied file stat, Path and serving client. Returns Error status
|
|
func FetchFile(client *Client, file *os.File, stat os.FileInfo, p *Path) error {
|
|
// If file too big, write direct to client
|
|
if stat.Size() > fileSizeMax {
|
|
return handleLargeFile(client, file, p)
|
|
}
|
|
|
|
// Get cache read lock
|
|
FileCache.RLock()
|
|
|
|
// Define cache here
|
|
var content FileContent
|
|
|
|
// Now check for file in cache
|
|
cached, ok := FileCache.Get(p.Absolute())
|
|
if !ok {
|
|
// Create new file contents with supplied function
|
|
content = newFileContent(p)
|
|
|
|
// Cache the file contents
|
|
err := content.Load(p, file)
|
|
if err != nil {
|
|
// Unlock, return error
|
|
FileCache.RUnlock()
|
|
return err
|
|
}
|
|
|
|
// Wrap contents in file, set fresh
|
|
cached = filecache.NewFile(p.Absolute(), true, content)
|
|
cached.UpdateLastRefresh()
|
|
|
|
// Try upgrade our lock, else error out (have to remember to unlock!!)
|
|
if !FileCache.UpgradeLock() {
|
|
FileCache.Unlock()
|
|
return ErrMutexUpgrade
|
|
}
|
|
|
|
// Put file in cache
|
|
FileCache.Put(cached)
|
|
|
|
// Try downgrade our lock, else error out (have to remember to runlock!!)
|
|
if !FileCache.DowngradeLock() {
|
|
FileCache.RUnlock()
|
|
return ErrMutexDowngrade
|
|
}
|
|
|
|
// Get file read lock
|
|
cached.RLock()
|
|
} else {
|
|
// Get file read lock
|
|
cached.RLock()
|
|
|
|
// Get contents from file
|
|
content = cached.Content().(FileContent)
|
|
|
|
// Check for file freshness
|
|
if !cached.IsFresh() {
|
|
// Try upgrade file lock, else error out (have to remember to unlock!!)
|
|
if !FileCache.UpgradeLock() {
|
|
FileCache.Unlock()
|
|
return ErrMutexUpgrade
|
|
}
|
|
|
|
// Refresh file contents
|
|
err := content.Load(p, file)
|
|
if err != nil {
|
|
// Unlock file, return error
|
|
FileCache.Unlock()
|
|
return err
|
|
}
|
|
|
|
// Set file as fresh
|
|
cached.UpdateLastRefresh()
|
|
|
|
// Try downgrade file lock, else error out (have to remember to runlock!!)
|
|
if !FileCache.DowngradeLock() {
|
|
FileCache.RUnlock()
|
|
return ErrMutexDowngrade
|
|
}
|
|
}
|
|
}
|
|
|
|
// Defer file + cache read unlock
|
|
defer func() {
|
|
cached.RUnlock()
|
|
FileCache.RUnlock()
|
|
}()
|
|
|
|
// Write to client
|
|
return content.WriteToClient(client, p)
|
|
}
|