Up.
This commit is contained in:
parent
9f5a579a11
commit
1e316312f8
@ -29,6 +29,7 @@ c.aliases = {
|
|||||||
"wq": "quit --save",
|
"wq": "quit --save",
|
||||||
"wqa": "quit --save",
|
"wqa": "quit --save",
|
||||||
"ddg": "open ddg",
|
"ddg": "open ddg",
|
||||||
|
"bk": "spawn --userscript buku",
|
||||||
}
|
}
|
||||||
|
|
||||||
## Time interval (in milliseconds) between auto-saves of
|
## 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.algorithm = "lightness-cielab"
|
||||||
c.colors.webpage.darkmode.contrast = 0.0
|
c.colors.webpage.darkmode.contrast = 0.0
|
||||||
c.colors.webpage.darkmode.enabled = True
|
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.images = "smart"
|
||||||
c.colors.webpage.darkmode.policy.page = "smart"
|
c.colors.webpage.darkmode.policy.page = "smart"
|
||||||
c.colors.webpage.darkmode.threshold.background = 205
|
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"
|
c.colors.webpage.preferred_color_scheme = "dark"
|
||||||
|
|
||||||
## Number of commands to save in the command history. 0: no history / -1:
|
## Number of commands to save in the command history. 0: no history / -1:
|
||||||
@ -1655,10 +1653,10 @@ config.bind("tg", "tab-give")
|
|||||||
# windows movements
|
# windows movements
|
||||||
config.bind("<Ctrl-Shift-w>", "close")
|
config.bind("<Ctrl-Shift-w>", "close")
|
||||||
|
|
||||||
# quickmarks
|
# # quickmarks
|
||||||
config.bind("b", "cmd-set-text -s :quickmark-load")
|
# config.bind("b", "cmd-set-text -s :quickmark-load")
|
||||||
config.bind("B", "cmd-set-text -s :quickmark-load -t")
|
# config.bind("B", "cmd-set-text -s :quickmark-load -t")
|
||||||
config.bind("m", "quickmark-save")
|
# config.bind("m", "quickmark-save")
|
||||||
|
|
||||||
config.bind("dc", "download-clear")
|
config.bind("dc", "download-clear")
|
||||||
|
|
||||||
@ -1698,7 +1696,7 @@ config.bind("csu", "config-cycle -p -t -u {url} content.javascript.enabled ;; re
|
|||||||
config.bind(
|
config.bind(
|
||||||
"qa",
|
"qa",
|
||||||
"spawn --userscript \
|
"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