diff --git a/fastcgi/client.go b/fastcgi/client.go index 5b4aaf9..2a876d4 100644 --- a/fastcgi/client.go +++ b/fastcgi/client.go @@ -4,12 +4,12 @@ import ( "bufio" "bytes" "encoding/binary" - "fmt" "io" "log" "net/http" "net/http/httputil" "net/textproto" + "os" "strconv" "strings" "sync" @@ -41,20 +41,15 @@ func (client *FCGIClient) writeRecord(r *Record) (err error) { } // We write long data such as FCGI_PARAMS or FCGI_STDIN -// as a stream. If a content's body is larger than -// maxWrite, we will split it into separate records. -// maxWrite is determined by the size of the -// contentLength field in the header (2 bytes) -// Meaning that the max value for contentLength -// we can encode is 65535. -func (c *FCGIClient) writeStream(records []Record) error { +// as a stream. Sending an empty record of the same +// type to signal the end. +func (c *FCGIClient) writeStream(req *FCGIRequest, records []Record) error { if len(records) == 0 { return nil } log.Printf("Writing %d records of type %d to stream", len(records), records[0].Header.Type) for _, r := range records { - // Write the piece of the record to the stream c.writeRecord(&r) } @@ -63,7 +58,7 @@ func (c *FCGIClient) writeStream(records []Record) error { // type so we'll just use the type from // the first item in the slice. log.Println("Ending stream") - end := NewRecord(records[0].Header.Type, nil) + end := req.NewRecord(records[0].Header.Type, nil) c.writeRecord(end) return nil } @@ -79,7 +74,7 @@ func readRecord(r io.Reader) (*Record, error) { var paddinglength uint8 var reserved uint8 - log.Println("Reading header of response record") + // log.Println("Reading header of response record") // Let's read the header fields of the record. binary.Read(r, binary.BigEndian, &version) binary.Read(r, binary.BigEndian, &recType) @@ -87,9 +82,8 @@ func readRecord(r io.Reader) (*Record, error) { binary.Read(r, binary.BigEndian, &contentlength) binary.Read(r, binary.BigEndian, &paddinglength) binary.Read(r, binary.BigEndian, &reserved) - log.Printf("Ver: %d Type: %d ID: %d Length: %d Padding: %d Reserved; %d", version, recType, id, contentlength, paddinglength, reserved) + // log.Printf("Ver: %d Type: %d ID: %d Length: %d Padding: %d Reserved; %d", version, recType, id, contentlength, paddinglength, reserved) - log.Println("Reading record contents") readLength := int(contentlength) + int(paddinglength) content := make([]byte, readLength) @@ -123,8 +117,6 @@ func (c *FCGIClient) readResponse() []byte { log.Printf("Encountered error when reading the stream: %s", err.Error()) } - log.Println("Read a record") - if r.Header.Type == FCGI_END_REQUEST { break } @@ -140,8 +132,8 @@ func (c *FCGIClient) readResponse() []byte { // // } -func (c *FCGIClient) BeginRequest() error { - err := c.writeRecord(NewBeginRequestRecord()) +func (c *FCGIClient) BeginRequest(req *FCGIRequest) error { + err := c.writeRecord(req.NewBeginRequestRecord()) if err != nil { return err } @@ -152,23 +144,34 @@ func (c *FCGIClient) BeginRequest() error { // Do made the request and returns a io.Reader that translates the data read // from fcgi responder out of fcgi packet before returning it. func (client *FCGIClient) Do(req *FCGIRequest) (http.Response, error) { - client.BeginRequest() + log.Printf("______REQUEST %d______", req.Id) + + log.Println("-- REQUEST CONTEXT --") + for k, v := range req.Context { + log.Printf("[%s] = %s", k, v) + } + + client.BeginRequest(req) // Write the request context as a stream log.Println("Sending FCGI_PARAMS") - client.writeStream(req.EncodeContext()) + client.writeStream(req, req.EncodeContext()) log.Println("Done") - // body := newWriter(client, FCGI_STDIN) - // if req != nil { - // io.Copy(body, req) - // } - // body.Close() + // Write the body (if any) + body := req.EncodeBody() + if len(body) > 0 { + client.writeStream(req, body) + } // Read the app response from the FCGI_STDOUT stream respContent := client.readResponse() - fmt.Println(parseHttp(respContent)) + log.Printf("______END REQUEST %d______", req.Id) + + f, _ := os.Create("./resp.txt") + defer f.Close() + f.Write(respContent) return parseHttp(respContent) } @@ -178,7 +181,7 @@ func parseHttp(raw []byte) (http.Response, error) { bf := bufio.NewReader(bytes.NewReader(raw)) tp := textproto.NewReader(bf) resp := new(http.Response) - // Parse the first line of the response. + // Ensure we have a valid http response line, err := tp.ReadLine() if err != nil { if err == io.EOF { @@ -186,12 +189,15 @@ func parseHttp(raw []byte) (http.Response, error) { } return http.Response{}, err } - if i := strings.IndexByte(line, ' '); i == -1 { - err = &badStringError{"malformed HTTP response", line} - } else { - resp.Proto = line[:i] - resp.Status = strings.TrimLeft(line[i+1:], " ") + + i := strings.IndexByte(line, ' ') + + if i == -1 { + return http.Response{}, &badStringError{"malformed HTTP response", line} } + + resp.Proto = line[:i] + resp.Status = strings.TrimLeft(line[i+1:], " ") statusCode := resp.Status if i := strings.IndexByte(resp.Status, ' '); i != -1 { statusCode = resp.Status[:i] diff --git a/fastcgi/request.go b/fastcgi/request.go index c4e35bb..b7569e0 100644 --- a/fastcgi/request.go +++ b/fastcgi/request.go @@ -3,106 +3,117 @@ package fastcgi import ( "bytes" "encoding/binary" + "fmt" "io" "log" "net/http" - "net/url" - "strconv" + "path/filepath" + "strings" ) type FCGIRequest struct { Id uint16 Context map[string]string - Body []byte + Body bytes.Buffer Records []Record } func RequestFromHttp(r *http.Request) *FCGIRequest { c := FCGIRequest{} + c.Id = 1 c.Context = make(map[string]string) c.Context["SERVER_SOFTWARE"] = "oasis / fastcgi" c.Context["QUERY_STRING"] = r.URL.RawQuery c.Context["REMOTE_ADDR"] = "127.0.0.1" + c.Context["REQUEST_METHOD"] = r.Method + c.Context["REQUEST_URI"] = r.URL.Path + c.Context["SERVER_ADDR"] = "localhost" + c.Context["SERVER_PORT"] = "8000" + c.Context["SERVER_NAME"] = "localhost" + + // HTTP headers should be sent as FCGI_PARAMS. + // We have to turn the name of the header + // into environment variable format. + // Ex: Content-Type => CONTENT_TYPE + // Parameters like CONTENT_TYPE or + // CONTENT_LENGTH are important, and + // they should come from the browser/request + // itself. If you're having issues, check + // if some important parameter is missing. + for name, value := range r.Header { + // FastCGI doesn't support multiple values per header. + // However, the go http library does, so we'll + // concatenate the values with , just in case. + k := strings.ToUpper(name) + k = strings.ReplaceAll(k, "-", "_") + c.Context[k] = strings.Join(value, ", ") + // TODO: In the future we can figure out which headers need the + // HTTP_ prefix. But for now we'll just add both params with + // and without the prefix. + c.Context[fmt.Sprintf("HTTP_%s", k)] = strings.Join(value, ", ") + } + + // Gotta add this one just in case + c.Context["HTTP_COOKIE"] = c.Context["COOKIE"] + + // HTTP body will be forwarded in FCGI_STDIN + body, err := io.ReadAll(r.Body) + + if err != nil { + panic("Somehow failed at reading the http body") + } + + c.Body.Write(body) return &c } -func (req *FCGIRequest) Script(path string) { - req.Context["SCRIPT_FILENAME"] = path +func (req *FCGIRequest) Script(filename string) { + req.Context["SCRIPT_FILENAME"] = filepath.Join(req.Context["DOCUMENT_ROOT"], filename) } -func (req *FCGIRequest) Method(m string) { - req.Context["REQUEST_METHOD"] = m +func (req *FCGIRequest) Root(path string) { + req.Context["DOCUMENT_ROOT"] = path } -// Get issues a GET request to the fcgi responder. -func (r *FCGIRequest) TypeGet() { +// The body of the http response (such as POST form data) +// will be encoded into records of type FCGI_STDIN +// to be sent as a stream. If the body is longer +// than maxWrite (in bytes) we will split it into separate +// records. The value of maxWrite is determined by +// the size of the ContentLength field of the +// Header struct. Since it's only a two byte +// integer, the max content length we can +// encode in a single record is 65,535 bytes. +func (req *FCGIRequest) EncodeBody() []Record { + // We made the request body a bytes.Buffer so the + // operation of splitting it into multiple + // records can be done by just reading + // from the buffer up to maxWrite + // until it's done. + chunks := [][]byte{} - r.Context["REQUEST_METHOD"] = "GET" - r.Context["CONTENT_LENGTH"] = "0" -} - -// Get issues a Post request to the fcgi responder. with request body -// in the format that bodyType specified -func (r *FCGIRequest) TypePost(bodyType string, body io.Reader, l int) { - - if len(r.Context["REQUEST_METHOD"]) == 0 || r.Context["REQUEST_METHOD"] == "GET" { - r.Context["REQUEST_METHOD"] = "POST" + log.Println("Encoding request body") + for len(req.Body.Bytes()) > 0 { + // Read either max write or the current buffer length, + // whichever is higher. + readSize := min(len(req.Body.Bytes()), maxWrite) + chunk := make([]byte, readSize) + req.Body.Read(chunk) + chunks = append(chunks, chunk) } - r.Context["CONTENT_LENGTH"] = strconv.Itoa(l) - if len(bodyType) > 0 { - r.Context["CONTENT_TYPE"] = bodyType - } else { - r.Context["CONTENT_TYPE"] = "application/x-www-form-urlencoded" + log.Printf("Body was split into %d chunks", len(chunks)) + + // Pack up the chunks into records + records := []Record{} + + for _, c := range chunks { + records = append(records, *req.NewRecord(FCGI_STDIN, c)) } -} -// PostForm issues a POST to the fcgi responder, with form -// as a string key to a list values (url.Values) -func (r *FCGIRequest) TypePostForm(data url.Values) { - body := bytes.NewReader([]byte(data.Encode())) - r.TypePost("application/x-www-form-urlencoded", body, body.Len()) + return records } -// PostFile issues a POST to the fcgi responder in multipart(RFC 2046) standard, -// with form as a string key to a list values (url.Values), -// and/or with file as a string key to a list file path. -// func (r *FCGIRequest) PostFile(p map[string]string, data url.Values, file map[string]string) { -// buf := &bytes.Buffer{} -// writer := multipart.NewWriter(buf) -// bodyType := writer.FormDataContentType() -// -// for key, val := range data { -// for _, v0 := range val { -// err = writer.WriteField(key, v0) -// if err != nil { -// return -// } -// } -// } -// -// for key, val := range file { -// fd, e := os.Open(val) -// if e != nil { -// return nil, e -// } -// defer fd.Close() -// -// part, e := writer.CreateFormFile(key, filepath.Base(val)) -// if e != nil { -// return nil, e -// } -// _, err = io.Copy(part, fd) -// } -// -// err = writer.Close() -// if err != nil { -// return -// } -// -// // return client.Post(p, bodyType, buf, buf.Len()) -// } - // Spec: https://www.mit.edu/~yandros/doc/specs/fcgi-spec.html#S3 // Name value pairs such as: SCRIPT_PATH = /some/path // Should be encoded as such: @@ -165,7 +176,7 @@ func (req *FCGIRequest) EncodeContext() []Record { buf.WriteString(k) buf.WriteString(v) - records = append(records, *NewRecord(FCGI_PARAMS, buf.Bytes())) + records = append(records, *req.NewRecord(FCGI_PARAMS, buf.Bytes())) buf.Reset() } diff --git a/index.php b/index.php index 1b13023..744d336 100644 --- a/index.php +++ b/index.php @@ -1,14 +1,28 @@ + +

Name is:

-

SAPI NAME: +

SAPI NAME:

+

Data Posted:

+

REQ URI:

+

UPLOADED FILE NAME:

+
+
+ + + +
+ diff --git a/main.go b/main.go index fd616c1..2d42e92 100644 --- a/main.go +++ b/main.go @@ -5,15 +5,45 @@ import ( "io" "log" "net/http" + "os" + "path/filepath" + "strings" "github.com/javif89/oasis/fastcgi" ) +func fileExists(path string) bool { + fileinfo, err := os.Stat(path) + if os.IsNotExist(err) || fileinfo.IsDir() { + return false + } + + return true +} + func handleRequest(w http.ResponseWriter, r *http.Request) { fmt.Println("Request received") + root := "/home/javi/projects/javierfeliz.com/public/" + // We will first try checking if the file exists + // in case a static file is being requested + // such as js, css, etc. + path := filepath.Join(root, r.URL.Path) + log.Printf("Request path: %s", r.URL.Path) + if fileExists(path) { + log.Printf("Checking for file: %s", path) + log.Println("Serving file") + fs := http.FileServer(http.Dir(root)) + fs.ServeHTTP(w, r) + return + } + + // If the request was not for a static file + // we will forward the request to php-fpm + // and return the result of that. + log.Println("Not a file. Forwarding to php-fpm") req := fastcgi.RequestFromHttp(r) - req.Script("/home/javi/projects/oasis/index.php") - req.TypeGet() + req.Root(root) + req.Script("index.php") fcgiClient, err := fastcgi.Dial("unix", "/var/run/php/php8.3-fpm.sock") defer fcgiClient.Close() @@ -28,10 +58,24 @@ func handleRequest(w http.ResponseWriter, r *http.Request) { } content, err := io.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { log.Println("err:", err) } + if resp.StatusCode != 0 { + w.WriteHeader(resp.StatusCode) + } + + for k, v := range resp.Header { + log.Printf("Header received: %s: %s", k, strings.Join(v, ", ")) + + for _, hv := range v { + w.Header().Add(k, hv) + } + } + w.Write(content) }