Merge branch 'master' of git.goatpr0n.de:jpk/sbeam
* 'master' of git.goatpr0n.de:jpk/sbeam: requirements Remove tui poc code from master Catch connection errors Directory structure refactoring Fixed setup script Updated documentation Request jsonlist instead of superlist Automatic request handler route registration Fixed Instance name Added version to setup and sbeam
This commit is contained in:
commit
ab3325616c
|
@ -1,6 +1,9 @@
|
|||
*.pyc
|
||||
*.egg-info
|
||||
|
||||
.python-version
|
||||
|
||||
doc/
|
||||
build/
|
||||
dist/
|
||||
venv/
|
||||
|
|
4
Makefile
4
Makefile
|
@ -5,8 +5,8 @@
|
|||
# from the environment for the first two.
|
||||
SPHINXOPTS ?=
|
||||
SPHINXBUILD ?= sphinx-build
|
||||
SOURCEDIR = source
|
||||
BUILDDIR = build
|
||||
SOURCEDIR = doc/source
|
||||
BUILDDIR = doc
|
||||
|
||||
# Put it first so that "make" without argument is like "make help".
|
||||
help:
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
#
|
||||
import os
|
||||
import sys
|
||||
sys.path.insert(0, os.path.abspath('../src'))
|
||||
sys.path.insert(0, os.path.abspath('../../src'))
|
||||
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
|
@ -22,10 +22,10 @@ copyright = '2019, Julian Knauer'
|
|||
author = 'Julian Knauer'
|
||||
|
||||
# The short X.Y version
|
||||
version = '0.3'
|
||||
version = '0.5'
|
||||
|
||||
# The full version, including alpha/beta/rc tags
|
||||
release = '0.3'
|
||||
release = 'alpha'
|
||||
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
|
@ -6,11 +6,24 @@
|
|||
Welcome to SuperBeam documentation!
|
||||
===================================
|
||||
|
||||
|
||||
Command line tool
|
||||
-----------------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Contents:
|
||||
|
||||
sbeam
|
||||
|
||||
|
||||
SuperBeam Development
|
||||
---------------------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Contents:
|
||||
|
||||
SuperBeam
|
||||
SuperBeam.RequestHandlers
|
||||
|
4
make.bat
4
make.bat
|
@ -7,8 +7,8 @@ REM Command file for Sphinx documentation
|
|||
if "%SPHINXBUILD%" == "" (
|
||||
set SPHINXBUILD=sphinx-build
|
||||
)
|
||||
set SOURCEDIR=source
|
||||
set BUILDDIR=build
|
||||
set SOURCEDIR=doc\source
|
||||
set BUILDDIR=doc
|
||||
|
||||
if "%1" == "" goto help
|
||||
|
||||
|
|
|
@ -1,7 +1,3 @@
|
|||
certifi==2019.6.16
|
||||
chardet==3.0.4
|
||||
Click==7.0
|
||||
idna==2.8
|
||||
PyQRCode==1.2.1
|
||||
requests==2.22.0
|
||||
urllib3==1.25.3
|
||||
|
|
12
setup.py
12
setup.py
|
@ -1,10 +1,18 @@
|
|||
import sys
|
||||
from setuptools import setup, find_namespace_packages
|
||||
from distutils.util import convert_path
|
||||
|
||||
sys.path.insert(0, 'src/')
|
||||
|
||||
main_ns = {}
|
||||
ver_path = convert_path('src/sbeam.py')
|
||||
with open(ver_path) as ver_file:
|
||||
exec(ver_file.read(), main_ns)
|
||||
|
||||
setup(
|
||||
name="sbeam",
|
||||
description="",
|
||||
version="0.3",
|
||||
description="SuperBeam CLI Client",
|
||||
version=main_ns['__VERSION__'],
|
||||
author="JayPiKay",
|
||||
author_email="jpk+python@goatpr0n.de",
|
||||
url='https://git.goatpr0n.de/',
|
||||
|
|
|
@ -1,15 +1,12 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""SuperBeam Handlers
|
||||
|
||||
SuperBeam Handlers define how to handle certain requests to the
|
||||
`SuperBeamServer`. Each handler needs to be registered in ThumbHandler
|
||||
`SuperBeamServer.do_GET()` function within the ``paths`` variable and imported
|
||||
in the top of the :mod:`SuperBeam` module.
|
||||
|
||||
The handlers itself are defined in this module.
|
||||
The handlers are automatically registered during the creation of the
|
||||
:class:`SuperBeamServer` instance.
|
||||
|
||||
Each class handler method needs to be a ``staticmethod``. The callee will call
|
||||
the `handle()` with the ``httpd`` instance of the :class:`SuperBeamServer`.
|
||||
the `handle()` with the ``httpd`` instance of the :class:`SuperBeamServer` as
|
||||
required argument.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
@ -33,15 +30,20 @@ class OctetStreamHandler(object):
|
|||
@staticmethod
|
||||
def handle(httpd):
|
||||
raise Exception(f'Not implemented: Unable to handle {httpd} '
|
||||
'({StreamHandler.mimetype}')
|
||||
'({OctetStreamHandler.mimetype}')
|
||||
|
||||
|
||||
class ChunkTemplateHandler(BaseTextHandler):
|
||||
pass
|
||||
"""TODO
|
||||
"""
|
||||
URI = ('/', '/index.htm', 'index')
|
||||
|
||||
|
||||
class ThumbHandler(object):
|
||||
"""TODO
|
||||
"""
|
||||
mimetype = 'image/png'
|
||||
URI = ('/getthumb',)
|
||||
|
||||
@staticmethod
|
||||
def handle(httpd):
|
||||
|
@ -54,7 +56,6 @@ class LegacyListHandler(BaseTextHandler):
|
|||
|
||||
URI: ``/superlist``
|
||||
"""
|
||||
|
||||
URI = ('/superlist',)
|
||||
|
||||
@staticmethod
|
||||
|
@ -110,11 +111,16 @@ class JsonListHandler(object):
|
|||
|
||||
|
||||
class SingleFileHandler(OctetStreamHandler):
|
||||
pass
|
||||
"""TODO
|
||||
"""
|
||||
URI = ('/get/',)
|
||||
|
||||
|
||||
class ZipFileHandler(object):
|
||||
"""TODO
|
||||
"""
|
||||
mimetype = 'application/zip'
|
||||
URI = ('/getzip',)
|
||||
|
||||
@staticmethod
|
||||
def handle(httpd):
|
||||
|
@ -130,6 +136,7 @@ class SuperStreamHandler(OctetStreamHandler):
|
|||
|
||||
URI: ``/getstream``
|
||||
"""
|
||||
URI = ('/getstream',)
|
||||
|
||||
@staticmethod
|
||||
def _get_streamsize(files):
|
||||
|
@ -160,11 +167,16 @@ class SuperStreamHandler(OctetStreamHandler):
|
|||
|
||||
|
||||
class AssetHandler(OctetStreamHandler):
|
||||
pass
|
||||
"""TODO
|
||||
"""
|
||||
URI = ('*',)
|
||||
|
||||
|
||||
class ApkRequestHandler(object):
|
||||
"""TODO
|
||||
"""
|
||||
mimetype = 'application/vnd.android.package-archive'
|
||||
URI = ('/getapk',)
|
||||
|
||||
@staticmethod
|
||||
def handle(httpd):
|
||||
|
|
|
@ -8,12 +8,8 @@ import socket
|
|||
import struct
|
||||
import base64
|
||||
|
||||
|
||||
from SuperBeam.RequestHandlers import (
|
||||
ChunkTemplateHandler, ThumbHandler, ApkRequestHandler, LegacyListHandler,
|
||||
SingleFileHandler, ZipFileHandler, SuperStreamHandler, AssetHandler,
|
||||
JsonListHandler
|
||||
)
|
||||
import inspect
|
||||
from SuperBeam import RequestHandlers
|
||||
|
||||
|
||||
class SuperBeamServer(BaseHTTPRequestHandler):
|
||||
|
@ -24,6 +20,9 @@ class SuperBeamServer(BaseHTTPRequestHandler):
|
|||
Class is built on top of :mod:`http.server.BaseHTTPRequestHandler` as
|
||||
simple http server.
|
||||
|
||||
During initialization, the RequestHandlers are registered as routable
|
||||
paths.
|
||||
|
||||
Call :func:`serve_forever()` to start the HTTP server. The mainloop will
|
||||
serve a list of files given as argument to :func:`serve_forever()`.
|
||||
|
||||
|
@ -37,9 +36,14 @@ class SuperBeamServer(BaseHTTPRequestHandler):
|
|||
files (list): list of files served by the `SuperBeamServer`.
|
||||
"""
|
||||
|
||||
def __init__(self, debug=False, *args, **kwargs):
|
||||
def __init__(self, request, client_address, server, debug=False):
|
||||
self.debug = debug
|
||||
super(SuperBeamServer, self).__init__()
|
||||
self.routes = {}
|
||||
for _, cls in inspect.getmembers(RequestHandlers, inspect.isclass):
|
||||
if hasattr(cls, 'URI'):
|
||||
for route in cls.URI:
|
||||
self.routes[route] = cls
|
||||
super(SuperBeamServer, self).__init__(request, client_address, server)
|
||||
|
||||
def do_GET(self):
|
||||
"""GET request handler
|
||||
|
@ -53,26 +57,12 @@ class SuperBeamServer(BaseHTTPRequestHandler):
|
|||
exists - forwarded to :func:`respond()`, where the request gets
|
||||
answered.
|
||||
"""
|
||||
# TODO Add globbing
|
||||
if self.debug:
|
||||
print(threading.current_thread())
|
||||
paths = {
|
||||
'index': ChunkTemplateHandler,
|
||||
'/': ChunkTemplateHandler,
|
||||
'/index.htm': ChunkTemplateHandler, # TODO handler2
|
||||
'/getthumb': ThumbHandler,
|
||||
'/light': ChunkTemplateHandler, # TODO handler2
|
||||
'/superlist': LegacyListHandler,
|
||||
'/jsonlist': JsonListHandler,
|
||||
'/get/': SingleFileHandler,
|
||||
'/getapk': ApkRequestHandler,
|
||||
'/getzip': ZipFileHandler,
|
||||
'/getstream': SuperStreamHandler,
|
||||
'*': AssetHandler,
|
||||
}
|
||||
|
||||
if self.path in paths: # TODO startswith key check
|
||||
self.respond(paths[self.path])
|
||||
# TODO Add globbing
|
||||
if self.path in self.routes: # TODO startswith key check
|
||||
self.respond(self.routes[self.path])
|
||||
|
||||
def respond(self, handler):
|
||||
"""Respond client with request with registered handler.
|
||||
|
|
|
@ -1,159 +0,0 @@
|
|||
import asyncio
|
||||
|
||||
import urwid
|
||||
|
||||
import weakref
|
||||
|
||||
import struct
|
||||
import base64
|
||||
import pyqrcode
|
||||
import socket
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
|
||||
def unhandled(key):
|
||||
if key == 'ctrl c':
|
||||
raise urwid.ExitMainLoop
|
||||
|
||||
|
||||
def get_primary_ip():
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
try:
|
||||
# doesn't even have to be reachable
|
||||
s.connect(('10.255.255.255', 1))
|
||||
IP = s.getsockname()[0]
|
||||
except Exception as exc:
|
||||
print(exc)
|
||||
IP = '127.0.0.1'
|
||||
finally:
|
||||
s.close()
|
||||
return IP
|
||||
|
||||
|
||||
def get_qrcode():
|
||||
primary_ip = get_primary_ip()
|
||||
octets = [1]
|
||||
octets.extend([int(_) for _ in primary_ip.split('.')[::-1]])
|
||||
url = 'http://superbe.am/q?' + base64.b64encode(
|
||||
struct.pack('BBBBB', *octets)
|
||||
).decode('utf-8')
|
||||
return pyqrcode.create(url)
|
||||
|
||||
|
||||
class ClientModel(object):
|
||||
|
||||
def __init__(self):
|
||||
self.clients = ['127.0.0.1', '192.168.178.34']
|
||||
|
||||
def get_clients(self):
|
||||
return self.clients
|
||||
|
||||
|
||||
class SuperBeamView(urwid.WidgetWrap):
|
||||
|
||||
palette = [
|
||||
('body', 'black', 'light gray', 'standout'),
|
||||
('header', 'white', 'dark red', 'bold'),
|
||||
('screen edge', 'light blue', 'dark cyan'),
|
||||
('main shadow', 'dark gray', 'black'),
|
||||
('line', 'black', 'light gray', 'standout'),
|
||||
('bg background', 'light gray', 'black'),
|
||||
('bg 1', 'black', 'dark blue', 'standout'),
|
||||
('bg 1 smooth', 'dark blue', 'black'),
|
||||
('bg 2', 'black', 'dark cyan', 'standout'),
|
||||
('bg 2 smooth', 'dark cyan', 'black'),
|
||||
('button normal', 'light gray', 'dark blue', 'standout'),
|
||||
('button select', 'white', 'dark green'),
|
||||
('line', 'black', 'light gray', 'standout'),
|
||||
('pg normal', 'white', 'black', 'standout'),
|
||||
('pg complete', 'white', 'dark magenta'),
|
||||
('pg smooth', 'dark magenta', 'black'),
|
||||
('qr code', 'black', 'white'),
|
||||
]
|
||||
|
||||
def __init__(self, controller):
|
||||
self.controller = controller
|
||||
self.client_model = ClientModel()
|
||||
urwid.WidgetWrap.__init__(self, self.main_window())
|
||||
|
||||
def main_shadow(self, w):
|
||||
bg = urwid.AttrWrap(urwid.SolidFill(u"\u2592"), 'screen edge')
|
||||
shadow = urwid.AttrWrap(urwid.SolidFill(u" "), 'main shadow')
|
||||
|
||||
bg = urwid.Overlay(shadow, bg,
|
||||
('fixed left', 3), ('fixed right', 1),
|
||||
('fixed top', 2), ('fixed bottom', 1))
|
||||
w = urwid.Overlay(w, bg,
|
||||
('fixed left', 2), ('fixed right', 3),
|
||||
('fixed top', 1), ('fixed bottom', 2))
|
||||
return w
|
||||
|
||||
def update_clock(self, widget_ref):
|
||||
widget = widget_ref()
|
||||
if not widget:
|
||||
return
|
||||
widget.set_text(datetime.now().isoformat())
|
||||
loop.call_later(1, self.update_clock, widget_ref)
|
||||
|
||||
def qrcode(self):
|
||||
qrcode = get_qrcode().terminal()
|
||||
qrcode = qrcode.replace('\x1b[0m\x1b[49m ', '██')
|
||||
qrcode = qrcode.replace('\x1b[0m\x1b[7m ', ' ')
|
||||
qrcode = qrcode.replace('\x1b[0m', '')
|
||||
qrcode = qrcode.replace('\x1b[7m', '')
|
||||
widgets = []
|
||||
for row in qrcode.split('\n'):
|
||||
widgets.append(urwid.AttrWrap(urwid.Text(row), 'qr code'))
|
||||
w = urwid.Pile(widgets)
|
||||
return urwid.Filler(urwid.AttrWrap(w, 'body'))
|
||||
|
||||
def client_list(self):
|
||||
clients = []
|
||||
for client in self.client_model.get_clients():
|
||||
clients.append(urwid.Filler(urwid.AttrWrap(urwid.Text(client),
|
||||
'line')))
|
||||
return urwid.Pile([(1, _) for _ in clients])
|
||||
|
||||
def main_window(self):
|
||||
header = urwid.AttrWrap(urwid.Text('SuperBeam Tui'), 'header')
|
||||
clock = urwid.Text('')
|
||||
self.update_clock(weakref.ref(clock))
|
||||
footer = urwid.AttrWrap(clock, 'header')
|
||||
|
||||
body = urwid.Pile([(44, self.qrcode()), self.client_list()])
|
||||
|
||||
window = urwid.Frame(body, header, footer)
|
||||
return self.main_shadow(window)
|
||||
|
||||
|
||||
class SuperBeamController(object):
|
||||
|
||||
def __init__(self):
|
||||
self.model = ClientModel()
|
||||
self.view = SuperBeamView(self)
|
||||
|
||||
def run(self):
|
||||
# self.loop = urwid.MainLoop(self.view, self.view.palette)
|
||||
self.loop = urwid.MainLoop(
|
||||
self.view, self.view.palette,
|
||||
event_loop=urwid.AsyncioEventLoop(loop=loop),
|
||||
unhandled_input=unhandled
|
||||
)
|
||||
self.loop.run()
|
||||
|
||||
|
||||
def run():
|
||||
screen = urwid.raw_display.Screen()
|
||||
size = screen.get_cols_rows()
|
||||
if size[0] < 87 or size[1] < 58:
|
||||
raise Exception('Not enough screen space to render QR code.')
|
||||
else:
|
||||
SuperBeamController().run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
run()
|
40
src/sbeam.py
40
src/sbeam.py
|
@ -6,24 +6,6 @@
|
|||
#
|
||||
# Distributed under terms of the MIT license.
|
||||
|
||||
"""
|
||||
> GET /getstream HTTP/1.1
|
||||
> Host: 192.168.178.50:8080
|
||||
> User-Agent: sbeam
|
||||
> Accept: */*
|
||||
>
|
||||
* Mark bundle as not supporting multiuse
|
||||
< HTTP/1.1 200 OK
|
||||
< ylfrettub: 1
|
||||
< Content-Disposition: attachment; filename="RedHotChillyStream"
|
||||
< Date: Fri, 09 Aug 2019 12:57:20 GMT
|
||||
< Content-Length: 8137967
|
||||
< Content-Type: application/octet-stream
|
||||
|
||||
Server QR Code = http://superbe.am/q?ATKyqMA=
|
||||
Base64 = 01 32 b2 a8 c0 = 01 50 178 168 192
|
||||
"""
|
||||
|
||||
import os
|
||||
import click
|
||||
import requests
|
||||
|
@ -31,7 +13,11 @@ import requests
|
|||
import SuperBeam
|
||||
|
||||
|
||||
__VERSION__ = '0.5'
|
||||
|
||||
|
||||
@click.group()
|
||||
@click.version_option(version=__VERSION__, prog_name='SuperBeam CLI')
|
||||
def cli():
|
||||
pass
|
||||
|
||||
|
@ -54,12 +40,12 @@ def serve(recursive, filename):
|
|||
|
||||
@cli.command()
|
||||
@click.argument('host', default='127.0.0.1')
|
||||
def superlist(host):
|
||||
"""Fetch superlist from HOST running SupberBeam.
|
||||
def jsonlist(host):
|
||||
"""Fetch jsonlist from HOST running SupberBeam.
|
||||
|
||||
HOST is the IP of the SuperBeam server.
|
||||
"""
|
||||
with requests.get(f'http://{host}:8080/superlist',
|
||||
with requests.get(f'http://{host}:8080/jsonlist',
|
||||
headers={'User-Agent': 'sbeam'}) as response:
|
||||
assert response.status_code == 200
|
||||
return response.text.split('\n')
|
||||
|
@ -101,8 +87,8 @@ def receive(host, destination):
|
|||
fullpath = os.path.join(destination, filepath[1:])
|
||||
os.makedirs(fullpath, exist_ok=True)
|
||||
with open(os.path.join(fullpath, filename), 'wb') as fd:
|
||||
# TODO split filesize in chunks with remainder
|
||||
fd.write(response.raw.read(int(filesize)))
|
||||
bar.update(len(filelist))
|
||||
|
||||
|
||||
@cli.command()
|
||||
|
@ -146,10 +132,8 @@ def split_stream(filename, superlist, directory):
|
|||
handle.write(blob.read(int(filesize)))
|
||||
|
||||
|
||||
@cli.command()
|
||||
def tui():
|
||||
SuperBeam.tui.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
cli()
|
||||
try:
|
||||
cli()
|
||||
except requests.exceptions.ConnectionError as conerr:
|
||||
print(f'Failed to connect. Target alive?')
|
||||
|
|
Loading…
Reference in New Issue