├── README.md ├── LICENSE ├── .gitignore └── gcat /README.md: -------------------------------------------------------------------------------- 1 | # gcat 2 | 3 | Like `cat` but for gemini. 4 | 5 | Installation: 6 | ``` 7 | git clone https://github.com/aaronjanse/gcat 8 | export PATH=$PWD/gcat:$PATH 9 | ``` 10 | 11 | Usage: 12 | ``` 13 | gcat gemini://gemini.circumlunar.space 14 | ``` 15 | 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) . All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, 4 | are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, 7 | this list of conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 14 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 15 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 16 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 17 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 18 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 19 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 20 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 21 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE 22 | USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ---> Python 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | .pytest_cache/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | db.sqlite3 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # IPython 78 | profile_default/ 79 | ipython_config.py 80 | 81 | # pyenv 82 | .python-version 83 | 84 | # celery beat schedule file 85 | celerybeat-schedule 86 | 87 | # SageMath parsed files 88 | *.sage.py 89 | 90 | # Environments 91 | .env 92 | .venv 93 | env/ 94 | venv/ 95 | ENV/ 96 | env.bak/ 97 | venv.bak/ 98 | 99 | # Spyder project settings 100 | .spyderproject 101 | .spyproject 102 | 103 | # Rope project settings 104 | .ropeproject 105 | 106 | # mkdocs documentation 107 | /site 108 | 109 | # mypy 110 | .mypy_cache/ 111 | .dmypy.json 112 | dmypy.json 113 | 114 | # Pyre type checker 115 | .pyre/ 116 | 117 | -------------------------------------------------------------------------------- /gcat: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import email.message 4 | import os 5 | import socket 6 | import ssl 7 | import sys 8 | import urllib.parse 9 | 10 | def absolutise_url(base, relative): 11 | # Absolutise relative links 12 | if "://" not in relative: 13 | # Python's URL tools somehow only work with known schemes? 14 | base = base.replace("gemini://","http://") 15 | relative = urllib.parse.urljoin(base, relative) 16 | relative = relative.replace("http://", "gemini://") 17 | return relative 18 | 19 | if len(sys.argv) != 2: 20 | print("Usage:") 21 | print("gcat gemini://gemini.circumlunar.space") 22 | sys.exit(1) 23 | 24 | url = sys.argv[1] 25 | parsed_url = urllib.parse.urlparse(url) 26 | if parsed_url.scheme == "": 27 | url = "gemini://"+url 28 | parsed_url = urllib.parse.urlparse(url) 29 | 30 | if parsed_url.scheme != "gemini": 31 | print("Sorry, Gemini links only.") 32 | sys.exit(1) 33 | if parsed_url.port is not None: 34 | useport = parsed_url.port 35 | else: 36 | useport = 1965 37 | # Do the Gemini transaction 38 | while True: 39 | s = socket.create_connection((parsed_url.hostname, useport)) 40 | context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) 41 | context.check_hostname = False 42 | context.verify_mode = ssl.CERT_NONE 43 | s = context.wrap_socket(s, server_hostname = parsed_url.netloc) 44 | s.sendall((url + '\r\n').encode("UTF-8")) 45 | # Get header and check for redirects 46 | fp = s.makefile("rb") 47 | header = fp.readline() 48 | print(header.decode("UTF-8"), end="") 49 | header = header.decode("UTF-8").strip() 50 | status, mime = header.split()[:2] 51 | # Handle input requests 52 | if status.startswith("1"): 53 | # Prompt 54 | query = input("INPUT" + mime + "> ") 55 | url += "?" + urllib.parse.quote(query) # Bit lazy... 56 | # Follow redirects 57 | elif status.startswith("3"): 58 | url = absolutise_url(url, mime) 59 | parsed_url = urllib.parse.urlparse(url) 60 | # Otherwise, we're done. 61 | else: 62 | break 63 | # Fail if transaction was not successful 64 | if status.startswith("2"): 65 | if mime.startswith("text/"): 66 | # Decode according to declared charset 67 | m = email.message.Message() 68 | m.get_params() 69 | body = fp.read() 70 | body = body.decode(m.get_param("charset", "UTF-8")) 71 | print(body, end="") 72 | else: 73 | print(fp.read(), end="") 74 | --------------------------------------------------------------------------------