├── env.default ├── go.mod ├── Dockerfile ├── go.sum ├── README.md ├── .gitignore ├── templates ├── render.go.html ├── index.js └── index.go.html └── main.go /env.default: -------------------------------------------------------------------------------- 1 | PORT=9876 2 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module base64-sites 2 | 3 | go 1.19 4 | 5 | require github.com/tdewolff/minify v2.3.6+incompatible 6 | 7 | require github.com/tdewolff/parse v2.3.4+incompatible // indirect 8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.14.4-alpine3.12 AS builder 2 | 3 | RUN apk add git 4 | 5 | COPY . /go/src/base64_site 6 | WORKDIR /go/src/base64_site 7 | 8 | RUN go get 9 | RUN go build -i main.go 10 | 11 | FROM scratch 12 | COPY --from=builder /go/src/base64_site/main /go/bin/main 13 | ENTRYPOINT ["/go/bin/main"] 14 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/tdewolff/minify v2.3.6+incompatible h1:2hw5/9ZvxhWLvBUnHE06gElGYz+Jv9R4Eys0XUzItYo= 2 | github.com/tdewolff/minify v2.3.6+incompatible/go.mod h1:9Ov578KJUmAWpS6NeZwRZyT56Uf6o3Mcz9CEsg8USYs= 3 | github.com/tdewolff/parse v2.3.4+incompatible h1:x05/cnGwIMf4ceLuDMBOdQ1qGniMoxpP46ghf0Qzh38= 4 | github.com/tdewolff/parse v2.3.4+incompatible/go.mod h1:8oBwCsVmUkgHO8M5iCzSIDtpzXOT0WXX9cWhz+bIzJQ= 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Base64 Site Golang edition 2 | 3 | ## What is this thing? 4 | 5 | Have you ever wanted to encode a tiny website in base64 but needed the link to be shared in mobile or otherwise handled by devices that only know about well-formed URLs? Well, me too. 6 | 7 | This is a rewrite of [this](//github.com/fidiego/base64-sites) identical project. This version is written in Go instead of python. 8 | 9 | Try it out the python version [here](//base64-sites.herokuapp.com). 10 | 11 | ## Dev Setup 12 | 13 | **Make a `.env` file** 14 | 15 | ``` 16 | cp env.default .env 17 | ``` 18 | 19 | **Install Dependencies** 20 | 21 | ``` 22 | go get 23 | ``` 24 | 25 | **Run** 26 | 27 | ``` 28 | go run main.go 29 | ``` 30 | 31 | **Try it out\*** 32 | 33 | ``` 34 | http POST localhost:9876/api content="" --json 35 | ``` 36 | 37 | ## TODO 38 | 39 | - [ ] figure out css for `/render` endpoint. Get rid of the excess space so iframe fills space with no scroll on body or html. 40 | - [ ] make a js only version that can be deployed on netlify 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | 3 | # Created by https://www.toptal.com/developers/gitignore/api/osx,vim,go 4 | # Edit at https://www.toptal.com/developers/gitignore?templates=osx,vim,go 5 | 6 | ### Go ### 7 | # Binaries for programs and plugins 8 | *.exe 9 | *.exe~ 10 | *.dll 11 | *.so 12 | *.dylib 13 | 14 | # Test binary, built with `go test -c` 15 | *.test 16 | 17 | # Output of the go coverage tool, specifically when used with LiteIDE 18 | *.out 19 | 20 | ### Go Patch ### 21 | /vendor/ 22 | /Godeps/ 23 | 24 | ### OSX ### 25 | # General 26 | .DS_Store 27 | .AppleDouble 28 | .LSOverride 29 | 30 | # Icon must end with two \r 31 | Icon 32 | 33 | # Thumbnails 34 | ._* 35 | 36 | # Files that might appear in the root of a volume 37 | .DocumentRevisions-V100 38 | .fseventsd 39 | .Spotlight-V100 40 | .TemporaryItems 41 | .Trashes 42 | .VolumeIcon.icns 43 | .com.apple.timemachine.donotpresent 44 | 45 | # Directories potentially created on remote AFP share 46 | .AppleDB 47 | .AppleDesktop 48 | Network Trash Folder 49 | Temporary Items 50 | .apdisk 51 | 52 | ### Vim ### 53 | # Swap 54 | [._]*.s[a-v][a-z] 55 | [._]*.sw[a-p] 56 | [._]s[a-rt-v][a-z] 57 | [._]ss[a-gi-z] 58 | [._]sw[a-p] 59 | 60 | # Session 61 | Session.vim 62 | 63 | # Temporary 64 | .netrwhist 65 | *~ 66 | # Auto-generated tag files 67 | tags 68 | # Persistent undo 69 | [._]*.un~ 70 | 71 | # End of https://www.toptal.com/developers/gitignore/api/osx,vim,go 72 | 73 | -------------------------------------------------------------------------------- /templates/render.go.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ if .Content }} 4 |
5 | 6 | 7 |Redirecting you to the homepage.
31 | 38 | 39 | {{ end }} 40 | 41 | -------------------------------------------------------------------------------- /templates/index.js: -------------------------------------------------------------------------------- 1 | const generateResults = function (response) { 2 | // remove results, if exists 3 | var results = document.getElementById('results'); 4 | if (results) { 5 | results.remove(); 6 | } 7 | var resultLinks = document.getElementById('results-links'); 8 | if (resultLinks) { 9 | results.remove(); 10 | } 11 | // generate result nodes 12 | var section = document.getElementById('try-it'); 13 | var node = document.createElement('PRE'); 14 | node.id = 'results'; 15 | node.textContent = JSON.stringify(response, null, '\t'); 16 | section.appendChild(node); 17 | var links = document.createElement('div'); 18 | links.id = 'results-link'; 19 | links.innerHTML = 20 | 'Copy base64_string (or this link) and paste it into your URL bar.
Or use this link on a mobile device.
'; 26 | // append results 27 | section.appendChild(links); 28 | }; 29 | 30 | const onSubmit = function () { 31 | var ta = document.getElementById('content-textarea'); 32 | var html = ta.value; 33 | var payload = { content: html }; 34 | fetch('/api', { 35 | method: 'POST', 36 | headers: { 'content-type': 'application/json' }, 37 | body: JSON.stringify(payload), 38 | }) 39 | .then(function (response) { 40 | response.json().then(function (resp) { 41 | generateResults(resp); 42 | }); 43 | }) 44 | .catch(function (err) { 45 | console.error(err); 46 | }); 47 | }; 48 | 49 | (function () { 50 | console.info('Document Ready: registering Event Listener'); 51 | var form = document.getElementById('content-form'); 52 | form.addEventListener('submit', function (e) { 53 | e.preventDefault(); 54 | onSubmit(); 55 | }); 56 | })(); 57 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "fmt" 7 | "html/template" 8 | "log" 9 | "net/http" 10 | "os" 11 | "strconv" 12 | "time" 13 | 14 | "github.com/tdewolff/minify" 15 | "github.com/tdewolff/minify/html" 16 | ) 17 | 18 | type ApiRequest struct { 19 | Content string `json:"content"` 20 | } 21 | 22 | type ApiErrorResponse struct { 23 | Success bool `json:"success"` 24 | Message string `json:"message"` 25 | } 26 | 27 | type ApiResponse struct { 28 | Base64String string `json:"base64_string"` 29 | Base64StringLength int `json:"base64_string_length"` 30 | ContentLength int `json:"content_length"` 31 | ExecutionTime float64 `json:"execution_time"` 32 | MinifiedContentLength int `json:"minified_content_length"` 33 | Success bool `json:"success"` 34 | } 35 | 36 | func apiHandler(w http.ResponseWriter, r *http.Request) { 37 | start := time.Now() // start timer 38 | 39 | // TODO: check if request is a post 40 | 41 | // parse JSON POST request 42 | decoder := json.NewDecoder(r.Body) 43 | var p ApiRequest 44 | err := decoder.Decode(&p) 45 | 46 | // Handle JSON Error 47 | if err != nil { 48 | log.Println("Bad Request - returning error message") 49 | // set headers 50 | w.WriteHeader(http.StatusBadRequest) 51 | w.Header().Set("Content-Type", "application/json") 52 | // compose error response payload 53 | errorResponse := ApiErrorResponse{ 54 | Success: false, 55 | Message: "Expecting a JSON payload with a single attribute \"content\" .", 56 | } 57 | log.Println(errorResponse) 58 | // write error response to buffer 59 | json.NewEncoder(w).Encode(errorResponse) 60 | return 61 | } 62 | 63 | // Happy Path 😊 64 | // set headers 65 | w.Header().Set("Content-Type", "application/json") 66 | w.WriteHeader(http.StatusAccepted) 67 | 68 | // minify HTML 69 | minifier := minify.New() 70 | minifier.AddFunc("text/html", html.Minify) 71 | minified, err := minifier.String("text/html", p.Content) 72 | // Handle minification error 73 | if err != nil { 74 | log.Println("Error during HTML Minification") 75 | w.WriteHeader(http.StatusBadRequest) 76 | w.Header().Set("Content-Type", "application/json") 77 | errorResponse := ApiErrorResponse{ 78 | Success: false, 79 | Message: "An unexpected error occurred while minifying the content. Make sure you're passing valid HTML.", 80 | } 81 | // write error response to buffer 82 | json.NewEncoder(w).Encode(errorResponse) 83 | return 84 | } 85 | 86 | // encode 87 | encoded := base64.StdEncoding.EncodeToString([]byte(p.Content)) 88 | base64_string := fmt.Sprintf("data:text/html;base64,%s", encoded) 89 | // calculate lengths 90 | content_length := len(p.Content) 91 | base64_string_length := len(base64_string) 92 | minified_content_length := len(minified) 93 | // get elapsed time 94 | elapsed := time.Now().Sub(start) 95 | 96 | // compose response object 97 | jsonResponse := ApiResponse{ 98 | Success: true, 99 | Base64StringLength: base64_string_length, 100 | Base64String: base64_string, 101 | ExecutionTime: elapsed.Seconds(), 102 | ContentLength: content_length, 103 | MinifiedContentLength: minified_content_length, 104 | } 105 | // write response 106 | json.NewEncoder(w).Encode(jsonResponse) 107 | } 108 | 109 | type RenderContext struct { 110 | Content template.URL 111 | } 112 | 113 | func renderHandler(w http.ResponseWriter, r *http.Request) { 114 | query := r.URL.Query() 115 | contents, present := query["content"] 116 | if !present || len(contents) == 0 { 117 | // return error response 118 | } 119 | t, err := template.ParseFiles("templates/render.go.html") 120 | if err != nil { 121 | log.Println(err) 122 | } 123 | content := contents[0] 124 | context := RenderContext{Content: template.URL(content)} 125 | t.Execute(w, context) 126 | } 127 | 128 | func indexHandler(w http.ResponseWriter, r *http.Request) { 129 | t, err := template.ParseFiles("templates/index.go.html", "templates/index.js") 130 | if err != nil { 131 | fmt.Println(err) 132 | } 133 | t.Execute(w, nil) 134 | } 135 | 136 | func main() { 137 | fmt.Println("Starting Base64 Site Service") 138 | 139 | // parse port from env var - use 8080 as a fallback if not set 140 | var portStr = os.Getenv("PORT") 141 | var port int32 142 | if portStr == "" { 143 | log.Println("PORT env not provided. Falling back to default 8080.") 144 | port = 8080 145 | } else { 146 | parsed, err := strconv.Atoi(portStr) 147 | if err != nil { 148 | log.Fatal("Error parsing PORT env var") 149 | os.Exit(1) 150 | } else { 151 | port = int32(parsed) 152 | } 153 | 154 | } 155 | 156 | fmt.Println(" -> loaded env vars") 157 | fmt.Printf(" - PORT=%d\n", port) 158 | 159 | server := &http.Server{ 160 | Addr: fmt.Sprintf(":%d", port), 161 | Handler: nil, 162 | ReadTimeout: 5 * time.Second, 163 | WriteTimeout: 5 * time.Second, 164 | MaxHeaderBytes: 1 << 20, 165 | } 166 | 167 | // register routes 168 | fmt.Println(" -> prepared http.Server") 169 | http.HandleFunc("/", indexHandler) 170 | http.HandleFunc("/api", apiHandler) 171 | http.HandleFunc("/render", renderHandler) 172 | 173 | fmt.Println(" -> registered routes") 174 | fmt.Printf(" -> starting on port:%d\n", port) 175 | 176 | log.Fatal(server.ListenAndServe()) 177 | } 178 | -------------------------------------------------------------------------------- /templates/index.go.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | 11 | 15 |Turn HTML into a link that is the site.
21 | 26 |/render endpoint to render
33 | the base64 encoded HTML in an iframe.
34 | 55 | To make tiny websites where the URL effectively is the website. 56 | The strings produced can be pasted into a URL bar directly. This is 57 | great if you're sharing content that doesn't merit hosting, for example 58 | an ephemeral HTML report. 59 |
60 |
61 | A problem arises when someone is on their phone since most messaging
62 | apps won't recognize the string as a URL. We address that by providing a
63 | /render endpoint that takes a
64 | context attribute. This endpoint returns an iframe with the
65 | base64 string as the value for the src attribute. The link
66 | can be shared and, given it's a valid url, can be opened on mobile
67 | devices.
68 |
73 | Browsers can render base64 encoded content. Base64 encoding is often 74 | used for images or svgs in css since the image can be encoded in the css 75 | file reducing the need for additional requests [1]. 79 |
80 |
81 | HTML can be similarly encoded. Check the source for the
82 | /api endpoint for the code. It simply:
83 |
data:text/html;base64, prefix.
89 | /render endpoint as the content param and that
94 | endpoint will return an iframe with content as the
95 | src.
96 |
100 | I often want to send very small pieces of information to a co-worker,
101 | some of which is best displayed in tables. I have some python scripts
102 | that pull data and generate these neat tables which I prefer to print to
103 | the terminal (shout out to
104 | tabulate). But,
105 | if I need to share this information, HTML is a much better format. The
106 | tabulate library has an HTML output option, but I don't want to set up
107 | hosting for such ephemeral information. It's easy to pipe the HTML to a
108 | script that bas64 encodes it and adds the
109 | 'data:text/html;base64,' prefix. The resulting string can
110 | then be opened in the browser.
111 |
113 | However! This is not the case on mobile. The string isn't 114 | recognized as a URL and the workarounds are just too many steps (open in 115 | Firefox Focus or copy and paste into a mobile Browser). But if I pass 116 | the base64 string as a query parameter to an endpoint that simply 117 | returns it as the src in an iframe (as suggested in 118 | this SO Answer), we're 119 | in business. I can now send these one-offs (that dont' require 120 | persistence) to folks on mobile devices and they can open them like any 121 | other link. 122 |
123 |