From ac022435a4496785ea5afaeaf596fc856beac4f1 Mon Sep 17 00:00:00 2001 From: Javier Feliz Date: Thu, 26 Dec 2024 18:34:03 -0500 Subject: [PATCH] Temporary in case i fuck up --- README.md | 5 + fastcgi/client.go | 161 ++++++++++++++++++++++++++ fastcgi/fastcgi.go | 140 +++++++++++++++++++++++ fastcgi/request.go | 244 ++++++++++++++++++++++++++++++++++++++++ fastcgi/streamreader.go | 28 +++++ fastcgi/streamwriter.go | 29 +++++ go.mod | 3 + go.sum | 0 index.php | 14 +++ installer/installer.go | 11 ++ installer/os.go | 5 + main.go | 47 ++++++++ 12 files changed, 687 insertions(+) create mode 100644 README.md create mode 100644 fastcgi/client.go create mode 100644 fastcgi/fastcgi.go create mode 100644 fastcgi/request.go create mode 100644 fastcgi/streamreader.go create mode 100644 fastcgi/streamwriter.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 index.php create mode 100644 installer/installer.go create mode 100644 installer/os.go create mode 100644 main.go diff --git a/README.md b/README.md new file mode 100644 index 0000000..5595469 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# Oasis + +A local development tool for the laravel framework. Inspired by Valet and Herd. + +I want to make a tool that also worked on linux, and has some extra features I want. diff --git a/fastcgi/client.go b/fastcgi/client.go new file mode 100644 index 0000000..779e666 --- /dev/null +++ b/fastcgi/client.go @@ -0,0 +1,161 @@ +package fastcgi + +import ( + "bufio" + "bytes" + "encoding/binary" + "io" + "net/http" + "net/http/httputil" + "net/textproto" + "strconv" + "strings" + "sync" +) + +type FCGIClient struct { + mutex sync.Mutex + rwc io.ReadWriteCloser + h Header + buf bytes.Buffer + keepAlive bool + reqId uint16 +} + +// Close fcgi connnection +func (client *FCGIClient) Close() { + client.rwc.Close() +} + +func (client *FCGIClient) writeRecord(recType FCGIRequestType, content []byte) (err error) { + client.mutex.Lock() + defer client.mutex.Unlock() + client.buf.Reset() + // Initialize the record + header := Header{} + header.init(recType, 1, len(content)) + rec := Record{ + Header: header, + Content: content, + } + + // Write the record to the connection + b, err := rec.toBytes() + _, err = client.rwc.Write(b) + return err +} + +func (client *FCGIClient) writeEndRequest(appStatus int, protocolStatus uint8) error { + b := make([]byte, 8) + binary.BigEndian.PutUint32(b, uint32(appStatus)) + b[4] = protocolStatus + return client.writeRecord(FCGI_END_REQUEST, b) +} + +func (client *FCGIClient) writePairs(recType FCGIRequestType, pairs map[string]string) error { + w := newWriter(client, recType) + b := make([]byte, 8) + nn := 0 + for k, v := range pairs { + m := 8 + len(k) + len(v) + if m > maxWrite { + // param data size exceed 65535 bytes" + vl := maxWrite - 8 - len(k) + v = v[:vl] + } + n := encodeSize(b, uint32(len(k))) + n += encodeSize(b[n:], uint32(len(v))) + m = n + len(k) + len(v) + if (nn + m) > maxWrite { + w.Flush() + nn = 0 + } + nn += m + if _, err := w.Write(b[:n]); err != nil { + return err + } + if _, err := w.WriteString(k); err != nil { + return err + } + if _, err := w.WriteString(v); err != nil { + return err + } + } + w.Close() + return nil +} + +// 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) { + beginRequestRecord := NewBeginRequestRecord() + err := client.writeRecord(beginRequestRecord.Header.Type, beginRequestRecord.Content) + if err != nil { + return http.Response{}, err + } + + err = client.writePairs(FCGI_PARAMS, req.Context) + if err != nil { + return http.Response{}, err + } + + // body := newWriter(client, FCGI_STDIN) + // if req != nil { + // io.Copy(body, req) + // } + // body.Close() + + r := &streamReader{c: client} + rb := bufio.NewReader(r) + tp := textproto.NewReader(rb) + resp := new(http.Response) + // Parse the first line of the response. + line, err := tp.ReadLine() + if err != nil { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + 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:], " ") + } + statusCode := resp.Status + if i := strings.IndexByte(resp.Status, ' '); i != -1 { + statusCode = resp.Status[:i] + } + if len(statusCode) != 3 { + err = &badStringError{"malformed HTTP status code", statusCode} + } + resp.StatusCode, err = strconv.Atoi(statusCode) + if err != nil || resp.StatusCode < 0 { + err = &badStringError{"malformed HTTP status code", statusCode} + } + var ok bool + if resp.ProtoMajor, resp.ProtoMinor, ok = http.ParseHTTPVersion(resp.Proto); !ok { + err = &badStringError{"malformed HTTP version", resp.Proto} + } + // Parse the response headers. + mimeHeader, err := tp.ReadMIMEHeader() + if err != nil { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + return http.Response{}, err + } + resp.Header = http.Header(mimeHeader) + // TODO: fixTransferEncoding ? + resp.TransferEncoding = resp.Header["Transfer-Encoding"] + resp.ContentLength, _ = strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 64) + + if chunked(resp.TransferEncoding) { + resp.Body = io.NopCloser(httputil.NewChunkedReader(rb)) + } else { + resp.Body = io.NopCloser(rb) + } + + return *resp, nil +} diff --git a/fastcgi/fastcgi.go b/fastcgi/fastcgi.go new file mode 100644 index 0000000..a0371c0 --- /dev/null +++ b/fastcgi/fastcgi.go @@ -0,0 +1,140 @@ +package fastcgi + +import ( + "bufio" + "encoding/binary" + "fmt" + "io" + "net" +) + +type FCGIRequestType uint8 + +const FCGI_LISTENSOCK_FILENO uint8 = 0 +const FCGI_HEADER_LEN uint8 = 8 +const VERSION_1 uint8 = 1 +const FCGI_NULL_REQUEST_ID uint8 = 0 +const FCGI_KEEP_CONN uint8 = 1 +const doubleCRLF = "\r\n\r\n" + +const ( + FCGI_BEGIN_REQUEST FCGIRequestType = iota + 1 + FCGI_ABORT_REQUEST + FCGI_END_REQUEST + FCGI_PARAMS + FCGI_STDIN + FCGI_STDOUT + FCGI_STDERR + FCGI_DATA + FCGI_GET_VALUES + FCGI_GET_VALUES_RESULT + FCGI_UNKNOWN_TYPE + FCGI_MAXTYPE = FCGI_UNKNOWN_TYPE +) + +const ( + FCGI_RESPONDER uint8 = iota + 1 + FCGI_AUTHORIZER + FCGI_FILTER +) + +const ( + FCGI_REQUEST_COMPLETE uint8 = iota + FCGI_CANT_MPX_CONN + FCGI_OVERLOADED + FCGI_UNKNOWN_ROLE +) + +const ( + FCGI_MAX_CONNS string = "MAX_CONNS" + FCGI_MAX_REQS string = "MAX_REQS" + FCGI_MPXS_CONNS string = "MPXS_CONNS" +) + +const ( + maxWrite = 65500 // 65530 may work, but for compatibility + maxPad = 255 +) + +// Connects to the fcgi responder at the specified network address. +// See func net.Dial for a description of the network and address parameters. +func Dial(network, address string) (fcgi *FCGIClient, err error) { + var conn net.Conn + + conn, err = net.Dial(network, address) + if err != nil { + return + } + + fcgi = &FCGIClient{ + rwc: conn, + keepAlive: false, + reqId: 1, + } + + return +} + +func readSize(s []byte) (uint32, int) { + if len(s) == 0 { + return 0, 0 + } + size, n := uint32(s[0]), 1 + if size&(1<<7) != 0 { + if len(s) < 4 { + return 0, 0 + } + n = 4 + size = binary.BigEndian.Uint32(s) + size &^= 1 << 31 + } + return size, n +} + +func readString(s []byte, size uint32) string { + if size > uint32(len(s)) { + return "" + } + return string(s[:size]) +} + +func encodeSize(b []byte, size uint32) int { + if size > 127 { + size |= 1 << 31 + binary.BigEndian.PutUint32(b, size) + return 4 + } + b[0] = byte(size) + return 1 +} + +// bufWriter encapsulates bufio.Writer but also closes the underlying stream when +// Closed. +type bufWriter struct { + closer io.Closer + *bufio.Writer +} + +func (w *bufWriter) Close() error { + if err := w.Writer.Flush(); err != nil { + w.closer.Close() + return err + } + return w.closer.Close() +} + +func newWriter(c *FCGIClient, recType FCGIRequestType) *bufWriter { + s := &streamWriter{c: c, recType: recType} + w := bufio.NewWriterSize(s, maxWrite) + return &bufWriter{s, w} +} + +type badStringError struct { + what string + str string +} + +func (e *badStringError) Error() string { return fmt.Sprintf("%s %q", e.what, e.str) } + +// Checks whether chunked is part of the encodings stack +func chunked(te []string) bool { return len(te) > 0 && te[0] == "chunked" } diff --git a/fastcgi/request.go b/fastcgi/request.go new file mode 100644 index 0000000..213ebf5 --- /dev/null +++ b/fastcgi/request.go @@ -0,0 +1,244 @@ +package fastcgi + +import ( + "bytes" + "encoding/binary" + "errors" + "io" + "net/http" + "net/url" + "strconv" +) + +// -- TYPES -- +// These are the types needed to house the data for an FCGI request +type Header struct { + Version uint8 + Type FCGIRequestType + Id uint16 + ContentLength uint16 + PaddingLength uint8 + Reserved uint8 +} + +func (h *Header) init(reqType FCGIRequestType, reqId uint16, l int) { + h.Version = 1 + h.Type = reqType + h.Id = reqId + h.ContentLength = uint16(l) + h.PaddingLength = uint8(-l & 7) +} + +// A record is essentially a "packet" in FastCGI. +// The header lets the server know what type +// of data is being sent, and it expects +// a certain structure depending on +// the type. For example, the +// FCGI_BEGIN_REQUEST record should +// have a body of 8 bytes with: +// - The first byte being the role +// - The second byte being also the role +// - The third byte being the flags +// - The last five bytes are reserved for future use +type Record struct { + Header Header + Content []byte + Buffer bytes.Buffer // Buffer to use when writing to the network + ReadBuffer []byte // Buffer to use when reading a response +} + +// Turn a record into a byte array so it can be +// sent over the network socket +func (r *Record) toBytes() ([]byte, error) { + // client.h.init(recType, client.reqId, len(content)) + if err := binary.Write(&r.Buffer, binary.BigEndian, r.Header); err != nil { + return nil, err + } + if _, err := r.Buffer.Write(r.Content); err != nil { + return nil, err + } + if _, err := r.Buffer.Write(pad[:r.Header.PaddingLength]); err != nil { + return nil, err + } + + return r.Buffer.Bytes(), nil +} + +func (rec *Record) read(r io.Reader) (buf []byte, err error) { + if err = binary.Read(r, binary.BigEndian, &rec.Header); err != nil { + return + } + if rec.Header.Version != 1 { + err = errors.New("fcgi: invalid header version") + return + } + if rec.Header.Type == FCGI_END_REQUEST { + err = io.EOF + return + } + n := int(rec.Header.ContentLength) + int(rec.Header.PaddingLength) + if len(rec.ReadBuffer) < n { + rec.ReadBuffer = make([]byte, n) + } + if n, err = io.ReadFull(r, rec.ReadBuffer[:n]); err != nil { + return + } + buf = rec.ReadBuffer[:int(rec.Header.ContentLength)] + + return +} + +// for padding so we don't have to allocate all the time +// not synchronized because we don't care what the contents are +var pad [maxPad]byte + +type FCGIRequest struct { + Id uint16 + Type FCGIRequestType + Context map[string]string + Records []Record +} + +func RequestFromHttp(r *http.Request) *FCGIRequest { + c := FCGIRequest{} + 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" + + return &c +} + +func NewBeginRequestRecord() *Record { + role := uint16(FCGI_RESPONDER) + flags := byte(0) + // Create an 8-byte array as per the FastCGI specification. + var b [8]byte + + // Split the 16-bit role into two bytes and assign them. + b[0] = byte(role >> 8) // High byte + b[1] = byte(role) // Low byte + + // Set the flags. + b[2] = flags + + // The reserved bytes (b[3] to b[7]) will remain zero by default. + + // Return a begin request record + h := Header{} + h.init(FCGI_BEGIN_REQUEST, 1, len(b)) + return &Record{ + Header: h, + Content: b[:], + } +} + +func (req *FCGIRequest) Script(path string) { + req.Context["SCRIPT_FILENAME"] = path +} + +func (req *FCGIRequest) Method(m string) { + req.Context["REQUEST_METHOD"] = m +} + +// Get issues a GET request to the fcgi responder. +func (r *FCGIRequest) TypeGet() { + + 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" + } + 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" + } +} + +// 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()) +} + +// 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()) +// } + +// Format the context for sending +// func (r *FCGIRequest) writePairs() error { +// w := newWriter(client, recType) +// b := make([]byte, 8) +// nn := 0 +// for k, v := range r.Context { +// m := 8 + len(k) + len(v) +// if m > maxWrite { +// // param data size exceed 65535 bytes" +// vl := maxWrite - 8 - len(k) +// v = v[:vl] +// } +// n := encodeSize(b, uint32(len(k))) +// n += encodeSize(b[n:], uint32(len(v))) +// m = n + len(k) + len(v) +// if (nn + m) > maxWrite { +// w.Flush() +// nn = 0 +// } +// nn += m +// if _, err := w.Write(b[:n]); err != nil { +// return err +// } +// if _, err := w.WriteString(k); err != nil { +// return err +// } +// if _, err := w.WriteString(v); err != nil { +// return err +// } +// } +// w.Close() +// return nil +// } diff --git a/fastcgi/streamreader.go b/fastcgi/streamreader.go new file mode 100644 index 0000000..4d685a6 --- /dev/null +++ b/fastcgi/streamreader.go @@ -0,0 +1,28 @@ +package fastcgi + +type streamReader struct { + c *FCGIClient + buf []byte +} + +func (w *streamReader) Read(p []byte) (n int, err error) { + + if len(p) > 0 { + if len(w.buf) == 0 { + rec := &Record{} + w.buf, err = rec.read(w.c.rwc) + if err != nil { + return + } + } + + n = len(p) + if n > len(w.buf) { + n = len(w.buf) + } + copy(p, w.buf[:n]) + w.buf = w.buf[n:] + } + + return +} diff --git a/fastcgi/streamwriter.go b/fastcgi/streamwriter.go new file mode 100644 index 0000000..26e97fb --- /dev/null +++ b/fastcgi/streamwriter.go @@ -0,0 +1,29 @@ +package fastcgi + +// streamWriter abstracts out the separation of a stream into discrete records. +// It only writes maxWrite bytes at a time. +type streamWriter struct { + c *FCGIClient + recType FCGIRequestType +} + +func (w *streamWriter) Write(p []byte) (int, error) { + nn := 0 + for len(p) > 0 { + n := len(p) + if n > maxWrite { + n = maxWrite + } + if err := w.c.writeRecord(w.recType, p[:n]); err != nil { + return nn, err + } + nn += n + p = p[n:] + } + return nn, nil +} + +func (w *streamWriter) Close() error { + // send empty record to close the stream + return w.c.writeRecord(w.recType, nil) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..9eb89d0 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/javif89/oasis + +go 1.23.4 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/index.php b/index.php new file mode 100644 index 0000000..1b13023 --- /dev/null +++ b/index.php @@ -0,0 +1,14 @@ + + + + + + + + +

Name is:

+

SAPI NAME: + + diff --git a/installer/installer.go b/installer/installer.go new file mode 100644 index 0000000..8051368 --- /dev/null +++ b/installer/installer.go @@ -0,0 +1,11 @@ +package installer + +// This package handle installing any +// necessary programs we need to run +// the oasis environment such as: +// - Nginx +// - DNSMasq +// It uses the package manager for +// the OS and ensures any config +// files needed go in the right +// place. diff --git a/installer/os.go b/installer/os.go new file mode 100644 index 0000000..94a6b42 --- /dev/null +++ b/installer/os.go @@ -0,0 +1,5 @@ +package installer + +// Return the correct package manager +// and config paths for the current +// operating system. diff --git a/main.go b/main.go new file mode 100644 index 0000000..2f5ca58 --- /dev/null +++ b/main.go @@ -0,0 +1,47 @@ +package main + +import ( + "fmt" + "io" + "log" + "net/http" + + "github.com/javif89/oasis/fastcgi" +) + +func handleRequest(w http.ResponseWriter, r *http.Request) { + fmt.Println("Request received") + req := fastcgi.RequestFromHttp(r) + req.Script("/home/javi/projects/oasis/index.php") + req.TypeGet() + + fcgiClient, err := fastcgi.Dial("unix", "/var/run/php/php8.3-fpm.sock") + if err != nil { + log.Println("err:", err) + } + + resp, err := fcgiClient.Do(req) + if err != nil { + log.Println("err:", err) + } + + content, err := io.ReadAll(resp.Body) + if err != nil { + log.Println("err:", err) + } + + w.Write(content) +} + +func main() { + + // Server + http.HandleFunc("/", handleRequest) + + fmt.Println("Starting server") + + err := http.ListenAndServe(":8000", nil) + if err != nil { + log.Fatal(err) + } +}