├── LICENSE ├── README.md ├── xerver.go └── xerver.php /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Mohammed Al Ashaal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | xerver v3.0 2 | ============ 3 | A transparent blazing fast fastcgi reverse proxy . 4 | 5 | Features 6 | ============ 7 | * Cross platform . 8 | * Accelerated and optimized without modules hell. 9 | * No configurations needed . 10 | * Standalone, Tiny & Lightweight . 11 | * Supports both http and https . 12 | * Automatically use HTTP/2 "in https" . 13 | * Control the whole webserver just with your preferred programming language . 14 | * Tell xerver to perform some operations using http-headers, i.e "send-file, proxy-pass, ... etc" . 15 | * More is coming, just stay tuned . 16 | 17 | How It Works 18 | ============= 19 | * A request hits the `xerver` . 20 | * `xerver` handles the request . 21 | * `xserver` send it to the backend `fastcgi` process and the main controller file . 22 | * the controller file contains your own logic . 23 | * `fastcgi` process reply to `xerver` with the result . 24 | * `xerver` parse the result and then prepare it to be sent to the client . 25 | 26 | Building from source 27 | ================== 28 | 1- make sure you have `Golang` installed . 29 | 2- `go get -u github.com/alash3al/xerver` 30 | 3- `go install github.com/alash3al/xerver` 31 | 4- make sure `$GOPATH` in your `$PATH` env var . 32 | 5- `xerver --help` 33 | 34 | Example (1) 35 | ============== 36 | **Only acts as a static file server by default on port 80** 37 | ```bash 38 | xerver --root=/path/to/www/ --http=:80 39 | ``` 40 | 41 | Example (2) 42 | ============== 43 | **Listen on address `0.0.0.0:80`** and send the requests to `./controller.php` 44 | ```bash 45 | xerver --backend=unix:/var/run/php5-fpm.sock controller=./controller.php --http=:80 46 | ``` 47 | ** OR Listen on address `0.0.0.0:80` & ``0.0.0.0:443`` ** and send the requests to `./controller.php` 48 | ```bash 49 | xerver --backend=unix:/var/run/php5-fpm.sock controller=./controller.php --http=:80 --https=:443 --cert=./cert.pem --key=./key.pem 50 | ``` 51 | 52 | **Open your ./controller.php** and : 53 | ```php 54 | " . print_r($_SERVER, 1); 60 | 61 | // some xerver internal header for some operations 62 | // 1)- tell xerver to serve a file/directory to the client . 63 | // header("Xerver-Internal-FileServer: " . __DIR__ . "/style.css"); 64 | 65 | // 2)- tell xerver to serve from another server "act as reverse proxy" . 66 | // header("Xerver-Internal-ProxyPass: http://localhost:8080/"); 67 | 68 | // 3)- tell xerver to hide its own tokens "A.K.A 'Server' header" 69 | // header("Xerver-Internal-ServerTokens: off"); 70 | 71 | // the above headers won't be sent to the client . 72 | ``` 73 | 74 | **Open your browser** and go to `localhost` or any `localhost` paths/subdomains . 75 | **You can use `xerver.php` as a production ready controller file** 76 | 77 | Author 78 | ================== 79 | By [Mohammed Al Ashaal](http://www.alash3al.xyz) 80 | -------------------------------------------------------------------------------- /xerver.go: -------------------------------------------------------------------------------- 1 | // xerver 3.0, a tiny and light transparent fastcgi reverse proxy, 2 | // copyright 2016, (c) Mohammed Al Ashaal , 3 | // published uner MIT licnese . 4 | // ----------------------------- 5 | // *> available options 6 | // >> --root [only use xerver as static file server], i.e "/var/www/" . 7 | // >> --backend [only use xerver as fastcgi reverse proxy], i.e "[unix|tcp]:/var/run/php5-fpm.sock" . 8 | // >> --controller [the fastcgi process main file "SCRIPT_FILENAME"], i.e "/var/www/main.php" 9 | // >> --http [the local http address to listen on], i.e ":80" 10 | // >> --https [the local https address to listen on], i.e ":443" 11 | // >> --cert [the ssl cert file path], i.e "/var/ssl/ssl.cert" 12 | // >> --key [the ssl key file path], i.e "/var/ssl/ssl.key" 13 | // *> available internals 14 | // >> Xerver-Internal-ServerTokens [off|on] 15 | // >> Xerver-Internal-FileServer [file|directory] 16 | // >> Xerver-Internal-ProxyPass [transparent-http-proxy] 17 | package main 18 | 19 | import "os" 20 | import "io" 21 | import "fmt" 22 | import "log" 23 | import "net" 24 | import "flag" 25 | import "strconv" 26 | import "strings" 27 | import "net/url" 28 | import "net/http" 29 | import "net/http/httputil" 30 | import "github.com/tomasen/fcgi_client" 31 | 32 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 33 | 34 | // user vars 35 | var ( 36 | ROOT *string = flag.String("root", "", "the static files root directory, (default empty)") 37 | BACKEND *string = flag.String("backend", "", "the fastcgi backend address, (default empty)") 38 | CONTROLLER *string = flag.String("controller", "", "the fastcgi main controller file, (default empty)") 39 | HTTP *string = flag.String("http", ":80", "the http-server local address") 40 | HTTPS *string = flag.String("https", "", "the https-server local address, (default empty)") 41 | CERT *string = flag.String("cert", "", "the ssl cert file, (default empty)") 42 | KEY *string = flag.String("key", "", "the ssl key file, (default empty)") 43 | ) 44 | 45 | // system vars 46 | var ( 47 | VERSION string = "xerver/v3.0" 48 | FCGI_PROTO string = "" 49 | FCGI_ADDR string = "" 50 | ) 51 | 52 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 53 | 54 | func ServeFCGI(res http.ResponseWriter, req *http.Request) { 55 | // connect to the fastcgi backend, 56 | // and check whether there is an error or not . 57 | fcgi, err := fcgiclient.Dial(FCGI_PROTO, FCGI_ADDR) 58 | if err != nil { 59 | log.Println(err) 60 | http.Error(res, "Unable to connect to the backend", 502) 61 | return 62 | } 63 | // automatically close the fastcgi connection and the requested body at the end . 64 | defer fcgi.Close() 65 | defer req.Body.Close() 66 | // prepare some vars : 67 | // -- http[addr, port] 68 | // -- https[addr, port] 69 | // -- remote[addr, host, port] 70 | // -- edit the request path 71 | // -- environment variables 72 | http_addr, http_port, _ := net.SplitHostPort(*HTTP) 73 | https_addr, https_port, _ := net.SplitHostPort(*HTTPS) 74 | remote_addr, remote_port, _ := net.SplitHostPort(req.RemoteAddr) 75 | req.URL.Path = req.URL.ResolveReference(req.URL).Path 76 | env := map[string]string { 77 | "SCRIPT_FILENAME" : *CONTROLLER, 78 | "REQUEST_METHOD" : req.Method, 79 | "REQUEST_URI" : req.URL.RequestURI(), 80 | "REQUEST_PATH" : req.URL.Path, 81 | "PATH_INFO" : req.URL.Path, 82 | "CONTENT_LENGTH" : fmt.Sprintf("%d", req.ContentLength), 83 | "CONTENT_TYPE" : req.Header.Get("Content-Type"), 84 | "REMOTE_ADDR" : remote_addr, 85 | "REMOTE_PORT" : remote_port, 86 | "REMOTE_HOST" : remote_addr, 87 | "QUERY_STRING" : req.URL.Query().Encode(), 88 | "SERVER_SOFTWARE" : VERSION, 89 | "SERVER_NAME" : req.Host, 90 | "SERVER_ADDR" : http_addr, 91 | "SERVER_PORT" : http_port, 92 | "SERVER_PROTOCOL" : req.Proto, 93 | "FCGI_PROTOCOL" : FCGI_PROTO, 94 | "FCGI_ADDR" : FCGI_ADDR, 95 | "HTTPS" : "", 96 | "HTTP_HOST" : req.Host, 97 | } 98 | // tell fastcgi backend that, this connection is done over https connection if enabled . 99 | if req.TLS != nil { 100 | env["HTTPS"] = "on" 101 | env["SERVER_PORT"] = https_port 102 | env["SERVER_ADDR"] = https_addr 103 | env["SSL_CERT"] = *CERT 104 | env["SSL_KEY"] = *KEY 105 | } 106 | // iterate over request headers and append them to the environment varibales in the valid format . 107 | for k, v := range req.Header { 108 | env["HTTP_" + strings.Replace(strings.ToUpper(k), "-", "_", -1)] = strings.Join(v, ";") 109 | } 110 | // fethcing the response from the fastcgi backend, 111 | // and check for errors . 112 | resp, err := fcgi.Request(env, req.Body) 113 | if err != nil { 114 | log.Println("err> ", err.Error()) 115 | http.Error(res, "Unable to fetch the response from the backend", 502) 116 | return 117 | } 118 | // parse the fastcgi status . 119 | resp.Status = resp.Header.Get("Status") 120 | resp.StatusCode, _ = strconv.Atoi(strings.Split(resp.Status, " ")[0]) 121 | if resp.StatusCode < 100 { 122 | resp.StatusCode = 200 123 | } 124 | // automatically close the fastcgi response body at the end . 125 | defer resp.Body.Close() 126 | // read the fastcgi response headers, 127 | // exclude "Xerver-Internal-*" headers from the response, 128 | // and apply the actions related to them . 129 | for k, v := range resp.Header { 130 | if ! strings.HasPrefix(k, "Xerver-Internal-") { 131 | for i := 0; i < len(v); i ++ { 132 | if res.Header().Get(k) == "" { 133 | res.Header().Set(k, v[i]) 134 | } else { 135 | res.Header().Add(k, v[i]) 136 | } 137 | } 138 | } 139 | } 140 | // remove server tokens from the response 141 | if resp.Header.Get("Xerver-Internal-ServerTokens") != "off" { 142 | res.Header().Set("Server", VERSION) 143 | } 144 | // serve the provided filepath using theinternal fileserver 145 | if resp.Header.Get("Xerver-Internal-FileServer") != "" { 146 | res.Header().Del("Content-Type") 147 | http.ServeFile(res, req, resp.Header.Get("Xerver-Internal-FileServer")) 148 | return 149 | } 150 | // serve the response from another backend "http-proxy" 151 | if resp.Header.Get("Xerver-Internal-ProxyPass") != "" { 152 | u, e := url.Parse(resp.Header.Get("Xerver-Internal-ProxyPass")) 153 | if e != nil { 154 | log.Println("err> ", e.Error()) 155 | http.Error(res, "Invalid internal-proxypass value", 502) 156 | return 157 | } 158 | httputil.NewSingleHostReverseProxy(u).ServeHTTP(res, req) 159 | return 160 | } 161 | // fix the redirect issues by fetching the fastcgi response location header 162 | // then redirect the client, then ignore any output . 163 | if resp.Header.Get("Location") != "" { 164 | http.Redirect(res, req, resp.Header.Get("Location"), resp.StatusCode) 165 | return 166 | } 167 | // write the response status code . 168 | res.WriteHeader(resp.StatusCode) 169 | // only sent the header if the request method isn't HEAD . 170 | if req.Method != "HEAD" { 171 | io.Copy(res, resp.Body) 172 | } 173 | } 174 | 175 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 176 | 177 | // - parse the cmd flags 178 | // - check for the required flags 179 | // - display welcome messages 180 | func init() { 181 | flag.Parse() 182 | if (*ROOT == "" && *BACKEND == "") || (*ROOT != "" && *BACKEND != "") { 183 | log.Fatal("Please, choose whether you want me as transparent static-server or reverse-proxy ?") 184 | } 185 | if *ROOT != "" { 186 | if _, e := os.Stat(*ROOT); e != nil { 187 | log.Fatal(e) 188 | } 189 | } 190 | if *BACKEND != "" { 191 | parts := strings.SplitN(*BACKEND, ":", 2) 192 | if ! strings.Contains(*BACKEND, ":") || len(parts) < 2 { 193 | log.Fatal("Please, provide a valid backend format [protocol:address]") 194 | } 195 | FCGI_PROTO = parts[0] 196 | FCGI_ADDR = parts[1] 197 | if _, e := os.Stat(*CONTROLLER); e != nil { 198 | log.Fatal(e) 199 | } 200 | } 201 | if strings.HasPrefix(*HTTP, ":") { 202 | *HTTP = "0.0.0.0" + *HTTP 203 | } 204 | if strings.HasPrefix(*HTTPS, ":") { 205 | *HTTPS = "0.0.0.0" + *HTTPS 206 | } 207 | fmt.Println("Welcome to ", VERSION) 208 | fmt.Println("Backend: ", *BACKEND) 209 | fmt.Println("CONTROLLER: ", *CONTROLLER) 210 | fmt.Println("HTTP Address: ", *HTTP) 211 | fmt.Println("ROOT: ", *ROOT) 212 | fmt.Println("HTTPS Address: ", *HTTPS) 213 | fmt.Println("SSL Cert: ", *CERT) 214 | fmt.Println("SSL Key: ", *KEY) 215 | fmt.Println("") 216 | } 217 | 218 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 219 | 220 | // let's play :) 221 | func main() { 222 | // handle any panic 223 | rcvr := func(){ 224 | if err := recover(); err != nil { 225 | log.Println("err> ", err) 226 | } 227 | } 228 | // the handler 229 | handler := func(res http.ResponseWriter, req *http.Request) { 230 | if *ROOT == "" { 231 | ServeFCGI(res, req) 232 | return 233 | } 234 | http.FileServer(http.Dir(*ROOT)).ServeHTTP(res, req) 235 | } 236 | // an error channel to catch any error 237 | err := make(chan error) 238 | // run a http server in a goroutine 239 | go (func(){ 240 | defer rcvr() 241 | err <- http.ListenAndServe(*HTTP, http.HandlerFunc(handler)) 242 | })() 243 | // run a https server in another goroutine 244 | go (func(){ 245 | if *HTTPS != "" && *CERT != "" && *KEY != "" { 246 | defer rcvr() 247 | err <- http.ListenAndServeTLS(*HTTPS, *CERT, *KEY, http.HandlerFunc(handler)) 248 | } 249 | })() 250 | // there is an error occurred, 251 | // let's catch it, then exit . 252 | log.Fatal(<- err) 253 | } 254 | -------------------------------------------------------------------------------- /xerver.php: -------------------------------------------------------------------------------- 1 | ["index.php", "index.html"], 20 | "directory_listing" => true, 21 | "e404" => function(){ 22 | exit("404 not found"); 23 | } 24 | ], $options); 25 | // set the current working directory 26 | $_SERVER["DOCUMENT_ROOT"] = $root; 27 | // get the requested file extension 28 | $info = pathinfo($_SERVER["REQUEST_PATH"]); 29 | if ( ! isset($info["extension"]) ) { 30 | $info = ["extension" => ""]; 31 | } 32 | $ext = $info["extension"]; 33 | // prepare the request path 34 | $required = rtrim(str_replace(["/", "\\"], DIRECTORY_SEPARATOR, $root), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . trim($_SERVER["REQUEST_PATH"], "/"); 35 | // 1)- a directory ? 36 | if ( is_dir($required) ) { 37 | if ( $_SERVER["REQUEST_PATH"][strlen($_SERVER["REQUEST_PATH"]) - 1] != "/" && $_SERVER["REQUEST_PATH"] != "/"&& $_SERVER["REQUEST_PATH"] != "" ) { 38 | header("Location: " . $_SERVER["REQUEST_PATH"] . "/"); 39 | exit; 40 | } 41 | chdir($required); 42 | $_SERVER["REQUEST_URI"] = rtrim($_SERVER["REQUEST_PATH"], "/") . "/" . ($_SERVER["QUERY_STRING"] ? ("?" . $_SERVER["QUERY_STRING"]) : ""); 43 | $_SERVER["REQUEST_PATH"] = $_SERVER["PATH_INFO"] .= "/"; 44 | foreach ( $options["index"] as $i ) { 45 | if ( is_file($filename = $required . "/" . $i) ) { 46 | $_SERVER["SCRIPT_FILENAME"] = $filename; 47 | $_SERVER["SCRIPT_NAME"] = $_SERVER["PHP_SELF"] = rtrim($_SERVER["REQUEST_PATH"], "/") . "/" . $i; 48 | ksort($_SERVER); 49 | require($filename); 50 | exit; 51 | } 52 | } 53 | if ( $options["directory_listing"] ) { 54 | header("Xerver-Internal-FileServer: " . $required); 55 | exit; 56 | } 57 | exit("You don't have permissions to view this directory"); 58 | } 59 | // 2)- is file ? 60 | else if ( is_file($required) ) { 61 | chdir(dirname($required)); 62 | $_SERVER["SCRIPT_FILENAME"] = $required; 63 | $_SERVER["SCRIPT_NAME"] = $_SERVER["PHP_SELF"] = $_SERVER["REQUEST_PATH"]; 64 | ksort($_SERVER); 65 | if ( $ext != "php" ) { 66 | header("Xerver-Internal-FileServer: " . $required); 67 | exit; 68 | } 69 | require($required); 70 | exit; 71 | } 72 | // 3)- not found ? 73 | else { 74 | $options["e404"](); 75 | } 76 | } 77 | 78 | // path rewriter 79 | function rewrite($old, $new) { 80 | $_SERVER["REQUEST_PATH"] = "/" . ltrim(preg_replace("~{$old}~", $new, $_SERVER["REQUEST_PATH"]), "/"); 81 | $_SERVER["PATH_INFO"] = $_SERVER["REQUEST_PATH"]; 82 | } 83 | 84 | // path router 85 | function on(string $pattern, Closure $fn) { 86 | $path = preg_replace("~/+~", "/", "/" . $_SERVER["PATH_INFO"] . "/"); 87 | $pattern = preg_replace("~/+~", "/", "/" . $pattern . "/"); 88 | if ( preg_match("~^{$pattern}$~", $path, $m) ) { 89 | array_shift($m); 90 | call_user_func_array($fn, $m); 91 | } 92 | } 93 | 94 | // vhost router 95 | function vhost($pattern, Closure $fn) { 96 | $host = explode(":", $_SERVER["HTTP_HOST"]); 97 | if ( ! isset($host[0]) ) { 98 | $host = "localhost"; 99 | } else { 100 | $host = $host[0]; 101 | } 102 | if ( preg_match("~^{$pattern}$~i", $host, $m) ) { 103 | array_shift($m); 104 | call_user_func_array($fn, $m); 105 | } 106 | } 107 | --------------------------------------------------------------------------------