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.
gophi/core/server.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)
}