├── .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 |
--------------------------------------------------------------------------------