This commit is contained in:
Javier Feliz 2024-12-29 17:58:01 -05:00
parent 677083b071
commit 1ec6482640
4 changed files with 180 additions and 105 deletions

View File

@ -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]

View File

@ -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()
}

View File

@ -1,14 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<?php
var_dump($_GET);
var_dump($_GET);
var_dump($_POST);
var_dump($_FILES);
?>
<h1>Name is: <?php echo $_GET['name'] ?? 'NOT SET' ?></h1>
<h2>SAPI NAME: <?php echo php_sapi_name() ?>
<h2>SAPI NAME: <?php echo php_sapi_name() ?></h2>
<h2>Data Posted: <?php echo $_POST['email'] ?? 'NONE' ?></h2>
<h2>REQ URI: <?php echo $_SERVER['REQUEST_URI'] ?? 'NONE' ?></h2>
<h2>UPLOADED FILE NAME: <?php echo $_FILES['logo']['name'] ?? 'NONE' ?></h2>
<form action="/submit-form" method="POST" enctype="multipart/form-data">
<input type="text" name="name" value="Javier" placeholder="enter name"><br />
<input type="text" name="email" value="me@javierfeliz.com" placeholder="enter email">
<input type="file" name="logo">
<button type="submit">submit</button>
</form>
</body>
</html>

48
main.go
View File

@ -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)
}