Up.
This commit is contained in:
parent
9f5a579a11
commit
1e316312f8
@ -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("<Ctrl-Shift-w>", "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",
|
||||
)
|
||||
|
||||
|
||||
|
@ -1,385 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
help() {
|
||||
blink=$'\e[1;31m' reset=$'\e[0m'
|
||||
cat <<EOF
|
||||
This script can only be used as a userscript for qutebrowser
|
||||
2015, Thorsten Wißmann <edu _at_ thorsten-wissmann _dot_ de>
|
||||
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 <b>'"${QUTE_URL//&/&}"'</b>' )
|
||||
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 <<EOF
|
||||
function isVisible(elem) {
|
||||
var style = elem.ownerDocument.defaultView.getComputedStyle(elem, null);
|
||||
|
||||
if (style.getPropertyValue("visibility") !== "visible" ||
|
||||
style.getPropertyValue("display") === "none" ||
|
||||
style.getPropertyValue("opacity") === "0") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return elem.offsetWidth > 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"
|
@ -1,251 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright 2017 Chris Braun (cryzed) <cryzed@googlemail.com>
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
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]<Tab>[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('<z><l>', 'spawn --userscript qute-pass')
|
||||
config.bind('<z><u><l>', 'spawn --userscript qute-pass --username-only')
|
||||
config.bind('<z><p><l>', 'spawn --userscript qute-pass --password-only')
|
||||
config.bind('<z><o><l>', '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 <Tab> (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 <Tab>')
|
||||
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))
|
Loading…
x
Reference in New Issue
Block a user