diff --git a/caddy/caddy.go b/caddy/caddy.go index 83cdc007df..330546aaf9 100644 --- a/caddy/caddy.go +++ b/caddy/caddy.go @@ -7,6 +7,7 @@ import ( "encoding/json" "errors" "net/http" + "path/filepath" "strconv" "github.com/caddyserver/caddy/v2" @@ -93,8 +94,6 @@ func (f *FrankenPHPApp) Start() error { } } - logger.Info("FrankenPHP started 🐘", zap.String("php_version", frankenphp.Version().Version)) - return nil } @@ -169,6 +168,10 @@ func (f *FrankenPHPApp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { if wc.FileName == "" { return errors.New(`The "file" argument must be specified`) } + + if frankenphp.EmbeddedAppPath != "" && filepath.IsLocal(wc.FileName) { + wc.FileName = filepath.Join(frankenphp.EmbeddedAppPath, wc.FileName) + } } f.Workers = append(f.Workers, wc) @@ -193,7 +196,7 @@ func parseGlobalOption(d *caddyfile.Dispenser, _ interface{}) (interface{}, erro } type FrankenPHPModule struct { - // Root sets the root folder to the site. Default: `root` directive. + // Root sets the root folder to the site. Default: `root` directive, or the path of the embed app it exists. Root string `json:"root,omitempty"` // SplitPath sets the substrings for splitting the URI into two parts. The first matching substring will be used to split the "path info" from the path. The first piece is suffixed with the matching substring and will be assumed as the actual resource (CGI script) name. The second piece will be set to PATH_INFO for the CGI script to use. Default: `.php`. SplitPath []string `json:"split_path,omitempty"` @@ -217,7 +220,12 @@ func (f *FrankenPHPModule) Provision(ctx caddy.Context) error { f.logger = ctx.Logger(f) if f.Root == "" { - f.Root = "{http.vars.root}" + if frankenphp.EmbeddedDocumentRoot == "" { + f.Root = "{http.vars.root}" + } else { + f.Root = frankenphp.EmbeddedDocumentRoot + f.ResolveRootSymlink = false + } } if len(f.SplitPath) == 0 { f.SplitPath = []string{".php"} @@ -425,6 +433,12 @@ func parsePhpServer(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) // unmarshaler can read it from the start dispenser.Reset() + if phpsrv.Root == "" && frankenphp.EmbeddedAppPath != "" { + phpsrv.Root = frankenphp.EmbeddedDocumentRoot + fsrv.Root = frankenphp.EmbeddedDocumentRoot + phpsrv.ResolveRootSymlink = false + } + // set up a route list that we'll append to routes := caddyhttp.RouteList{} diff --git a/caddy/php-server.go b/caddy/php-server.go index a6b3fbbccc..f173f8a21f 100644 --- a/caddy/php-server.go +++ b/caddy/php-server.go @@ -15,6 +15,7 @@ import ( "github.com/caddyserver/caddy/v2/modules/caddyhttp/fileserver" "github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite" "github.com/caddyserver/certmagic" + "github.com/dunglas/frankenphp" "go.uber.org/zap" "github.com/spf13/cobra" @@ -60,6 +61,10 @@ func cmdPHPServer(fs caddycmd.Flags) (int, error) { debug := fs.Bool("debug") compress := !fs.Bool("no-compress") + if root == "" && frankenphp.EmbeddedDocumentRoot != "" { + root = frankenphp.EmbeddedDocumentRoot + } + const indexFile = "index.php" extensions := []string{"php"} tryFiles := []string{"{http.request.uri.path}", "{http.request.uri.path}/" + indexFile, indexFile} diff --git a/embed.go b/embed.go new file mode 100644 index 0000000000..7eba1cde0a --- /dev/null +++ b/embed.go @@ -0,0 +1,98 @@ +package frankenphp + +import ( + "crypto/md5" + "embed" + _ "embed" + "encoding/hex" + "fmt" + "io/fs" + "os" + "path/filepath" + "strings" +) + +const embedDir = "embed" +const publicDir = "public" + +// The path of the embedded PHP application (empty if none) +var EmbeddedAppPath string + +// The path of the document root of the embedded PHP application (empty if none) +var EmbeddedDocumentRoot string + +//go:embed all:embed +var embeddedApp embed.FS + +func init() { + _, err := embeddedApp.ReadDir(embedDir + "/" + publicDir) + if err != nil { + // no embedded app + return + } + + e, err := os.Executable() + if err != nil { + panic(err) + } + + e, err = filepath.EvalSymlinks(e) + if err != nil { + panic(err) + } + + // TODO: use XXH3 instead of MD5 + h := md5.Sum([]byte(e)) + appPath := fmt.Sprintf("%sfrankenphp_%s/", os.TempDir(), hex.EncodeToString(h[:])) + + entries, err := embeddedApp.ReadDir(embedDir) + if err != nil { + panic(err) + } + + if err := os.RemoveAll(appPath); err != nil { + panic(err) + } + if err := copyToDisk(appPath, embedDir, entries); err != nil { + os.RemoveAll(appPath) + panic(err) + } + + EmbeddedAppPath = appPath + EmbeddedDocumentRoot = appPath + publicDir +} + +func copyToDisk(appPath string, currentDir string, entries []fs.DirEntry) error { + if err := os.Mkdir(appPath+strings.TrimPrefix(currentDir, embedDir), 0700); err != nil { + return err + } + + for _, entry := range entries { + name := entry.Name() + + if entry.IsDir() { + entries, err := embeddedApp.ReadDir(currentDir + "/" + name) + if err != nil { + return err + } + + if err := copyToDisk(appPath, currentDir+"/"+name, entries); err != nil { + return err + } + + continue + } + + data, err := embeddedApp.ReadFile(currentDir + "/" + name) + if err != nil { + return err + } + + f := appPath + "/" + strings.TrimPrefix(currentDir, embedDir) + "/" + name + if err := os.WriteFile(f, data, 0500); err != nil { + return err + } + } + + return nil +} diff --git a/embed/.gitignore b/embed/.gitignore new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frankenphp.go b/frankenphp.go index e63d4a2660..a0ba4fadb8 100644 --- a/frankenphp.go +++ b/frankenphp.go @@ -152,9 +152,13 @@ func NewRequestWithContext(r *http.Request, opts ...RequestOption) (*http.Reques } if fc.documentRoot == "" { - var err error - if fc.documentRoot, err = os.Getwd(); err != nil { - return nil, err + if EmbeddedAppPath != "" { + fc.documentRoot = EmbeddedAppPath + } else { + var err error + if fc.documentRoot, err = os.Getwd(); err != nil { + return nil, err + } } } @@ -315,7 +319,10 @@ func Init(options ...Option) error { return err } - logger.Debug("FrankenPHP started") + logger.Info("FrankenPHP started 🐘", zap.String("php_version", Version().Version)) + if EmbeddedAppPath != "" { + logger.Info("embedded PHP app 📦", zap.String("path", EmbeddedAppPath), zap.String("document_root", EmbeddedDocumentRoot)) + } return nil } @@ -330,6 +337,11 @@ func Shutdown() { // Always reset the WaitGroup to ensure we're in a clean state workersReadyWG = sync.WaitGroup{} + // Remove the installed app + if EmbeddedAppPath != "" { + os.RemoveAll(EmbeddedAppPath) + } + logger.Debug("FrankenPHP shut down") }