├── 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="hello world" --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 | Base64 Site - Render Base 64 Sites 8 | 13 | 14 | 15 | 16 | 17 | {{ else }} 18 | 19 | 20 | 21 | 22 | 26 | Base64 Site - Render Base 64 Sites 27 | 28 | 29 |

No Content Provided

30 |

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.

' + 23 | '

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 | Base64 Site - Render Base 64 Sites 16 | 17 | 18 |
19 |

Base64 Site

20 |

Turn HTML into a link that is the site.

21 | 26 |
27 |
28 |

What does it do?

29 |
    30 |
  1. Takes HTML and converts it to base64.
  2. 31 |
  3. 32 | Offers a /render endpoint to render 33 | the base64 encoded HTML in an iframe. 34 |
  4. 35 |
36 |
37 |
38 |

Try It

39 |
40 | 41 | 48 | 49 |
50 |
51 |
52 |
53 |

Why

54 |

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 |

69 |
70 |
71 |

How it works

72 |

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 |

84 |
    85 |
  1. Minifies the HTML string
  2. 86 |
  3. 87 | Base64 encodes the minified HTML string and add the 88 | data:text/html;base64, prefix. 89 |
  4. 90 |
  5. URI Encode the base64 string.
  6. 91 |
92 | One can then simply pass the URI encoded base64 encoded HTML string to the 93 | /render endpoint as the content param and that 94 | endpoint will return an iframe with content as the 95 | src. 96 |
97 |
98 |

But why though?

99 |

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 |

112 |

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 |
124 |
125 | 129 | 132 | 133 | 134 | --------------------------------------------------------------------------------