It works
This commit is contained in:
parent
677083b071
commit
1ec6482640
@ -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]
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
|
18
index.php
18
index.php
@ -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
48
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)
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user