use net/url's path parser, pass more Request objects round instead of RequestPath

Signed-off-by: kim (grufwub) <grufwub@gmail.com>
master
kim (grufwub) 4 years ago
parent 9a57822766
commit 8a67a88660

@ -19,7 +19,6 @@ type ServerConfig struct {
MaxExecRunTime time.Duration
/* Content settings */
CharSet string
FooterText []byte
PageWidth int

@ -77,38 +77,32 @@ func (l *GophorListener) Accept() (*GophorConn, error) {
return nil, err
}
gophorConn := new(GophorConn)
gophorConn.Conn = &DeadlineConn{ conn }
/* Copy over listener host */
gophorConn.Host = l.Host
gophorConn.Root = l.Root
/* Should always be ok as listener is type TCP (see above) */
addr, _ := conn.RemoteAddr().(*net.TCPAddr)
gophorConn.Client = &ConnClient{
addr.IP.String(),
strconv.Itoa(addr.Port),
}
client := &ConnClient{ addr.IP.String(), strconv.Itoa(addr.Port) }
return gophorConn, nil
return NewGophorConn(NewDeadlineConn(conn), l.Host, client, l.Root), nil
}
type DeadlineConn struct {
/* Simple wrapper to net.Conn that's sets deadlines
/* Simple wrapper to net.Conn that sets deadlines
* on each call to Read() / Write()
*/
conn net.Conn
}
func NewDeadlineConn(conn net.Conn) *DeadlineConn {
return &DeadlineConn{ conn }
}
func (c *DeadlineConn) Read(b []byte) (int, error) {
/* Implements reader + updates deadline */
/* Implements a regular net.Conn + updates deadline */
c.conn.SetReadDeadline(time.Now().Add(Config.SocketReadDeadline))
return c.conn.Read(b)
}
func (c *DeadlineConn) Write(b []byte) (int, error) {
/* Implements writer + updates deadline */
/* Implements a regular net.Conn + updates deadline */
c.conn.SetWriteDeadline(time.Now().Add(Config.SocketWriteDeadline))
return c.conn.Write(b)
}
@ -127,6 +121,15 @@ type GophorConn struct {
Root string
}
func NewGophorConn(conn *DeadlineConn, host *ConnHost, client *ConnClient, root string) *GophorConn {
return &GophorConn{
conn,
host,
client,
root,
}
}
func (c *GophorConn) Read(b []byte) (int, error) {
return c.Conn.Read(b)
}

@ -16,7 +16,7 @@ func setupExecEnviron(path string) []string {
}
/* Setup initial (i.e. constant) CGI environment variables */
func setupInitialCgiEnviron(path string) []string {
func setupInitialCgiEnviron(path, charset string) []string {
return []string{
/* RFC 3875 standard */
envKeyValue("GATEWAY_INTERFACE", "CGI/1.1"), /* MUST be set to the dialect of CGI being used by the server */
@ -28,7 +28,7 @@ func setupInitialCgiEnviron(path string) []string {
/* Non-standard */
envKeyValue("PATH", path),
envKeyValue("COLUMNS", strconv.Itoa(Config.PageWidth)),
envKeyValue("GOPHER_CHARSET", Config.CharSet),
envKeyValue("GOPHER_CHARSET", charset),
}
}
@ -39,20 +39,12 @@ func generateCgiEnvironment(responder *Responder) []string {
env = append(env, envKeyValue("SERVER_NAME", responder.Host.Name())) /* MUST be set to name of server host client is connecting to */
env = append(env, envKeyValue("SERVER_PORT", responder.Host.Port())) /* MUST be set to the server port that client is connecting to */
env = append(env, envKeyValue("REMOTE_ADDR", responder.Client.Ip())) /* Remote client addr, MUST be set */
/* We store the query string in Parameters[0]. Ensure we git without initial delimiter */
var queryString string
if len(responder.Request.Parameters[0]) > 0 {
queryString = responder.Request.Parameters[0][1:]
} else {
queryString = responder.Request.Parameters[0]
}
env = append(env, envKeyValue("QUERY_STRING", queryString)) /* URL encoded search or parameter string, MUST be set even if empty */
env = append(env, envKeyValue("QUERY_STRING", responder.Request.Parameters)) /* URL encoded search or parameter string, MUST be set even if empty */
env = append(env, envKeyValue("SCRIPT_NAME", "/"+responder.Request.Path.Relative())) /* URI path (not URL encoded) which could identify the CGI script (rather than script's output) */
env = append(env, envKeyValue("SCRIPT_FILENAME", responder.Request.Path.Absolute())) /* Basically SCRIPT_NAME absolute path */
env = append(env, envKeyValue("SELECTOR", responder.Request.Path.Selector()))
env = append(env, envKeyValue("DOCUMENT_ROOT", responder.Request.Path.RootDir()))
env = append(env, envKeyValue("REQUEST_URI", "/"+responder.Request.Path.Relative()+responder.Request.Parameters[0]))
env = append(env, envKeyValue("REQUEST_URI", "/"+responder.Request.Path.Relative()+responder.Request.Parameters))
return env
}
@ -62,7 +54,7 @@ var executeCgi func(*Responder) *GophorError
/* Execute CGI script and serve as-is */
func executeCgiNoHttp(responder *Responder) *GophorError {
return execute(responder.Writer, generateCgiEnvironment(responder), responder.Request.Path.Absolute(), nil)
return execute(responder.Writer, generateCgiEnvironment(responder), responder.Request.Path.Absolute())
}
/* Execute CGI script and strip HTTP headers */
@ -71,7 +63,7 @@ func executeCgiStripHttp(responder *Responder) *GophorError {
httpStripWriter := NewHttpStripWriter(responder.Writer)
/* Execute the CGI script using the new httpStripWriter */
gophorErr := execute(httpStripWriter, generateCgiEnvironment(responder), responder.Request.Path.Absolute(), nil)
gophorErr := execute(httpStripWriter, generateCgiEnvironment(responder), responder.Request.Path.Absolute())
/* httpStripWriter's error takes priority as it might have parsed the status code */
cgiStatusErr := httpStripWriter.FinishUp()
@ -84,23 +76,18 @@ func executeCgiStripHttp(responder *Responder) *GophorError {
/* Execute any file (though only allowed are gophermaps) */
func executeFile(responder *Responder) *GophorError {
return execute(responder.Writer, Config.Env, responder.Request.Path.Absolute(), responder.Request.Parameters)
return execute(responder.Writer, Config.Env, responder.Request.Path.Absolute())
}
/* Execute a supplied path with arguments and environment, to writer */
func execute(writer io.Writer, env []string, path string, args []string) *GophorError {
func execute(writer io.Writer, env []string, path string) *GophorError {
/* If CGI disbabled, just return error */
if !Config.CgiEnabled {
return &GophorError{ CgiDisabledErr, nil }
}
/* Setup command */
var cmd *exec.Cmd
if args != nil {
cmd = exec.Command(path, args...)
} else {
cmd = exec.Command(path)
}
cmd := exec.Command(path)
/* Set new proccess group id */
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}

@ -64,7 +64,7 @@ type GophermapContents struct {
* renderable sections of the gophermap.
*/
Path *RequestPath
Request *Request
Sections []GophermapSection
}
@ -85,7 +85,7 @@ func (gc *GophermapContents) Render(responder *Responder) *GophorError {
func (gc *GophermapContents) Load() *GophorError {
/* Load the gophermap into memory as gophermap sections */
var gophorErr *GophorError
gc.Sections, gophorErr = readGophermap(gc.Path)
gc.Sections, gophorErr = readGophermap(gc.Request)
return gophorErr
}
@ -114,22 +114,22 @@ type GophermapDirectorySection struct {
* to hide from the client when rendering.
*/
Path *RequestPath
Request *Request
Hidden map[string]bool
}
func (g *GophermapDirectorySection) Render(responder *Responder) *GophorError {
/* Create new responder from supplied and using stored path */
return listDir(responder.CloneWithRequest(&Request{ g.Path, nil }), g.Hidden)
return listDir(responder.CloneWithRequest(g.Request), g.Hidden)
}
type GophermapFileSection struct {
/* Holds onto a file path to be read and rendered when requested */
Path *RequestPath
Request *Request
}
func (g *GophermapFileSection) Render(responder *Responder) *GophorError {
fileContents, gophorErr := readIntoGophermap(g.Path.Absolute())
fileContents, gophorErr := readIntoGophermap(g.Request.Path.Absolute())
if gophorErr != nil {
return gophorErr
}
@ -138,12 +138,12 @@ func (g *GophermapFileSection) Render(responder *Responder) *GophorError {
type GophermapSubmapSection struct {
/* Holds onto a gophermap path to be read and rendered when requested */
Path *RequestPath
Request *Request
}
func (g *GophermapSubmapSection) Render(responder *Responder) *GophorError {
/* Load the gophermap into memory as gophermap sections */
sections, gophorErr := readGophermap(g.Path)
sections, gophorErr := readGophermap(g.Request)
if gophorErr != nil {
return gophorErr
}
@ -180,21 +180,24 @@ func (g *GophermapExecFileSection) Render(responder *Responder) *GophorError {
}
/* Read and parse a gophermap into separately cacheable and renderable GophermapSection */
func readGophermap(path *RequestPath) ([]GophermapSection, *GophorError) {
func readGophermap(request *Request) ([]GophermapSection, *GophorError) {
/* Create return slice */
sections := make([]GophermapSection, 0)
/* Create hidden files map now in case dir listing requested */
hidden := map[string]bool{
path.Relative(): true, /* Ignore current gophermap */
CgiBinDirStr: true, /* Ignore cgi-bin if found */
request.Path.Relative(): true, /* Ignore current gophermap */
CgiBinDirStr: true, /* Ignore cgi-bin if found */
}
/* Keep track of whether we've already come across a title line (only 1 allowed!) */
titleAlready := false
/* Error setting within nested function below */
var returnErr *GophorError
/* Perform buffered scan with our supplied splitter and iterators */
gophorErr := bufferedScan(path.Absolute(),
gophorErr := bufferedScan(request.Path.Absolute(),
func(scanner *bufio.Scanner) bool {
line := scanner.Text()
@ -218,39 +221,40 @@ func readGophermap(path *RequestPath) ([]GophermapSection, *GophorError) {
case TypeHiddenFile:
/* Add to hidden files map */
hidden[path.JoinRel(line[1:])] = true
hidden[request.Path.JoinRel(line[1:])] = true
case TypeSubGophermap:
/* Parse new RequestPath and parameters */
subPath, parameters := parseLineRequestString(path, line[1:])
if subPath.Relative() == "" || subPath.Relative() == path.Relative() {
subRequest, gophorErr := parseLineRequestString(request.Path, line[1:])
if gophorErr != nil || subRequest.Path.Relative() == "" || subRequest.Path.Relative() == request.Path.Relative() {
/* Either path parsing failed, or we've been supplied same gophermap, and recursion is
* recursion is recursion is bad kids!
* recursion is recursion is bad kids! Set return error and request finish.
*/
break
returnErr = gophorErr
return true
}
/* Perform file stat */
stat, err := os.Stat(subPath.Absolute())
stat, err := os.Stat(subRequest.Path.Absolute())
if (err != nil) || (stat.Mode() & os.ModeDir != 0) {
/* File read error or is directory */
break
}
/* Check if we've been supplied subgophermap or regular file */
if isGophermap(subPath) {
if isGophermap(subRequest.Path.Relative()) {
/* If executable, store as GophermapExecFileSection, else GophermapSubmapSection */
if stat.Mode().Perm() & 0100 != 0 {
sections = append(sections, &GophermapExecFileSection { &Request{ subPath, parameters } })
sections = append(sections, &GophermapExecFileSection { subRequest })
} else {
sections = append(sections, &GophermapSubmapSection{ subPath })
sections = append(sections, &GophermapSubmapSection{ subRequest })
}
} else {
/* If stored in cgi-bin store as GophermapExecCgiSection, else GophermapFileSection */
if subPath.HasRelPrefix(CgiBinDirStr) {
sections = append(sections, &GophermapExecCgiSection{ &Request{ subPath, parameters} })
if withinCgiBin(subRequest.Path.Relative()) {
sections = append(sections, &GophermapExecCgiSection{ subRequest })
} else {
sections = append(sections, &GophermapFileSection{ subPath })
sections = append(sections, &GophermapFileSection{ subRequest })
}
}
@ -262,7 +266,7 @@ func readGophermap(path *RequestPath) ([]GophermapSection, *GophorError) {
case TypeEndBeginList:
/* Append GophermapDirectorySection object then break, as with TypeEnd. */
dirRequest := NewRequestPath(path.RootDir(), path.TrimRelSuffix(GophermapFileStr))
dirRequest := &Request{ NewRequestPath(request.Path.RootDir(), request.Path.TrimRelSuffix(GophermapFileStr)), "" }
sections = append(sections, &GophermapDirectorySection{ dirRequest, hidden })
return false
@ -278,6 +282,8 @@ func readGophermap(path *RequestPath) ([]GophermapSection, *GophorError) {
/* Check the bufferedScan didn't exit with error */
if gophorErr != nil {
return nil, gophorErr
} else if returnErr != nil {
return nil, returnErr
}
return sections, nil

@ -103,7 +103,7 @@ func (fs *FileSystem) HandleRequest(responder *Responder) *GophorError {
/* Directory */
case stat.Mode() & os.ModeDir != 0:
/* Ignore anything under cgi-bin directory */
if withinCgiBin(responder.Request.Path) {
if withinCgiBin(responder.Request.Path.Relative()) {
return &GophorError{ IllegalPathErr, nil }
}
@ -215,8 +215,8 @@ func (fs *FileSystem) FetchFile(responder *Responder) *GophorError {
/* Create new file contents */
var contents FileContents
if isGophermap(responder.Request.Path) {
contents = &GophermapContents{ responder.Request.Path, nil }
if isGophermap(responder.Request.Path.Relative()) {
contents = &GophermapContents{ responder.Request, nil }
} else {
contents = &RegularFileContents{ responder.Request.Path, nil }
}

@ -0,0 +1,6 @@
package main
type GopherUrl struct {
Path string
Parameters string
}

@ -81,6 +81,7 @@ func setupServer() []*GophorListener {
/* Content settings */
pageWidth := flag.Int("page-width", 80, "Change page width used when formatting output.")
// charSet := flag.String("charset", "", "Change default output charset.")
charSet := "utf-8"
footerText := flag.String("footer", " Gophor, a Gopher server in Go.", "Change gophermap footer text (Unix new-line separated lines).")
footerSeparator := flag.Bool("no-footer-separator", false, "Disable footer line separator.")
@ -165,7 +166,7 @@ func setupServer() []*GophorListener {
/* Set safe executable path and setup environments */
Config.SysLog.Info("", "Setting safe executable path: %s\n", *safeExecPath)
Config.Env = setupExecEnviron(*safeExecPath)
Config.CgiEnv = setupInitialCgiEnviron(*safeExecPath)
Config.CgiEnv = setupInitialCgiEnviron(*safeExecPath, charSet)
/* Set executable watchdog */
Config.SysLog.Info("", "Max executable time: %s\n", *maxExecRunTime)

@ -2,21 +2,38 @@ package main
import (
"strings"
"net/url"
)
/* Parse a request string into a path and parameters string */
func parseRequestString(request string) (string, []string) {
/* Read up to first '?' and then put rest into single slice string array */
i := 0
for i < len(request) {
if request[i] == '?' {
break
func parseGopherUrl(request string) (*GopherUrl, *GophorError) {
if strings.Contains(request, "#") || // we don't support fragments
strings.HasPrefix(request, "GET ") { // we don't support HTTP requests
return nil, &GophorError{ InvalidRequestErr, nil }
}
/* Check if string contains any ASCII control byte */
for i := 0; i < len(request); i += 1 {
if request[i] < ' ' || request[i] == 0x7f {
return nil, &GophorError{ InvalidRequestErr, nil }
}
i += 1
}
/* Use strings.TrimPrefix() as it returns empty string for zero length string */
return request[:i], []string{ request[i:] }
/* Split into 2 substrings by '?'. Url path and query */
split := strings.SplitN(request, "?", 2)
/* Unescape path */
path, err := url.PathUnescape(split[0])
if err != nil {
return nil, &GophorError{ InvalidRequestErr, nil }
}
/* Return GopherUrl based on this split request */
if len(split) == 1 {
return &GopherUrl{ path, "" }, nil
} else {
return &GopherUrl{ path, split[1] }, nil
}
}
/* Parse line type from contents */
@ -62,27 +79,35 @@ func parseLineType(line string) ItemType {
return ItemType(line[0])
}
/* Parses a line in a gophermap into a filesystem request path and a string slice of arguments */
func parseLineRequestString(requestPath *RequestPath, lineStr string) (*RequestPath, []string) {
/* Parses a line in a gophermap into a new request object */
func parseLineRequestString(requestPath *RequestPath, lineStr string) (*Request, *GophorError) {
if strings.HasPrefix(lineStr, "/") {
/* Assume is absolute (well, seeing server root as '/') */
if strings.HasPrefix(lineStr[1:], CgiBinDirStr) {
if withinCgiBin(lineStr[1:]) {
/* CGI script, parse request path and parameters */
relPath, parameters := parseRequestString(lineStr)
return NewRequestPath(requestPath.RootDir(), relPath), parameters
url, gophorErr := parseGopherUrl(lineStr[1:])
if gophorErr != nil {
return nil, gophorErr
} else {
return &Request{ NewRequestPath(requestPath.RootDir(), url.Path), url.Parameters }, nil
}
} else {
/* Regular file, no more parsing */
return NewRequestPath(requestPath.RootDir(), lineStr), []string{}
return &Request{ NewRequestPath(requestPath.RootDir(), lineStr[1:]), "" }, nil
}
} else {
/* Assume relative to current directory */
if strings.HasPrefix(lineStr, CgiBinDirStr) && requestPath.Relative() == "" {
if withinCgiBin(lineStr) && requestPath.Relative() == "" {
/* If begins with cgi-bin and is at root dir, parse as cgi-bin */
relPath, parameters := parseRequestString(lineStr)
return NewRequestPath(requestPath.RootDir(), relPath), parameters
url, gophorErr := parseGopherUrl(lineStr)
if gophorErr != nil {
return nil, gophorErr
} else {
return &Request{ NewRequestPath(requestPath.RootDir(), url.Path), url.Parameters }, nil
}
} else {
/* Regular file, no more parsing */
return NewRequestPath(requestPath.RootDir(), requestPath.JoinCurDir(lineStr)), []string{}
return &Request{ NewRequestPath(requestPath.RootDir(), requestPath.JoinCurDir(lineStr)), "" }, nil
}
}
}

@ -84,11 +84,11 @@ func compileUserRemapRegex(remaps string) []*FileRemap {
}
/* Check if file path is gophermap */
func isGophermap(path *RequestPath) bool {
return Config.RgxGophermap.MatchString(path.Relative())
func isGophermap(path string) bool {
return Config.RgxGophermap.MatchString(path)
}
/* Check if file path within cgi-bin */
func withinCgiBin(path *RequestPath) bool {
return Config.RgxCgiBin.MatchString(path.Relative())
func withinCgiBin(path string) bool {
return Config.RgxCgiBin.MatchString(path)
}

@ -22,7 +22,7 @@ func NewRequestPath(rootDir, relPath string) *RequestPath {
}
func (rp *RequestPath) RemapPath(newPath string) *RequestPath {
requestPath := NewRequestPath(rp.RootDir(), sanitizeRelativePath(rp.RootDir(), newPath))
requestPath := NewRequestPath(rp.RootDir(), sanitizeRawPath(rp.RootDir(), newPath))
requestPath.Select = rp.Relative()
return requestPath
}
@ -98,11 +98,21 @@ type Request struct {
*/
Path *RequestPath
Parameters []string
Parameters string
}
func NewSanitizedRequest(conn *GophorConn, url *GopherUrl) *Request {
return &Request{
NewRequestPath(
conn.RootDir(),
sanitizeRawPath(conn.RootDir(), url.Path),
),
url.Parameters,
}
}
/* Sanitize a request path string */
func sanitizeRelativePath(rootDir, relPath string) string {
func sanitizeRawPath(rootDir, relPath string) string {
/* Start with a clean :) */
relPath = path.Clean(relPath)

@ -12,12 +12,6 @@ type Responder struct {
Request *Request
}
func NewSanitizedRequest(conn *GophorConn, requestStr string) *Request {
relPath, paramaters := parseRequestString(requestStr)
relPath = sanitizeRelativePath(conn.RootDir(), relPath)
return &Request{ NewRequestPath(conn.RootDir(), relPath), paramaters }
}
func NewResponder(conn *GophorConn, request *Request) *Responder {
bufWriter := bufio.NewWriterSize(conn.Conn, Config.SocketWriteBufSize)
return &Responder{ conn.Host, conn.Client, bufWriter, request }

@ -80,33 +80,36 @@ func (worker *Worker) Serve() {
/* Do nothing */
}
/* Create new request from received */
request := NewSanitizedRequest(worker.Conn, received)
/* Create new responder from request */
responder := NewResponder(worker.Conn, request)
/* Handle request with supplied responder */
gophorErr := Config.FileSystem.HandleRequest(responder)
/* Handle any error */
if gophorErr != nil {
/* Log serve failure to error to system */
Config.SysLog.Error("", gophorErr.Error())
/* Create GopherUrl object from request string */
url, gophorErr := parseGopherUrl(received)
if gophorErr == nil {
/* Create new request from url object */
request := NewSanitizedRequest(worker.Conn, url)
/* Create new responder from request */
responder := NewResponder(worker.Conn, request)
/* Handle request with supplied responder */
gophorErr = Config.FileSystem.HandleRequest(responder)
if gophorErr == nil {
/* Log success to access and return! */
responder.AccessLogInfo("Served: %s\n", request.Path.Absolute())
return
} else {
/* Log failure to access */
responder.AccessLogError("Failed to serve: %s\n", request.Path.Absolute())
}
}
/* Generate response bytes from error code */
errResponse := generateGopherErrorResponseFromCode(gophorErr.Code)
/* Log serve failure to error to system */
Config.SysLog.Error("", gophorErr.Error())
/* If we got response bytes to send? SEND 'EM! */
if errResponse != nil {
/* No gods. No masters. We don't care about error checking here */
responder.WriteFlush(errResponse)
}
/* Generate response bytes from error code */
errResponse := generateGopherErrorResponseFromCode(gophorErr.Code)
/* Log failure to access */
responder.AccessLogError("Failed to serve: %s\n", request.Path.Absolute())
} else {
/* Log served to access */
responder.AccessLogInfo("Served: %s\n", request.Path.Absolute())
/* If we got response bytes to send? SEND 'EM! */
if errResponse != nil {
/* No gods. No masters. We don't care about error checking here */
worker.Conn.Write(errResponse)
}
}

Loading…
Cancel
Save