add support for multiple password backends

This commit is contained in:
Izalia Mae 2022-08-10 18:02:20 -04:00
parent 3dd8f17c71
commit ffa4d71ce2
30 changed files with 2005 additions and 141 deletions

View file

@ -18,6 +18,7 @@ clean:
find . -name '*.pyc' -exec rm --force {} +
find . -name '*.pyo' -exec rm --force {} +
rm --recursive --force $(VENV)
rm barkshark_web/bin/bw
build-ext:
mkdir --parent $(SRCNAME)/bin
@ -28,6 +29,12 @@ clean-ext:
rm --force $(PY_LOADER)
rm --recursive --force webextension/build
dl-bw:
wget "https://vault.bitwarden.com/download/?app=cli&platform=linux" -O /tmp/bw.zip
unzip /tmp/bw.zip -d barkshark_web/bin/
rm /tmp/bw.zip
chmod +x barkshark_web/bin/bw
aptdeps:
sudo apt install `cat apt-requirements.txt | xargs` --no-install-recommends -y

View file

@ -42,6 +42,7 @@ izzylib.add_builtins(
get_app = lambda : Gio.Application.get_default()
)
scriptpath = Path(__file__).resolve().parent
proto = 'pyweb'
var = ObjectBase(
readonly_props = True,

View file

@ -9,13 +9,14 @@ from urllib.parse import quote
from .web_context import WebContext
from .window import Window
from .. import dbus, var
from .. import dbus, scriptpath, var
from .. import __version__ as version, __software__ as swname
from ..cookies import get_cookie_db
from ..database import get_database
from ..enums import LibraryPage
from ..exceptions import AccountNotFoundError, NoAccountsError
from ..functions import connect
from ..passwords import PASSWORD_STORAGE
class Application(Gtk.Application):
@ -35,6 +36,7 @@ class Application(Gtk.Application):
self.cookies = None
self.context = None
self.window = None
self.password = None
self.setup(profile)
@ -45,7 +47,6 @@ class Application(Gtk.Application):
def setup(self, profile):
script = Path(__file__).parent.parent
data = Path.app_data_dir('config', 'barkshark', 'web')
profilepath = data.join(profile or 'DEFAULT')
@ -55,11 +56,11 @@ class Application(Gtk.Application):
# misc paths
downloads = Path('~/Downloads'),
data = data,
script = script,
script = scriptpath,
# browser paths
localweb = script.join('localweb'),
resources = script.join('resources'),
localweb = scriptpath.join('localweb'),
resources = scriptpath.join('resources'),
# data paths
profile = profilepath,
@ -126,6 +127,39 @@ class Application(Gtk.Application):
})
def setup_password_storage(self, passtype=None):
if self.password:
self.password.stop()
self.password = None
with self.db.session as s:
config = s.get_config()
if not passtype:
passtype = config.pass_type
storeclass = PASSWORD_STORAGE[passtype]
if passtype == 'bitwarden':
self.password = storeclass(
host = config.bw_host,
port = config.bw_port,
external = config.bw_external,
session_key = config.bw_session_key
)
self.password.start()
elif passtype == 'gnome':
self.password = storeclass()
elif passtype == 'default':
self.password = storeclass(self.db)
else:
raise TypeError(f'Not a valid password type: {passtype}')
def get_account_by_handle(self, username, domain=None):
if not len(self.accounts):
raise NoAccountsError('No accounts')
@ -186,6 +220,8 @@ class Application(Gtk.Application):
self.add_window(self.window)
self.setup_password_storage()
with self.db.session as s:
self.accounts = s.fetch('accounts').all()

View file

@ -34,11 +34,13 @@ class WebContext(WebKit2.WebContext):
## Register local uri schemes
self.security_manager = self.get_security_manager()
self.security_manager.register_uri_scheme_as_local(var.local) #Local webui
self.security_manager.register_uri_scheme_as_local('bsweb://')
self.security_manager.register_uri_scheme_as_local('local://') #Renamed "file://" handler
self.security_manager.register_uri_scheme_as_secure('oauth://')
## Setup custom protocols
self.register_uri_scheme(var.local_proto, protocol.Local)
self.register_uri_scheme('bsweb', protocol.LocalWeb)
self.register_uri_scheme('sftp', protocol.Sftp)
self.register_uri_scheme('source', protocol.Source)
self.register_uri_scheme(protocol.Oauth.protocol, protocol.Oauth)

View file

@ -328,7 +328,7 @@ class WebTab(BuilderBase, Gtk.Box):
def set_url(self, url=None):
self['navbar-url'].set_text(
url or self.webview.get_uri()
url or self.webview.get_uri() or ''
)

View file

@ -393,7 +393,7 @@ class WebviewHandler(ComponentBase):
if error.domain == 'WebKitPolicyError' and 'interrupted' in error.message.lower():
if not url.mimetype:
self.window.notification(f'Cannot open url: {url}')
self.window.notification(f'Cannot open url: {url}', 'warning')
elif not self.webview.can_show_mime_type(url.mimetype):
self.app.context.download_uri(url)

View file

@ -36,6 +36,7 @@ default_config = {
'load_tabs': (True, 'bool'),
'local_underline_links': (False, 'bool'),
'maximized': (True, 'bool'),
'pass_type': ('bitwarden', 'str'),
'post_check': (False, 'bool'),
'search': ('ddg', 'str'),
'scale': (1.0, 'float'),
@ -43,7 +44,13 @@ default_config = {
'location': (DotDict({'x': None, 'y': None}), 'dict'),
'tab_after_current': (True, 'bool'),
'tab_side': ('TOP', 'str'),
'theme': ('default', 'str')
'theme': ('default', 'str'),
## Bitwarden storage config
'bw_host': ('localhost', 'str'),
'bw_port': (None, 'int'),
'bw_external': (False, 'bool'),
'bw_session_key': (None, 'str')
}
default_permissions = {
@ -176,7 +183,6 @@ tables = {
Column('username', 'text', nullable=False),
Column('password', 'text', nullable=False),
Column('label', 'text', nullable=False),
Column('domain', 'text', nullable=False),
Column('url', 'text'),
Column('note', 'text'),
Column('created', 'datetime', nullable=False),
@ -294,10 +300,6 @@ class CustomSession(Session):
return self.get_cached('history', url, url=url)
def get_password(self, username, host):
return self.fetch('passwords', username=username, domain=host).one()
def get_permission(self, domain, cache=True):
row = self.get_cached('siteoptions', domain, domain = domain)
@ -454,59 +456,7 @@ class CustomSession(Session):
def put_history_from_tab(self, tab):
return self.put_history(tab.url, tab.title, (True if tab.fedi_post else False))
def put_password(self, username, domain, password, url=None, label=None, note=None):
data = DotDict(
username = username,
domain = domain,
password = password,
created = datetime.now()
)
for key, value in (('url', url), ('label', label), ('note', note)):
if value:
data[key] = value
if (row := self.get_password(username, domain)):
return self.put_password_row(row, **data)
if not data.get('label'):
data['label'] = f'{username} @ {domain}'
self.insert('passwords', **data)
return self.get_password(username, domain)
def put_password_row(self, row, **kwargs):
for key in kwargs.keys():
if key not in password_fields:
raise KeyError(f'Invalid password field: {key}')
kwargs.update({'modified': datetime.now()})
self.update_row(row, **kwargs)
row.update(**kwargs)
return row
def put_passfield(self, url, userfield, passfield):
if None in [userfield, passfield]:
logging.verbose('Not inserting empty login fields')
return
row = self.s.get.passfield(url)
data = {
'url': url.without_query,
'domain': url.domain,
'userfield': userfield,
'passfield': passfield
}
return self.put_cached('passfields', url.without_query, self.get_passfield(url), **data)
return self.put_history(tab.url, tab.title, (True if tab._data.post else False))
def put_permission(self, domain, key, value):
@ -578,10 +528,6 @@ class CustomSession(Session):
return rows_affected
def del_passfield(self, domain):
self.del_cached('passfields', domain, domain=domain)
def del_permission(self, domain):
self.del_cached('siteoptions', domain, domain=domain)

View file

@ -1,6 +1,77 @@
from enum import Enum
from enum import Enum, IntEnum
__all__ = []
def register(enum):
__all__.append(enum.__name__)
return enum
@register
class BitwardenFieldType(IntEnum):
TEXT = 0
HIDDEN = 1
BOOLEAN = 2
@register
class BitwardenItemType(IntEnum):
LOGIN = 1
NOTE = 2
CARD = 3
IDENTITY = 4
@register
class BitwardenLoginType(IntEnum):
AUTHENTICATOR = 0
EMAIL = 1
YUBIKEY = 3
@register
class BitwardenTemplateType(Enum):
ITEM = 'item'
FIELD = 'item.field'
LOGIN = 'item.login'
URL = 'item.login.uri'
CARD = 'item.card'
IDENTITY = 'item.identity'
NOTE = 'item.securenote'
FOLDER = 'folder'
COLLECTION = 'collection'
ITEM_COLLECTION = 'item-collections'
ORG_COLLECTION = 'org-collection'
@register
class BitwardenUserStatusType(IntEnum):
INVITED = 0
ACCEPTED = 1
CONFIRMED = 2
@register
class BitwardenUserType(IntEnum):
OWNER = 0
ADMIN = 1
USER = 2
MANAGER = 3
@register
class BitwardenUrlMatchType(IntEnum):
DOMAIN = 0
HOST = 1
STARTS_WITH = 2
EXACT = 3
REGEX = 4
NEVER = 5
@register
class EditAction(Enum):
COPY = WebKit2.EDITING_COMMAND_COPY
LINK = WebKit2.EDITING_COMMAND_CREATE_LINK
@ -13,6 +84,7 @@ class EditAction(Enum):
UNDO = WebKit2.EDITING_COMMAND_UNDO
@register
class Javascript(Enum):
SELECTION = 'window.getSelection().toString()'
EXEC = 'window.execCommand("{}")'
@ -21,6 +93,7 @@ class Javascript(Enum):
DELETE = 'document.activeElement.setRangeText("")'
@register
class LibraryPage(Enum):
HOME = ''
BOOKMARKS = 'bookmarks'
@ -34,6 +107,7 @@ class LibraryPage(Enum):
HELP = 'help'
@register
class WebviewContextActions(Enum):
AUDIO_COPY = WebKit2.ContextMenuAction.COPY_AUDIO_LINK_TO_CLIPBOARD
AUDIO_DOWNLOAD = WebKit2.ContextMenuAction.DOWNLOAD_AUDIO_TO_DISK

View file

@ -3,3 +3,13 @@ class NoAccountsError(Exception):
class AccountNotFoundError(Exception):
'Raise when a specific account is not found'
class BitwardenApiError(Exception):
'Raise when a Bitwarden command fails'
class NoPasswordError(Exception):
'Raise when there is not result when fetching a single password row'
def __init__(self, item_id):
Exception.__init__(self, f'No row for password with ID: {item_id}')
self.item_id = item_id

View file

@ -1,10 +1,18 @@
// General
function connect_event(name, signal, callback) {
const element = document.getElementById(name);
var element = document.getElementById(name);
element.addEventListener(signal, callback);
}
function trigger_event(name, type) {
var element = document.getElementById(name);
var event = new Event(type);
element.dispatchEvent(event);
}
function delete_item(base_url, id) {
request(`${base_url}/${id}`, (response, body) => {
if (response.status != 200) {return;}
@ -86,6 +94,8 @@ function handle_save_config(event) {
request(url, (response, body) => {
if (response.status == 200) {
console.log(`Set config: ${input.id}=${value}`);
} else {
console.log(`Failed to set config: ${input.id}`)
}
});
}

View file

@ -20,6 +20,29 @@
connect_event('{{key}}', 'change', handle_save_config);
-macro config_dropdown(label, key, value, options):
%label for='{{key}}' << {{label}}
%select id='{{key}}' name='{{key}}'
-for option in options:
-if value == option:
%option value='{{option}}' selected << {{option.title()}}
-else
%option value='{{option}}' << {{option.title()}}
%script type='application/javascript'
connect_event('{{key}}', 'change', handle_save_config)
-macro config_number(label, key, value, min=0, max=100):
%label for='{{key}}' << {{label}}
%input type='number' id='{{key}}' name='{{key}}' value='{{value}}' min='{{min}}' max='{{max}}'
%script type='application/javascript'
connect_event('{{key}}', 'focusout', handle_save_config);
connect_event('{{key}}', 'keypress', handle_key_enter);
-macro menu_item(label, path, icon):
%a.menu-item.grid-container href='{{var.local}}{{path}}'
%img.image.grid-item src='{{var.local}}/icon/{{icon}}' title='{{label}}'

View file

@ -24,9 +24,6 @@
%label.grid-item for='password' << Password
%input#password.grid-item id='password' class='full-width' type='password' name='password' placeholder='password' value='{{password.password}}'
%label.grid-item for='domain' << Domain
%input.grid-item id='domain' class='full-width' type='text' name='domain' placeholder='domain' value='{{password.domain}}'
%label.grid-item for='url' << Url
%input.grid-item id='url' class='full-width' type='text' name='url' placeholder='url' value='{{password.url or ""}}'
@ -37,7 +34,7 @@
%a.button onclick='javascript:document.getElementsByTagName("form")[0].submit();' << Save
-if password.id
%a.button onclick='wine.location="{{var.local}}/passwords/delete/{{password.id}}?redir=/passwords"' << Delete
%a.button onclick='window.location="{{var.local}}/passwords/delete/{{password.id}}?redir=/passwords"' << Delete
%a.button onclick='window.location.href="{{var.local}}/passwords"' << Cancel

View file

@ -5,7 +5,7 @@
%link rel='stylesheet' type='text/css' href='{{var.local}}/css/passwords.css'
-block content
%form.buttons
%form#search-form.buttons
%a.button onclick='toggle_all_details("item", true)' << Open
%a.button onclick='toggle_all_details("item", false)' << Close
%a.button onclick='window.location.href="{{var.local}}/passwords/edit"' << New

View file

@ -1,6 +1,6 @@
-set page = 'Preferences'
-extends 'base.haml'
-from 'macro.haml' import config_checkbox, config_dirpicker, config_textbox
-from 'macro.haml' import config_checkbox, config_textbox, config_dropdown, config_number
-block meta
%link rel='stylesheet' type='text/css' href='{{var.local}}/css/preferences.css'
@ -23,6 +23,36 @@
%p -> =config_checkbox('Load all tabs on startup', 'load_tabs', config.load_tabs)
%p -> =config_checkbox('Load tab when switching if it is unloaded', 'load_switch', config.load_switch)
%details.category.section open
%summary << Passwor Storage
.container
%p -> =config_dropdown('Password storage backend', 'pass_type', config.pass_type, pass_backends)
%details.category.section {{'open' if app.password.storage_type == 'bitwarden' else ''}}
%summary << Password Storage: Bitwarden
.container
-if app.password.storage_type == 'bitwarden'
%p
Account E-Mail:
%input type='text' value='{{app.password.account}}' disabled
%p
Unlocked:
-if app.password.session_key
%input type='checkbox' disabled checked
-else
%input type='checkbox' disabled
%hr
%p -> =config_textbox('Hostname for the api server to listen on', 'bw_host', config.bw_host)
%p
=config_number('Port for the api server to listen on', 'bw_port', config.bw_port, 1024, 65535)
%input type='button' onclick='document.getElementById("bw_port").value = "0";trigger_event("bw_port", "focusout")' value='Random'
%p -> =config_checkbox('Use an external api server', 'bw_external', config.bw_external)
%details.category.section open
%summary << Advanced
.container

View file

@ -0,0 +1,10 @@
PASSWORD_STORAGE = DotDict()
from .bitwarden import BitwardenStorage, BitwardenItem, BitwardenResult
from .gnome_keyring import GnomeKeyringStorage
from .sql import SqlStorage
PASSWORD_STORAGE['default'] = SqlStorage
PASSWORD_STORAGE['bitwarden'] = BitwardenStorage
PASSWORD_STORAGE['gnome'] = GnomeKeyringStorage

View file

@ -0,0 +1,257 @@
from izzylib.misc import class_name
from . import PASSWORD_STORAGE
from ..functions import TimeoutCallback, get_app, run_in_gui_thread
PASSWORD_KEYS = [
'id',
'label',
'username',
'password',
'domain',
'url',
'note',
'created',
'modified'
]
class PasswordResult:
def __init__(self, storage, items):
self._storage = storage
self._items = items
def __iter__(self):
for item in self._items:
yield self._storage.item_class(self._storage, item)
def all(self):
return tuple(item for item in self)
def one(self):
for item in self:
return item
class PasswordItem:
def __init__(self, storage, item):
self._storage = storage
self._data = item
def __repr__(self):
return f'{class_name(self)}("{self.label}", username="{self.username}", domain="{self.domain}")'
def __getitem__(self, key):
if key not in self.keys():
raise KeyError(key)
value = self._get_value(key)
return self.__default_parse(key, value)
def __setitem__(self, key, value):
if key in ['id', 'created', 'modified'] or key not in self.keys():
raise KeyError(key)
value = self.__default_parse(key, value)
self._set_value(key, value)
def __delitem__(self, key):
raise AttributeError('Cannot delete item values')
def __getattr__(self, key):
try:
return self.__getitem__(key)
except KeyError:
return object.__getattribute__(self, key)
def __setattr__(self, key, value):
try:
self.__setitem__(key, value)
except KeyError:
object.__setattr__(self, key, value)
def __default_parse(self, key, value):
if key == 'url' and not isinstance(value, (Url, type(None))):
return Url(value)
elif key in ['created', 'modified'] and not isinstance(value, (DateString, type(None))):
return DateString.new_http(value)
return self._parse_value(key, value)
def _parse_value(self, key, value):
return value
def _get_value(self, key):
return self._item[key]
def _set_value(self, key, value):
self._item[key] = value
def copy_password(self, timeout=60):
app = get_app()
app.set_clipboard_text(self.password)
timer = TimeoutCallback(timeout, run_in_gui_thread, app.handle_clipboard_clear_password, self.password)
timer.start()
def copy_username(self):
get_app().set_clipboard_text(self.username)
def insert_row(self, storage=None):
if self.id:
raise RuntimeError('Row already has an ID and is probably in the DB')
if storage:
return storage.insert(**self.to_dict())
return self._storage.insert(**self.to_dict())
def update_row(self, **kwargs):
self.update(**kwargs)
self._storage.update(self.id, **kwargs)
def remove_row(self):
self._storage.remove(self.id)
def to_dict(self, indent=None):
return DotDict({key: self[key] for key in self.keys()})
## dict methods
def items(self):
for key in self.keys():
yield (key, self[key])
def keys(self):
return PASSWORD_KEYS.copy()
def values(self):
return tuple(self[key] for key in self.keys())
def update(self, _data={}, **kwargs):
for key, value in [*_data.items(), *kwargs.items()]:
if key not in self.keys():
raise KeyError(f'Invalid PasswordItem key: {key}')
self[key] = value
class PasswordStorage:
result_class = PasswordResult
item_class = PasswordItem
@property
def app(self):
return get_app()
@property
def window(self):
return self.app.window
@property
def storage_type(self):
for key, value in PASSWORD_STORAGE.items():
if self.__class__ == value:
return key
def start(self):
pass
def stop(self):
pass
def _fetch(self, **kwargs):
pass
def lock(self):
pass
def unlock(self, password=None):
pass
def create_row(self, *args, **kwargs):
return self.item_class.new(*args, **kwargs, storage=self)
def parse_data(self, username=None, domain=None, password=None, url=None, note=None, label=None, **kwargs):
kwargs.update(
username = username,
domain = domain,
url = url,
note = note,
password = password,
label = label
)
for key, value in tuple(kwargs.items()):
if not value:
del kwargs[key]
elif key == 'url':
kwargs['url'] = Url(url)
return kwargs
def fetch(self, label=None, username=None, domain=None, **kwargs):
if not any((name, username, domain)):
raise ValueError('Name, username, or domain not provided')
if name:
kwargs['name'] = name
if username:
kwargs['username'] = username
if domain:
kwargs['domain'] = domain
return self.result_class(self, self._fetch(**kwargs))
def insert(self, name, username, domain, **kwargs):
pass
def update(self, row_id, **kwargs):
pass
def remove(self, row_id):
pass

View file

@ -0,0 +1,3 @@
from .item import BitwardenItem
from .result import BitwardenResult
from .storage import BitwardenStorage

View file

@ -0,0 +1,200 @@
from izzylib import logging
from izzylib.datestring import DateString
from izzylib.misc import convert_to_boolean
from ..base import PasswordItem
from ...enums import *
class BitwardenItem(PasswordItem):
def __init__(self, storage, item):
PasswordItem.__init__(self, storage, DotDict(item))
@classmethod
def new(cls, *args, **kwargs):
return cls.new_login(*args, **kwargs)
@classmethod
def new_login(cls, username, password, url, label=None, note=None, storage=None, **kwargs):
if not isinstance(url, Url):
url = Url(url)
data = DotDict(
type = BitwardenItemType.LOGIN,
organizationId = kwargs.get('organization_id'),
collectionIds = kwargs.get('collection_ids'),
name = label or url.domain,
notes = note,
favorite = convert_to_boolean(kwargs.get('favourite')),
reprompt = 0,
login = DotDict(
username = username,
password = password,
totp = None,
uris = [
DotDict(match=BitwardenUrlMatchType.DOMAIN, uri=url)
]
),
fields = [
DotDict(
name = 'created',
value = kwargs.get('created') or DateString.now('http'),
type = BitwardenFieldType.TEXT
)
]
)
return cls(storage, data)
def _get_value(self, key):
if key == 'id':
return self._data['id']
elif key == 'label':
return self._data['name']
elif key == 'url':
try:
return self._data['login']['uris'][0]['uri']
except KeyError:
return
elif key == 'domain':
try:
return self.url.domain
except AttributeError:
return None
elif key == 'username':
return self._data['login']['username']
elif key == 'password':
return self._data['login']['password']
elif key == 'note':
return self._data['notes']
elif key == 'created':
return self.get_field('created')
elif key == 'modified':
return self._data['revisionDate']
elif key == 'deleted':
return self._data['deletedDate']
def _set_value(self, key, value):
if key == 'label':
self._data['name'] = value
elif key == 'url':
try:
self._data['login']['uris'][0]['uri'] = value
except (KeyError, IndexError):
self._data['login']['uris'] = [{'uri': value, 'match': BitwardenUrlMatchType.DOMAIN}]
elif key == 'username':
try:
self._data['login']['username'] = value
except KeyError:
self._data['login'] = {'username': value}
elif key == 'password':
try:
self._data['login']['password'] = value
except KeyError:
self._data['login'] = {'password': value}
elif key == 'note':
self._data['notes'] = value
elif key == 'created':
self.set_field(key, value)
elif key == 'modified':
self._data['revisionDate'] = value
elif key == 'deleted':
self._data['deletedDate'] = value
elif key in ['id', 'domain']:
pass
else:
raise KeyError(key)
@property
def trash(self):
return True if self.deleted else False
def compare(self, **kwargs):
for key, value in kwargs.items():
item_value = self[key]
if item_value != value and (isinstance(item_value, str) and value not in item_value):
#print(value, item_value, item_value != value, (isinstance(item_value, str) and value not in item_value))
return False
return True
def get_field(self, key, default=None):
try:
for item in self._data['fields']:
if item['name'].lower() == key.lower():
return item['value']
except AttributeError:
pass
return default
def set_field(self, key, value, type=BitwardenFieldType.TEXT):
if not isinstance(self._data['fields'], list):
self._data['fields'] = []
for item in self._data['fields']:
if item['name'].lower() == key.lower():
item['value'] = value
item['type'] = type
return
self._data['fields'].append(dict(
name = key,
value = value,
type = type
))
def generate_password(self, *args, **kwargs):
self._data.login.password = self._storage.generate_password(*args, **kwargs)
def generate_passphrase(self, *args, **kwargs):
self._data.login.password = self._storage.generate_passphrase(*args, **kwargs)
def purge(self):
self._storage.purge(self.id)
def to_json(self, indent=None):
return self._data.to_json(indent)
def update_row(self, **kwargs):
self.update(kwargs)
self._data = self._storage.send(f'object/item/{self.id}', 'PUT', self.to_json())

View file

@ -0,0 +1,6 @@
from ..base import PasswordResult
class BitwardenResult(PasswordResult):
def __iter__(self):
for item in self._items:
yield item

View file

@ -0,0 +1,439 @@
import time
from datetime import datetime, timedelta
from izzylib import logging
from izzylib.datestring import DateString
from izzylib.exceptions import HttpClientError
from izzylib.http_client import HttpClient
from izzylib.misc import convert_to_boolean, convert_to_string, random_port
from json.decoder import JSONDecodeError
from nodejs import node, npm
from .item import BitwardenItem
from .result import BitwardenResult
from ..base import PasswordItem, PasswordStorage
from ... import scriptpath
from ...enums import *
from ...exceptions import BitwardenApiError, NoPasswordError
bwbin = scriptpath.join('bin/bw')
bwjs = scriptpath.parent.join('node_modules/@bitwarden/cli/build/bw.js')
def bool_str(value):
if convert_to_boolean(value):
return 'true'
return 'false'
def check_deps():
if not bwjs.exists():
npm.run(['install', '@bitwarden/cli'])
class BitwardenStorage(PasswordStorage):
item_class = BitwardenItem
result_class = BitwardenResult
def __init__(self, host='localhost', port=None, external=False, session_key=None):
self._proc = None
self._account = None
self._current_port = None
self._client = HttpClient(headers={
'Accept': 'application/json',
'Content-Type': 'application/json'
})
self.host = host
self.port = port
self.external = external
self.session_key = session_key
if not self.port:
if external:
self.port = 8087
else:
self.port = random_port(8000, 16000)
check_deps()
def __del__(self):
self.stop()
@property
def api_active(self):
return self.external or (self._proc and self._proc.poll() == None)
@property
def unlocked(self):
return True if self.session_key else False
@property
def account(self):
if not self._account:
self._account = self.status().userEmail
return self._account
## broken
#return self.status().status == 'unlocked'
def _process_response(self, raw_data, return_raw=False):
try:
data = DotDict(raw_data)
if return_raw:
return data
if 'success' not in data:
raise BitwardenApiError(data.message)
try:
return data.data
except AttributeError:
return data
except JSONDecodeError as e:
print(raw_data)
raise e from None
def get_port(self):
if self.port:
return self.port
if not self._current_port:
if self.external:
self._current_port = 8087
else:
self._current_port = random_port(8000, 16000)
return self._current_port
def create_row(self, *args, **kwargs):
return self.item_class.new(*args, **kwargs, storage=self)
## send cli command
def execute(self, command, *args, data=None, key=True, return_raw=False):
#cmd = [bwbin, '--response']
cmd = [bwjs, '--response']
if key:
if not self.session_key:
raise ValueError('Missing session key. Try calling login first.')
cmd.append('--session')
cmd.append(self.session_key)
proc_kwargs = {
'capture_output': True,
'encoding': 'utf-8'
}
if data:
proc_kwargs['input'] = convert_to_string(data)
result = node.run([*cmd, command, *args], **proc_kwargs)
return self._process_response(result.stdout, return_raw)
## send api command
def send(self, api, method, data=None, return_raw=False):
if not self.api_active:
raise ConnectionError('API server is not running')
kwargs = {'method': method}
url = Url.new(self.host,
port = self.get_port(),
path = api,
proto = 'http'
)
if data and method == 'GET':
url = url.replace_property('query', data)
with self._client.request(url, method, data, raise_error=True) as resp:
return self._process_response(resp.text, return_raw)
## api server
def start(self, password=None):
if self.external or self.api_active:
return
if password:
self.unlock(password)
if not self.session_key:
raise ValueError('Missing session key')
cmd = [bwjs, 'serve']
cmd.extend(['--hostname', self.host])
cmd.extend(['--port', str(self.port)])
cmd.extend(['--session', self.session_key])
self._proc = node.Popen(cmd)
while True:
try:
self.account
return
except ConnectionRefusedError:
time.sleep(0.1)
def stop(self, lock=False):
if self.external or not self.api_active:
return
self._proc.terminate()
wait_time = datetime.now()
while self._proc.poll() == None:
if wait_time + timedelta(seconds=5) >= datetime.now():
self._proc.kill()
break
if lock:
self.lock()
self._proc = None
self._account = None
self._current_port = None
## command methods
def login(self, email, password):
data = self.execute('login', email, password, key=False)
self.session_key = data.raw
return data.raw
def logout(self):
self.execute('logout')
self.session_key = None
def lock(self):
self.execute('lock')
self.session_key = None
def unlock(self, password):
data = self.execute('unlock', password)
self.session_key = data.raw
return data.raw
## api methods
def status(self):
if self.session_key:
data = self.send('status', 'GET')
else:
data = self.execute('status', key=None)
return data.template
def fetch(self, text=None, trash=False, **fields):
kwargs = {}
if (row_id := fields.get('id')):
row = self.send(f'object/item/{row_id}', 'GET')
return self.item_class(self, row)
if text:
kwargs['search'] = text
if trash:
kwargs['trash'] = trash
rows = []
data = self.send('list/object/items', 'GET', kwargs)
for row in data.data:
row = self.item_class(self, row)
if row.compare(**fields):
rows.append(row)
return self.result_class(self, rows)
def insert(self, *args, **kwargs):
self.insert_row(self.item_class.new(*args, **kwargs, storage=self))
def insert_row(self, row):
data = self.execute('create', 'item',
self.execute('encode', data=row.to_json()).data
)
return self.item_class(self, data)
## Causes the http server to return a 500 error
def insert_row_api(self, row):
data = self.send('object/item', 'POST', row._data)
return self.item_class(self, data)
def update(self, item_id, **kwargs):
row = self.fetch(id=item_id)
new_row = self.update_row(row, **kwargs)
return new_row
def update_row(self, row, **kwargs):
row.update(kwargs)
new_row = self.send(f'object/item/{row.id}', 'PUT', row.to_json())
return self.item_class(self, new_row)
def remove(self, item_id):
data = self.send(f'object/item/{item_id}', 'DELETE', return_raw=True)
if not data.get('success'):
raise BitwardenApiError(data)
return True
def purge(self, item_id):
data = self.execute('delete', 'item', item_id, '--permanent')
if not data.get('success'):
raise BitwardenApiError(data)
return True
def purge_all(self, dry_run=False):
rows = self.fetch(trash=True).all()
for row in rows:
if not dry_run:
self.purge(row.id)
return rows
def restore(self, item_id):
data = self.send(f'restore/item/{item_id}', 'POST', return_raw=True)
if not data.get('success'):
raise BitwardenApiError(data)
return True
def passgen(self, passphrase=False, **kwargs):
if passphrase:
if 'separator' in kwargs:
assert len(kwargs['separator']) == 1, 'Separator must be exactly 1 character'
data = dict(
passphrase = 'true',
words = int(kwargs.get('words', 4)),
capitalize = bool_str(kwargs.get('capitalize')),
includeNumber = bool_str(kwargs.get('numbers')),
separator = kwargs.get('separator', '-')
)
else:
data = dict(
length = int(kwargs.get('length', 20)),
uppercase = bool_str(kwargs.get('uppercase', True)),
lowercase = bool_str(kwargs.get('lowercase', True)),
number = bool_str(kwargs.get('numbers', True)),
special = bool_str(kwargs.get('special', True))
)
return self.send('generate', 'GET', data).data
def generate_passphrase(self, words=4, capitalize=False, numbers=False, separator='-'):
return self.passgen(
passphrase = True,
words = words,
capitalize = capitalize,
numbers = numbers,
separator = separator
)
def generate_password(self, length=20, uppercase=True, lowercase=True, numbers=True, special=True):
return self.passgen(
passphrase = False,
length = length,
uppercase = uppercase,
lowercase = lowercase,
numbers = numbers,
special = special
)
def template(self, name, api=False):
if api:
return self.send(f'template/{name}', 'GET').template
return self.execute('get', 'template', name).template
def get_template(self, type='login'):
if type == 'login':
template = self.template('item')
template.login = self.template('item.login')
template.login.uris = [self.template('item.login.uri')]
template.fields = [self.template('item.field')]
elif type == 'card':
template = self.template('item')
template.card = self.template('item.card')
template.fields = []
elif type == 'identity':
template = self.template('item')
template.identity = self.template('item.identity')
template.fields = []
elif type == 'note':
template = self.template('item')
template.securenote = self.template('item.securenote')
template.fields = []
elif type == 'folder':
template = self.template('folder')
elif type == 'collection':
template = self.template('collection')
else:
raise ValueError(f'Not a valid template: {type}')
return template
def sync(self):
data = self.send('sync', 'POST', return_raw=True)
return data.success

View file

@ -0,0 +1,311 @@
import json, secretstorage
from datetime import datetime
from secretstorage.collection import Collection, get_collection_by_alias, create_collection
from secretstorage.exceptions import ItemNotFoundException, LockedException
from .base import PasswordItem, PasswordStorage, PasswordResult
from ..functions import TimeoutCallback, get_app, run_in_gui_thread
pass_store_keys = ['username', 'domain', 'url', 'note']
pass_keys = [*pass_store_keys, 'created', 'modified', 'label', 'password']
def parse_data(self, username=None, domain=None, password=None, url=None, note=None, label=None, **kwargs):
kwargs.update(
username = username,
domain = domain,
url = url,
note = note,
password = password,
label = label
)
for key, value in tuple(kwargs.items()):
if not value:
del new_data[key]
return kwargs
class GnomeKeyringStorage(PasswordStorage):
def __init__(self, name='BarksharkWeb'):
self.name = name
self.connection = None
self.collection = None
def __getitem__(self, key):
if (item := self.fetch(id=key).one()):
return item
raise KeyError(f'No password with label: {key}')
def __setitem__(self, key, value):
if (item := self[key]):
item.update(**value)
else:
self.insert(label=key, **value)
def __enter__(self):
self.connect()
return self
def __exit__(self, *args):
self.disconnect()
def connect(self):
if self.connection and self.collection:
return
if not self.connection:
self.connection = secretstorage.dbus_init()
if not secretstorage.check_service_availability(self.connection):
self.disconnect()
raise ConnectionError('Failed to connect to Secret Service server')
return self.get_keyring()
def disconnect(self):
if self.connection:
self.connection.close()
self.connection = None
self.collection = None
def get_keyring(self):
if not self.collection:
try:
self.collection = Collection(self.connection, f'/org/freedesktop/secrets/collection/{self.name}')
except ItemNotFoundException:
create_collection(self.connection, self.name)
self.collection = Collection(self.connection, f'/org/freedesktop/secrets/collection/{self.name}')
try:
self.collection.ensure_not_locked()
except LockedException:
self.unlock()
return self.collection
def unlock(self):
return self.collection.unlock()
def lock(self):
return self.collection.lock()
def fetch(self, *args, **kwargs):
data = parse_data(*args, **kwargs)
data.pop('password', None)
if not any(data.values()):
rows = self.collection.get_all_items()
else:
rows = self.collection.search_items(data)
return PasswordResult(self, rows)
def insert(self, *args, label=None, **kwargs):
data = parse_data(*args, **kwargs)
data['id'] = random_str()
required = {key: data.get(key) for key in ['username', 'domain', 'password']}
if None in required.values():
raise ValueError(f'Forgot username, domain, or password: {json.dumps(required)}')
username = data['username']
password = data.pop('password')
if (row := self.fetch(username, password).one()):
logging.verbose('Password already exists:', row.label)
return row
if not label:
label = f'{username} @ {password}'
row = self.collection.create_item(label, data, password.encode())
return PasswordItem(row)
def update(self, username, domain, **kwargs):
row = self.fetch(username, domain).one()
row.update(**kwargs)
return row
def remove(self, *args, **kwargs):
for row in self.fetch(*args, **kwargs):
row.delete()
class GnomeKeyringItem(PasswordItem):
def __init__(self, item):
super().__init__()
self._item = item
self._attrs = item.get_attributes()
if not self._attrs.get('id'):
self.update({'id': random_str()})
def __repr__(self):
return f'PasswordItem(id={self.id}, username={self.username}, domain={self.domain})'
def __getitem__(self, key):
if key not in pass_keys:
raise KeyError(key)
return getattr(self, key)
def __setitem__(self, key, value):
if key not in pass_store_keys:
raise KeyError(key)
self.update(**{key: value})
@property
def created(self):
return datetime.fromtimestamp(self._item.get_created())
@property
def modified(self):
return datetime.fromtimestamp(self._item.get_modified())
@property
def label(self):
if not (label := self._item.get_label()):
return f'{self.username} @ {self.domain}'
return label
@property
def id(self):
return self._attrs.get('id')
@property
def password(self):
return self._item.get_secret().decode()
@property
def username(self):
return self._attrs.get('username')
@property
def domain(self):
return self._attrs.get('domain')
@property
def url(self):
if (url := self._attrs.get('url')):
return Url(url)
@property
def note(self):
return self._attrs.get('note')
@password.setter
def password(self, value):
self.update(password=value)
@username.setter
def username(self, value):
self.update(username=value)
@domain.setter
def domain(self, value):
self.update(domain=value)
@url.setter
def url(self, value):
return self.update(url=str(value))
@note.setter
def note(self, value):
return self.update(note=value)
def copy_password(self, timeout=60):
app = get_app()
app.set_clipboard_text(self.password)
timer = TimeoutCallback(timeout, run_in_gui_thread, app.handle_clipboard_clear_password, self.password)
timer.start()
def copy_username(self):
get_app().set_clipboard_text(self.username)
def as_dict(self):
return DotDict(
id = self.id,
domain = self.domain,
username = self.username,
password = self.password,
url = self.url,
note = self.note,
created = self.created,
modified = self.modified
)
def to_json(self, indent=None):
return self.as_dict().to_json(indent)
def update(self, data):
label = data.pop('label', None)
if (password := data.pop('password', None)):
self._item.set_secret(password.encode())
if not len(data):
return
new_data = parse_data(**self.as_dict())
new_data.update(parse_data(**data))
self._item.set_attributes(new_data)
self._attrs = self._item.get_attributes()
if label:
self._item.set_label(label)
elif any(map(data.get, ['username', 'domain'])) and not self._item.get_label():
self._item.set_label(self.label)
def delete(self):
self._item.delete()

View file

@ -0,0 +1,182 @@
from datetime import datetime
from .base import PasswordItem, PasswordStorage
class SqlItem(PasswordItem):
def __init__(self, storage, row):
self._storage = storage
self._data = row
def __repr__(self):
return f'{class_name(self)}("{self.label}", username="{self.username}", domain="{self.domain}")'
def __getitem__(self, key):
if key not in self.keys():
raise KeyError(key)
value = self._get_value(key)
return self.__default_parse(key, value)
def __setitem__(self, key, value):
if key in ['id', 'created', 'modified', 'domain'] or key not in self.keys():
raise KeyError(key)
value = self.__default_parse(key, value)
self._set_value(key, value)
def __delitem__(self, key):
raise AttributeError('Cannot delete item values')
def __getattr__(self, key):
try:
return self.__getitem__(key)
except KeyError:
return object.__getattribute__(self, key)
def __setattr__(self, key, value):
try:
self.__setitem__(key, value)
except KeyError:
object.__setattr__(self, key, value)
def __default_parse(self, key, value):
if key == 'url' and not isinstance(value, (Url, type(None))):
return Url(value)
elif key in ['created', 'modified'] and not isinstance(value, (DateString, type(None))):
return DateString.new_http(value)
return self._parse_value(key, value)
def _parse_value(self, key, value):
return value
def _get_value(self, key):
return self.row[key]
def _set_value(self, key, value):
self.row[key] = value
def copy_password(self, timeout=60):
app = get_app()
app.set_clipboard_text(self.password)
timer = TimeoutCallback(timeout, run_in_gui_thread, app.handle_clipboard_clear_password, self.password)
timer.start()
def copy_username(self):
get_app().set_clipboard_text(self.username)
def insert_row(self, storage=None):
if self.id:
raise RuntimeError('Row already has an ID and is probably in the DB')
if storage:
return storage.insert(**self.to_dict())
return self._storage.insert(**self.to_dict())
def update_row(self, **kwargs):
self.update(**kwargs)
self._storage.update(self.id, **kwargs)
def remove_row(self):
self._storage.remove(self.id)
def to_dict(self, indent=None):
return DotDict({key: self[key] for key in self.keys()})
## dict methods
def items(self):
for key in self.keys():
yield (key, self[key])
def keys(self):
return PASSWORD_KEYS.copy()
def values(self):
return tuple(self[key] for key in self.keys())
def update(self, _data={}, **kwargs):
kwargs.update(data)
self.data.update(kwargs)
class SqlStorage(PasswordStorage):
item_class = SqlItem
def __init__(self, db):
self.db = db
def fetch(self, *args, deleted=None, **kwargs):
with self.db.session as s:
if (rowid := kwargs.get('id')):
if not (row := s.fetch('passwords', id=rowid).one()):
raise NoPasswordError(rowid)
return row
data = self.parse_data(*args, **kwargs)
return s.fetch('passwords', **data)
def insert(self, *args, **kwargs):
data = self.parse_data(*args, **kwargs)
data['created'] = datetime.now()
if not data.get('domain') and data.get('url'):
data['domain'] = data['url'].hostname()
with self.db.session as s:
s.insert('passwords', **data)
return s.fetch('passwords',
username = data['username'],
password = data['password'],
domain = data['url'].domain if data.get('url') else None
).one()
def update(self, row_id, *args, **kwargs):
data = self.parse_data(*args, **kwargs)
data['modified'] = datetime.now()
if (url := data.get('domain')):
data['domain'] = url.hostname()
with self.db.session as s:
s.update('passwords', data, id=row_id)
return s.fetch('passwords', id=row_id).one()
def remove(self, row_id):
with self.db.session as s:
s.remove('passwords', id=row_id)
return True

View file

@ -8,6 +8,7 @@ from .handler import (
from .file import File
from .ftp import Ftp
from .local import Local
from .local_wasm import LocalWeb
from .oauth import Oauth
from .sftp import Sftp
from .source import Source

View file

@ -60,32 +60,36 @@ def handle_remote_file(request, data):
return request.error(f'Unhandled mimetype: {mimetype}', 400)
class ProtocolRequest:
class ProtocolRequest(ObjectBase):
def __init__(self, handler, request):
self._handler = handler
self._request = request
self._finished = False
self._finished_thread = False
ObjectBase.__init__(self,
handler = handler,
request = request,
ctx = DotDict(),
finished = False,
finished_thread = False,
readonly_props = ['handler', 'request', 'ctx']
)
@property
def app(self):
return self._handler.app
return self.handler.app
@property
def db(self):
return self._handler.app.db
return self.handler.app.db
@property
def window(self):
return self._handler.window
return self.handler.window
@property
def webview(self):
return self._request.get_web_view()
return self.request.get_web_view()
@property
@ -93,14 +97,9 @@ class ProtocolRequest:
return self.webview.parent
@property
def finished(self):
return self._finished
@property
def url(self):
return Url(self._request.get_uri())
return Url(self.request.get_uri())
@property
@ -150,12 +149,12 @@ class ProtocolRequest:
data = data.encode('UTF-8')
bytestream = Gio.MemoryInputStream.new_from_bytes(GLib.Bytes(data))
self._request.finish(bytestream, len(data), ctype)
self._finished = True
self.request.finish(bytestream, len(data), ctype)
self.finished = True
def finish_thread(self, func, *args, **kwargs):
self._finished_thread = True
self.finished_thread = True
Thread(target=func, args=args, kwargs=kwargs).start()
@ -164,8 +163,8 @@ class ProtocolRequest:
return logging.error('Already responded to request:', self.url)
error = GLib.Error.new_literal(GLib.quark_from_string('HandlerError'), body, status)
self._request.finish_error(error)
self._finished = True
self.request.finish_error(error)
self.finished = True
def page(self, page, context={}):
@ -199,7 +198,7 @@ class ProtocolRequest:
self.window.notification(message, level)
if url and url.startswith('/'):
url = self._handler.protocol + '://' + url
url = self.handler.protocol + '://' + url
if not self.finished:
self.response('OK')
@ -213,6 +212,7 @@ class ProtocolHandler(ObjectBase):
protocol = protocol,
router = http_router.Router(),
request_class = request_class,
ctx = DotDict(),
readonly_props = True
)
@ -237,7 +237,7 @@ class ProtocolHandler(ObjectBase):
else:
response = route.target(self, request, **(route.params or {}))
if request._finished_thread:
if request.finished_thread:
return
if not request.finished:

View file

@ -9,6 +9,7 @@ from .functions import error, finish_request, finish_request_error, redirect
from .. import var
from ..functions import TimeoutCallback, get_app, run_in_gui_thread
from ..passwords import PASSWORD_STORAGE
class LocalRequest(ProtocolRequest):
@ -293,22 +294,21 @@ def history_clear(handler, request):
### Passwords ###
@Local.route('/passwords')
def passwords_home(handler, request):
def passwords_home(handler,request):
domain = request.query.get('domain')
with handler.db.session as s:
if (search_text := request.query.get('text')):
items = []
if (search_text := request.query.get('text')):
items = []
for row in s.fetch('passwords', domain=domain):
if True in fuzzy_string_match(search_text, row.domain, row.url, row.username, row.note, row.label):
items.append(row)
for row in handler.app.password.fetch():
if True in fuzzy_string_match(search_text, row.domain, row.url, row.username, row.note, row.label):
items.append(row)
elif domain:
items = s.fetch('passwords', domain=domain).all()
elif domain:
items = handler.app.password.fetch(domain=domain).all()
else:
items = s.fetch('passwords').all()
else:
items = handler.app.password.fetch().all()
return request.page('passwords', {'items': items})
@ -318,11 +318,10 @@ def passwords_edit(handler, request, rowid=None):
if not rowid:
return request.page('password-edit', {'password': None})
with handler.db.session as s:
if not (item := s.fetch('passwords', id=rowid).one()):
return request.error(f'Cannot find password: {rowid}', 404)
if not (row := handler.app.password.fetch(id=rowid)):
return request.error(f'Cannot find password with row ID: {rowid}', 404)
return request.page('password-edit', {'password': item})
return request.page('password-edit', {'password': row})
@Local.route('/passwords/update', '/passwords/update/{rowid}')
@ -330,35 +329,29 @@ def passwords_update(handler, request, rowid=None):
query = request.query.copy()
query.pop('redir', None)
print(query.get('label'))
if not query.get('label'):
query['label'] = f'{query.username} @ {query.domain}'
print(query.get('label'))
for key, value in list(query.items()):
if isinstance(value, str) and not value:
del query[key]
with handler.db.session as s:
if not rowid:
row = s.put_password(**query)
return request.ok_or_redirect(f'Created new password: {row.label}')
if not rowid:
row = handler.app.password.insert(**query)
return request.ok_or_redirect(f'Created new password: {row.label}')
if not (row := s.fetch('passwords', id=rowid).one()):
return request.error(f'Cannot find password: {rowid}', 404)
if not all(map(query.get, ('username', 'password', 'url'))):
return request.error(f'Missing username, password or url', 400)
row = s.put_password_row(row, **query)
return request.ok_or_redirect(f'updated password: {row.label}')
row = handler.app.password.update(rowid, **query)
return request.ok_or_redirect(f'Updated password: {row.label}')
@Local.route('/passwords/copy/{rowid}')
def passwords_copy(handler, request, rowid):
with handler.db.session as s:
if not (row := s.fetch('passwords', id=rowid).one()):
return request.error(f'Cannot find password: {rowid}', 404)
if not (row := handler.app.password.fetch(id=rowid)):
return request.error(f'Cannot find password with ID: {rowid}', 404)
row.copy_password()
return request.ok_or_redirect(f'Copied password for 60 seconds')
@ -366,19 +359,20 @@ def passwords_copy(handler, request, rowid):
@Local.route('/passwords/delete/{rowid}')
def passwords_update(handler, request, rowid):
with handler.db.session as s:
try:
s.remove('passwords', id=rowid)
return request.ok_or_redirect(f'Deleted password: {label}')
if not (row := handler.app.password.fetch(id=rowid)):
return request.error(f'Cannot find password with ID: {rowid}', 404)
except KeyError:
return request.error(f'Cannot find password: {rowid}', 404)
handler.app.password.remove(rowid)
return request.ok_or_redirect(f'Deleted password: {row.label}')
#return request.error(f'Cannot find password: {rowid}', 404)
### Preferences ###
@Local.route('/preferences')
def preferences_home(handler, request):
context = DotDict(
pass_backends = list(PASSWORD_STORAGE.keys()),
settings = {
'font': 'sans undertale',
'font-size': '14'
@ -398,6 +392,9 @@ def preferences_update(handler, request):
if key == 'redir':
continue
elif key == 'pass_type':
handler.app.setup_password_storage(value)
row = s.put_config(key, value)
logging.verbose(f'Updated config: {row.key} = {row.value}')

View file

@ -0,0 +1,132 @@
import objgraph, mimetypes
from functools import partial
from jinja2.exceptions import TemplateNotFound
from izzylib import Color, class_name, fuzzy_string_match
from izzylib_http_async import Template
from threading import Thread
from urllib.parse import quote
from . import ProtocolHandler, ProtocolRequest, handle_remote_file, list_directory
from .functions import error, finish_request, finish_request_error, redirect
from .. import var, scriptpath
from .. import __version__ as version, __software__ as swname
from ..functions import TimeoutCallback, get_app, run_in_gui_thread
from ..passwords import PASSWORD_STORAGE
class LocalWebRequest(ProtocolRequest):
def _render_template(self, path, context={}):
try:
return self._handler.ctx.template.render(path, context)
except TemplateNotFound:
logging.error('Cannot find template:', path)
return self._handler.ctx.template.render('error.haml', {'title': 'Template Not Found', 'error_msg': path})
def ok_or_redirect(self, *message, level='INFO'):
if redir := self.query.get('redir'):
return self.redirect(redir, ' '.join(message), level)
self.window.notification(' '.join(message), level=level)
return self.response('ok')
def handle_refresh_account(self, row):
row.refresh()
self.ok_or_redirect(f'Refreshed account info: {row.fullhandle}')
def handle_return_account(self, row):
html = self._render_template('account_row.haml', {'row': row})
return self.response(html)
def handle_template_context(handler, context):
return context
LocalWeb = ProtocolHandler('bsweb', LocalWebRequest)
LocalWeb.ctx.template = Template(
autoescape = False,
context = partial(handle_template_context, LocalWeb),
search = [
scriptpath.join('localweb2'),
scriptpath.join('resources')
]
)
LocalWeb.ctx.template.update_env({
'len': len,
'str': str,
'app': LocalWeb.app,
'var': var,
'version': version,
'swname': swname,
'quote': quote
})
### Home ###
@LocalWeb.route('/')
def home(handler, request):
ctx = dict(
page = 'Merp!'
)
try:
return request.template('base.haml', ctx, 'text/html')
#return request.response('UvU')
except Exception as e:
print(class_name(e), e)
raise e from None
### Resources ###
@LocalWeb.route('/css/{filename}')
def css(handler, request, filename):
context = DotDict(
primary = Color('#e7a'),
secondary = Color('#e7a'),
background = Color('#191919'),
positive = Color('#ada'),
negative = Color('#daa'),
speed = 350
)
return request.template(f'css/{filename}', context)
@LocalWeb.route('/js/{filename}')
def javascript(handler, request, filename):
with handler.app.path.script.join('localweb2', 'js', filename).open() as fd:
return request.response(fd.read(), 'application/javascript')
@LocalWeb.route('/py/{filename}')
def python_module(handler, request, filename):
with handler.app.path.script.join('localweb2', 'py', filename).open() as fd:
return request.response(fd.read(), 'text/python')
@LocalWeb.route('/font/{name}/{filename}')
def font(handler, request, name, filename):
with handler.app.path.localweb.join('fonts', name, filename).open('rb') as fd:
return request.response(fd.read(), request.mimetype)
@LocalWeb.route('/icon/{name}')
def icon(handler, request, name):
if name == 'app.png':
path = handler.app.path.resources.join('icon.png')
mime = 'image/png'
else:
path = handler.app.path.resources.join('icons', name)
mime = 'image/svg+xml'
with path.open('rb') as fd:
return request.response(fd.read(), mime)

View file

@ -2,6 +2,37 @@
<!-- Generated with glade 3.38.2 -->
<interface>
<requires lib="gtk+" version="3.24"/>
<object class="GtkDialog">
<property name="can-focus">False</property>
<property name="type-hint">dialog</property>
<child internal-child="vbox">
<object class="GtkBox">
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<child internal-child="action_area">
<object class="GtkButtonBox">
<property name="can-focus">False</property>
<property name="layout-style">end</property>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<placeholder/>
</child>
</object>
</child>
</object>
<object class="GtkImage" id="bookmark-icon">
<property name="visible">True</property>
<property name="can-focus">False</property>
@ -198,6 +229,149 @@
<property name="can-focus">False</property>
<property name="icon-name">window-close</property>
</object>
<object class="GtkDialog" id="password-login">
<property name="can-focus">False</property>
<property name="title" translatable="yes">Password Storage Login</property>
<property name="type-hint">dialog</property>
<child internal-child="vbox">
<object class="GtkBox">
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<child internal-child="action_area">
<object class="GtkButtonBox">
<property name="can-focus">False</property>
<property name="layout-style">end</property>
<child>
<object class="GtkButton" id="password-login-cancel">
<property name="label" translatable="yes">Cancel</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="password-login-reset">
<property name="label" translatable="yes">Reset</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="password-login-login">
<property name="label" translatable="yes">Login</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<!-- n-columns=2 n-rows=4 -->
<object class="GtkGrid">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="row-spacing">5</property>
<property name="column-spacing">5</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Password</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">3</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="password-login-password">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="hexpand">True</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">3</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="password-login-email">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="hexpand">True</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">2</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">E-Mail</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">2</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="password-login-error">
<property name="visible">True</property>
<property name="can-focus">False</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">1</property>
<property name="width">2</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Login to your Bitwarden account</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">0</property>
<property name="width">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
<object class="GtkAdjustment" id="preferences-closed_tabs_limit-adjustment">
<property name="upper">1000</property>
<property name="value">100</property>

View file

@ -46,6 +46,11 @@
"options": [],
"url": null
},
"nodejs-bin": {
"version": "18.4.0a4",
"options": [],
"url": null
},
"objgraph": {
"version": "3.5.0",
"options": [],
@ -70,6 +75,11 @@
"version": "0.2.9",
"options": [],
"url": null
},
"secretstorage": {
"version": "3.3.2",
"options": [],
"url": null
}
},
"watcher": {
@ -88,7 +98,9 @@
"ignore_dirs": [
"build",
"config",
"data"
"data",
"localweb",
"localweb2"
],
"ignore_files": [
"reload.py",

View file

@ -7,8 +7,10 @@ izzylib-http-async@git+https://git.barkshark.xyz/izaliamae/izzylib-http-async
izzylib-sql@git+https://git.barkshark.xyz/izaliamae/izzylib-sql
lxml==4.6.3
mastodon.py==1.5.1
nodejs-bin==18.4.0a4
objgraph==3.5.0
pillow==8.3.2
psutil==5.8.0
pygobject==3.38.0
pysftp==0.2.9
secretstorage==3.3.2

View file

@ -33,20 +33,22 @@ python_requires = >= 3.8
packages =
barkshark_web
setup_requires =
izzylib @ git+https://git.barkshark.xyz/izaliamae/izzylib
izzylib_sql @ git+https://git.barkshark.xyz/izaliamae/izzylib-sql
izzylib_http_async @ git+https://git.barkshark.xyz/izaliamae/izzylib-http-async
beautifulsoup4==4.9.3
click==8.1.0
dasbus==1.6
http-router==2.6.5
izzylib@git+https://git.barkshark.xyz/izaliamae/izzylib
izzylib-http-async@git+https://git.barkshark.xyz/izaliamae/izzylib-http-async
izzylib-sql@git+https://git.barkshark.xyz/izaliamae/izzylib-sql
lxml==4.6.3
mastodon.py==1.5.1
nodejs-bin==18.4.0a4
objgraph==3.5.0
pillow==8.3.2
psutil==5.8.0
pygobject==3.38.0
pysftp==0.2.9
secretstorage==3.3.2
[options.entry_points]
console_scripts =