mirror of
https://github.com/dunglas/frankenphp.git
synced 2025-09-26 19:41:13 +08:00

* create a simple thread framework Signed-off-by: Robert Landers <landers.robert@gmail.com> * add tests Signed-off-by: Robert Landers <landers.robert@gmail.com> * fix comment Signed-off-by: Robert Landers <landers.robert@gmail.com> * remove mention of an old function that no longer exists Signed-off-by: Robert Landers <landers.robert@gmail.com> * simplify providing a request Signed-off-by: Robert Landers <landers.robert@gmail.com> * satisfy linter Signed-off-by: Robert Landers <landers.robert@gmail.com> * add error handling and handle shutdowns Signed-off-by: Robert Landers <landers.robert@gmail.com> * add tests Signed-off-by: Robert Landers <landers.robert@gmail.com> * pipes are tied to workers, not threads Signed-off-by: Robert Landers <landers.robert@gmail.com> * fix test Signed-off-by: Robert Landers <landers.robert@gmail.com> * add a way to detect when a request is completed Signed-off-by: Robert Landers <landers.robert@gmail.com> * we never shutdown workers or remove them, so we do not need this Signed-off-by: Robert Landers <landers.robert@gmail.com> * add more comments Signed-off-by: Robert Landers <landers.robert@gmail.com> * Simplify modular threads (#1874) * Simplify * remove unused variable * log thread index * feat: allow passing parameters to the PHP callback and accessing its return value (#1881) * fix formatting Signed-off-by: Robert Landers <landers.robert@gmail.com> * fix test compilation Signed-off-by: Robert Landers <landers.robert@gmail.com> * fix segfaults Signed-off-by: Robert Landers <landers.robert@gmail.com> * Update frankenphp.c Co-authored-by: Kévin Dunglas <kevin@dunglas.fr> --------- Signed-off-by: Robert Landers <landers.robert@gmail.com> Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
165 lines
3.6 KiB
Go
165 lines
3.6 KiB
Go
package frankenphp
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// frankenPHPContext provides contextual information about the Request to handle.
|
|
type frankenPHPContext struct {
|
|
documentRoot string
|
|
splitPath []string
|
|
env PreparedEnv
|
|
logger *slog.Logger
|
|
request *http.Request
|
|
originalRequest *http.Request
|
|
worker *worker
|
|
|
|
docURI string
|
|
pathInfo string
|
|
scriptName string
|
|
scriptFilename string
|
|
|
|
// Whether the request is already closed by us
|
|
isDone bool
|
|
|
|
responseWriter http.ResponseWriter
|
|
handlerParameters any
|
|
handlerReturn any
|
|
|
|
done chan any
|
|
startedAt time.Time
|
|
}
|
|
|
|
// fromContext extracts the frankenPHPContext from a context.
|
|
func fromContext(ctx context.Context) (fctx *frankenPHPContext, ok bool) {
|
|
fctx, ok = ctx.Value(contextKey).(*frankenPHPContext)
|
|
return
|
|
}
|
|
|
|
// NewRequestWithContext creates a new FrankenPHP request context.
|
|
func NewRequestWithContext(r *http.Request, opts ...RequestOption) (*http.Request, error) {
|
|
fc := &frankenPHPContext{
|
|
done: make(chan any),
|
|
startedAt: time.Now(),
|
|
request: r,
|
|
}
|
|
for _, o := range opts {
|
|
if err := o(fc); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if fc.logger == nil {
|
|
fc.logger = logger
|
|
}
|
|
|
|
if fc.documentRoot == "" {
|
|
if EmbeddedAppPath != "" {
|
|
fc.documentRoot = EmbeddedAppPath
|
|
} else {
|
|
var err error
|
|
if fc.documentRoot, err = os.Getwd(); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
// If a worker is already assigned explicitly, use its filename and skip parsing path variables
|
|
if fc.worker != nil {
|
|
fc.scriptFilename = fc.worker.fileName
|
|
} else {
|
|
// If no worker was assigned, split the path into the "traditional" CGI path variables.
|
|
// This needs to already happen here in case a worker script still matches the path.
|
|
splitCgiPath(fc)
|
|
}
|
|
|
|
c := context.WithValue(r.Context(), contextKey, fc)
|
|
|
|
return r.WithContext(c), nil
|
|
}
|
|
|
|
// newDummyContext creates a fake context from a request path
|
|
func newDummyContext(requestPath string, opts ...RequestOption) (*frankenPHPContext, error) {
|
|
r, err := http.NewRequest(http.MethodGet, requestPath, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
fr, err := NewRequestWithContext(r, opts...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
fc, _ := fromContext(fr.Context())
|
|
|
|
return fc, nil
|
|
}
|
|
|
|
// closeContext sends the response to the client
|
|
func (fc *frankenPHPContext) closeContext() {
|
|
if fc.isDone {
|
|
return
|
|
}
|
|
|
|
close(fc.done)
|
|
fc.isDone = true
|
|
}
|
|
|
|
// validate checks if the request should be outright rejected
|
|
func (fc *frankenPHPContext) validate() bool {
|
|
if strings.Contains(fc.request.URL.Path, "\x00") {
|
|
fc.rejectBadRequest("Invalid request path")
|
|
|
|
return false
|
|
}
|
|
|
|
contentLengthStr := fc.request.Header.Get("Content-Length")
|
|
if contentLengthStr != "" {
|
|
if contentLength, err := strconv.Atoi(contentLengthStr); err != nil || contentLength < 0 {
|
|
fc.rejectBadRequest("invalid Content-Length header: " + contentLengthStr)
|
|
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func (fc *frankenPHPContext) clientHasClosed() bool {
|
|
select {
|
|
case <-fc.request.Context().Done():
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// reject sends a response with the given status code and message
|
|
func (fc *frankenPHPContext) reject(statusCode int, message string) {
|
|
if fc.isDone {
|
|
return
|
|
}
|
|
|
|
rw := fc.responseWriter
|
|
if rw != nil {
|
|
rw.WriteHeader(statusCode)
|
|
_, _ = rw.Write([]byte(message))
|
|
|
|
if f, ok := rw.(http.Flusher); ok {
|
|
f.Flush()
|
|
}
|
|
}
|
|
|
|
fc.closeContext()
|
|
}
|
|
|
|
func (fc *frankenPHPContext) rejectBadRequest(message string) {
|
|
fc.reject(http.StatusBadRequest, message)
|
|
}
|