diff --git a/.env.example b/.env.example index c32299c..e69de29 100644 --- a/.env.example +++ b/.env.example @@ -1,38 +0,0 @@ -# the http server listening address (required) -# [host]: -PHOO_LISTEN="0.0.0.0:8000" - -# the document root (required) -# from where you would like to serve static files? -PHOO_DOCUMENT_ROOT="/var/www/html/public" - -# the router (required) -# where is your main script (the default script) -# till now we don't support serving multiple php files, it is only one, -# this works with modern frameworks -PHOO_ROUTER="/var/www/html/public/index.php" - -# whether to enable/disable logs (optional, default: true) -# logs here means the http server level logs -PHOO_LOGS=true - -# where is the php-fpm binary? (optional, default: php-fpm) -PHOO_FPM_BIN="php-fpm8.1" - -# the php-fpm workers count (optional, default: CPU cors count) -PHOO_WORKERS_COUNT=100 - -# the maximum number each worker should handle before restarting -# this prevent memory-leaks some how (required). -PHOO_WORKER_MAX_REQUESTS=100 - -# the maximum time the request should take before killing it and its worker -PHOO_REQUEST_TIMEOUT=15s - -# additional ini settings (optional, default: "") -# example: "extension=x.so;some_key=some_value;another_key=another_value" -PHOO_PHP_INI="" - -# the user and the group used to run PHP-FPM as, (optional, default: www-data) -PHOO_PHP_USER="www-data" -PHOO_PHP_GROUP="www-data" \ No newline at end of file diff --git a/.github/workflows/releaser.yaml b/.github/workflows/releaser.yaml new file mode 100644 index 0000000..57a8259 --- /dev/null +++ b/.github/workflows/releaser.yaml @@ -0,0 +1,63 @@ +env: + CGO_ENABLED: "0" + REGISTRY: ghcr.io + IMAGE_NAME: alash3al/phoo + +on: + release: + types: [created] + +jobs: + binary-releaser: + name: Release Go Binary + runs-on: ubuntu-latest + strategy: + matrix: + goos: [linux, darwin] + goarch: [amd64, arm64] + steps: + - uses: actions/checkout@v3 + - uses: wangyoucao577/go-release-action@v1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + goos: ${{ matrix.goos }} + goarch: ${{ matrix.goarch }} + goversion: "https://dl.google.com/go/go1.21.0.linux-amd64.tar.gz" + project_path: "./cmd" + binary_name: "phoo" + ldflags: "-s -w" + + docker-releaser: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Get Latest Tag + id: var_tag + run: echo "name=$(git describe --tags --abbrev=0)" >> $GITHUB_OUTPUT + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Log in to Github Container registry + uses: docker/login-action@v2 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push Docker image + uses: docker/build-push-action@v3 + with: + file: Dockerfile + context: . + push: true + tags: | + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.var_tag.outputs.name }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.gitignore b/.gitignore index 892ea5e..b657cc0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ -.* -!.env.example \ No newline at end of file +.phoo +!.env.example +.idea \ No newline at end of file diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 34122a2..0000000 --- a/Dockerfile +++ /dev/null @@ -1,21 +0,0 @@ -FROM golang:1.18-alpine As builder - -WORKDIR /phoo/ - -RUN apk update && apk add git upx - -COPY go.mod go.sum ./ - -RUN go mod download - -COPY . . - -RUN CGO_ENABLED=0 go build -ldflags "-s -w" -o /usr/bin/phoo ./cmd/ - -RUN upx -9 /usr/bin/phoo - -FROM alpine - -WORKDIR /phoo/ - -COPY --from=builder /usr/bin/phoo /usr/bin/phoo \ No newline at end of file diff --git a/README.md b/README.md index 6b39ab6..8027270 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,60 @@ PHOO ==== -> PHP high-performance application server and `php-fpm` supervisor. +> modern php application server, it depends on the bullet-proof `php-fpm` but it controls how it is being run. -Why? -==== -> PHP isn't built for async world, so adopting the community, ecosystem and the mindset -> to be async isn't an easy task, -> but also I want very simple command to run, and it handles everything without too many configurations files, -> today most of the apps are using environment variables and the well-known `.env` file, so why there isn't a tool -> that you can ask to just run and configure everything from a single `.env` file, I don't want to add a hassle for understanding -> how `PHP-FPM` is working or anything else, all what I need it `$ phoo serve`, that's all. - -How? -==== -> Basically, `phoo` is a simple static-file as well a fastcgi reverse-proxy, but mainly focuses on `PHP`, not only that, -> but also, you can consider `phoo` a supervisor that manages `PHP-FPM` and its configurations to match today's setup. -> `phoo` is loading all the files in the document root into memory to accelerate static files serving, so don't put any huge file there. +Examples +======== +> Imagine you have a php application uses modern framework like laravel, symfony ... etc +> that app contains a public directory, and that public directory contains the main bootstrap file that +> serves the incoming requests named as `index.php`. +```shell +# this all what you need to serve a laravel application! +$ phoo serve -r ./public +⇨ http server started on [::]:8000 +``` + +#### But how about changing the address it is listening on to 0.0.0.0:80? +```shell +# no problem +$ phoo serve -r ./public --http 0.0.0.0:80 +⇨ http server started on [::]:80 +``` + +#### Sometimes I want to add custom `php.ini` settings, is it easy? +```shell +# is this ok for you? ;) +$ phoo serve -r ./public -i display_errors=Off -i another_key=another_value +⇨ http server started on [::]:8000 +``` +#### I have a high traffic web-app and I want to increase the php workers +```shell +# just increase the workers count +$ phoo serve -r ./public --workers=20 +⇨ http server started on [::]:8000 +``` + +#### Hmmmm, but I want to monitor my app via prometheus metrics, I don't want to do it manually +```shell +# no need to do it yourself, this will enable prometheus metrics at the specified `/metrics` path +$ phoo serve -r ./public --metrics "/metrics" +⇨ http server started on [::]:8000 +``` + +#### Wow!, seems `phoo` has a lot of simple flags/configs, is it documented anywhere? +> just run `phoo serve --help` and enjoy it :), you will find that you can also pass flags via `ENV` vars, and it will automatically read `.env` file in the current working directory. + +Requirements +============ +- `php-fpm` +- a shell access to run `phoo` :D + +Installation +============ +- Binary installations could be done via the [releases](https://github.com/alash3al/phoo/releases). +- Docker image is available at [`ghcr.io/alash3al/phoo`](https://github.com/alash3al/phoo/pkgs/container/phoo) + - you can easily `COPY --from=ghcr.io/alash3al/phoo:2.1.8 /usr/bin/phoo /usr/bin/phoo` to run it into your own custom image! -Usage? -====== -> for now, use our official docker image [here](https://github.com/alash3al/phoo/pkgs/container/phoo) \ No newline at end of file +TODOs +===== +- [ ] Add `.env.example` with comments to describe each var. +- [ ] Add future plans/thoughts. \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go index 06bb5c8..539ae4f 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -2,33 +2,30 @@ package main import ( "github.com/alash3al/phoo/cmd/serve" - "github.com/alash3al/phoo/pkg/symbols" - "github.com/joho/godotenv" - "github.com/labstack/gommon/log" "github.com/urfave/cli/v2" + "log" "os" ) +import _ "github.com/joho/godotenv/autoload" func main() { - app := cli.NewApp() - app.Name = symbols.AppName - app.Version = symbols.AppVersion - app.Before = func(ctxCli *cli.Context) error { - filename := ctxCli.String(symbols.FlagNameEnvFilename) - if len(filename) < 1 { - return nil - } - return godotenv.Load(filename) + app := &cli.App{ + Name: "phoo", + Description: "php modern applications serve that utilizes the bullet-proof php-fpm under-the-hood", + Suggest: true, + Version: "v3.x", + EnableBashCompletion: true, + SliceFlagSeparator: ";", } - app.Flags = append(app.Flags, &cli.StringFlag{ - Name: symbols.FlagNameEnvFilename, - Usage: "if provided, the configuration values will be loaded from it", + app.Commands = append(app.Commands, &cli.Command{ + Name: "serve", + Flags: serve.DefaultFlags("PHOO"), + Before: serve.Before(), + Action: serve.Action(), }) - app.Commands = append(app.Commands, serve.Command()) - if err := app.Run(os.Args); err != nil { - log.Error(err.Error()) + log.Fatal(err.Error()) } } diff --git a/cmd/serve/config.go b/cmd/serve/config.go deleted file mode 100644 index b919925..0000000 --- a/cmd/serve/config.go +++ /dev/null @@ -1,55 +0,0 @@ -package serve - -import ( - "github.com/alash3al/phoo/pkg/fastcgi" - "github.com/alash3al/phoo/pkg/fpm" - "os" - "os/exec" - "os/signal" - "path/filepath" - "syscall" -) - -type Config struct { - FPM fpm.Config - FastCGI fastcgi.Config - HTTPListenAddr string - DocumentRoot string - EnableLogs bool -} - -func (c *Config) Verify() error { - paths := []*string{ - &(c.FPM.ConfigFilename), - &(c.FPM.PIDFilename), - &(c.FPM.SocketFilename), - &(c.FastCGI.DefaultScript), - &(c.DocumentRoot), - } - - for _, path := range paths { - abs, err := filepath.Abs(*path) - if err != nil { - return err - } - *path = abs - } - - c.FPM.Clean() - - if _, err := exec.LookPath(c.FPM.Bin); err != nil { - return err - } - - c.FastCGI.FastCGIServerURL = "unix://" + c.FPM.SocketFilename - - signalChannel := make(chan os.Signal, 2) - signal.Notify(signalChannel, os.Interrupt, syscall.SIGTERM) - go func() { - _ = <-signalChannel - c.FPM.Clean() - os.Exit(0) - }() - - return nil -} diff --git a/cmd/serve/flags.go b/cmd/serve/flags.go new file mode 100644 index 0000000..2c2f0f8 --- /dev/null +++ b/cmd/serve/flags.go @@ -0,0 +1,140 @@ +package serve + +import ( + "fmt" + "github.com/labstack/echo/v4/middleware" + "github.com/urfave/cli/v2" + "runtime" +) + +func DefaultFlags(envPrefix string) []cli.Flag { + prefixWrapper := func(k string) string { + return fmt.Sprintf("%s_%s", envPrefix, k) + } + + return []cli.Flag{ + &cli.StringFlag{ + Name: "root", + Usage: "the document root full path", + Aliases: []string{"r"}, + EnvVars: []string{prefixWrapper("DOCUMENT_ROOT")}, + Required: true, + }, + + &cli.StringFlag{ + Name: "entrypoint", + Usage: "the default php entrypoint script", + Aliases: []string{"e"}, + EnvVars: []string{"ENTRYPOINT"}, + }, + + &cli.StringFlag{ + Name: "http", + Usage: "the http listen address in the form of [address]:port", + EnvVars: []string{prefixWrapper("HTTP_LISTEN_ADDR")}, + Value: ":8000", + }, + + &cli.BoolFlag{ + Name: "logs", + Usage: "whether to enable access logs or not", + EnvVars: []string{prefixWrapper("ENABLE_ACCESS_LOGS")}, + Value: false, + }, + + &cli.StringFlag{ + Name: "fpm", + Usage: "the php-fpm binary filename", + EnvVars: []string{prefixWrapper("PHP_FPM")}, + Value: "php-fpm", + }, + + &cli.StringFlag{ + Name: "data", + Usage: "the directory to store phoo related files in", + EnvVars: []string{prefixWrapper("DATA_DIR")}, + Value: "./.phoo", + }, + + &cli.StringSliceFlag{ + Name: "ini", + Usage: "php ini settings in the form of key=value, this flag could be repeated for multiple ini settings", + Aliases: []string{"i"}, + EnvVars: []string{prefixWrapper("PHP_INI")}, + }, + + &cli.IntFlag{ + Name: "workers", + Usage: "php fpm workers, this is the maximum requests to be served at the same time", + EnvVars: []string{prefixWrapper("WORKER_COUNT")}, + Value: runtime.NumCPU(), + }, + + &cli.IntFlag{ + Name: "requests", + Usage: "php fpm max requests per worker, if a worker reached this number, it would be recycled", + EnvVars: []string{prefixWrapper("WORKER_MAX_REQUEST_COUNT")}, + Value: runtime.NumCPU() * 100, + }, + + &cli.IntFlag{ + Name: "timeout", + Usage: "php fpm max request time in seconds per worker, if a worker reached this number, it would be terminated, 0 means 'Disabled'", + EnvVars: []string{prefixWrapper("WORKER_MAX_REQUEST_TIME")}, + Value: 300, + }, + + &cli.StringFlag{ + Name: "metrics", + Usage: "the prometheus metrics endpoint, empty means disabled", + EnvVars: []string{prefixWrapper("METRICS_PATH")}, + }, + + &cli.BoolFlag{ + Name: "cors", + Usage: "whether to enable/disable the cors-* features/flags", + EnvVars: []string{prefixWrapper("CORS_ENABLED")}, + Value: false, + }, + + &cli.StringSliceFlag{ + Name: "cors-origin", + Usage: "this flag adds the specified origin to the list of allowed cors origins, you can call it multiple times to add multiple origins", + EnvVars: []string{prefixWrapper("CORS_ORIGINS")}, + Value: cli.NewStringSlice("*"), + }, + + &cli.StringSliceFlag{ + Name: "cors-methods", + Usage: "this flag adds the specified methods to the list of allowed cors methods, you can call it multiple times to add multiple methods", + EnvVars: []string{prefixWrapper("CORS_METHODS")}, + Value: cli.NewStringSlice(middleware.DefaultCORSConfig.AllowMethods...), + }, + + &cli.StringSliceFlag{ + Name: "cors-headers", + Usage: "this flag adds the specified headers to the list of allowed cors headers the client can send, you can call it multiple times to add multiple headers", + EnvVars: []string{prefixWrapper("CORS_HEADERS")}, + }, + + &cli.StringSliceFlag{ + Name: "cors-expose", + Usage: "this flag adds the specified headers to the list of allowed headers the client can access, you can call it multiple times to add multiple headers", + EnvVars: []string{prefixWrapper("CORS_EXPOSE")}, + }, + + &cli.BoolFlag{ + Name: "cors-credentials", + Usage: "this flag indicates whether or not the actual request can be made using credentials", + EnvVars: []string{prefixWrapper("CORS_CREDENTIALS")}, + Value: false, + }, + + &cli.IntFlag{ + Name: "cors-age", + Usage: "the cors max_age in seconds", + EnvVars: []string{prefixWrapper("CORS_AGE")}, + Value: 0, + }, + } +} diff --git a/cmd/serve/middlewares.go b/cmd/serve/middlewares.go index 698ea92..9d99121 100644 --- a/cmd/serve/middlewares.go +++ b/cmd/serve/middlewares.go @@ -1,78 +1,56 @@ package serve import ( - "github.com/labstack/gommon/log" - "io/fs" - "mime" - "net/http" + "github.com/labstack/echo-contrib/echoprometheus" + "github.com/labstack/echo/v4" + "github.com/yookoala/gofast" "os" - "path/filepath" + "path" "strings" - "sync" ) -func loggerMiddleware(enable bool, handlerFunc http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - if enable { - log.Infoj(map[string]interface{}{ - "host": r.Host, - "uri": r.URL.RequestURI(), - }) - } +func servePrometheusMetricsMiddleware(metricsPath string) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + currentPath := strings.Trim(c.Request().URL.Path, "/") + metricsPath = strings.Trim(metricsPath, "/") - handlerFunc(w, r) - } -} + if metricsPath == "" { + return next(c) + } -func recoverMiddleware(handlerFunc http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - defer (func() { - if err := recover(); err != nil { - log.Error(err) + if strings.EqualFold(currentPath, metricsPath) { + return echoprometheus.NewHandler()(c) } - })() - handlerFunc(w, r) + return next(c) + } } } -func assetsCacheMiddleware(config *Config, handlerFunc http.HandlerFunc) (http.HandlerFunc, error) { - memfs := sync.Map{} - - if err := filepath.WalkDir(config.DocumentRoot, func(path string, d fs.DirEntry, err error) error { - if d.IsDir() { - return nil - } - - if filepath.Ext(config.DocumentRoot) == filepath.Ext(path) { - return nil - } +func serveStaticFilesOnlyMiddleware(documentRoot string) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + filename := path.Join(documentRoot, path.Clean(c.Request().URL.Path)) + ext := strings.ToLower(path.Ext(filename)) - if strings.HasPrefix(filepath.Base(path), ".") { - return nil - } + stat, err := os.Stat(filename) + if err != nil || stat.IsDir() || (ext == "php") || strings.HasPrefix(path.Base(filename), ".") { + return next(c) + } - data, err := os.ReadFile(path) - if err != nil { - return err + return c.File(filename) } - - memfs.Store(path, data) - - return nil - }); err != nil { - return nil, err } +} - return func(w http.ResponseWriter, r *http.Request) { - filename := filepath.Join(config.DocumentRoot, r.URL.Path) - contents, found := memfs.Load(filename) - if !found { - handlerFunc(w, r) - return - } +func serveFastCGIMiddleware(routerFilename, fastcgiServerNetwork, fastcgiServerAddr string) echo.MiddlewareFunc { + connFactory := gofast.SimpleConnFactory(fastcgiServerNetwork, fastcgiServerAddr) - w.Header().Set("Content-Type", mime.TypeByExtension(filepath.Ext(r.URL.Path))) - w.Write(contents.([]byte)) - }, nil + return func(next echo.HandlerFunc) echo.HandlerFunc { + return echo.WrapHandler(gofast.NewHandler( + gofast.NewFileEndpoint(routerFilename)(gofast.BasicSession), + gofast.SimpleClientFactory(connFactory), + )) + } } diff --git a/cmd/serve/serve.go b/cmd/serve/serve.go index 2ed7454..ff822a1 100644 --- a/cmd/serve/serve.go +++ b/cmd/serve/serve.go @@ -1,186 +1,109 @@ package serve import ( - "fmt" - "github.com/NYTimes/gziphandler" - "github.com/alash3al/phoo/pkg/fastcgi" - "github.com/alash3al/phoo/pkg/fpm" - "github.com/alash3al/phoo/pkg/symbols" - "github.com/labstack/gommon/log" + "errors" + "github.com/alash3al/phoo/internals/fpm" + "github.com/labstack/echo-contrib/echoprometheus" + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" "github.com/urfave/cli/v2" - "net/http" "os" - "runtime" - "strings" + "path/filepath" "time" ) -func Command() *cli.Command { - return &cli.Command{ - Name: "serve", - Description: "start the http server", - Action: listenAndServe(), - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: symbols.FlagNameHTTPListenAddr, - Usage: "the http address to listen on", - EnvVars: []string{symbols.EnvKeyListen}, - Required: true, - Category: symbols.AppName, - }, - &cli.StringFlag{ - Name: symbols.FlagNameDocumentRoot, - Usage: "the document root", - EnvVars: []string{symbols.EnvKeyDocumentRoot}, - Required: true, - Category: symbols.AppName, - }, - &cli.BoolFlag{ - Name: symbols.FlagNameEnableLogs, - Usage: "whether to enable/disable access log", - EnvVars: []string{symbols.EnvKeyEnableLogs}, - Value: true, - Category: symbols.AppName, - }, - &cli.StringFlag{ - Name: symbols.FlagNamePHPFPMBinary, - Usage: "the PHP-FPM binary", - EnvVars: []string{symbols.EnvKeyFPMBin}, - Value: "php-fpm", - Category: "php", - }, - &cli.StringFlag{ - Name: symbols.FlagNamePHPINI, - Usage: "additional PHP INI settings separated with semicolon (;)", - EnvVars: []string{symbols.EnvKeyPHPINI}, - Category: "php", - }, - &cli.StringFlag{ - Name: symbols.FlagNamePHPUser, - Usage: "the user who will PHP-FPM listen as", - EnvVars: []string{symbols.EnvKeyPHPUser}, - Value: "www-data", - Category: "php", - }, - &cli.StringFlag{ - Name: symbols.FlagNamePHPGroup, - Usage: "the group who will PHP-FPM listen as", - EnvVars: []string{symbols.EnvKeyPHPGroup}, - Value: "www-data", - Category: "php", - }, - &cli.Int64Flag{ - Name: symbols.FlagNameWorkersCount, - Usage: "the PHP workers count", - EnvVars: []string{symbols.EnvKeyWorkersCount}, - Value: int64(runtime.NumCPU()), - Category: "php", - }, - &cli.Int64Flag{ - Name: symbols.FlagNameWorkerMaxRequests, - Usage: "the PHP worker max requests (the worker will be restarted after reaching this value)", - EnvVars: []string{symbols.EnvKeyWorkerMaxRequests}, - Value: 500, - Category: "php", - }, - &cli.StringFlag{ - Name: symbols.FlagNameRequestTimeout, - Usage: "the request timeout", - EnvVars: []string{symbols.EnvKeyRequestTimeout}, - Value: "0", - Category: "php", - }, - &cli.StringFlag{ - Name: symbols.FlagNameDefaultScript, - Usage: "the default script used as router", - EnvVars: []string{symbols.EnvKeyRouter}, - Required: true, - Category: "php", - }, - }, - } -} - -func listenAndServe() cli.ActionFunc { - return func(cliCtx *cli.Context) error { - config := Config{ - HTTPListenAddr: cliCtx.String(symbols.FlagNameHTTPListenAddr), - DocumentRoot: cliCtx.String(symbols.FlagNameDocumentRoot), - EnableLogs: cliCtx.Bool(symbols.FlagNameEnableLogs), - FPM: fpm.Config{ - ConfigFilename: ".fpm.config.ini", - PIDFilename: ".fpm.pid", - SocketFilename: ".fpm.sock", - User: cliCtx.String(symbols.FlagNamePHPUser), - Group: cliCtx.String(symbols.FlagNamePHPGroup), - Bin: cliCtx.String(symbols.FlagNamePHPFPMBinary), - RequestTimeout: cliCtx.String(symbols.FlagNameRequestTimeout), - WorkerMaxRequests: cliCtx.Int64(symbols.FlagNameWorkerMaxRequests), - WorkersCount: cliCtx.Int64(symbols.FlagNameWorkersCount), - INI: strings.Split(cliCtx.String(symbols.FlagNamePHPINI), ";"), - Stdout: os.Stdout, - Stderr: os.Stderr, - }, - FastCGI: fastcgi.Config{ - DefaultScript: cliCtx.String(symbols.FlagNameDefaultScript), - RestrictDotFilesAccess: true, - FastCGIParams: map[string]string{ - "SERVER_SOFTWARE": fmt.Sprintf("%s/%s", symbols.AppName, symbols.AppVersion), - }, - }, - } +var fpmProcess *fpm.Process - if err := config.Verify(); err != nil { +func Before() cli.BeforeFunc { + return func(ctx *cli.Context) error { + if err := os.RemoveAll(ctx.String("data")); err != nil { return err } - fastCGIHandler, err := fastcgi.New(config.FastCGI) - if err != nil { + if err := os.MkdirAll(ctx.String("data"), 0755); err != nil { return err } - mainHandler, err := assetsCacheMiddleware(&config, recoverMiddleware( - fastCGIHandler.ServeHTTP, - )) + if ctx.String("entrypoint") == "" { + entrypoints := []string{ + filepath.Join(ctx.String("root"), "index.php"), + filepath.Join(ctx.String("root"), "app.php"), + filepath.Join(ctx.String("root"), "server.php"), + } - if err != nil { - return err + detectedEntrypoint := "" + + for _, entrypoint := range entrypoints { + stat, err := os.Stat(entrypoint) + if err != nil { + continue + } + + if stat.IsDir() { + continue + } + + detectedEntrypoint = entrypoint + } + + if detectedEntrypoint == "" { + return errors.New("unable to auto-detect the entrypoint script, try to put it yourself") + } + + if err := ctx.Set("entrypoint", detectedEntrypoint); err != nil { + return err + } } - runner, err := fpm.New(config.FPM) - if err != nil { - return err + fpmProcess = &fpm.Process{ + BinFilename: ctx.String("fpm"), + PIDFilename: filepath.Join(ctx.String("data"), "fpm.pid"), + ConfigFilename: filepath.Join(ctx.String("data"), "fpm.ini"), + SocketFilename: filepath.Join(ctx.String("data"), "fpm.sock"), + INI: ctx.StringSlice("ini"), + WorkerCount: ctx.Int("workers"), + WorkerMaxRequestCount: ctx.Int("requests"), + WorkerMaxRequestTime: ctx.Int("timeout"), } - for { - if _, err := os.Stat(config.FPM.SocketFilename); err != nil { - log.Warn("waiting till fastcgi server starts ...") - time.Sleep(time.Second * 1) - continue - } + return fpmProcess.Start() + } +} + +func Action() cli.ActionFunc { + return func(ctx *cli.Context) error { + e := echo.New() + e.HideBanner = true - log.Info("the fastcgi server has been started ...") - break + if ctx.Bool("logs") { + e.Use(middleware.Logger()) } - go (func() { - if err := runner.Wait(); err != nil { - log.Fatal(err.Error()) - } - })() - - log.Infoj(map[string]interface{}{ - "message": "configurations", - "configs": config, - "fpm-cmd": runner.String(), - }) - - return http.ListenAndServe( - config.HTTPListenAddr, - gziphandler.GzipHandler(loggerMiddleware( - config.EnableLogs, - mainHandler, - )), - ) + if ctx.Bool("cors") { + e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ + AllowOrigins: ctx.StringSlice("cors-origins"), + AllowMethods: ctx.StringSlice("cors-methods"), + AllowHeaders: ctx.StringSlice("cors-headers"), + AllowCredentials: ctx.Bool("cors-credentials"), + ExposeHeaders: ctx.StringSlice("cors-expose"), + MaxAge: ctx.Int("cors-age"), + })) + } + + e.Use(middleware.TimeoutWithConfig(middleware.TimeoutConfig{ + Timeout: time.Duration(ctx.Int("timeout")) * time.Second, + })) + + e.Use(middleware.Recover()) + e.Use(echoprometheus.NewMiddleware("PHOO")) + e.Use(servePrometheusMetricsMiddleware(ctx.String("metrics"))) + e.Use(serveStaticFilesOnlyMiddleware(ctx.String("root"))) + e.Use(serveFastCGIMiddleware( + ctx.String("entrypoint"), + "unix", + fpmProcess.SocketFilename, + )) + + return e.Start(ctx.String("http")) } } diff --git a/example/css/style.css b/example/css/style.css new file mode 100644 index 0000000..f69c279 --- /dev/null +++ b/example/css/style.css @@ -0,0 +1,7 @@ +body { + font-weight: bold; + font-size: 100px; + color: #444; + text-align: center; + margin: auto; +} \ No newline at end of file diff --git a/example/index.php b/example/index.php new file mode 100644 index 0000000..3137003 --- /dev/null +++ b/example/index.php @@ -0,0 +1,13 @@ +"; + +echo "PHOOOOOOOO ;)"; + +echo sprintf(<<<"HTML" +
+         
+             %s
+         
+     
+HTML, print_r($_SERVER, true)); diff --git a/example/phoo.yaml b/example/phoo.yaml new file mode 100644 index 0000000..a3603c0 --- /dev/null +++ b/example/phoo.yaml @@ -0,0 +1,18 @@ +document_root: "./example" +default_script: "./example/index.php" +gzip_level: 0 +http_listen_addr: ":8000" +data_dir: ".phoo" +enable_access_logs: true + +env: + APP_NAME: "laravel" + APP_ENV: "debug" + +fpm: + bin: "php-fpm8.2" + worker_count: 4 + worker_max_requests: 500 + +ini: + display_errors: On diff --git a/go.mod b/go.mod index 7835ca6..5708fdd 100644 --- a/go.mod +++ b/go.mod @@ -1,24 +1,43 @@ module github.com/alash3al/phoo -go 1.19 +go 1.21 require ( - github.com/NYTimes/gziphandler v1.1.1 - github.com/joho/godotenv v1.4.0 - github.com/labstack/gommon v0.4.0 - github.com/urfave/cli/v2 v2.23.7 + github.com/labstack/echo/v4 v4.11.1 + github.com/urfave/cli/v2 v2.25.7 + github.com/valyala/fasttemplate v1.2.2 github.com/yookoala/gofast v0.7.0 + gopkg.in/yaml.v3 v3.0.1 ) require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/caarlos0/env/v9 v9.0.0 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/golang-jwt/jwt v3.2.2+incompatible // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/joho/godotenv v1.5.1 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/labstack/echo-contrib v0.15.0 // indirect + github.com/labstack/gommon v0.4.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.16 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/prometheus/client_golang v1.14.0 // indirect + github.com/prometheus/client_model v0.3.0 // indirect + github.com/prometheus/common v0.40.0 // indirect + github.com/prometheus/procfs v0.9.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/stretchr/testify v1.8.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasttemplate v1.2.2 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect - golang.org/x/sys v0.3.0 // indirect - golang.org/x/tools v0.1.12 // indirect + golang.org/x/crypto v0.11.0 // indirect + golang.org/x/net v0.12.0 // indirect + golang.org/x/sys v0.10.0 // indirect + golang.org/x/text v0.11.0 // indirect + golang.org/x/time v0.3.0 // indirect + golang.org/x/tools v0.6.0 // indirect + google.golang.org/protobuf v1.28.1 // indirect ) diff --git a/go.sum b/go.sum index a307e58..f4eea9a 100644 --- a/go.sum +++ b/go.sum @@ -1,42 +1,73 @@ -github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= -github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/caarlos0/env/v9 v9.0.0 h1:SI6JNsOA+y5gj9njpgybykATIylrRMklbs5ch6wO6pc= +github.com/caarlos0/env/v9 v9.0.0/go.mod h1:ye5mlCVMYh6tZ+vCgrs/B95sj88cg5Tlnc0XIzgZ020= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-restit/lzjson v0.0.0-20161206095556-efe3c53acc68/go.mod h1:7vXSKQt83WmbPeyVjCfNT9YDJ5BUFmcwFsEjI9SCvYM= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= -github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/labstack/echo-contrib v0.15.0 h1:9K+oRU265y4Mu9zpRDv3X+DGTqUALY6oRHCSZZKCRVU= +github.com/labstack/echo-contrib v0.15.0/go.mod h1:lei+qt5CLB4oa7VHTE0yEfQSEB9XTJI1LUqko9UWvo4= +github.com/labstack/echo/v4 v4.11.1 h1:dEpLU2FLg4UVmvCGPuk/APjlH6GDpbEPti61srUUUs4= +github.com/labstack/echo/v4 v4.11.1/go.mod h1:YuYRTSM3CHs2ybfrL8Px48bO6BAnYIN4l8wSTMP6BDQ= github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= +github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= +github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= +github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +github.com/prometheus/common v0.40.0 h1:Afz7EVRqGg2Mqqf4JuF9vdvp1pi220m55Pi9T2JnO4Q= +github.com/prometheus/common v0.40.0/go.mod h1:L65ZJPSmfn/UBWLQIHV7dBrKFidB/wPlF1y5TlSt9OE= +github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= +github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v1.1.1/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/urfave/cli/v2 v2.23.7 h1:YHDQ46s3VghFHFf1DdF+Sh7H4RqhcM+t0TmZRJx4oJY= -github.com/urfave/cli/v2 v2.23.7/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= +github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= +github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= @@ -50,11 +81,16 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= +golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= +golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -64,17 +100,28 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= -golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= +golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200908211811-12e1bf57a112/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/ini.v1 v1.38.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internals/fpm/fpm.go b/internals/fpm/fpm.go new file mode 100644 index 0000000..dff9700 --- /dev/null +++ b/internals/fpm/fpm.go @@ -0,0 +1,95 @@ +package fpm + +import ( + _ "embed" + "fmt" + "github.com/valyala/fasttemplate" + "os" + "os/exec" + "os/signal" + "path/filepath" + "syscall" + "time" +) + +//go:embed php-fpm.conf +var configTemplate string + +type Process struct { + BinFilename string + PIDFilename string + ConfigFilename string + SocketFilename string + INI []string + + WorkerCount int + WorkerMaxRequestCount int + WorkerMaxRequestTime int +} + +func (p *Process) Start() error { + paths := []*string{ + &p.ConfigFilename, + &p.SocketFilename, + &p.PIDFilename, + } + + for _, path := range paths { + abs, err := filepath.Abs(*path) + if err != nil { + return err + } + + *path = abs + } + + fpmConfigFileContents := fasttemplate.ExecuteString(configTemplate, "{{", "}}", map[string]any{ + "files.pid": p.PIDFilename, + "files.socket": p.SocketFilename, + "worker.count": fmt.Sprintf("%v", p.WorkerCount), + "worker.request.max_count": fmt.Sprintf("%v", p.WorkerMaxRequestCount), + "worker.request.max_time": fmt.Sprintf("%v", p.WorkerMaxRequestTime), + }) + + if err := os.WriteFile(p.ConfigFilename, []byte(fpmConfigFileContents), 0755); err != nil { + return err + } + + return p.execAndWait() +} + +func (p *Process) execAndWait() error { + cmd := exec.Command(p.BinFilename, "-F", "-O", "-y", p.ConfigFilename) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + for _, v := range p.INI { + cmd.Args = append(cmd.Args, "-d", v) + } + + if err := cmd.Start(); err != nil { + return err + } + + for { + if _, err := os.Stat(p.SocketFilename); err != nil { + time.Sleep(1 * time.Second) + continue + } + + break + } + + sig := make(chan os.Signal) + + signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) + + go func() { + for s := range sig { + _ = cmd.Process.Signal(s) + os.Exit(0) + } + }() + + return nil +} diff --git a/internals/fpm/php-fpm.conf b/internals/fpm/php-fpm.conf new file mode 100644 index 0000000..19751fc --- /dev/null +++ b/internals/fpm/php-fpm.conf @@ -0,0 +1,14 @@ +[global] +pid = "{{files.pid}}" +log_level = "error" +error_log = "/dev/null" +daemonize = no +process_control_timeout = 5s + +[www] +listen = "{{files.socket}}" +pm = static +pm.max_children = {{worker.count}} +pm.max_requests = {{worker.request.max_count}} +request_terminate_timeout = {{worker.request.max_time}} +clear_env = no diff --git a/pkg/fastcgi/client.go b/pkg/fastcgi/client.go deleted file mode 100644 index e294a41..0000000 --- a/pkg/fastcgi/client.go +++ /dev/null @@ -1,87 +0,0 @@ -package fastcgi - -import ( - "errors" - "github.com/yookoala/gofast" - "net/http" - "strings" -) - -type Config struct { - FastCGIServerURL string - DefaultScript string - RestrictDotFilesAccess bool - FastCGIParams map[string]string -} - -type Client struct { - config Config - handler http.Handler - fastCGIServerNetwork string - fastCGIServerAddr string - defaultScriptExtension string -} - -func New(config Config) (*Client, error) { - client := Client{ - config: config, - } - - if err := client.setFastCGIServerDetails(); err != nil { - return nil, err - } - - client.setFastCGIHandler() - - return &client, nil -} - -func (c *Client) setFastCGIServerDetails() error { - urlParts := strings.Split(c.config.FastCGIServerURL, "://") - - if len(urlParts) != 2 { - return errors.New("invalid 'FastCGI Server Address' specified") - } - - c.fastCGIServerNetwork = urlParts[0] - c.fastCGIServerAddr = urlParts[1] - - return nil -} - -func (c *Client) setFastCGIHandler() { - sessionHandler := gofast.Chain( - gofast.MapHeader, - gofast.BasicParamsMap, - gofast.MapRemoteHost, - c.addParams(c.config.FastCGIParams), - )(gofast.BasicSession) - - c.handler = gofast.NewHandler( - gofast.NewFileEndpoint(c.config.DefaultScript)(sessionHandler), - gofast.SimpleClientFactory( - gofast.SimpleConnFactory( - c.fastCGIServerNetwork, - c.fastCGIServerAddr, - ), - ), - ) -} - -func (c *Client) addParams(params map[string]string) gofast.Middleware { - return func(inner gofast.SessionHandler) gofast.SessionHandler { - return func(client gofast.Client, req *gofast.Request) (*gofast.ResponsePipe, error) { - req.KeepConn = true - - for k, v := range params { - req.Params[k] = v - } - - return inner(client, req) - } - } -} - -func (c *Client) ServeHTTP(w http.ResponseWriter, r *http.Request) { - c.handler.ServeHTTP(w, r) -} diff --git a/pkg/fpm/config.ini b/pkg/fpm/config.ini deleted file mode 100644 index 4f4dc1d..0000000 --- a/pkg/fpm/config.ini +++ /dev/null @@ -1,14 +0,0 @@ -[global] -pid = {{.PIDFilename}} -error_log = /dev/null - -[www] -user = {{.User}} -group = {{.Group}} -listen = {{.SocketFilename}} - -pm = static -pm.max_children = {{.WorkersCount}} -pm.max_requests = {{.WorkerMaxRequests}} -request_terminate_timeout = {{.RequestTimeout}} -clear_env = no \ No newline at end of file diff --git a/pkg/fpm/fpm.go b/pkg/fpm/fpm.go deleted file mode 100644 index 89f377a..0000000 --- a/pkg/fpm/fpm.go +++ /dev/null @@ -1,70 +0,0 @@ -package fpm - -import ( - "bytes" - _ "embed" - "io" - "os" - "os/exec" - "strings" - "text/template" -) - -//go:embed config.ini -var configTemplate string - -type Config struct { - PIDFilename string - SocketFilename string - ConfigFilename string - User string - Group string - Bin string - WorkersCount int64 - WorkerMaxRequests int64 - RequestTimeout string - INI []string - Stdout io.Writer - Stderr io.Writer -} - -func (c *Config) Clean() { - _ = os.Remove(c.ConfigFilename) - _ = os.Remove(c.PIDFilename) - _ = os.Remove(c.SocketFilename) -} - -func New(config Config) (*exec.Cmd, error) { - tpl, err := template.New("template").Parse(configTemplate) - if err != nil { - return nil, err - } - - var finalConfig bytes.Buffer - - if err := tpl.Execute(&finalConfig, config); err != nil { - return nil, err - } - - if err := os.WriteFile(config.ConfigFilename, finalConfig.Bytes(), 0755); err != nil { - return nil, err - } - - cmd := exec.Command(config.Bin, "-F", "-O", "-y", config.ConfigFilename) - cmd.Stdout = config.Stdout - cmd.Stderr = config.Stderr - cmd.Env = os.Environ() - - for _, entry := range config.INI { - entry = strings.TrimSpace(entry) - if entry != "" { - cmd.Args = append(cmd.Args, "-d", entry) - } - } - - if err := cmd.Start(); err != nil { - return nil, err - } - - return cmd, nil -} diff --git a/pkg/symbols/sym.go b/pkg/symbols/sym.go deleted file mode 100644 index 2e7e2a7..0000000 --- a/pkg/symbols/sym.go +++ /dev/null @@ -1,31 +0,0 @@ -package symbols - -const ( - AppName = "phoo" - AppVersion = "v2.0.0" - - FlagNameHTTPListenAddr = "listen" - FlagNameDocumentRoot = "root" - FlagNamePHPFPMBinary = "php-fpm" - FlagNamePHPINI = "php-ini" - FlagNamePHPUser = "php-user" - FlagNamePHPGroup = "php-group" - FlagNameWorkersCount = "workers" - FlagNameWorkerMaxRequests = "worker-max-requests" - FlagNameRequestTimeout = "timeout" - FlagNameDefaultScript = "router" - FlagNameEnableLogs = "logs" - FlagNameEnvFilename = "env-file" - - EnvKeyDocumentRoot = "PHOO_DOCUMENT_ROOT" - EnvKeyListen = "PHOO_LISTEN" - EnvKeyFPMBin = "PHOO_FPM_BIN" - EnvKeyWorkersCount = "PHOO_WORKERS_COUNT" - EnvKeyWorkerMaxRequests = "PHOO_WORKER_MAX_REQUESTS" - EnvKeyRequestTimeout = "PHOO_REQUEST_TIMEOUT" - EnvKeyRouter = "PHOO_ROUTER" - EnvKeyEnableLogs = "PHOO_LOGS" - EnvKeyPHPINI = "PHOO_PHP_INI" - EnvKeyPHPUser = "PHOO_PHP_USER" - EnvKeyPHPGroup = "PHOO_PHP_GROUP" -)