├── env.default ├── Procfile ├── Procfile.dev ├── Pipfile ├── app.json ├── LICENSE ├── README.md ├── templates ├── render.html ├── index.js └── index.html ├── web.py ├── .gitignore └── Pipfile.lock /env.default: -------------------------------------------------------------------------------- 1 | PORT=5500 2 | DEBUG=True 3 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: uvicorn web:app --host 0.0.0.0 --port $PORT 2 | -------------------------------------------------------------------------------- /Procfile.dev: -------------------------------------------------------------------------------- 1 | web: uvicorn web:app --host 127.0.0.1 --port $PORT 2 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | honcho = "*" 8 | flake8 = "*" 9 | 10 | [packages] 11 | starlette = "*" 12 | uvicorn = "*" 13 | gunicorn = "*" 14 | aiofiles = "*" 15 | jinja2 = "*" 16 | htmlmin = "*" 17 | 18 | [requires] 19 | python_version = "3.7" 20 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Base64 Sites", 3 | "description": "A light utility to render Base64 Encoded HTML Websites.", 4 | "repository": "https://github.com/fidiego/base64-sites", 5 | "keywords": ["html", "website", "tiny", "base64", "python", "starlette"], 6 | "env": { 7 | "HOST": { 8 | "description": "The host to bind on.", 9 | "value": "0.0.0.0" 10 | }, 11 | "DEBUG": { 12 | "description": "Wether or not to show debug messages.", 13 | "value": "False" 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Cognitive Reflex 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Base64 Sites 2 | 3 | [](https://heroku.com/deploy?template=https://github.com/fidiego/base64-sites) 4 | 5 | ## What is this thing? 6 | 7 | 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. 8 | 9 | Try it out [here](//base64-sites.herokuapp.com). 10 | 11 | ## Other Info 12 | 13 | - Built with python3 and [starlette](https://www.starlette.io/). 14 | - HTML formatted with [tidy](http://www.html-tidy.org/). 15 | - JS formatted with [prettier](https://prettier.io/). 16 | - Python formatted with [Black](https://pypi.org/project/black/). 17 | 18 | ## Dev Setup 19 | 20 | **Install Dependencies** 21 | 22 | ``` 23 | pipenv install --dev 24 | ``` 25 | 26 | **Make a `.env` file** 27 | 28 | ``` 29 | cp env.default .env 30 | ``` 31 | 32 | **Run with honcho** 33 | 34 | ``` 35 | pipenv run honcho -f Procfile.dev start 36 | ``` 37 | 38 | ## TODO 39 | 40 | - [ ] figure out css for `/render` endpoint. Get rid of the excess space so iframe fills space with no scroll on body or html. 41 | - [ ] make a js only version that can be deployed on netlify 42 | -------------------------------------------------------------------------------- /templates/render.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |Redirecting you to the homepage.
27 | 30 | {% endif %} 31 | 32 | 33 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /web.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import time 3 | from json.decoder import JSONDecodeError 4 | 5 | from starlette.applications import Starlette 6 | from starlette.config import environ 7 | from starlette.responses import JSONResponse 8 | from starlette.routing import Route 9 | from starlette.templating import Jinja2Templates 10 | 11 | import htmlmin 12 | 13 | 14 | templates = Jinja2Templates(directory="templates") 15 | 16 | 17 | async def index(request): 18 | return templates.TemplateResponse("index.html", {"request": request}) 19 | 20 | 21 | async def render(request): 22 | content = request.query_params.get("content") 23 | return templates.TemplateResponse( 24 | "render.html", {"request": request, "content": content} 25 | ) 26 | 27 | 28 | async def ping(request): 29 | return JSONResponse({"success": True, "time": int(time.time())}) 30 | 31 | 32 | async def api(request): 33 | """ 34 | minify and encode content. returns payload with encoded content and some statistics. 35 | """ 36 | start = time.time() 37 | try: 38 | body = await request.json() 39 | except JSONDecodeError as e: 40 | return JSONResponse( 41 | dict(success=False, message="Invalid JSON Payload."), status_code=415 42 | ) 43 | content = body.get("content") 44 | if not content: 45 | return JSONResponse( 46 | dict( 47 | success=False, 48 | message='"content" may not be empty.', 49 | execution_time=start - time.time(), 50 | ) 51 | ) 52 | minified = htmlmin.minify(content, remove_empty_space=True) 53 | encoded = base64.b64encode(minified.encode()) 54 | base64_string = f"data:text/html;base64,{encoded.decode()}" 55 | payload = dict( 56 | success=True, 57 | content_length=len(content), 58 | minified_content_length=(len(minified)), 59 | base64_string_length=len(base64_string), 60 | base64_string=base64_string, 61 | execution_time=time.time() - start, 62 | ) 63 | return JSONResponse(payload) 64 | 65 | 66 | DEBUG = environ.get("DEBUG", "False").strip().lower() == "true" 67 | app = Starlette( 68 | debug=DEBUG, 69 | routes=[ 70 | Route("/", index, methods=["GET"]), 71 | Route("/render", render, methods=["GET"]), 72 | Route("/api", api, methods=["POST"]), 73 | ], 74 | ) 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | 3 | # Created by https://www.gitignore.io/api/osx,vim,python 4 | # Edit at https://www.gitignore.io/?templates=osx,vim,python 5 | 6 | ### OSX ### 7 | # General 8 | .DS_Store 9 | .AppleDouble 10 | .LSOverride 11 | 12 | # Icon must end with two \r 13 | Icon 14 | 15 | # Thumbnails 16 | ._* 17 | 18 | # Files that might appear in the root of a volume 19 | .DocumentRevisions-V100 20 | .fseventsd 21 | .Spotlight-V100 22 | .TemporaryItems 23 | .Trashes 24 | .VolumeIcon.icns 25 | .com.apple.timemachine.donotpresent 26 | 27 | # Directories potentially created on remote AFP share 28 | .AppleDB 29 | .AppleDesktop 30 | Network Trash Folder 31 | Temporary Items 32 | .apdisk 33 | 34 | ### Python ### 35 | # Byte-compiled / optimized / DLL files 36 | __pycache__/ 37 | *.py[cod] 38 | *$py.class 39 | 40 | # C extensions 41 | *.so 42 | 43 | # Distribution / packaging 44 | .Python 45 | build/ 46 | develop-eggs/ 47 | dist/ 48 | downloads/ 49 | eggs/ 50 | .eggs/ 51 | lib/ 52 | lib64/ 53 | parts/ 54 | sdist/ 55 | var/ 56 | wheels/ 57 | pip-wheel-metadata/ 58 | share/python-wheels/ 59 | *.egg-info/ 60 | .installed.cfg 61 | *.egg 62 | MANIFEST 63 | 64 | # PyInstaller 65 | # Usually these files are written by a python script from a template 66 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 67 | *.manifest 68 | *.spec 69 | 70 | # Installer logs 71 | pip-log.txt 72 | pip-delete-this-directory.txt 73 | 74 | # Unit test / coverage reports 75 | htmlcov/ 76 | .tox/ 77 | .nox/ 78 | .coverage 79 | .coverage.* 80 | .cache 81 | nosetests.xml 82 | coverage.xml 83 | *.cover 84 | .hypothesis/ 85 | .pytest_cache/ 86 | 87 | # Translations 88 | *.mo 89 | *.pot 90 | 91 | # Scrapy stuff: 92 | .scrapy 93 | 94 | # Sphinx documentation 95 | docs/_build/ 96 | 97 | # PyBuilder 98 | target/ 99 | 100 | # pyenv 101 | .python-version 102 | 103 | # pipenv 104 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 105 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 106 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 107 | # install all needed dependencies. 108 | #Pipfile.lock 109 | 110 | # celery beat schedule file 111 | celerybeat-schedule 112 | 113 | # SageMath parsed files 114 | *.sage.py 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # Mr Developer 124 | .mr.developer.cfg 125 | .project 126 | .pydevproject 127 | 128 | # mkdocs documentation 129 | /site 130 | 131 | # mypy 132 | .mypy_cache/ 133 | .dmypy.json 134 | dmypy.json 135 | 136 | # Pyre type checker 137 | .pyre/ 138 | 139 | ### Vim ### 140 | # Swap 141 | [._]*.s[a-v][a-z] 142 | [._]*.sw[a-p] 143 | [._]s[a-rt-v][a-z] 144 | [._]ss[a-gi-z] 145 | [._]sw[a-p] 146 | 147 | # Session 148 | Session.vim 149 | Sessionx.vim 150 | 151 | # Temporary 152 | .netrwhist 153 | *~ 154 | 155 | # Auto-generated tag files 156 | tags 157 | 158 | # Persistent undo 159 | [._]*.un~ 160 | 161 | # Coc configuration directory 162 | .vim 163 | 164 | # End of https://www.gitignore.io/api/osx,vim,python 165 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |Turn HTML into a link that is the site.
15 | 18 |/render endpoint to render the base64 encoded HTML in an iframe.
24 | To make tiny websites where the URL effectively is the website. The strings produced can be pasted into a URL bar directly. This is great if you're sharing content that doesn't merit hosting, for example an ephemeral HTML report.
38 |A problem arises when someone is on their phone since most messaging apps won't recognize the string as a URL. We address that by providing a /render endpoint that takes a context attribute. This endpoint returns an iframe with the base64 string as the value for the src attribute. The link can be shared and, given it's a valid url, can be opened on mobile devices.
Browsers can render base64 encoded content. Base64 encoding is often used for images or svgs in css since the image can be encoded in the css file reducing the need for additional requests [1].
43 |HTML can be similarly encoded. Check the source for the /api endpoint for the code. It simply:
data:text/html;base64, prefix./render endpoint as the content param and that endpoint will return an iframe with content as the src.
49 | I often want to send very small pieces of information to a co-worker, some of which is best displayed in tables. I have some python scripts that pull data and generate these neat tables which I prefer to print to the terminal (shout out to tabulate). But, if I need to share this information, HTML is a much better format. The tabulate library has an HTML output option, but I don't want to set up hosting for such ephemeral information. It's easy to pipe the HTML to a script that bas64 encodes it and adds the 'data:text/html;base64,' prefix. The resulting string can then be opened in the browser.
However! This is not the case on mobile. The string isn't recognized as a URL and the workarounds are just too many steps (open in Firefox Focus or copy and paste into a mobile Browser). But if I pass the base64 string as a query parameter to an endpoint that simply returns it as the src in an iframe (as suggested in this SO Answer), we're in business. I can now send these one-offs (that dont' require persistence) to folks on mobile devices and they can open them like any other link.
54 |