store passwords in database

This commit is contained in:
Izalia Mae 2022-07-03 19:13:58 -04:00
parent 3ff8e4886f
commit b6a085b538
16 changed files with 314 additions and 442 deletions

View file

@ -3,7 +3,6 @@ from ..database import default_permissions
from ..exceptions import AccountNotFoundError, NoAccountsError
from ..functions import SignalBlock, connect, get_buffer_text
from ..objects.login_rows import SavedLoginRow
from ..passwords import passdb
class StatusBar:
@ -392,8 +391,9 @@ class StatusBar:
for child in login_list.get_children():
child.destroy()
for row in passdb.fetch(domain=self.window.active_tab.url.hostname()):
login_list.add(SavedLoginRow(row, self.window.active_tab.url)['container'])
with self.db.session as s:
for row in s.fetch('passwords', domain=self.window.active_tab.url.hostname()):
login_list.add(SavedLoginRow(row, self.window.active_tab.url)['container'])
def handle_toot_key_press(self, textview, event, *args):

View file

@ -7,7 +7,6 @@ from .web_view import Webview
from .. import var, __software__
from ..functions import Thread, run_in_gui_thread, connect, get_app, icon_set
from ..passwords import passdb
from ..themes import Themes
from ..widgets import FileChooser, Menu, MenuButtonRefresh
@ -548,7 +547,6 @@ class Window(Gtk.ApplicationWindow):
def handle_window_close(self, *args):
logging.verbose('Saving data')
passdb.disconnect()
self.save_tabs()
with self.app.db.session as s:

View file

@ -78,6 +78,17 @@ default_searches = {
}
password_fields = [
'username',
'domain',
'label',
'password',
'label',
'url',
'note'
]
tables = {
'config': [
Column('id'),
@ -160,23 +171,16 @@ tables = {
Column('count', 'integer', nullable=False, default=0),
Column('last_access', 'datetime')
],
'passfields': [
Column('id'),
Column('domain', 'text', nullable=False),
Column('url', 'text', nullable=False),
Column('userfield', 'text', nullable=False),
Column('passfield', 'text', nullable=False)
],
'passwords': [
Column('id'),
Column('username', 'text', nullable=False),
Column('password', 'text', nullable=False),
Column('name', 'text', nullable=False),
Column('domain', 'text'),
Column('label', 'text', nullable=False),
Column('domain', 'text', nullable=False),
Column('url', 'text'),
Column('note', 'text', nullable=False),
Column('note', 'text'),
Column('created', 'datetime', nullable=False),
Column('modified', 'datetime', nullable=False)
Column('modified', 'datetime')
]
}
@ -290,8 +294,8 @@ class CustomSession(Session):
return self.get_cached('history', url, url=url)
def get_passfield(self, url):
return self.get_cached('passfields', url.without_query, url=url.without_query)
def get_password(self, username, host):
return self.fetch('passwords', username=username, domain=host).one()
def get_permission(self, domain, cache=True):
@ -453,6 +457,41 @@ class CustomSession(Session):
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')

View file

@ -4,7 +4,6 @@ from .base import tables
from .. import var
from ..functions import get_app
from ..passwords import passdb
def now(db, version):
@ -33,11 +32,9 @@ def now(db, version):
for keyword, data in s.default_searches.items():
s.put_search(data[0], keyword, data[1])
if version <= 20210905:
if version < 20210905:
s.create_tables({'passwords': tables['passwords']})
else:
raise KeyError('heck')
s.drop_table('passfields')
s.put_config('version', var.dbversion, 'int')

View file

@ -7,7 +7,7 @@ from mastodon import Mastodon
from PIL import Image
from urllib.parse import quote_plus
from ..functions import get_app
from ..functions import TimeoutCallback, get_app, run_in_gui_thread
row_classes = {}
@ -22,8 +22,14 @@ def register_class(table):
return wrapper
class RowBase(Row):
@property
def app(self):
return get_app()
@register_class('accounts')
class Account(Row):
class Account(RowBase):
_api = None
_emojis = None
@ -178,8 +184,21 @@ class Account(Row):
s.put_config('active_acct', self.id)
@register_class('passwords')
class Password(RowBase):
def copy_password(self, timeout=60):
self.app.set_clipboard_text(self.password)
timer = TimeoutCallback(timeout, run_in_gui_thread, self.app.handle_clipboard_clear_password, self.password)
timer.start()
def copy_username(self):
self.app.set_clipboard_text(self.username)
@register_class('search')
class Search(Row):
class Search(RowBase):
def compile(self, text):
if text.startswith(f'{self.keyword} '):
text = text[len(self.keyword)+1:]

View file

@ -37,6 +37,15 @@ class BarksharkWebExtension:
self.enabled = True
def __getattr__(self, key):
try:
if object.__getattribute__(self, 'manifest'):
return self.manifest[key]
finally:
return object.__getattribute(self, key)
def __getitem__(self, key):
return self.config[key]
@ -91,23 +100,23 @@ class BarksharkWebExtension:
self.logging.setConfig({'level': level})
def handle_initialize(self, client, tab):
def handle_initialize(self):
pass
def handle_page_created(self, client, tab):
def handle_page_created(self, pageid: int):
pass
def handle_document_loaded(self, client, tab, page_url: str):
def handle_document_loaded(self, page_url: Url):
pass
def handle_send_request(self, client, tab, page_url: str, request: dict, redirect: dict):
def handle_send_request(self, page_url: Url, redirect: Url):
pass
def handle_console_message(self, client, tab, page_url: str, text: str, level: str, line: int, source: str, source_id: int):
def handle_console_message_sent(self, text: str, level: str, line: int):
pass
@ -115,5 +124,5 @@ class BarksharkWebExtension:
pass
def handle_form_sent(self, data, stage):
def handle_form_sent(self, stage: str, fields: DotDict):
pass

View file

@ -86,40 +86,50 @@ class WebExtensions(ObjectBase):
assert message.get_name() not in ['command']
try:
func = getattr(self, f'handle_{cmd_name}')
func = getattr(self, f'parse_{cmd_name}')
except AttributeError:
return logging.error(f'Invalid command: {cmd_name}')
func(*message.get_parameters())
try:
args, kwargs = func(*message.get_parameters())
def handle_page_created(self, page_id):
return
print(f'page-created: {page_id}')
def handle_form_sent(self, stage, raw_data):
raw_data = DotDict(raw_data)
except:
traceback.print_exc()
logging.error('Error parsing data for signal:', cmd_name)
return
for ext in (*self.system.values(), *self.user.values()):
try:
ext.handle_form_sent(
DotDict(dict(zip(raw_data['keys'], raw_data['values']))),
stage.replace('WEBKIT_FORM_SUBMISSION_WILL_', '')
)
getattr(ext, f'handle_{cmd_name}')(*args, **kwargs)
except:
traceback.print_exc()
logging.error('Error handling handle_form_sent for extension:', ext.name)
def handle_console_message_sent(self, text, level, line):
def parse_page_created(self, page_id):
return (page_id, ), {}
def parse_form_sent(self, stage, raw_data):
raw_data = DotDict(raw_data)
args = [
DotDict(dict(zip(raw_data['keys'], raw_data['values']))),
stage.replace('WEBKIT_FORM_SUBMISSION_WILL_', '')
]
return args, {}
def parse_console_message_sent(self, text, level, line):
log_level = level.replace('WEBKIT_CONSOLE_MESSAGE_LEVEL_', '').upper()
if log_level == 'LOG':
log_level = 'INFO'
#logging.log(LogLevel[log_level], text.strip())
return (text, level, line), {}
class ExtPasswordManager(BarksharkWebExtension):

View file

@ -175,3 +175,14 @@ function copy_password(id) {
}
});
}
function setup_password_handlers() {
connect_event("password", "focus", (event) => {
event.currentTarget.type = "text";
});
connect_event("password", "blur", (event) => {
event.currentTarget.type = "password";
});
}

View file

@ -1,6 +1,6 @@
-if password
-set page = 'Edit Password: ' + password.label
-set action = var.local + '/passwords/update/' + password.id
-set action = var.local + '/passwords/update/' + str(password.id)
-else
-set page = 'New Password'
-set action = var.local + '/passwords/update'
@ -15,18 +15,18 @@
%input type='hidden' name='redir' value='/passwords'
.grid-container
%label.grid-item for='label' << Label
%input.grid-item id='label' class='full-width' type='text' name='label' placeholder='label' value='{{password.label or ""}}'
%label.grid-item for='username' << Username
%input.grid-item id='username' class='full-width' type='text' name='username' placeholder='username' value='{{password.username}}'
%label.grid-item for='password' << Password
%input.grid-item id='password' class='full-width' type='password' name='password' placeholder='password' value='{{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='label' << Label
%input.grid-item id='label' class='full-width' type='text' name='label' placeholder='label' value='{{password.label}}'
%label.grid-item for='url' << Url
%input.grid-item id='url' class='full-width' type='text' name='url' placeholder='url' value='{{password.url or ""}}'
@ -40,3 +40,6 @@
%a.button onclick='wine.location="{{var.local}}/passwords/delete/{{password.id}}?redir=/passwords"' << Delete
%a.button onclick='window.location.href="{{var.local}}/passwords"' << Cancel
%script type='application/javascript'
setup_password_handlers()

View file

@ -16,7 +16,7 @@
-if items
-for row in items
%details.item id='{{row.id}}' open
%details.item id='{{row.id}}'
%summary << {{row.label}}
.details-indent

View file

@ -1,5 +1,4 @@
from ..functions import TimeoutCallback, connect, get_app, run_in_gui_thread
from ..passwords import passdb
class LoginRowBase:
@ -14,7 +13,7 @@ class LoginRowBase:
@property
def db(self):
return self.ap.db
return self.app.db
@property
@ -35,14 +34,8 @@ class SavedLoginRow(LoginRowBase):
self.ui = Gtk.Builder.new_from_file(self.app.path.resources.join('password_saved.ui'))
self.row = row
with self.db.session as s:
self.dbrow = s.get_passfield(page_url)
self['username'].set_text(self.row['username'])
#if self.row.url != page_url or not self.dbrow:
#self['fill'].set_sensitive(False)
self.connect('fill', 'clicked', self.handle_fill_password)
self.connect('copy-password', 'clicked', self.handle_copy_password)
self.connect('copy-username', 'clicked', self.row.copy_username)
@ -75,9 +68,6 @@ class UnsavedLoginRow(LoginRowBase):
self.ext = ext
self.form = form
with self.db.session as s:
self.row = s.get_passfield(form.form_url)
self.set_data(form.username[1], form.password[1])
self.connect('save', 'clicked', self.handle_save)
@ -123,6 +113,7 @@ class UnsavedLoginRow(LoginRowBase):
def handle_save(self):
data = self.get_data()
passdb.insert(data.username, data.url.domain, data.password, data.url, data.note)
self.window.passwords.refresh()
with get_app().db.session as s:
s.put_password(data.username, data.password, data.url, name=None, note=data.note)
self.handle_cancel()

View file

@ -1,335 +0,0 @@
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 .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(username=None, domain=None, password=None, url=None, note=None, label=None, id=None, **kwargs):
new_data = DotDict(
id = id,
username = username,
domain = domain,
url = url,
note = note,
password = password,
label = label
)
for key, value in tuple(new_data.items()):
if not value:
del new_data[key]
return new_data
class 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(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['password']
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 PasswordResult:
def __init__(self, items):
self._items = items
def __iter__(self):
for item in self.all():
yield item
def all(self):
for item in self._items:
yield PasswordItem(item)
def one(self):
for item in self.all():
return item
class 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()
passdb = PasswordStorage()
passdb.connect()

View file

@ -9,7 +9,6 @@ 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 passdb
class LocalRequest(ProtocolRequest):
@ -292,19 +291,19 @@ def history_clear(handler, request):
def passwords_home(handler, request):
domain = request.query.get('domain')
if (search_text := request.query.get('text')):
items = []
rows = passdb.fetch(domain=domain)
with handler.db.session as s:
if (search_text := request.query.get('text')):
items = []
for row in rows:
if True in fuzzy_string_match(search_text, row.domain, row.url, row.username, row.note, row.label):
items.append(row)
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)
elif domain:
items = passdb.fetch(domain=domain).all()
elif domain:
items = s.fetch('passwords', domain=domain).all()
else:
items = passdb.fetch().all()
else:
items = s.fetch('passwords').all()
return request.page('passwords', {'items': items})
@ -314,60 +313,61 @@ def passwords_edit(handler, request, rowid=None):
if not rowid:
return request.page('password-edit', {'password': None})
try:
item = passdb[rowid]
except KeyError:
return request.error(f'Cannot find password: {rowid}', 404)
with handler.db.session as s:
if not (item := s.fetch('passwords', id=rowid).one()):
return request.error(f'Cannot find password: {rowid}', 404)
return request.page('password-edit', {'password': item})
@Local.route('/passwords/update', '/passwords/update/{rowid}')
def passwords_update(handler, request, rowid=None):
query = request.query
query = request.query.copy()
query.pop('redir', None)
print(query.get('label'))
if not query.get('label'):
query['label'] = f'{query.username} @ {query.domain}'
if not rowid:
row = passdb.insert(**query)
print(query.get('label'))
return request.ok_or_redirect(f'Created new password: {row.label}')
for key, value in list(query.items()):
if isinstance(value, str) and not value:
del query[key]
try:
row = passdb[rowid]
row.update(query)
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}')
except KeyError:
return request.error(f'Cannot find password: {rowid}', 404)
if not (row := s.fetch('passwords', id=rowid).one()):
return request.error(f'Cannot find password: {rowid}', 404)
return request.ok_or_redirect(f'Updated password: {row.label}')
row = s.put_password_row(row, **query)
return request.ok_or_redirect(f'updated password: {row.label}')
@Local.route('/passwords/copy/{rowid}')
def passwords_copy(handler, request, rowid):
try:
row = passdb[rowid]
except KeyError:
return request.error(f'Cannot find password: {rowid}', 404)
with handler.db.session as s:
if not (row := s.fetch('passwords', id=rowid).one()):
return request.error(f'Cannot find password: {rowid}', 404)
row.copy_password()
return request.ok_or_redirect(f'Copied password for 60 seconds')
@Local.route('/passwords/delete/{rowid}')
def passwords_update(handler, request, rowid):
try:
row = passdb[rowid]
label = row.label
row.delete()
return request.ok_or_redirect(f'Deleted password: {label}')
with handler.db.session as s:
try:
s.remove('passwords', id=rowid)
return request.ok_or_redirect(f'Deleted password: {label}')
except KeyError:
return request.error(f'Cannot find password: {rowid}', 404)
except KeyError:
return request.error(f'Cannot find password: {rowid}', 404)
### Preferences ###

@ -1 +0,0 @@
Subproject commit 6c71f895d8fc58a4cc58b593c8b7d2c509a7181e

10
webextension/meson.build Normal file
View file

@ -0,0 +1,10 @@
project('BarksharkWebExtension', 'c', 'vala', version: '0.1')
shared_library('extproxy',
'webextension.vala',
dependencies: [
dependency('webkit2gtk-web-extension-4.0'),
dependency('json-glib-1.0')
],
install: true
)

View file

@ -0,0 +1,121 @@
private void handle_console_message_sent(WebKit.WebExtension extension, WebKit.ConsoleMessage message) {
var ext_message = new WebKit.UserMessage(
"console-message-sent",
new Variant("(ssi)", message.get_text(), message.get_level().to_string(), message.get_line())
);
extension.send_message_to_context.begin(ext_message, null);
}
// private void handle_form_controls(WebKit.WebExtension extension, WebKit.WebPage page, GenericArray<WebKit.DOM.Element> elements, WebKit.Frame frame) {
// var data = new Json.Builder();
// data.begin_array();
//
// elements.foreach((elem) => {
// data.begin_object();
//
// data.set_member_name("tag");data.add_string_value(elem.tag_name);
// data.set_member_name("id");data.add_string_value(elem.id);
// data.set_member_name("name");data.add_string_value(elem.attributes.name);
//
// data.end_object();
// })
//
// data.end_array();
//
// var ext_message = new WebKit.UserMessage(
// "context-menu",
// new Variant("",
// new Variant("()", )
// )
// )
// }
// private async bool handle_send_request(WebKit.WebExtension extension, WebKit.URIRequest request, WebKit.URIResponse redirected_response) {
// var ext_message = new WebKit.UserMessage(
// "send-request",
// new Variant("(s)", request.get_uri())
// );
//
// var result = yield extension.send_message_to_context(ext_message, null);
// return result.get_parameters().get_boolean();
// }
private void handle_form_submit(WebKit.WebExtension extension, WebKit.FormSubmissionStep stage, GenericArray<string> keys, GenericArray<string> values) {
var gen = new Json.Generator();
var data = new Json.Builder();
data.begin_object();
data.set_member_name("keys");
data.begin_array();
keys.foreach((key) => {
data.add_string_value(key);
});
data.end_array();
data.set_member_name("values");
data.begin_array();
values.foreach((value) => {
data.add_string_value(value);
});
data.end_array();
data.end_object();
gen.set_root(data.get_root());
var ext_message = new WebKit.UserMessage(
"form-sent",
new Variant("(ss)", stage.to_string(), gen.to_data(null))
);
extension.send_message_to_context.begin(ext_message, null);
}
private void handle_page_created(WebKit.WebExtension extension, WebKit.WebPage page) {
var ext_message = new WebKit.UserMessage(
"page-created",
new Variant("(t)", page.get_id())
);
extension.send_message_to_context.begin(ext_message, null);
page.console_message_sent.connect((message) => {
handle_console_message_sent(extension, message);
});
// page.context_menu.connect((ContextMenu menu, WebHitResult, hit_result) => {
// handle_context_menu(extension, menu, hit_result)
// });
// page.form_controls_associated_for_frame.connect((web_page, elements, frame) => {
// handle_form_controls(extension, web_page, elements, frame)
// });
// page.send_request.connect((request, redirected_response) => {
// return handle_send_request(extension, request, redirected_response);
// });
page.will_submit_form.connect((form, stage, source_frame, target_rame, field_names, field_values) => {
handle_form_submit(extension, stage, field_names, field_values);
});
}
private void handle_message_recv(WebKit.WebExtension extension, WebKit.UserMessage message) {
print(message.get_name());
}
[CCode (cname="G_MODULE_EXPORT webkit_web_extension_initialize", has_target = false)]
public static void webkit_web_extension_initialize(WebKit.WebExtension extension) {
extension.page_created.connect(handle_page_created);
extension.user_message_received.connect(handle_message_recv);
}