package fcgi
import (
"context"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/http/cgi"
"os"
"strings"
"time"
)
type request struct {
pw *io .PipeWriter
reqId uint16
params map [string ]string
buf [1024 ]byte
rawParams []byte
keepConn bool
}
type envVarsContextKey struct {}
func newRequest(reqId uint16 , flags uint8 ) *request {
r := &request {
reqId : reqId ,
params : map [string ]string {},
keepConn : flags &flagKeepConn != 0 ,
}
r .rawParams = r .buf [:0 ]
return r
}
func (r *request ) parseParams () {
text := r .rawParams
r .rawParams = nil
for len (text ) > 0 {
keyLen , n := readSize (text )
if n == 0 {
return
}
text = text [n :]
valLen , n := readSize (text )
if n == 0 {
return
}
text = text [n :]
if int (keyLen )+int (valLen ) > len (text ) {
return
}
key := readString (text , keyLen )
text = text [keyLen :]
val := readString (text , valLen )
text = text [valLen :]
r .params [key ] = val
}
}
type response struct {
req *request
header http .Header
code int
wroteHeader bool
wroteCGIHeader bool
w *bufWriter
}
func newResponse(c *child , req *request ) *response {
return &response {
req : req ,
header : http .Header {},
w : newWriter (c .conn , typeStdout , req .reqId ),
}
}
func (r *response ) Header () http .Header {
return r .header
}
func (r *response ) Write (p []byte ) (n int , err error ) {
if !r .wroteHeader {
r .WriteHeader (http .StatusOK )
}
if !r .wroteCGIHeader {
r .writeCGIHeader (p )
}
return r .w .Write (p )
}
func (r *response ) WriteHeader (code int ) {
if r .wroteHeader {
return
}
r .wroteHeader = true
r .code = code
if code == http .StatusNotModified {
r .header .Del ("Content-Type" )
r .header .Del ("Content-Length" )
r .header .Del ("Transfer-Encoding" )
}
if r .header .Get ("Date" ) == "" {
r .header .Set ("Date" , time .Now ().UTC ().Format (http .TimeFormat ))
}
}
func (r *response ) writeCGIHeader (p []byte ) {
if r .wroteCGIHeader {
return
}
r .wroteCGIHeader = true
fmt .Fprintf (r .w , "Status: %d %s\r\n" , r .code , http .StatusText (r .code ))
if _ , hasType := r .header ["Content-Type" ]; r .code != http .StatusNotModified && !hasType {
r .header .Set ("Content-Type" , http .DetectContentType (p ))
}
r .header .Write (r .w )
r .w .WriteString ("\r\n" )
r .w .Flush ()
}
func (r *response ) Flush () {
if !r .wroteHeader {
r .WriteHeader (http .StatusOK )
}
r .w .Flush ()
}
func (r *response ) Close () error {
r .Flush ()
return r .w .Close ()
}
type child struct {
conn *conn
handler http .Handler
requests map [uint16 ]*request
}
func newChild(rwc io .ReadWriteCloser , handler http .Handler ) *child {
return &child {
conn : newConn (rwc ),
handler : handler ,
requests : make (map [uint16 ]*request ),
}
}
func (c *child ) serve () {
defer c .conn .Close ()
defer c .cleanUp ()
var rec record
for {
if err := rec .read (c .conn .rwc ); err != nil {
return
}
if err := c .handleRecord (&rec ); err != nil {
return
}
}
}
var errCloseConn = errors .New ("fcgi: connection should be closed" )
var emptyBody = io .NopCloser (strings .NewReader ("" ))
var ErrRequestAborted = errors .New ("fcgi: request aborted by web server" )
var ErrConnClosed = errors .New ("fcgi: connection to web server closed" )
func (c *child ) handleRecord (rec *record ) error {
req , ok := c .requests [rec .h .Id ]
if !ok && rec .h .Type != typeBeginRequest && rec .h .Type != typeGetValues {
return nil
}
switch rec .h .Type {
case typeBeginRequest :
if req != nil {
return errors .New ("fcgi: received ID that is already in-flight" )
}
var br beginRequest
if err := br .read (rec .content ()); err != nil {
return err
}
if br .role != roleResponder {
c .conn .writeEndRequest (rec .h .Id , 0 , statusUnknownRole )
return nil
}
req = newRequest (rec .h .Id , br .flags )
c .requests [rec .h .Id ] = req
return nil
case typeParams :
if len (rec .content ()) > 0 {
req .rawParams = append (req .rawParams , rec .content ()...)
return nil
}
req .parseParams ()
return nil
case typeStdin :
content := rec .content ()
if req .pw == nil {
var body io .ReadCloser
if len (content ) > 0 {
body , req .pw = io .Pipe ()
} else {
body = emptyBody
}
go c .serveRequest (req , body )
}
if len (content ) > 0 {
req .pw .Write (content )
} else {
delete (c .requests , req .reqId )
if req .pw != nil {
req .pw .Close ()
}
}
return nil
case typeGetValues :
values := map [string ]string {"FCGI_MPXS_CONNS" : "1" }
c .conn .writePairs (typeGetValuesResult , 0 , values )
return nil
case typeData :
return nil
case typeAbortRequest :
delete (c .requests , rec .h .Id )
c .conn .writeEndRequest (rec .h .Id , 0 , statusRequestComplete )
if req .pw != nil {
req .pw .CloseWithError (ErrRequestAborted )
}
if !req .keepConn {
return errCloseConn
}
return nil
default :
b := make ([]byte , 8 )
b [0 ] = byte (rec .h .Type )
c .conn .writeRecord (typeUnknownType , 0 , b )
return nil
}
}
func filterOutUsedEnvVars(envVars map [string ]string ) map [string ]string {
withoutUsedEnvVars := make (map [string ]string )
for k , v := range envVars {
if addFastCGIEnvToContext (k ) {
withoutUsedEnvVars [k ] = v
}
}
return withoutUsedEnvVars
}
func (c *child ) serveRequest (req *request , body io .ReadCloser ) {
r := newResponse (c , req )
httpReq , err := cgi .RequestFromMap (req .params )
if err != nil {
r .WriteHeader (http .StatusInternalServerError )
c .conn .writeRecord (typeStderr , req .reqId , []byte (err .Error()))
} else {
httpReq .Body = body
withoutUsedEnvVars := filterOutUsedEnvVars (req .params )
envVarCtx := context .WithValue (httpReq .Context (), envVarsContextKey {}, withoutUsedEnvVars )
httpReq = httpReq .WithContext (envVarCtx )
c .handler .ServeHTTP (r , httpReq )
}
r .Write (nil )
r .Close ()
c .conn .writeEndRequest (req .reqId , 0 , statusRequestComplete )
io .CopyN (io .Discard , body , 100 <<20 )
body .Close ()
if !req .keepConn {
c .conn .Close ()
}
}
func (c *child ) cleanUp () {
for _ , req := range c .requests {
if req .pw != nil {
req .pw .CloseWithError (ErrConnClosed )
}
}
}
func Serve (l net .Listener , handler http .Handler ) error {
if l == nil {
var err error
l , err = net .FileListener (os .Stdin )
if err != nil {
return err
}
defer l .Close ()
}
if handler == nil {
handler = http .DefaultServeMux
}
for {
rw , err := l .Accept ()
if err != nil {
return err
}
c := newChild (rw , handler )
go c .serve ()
}
}
func ProcessEnv (r *http .Request ) map [string ]string {
env , _ := r .Context ().Value (envVarsContextKey {}).(map [string ]string )
return env
}
func addFastCGIEnvToContext(s string ) bool {
switch s {
case "CONTENT_LENGTH" , "CONTENT_TYPE" , "HTTPS" ,
"PATH_INFO" , "QUERY_STRING" , "REMOTE_ADDR" ,
"REMOTE_HOST" , "REMOTE_PORT" , "REQUEST_METHOD" ,
"REQUEST_URI" , "SCRIPT_NAME" , "SERVER_PROTOCOL" :
return false
}
if strings .HasPrefix (s , "HTTP_" ) {
return false
}
switch s {
case "REMOTE_USER" :
return true
}
return true
}
The pages are generated with Golds v0.7.0-preview . (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu .
PR and bug reports are welcome and can be submitted to the issue list .
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds .