diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..e69de29 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 new file mode 100644 index 0000000..b657cc0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.phoo +!.env.example +.idea \ No newline at end of file diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 529c98c..0000000 --- a/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM golang:alpine - -RUN apk update && apk add git - -RUN go get github.com/alash3al/http2fcgi - -ENTRYPOINT ["http2fcgi"] - -WORKDIR /root/ \ No newline at end of file diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 719bcfe..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2018 Mohammed Al Ashaal - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/README.md b/README.md index c916058..8027270 100644 --- a/README.md +++ b/README.md @@ -1,59 +1,60 @@ -HTTP2FCGI -========== -> Quickly serve any `FastCGI` based application with no hassle. - -Quick Overview -============== -> `➜ http2fcgi --root /var/www/laravel-project/public --http :8000` +PHOO +==== +> modern php application server, it depends on the bullet-proof `php-fpm` but it controls how it is being run. -Help? -===== -```bash -➜ http2fcgi -h -Usage of http2fcgi: - -ext comma separated list - the fastcgi file extension(s) comma separated list (default "php") - -fcgi string - the fcgi backend to connect to, you can pass more fcgi related params as query params (default "unix:///var/run/php/php7.0-fpm.sock") - -http string - the http address to listen on (default ":6065") - -index string comma separated list - the default index file (default "index.php,index.html") - -listing - whether to allow directory listing or not - -root string - the document root (default "./") - -router string - the router filename incase of any 404 error (default "index.php") - -rtimeout int - the read timeout, zero means unlimited - -wtimeout int - the write timeout, zero means unlimited +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 ``` -What? -======= -> http2fcgi is a reverse proxy that will convert the standard `http` request to `fcgi` -request so it can served by i.e `php`, `python` ... etc. +#### 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 +``` -Why? -==== -> I wanted a production ready simple and tiny solution to serve some of my `laravel` based projects. +#### 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 +``` -Download -========== -- Using `Docker` `➜ docker run --network=host -v /var/www/site/public:/var/www/site/public -v /var/run/php/php7.0-fpm.sock:/var/run/php/php7.0-fpm.sock alash3al/http2fcgi -root /var/www/site/public -http :8085` +#### 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 +``` -- Using `Go` `➜ go get github.com/alash3al/http2fcgi` +#### 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. -Advanced -========= -> From your app you can ask `http2fcgi` to send a file with any size directly to the browser without any hassle in your app logic, just send a header `X-SendFile: /full/path/to/file` then let `http2fcgi` deal with it. +Requirements +============ +- `php-fpm` +- a shell access to run `phoo` :D -Author -======== -Mohammed Al Ashaal +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! -License -======== -MIT License \ 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 new file mode 100644 index 0000000..539ae4f --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,31 @@ +package main + +import ( + "github.com/alash3al/phoo/cmd/serve" + "github.com/urfave/cli/v2" + "log" + "os" +) +import _ "github.com/joho/godotenv/autoload" + +func main() { + 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.Commands = append(app.Commands, &cli.Command{ + Name: "serve", + Flags: serve.DefaultFlags("PHOO"), + Before: serve.Before(), + Action: serve.Action(), + }) + + if err := app.Run(os.Args); err != nil { + log.Fatal(err.Error()) + } +} 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 new file mode 100644 index 0000000..9d99121 --- /dev/null +++ b/cmd/serve/middlewares.go @@ -0,0 +1,56 @@ +package serve + +import ( + "github.com/labstack/echo-contrib/echoprometheus" + "github.com/labstack/echo/v4" + "github.com/yookoala/gofast" + "os" + "path" + "strings" +) + +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, "/") + + if metricsPath == "" { + return next(c) + } + + if strings.EqualFold(currentPath, metricsPath) { + return echoprometheus.NewHandler()(c) + } + + return next(c) + } + } +} + +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)) + + stat, err := os.Stat(filename) + if err != nil || stat.IsDir() || (ext == "php") || strings.HasPrefix(path.Base(filename), ".") { + return next(c) + } + + return c.File(filename) + } + } +} + +func serveFastCGIMiddleware(routerFilename, fastcgiServerNetwork, fastcgiServerAddr string) echo.MiddlewareFunc { + connFactory := gofast.SimpleConnFactory(fastcgiServerNetwork, fastcgiServerAddr) + + 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 new file mode 100644 index 0000000..ff822a1 --- /dev/null +++ b/cmd/serve/serve.go @@ -0,0 +1,109 @@ +package serve + +import ( + "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" + "os" + "path/filepath" + "time" +) + +var fpmProcess *fpm.Process + +func Before() cli.BeforeFunc { + return func(ctx *cli.Context) error { + if err := os.RemoveAll(ctx.String("data")); err != nil { + return err + } + + if err := os.MkdirAll(ctx.String("data"), 0755); err != nil { + return err + } + + 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"), + } + + 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 + } + } + + 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"), + } + + return fpmProcess.Start() + } +} + +func Action() cli.ActionFunc { + return func(ctx *cli.Context) error { + e := echo.New() + e.HideBanner = true + + if ctx.Bool("logs") { + e.Use(middleware.Logger()) + } + + 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 new file mode 100644 index 0000000..5708fdd --- /dev/null +++ b/go.mod @@ -0,0 +1,43 @@ +module github.com/alash3al/phoo + +go 1.21 + +require ( + 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.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/valyala/bytebufferpool v1.0.0 // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // 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 new file mode 100644 index 0000000..f4eea9a --- /dev/null +++ b/go.sum @@ -0,0 +1,130 @@ +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.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/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/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.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +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= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +github.com/yookoala/gofast v0.7.0 h1:wVqXc+S0FDmlkieRNDxabGRX44znHT++Hb9lEfWi4iM= +github.com/yookoala/gofast v0.7.0/go.mod h1:OJU201Q6HCaE1cASckaTbMm3KB6e0cZxK0mgqfwOKvQ= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +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= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +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.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.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= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/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/main.go b/main.go deleted file mode 100644 index a4184bd..0000000 --- a/main.go +++ /dev/null @@ -1,322 +0,0 @@ -package main - -import ( - "errors" - "flag" - "fmt" - "io" - "log" - "net" - "net/http" - "net/url" - "os" - "path/filepath" - "strings" - "time" - - "github.com/alash3al/go-fastcgi-client" -) - -var ( - // FlagHTTPAddr . - FlagHTTPAddr = flag.String("http", ":6065", "the http address to listen on") - - // FlagDocRoot . - FlagDocRoot = flag.String("root", "./", "the document root") - - // FlagFCGIBackend . - FlagFCGIBackend = flag.String("fcgi", "unix:///var/run/php/php7.0-fpm.sock", "the fcgi backend to connect to, you can pass more fcgi related params as query params") - - // FlagIndex . - FlagIndex = flag.String("index", "index.php,index.html", "the default index file `comma separated list`") - - // FlagRouter . - FlagRouter = flag.String("router", "index.php", "the router filename incase of any 404 error") - - // FlagAllowListing . - FlagAllowListing = flag.Bool("listing", false, "whether to allow directory listing or not") - - // FlagFCGIExt . - FlagFCGIExt = flag.String("ext", "php", "the fastcgi file extension(s) `comma separated list`") - - // FlagReadTimeout . - FlagReadTimeout = flag.Int("rtimeout", 0, "the read timeout, zero means unlimited") - - // FlagWriteTimeout . - FlagWriteTimeout = flag.Int("wtimeout", 0, "the write timeout, zero means unlimited") -) - -var ( - // FCGIBackendConfig . - FCGIBackendConfig *BackendConfig -) - -// BackendConfig the backend configurations i.e 'ext' or any other fcgi params -type BackendConfig struct { - Network string - Address string - Ext []string - Params map[string]string -} - -func main() { - flag.Parse() - - fmt.Println("⇨ checking the fcgi backend ...") - cnf, err := GetBackendConfig(*FlagFCGIBackend) - - if err != nil { - log.Fatal(err) - } - - FCGIBackendConfig = cnf - - fmt.Printf("⇨ http server started on %s\n", *FlagHTTPAddr) - log.Fatal(http.ListenAndServe(*FlagHTTPAddr, http.HandlerFunc(Serve))) -} - -// Serve the main http handler -func Serve(res http.ResponseWriter, req *http.Request) { - defer req.Body.Close() - - defer func() { - if err := recover(); err != nil { - res.WriteHeader(500) - res.Write([]byte("ForwarderError, please see the logs")) - log.Println(err) - } - }() - - filename := filepath.Join(*FlagDocRoot, req.URL.Path) - scriptname := req.URL.Path - isFCGI := false - tryIndex := false - dir := *FlagDocRoot - - if !IsValidFile(filename) { - tryIndex = true - } else if IsValidDir(filename) { - tryIndex, dir = true, filename - } else if IsValidFCGIExt(filepath.Ext(filename)) { - isFCGI = true - } - - if tryIndex { - for _, v := range strings.Split(*FlagIndex, ",") { - if f := filepath.Join(dir, v); IsValidFile(f) { - filename = filepath.Join(dir, v) - scriptname = filepath.Join("/", v) - if IsValidFCGIExt(filepath.Ext(filename)) { - isFCGI = true - } - break - } else { - filename = filepath.Join(dir, *FlagRouter) - scriptname = filepath.Join("/", *FlagRouter) - isFCGI = true - } - } - } - - fullfilename, _ := filepath.Abs(filename) - if fullfilename == "" { - fullfilename = filename - } - - if !isFCGI && IsValidDir(fullfilename) && !*FlagAllowListing { - http.Error(res, "DirectoryListing isn't allowed!", 403) - return - } - - if !isFCGI { - http.ServeFile(res, req, fullfilename) - return - } - - if !IsValidFile(fullfilename) { - res.WriteHeader(404) - res.Write([]byte("Cannot find the requested resource :(")) - return - } - - pathInfo := req.URL.Path - if strings.Contains(pathInfo, *FlagRouter) { - parts := strings.Split(pathInfo, *FlagRouter) - if len(parts) < 2 { - parts = append(parts, "/") - } - pathInfo = filepath.Join("/", parts[1]) - } - - host, port, _ := net.SplitHostPort(req.RemoteAddr) - params := map[string]string{ - "SERVER_SOFTWARE": "http2fcgi", - "SERVER_PROTOCOL": req.Proto, - "REQUEST_METHOD": req.Method, - "REQUEST_TIME": fmt.Sprintf("%d", time.Now().Unix()), - "REQUEST_TIME_FLOAT": fmt.Sprintf("%d", time.Now().UnixNano()/int64(time.Microsecond)), - "QUERY_STRING": req.URL.RawQuery, - "DOCUMENT_ROOT": fullfilename, - "REMOTE_ADDR": host, - "REMOTE_PORT": port, - "SCRIPT_FILENAME": fullfilename, - "PATH_TRANSLATED": fullfilename, - "SCRIPT_NAME": scriptname, - "REQUEST_URI": req.URL.RequestURI(), - "AUTH_DIGEST": req.Header.Get("Authorization"), - "PATH_INFO": pathInfo, - "ORIG_PATH_INFO": pathInfo, - } - - for k, v := range req.Header { - if len(v) < 1 { - continue - } - k = strings.ToUpper(fmt.Sprintf("HTTP_%s", strings.Replace(k, "-", "_", -1))) - params[k] = strings.Join(v, ";") - } - - c, e := fcgiclient.Dial(FCGIBackendConfig.Network, FCGIBackendConfig.Address) - if c == nil { - res.WriteHeader(500) - res.Write([]byte(e.Error())) - return - } - defer c.Close() - - c.SetReadTimeout(time.Duration(*FlagReadTimeout) * time.Second) - c.SetSendTimeout(time.Duration(*FlagWriteTimeout) * time.Second) - - resp, err := c.Request(params, req.Body) - if resp == nil || resp.Body == nil || err != nil { - res.WriteHeader(500) - res.Write([]byte(err.Error())) - return - } - defer resp.Body.Close() - - for k, vals := range resp.Header { - for _, v := range vals { - res.Header().Add(k, v) - } - } - - res.Header().Set("Server", "http2fcgi") - - if res.Header().Get("X-SendFile") != "" && IsValidFile(res.Header().Get("X-SendFile")) { - sendFilename := res.Header().Get("X-SendFile") - res.Header().Del("X-SendFile") - http.ServeFile(res, req, sendFilename) - return - } - - if resp.ContentLength > 0 { - res.Header().Set("Content-Length", fmt.Sprintf("%d", resp.ContentLength)) - } - - res.WriteHeader(resp.StatusCode) - - n, _ := io.Copy(res, resp.Body) - if n < 1 { - stderr := c.Stderr() - stderr.WriteTo(res) - } -} - -// GetBackendConfig returns the configs of the fcgi backend -func GetBackendConfig(backend string) (cnf *BackendConfig, err error) { - var u *url.URL - - u, err = url.Parse(backend) - if err != nil { - return nil, err - } - - cnf = &BackendConfig{} - cnf.Params = map[string]string{} - cnf.Ext = []string{} - - if ext := strings.ToLower(*FlagFCGIExt); ext != "" { - cnf.Ext = strings.Split(ext, ",") - } else { - return nil, errors.New("You should specifiy the fastcgi script extension i,e '?ext=php'") - } - - u.Scheme = strings.ToLower(u.Scheme) - - if u.Scheme == "" && u.Host == "" && u.Path != "" { - cnf.Network, cnf.Address = "unix", u.Path - } - if u.Scheme == "" && u.Host != "" && u.Path == "" { - cnf.Network, cnf.Address = "tcp", u.Host - } - if u.Scheme == "unix" && u.Path != "" { - cnf.Network, cnf.Address = "unix", u.Path - } - if u.Scheme == "tcp" && u.Host != "" { - cnf.Network, cnf.Address = "tcp", u.Host - } - - for k, v := range u.Query() { - if len(v) < 1 { - v = []string{""} - } - cnf.Params[k] = v[0] - } - - if cnf.Network == "" || cnf.Address == "" { - return nil, errors.New("Invalid fastcgi address (" + backend + ") specified `malformed`") - } - - if cnf.Network == "unix" && !IsValidFile(cnf.Address) { - return nil, errors.New("Invalid fastcgi address (" + backend + ") specified `invalid filename`") - } - - if cnf.Network == "tcp" && !IsValidHost(cnf.Address) { - return nil, errors.New("Invalid fastcgi address (" + backend + ") specified `invalid host`") - } - - return cnf, nil -} - -// IsValidFile whether the specified filename is valid -func IsValidFile(filename string) bool { - if filename == "" { - return false - } - _, err := os.Stat(filename) - return err == nil -} - -// IsValidDir whther the specified directory is valid or not -func IsValidDir(filename string) bool { - stat, err := os.Stat(filename) - if err != nil { - return false - } - return stat.IsDir() -} - -// IsValidHost whether the specified host is online or not -func IsValidHost(host string) bool { - if host == "" { - return false - } - timeout := time.Duration(2 * time.Second) - conn, err := net.DialTimeout("tcp", host, timeout) - if err != nil { - return false - } - conn.Close() - return true -} - -// IsValidFCGIExt wehther the specified extension is valid fcgi or not -func IsValidFCGIExt(ext string) bool { - for _, x := range FCGIBackendConfig.Ext { - if ext == "."+x { - return true - } - } - return false -}