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