add support for CGI HTTP status checking, also disabling CGI HTTP compat

Signed-off-by: kim (grufwub) <grufwub@gmail.com>
master
kim (grufwub) 4 years ago
parent 38e9bd4a6d
commit 340f746930

@ -34,6 +34,18 @@ const (
CgiDisabledErr ErrorCode = iota
RestrictedCommandErr ErrorCode = iota
/* Wrapping CGI http status codes */
CgiStatus400Err ErrorCode = iota
CgiStatus401Err ErrorCode = iota
CgiStatus403Err ErrorCode = iota
CgiStatus404Err ErrorCode = iota
CgiStatus408Err ErrorCode = iota
CgiStatus410Err ErrorCode = iota
CgiStatus500Err ErrorCode = iota
CgiStatus501Err ErrorCode = iota
CgiStatus503Err ErrorCode = iota
CgiStatusUnknownErr ErrorCode = iota
/* Error Response Codes */
ErrorResponse200 ErrorResponseCode = iota
ErrorResponse400 ErrorResponseCode = iota
@ -96,6 +108,27 @@ func (e *GophorError) Error() string {
case RestrictedCommandErr:
str = "command use restricted"
case CgiStatus400Err:
str = "CGI script error status 400"
case CgiStatus401Err:
str = "CGI script error status 401"
case CgiStatus403Err:
str = "CGI script error status 403"
case CgiStatus404Err:
str = "CGI script error status 404"
case CgiStatus408Err:
str = "CGI script error status 408"
case CgiStatus410Err:
str = "CGI script error status 410"
case CgiStatus500Err:
str = "CGI script error status 500"
case CgiStatus501Err:
str = "CGI script error status 501"
case CgiStatus503Err:
str = "CGI script error status 503"
case CgiStatusUnknownErr:
str = "CGI script error unknown status code"
default:
str = "Unknown"
}
@ -150,6 +183,27 @@ func gophorErrorToResponseCode(code ErrorCode) ErrorResponseCode {
case RestrictedCommandErr:
return ErrorResponse500
case CgiStatus400Err:
return ErrorResponse400
case CgiStatus401Err:
return ErrorResponse401
case CgiStatus403Err:
return ErrorResponse403
case CgiStatus404Err:
return ErrorResponse404
case CgiStatus408Err:
return ErrorResponse408
case CgiStatus410Err:
return ErrorResponse410
case CgiStatus500Err:
return ErrorResponse500
case CgiStatus501Err:
return ErrorResponse501
case CgiStatus503Err:
return ErrorResponse503
case CgiStatusUnknownErr:
return ErrorResponse500
default:
return ErrorResponse503
}

@ -32,13 +32,13 @@ func setupInitialCgiEnviron(path string) []string {
}
}
/* Execute a CGI script */
func executeCgi(responder *Responder) *GophorError {
/* Generate CGI environment */
func generateCgiEnvironment(responder *Responder) []string {
/* Get initial CgiEnv variables */
cgiEnv := Config.CgiEnv
cgiEnv = append(cgiEnv, envKeyValue("SERVER_NAME", responder.Host.Name())) /* MUST be set to name of server host client is connecting to */
cgiEnv = append(cgiEnv, envKeyValue("SERVER_PORT", responder.Host.Port())) /* MUST be set to the server port that client is connecting to */
cgiEnv = append(cgiEnv, envKeyValue("REMOTE_ADDR", responder.Client.Ip())) /* Remote client addr, MUST be set */
env := Config.CgiEnv
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
@ -47,43 +47,38 @@ func executeCgi(responder *Responder) *GophorError {
} else {
queryString = responder.Request.Parameters[0]
}
cgiEnv = append(cgiEnv, envKeyValue("QUERY_STRING", queryString)) /* URL encoded search or parameter string, MUST be set even if empty */
cgiEnv = append(cgiEnv, envKeyValue("SCRIPT_NAME", "/"+responder.Request.Path.Relative())) /* URI path (not URL encoded) which could identify the CGI script (rather than script's output) */
cgiEnv = append(cgiEnv, envKeyValue("SCRIPT_FILENAME", responder.Request.Path.Absolute())) /* Basically SCRIPT_NAME absolute path */
cgiEnv = append(cgiEnv, envKeyValue("SELECTOR", responder.Request.Path.Selector()))
cgiEnv = append(cgiEnv, envKeyValue("DOCUMENT_ROOT", responder.Request.Path.RootDir()))
cgiEnv = append(cgiEnv, envKeyValue("REQUEST_URI", "/"+responder.Request.Path.Relative()+responder.Request.Parameters[0]))
/* Fuck it. For now, we don't support PATH_INFO. It's a piece of shit variable */
// cgiEnv = append(cgiEnv, envKeyValue("PATH_INFO", responder.Parameters[0])) /* Sub-resource to be fetched by script, derived from path hierarch portion of URI. NOT URL encoded */
// cgiEnv = append(cgiEnv, envKeyValue("PATH_TRANSLATED", responder.AbsPath())) /* Take PATH_INFO, parse as local URI and append root dir */
/* We ignore these due to just CBA and we're not implementing authorization yet */
// cgiEnv = append(cgiEnv, envKeyValue("AUTH_TYPE", "")) /* Any method used my server to authenticate user, MUST be set if auth'd */
// cgiEnv = append(cgiEnv, envKeyValue("CONTENT_TYPE", "")) /* Only a MUST if HTTP content-type set (so never for gopher) */
// cgiEnv = append(cgiEnv, envKeyValue("REMOTE_IDENT", "")) /* Remote client identity information */
// cgiEnv = append(cgiEnv, envKeyValue("REMOTE_HOST", "")) /* Remote client domain name */
// cgiEnv = append(cgiEnv, envKeyValue("REMOTE_USER", "")) /* Remote user ID, if AUTH_TYPE, MUST be set */
/* Create nwe SkipPrefixwriter from underlying response writer set to skip up to:
* \r\n\r\n
* Then checks if it contains a valid 'content-type:' header, if so it strips these.
*/
env = append(env, envKeyValue("QUERY_STRING", queryString)) /* 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]))
return env
}
/* Execute a CGI script (pointer to correct function) */
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)
}
/* Execute CGI script and strip HTTP headers */
func executeCgiStripHttp(responder *Responder) *GophorError {
/* HTTP header stripping writer that also parses HTTP status codes */
httpStripWriter := NewHttpStripWriter(responder.Writer)
/* Execute the CGI script using the new SkipBufferedWriter and above environment */
gophorErr := execute(httpStripWriter, cgiEnv, responder.Request.Path.Absolute(), nil)
if gophorErr != nil {
/* Error, return :( */
return gophorErr
/* Execute the CGI script using the new httpStripWriter */
gophorErr := execute(httpStripWriter, generateCgiEnvironment(responder), responder.Request.Path.Absolute(), nil)
/* httpStripWriter's error takes priority as it might have parsed the status code */
cgiStatusErr := httpStripWriter.FinishUp()
if cgiStatusErr != nil {
return cgiStatusErr
} else {
/* Returned execute() fine, perform skip buffer flush. */
err := httpStripWriter.FlushSkipBuffer()
if err != nil {
return &GophorError{ BufferedWriteFlushErr, err }
} else {
return nil
}
return gophorErr
}
}
@ -157,9 +152,13 @@ func execute(writer io.Writer, env []string, path string, args []string) *Gophor
exitCode := 0
if err != nil {
/* Error, try to get exit code */
exitError, _ := err.(*exec.ExitError)
waitStatus := exitError.Sys().(syscall.WaitStatus)
exitCode = waitStatus.ExitStatus()
exitError, ok := err.(*exec.ExitError)
if ok {
waitStatus := exitError.Sys().(syscall.WaitStatus)
exitCode = waitStatus.ExitStatus()
} else {
exitCode = 1
}
} else {
/* No error! Get exit code direct from command */
waitStatus := cmd.ProcessState.Sys().(syscall.WaitStatus)

@ -97,6 +97,7 @@ func setupServer() []*GophorListener {
/* Exec settings */
disableCgi := flag.Bool("disable-cgi", false, "Disable CGI and all executable support.")
httpCompatCgi := flag.Bool("http-compat-cgi", false, "Enable using HTTP CGI scripts (will strip headers).")
safeExecPath := flag.String("safe-path", "/usr/bin:/bin", "Set safe PATH variable to be used when executing CGI scripts, gophermaps and inline shell commands.")
maxExecRunTime := flag.Duration("max-exec-time", time.Second*3, "Change max executable CGI, gophermap and inline shell command runtime.")
@ -136,13 +137,20 @@ func setupServer() []*GophorListener {
/* Set CGI support status */
if *disableCgi {
Config.SysLog.Info("", "CGI support disabled")
Config.SysLog.Info("", "CGI support disabled\n")
Config.CgiEnabled = false
} else {
/* Enable CGI */
Config.SysLog.Info("", "CGI support enabled")
Config.SysLog.Info("", "CGI support enabled\n")
Config.CgiEnabled = true
if *httpCompatCgi {
Config.SysLog.Info("", "Enabling HTTP CGI script compatibility\n")
executeCgi = executeCgiStripHttp
} else {
executeCgi = executeCgiNoHttp
}
/* Set safe executable path and setup environments */
Config.SysLog.Info("", "Setting safe executable path: %s\n", *safeExecPath)
Config.Env = setupExecEnviron(*safeExecPath)

@ -0,0 +1,212 @@
package main
import (
"io"
"bufio"
"bytes"
)
type HttpStripWriter struct {
/* Wrapper to bufio.Writer that reads a predetermined amount into a buffer
* then parses the buffer for valid HTTP headers and status code, deciding
* whether to strip these headers or returning with an HTTP status code.
*/
/* We set underlying write function with a variable, so that each call
* to .Write() doesn't have to perform a check every time whether we need
* to keep checking for headers to skip.
*/
WriteFunc func([]byte) (int, error)
Writer *bufio.Writer
SkipBuffer []byte
SkipIndex int
Err *GophorError
}
func NewHttpStripWriter(writer *bufio.Writer) *HttpStripWriter {
w := &HttpStripWriter{}
w.Writer = writer
w.WriteFunc = w.WriteCheckForHeaders
w.SkipBuffer = make([]byte, Config.SkipPrefixBufSize)
w.SkipIndex = 0
return w
}
func (w *HttpStripWriter) Size() int {
/* Size of the skip buffer */
return len(w.SkipBuffer)
}
func (w *HttpStripWriter) Available() int {
/* How much space have we got left in the skip buffer */
return w.Size() - w.SkipIndex
}
func (w *HttpStripWriter) AddToSkipBuffer(data []byte) int {
/* Figure out how much data we need to add */
toAdd := w.Available()
if len(data) < toAdd {
toAdd = len(data)
}
/* Add the data to the skip buffer! */
copy(w.SkipBuffer[w.SkipIndex:], data[:toAdd])
w.SkipIndex += toAdd
return toAdd
}
func (w *HttpStripWriter) ParseHttpHeaderSection() (bool, bool) {
/* Check if this is a valid HTTP header section and determine from status if we should continue */
validHeaderSection, shouldContinue := false, true
for _, header := range bytes.Split(w.SkipBuffer, []byte(DOSLineEnd)) {
header = bytes.ToLower(header)
if bytes.Contains(header, []byte("content-type: ")) {
/* This whole header section is now _valid_ */
validHeaderSection = true
} else if bytes.Contains(header, []byte("status: ")) {
/* Try parse status code */
statusStr := string(bytes.Split(bytes.TrimPrefix(header, []byte("status: ")), []byte(" "))[0])
if statusStr == "200" {
/* We ignore this */
continue
}
/* Any other values indicate error, we should not continue writing */
shouldContinue = false
/* Try parse error code */
errorCode := CgiStatusUnknownErr
switch statusStr {
case "400":
errorCode = CgiStatus400Err
case "401":
errorCode = CgiStatus401Err
case "403":
errorCode = CgiStatus403Err
case "404":
errorCode = CgiStatus404Err
case "408":
errorCode = CgiStatus408Err
case "410":
errorCode = CgiStatus410Err
case "500":
errorCode = CgiStatus500Err
case "501":
errorCode = CgiStatus501Err
case "503":
errorCode = CgiStatus503Err
}
/* Set struct error */
w.Err = &GophorError{ errorCode, nil }
}
}
return validHeaderSection, shouldContinue
}
func (w *HttpStripWriter) WriteSkipBuffer() (bool, error) {
defer func() {
w.SkipIndex = 0
}()
/* First try parse the headers, determine what to do next */
validHeaders, shouldContinue := w.ParseHttpHeaderSection()
if validHeaders {
/* Valid headers, we don't bother writing. Return whether
* shouldContinue whatever value that may be.
*/
return shouldContinue, nil
}
/* Default is to write skip buffer contents. shouldContinue only
* means something as long as we have valid headers.
*/
_, err := w.Writer.Write(w.SkipBuffer[:w.SkipIndex])
return true, err
}
func (w *HttpStripWriter) FinishUp() *GophorError {
/* If SkipBuffer still has contents, in case of data written being less
* than w.Size() --> check this data for HTTP headers to strip, parse
* any status codes and write this content with underlying writer if
* necessary.
*/
if w.SkipIndex > 0 {
w.WriteSkipBuffer()
}
/* Return HttpStripWriter error code if set */
return w.Err
}
func (w *HttpStripWriter) Write(data []byte) (int, error) {
/* Write using whatever write function is currently set */
return w.WriteFunc(data)
}
func (w *HttpStripWriter) WriteRegular(data []byte) (int, error) {
/* Regular write function */
return w.Writer.Write(data)
}
func (w *HttpStripWriter) WriteCheckForHeaders(data []byte) (int, error) {
split := bytes.Split(data, []byte(DOSLineEnd+DOSLineEnd))
if len(split) == 1 {
/* Try add these to skip buffer */
added := w.AddToSkipBuffer(data)
if added < len(data) {
defer func() {
/* Having written skipbuffer after this if clause, set write to regular */
w.WriteFunc = w.WriteRegular
}()
doContinue, err := w.WriteSkipBuffer()
if !doContinue {
return len(data), io.EOF
} else if err != nil {
return added, err
}
/* Write remaining data not added to skip buffer */
count, err := w.Writer.Write(data[added:])
if err != nil {
return added+count, err
}
}
return len(data), nil
} else {
defer func() {
/* No use for skip buffer after this clause, set write to regular */
w.WriteFunc = w.WriteRegular
w.SkipIndex = 0
}()
/* Try add what we can to skip buffer */
added := w.AddToSkipBuffer(append(split[0], []byte(DOSLineEnd+DOSLineEnd)...))
/* Write skip buffer data if necessary, check if we should continue */
doContinue, err := w.WriteSkipBuffer()
if !doContinue {
return len(data), io.EOF
} else if err != nil {
return added, err
}
/* Write remaining data not added to skip buffer */
count, err := w.Writer.Write(data[added:])
if err != nil {
return added+count, err
}
return len(data), nil
}
}

@ -3,7 +3,6 @@ package main
import (
"io"
"bufio"
"bytes"
)
type Responder struct {
@ -85,203 +84,3 @@ func (r *Responder) CloneWithRequest(request *Request) *Responder {
request,
}
}
type HttpStripWriter struct {
/* Wrapper to bufio writer that allows read up to
* some predefined prefix into a buffer, then continuing
* write to expected writer destination either after prefix
* reached, or skip buffer filled (whichever comes first).
*/
Writer *bufio.Writer
/* This allows us to specify the write function so that after
* having performed the skip we can modify the write function used
* and not have to use an if-case EVERY SINGLE TIME.
*/
WriteFunc func([]byte) (int, error)
SkipBuffer []byte
SkipIndex int
}
func NewHttpStripWriter(writer *bufio.Writer) *HttpStripWriter {
w := &HttpStripWriter{}
w.Writer = writer
w.WriteFunc = w.WriteCheckForHeaders
w.SkipBuffer = make([]byte, Config.SkipPrefixBufSize)
w.SkipIndex = 0
return w
}
func (w *HttpStripWriter) Size() int {
return len(w.SkipBuffer)
}
func (w *HttpStripWriter) Available() int {
return w.Size() - w.SkipIndex
}
func (w *HttpStripWriter) AddToSkipBuffer(data []byte) int {
toAdd := w.Available()
if len(data) < toAdd {
toAdd = len(data)
}
copy(w.SkipBuffer[w.SkipIndex:], data[:toAdd])
w.SkipIndex += toAdd
return toAdd
}
func (w *HttpStripWriter) ParseHttpHeaderSection() (bool, ErrorResponseCode) {
/* Check if this is a valid HTTP header section and check status code */
validHeaderSection := false
statusCode := ErrorResponse200
for _, header := range bytes.Split(w.SkipBuffer, []byte(DOSLineEnd)) {
header = bytes.ToLower(header)
if bytes.Contains(header, []byte("content-type: ")) {
/* This whole header section is now _valid_ */
validHeaderSection = true
} else if bytes.Contains(header, []byte("status: ")) {
/* Try parse status code */
statusStr := bytes.Split(bytes.TrimPrefix(header, []byte("status: ")), []byte(" "))[0]
switch string(statusStr) {
case "200":
statusCode = ErrorResponse200
case "400":
statusCode = ErrorResponse400
case "401":
statusCode = ErrorResponse401
case "403":
statusCode = ErrorResponse403
case "404":
statusCode = ErrorResponse404
case "408":
statusCode = ErrorResponse408
case "410":
statusCode = ErrorResponse410
case "500":
statusCode = ErrorResponse500
case "501":
statusCode = ErrorResponse501
case "503":
statusCode = ErrorResponse503
default:
statusCode = ErrorResponse500
}
}
}
return validHeaderSection, statusCode
}
func (w *HttpStripWriter) WriteSkipBuffer() (bool, error) {
defer func() {
w.SkipIndex = 0
}()
/* First try parse the headers, determine what to do next */
validHeaderSection, statusCode := w.ParseHttpHeaderSection()
if validHeaderSection {
/* Contains valid HTTP headers, if anything other than 200 statusCode we write error and return nil */
if statusCode != ErrorResponse200 {
/* Non-200 status code. Try send error response bytes and return with false (don't continue) */
_, err := w.Writer.Write(generateGopherErrorResponse(statusCode))
return false, err
} else {
/* Status code all good, we just return a true (do continue) */
return true, nil
}
}
/* Default is just write skip buffer contents */
_, err := w.Writer.Write(w.SkipBuffer[:w.SkipIndex])
return true, err
}
func (w *HttpStripWriter) FlushSkipBuffer() error {
/* If SkipBuffer non-nil and has contents, make sure this is written!
* This happens if caller to Write has supplied content length < w.Size().
* This MUST be called.
*/
if w.SkipIndex > 0 {
_, err := w.WriteSkipBuffer()
return err
}
return nil
}
func (w *HttpStripWriter) Write(data []byte) (int, error) {
/* Write using whatever write function is currently set */
return w.WriteFunc(data)
}
func (w *HttpStripWriter) WriteRegular(data []byte) (int, error) {
/* Regular write function */
return w.Writer.Write(data)
}
func (w *HttpStripWriter) WriteCheckForHeaders(data []byte) (int, error) {
split := bytes.Split(data, []byte(DOSLineEnd+DOSLineEnd))
if len(split) == 1 {
/* Try add these to skip buffer */
added := w.AddToSkipBuffer(data)
if added < len(data) {
defer func() {
/* Having written skipbuffer after this if clause, set write to regular */
w.WriteFunc = w.WriteRegular
}()
doContinue, err := w.WriteSkipBuffer()
if !doContinue {
/* If we shouldn't continue, return 'added' and unexpect EOF error */
return added, io.ErrUnexpectedEOF
} else if err != nil {
/* If err not nil, return that we wrote up to 'added' and err */
return added, err
}
/* Write remaining data not added to skip buffer */
count, err := w.Writer.Write(data[added:])
if err != nil {
/* We return added+count */
return added+count, err
}
}
return len(data), nil
} else {
defer func() {
/* No use for skip buffer after this clause, set write to regular */
w.WriteFunc = w.WriteRegular
w.SkipIndex = 0
}()
/* Try add what we can to skip buffer */
added := w.AddToSkipBuffer(append(split[0], []byte(DOSLineEnd+DOSLineEnd)...))
doContinue, err := w.WriteSkipBuffer()
if !doContinue {
/* If we shouldn't continue, return 'added' and unexpect EOF error */
return added, io.ErrUnexpectedEOF
} else if err != nil {
/* If err not nil, return that we wrote up to 'added' and err */
return added, err
}
/* Write remaining data not added to skip buffer */
count, err := w.Writer.Write(data[added:])
if err != nil {
/* We return added+count */
return added+count, err
}
return len(data), nil
}
}

Loading…
Cancel
Save