repo: gemcall action: commit revision: path_from: revision_from: 7c3c3b42ac0ea50ef8d79ac6359199a2f6c4cfe1: path_to: revision_to:
commit 7c3c3b42ac0ea50ef8d79ac6359199a2f6c4cfe1 Author: Björn WärmedalDate: Sun Jul 18 16:29:06 2021 +0200 Made installable by pip. diff --git a/gemcall b/gemcall deleted file mode 100755 index 5546e94079b94f986b5f9d947911dcc77c29e9ad..0000000000000000000000000000000000000000 --- a/gemcall +++ /dev/null @@ -1,92 +0,0 @@ -#!/usr/bin/env python3 -# vim: tabstop=4 shiftwidth=4 expandtab - -import socket -import urllib.parse -import ssl -import argparse -import sys -from cryptography import x509 -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives.serialization import Encoding,PublicFormat - - - -class Response(): - def __init__(self, socket: socket.socket) -> "Response": - self._socket = socket - - cert = self._socket.getpeercert(binary_form=True) - certobj = x509.load_der_x509_certificate(cert, default_backend()) - pubkey = certobj.public_key() - - self.serverpubkey = pubkey.public_bytes(Encoding('PEM'), PublicFormat('X.509 subjectPublicKeyInfo with PKCS#1')) - self._filehandle = self._socket.makefile(mode = "rb") - - # Two code digits, one space and a maximum of 1024 bytes of meta info. - try: - self.responsecode, self.meta = self._filehandle.readline(1027).split(maxsplit=1) - self.responsecode = int(self.responsecode) - self.meta = self.meta.strip().decode("UTF-8") - except: - self.discard() - raise RuntimeError("Received malformed header from gemini server") - - def read(self, bufsize: int = 4096) -> "bytes": - return self._filehandle.read(bufsize) - - def discard(self) -> None: - self._filehandle.close() - self._socket.close() - -def request(url: str = "", clientcert: str = None, clientkey: str = None, timeout: int = 3) -> "Response": - parsed = urllib.parse.urlparse(url) - context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) - context.minimum_version = ssl.TLSVersion.TLSv1_2 - context.check_hostname = False - context.verify_mode = ssl.CERT_NONE - if (clientcert and clientkey): - context.load_cert_chain(clientcert, clientkey) - - sock = socket.create_connection((parsed.hostname, parsed.port or 1965)) - ssock = context.wrap_socket(sock, server_hostname=parsed.hostname) - ssock.sendall((url+"\r\n").encode("UTF-8")) - - return Response(ssock) - -if __name__ == "__main__": - - parser = argparse.ArgumentParser(description="Python module/CLI program for making network requests with the gemini protocol.") - parser.add_argument("-c","--clientcert",help="Path to client certificate. This is optional, but must be used when -k/--clientkey is used.") - parser.add_argument("-k","--clientkey",help="Path to the private key file for a client certificate. This is optional, but must be used when -c/--clientcert is used.") - parser.add_argument("-u","--url",help="Fully qualified URL to fetch.") - parser.add_argument("-o","--outputfile",help="File to output response body to.") - parser.add_argument("-t","--timeout",help="Timeout of connection attempt, in seconds. Default is 3.",default=3,type=int) - parser.add_argument("-q","--quiet",help="Don't print response header.",action="store_true") - parser.add_argument("-n","--nobody",help="Only print response header.",action="store_true") - parser.add_argument("-s","--stdoutonly",help="Print everything to stdout",action="store_true") - args = parser.parse_args() - - if not args.url: - parser.print_help() - else: - responseobj = request(url = args.url, clientcert = args.clientcert, clientkey = args.clientkey, timeout = args.timeout) - - if not args.quiet: - header = str.encode("%s %s\n" % (responseobj.responsecode, responseobj.meta)) - if args.stdoutonly: - sys.stdout.buffer.write(header) - else: - sys.stderr.buffer.write(header) - outputfile = open(args.outputfile, "wb") if args.outputfile else None - while not args.nobody: - buf = responseobj.read() - if len(buf) == 0: - break - elif outputfile: - outputfile.write(buf) - outputfile.close() - else: - sys.stdout.buffer.write(buf) - - responseobj.discard() diff --git a/gemcall/__init__.py b/gemcall/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..7d3cb203ef94324d7bfd1f24fb8e42f1b32ffe9d --- /dev/null +++ b/gemcall/__init__.py @@ -0,0 +1 @@ +from .gemcall import request, Response diff --git a/gemcall/__main__.py b/gemcall/__main__.py new file mode 100755 index 0000000000000000000000000000000000000000..9b58ad95392aafb37f65b3e7e7ec41a60c790a3f --- /dev/null +++ b/gemcall/__main__.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +# vim: tabstop=4 shiftwidth=4 expandtab + +import argparse +import sys +import gemcall + +if __name__ == "__main__": + + parser = argparse.ArgumentParser(prog="gemcall",description="Python module/CLI program for making network requests with the gemini protocol.") + parser.add_argument("-c","--clientcert",help="Path to client certificate. This is optional, but must be used when -k/--clientkey is used.") + parser.add_argument("-k","--clientkey",help="Path to the private key file for a client certificate. This is optional, but must be used when -c/--clientcert is used.") + parser.add_argument("-u","--url",help="Fully qualified URL to fetch.") + parser.add_argument("-o","--outputfile",help="File to output response body to.") + parser.add_argument("-t","--timeout",help="Timeout of connection attempt, in seconds. Default is 3.",default=3,type=int) + parser.add_argument("-q","--quiet",help="Don't print response header.",action="store_true") + parser.add_argument("-n","--nobody",help="Only print response header.",action="store_true") + parser.add_argument("-s","--stdoutonly",help="Print everything to stdout",action="store_true") + args = parser.parse_args() + + if not args.url: + parser.print_help() + else: + responseobj = gemcall.request(url = args.url, clientcert = args.clientcert, clientkey = args.clientkey, timeout = args.timeout) + + if not args.quiet: + header = str.encode("%s %s\n" % (responseobj.responsecode, responseobj.meta)) + if args.stdoutonly: + sys.stdout.buffer.write(header) + else: + sys.stderr.buffer.write(header) + outputfile = open(args.outputfile, "wb") if args.outputfile else None + while not args.nobody: + buf = responseobj.read() + if len(buf) == 0: + break + elif outputfile: + outputfile.write(buf) + outputfile.close() + else: + sys.stdout.buffer.write(buf) + + responseobj.discard() diff --git a/gemcall/gemcall.py b/gemcall/gemcall.py new file mode 100755 index 0000000000000000000000000000000000000000..f7e0d60792991f62eea320757ab503fba26e082e --- /dev/null +++ b/gemcall/gemcall.py @@ -0,0 +1,51 @@ +# vim: tabstop=4 shiftwidth=4 expandtab + +import socket +import urllib.parse +import ssl +from cryptography import x509 +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.serialization import Encoding,PublicFormat + +class Response(): + def __init__(self, socket: socket.socket) -> "Response": + self._socket = socket + + cert = self._socket.getpeercert(binary_form=True) + certobj = x509.load_der_x509_certificate(cert, default_backend()) + pubkey = certobj.public_key() + + self.serverpubkey = pubkey.public_bytes(Encoding('PEM'), PublicFormat('X.509 subjectPublicKeyInfo with PKCS#1')) + self._filehandle = self._socket.makefile(mode = "rb") + + # Two code digits, one space and a maximum of 1024 bytes of meta info. + try: + self.responsecode, self.meta = self._filehandle.readline(1027).split(maxsplit=1) + self.responsecode = int(self.responsecode) + self.meta = self.meta.strip().decode("UTF-8") + except: + self.discard() + raise RuntimeError("Received malformed header from gemini server") + + def read(self, bufsize: int = 4096) -> "bytes": + return self._filehandle.read(bufsize) + + def discard(self) -> None: + self._filehandle.close() + self._socket.close() + +def request(url: str = "", clientcert: str = None, clientkey: str = None, timeout: int = 3) -> "Response": + parsed = urllib.parse.urlparse(url) + context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + context.minimum_version = ssl.TLSVersion.TLSv1_2 + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + if (clientcert and clientkey): + context.load_cert_chain(clientcert, clientkey) + + sock = socket.create_connection((parsed.hostname, parsed.port or 1965)) + ssock = context.wrap_socket(sock, server_hostname=parsed.hostname) + ssock.sendall((url+"\r\n").encode("UTF-8")) + + return Response(ssock) + diff --git a/setup.py b/setup.py new file mode 100644 index 0000000000000000000000000000000000000000..51849e94129bf3628bd1558b075d9cb7db8725e7 --- /dev/null +++ b/setup.py @@ -0,0 +1,32 @@ +from setuptools import setup + +setup( + name='gemcall', + version='0.6', + description='A library and CLI tool for making gemini requests.', + url='https://notabug.org/tinyrabbit/gemcall', + author='Björn Wärmedal', + author_email='bjorn.warmedal@gmail.com', + license='MIT License', + packages=['gemcall'], + install_requires=[], + + classifiers=[ + 'Development Status :: 4 - Beta', + 'Intended Audience :: End Users/Desktop', + 'License :: OSI Approved :: MIT License', + 'Operating System :: POSIX :: Linux', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Topic :: Internet :: Gemini', + 'Topic :: Software Development :: Libraries', + 'Topic :: Utilities', + 'Typing :: Typed', + ], +) +
-----END OF PAGE-----