diff --git a/conf.d/qutebrowser/config.py b/conf.d/qutebrowser/config.py index 476e4b5..5e50607 100644 --- a/conf.d/qutebrowser/config.py +++ b/conf.d/qutebrowser/config.py @@ -29,6 +29,7 @@ c.aliases = { "wq": "quit --save", "wqa": "quit --save", "ddg": "open ddg", + "bk": "spawn --userscript buku", } ## Time interval (in milliseconds) between auto-saves of @@ -402,13 +403,10 @@ c.backend = "webengine" c.colors.webpage.darkmode.algorithm = "lightness-cielab" c.colors.webpage.darkmode.contrast = 0.0 c.colors.webpage.darkmode.enabled = True -c.colors.webpage.darkmode.grayscale.all = False -c.colors.webpage.darkmode.grayscale.images = 0.0 -c.colors.webpage.darkmode.increase_text_contrast = True c.colors.webpage.darkmode.policy.images = "smart" c.colors.webpage.darkmode.policy.page = "smart" c.colors.webpage.darkmode.threshold.background = 205 -c.colors.webpage.darkmode.threshold.text = 150 +c.colors.webpage.darkmode.threshold.foreground = 150 c.colors.webpage.preferred_color_scheme = "dark" ## Number of commands to save in the command history. 0: no history / -1: @@ -1655,10 +1653,10 @@ config.bind("tg", "tab-give") # windows movements config.bind("", "close") -# quickmarks -config.bind("b", "cmd-set-text -s :quickmark-load") -config.bind("B", "cmd-set-text -s :quickmark-load -t") -config.bind("m", "quickmark-save") +# # quickmarks +# config.bind("b", "cmd-set-text -s :quickmark-load") +# config.bind("B", "cmd-set-text -s :quickmark-load -t") +# config.bind("m", "quickmark-save") config.bind("dc", "download-clear") @@ -1698,7 +1696,7 @@ config.bind("csu", "config-cycle -p -t -u {url} content.javascript.enabled ;; re config.bind( "qa", "spawn --userscript \ - ~/.config/qutebrowser/userscripts/password_fill", + /usr/share/qutebrowser/userscripts/password_fill", ) diff --git a/conf.d/qutebrowser/userscripts/password_fill b/conf.d/qutebrowser/userscripts/password_fill deleted file mode 100755 index c46253d..0000000 --- a/conf.d/qutebrowser/userscripts/password_fill +++ /dev/null @@ -1,385 +0,0 @@ -#!/usr/bin/env bash -help() { - blink=$'\e[1;31m' reset=$'\e[0m' -cat < -In case of questions or suggestions, do not hesitate to send me an E-Mail or to -directly ask me via IRC (nickname thorsten\`) in #qutebrowser on Libera Chat. - - $blink!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!$reset - WARNING: the passwords are stored in qutebrowser's - debug log reachable via the url qute://log - $blink!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!$reset - -Usage: run as a userscript form qutebrowser, e.g.: - spawn --userscript ~/.config/qutebrowser/password_fill - -Pass backend: (see also passwordstore.org) - This script expects pass to store the credentials of each page in an extra - file, where the filename (or filepath) contains the domain of the respective - page. The first line of the file must contain the password, the login name - must be contained in a later line beginning with "user:", "login:", or - "username:" (configurable by the user_pattern variable). - -Behavior: - It will try to find a username/password entry in the configured backend - (currently only pass) for the current website and will load that pair of - username and password to any form on the current page that has some password - entry field. If multiple entries are found, a zenity menu is offered. - - If no entry is found, then it crops subdomains from the url if at least one - entry is found in the backend. (In that case, it always shows a menu) - -Configuration: - This script loads the bash script ~/.config/qutebrowser/password_fill_rc (if - it exists), so you can change any configuration variable and overwrite any - function you like. - -EOF -} - -set -o errexit -set -o pipefail -shopt -s nocasematch # make regexp matching in bash case insensitive - -if [ -z "$QUTE_FIFO" ] ; then - help - exit -fi - -error() { - local msg="$*" - echo "message-error '${msg//\'/\\\'}'" >> "$QUTE_FIFO" -} -msg() { - local msg="$*" - echo "message-info '${msg//\'/\\\'}'" >> "$QUTE_FIFO" -} -die() { - error "$*" - exit 0 -} - -javascript_escape() { - # print the first argument in an escaped way, such that it can safely - # be used within javascripts double quotes - # shellcheck disable=SC2001 - sed "s,[\\\\'\"],\\\\&,g" <<< "$1" -} - -# ======================================================= # -# CONFIGURATION -# ======================================================= # -# The configuration file is per default located in -# ~/.config/qutebrowser/password_fill_rc and is a bash script that is loaded -# later in the present script. So basically you can replace all of the -# following definitions and make them fit your needs. - -# The following simplifies a URL to the domain (e.g. "wiki.qutebrowser.org") -# which is later used to search the correct entries in the password backend. If -# you e.g. don't want the "www." to be removed or if you want to distinguish -# between different paths on the same domain. - -simplify_url() { - simple_url="${1##*://}" # remove protocol specification - simple_url="${simple_url%%\?*}" # remove GET parameters - simple_url="${simple_url%%/*}" # remove directory path - simple_url="${simple_url%:*}" # remove port - simple_url="${simple_url##www.}" # remove www. subdomain -} - -# no_entries_found() is called if the first query_entries() call did not find -# any matching entries. Multiple implementations are possible: -# The easiest behavior is to quit: -#no_entries_found() { -# if [ 0 -eq "${#files[@]}" ] ; then -# die "No entry found for »$simple_url«" -# fi -#} -# But you could also fill the files array with all entries from your pass db -# if the first db query did not find anything -# no_entries_found() { -# if [ 0 -eq "${#files[@]}" ] ; then -# query_entries "" -# if [ 0 -eq "${#files[@]}" ] ; then -# die "No entry found for »$simple_url«" -# fi -# fi -# } - -# Another behavior is to drop another level of subdomains until search hits -# are found: -no_entries_found() { - while [ 0 -eq "${#files[@]}" ] && [ -n "$simple_url" ]; do - # shellcheck disable=SC2001 - shorter_simple_url=$(sed 's,^[^.]*\.,,' <<< "$simple_url") - if [ "$shorter_simple_url" = "$simple_url" ] ; then - # if no dot, then even remove the top level domain - simple_url="" - query_entries "$simple_url" - break - fi - simple_url="$shorter_simple_url" - query_entries "$simple_url" - #die "No entry found for »$simple_url«" - # enforce menu if we do "fuzzy" matching - menu_if_one_entry=1 - done - if [ 0 -eq "${#files[@]}" ] ; then - die "No entry found for »$simple_url«" - fi -} - -# Backend implementations tell, how the actual password store is accessed. -# Right now, there is only one fully functional password backend, namely for -# the program "pass". -# A password backend consists of three actions: -# - init() initializes backend-specific things and does sanity checks. -# - query_entries() is called with a simplified url and is expected to fill -# the bash array $files with the names of matching password entries. There -# are no requirements how these names should look like. -# - open_entry() is called with some specific entry of the $files array and is -# expected to write the username of that entry to the $username variable and -# the corresponding password to $password - -reset_backend() { - init() { true ; } - query_entries() { true ; } - open_entry() { true ; } -} - -# choose_entry() is expected to choose one entry from the array $files and -# write it to the variable $file. -choose_entry() { - choose_entry_zenity -} - -# The default implementation chooses a random entry from the array. So if there -# are multiple matching entries, multiple calls to this userscript will -# eventually pick the "correct" entry. I.e. if this userscript is bound to -# "zl", the user has to press "zl" until the correct username shows up in the -# login form. -choose_entry_random() { - local nr=${#files[@]} - file="${files[$((RANDOM % nr))]}" - # Warn user, that there might be other matching password entries - if [ "$nr" -gt 1 ] ; then - msg "Picked $file out of $nr entries: ${files[*]}" - fi -} - -# another implementation would be to ask the user via some menu (like rofi or -# dmenu or zenity or even qutebrowser completion in future?) which entry to -# pick -MENU_COMMAND=( head -n 1 ) -# whether to show the menu if there is only one entry in it -menu_if_one_entry=0 -choose_entry_menu() { - local nr=${#files[@]} - if [ "$nr" -eq 1 ] && ! ((menu_if_one_entry)) ; then - file="${files[0]}" - else - file=$( printf '%s\n' "${files[@]}" | "${MENU_COMMAND[@]}" ) - fi -} - -choose_entry_rofi() { - MENU_COMMAND=( rofi -p "qutebrowser> " -dmenu - -mesg $'Pick a password entry for '"${QUTE_URL//&/&}"'' ) - choose_entry_menu || true -} - -choose_entry_zenity() { - MENU_COMMAND=( zenity --list --title "qutebrowser password fill" - --text "Pick the password entry:" - --column "Name" ) - choose_entry_menu || true -} - -choose_entry_zenity_radio() { - zenity_helper() { - awk '{ print $0 ; print $0 }' \ - | zenity --list --radiolist \ - --title "qutebrowser password fill" \ - --text "Pick the password entry:" \ - --column " " --column "Name" - } - MENU_COMMAND=( zenity_helper ) - choose_entry_menu || true -} - -# ======================================================= -# backend: PASS - -# configuration options: -match_filename=1 # whether allowing entry match by filepath -match_line=0 # whether allowing entry match by URL-Pattern in file - # Note: match_line=1 gets very slow, even for small password stores! -match_line_pattern='^url: .*' # applied using grep -iE -user_pattern='^(user|username|login): ' - -GPG_OPTS=( "--quiet" "--yes" "--compress-algo=none" "--no-encrypt-to" ) -GPG="gpg" -export GPG_TTY="${GPG_TTY:-$(tty 2>/dev/null)}" -command -v gpg2 &>/dev/null && GPG="gpg2" -[[ -n $GPG_AGENT_INFO || $GPG == "gpg2" ]] && GPG_OPTS+=( "--batch" "--use-agent" ) - -pass_backend() { - init() { - PREFIX="${PASSWORD_STORE_DIR:-$HOME/.password-store}" - if ! [ -d "$PREFIX" ] ; then - die "Can not open password store dir »$PREFIX«" - fi - } - query_entries() { - local url="$1" - - if ((match_line)) ; then - # add entries with matching URL-tag - while read -r -d "" passfile ; do - if $GPG "${GPG_OPTS[@]}" -d "$passfile" \ - | grep --max-count=1 -iE "${match_line_pattern}${url}" > /dev/null - then - passfile="${passfile#$PREFIX}" - passfile="${passfile#/}" - files+=( "${passfile%.gpg}" ) - fi - done < <(find -L "$PREFIX" -iname '*.gpg' -print0) - fi - if ((match_filename)) ; then - # add entries with matching filepath - while read -r passfile ; do - passfile="${passfile#$PREFIX}" - passfile="${passfile#/}" - files+=( "${passfile%.gpg}" ) - done < <(find -L "$PREFIX" -iname '*.gpg' | grep "$url") - fi - } - open_entry() { - local path="$PREFIX/${1}.gpg" - password="" - local firstline=1 - while read -r line ; do - if ((firstline)) ; then - password="$line" - firstline=0 - else - if [[ $line =~ $user_pattern ]] ; then - # remove the matching prefix "user: " from the beginning of the line - username=${line#${BASH_REMATCH[0]}} - break - fi - fi - done < <($GPG "${GPG_OPTS[@]}" -d "$path" | awk 1 ) - } -} -# ======================================================= - -# ======================================================= -# backend: secret -secret_backend() { - init() { - return - } - query_entries() { - local domain="$1" - while read -r line ; do - if [[ "$line" == "attribute.username = "* ]] ; then - files+=("$domain ${line:21}") - fi - done < <( secret-tool search --unlock --all domain "$domain" 2>&1 ) - } - open_entry() { - local domain="${1%% *}" - username="${1#* }" - password=$(secret-tool lookup domain "$domain" username "$username") - } -} -# ======================================================= - -# load some sane default backend -reset_backend -pass_backend -# load configuration -QUTE_CONFIG_DIR=${QUTE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/qutebrowser/} -PWFILL_CONFIG=${PWFILL_CONFIG:-${QUTE_CONFIG_DIR}/password_fill_rc} -if [ -f "$PWFILL_CONFIG" ] ; then - # shellcheck source=/dev/null - source "$PWFILL_CONFIG" -fi -init - -simplify_url "$QUTE_URL" -query_entries "${simple_url}" -no_entries_found -# remove duplicates -mapfile -t files < <(printf '%s\n' "${files[@]}" | sort | uniq ) -choose_entry -if [ -z "$file" ] ; then - # choose_entry didn't want any of these entries - exit 0 -fi -open_entry "$file" -#username="$(date)" -#password="XYZ" -#msg "$username, ${#password}" - -[ -n "$username" ] || die "Username not set in entry $file" -[ -n "$password" ] || die "Password not set in entry $file" - -js() { -cat < 0 && elem.offsetHeight > 0; - }; - function hasPasswordField(form) { - var inputs = form.getElementsByTagName("input"); - for (var j = 0; j < inputs.length; j++) { - var input = inputs[j]; - if (input.type == "password") { - return true; - } - } - return false; - }; - function loadData2Form (form) { - var inputs = form.getElementsByTagName("input"); - for (var j = 0; j < inputs.length; j++) { - var input = inputs[j]; - if (isVisible(input) && (input.type == "text" || input.type == "email")) { - input.focus(); - input.value = "$(javascript_escape "${username}")"; - input.dispatchEvent(new Event('change')); - input.blur(); - } - if (input.type == "password") { - input.focus(); - input.value = "$(javascript_escape "${password}")"; - input.dispatchEvent(new Event('change')); - input.blur(); - } - } - }; - - var forms = document.getElementsByTagName("form"); - for (i = 0; i < forms.length; i++) { - if (hasPasswordField(forms[i])) { - loadData2Form(forms[i]); - } - } -EOF -} - -printjs() { - js | sed 's,//.*$,,' | tr '\n' ' ' -} -echo "jseval -q $(printjs)" >> "$QUTE_FIFO" diff --git a/conf.d/qutebrowser/userscripts/qute-pass b/conf.d/qutebrowser/userscripts/qute-pass deleted file mode 100755 index 4197816..0000000 --- a/conf.d/qutebrowser/userscripts/qute-pass +++ /dev/null @@ -1,251 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2017 Chris Braun (cryzed) -# -# This file is part of qutebrowser. -# -# qutebrowser is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# qutebrowser is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with qutebrowser. If not, see . - -""" -Insert login information using pass and a dmenu-compatible application (e.g. dmenu, rofi -dmenu, ...). A short -demonstration can be seen here: https://i.imgur.com/KN3XuZP.gif. -""" - -USAGE = """The domain of the site has to appear as a segment in the pass path, for example: "github.com/cryzed" or -"websites/github.com". How the username and password are determined is freely configurable using the CLI arguments. The -login information is inserted by emulating key events using qutebrowser's fake-key command in this manner: -[USERNAME][PASSWORD], which is compatible with almost all login forms. - -If you use gopass with multiple mounts, use the CLI switch --mode gopass to switch to gopass mode. - -Suggested bindings similar to Uzbl's `formfiller` script: - - config.bind('', 'spawn --userscript qute-pass') - config.bind('', 'spawn --userscript qute-pass --username-only') - config.bind('

', 'spawn --userscript qute-pass --password-only') - config.bind('', 'spawn --userscript qute-pass --otp-only') -""" - -EPILOG = """Dependencies: tldextract (Python 3 module), pass, pass-otp (optional). -For issues and feedback please use: https://github.com/cryzed/qutebrowser-userscripts. - -WARNING: The login details are viewable as plaintext in qutebrowser's debug log (qute://log) and might be shared if -you decide to submit a crash report!""" - -import argparse -import enum -import fnmatch -import functools -import os -import re -import shlex -import subprocess -import sys - -import tldextract - -argument_parser = argparse.ArgumentParser(description=__doc__, usage=USAGE, epilog=EPILOG) -argument_parser.add_argument('url', nargs='?', default=os.getenv('QUTE_URL')) -argument_parser.add_argument('--password-store', '-p', - default=os.getenv('PASSWORD_STORE_DIR', default=os.path.expanduser('~/.password-store')), - help='Path to your pass password-store (only used in pass-mode)') -argument_parser.add_argument('--mode', '-M', choices=['pass', 'gopass'], default="pass", - help='Select mode [gopass] to use gopass instead of the standard pass.') -argument_parser.add_argument('--username-pattern', '-u', default=r'.*/(.+)', - help='Regular expression that matches the username') -argument_parser.add_argument('--username-target', '-U', choices=['path', 'secret'], default='path', - help='The target for the username regular expression') -argument_parser.add_argument('--password-pattern', '-P', default=r'(.*)', - help='Regular expression that matches the password') -argument_parser.add_argument('--dmenu-invocation', '-d', default='rofi -dmenu', - help='Invocation used to execute a dmenu-provider') -argument_parser.add_argument('--no-insert-mode', '-n', dest='insert_mode', action='store_false', - help="Don't automatically enter insert mode") -argument_parser.add_argument('--io-encoding', '-i', default='UTF-8', - help='Encoding used to communicate with subprocesses') -argument_parser.add_argument('--merge-candidates', '-m', action='store_true', - help='Merge pass candidates for fully-qualified and registered domain name') -argument_parser.add_argument('--extra-url-suffixes', '-s', default='', - help='Comma-separated string containing extra suffixes (e.g local)') -group = argument_parser.add_mutually_exclusive_group() -group.add_argument('--username-only', '-e', action='store_true', help='Only insert username') -group.add_argument('--password-only', '-w', action='store_true', help='Only insert password') -group.add_argument('--otp-only', '-o', action='store_true', help='Only insert OTP code') - -stderr = functools.partial(print, file=sys.stderr) - - -class ExitCodes(enum.IntEnum): - SUCCESS = 0 - FAILURE = 1 - # 1 is automatically used if Python throws an exception - NO_PASS_CANDIDATES = 2 - COULD_NOT_MATCH_USERNAME = 3 - COULD_NOT_MATCH_PASSWORD = 4 - - -def qute_command(command): - with open(os.environ['QUTE_FIFO'], 'w') as fifo: - fifo.write(command + '\n') - fifo.flush() - - -def find_pass_candidates(domain, password_store_path): - candidates = [] - - if arguments.mode == "gopass": - all_passwords = subprocess.run(["gopass", "list", "--flat" ], stdout=subprocess.PIPE).stdout.decode("UTF-8").splitlines() - - for password in all_passwords: - if domain in password: - candidates.append(password) - else: - for path, directories, file_names in os.walk(password_store_path, followlinks=True): - if directories or domain not in path.split(os.path.sep): - continue -# secrets = fnmatch.filter(file_names, '*.gpg') -# if not secrets: -# continue -# - # Strip password store path prefix to get the relative pass path - pass_path = path[len(password_store_path):] - secrets = fnmatch.filter(file_names, '*.gpg') - candidates.extend(os.path.join(pass_path, os.path.splitext(secret)[0]) for secret in secrets) - -# pass_path = path[len(password_store_path):] -# split_path = pass_path.split(os.path.sep) -# for secret in secrets: -# secret_base = os.path.splitext(secret)[0] -# if domain not in (split_path + [secret_base]): -# continue -# -# candidates.append(os.path.join(pass_path, secret_base)) - return candidates - - -def _run_pass(pass_arguments, encoding): - # The executable is conveniently named after it's mode [pass|gopass]. - pass_command = [arguments.mode] - process = subprocess.run(pass_command + pass_arguments, stdout=subprocess.PIPE) - return process.stdout.decode(encoding).strip() - - -def pass_(path, encoding): - return _run_pass([path], encoding) - - -def pass_otp(path, encoding): - return _run_pass(['otp', path], encoding) - - -def dmenu(items, invocation, encoding): - command = shlex.split(invocation) - process = subprocess.run(command, input='\n'.join(items).encode(encoding), stdout=subprocess.PIPE) - return process.stdout.decode(encoding).strip() - - -def fake_key_raw(text): - for character in text: - # Escape all characters by default, space requires special handling - sequence = '" "' if character == ' ' else '\{}'.format(character) - qute_command('fake-key {}'.format(sequence)) - - -def main(arguments): - if not arguments.url: - argument_parser.print_help() - return ExitCodes.FAILURE - - extractor = tldextract.TLDExtract(extra_suffixes=arguments.extra_url_suffixes.split(',')) - extract_result = extractor(arguments.url) - - # Expand potential ~ in paths, since this script won't be called from a shell that does it for us - password_store_path = os.path.expanduser(arguments.password_store) - # Add trailing slash if not present - password_store_path = os.path.join(password_store_path, '') - - # Try to find candidates using targets in the following order: fully-qualified domain name (includes subdomains), - # the registered domain name, the IPv4 address if that's what the URL represents and finally the private domain - # (if a non-public suffix was used). - candidates = set() - attempted_targets = [] - - private_domain = '' - if not extract_result.suffix: - private_domain = ('.'.join((extract_result.subdomain, extract_result.domain)) - if extract_result.subdomain else extract_result.domain) - - for target in filter(None, [extract_result.fqdn, extract_result.registered_domain, extract_result.ipv4, private_domain]): - attempted_targets.append(target) - target_candidates = find_pass_candidates(target, password_store_path) - if not target_candidates: - continue - - candidates.update(target_candidates) - if not arguments.merge_candidates: - break - else: - if not candidates: - stderr('No pass candidates for URL {!r} found! (I tried {!r})'.format(arguments.url, attempted_targets)) - return ExitCodes.NO_PASS_CANDIDATES - - selection = candidates.pop() if len(candidates) == 1 else dmenu(sorted(candidates), arguments.dmenu_invocation, - arguments.io_encoding) - # Nothing was selected, simply return - if not selection: - return ExitCodes.SUCCESS - - # If username-target is path and user asked for username-only, we don't need to run pass - secret = None - if not (arguments.username_target == 'path' and arguments.username_only): - secret = pass_(selection, arguments.io_encoding) - - # Match password - match = re.match(arguments.password_pattern, secret) - if not match: - stderr('Failed to match password pattern on secret!') - return ExitCodes.COULD_NOT_MATCH_PASSWORD - password = match.group(1) - - # Match username - target = selection if arguments.username_target == 'path' else secret - match = re.search(arguments.username_pattern, target, re.MULTILINE) - if not match: - stderr('Failed to match username pattern on {}!'.format(arguments.username_target)) - return ExitCodes.COULD_NOT_MATCH_USERNAME - username = match.group(1) - - if arguments.username_only: - fake_key_raw(username) - elif arguments.password_only: - fake_key_raw(password) - elif arguments.otp_only: - otp = pass_otp(selection, arguments.io_encoding) - fake_key_raw(otp) - else: - # Enter username and password using fake-key and (which seems to work almost universally), then switch - # back into insert-mode, so the form can be directly submitted by hitting enter afterwards - fake_key_raw(username) - qute_command('fake-key ') - fake_key_raw(password) - - if arguments.insert_mode: - qute_command('mode-enter insert') - - return ExitCodes.SUCCESS - - -if __name__ == '__main__': - arguments = argument_parser.parse_args() - sys.exit(main(arguments))