├── .gitignore ├── .goreleaser.yml ├── LICENSE ├── README.md ├── go.mod └── spark.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | build 24 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # .goreleaser.yml 2 | # Build customization 3 | builds: 4 | - binary: spark 5 | goos: 6 | - windows 7 | - darwin 8 | - linux 9 | - freebsd 10 | goarch: 11 | - amd64 12 | - 386 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Radu Ioan Fericean 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # spark 2 | 3 | Emergency web server 4 | 5 | For those occasions when your webserver is down and you want to display a quick maintainance note. Or just want to quickly demo a static site. Or whatever :) 6 | 7 | It can take a directory, a file or directly the body string. The `-proxy` flag can be useful when used as a development server. 8 | 9 | 10 | ``` 11 | ❯ spark -h 12 | Usage of spark: 13 | -address string 14 | Listening address (default "0.0.0.0") 15 | -cert string 16 | SSL certificate path (default "cert.pem") 17 | -contentType string 18 | Set response Content-Type 19 | -corsHeaders string 20 | Allowed CORS headers (default "Content-Type, Authorization, X-Requested-With") 21 | -corsMethods string 22 | Allowd CORS methods (default "POST, GET, OPTIONS, PUT, DELETE") 23 | -corsOrigin string 24 | Allow CORS request from this origin (can be '*') 25 | -deny string 26 | Sensitive directory or file patterns to be denied when serving directory (comma separated) 27 | -key string 28 | SSL private Key path (default "key.pem") 29 | -path string 30 | URL path (default "/") 31 | -port string 32 | Listening port (default "8080") 33 | -proxy string 34 | URL prefixes to be proxied to another server e.g. /api=>http://localhost:3000 will forward all requests starting with /api to http://localhost:3000 (comma separated) 35 | -sslPort string 36 | SSL listening port (default "10433") 37 | -status int 38 | Returned HTTP status code (default 200) 39 | ``` 40 | 41 | ## install 42 | - from source 43 | ``` 44 | go get github.com/rif/spark 45 | ``` 46 | - static binaries (linux/arm/osx/windows): 47 | Binary downloads 48 | 49 | ## examples 50 | 51 | ``` 52 | $ spark message.html 53 | $ spark "

Out of order

Working on it...

" 54 | $ spark static_site/ 55 | $ spark -port 80 -sslPort 443 "

Ooops!

" 56 | $ spark -deny ".git*,LICENSE" ~/go/rif/spark 57 | $ spark -proxy "/api=>http://localhost:9090/api" . 58 | $ spark -port 9000 -corsOrigin "https://www.mydomain.com" -contentType "application/json" '{"message":"Hello"}' 59 | ``` 60 | 61 | To quickly generate a ssl certificate run: 62 | 63 | ``` 64 | go run $GOROOT/src/crypto/tls/generate_cert.go --host="localhost" 65 | ``` 66 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/rif/spark 2 | 3 | go 1.22 4 | -------------------------------------------------------------------------------- /spark.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "io" 6 | "log" 7 | "net/http" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | var ( 15 | address = flag.String("address", "0.0.0.0", "Listening address") 16 | port = flag.String("port", "8080", "Listening port") 17 | sslPort = flag.String("sslPort", "10433", "SSL listening port") 18 | path = flag.String("path", "/", "URL path") 19 | deny = flag.String("deny", "", "Sensitive directory or file patterns to be denied when serving directory (comma separated)") 20 | status = flag.Int("status", 200, "Returned HTTP status code") 21 | cert = flag.String("cert", "cert.pem", "SSL certificate path") 22 | key = flag.String("key", "key.pem", "SSL private Key path") 23 | proxy = flag.String("proxy", "", "URL prefixes to be proxied to another server e.g. /api=>http://localhost:3000 will forward all requests starting with /api to http://localhost:3000 (comma separated)") 24 | corsOrigin = flag.String("corsOrigin", "", "Allow CORS requests from this origin (can be '*')") 25 | corsMethods = flag.String("corsMethods", "POST, GET, OPTIONS, PUT, DELETE", "Allowd CORS methods") 26 | corsHeaders = flag.String("corsHeaders", "Content-Type, Authorization, X-Requested-With", "Allowed CORS headers") 27 | contentType = flag.String("contentType", "", "Set response Content-Type") 28 | ) 29 | 30 | type bytesHandler []byte 31 | 32 | func (h bytesHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { 33 | w.WriteHeader(*status) 34 | w.Write(h) 35 | } 36 | 37 | type proxyHandler struct { 38 | prefix string 39 | proxyURL string 40 | } 41 | 42 | func middleware(next http.Handler) http.Handler { 43 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 44 | if *contentType != "" { 45 | w.Header().Set("Content-Type", *contentType) 46 | } 47 | if corsOrigin != nil && *corsOrigin != "" { 48 | // Set CORS headers 49 | w.Header().Set("Access-Control-Allow-Origin", *corsOrigin) 50 | w.Header().Set("Access-Control-Allow-Methods", *corsMethods) 51 | w.Header().Set("Access-Control-Allow-Headers", *corsHeaders) 52 | 53 | // Check if the request is a preflight request and handle it. 54 | if r.Method == "OPTIONS" { 55 | w.WriteHeader(http.StatusOK) 56 | return 57 | } 58 | } 59 | 60 | // Call the next handler, which can be another middleware in the chain, or the final handler. 61 | next.ServeHTTP(w, r) 62 | }) 63 | } 64 | 65 | func (ph *proxyHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { 66 | client := &http.Client{ 67 | Timeout: 5 * time.Second, 68 | } 69 | request, err := http.NewRequest(req.Method, ph.proxyURL+strings.TrimLeft(req.URL.String(), ph.prefix), req.Body) 70 | if err != nil { 71 | log.Println(err) 72 | } 73 | request.Header = req.Header 74 | 75 | response, err := client.Do(request) 76 | if err != nil { 77 | log.Print("error response form proxy: ", err) 78 | } else { 79 | if _, err := io.Copy(w, response.Body); err != nil { 80 | log.Print("error copyting the response from proxy: ", err) 81 | } 82 | } 83 | } 84 | 85 | func isDenied(path, denyList string) bool { 86 | if len(denyList) == 0 { 87 | return false 88 | } 89 | for _, pathElement := range strings.Split(path, string(filepath.Separator)) { 90 | for _, denyElement := range strings.Split(denyList, ",") { 91 | match, err := filepath.Match(strings.TrimSpace(denyElement), pathElement) 92 | if err != nil { 93 | log.Print("error matching file path element: ", err) 94 | } 95 | if match { 96 | return true 97 | } 98 | } 99 | } 100 | return false 101 | } 102 | 103 | func parseProxy(flagStr string) (handlers []*proxyHandler) { 104 | proxyList := strings.Split(flagStr, ",") 105 | for _, proxyRedirect := range proxyList { 106 | proxyElements := strings.Split(proxyRedirect, "=>") 107 | if len(proxyElements) == 2 { 108 | prefix := strings.TrimSpace(proxyElements[0]) 109 | proxyURL := strings.TrimSpace(proxyElements[1]) 110 | if strings.HasPrefix(prefix, "/") && strings.HasPrefix(proxyURL, "http") { 111 | handlers = append(handlers, &proxyHandler{ 112 | prefix: prefix, 113 | proxyURL: proxyURL, 114 | }) 115 | } else { 116 | log.Printf("bad proxy pair: %s=>%s", prefix, proxyURL) 117 | } 118 | } 119 | } 120 | return 121 | } 122 | 123 | type protectdFileSystem struct { 124 | fs http.FileSystem 125 | } 126 | 127 | func (pfs protectdFileSystem) Open(path string) (http.File, error) { 128 | if isDenied(path, *deny) { 129 | return nil, os.ErrPermission 130 | } 131 | return pfs.fs.Open(path) 132 | } 133 | 134 | func main() { 135 | flag.Parse() 136 | listen := *address + ":" + *port 137 | listenTLS := *address + ":" + *sslPort 138 | body := flag.Arg(0) 139 | if body == "" { 140 | body = "." 141 | } 142 | var handler http.Handler 143 | if fi, err := os.Stat(body); err == nil { 144 | switch mode := fi.Mode(); { 145 | case mode.IsDir(): 146 | if *deny == "" { 147 | log.Print("Warning: serving files without any filter!") 148 | } 149 | handler = http.StripPrefix(*path, http.FileServer(protectdFileSystem{http.Dir(body)})) 150 | case mode.IsRegular(): 151 | if content, err := os.ReadFile(body); err != nil { 152 | log.Fatal("Error reading file: ", err) 153 | } else { 154 | handler = bytesHandler(content) 155 | } 156 | } 157 | } else { 158 | handler = bytesHandler(body) 159 | } 160 | http.Handle(*path, middleware(handler)) 161 | 162 | if proxy != nil { 163 | proxyHandlers := parseProxy(*proxy) 164 | for _, ph := range proxyHandlers { 165 | log.Printf("sending %s to %s", ph.prefix, ph.proxyURL) 166 | http.Handle(ph.prefix, middleware(ph)) 167 | } 168 | } 169 | 170 | go func() { 171 | if _, err := os.Stat(*cert); err != nil { 172 | return 173 | } 174 | if _, err := os.Stat(*key); err != nil { 175 | return 176 | } 177 | log.Fatal(http.ListenAndServeTLS(listenTLS, *cert, *key, nil)) 178 | }() 179 | log.Printf("Serving %s on %s%s...", body, listen, *path) 180 | log.Fatal(http.ListenAndServe(listen, nil)) 181 | } 182 | --------------------------------------------------------------------------------