├── LICENSE
├── README.md
├── cmd.go
├── log.go
├── logo.png
├── main.go
└── save.go
/LICENSE:
--------------------------------------------------------------------------------
1 | mMIT License
2 |
3 | Copyright (c) 2020 PJ Engineering and Business Solutions Pty. Ltd.
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 |
23 | The usage of this software must not be knowingly used for an application
24 | that will be directly or indirectly used for military purposes.
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | friendly - The FRiENDly webserver for front-end developers (For local development)
2 | ======
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | ⭐ **the project to show your appreciation.**
11 |
12 | When you are developing your website locally, you may be encountering **CORS** issues.
13 | In my case, I had an `iframe` that was calling `parent` to access the host site.
14 | All major browsers such as Chrome, Safari and Firefox were blocking the call.
15 |
16 | You can use `friendly` by placing the server in the same directory as your project to
17 | run your website as if it was run on an actual production server.
18 |
19 | It's that simple and easy to use.
20 |
21 | It supports:
22 | - http and https
23 | - custom ports
24 | - custom paths (so you don't need to place it in the same directory as your project)
25 |
26 |
27 | **[Download here](https://github.com/rocketlaunchr/friendly/releases)**
28 |
29 | ## Usage
30 |
31 | ```bash
32 | ./friendly -d "" -b --save -r -s
33 | ```
34 |
35 | or just place the application in your project path and run it without flags.
36 |
37 | The recommended way is to install it globally (add to $PATH). Then you can run the server from anywhere without setting a `path` (which defaults to the current working directory).
38 |
39 | ## Flags
40 |
41 | ### port (p)
42 |
43 | Set a custom port. By default, it is `8080` for http and `4430` for https.
44 |
45 | ### path (d)
46 |
47 | Point to the directory of your project.
48 |
49 | ### https (s)
50 |
51 | Automatically create a self-signed SSL certificate. The browser will ask whether you trust the certificate. Allow it.
52 |
53 | ### browser (b)
54 |
55 | Open the project automatically on your default browser the moment the server starts up.
56 |
57 | ### save
58 |
59 | In https mode, everytime the server starts, it will create a new self-signed certificate.
60 | The browser will repetitively ask if you trust the certificate. This can be annoying.
61 | Use this setting to reuse the same certificate.
62 |
63 | ### remove (r)
64 |
65 | Delete a certificate you may have saved in the past.
66 |
67 | ### quiet (q)
68 |
69 | Don't show any logs of the incoming requests.
70 |
71 |
72 | ## Installation
73 |
74 | Just download the prebuilt executables from the [Releases](https://github.com/rocketlaunchr/friendly/releases). It is available for **Windows**, **macOS** and **Linux**.
75 |
76 | If you want to customize the project to your needs, then clone this repo. You will need to know how to build Go projects after downloading the dependencies.
77 |
78 |
79 | ```bash
80 | GITCOMMIT=$(git rev-parse --short HEAD) && \
81 | VERSION=$(git describe --always) && \
82 | env GOOS=darwin GOARCH=amd64 go build -ldflags "-X main.GITCOMMIT=$GITCOMMIT -X main.VERSION=$VERSION -s -w" .
83 | ```
84 |
85 | **NOTE:** Replace GOOS with `darwin`(macOS), `windows` or `linux`.
86 |
87 |
88 |
89 | Other useful packages
90 | ------------
91 |
92 | - [dataframe-go](https://github.com/rocketlaunchr/dataframe-go) - Statistics and data manipulation
93 | - [dbq](https://github.com/rocketlaunchr/dbq) - Zero boilerplate database operations for Go
94 | - [electron-alert](https://github.com/rocketlaunchr/electron-alert) - SweetAlert2 for Electron Applications
95 | - [igo](https://github.com/rocketlaunchr/igo) - A Go transpiler with cool new syntax such as fordefer (defer for for-loops)
96 | - [mysql-go](https://github.com/rocketlaunchr/mysql-go) - Properly cancel slow MySQL queries
97 | - [react](https://github.com/rocketlaunchr/react) - Build front end applications using Go
98 | - [remember-go](https://github.com/rocketlaunchr/remember-go) - Cache slow database queries
99 |
100 |
101 | ## Legal Information
102 |
103 | The license is a modified MIT license. Refer to the `LICENSE` file for more details.
104 |
105 | **© 2020 PJ Engineering and Business Solutions Pty. Ltd.**
106 |
107 | ## Final Notes
108 |
109 | Feel free to enhance features by issuing pull-requests.
--------------------------------------------------------------------------------
/cmd.go:
--------------------------------------------------------------------------------
1 | // Copyright 2020 PJ Engineering and Business Solutions Pty. Ltd. All rights reserved.
2 |
3 | package main
4 |
5 | import (
6 | "crypto/tls"
7 | "fmt"
8 | "log"
9 | "net/http"
10 | "os"
11 | "path/filepath"
12 | "strings"
13 | "time"
14 |
15 | "github.com/fatih/color"
16 | "github.com/pkg/browser"
17 | "github.com/rocketlaunchr/https-go"
18 | "github.com/spf13/cobra"
19 | )
20 |
21 | var noCacheHeaders = map[string]string{
22 | "Expires": time.Unix(0, 0).Format(time.RFC1123),
23 | "Cache-Control": "no-cache, no-store, must-revalidate, private, max-age=0",
24 | "Pragma": "no-cache",
25 | "X-Accel-Expires": "0",
26 | }
27 |
28 | var lastReqTime *time.Time
29 |
30 | type wrapHandler struct {
31 | fs http.Handler
32 | quiet bool
33 | localPath string
34 | }
35 |
36 | func (h *wrapHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
37 | if !h.quiet {
38 | start := time.Now()
39 | defer func() {
40 | end := time.Now()
41 | d := end.Sub(start)
42 |
43 | if lastReqTime != nil && time.Since(*lastReqTime) > 1*time.Second {
44 | fmt.Println("============================")
45 | }
46 |
47 | lastReqTime = &start
48 |
49 | magenta1 := color.New(color.FgWhite, color.BgMagenta, color.Underline).SprintfFunc()
50 | magenta2 := color.New(color.FgWhite, color.BgCyan).SprintfFunc()
51 |
52 | html := color.New(color.Bold, color.FgBlue).SprintFunc()
53 | css := color.New(color.Bold, color.FgGreen).SprintFunc()
54 | js := color.New(color.Bold, color.FgRed).SprintFunc()
55 | img := color.New(color.Bold, color.FgYellow).SprintFunc()
56 | other := color.New(color.Bold, color.FgBlack).SprintFunc()
57 |
58 | // Get file size
59 | var sizeStr string
60 | if r.URL.Path == "/" {
61 | filePath := filepath.Join(h.localPath, "index.html") // wierd: Can't move filePath to outside if
62 | fi, err := os.Stat(filePath)
63 | if err == nil {
64 | size := fi.Size()
65 | if size/1024 != 0 { // converted to KB
66 | sizeStr = fmt.Sprintf("[%dKB]", size/1024)
67 | } else {
68 | sizeStr = fmt.Sprintf("[%dB]", size)
69 | }
70 | }
71 | } else {
72 | filePath := filepath.Join(h.localPath, r.URL.Path) // wierd: Can't move filePath to outside if
73 | fi, err := os.Stat(filePath)
74 | if err == nil {
75 | size := fi.Size()
76 | if size/1024 != 0 { // converted to KB
77 | sizeStr = fmt.Sprintf("[%dKB]", size/1024)
78 | } else {
79 | sizeStr = fmt.Sprintf("[%dB]", size)
80 | }
81 | }
82 | }
83 |
84 | switch filepath.Ext(r.URL.Path) {
85 | case ".html":
86 | fmt.Printf("[%s#%s] %s %s %s\n", magenta1("%s", start.Local().Format("15:04:05.000")), magenta2("%s", d.String()), strings.ToUpper(r.Method), html(r.URL.Path), sizeStr)
87 | case ".js":
88 | fmt.Printf("[%s#%s] %s %s %s\n", magenta1("%s", start.Local().Format("15:04:05.000")), magenta2("%s", d.String()), strings.ToUpper(r.Method), js(r.URL.Path), sizeStr)
89 | case ".css":
90 | fmt.Printf("[%s#%s] %s %s %s\n", magenta1("%s", start.Local().Format("15:04:05.000")), magenta2("%s", d.String()), strings.ToUpper(r.Method), css(r.URL.Path), sizeStr)
91 | case ".png", ".jpg", ".jpeg", ".ico", ".svg":
92 | fmt.Printf("[%s#%s] %s %s %s\n", magenta1("%s", start.Local().Format("15:04:05.000")), magenta2("%s", d.String()), strings.ToUpper(r.Method), img(r.URL.Path), sizeStr)
93 | default:
94 | if r.URL.Path == "/" {
95 | fmt.Printf("[%s#%s] %s %s %s\n", magenta1("%s", start.Local().Format("15:04:05.000")), magenta2("%s", d.String()), strings.ToUpper(r.Method), html("/index.html"), sizeStr)
96 | } else {
97 | fmt.Printf("[%s#%s] %s %s %s\n", magenta1("%s", start.Local().Format("15:04:05.000")), magenta2("%s", d.String()), strings.ToUpper(r.Method), other(r.URL.Path), sizeStr)
98 | }
99 | }
100 | }()
101 | }
102 |
103 | // Prevent caching
104 | for k, v := range noCacheHeaders {
105 | w.Header().Set(k, v)
106 | }
107 |
108 | h.fs.ServeHTTP(w, r)
109 | }
110 |
111 | func runCmd(cmd *cobra.Command, args []string) {
112 |
113 | quietMode, _ := cmd.Flags().GetBool("quiet")
114 | port, _ := cmd.Flags().GetString("port")
115 | path, _ := cmd.Flags().GetString("path")
116 | httpsOn, _ := cmd.Flags().GetBool("https")
117 | save, _ := cmd.Flags().GetBool("save")
118 | browserOpen, _ := cmd.Flags().GetBool("browser")
119 | removeCerts, _ := cmd.Flags().GetBool("remove")
120 |
121 | if port == "" {
122 | if httpsOn {
123 | port = "4430"
124 | } else {
125 | port = "8080"
126 | }
127 | }
128 |
129 | http.Handle("/", &wrapHandler{http.FileServer(http.Dir(path)), quietMode, path})
130 |
131 | var homeURL string
132 | if httpsOn {
133 | homeURL = "https://localhost:" + port
134 | } else {
135 | homeURL = "http://localhost:" + port
136 | }
137 |
138 | if !quietMode {
139 | if httpsOn {
140 | fmt.Printf("running server on https://localhost:%s\n", port)
141 | } else {
142 | fmt.Printf("running server on http://localhost:%s\n", port)
143 | }
144 | }
145 |
146 | if removeCerts {
147 | err := deleteCerts()
148 | if err != nil {
149 | if httpsOn && !strings.Contains(err.Error(), "no such file or directory") {
150 | log.Fatal(err)
151 | }
152 | }
153 | }
154 |
155 | if !httpsOn {
156 |
157 | if browserOpen {
158 | go func() {
159 | time.Sleep(1250 * time.Millisecond)
160 | browser.OpenURL(homeURL)
161 | }()
162 | }
163 |
164 | log.Fatal(http.ListenAndServe(":"+port, nil))
165 | } else {
166 |
167 | pub, priv, err := getCerts(!save)
168 | if err != nil {
169 | pub, priv, err = https.GenerateKeys(https.GenerateOptions{Host: "localhost"})
170 | if err != nil {
171 | // could not generate keys
172 | log.Fatal(err)
173 | }
174 | if save {
175 | saveCerts(pub, priv)
176 | }
177 | }
178 |
179 | cert, err := tls.X509KeyPair(pub, priv)
180 | if err != nil {
181 | log.Fatal(err)
182 | }
183 |
184 | httpServer := &http.Server{
185 | Addr: ":" + port,
186 | TLSConfig: &tls.Config{Certificates: []tls.Certificate{cert}},
187 | ErrorLog: log.New(&ThrowAway{}, "", 0),
188 | }
189 |
190 | if browserOpen {
191 | go func() {
192 | time.Sleep(1250 * time.Millisecond)
193 | browser.OpenURL(homeURL)
194 | }()
195 | }
196 |
197 | log.Fatal(httpServer.ListenAndServeTLS("", ""))
198 | }
199 |
200 | }
201 |
--------------------------------------------------------------------------------
/log.go:
--------------------------------------------------------------------------------
1 | // Copyright 2020 PJ Engineering and Business Solutions Pty. Ltd. All rights reserved.
2 |
3 | package main
4 |
5 | type ThrowAway struct{}
6 |
7 | func (*ThrowAway) Write(p []byte) (n int, err error) {
8 | return len(p), nil
9 | }
10 |
--------------------------------------------------------------------------------
/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rocketlaunchr/friendly/6db17b67dc4c41a9971480381ecb4b576bcd6878/logo.png
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | // Copyright 2020 PJ Engineering and Business Solutions Pty. Ltd. All rights reserved.
2 |
3 | package main
4 |
5 | import (
6 | "fmt"
7 | "log"
8 | "os"
9 |
10 | "github.com/spf13/cobra"
11 | )
12 |
13 | var VERSION = "" // Use git tag: $(git describe --always)
14 | var GITCOMMIT = "" // Use current git commit id: $(git rev-parse --short HEAD)
15 |
16 | func main() {
17 |
18 | var versionCmd = &cobra.Command{
19 | Use: "version",
20 | Short: "Version prints the version of friendly",
21 | Run: func(cmd *cobra.Command, args []string) {
22 | fmt.Println("friendly version: " + VERSION + " (" + GITCOMMIT + ")")
23 | os.Exit(0)
24 | },
25 | }
26 |
27 | var rootCmd = &cobra.Command{
28 | Use: "friendly",
29 | Run: runCmd,
30 | }
31 |
32 | rootCmd.AddCommand(versionCmd)
33 |
34 | rootCmd.Flags().StringP("port", "p", "", "listen port (default: 8080 [http] & 4430 [https])")
35 | rootCmd.Flags().StringP("path", "d", ".", "directory of files")
36 | rootCmd.Flags().BoolP("https", "s", false, "enable https")
37 | rootCmd.Flags().BoolP("browser", "b", false, "open site on browser")
38 | rootCmd.Flags().Bool("save", false, "save ssl certificate for reuse")
39 | rootCmd.Flags().BoolP("remove", "r", false, "remove stored ssl certificate")
40 | rootCmd.Flags().BoolP("quiet", "q", false, "don't produce logs")
41 |
42 | err := rootCmd.Execute()
43 | if err != nil {
44 | log.Fatal(err)
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/save.go:
--------------------------------------------------------------------------------
1 | // Copyright 2020 PJ Engineering and Business Solutions Pty. Ltd. All rights reserved.
2 |
3 | package main
4 |
5 | import (
6 | "encoding/json"
7 | "errors"
8 | "io/ioutil"
9 | "os"
10 | "path/filepath"
11 | )
12 |
13 | func getCerts(gen bool) ([]byte, []byte, error) {
14 |
15 | if gen {
16 | return nil, nil, errors.New("generate key")
17 | }
18 |
19 | exePath, err := os.Executable()
20 | if err != nil {
21 | return nil, nil, err
22 | }
23 | exePath = filepath.Dir(exePath)
24 |
25 | content, err := ioutil.ReadFile(filepath.Join(exePath, "friendly.cert"))
26 | if err != nil {
27 | return nil, nil, err
28 | }
29 |
30 | certs := map[string]string{}
31 | err = json.Unmarshal(content, &certs)
32 | if err != nil {
33 | return nil, nil, err
34 | }
35 |
36 | return []byte(certs["public"]), []byte(certs["private"]), nil
37 | }
38 |
39 | func saveCerts(public []byte, private []byte) error {
40 |
41 | exePath, err := os.Executable()
42 | if err != nil {
43 | return err
44 | }
45 | exePath = filepath.Dir(exePath)
46 |
47 | data, _ := json.Marshal(map[string]string{
48 | "private": string(private),
49 | "public": string(public),
50 | })
51 |
52 | return ioutil.WriteFile(filepath.Join(exePath, "friendly.cert"), data, 0644)
53 | }
54 |
55 | func deleteCerts() error {
56 | exePath, err := os.Executable()
57 | if err != nil {
58 | return err
59 | }
60 | exePath = filepath.Dir(exePath)
61 |
62 | return os.Remove(filepath.Join(exePath, "friendly.cert"))
63 | }
64 |
--------------------------------------------------------------------------------