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" "bufio"
"bytes" "bytes"
"encoding/binary" "encoding/binary"
"fmt"
"io" "io"
"log" "log"
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
"net/textproto" "net/textproto"
"os"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
@ -41,20 +41,15 @@ func (client *FCGIClient) writeRecord(r *Record) (err error) {
} }
// We write long data such as FCGI_PARAMS or FCGI_STDIN // We write long data such as FCGI_PARAMS or FCGI_STDIN
// as a stream. If a content's body is larger than // as a stream. Sending an empty record of the same
// maxWrite, we will split it into separate records. // type to signal the end.
// maxWrite is determined by the size of the func (c *FCGIClient) writeStream(req *FCGIRequest, records []Record) error {
// 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 {
if len(records) == 0 { if len(records) == 0 {
return nil return nil
} }
log.Printf("Writing %d records of type %d to stream", len(records), records[0].Header.Type) log.Printf("Writing %d records of type %d to stream", len(records), records[0].Header.Type)
for _, r := range records { for _, r := range records {
// Write the piece of the record to the stream
c.writeRecord(&r) c.writeRecord(&r)
} }
@ -63,7 +58,7 @@ func (c *FCGIClient) writeStream(records []Record) error {
// type so we'll just use the type from // type so we'll just use the type from
// the first item in the slice. // the first item in the slice.
log.Println("Ending stream") log.Println("Ending stream")
end := NewRecord(records[0].Header.Type, nil) end := req.NewRecord(records[0].Header.Type, nil)
c.writeRecord(end) c.writeRecord(end)
return nil return nil
} }
@ -79,7 +74,7 @@ func readRecord(r io.Reader) (*Record, error) {
var paddinglength uint8 var paddinglength uint8
var reserved 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. // Let's read the header fields of the record.
binary.Read(r, binary.BigEndian, &version) binary.Read(r, binary.BigEndian, &version)
binary.Read(r, binary.BigEndian, &recType) 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, &contentlength)
binary.Read(r, binary.BigEndian, &paddinglength) binary.Read(r, binary.BigEndian, &paddinglength)
binary.Read(r, binary.BigEndian, &reserved) 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) readLength := int(contentlength) + int(paddinglength)
content := make([]byte, readLength) 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.Printf("Encountered error when reading the stream: %s", err.Error())
} }
log.Println("Read a record")
if r.Header.Type == FCGI_END_REQUEST { if r.Header.Type == FCGI_END_REQUEST {
break break
} }
@ -140,8 +132,8 @@ func (c *FCGIClient) readResponse() []byte {
// //
// } // }
func (c *FCGIClient) BeginRequest() error { func (c *FCGIClient) BeginRequest(req *FCGIRequest) error {
err := c.writeRecord(NewBeginRequestRecord()) err := c.writeRecord(req.NewBeginRequestRecord())
if err != nil { if err != nil {
return err 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 // Do made the request and returns a io.Reader that translates the data read
// from fcgi responder out of fcgi packet before returning it. // from fcgi responder out of fcgi packet before returning it.
func (client *FCGIClient) Do(req *FCGIRequest) (http.Response, error) { 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 // Write the request context as a stream
log.Println("Sending FCGI_PARAMS") log.Println("Sending FCGI_PARAMS")
client.writeStream(req.EncodeContext()) client.writeStream(req, req.EncodeContext())
log.Println("Done") log.Println("Done")
// body := newWriter(client, FCGI_STDIN) // Write the body (if any)
// if req != nil { body := req.EncodeBody()
// io.Copy(body, req) if len(body) > 0 {
// } client.writeStream(req, body)
// body.Close() }
// Read the app response from the FCGI_STDOUT stream // Read the app response from the FCGI_STDOUT stream
respContent := client.readResponse() 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) return parseHttp(respContent)
} }
@ -178,7 +181,7 @@ func parseHttp(raw []byte) (http.Response, error) {
bf := bufio.NewReader(bytes.NewReader(raw)) bf := bufio.NewReader(bytes.NewReader(raw))
tp := textproto.NewReader(bf) tp := textproto.NewReader(bf)
resp := new(http.Response) resp := new(http.Response)
// Parse the first line of the response. // Ensure we have a valid http response
line, err := tp.ReadLine() line, err := tp.ReadLine()
if err != nil { if err != nil {
if err == io.EOF { if err == io.EOF {
@ -186,12 +189,15 @@ func parseHttp(raw []byte) (http.Response, error) {
} }
return http.Response{}, err return http.Response{}, err
} }
if i := strings.IndexByte(line, ' '); i == -1 {
err = &badStringError{"malformed HTTP response", line} i := strings.IndexByte(line, ' ')
} else {
resp.Proto = line[:i] if i == -1 {
resp.Status = strings.TrimLeft(line[i+1:], " ") return http.Response{}, &badStringError{"malformed HTTP response", line}
} }
resp.Proto = line[:i]
resp.Status = strings.TrimLeft(line[i+1:], " ")
statusCode := resp.Status statusCode := resp.Status
if i := strings.IndexByte(resp.Status, ' '); i != -1 { if i := strings.IndexByte(resp.Status, ' '); i != -1 {
statusCode = resp.Status[:i] statusCode = resp.Status[:i]

View File

@ -3,106 +3,117 @@ package fastcgi
import ( import (
"bytes" "bytes"
"encoding/binary" "encoding/binary"
"fmt"
"io" "io"
"log" "log"
"net/http" "net/http"
"net/url" "path/filepath"
"strconv" "strings"
) )
type FCGIRequest struct { type FCGIRequest struct {
Id uint16 Id uint16
Context map[string]string Context map[string]string
Body []byte Body bytes.Buffer
Records []Record Records []Record
} }
func RequestFromHttp(r *http.Request) *FCGIRequest { func RequestFromHttp(r *http.Request) *FCGIRequest {
c := FCGIRequest{} c := FCGIRequest{}
c.Id = 1
c.Context = make(map[string]string) c.Context = make(map[string]string)
c.Context["SERVER_SOFTWARE"] = "oasis / fastcgi" c.Context["SERVER_SOFTWARE"] = "oasis / fastcgi"
c.Context["QUERY_STRING"] = r.URL.RawQuery c.Context["QUERY_STRING"] = r.URL.RawQuery
c.Context["REMOTE_ADDR"] = "127.0.0.1" 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 return &c
} }
func (req *FCGIRequest) Script(path string) { func (req *FCGIRequest) Script(filename string) {
req.Context["SCRIPT_FILENAME"] = path req.Context["SCRIPT_FILENAME"] = filepath.Join(req.Context["DOCUMENT_ROOT"], filename)
} }
func (req *FCGIRequest) Method(m string) { func (req *FCGIRequest) Root(path string) {
req.Context["REQUEST_METHOD"] = m req.Context["DOCUMENT_ROOT"] = path
} }
// Get issues a GET request to the fcgi responder. // The body of the http response (such as POST form data)
func (r *FCGIRequest) TypeGet() { // 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" log.Println("Encoding request body")
r.Context["CONTENT_LENGTH"] = "0" for len(req.Body.Bytes()) > 0 {
} // Read either max write or the current buffer length,
// whichever is higher.
// Get issues a Post request to the fcgi responder. with request body readSize := min(len(req.Body.Bytes()), maxWrite)
// in the format that bodyType specified chunk := make([]byte, readSize)
func (r *FCGIRequest) TypePost(bodyType string, body io.Reader, l int) { req.Body.Read(chunk)
chunks = append(chunks, chunk)
if len(r.Context["REQUEST_METHOD"]) == 0 || r.Context["REQUEST_METHOD"] == "GET" {
r.Context["REQUEST_METHOD"] = "POST"
} }
r.Context["CONTENT_LENGTH"] = strconv.Itoa(l) log.Printf("Body was split into %d chunks", len(chunks))
if len(bodyType) > 0 {
r.Context["CONTENT_TYPE"] = bodyType // Pack up the chunks into records
} else { records := []Record{}
r.Context["CONTENT_TYPE"] = "application/x-www-form-urlencoded"
for _, c := range chunks {
records = append(records, *req.NewRecord(FCGI_STDIN, c))
} }
}
// PostForm issues a POST to the fcgi responder, with form return records
// 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())
} }
// 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 // Spec: https://www.mit.edu/~yandros/doc/specs/fcgi-spec.html#S3
// Name value pairs such as: SCRIPT_PATH = /some/path // Name value pairs such as: SCRIPT_PATH = /some/path
// Should be encoded as such: // Should be encoded as such:
@ -165,7 +176,7 @@ func (req *FCGIRequest) EncodeContext() []Record {
buf.WriteString(k) buf.WriteString(k)
buf.WriteString(v) buf.WriteString(v)
records = append(records, *NewRecord(FCGI_PARAMS, buf.Bytes())) records = append(records, *req.NewRecord(FCGI_PARAMS, buf.Bytes()))
buf.Reset() buf.Reset()
} }

View File

@ -1,14 +1,28 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title></title> <title></title>
</head> </head>
<body> <body>
<?php <?php
var_dump($_GET); var_dump($_GET);
var_dump($_POST);
var_dump($_FILES);
?> ?>
<h1>Name is: <?php echo $_GET['name'] ?? 'NOT SET' ?></h1> <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> </body>
</html> </html>

48
main.go
View File

@ -5,15 +5,45 @@ import (
"io" "io"
"log" "log"
"net/http" "net/http"
"os"
"path/filepath"
"strings"
"github.com/javif89/oasis/fastcgi" "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) { func handleRequest(w http.ResponseWriter, r *http.Request) {
fmt.Println("Request received") 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 := fastcgi.RequestFromHttp(r)
req.Script("/home/javi/projects/oasis/index.php") req.Root(root)
req.TypeGet() req.Script("index.php")
fcgiClient, err := fastcgi.Dial("unix", "/var/run/php/php8.3-fpm.sock") fcgiClient, err := fastcgi.Dial("unix", "/var/run/php/php8.3-fpm.sock")
defer fcgiClient.Close() defer fcgiClient.Close()
@ -28,10 +58,24 @@ func handleRequest(w http.ResponseWriter, r *http.Request) {
} }
content, err := io.ReadAll(resp.Body) content, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil { if err != nil {
log.Println("err:", err) 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) w.Write(content)
} }