add support for multiple password backends
This commit is contained in:
parent
3dd8f17c71
commit
ffa4d71ce2
7
Makefile
7
Makefile
|
@ -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
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 ''
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}`)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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}}'
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
10
barkshark_web/passwords/__init__.py
Normal file
10
barkshark_web/passwords/__init__.py
Normal 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
|
257
barkshark_web/passwords/base.py
Normal file
257
barkshark_web/passwords/base.py
Normal 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
|
3
barkshark_web/passwords/bitwarden/__init__.py
Normal file
3
barkshark_web/passwords/bitwarden/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from .item import BitwardenItem
|
||||
from .result import BitwardenResult
|
||||
from .storage import BitwardenStorage
|
200
barkshark_web/passwords/bitwarden/item.py
Normal file
200
barkshark_web/passwords/bitwarden/item.py
Normal 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())
|
||||
|
6
barkshark_web/passwords/bitwarden/result.py
Normal file
6
barkshark_web/passwords/bitwarden/result.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
from ..base import PasswordResult
|
||||
|
||||
class BitwardenResult(PasswordResult):
|
||||
def __iter__(self):
|
||||
for item in self._items:
|
||||
yield item
|
439
barkshark_web/passwords/bitwarden/storage.py
Normal file
439
barkshark_web/passwords/bitwarden/storage.py
Normal 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
|
311
barkshark_web/passwords/gnome_keyring.py
Normal file
311
barkshark_web/passwords/gnome_keyring.py
Normal 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()
|
182
barkshark_web/passwords/sql.py
Normal file
182
barkshark_web/passwords/sql.py
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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}')
|
||||
|
||||
|
|
132
barkshark_web/protocol/local_wasm.py
Normal file
132
barkshark_web/protocol/local_wasm.py
Normal 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)
|
|
@ -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>
|
||||
|
|
14
pyvenv.json
14
pyvenv.json
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 =
|
||||
|
|
Reference in a new issue