Compare commits
23 Commits
Author | SHA1 | Date |
---|---|---|
simonkellet | f84de51208 | 8 months ago |
simonkellet | 3752afd442 | 11 months ago |
simonkellet | d0b12cc649 | 11 months ago |
simonkellet | 1c493a18d5 | 2 years ago |
simonkellet | 9d21e238eb | 2 years ago |
simonkellet | c3f73a7863 | 2 years ago |
simonkellet | 44cac44e4c | 2 years ago |
simonkellet | d7d1a8eedc | 2 years ago |
simonkellet | de6455136e | 2 years ago |
simonkellet | 79bd690e49 | 2 years ago |
simonkellet | 04c9ceda03 | 2 years ago |
Simon Kellet | 3a1e82ff49 | 2 years ago |
Simon Kellet | 09a241efb9 | 2 years ago |
Simon Kellet | 7bff8a4f00 | 2 years ago |
Simon Kellet | 53582d2a7c | 2 years ago |
Simon Kellet | 3921a49856 | 2 years ago |
Simon Kellet | 38849a9f8b | 2 years ago |
Simon Kellet | 9c25c9f531 | 2 years ago |
Simon Kellet | d314f20b5e | 2 years ago |
Simon Kellet | e7462827d5 | 2 years ago |
Simon Kellet | 8212d1f5e2 | 2 years ago |
Simon Kellet | c38f8b5633 | 2 years ago |
Simon Kellet | faa991bbaf | 2 years ago |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,35 @@ |
||||
#!/bin/sh |
||||
|
||||
BLANK='#282a36' |
||||
CLEAR='#282a36' |
||||
DEFAULT='#bd93f9' |
||||
TEXT='#bd93f9' |
||||
WRONG='#ff5555' |
||||
VERIFYING='#50fa7b' |
||||
|
||||
i3lock \ |
||||
--insidever-color=$CLEAR \ |
||||
--ringver-color=$VERIFYING \ |
||||
\ |
||||
--insidewrong-color=$CLEAR \ |
||||
--ringwrong-color=$WRONG \ |
||||
\ |
||||
--inside-color=$BLANK \ |
||||
--ring-color=$DEFAULT \ |
||||
--line-color=$BLANK \ |
||||
--separator-color=$DEFAULT \ |
||||
\ |
||||
--verif-color=$TEXT \ |
||||
--wrong-color=$TEXT \ |
||||
--time-color=$TEXT \ |
||||
--date-color=$TEXT \ |
||||
--layout-color=$TEXT \ |
||||
--keyhl-color=$WRONG \ |
||||
--bshl-color=$WRONG \ |
||||
\ |
||||
--screen 1 \ |
||||
--blur 5 \ |
||||
--clock \ |
||||
--indicator \ |
||||
--time-str="%H:%M:%S" \ |
||||
--date-str="%A %d/%m/%Y" \ |
@ -0,0 +1,31 @@ |
||||
#!/bin/sh |
||||
|
||||
i3lock \ |
||||
--blur 5 \ |
||||
--bar-indicator \ |
||||
--bar-pos y+h \ |
||||
--bar-direction 1 \ |
||||
--bar-max-height 50 \ |
||||
--bar-base-width 50 \ |
||||
--bar-color 000000cc \ |
||||
--keyhl-color 880088cc \ |
||||
--bar-periodic-step 50 \ |
||||
--bar-step 50 \ |
||||
--redraw-thread \ |
||||
\ |
||||
--clock \ |
||||
--force-clock \ |
||||
--time-pos x+5:y+h-80 \ |
||||
--time-color 880088ff \ |
||||
--date-pos tx:ty+15 \ |
||||
--date-color 990099ff \ |
||||
--date-align 1 \ |
||||
--time-align 1 \ |
||||
--ringver-color 8800ff88 \ |
||||
--ringwrong-color ff008888 \ |
||||
--status-pos x+5:y+h-16 \ |
||||
--verif-align 1 \ |
||||
--wrong-align 1 \ |
||||
--verif-color ffffffff \ |
||||
--wrong-color ffffffff \ |
||||
--modif-pos -50:-50 |
@ -0,0 +1,19 @@ |
||||
foreground #f8f8f2 |
||||
background #282a36 |
||||
title_fg #f8f8f2 |
||||
title_bg #282a36 |
||||
margin_bg #6272a4 |
||||
margin_fg #44475a |
||||
removed_bg #ff5555 |
||||
highlight_removed_bg #ff5555 |
||||
removed_margin_bg #ff5555 |
||||
added_bg #50fa7b |
||||
highlight_added_bg #50fa7b |
||||
added_margin_bg #50fa7b |
||||
filler_bg #44475a |
||||
hunk_margin_bg #44475a |
||||
hunk_bg #bd93f9 |
||||
search_bg #8be9fd |
||||
search_fg #282a36 |
||||
select_bg #f1fa8c |
||||
select_fg #282a36 |
@ -0,0 +1,62 @@ |
||||
# https://draculatheme.com/kitty |
||||
# |
||||
# Installation instructions: |
||||
# |
||||
# cp dracula.conf ~/.config/kitty/ |
||||
# echo "include dracula.conf" >> ~/.config/kitty/kitty.conf |
||||
# |
||||
# Then reload kitty for the config to take affect. |
||||
# Alternatively copy paste below directly into kitty.conf |
||||
|
||||
foreground #f8f8f2 |
||||
background #282a36 |
||||
selection_foreground #ffffff |
||||
selection_background #44475a |
||||
|
||||
url_color #8be9fd |
||||
|
||||
# black |
||||
color0 #21222c |
||||
color8 #6272a4 |
||||
|
||||
# red |
||||
color1 #ff5555 |
||||
color9 #ff6e6e |
||||
|
||||
# green |
||||
color2 #50fa7b |
||||
color10 #69ff94 |
||||
|
||||
# yellow |
||||
color3 #f1fa8c |
||||
color11 #ffffa5 |
||||
|
||||
# blue |
||||
color4 #bd93f9 |
||||
color12 #d6acff |
||||
|
||||
# magenta |
||||
color5 #ff79c6 |
||||
color13 #ff92df |
||||
|
||||
# cyan |
||||
color6 #8be9fd |
||||
color14 #a4ffff |
||||
|
||||
# white |
||||
color7 #f8f8f2 |
||||
color15 #ffffff |
||||
|
||||
# Cursor colors |
||||
cursor #f8f8f2 |
||||
cursor_text_color background |
||||
|
||||
# Tab bar colors |
||||
active_tab_foreground #282a36 |
||||
active_tab_background #f8f8f2 |
||||
inactive_tab_foreground #282a36 |
||||
inactive_tab_background #6272a4 |
||||
|
||||
# Marks |
||||
mark1_foreground #282a36 |
||||
mark1_background #ff5555 |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1 @@ |
||||
Subproject commit b1abdd54ba655ef34f75a568d78625981bf1722c |
@ -0,0 +1,5 @@ |
||||
snippet beg "begin{} / end{}" bA |
||||
\begin{$1} |
||||
$0 |
||||
\end{$1} |
||||
endsnippet |
@ -0,0 +1,3 @@ |
||||
snippet std "use namespace std" b |
||||
using namespace std; |
||||
endsnippet |
Binary file not shown.
@ -0,0 +1,8 @@ |
||||
[FileDialog] |
||||
history=file:///home/simon/Calibre Library/Jason Cannon/Linux for Beginners_ An Introduction to the Linux Operating System and Command Line (7), file:///home/simon/Calibre Library/Jonathan Moeller/The Linux Command Line Beginner's Guide (6), file:///home/simon, file:///home/simon/Downloads |
||||
lastVisited=file:///home/simon/Downloads |
||||
qtVersion=5.15.2 |
||||
shortcuts=file:, file:///home/simon |
||||
sidebarWidth=97 |
||||
treeViewHeader="@ByteArray(\0\0\0\xff\0\0\0\0\0\0\0\x1\0\0\0\0\0\0\0\0\x1\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x1\xeb\0\0\0\x4\x1\x1\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x64\xff\xff\xff\xff\0\0\0\x81\0\0\0\0\0\0\0\x4\0\0\x1\b\0\0\0\x1\0\0\0\0\0\0\0;\0\0\0\x1\0\0\0\0\0\0\0@\0\0\0\x1\0\0\0\0\0\0\0h\0\0\0\x1\0\0\0\0\0\0\x3\xe8\0\xff\xff\xff\xff)" |
||||
viewMode=Detail |
@ -1,278 +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 <https://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. As an |
||||
example, if you instead store the username as part of the secret (and use a |
||||
site's name as filename), instead of the default configuration, use |
||||
`--username-target secret` and `--username-pattern "username: (.+)"`. |
||||
|
||||
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 |
||||
|
||||
|
||||
def expanded_path(path): |
||||
# Expand potential ~ in paths, since this script won't be called from a shell that does it for us |
||||
expanded = os.path.expanduser(path) |
||||
# Add trailing slash if not present |
||||
return os.path.join(expanded, '') |
||||
|
||||
|
||||
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=expanded_path(os.getenv('PASSWORD_STORE_DIR', default='~/.password-store')), |
||||
help='Path to your pass password-store (only used in pass-mode)', type=expanded_path) |
||||
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('--noinsert-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 |
||||
|
||||
|
||||
class CouldNotMatchUsername(Exception): |
||||
pass |
||||
|
||||
|
||||
class CouldNotMatchPassword(Exception): |
||||
pass |
||||
|
||||
|
||||
def qute_command(command): |
||||
with open(os.environ['QUTE_FIFO'], 'w') as fifo: |
||||
fifo.write(command + '\n') |
||||
fifo.flush() |
||||
|
||||
|
||||
def find_pass_candidates(domain): |
||||
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(arguments.password_store, followlinks=True): |
||||
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(arguments.password_store):] |
||||
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): |
||||
# The executable is conveniently named after it's mode [pass|gopass]. |
||||
pass_command = [arguments.mode] |
||||
env = os.environ.copy() |
||||
env['PASSWORD_STORE_DIR'] = arguments.password_store |
||||
process = subprocess.run(pass_command + pass_arguments, env=env, stdout=subprocess.PIPE) |
||||
return process.stdout.decode(arguments.io_encoding).strip() |
||||
|
||||
|
||||
def pass_(path): |
||||
return _run_pass(['show', path]) |
||||
|
||||
|
||||
def pass_otp(path): |
||||
if arguments.mode == "gopass": |
||||
return _run_pass(['otp', '-o', path]) |
||||
return _run_pass(['otp', path]) |
||||
|
||||
|
||||
def dmenu(items, invocation): |
||||
command = shlex.split(invocation) |
||||
process = subprocess.run(command, input='\n'.join(items).encode(arguments.io_encoding), stdout=subprocess.PIPE) |
||||
return process.stdout.decode(arguments.io_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 extract_password(secret, pattern): |
||||
match = re.match(pattern, secret) |
||||
if not match: |
||||
raise CouldNotMatchPassword("Pattern did not match target") |
||||
try: |
||||
return match.group(1) |
||||
except IndexError: |
||||
raise CouldNotMatchPassword("Pattern did not contain capture group, please use capture group. Example: (.*)") |
||||
|
||||
|
||||
def extract_username(target, pattern): |
||||
match = re.search(pattern, target, re.MULTILINE) |
||||
if not match: |
||||
raise CouldNotMatchUsername("Pattern did not match target") |
||||
try: |
||||
return match.group(1) |
||||
except IndexError: |
||||
raise CouldNotMatchUsername("Pattern did not contain capture group, please use capture group. Example: (.*)") |
||||
|
||||
|
||||
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) |
||||
|
||||
# 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) |
||||
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) |
||||
# 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. |
||||
# Or if using otp-only, it will run pass on its own. |
||||
secret = None |
||||
if not (arguments.username_target == 'path' and arguments.username_only) and not arguments.otp_only: |
||||
secret = pass_(selection) |
||||
username_target = selection if arguments.username_target == 'path' else secret |
||||
try: |
||||
if arguments.username_only: |
||||
fake_key_raw(extract_username(username_target, arguments.username_pattern)) |
||||
elif arguments.password_only: |
||||
fake_key_raw(extract_password(secret, arguments.password_pattern)) |
||||
elif arguments.otp_only: |
||||
otp = pass_otp(selection) |
||||
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(extract_username(username_target, arguments.username_pattern)) |
||||
qute_command('fake-key <Tab>') |
||||
fake_key_raw(extract_password(secret, arguments.password_pattern)) |
||||
except CouldNotMatchPassword as e: |
||||
stderr('Failed to match password, target: secret, error: {}'.format(e)) |
||||
return ExitCodes.COULD_NOT_MATCH_PASSWORD |
||||
except CouldNotMatchUsername as e: |
||||
stderr('Failed to match username, target: {}, error: {}'.format(arguments.username_target, e)) |
||||
return ExitCodes.COULD_NOT_MATCH_USERNAME |
||||
|
||||
if arguments.insert_mode: |
||||
qute_command('mode-enter insert') |
||||
|
||||
return ExitCodes.SUCCESS |
||||
|
||||
|
||||
if __name__ == '__main__': |
||||
arguments = argument_parser.parse_args() |
||||
sys.exit(main(arguments)) |
@ -1,278 +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 <https://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. As an |
||||
example, if you instead store the username as part of the secret (and use a |
||||
site's name as filename), instead of the default configuration, use |
||||
`--username-target secret` and `--username-pattern "username: (.+)"`. |
||||
|
||||
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 |
||||
|
||||
|
||||
def expanded_path(path): |
||||
# Expand potential ~ in paths, since this script won't be called from a shell that does it for us |
||||
expanded = os.path.expanduser(path) |
||||
# Add trailing slash if not present |
||||
return os.path.join(expanded, '') |
||||
|
||||
|
||||
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=expanded_path(os.getenv('PASSWORD_STORE_DIR', default='~/.password-store')), |
||||
help='Path to your pass password-store (only used in pass-mode)', type=expanded_path) |
||||
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('--noinsert-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 |
||||
|
||||
|
||||
class CouldNotMatchUsername(Exception): |
||||
pass |
||||
|
||||
|
||||
class CouldNotMatchPassword(Exception): |
||||
pass |
||||
|
||||
|
||||
def qute_command(command): |
||||
with open(os.environ['QUTE_FIFO'], 'w') as fifo: |
||||
fifo.write(command + '\n') |
||||
fifo.flush() |
||||
|
||||
|
||||
def find_pass_candidates(domain): |
||||
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(arguments.password_store, followlinks=True): |
||||
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(arguments.password_store):] |
||||
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): |
||||
# The executable is conveniently named after it's mode [pass|gopass]. |
||||
pass_command = [arguments.mode] |
||||
env = os.environ.copy() |
||||
env['PASSWORD_STORE_DIR'] = arguments.password_store |
||||
process = subprocess.run(pass_command + pass_arguments, env=env, stdout=subprocess.PIPE) |
||||
return process.stdout.decode(arguments.io_encoding).strip() |
||||
|
||||
|
||||
def pass_(path): |
||||
return _run_pass(['show', path]) |
||||
|
||||
|
||||
def pass_otp(path): |
||||
if arguments.mode == "gopass": |
||||
return _run_pass(['otp', '-o', path]) |
||||
return _run_pass(['otp', path]) |
||||
|
||||
|
||||
def dmenu(items, invocation): |
||||
command = shlex.split(invocation) |
||||
process = subprocess.run(command, input='\n'.join(items).encode(arguments.io_encoding), stdout=subprocess.PIPE) |
||||
return process.stdout.decode(arguments.io_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 extract_password(secret, pattern): |
||||
match = re.match(pattern, secret) |
||||
if not match: |
||||
raise CouldNotMatchPassword("Pattern did not match target") |
||||
try: |
||||
return match.group(1) |
||||
except IndexError: |
||||
raise CouldNotMatchPassword("Pattern did not contain capture group, please use capture group. Example: (.*)") |
||||
|
||||
|
||||
def extract_username(target, pattern): |
||||
match = re.search(pattern, target, re.MULTILINE) |
||||
if not match: |
||||
raise CouldNotMatchUsername("Pattern did not match target") |
||||
try: |
||||
return match.group(1) |
||||
except IndexError: |
||||
raise CouldNotMatchUsername("Pattern did not contain capture group, please use capture group. Example: (.*)") |
||||
|
||||
|
||||
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) |
||||
|
||||
# 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) |
||||
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) |
||||
# 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. |
||||
# Or if using otp-only, it will run pass on its own. |
||||
secret = None |
||||
if not (arguments.username_target == 'path' and arguments.username_only) and not arguments.otp_only: |
||||
secret = pass_(selection) |
||||
username_target = selection if arguments.username_target == 'path' else secret |
||||
try: |
||||
if arguments.username_only: |
||||
fake_key_raw(extract_username(username_target, arguments.username_pattern)) |
||||
elif arguments.password_only: |
||||
fake_key_raw(extract_password(secret, arguments.password_pattern)) |
||||
elif arguments.otp_only: |
||||
otp = pass_otp(selection) |
||||
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(extract_username(username_target, arguments.username_pattern)) |
||||
qute_command('fake-key <Tab>') |
||||
fake_key_raw(extract_password(secret, arguments.password_pattern)) |
||||
except CouldNotMatchPassword as e: |
||||
stderr('Failed to match password, target: secret, error: {}'.format(e)) |
||||
return ExitCodes.COULD_NOT_MATCH_PASSWORD |
||||
except CouldNotMatchUsername as e: |
||||
stderr('Failed to match username, target: {}, error: {}'.format(arguments.username_target, e)) |
||||
return ExitCodes.COULD_NOT_MATCH_USERNAME |
||||
|
||||
if arguments.insert_mode: |
||||
qute_command('mode-enter insert') |
||||
|
||||
return ExitCodes.SUCCESS |
||||
|
||||
|
||||
if __name__ == '__main__': |
||||
arguments = argument_parser.parse_args() |
||||
sys.exit(main(arguments)) |
@ -0,0 +1,126 @@ |
||||
/*Dracula theme based on the Purple official rofi theme*/ |
||||
|
||||
* { |
||||
font: "Fira Code 14"; |
||||
foreground: #f8f8f2; |
||||
background-color: #282a36; |
||||
active-background: #6272a4; |
||||
urgent-background: #ff5555; |
||||
selected-background: @active-background; |
||||
selected-urgent-background: @urgent-background; |
||||
selected-active-background: @active-background; |
||||
separatorcolor: @active-background; |
||||
bordercolor: @active-background; |
||||
} |
||||
|
||||
#window { |
||||
background-color: @background-color; |
||||
border: 1; |
||||
border-radius: 6; |
||||
border-color: @bordercolor; |
||||
padding: 5; |
||||
} |
||||
#mainbox { |
||||
border: 0; |
||||
padding: 0; |
||||
} |
||||
#message { |
||||
border: 1px dash 0px 0px ; |
||||
border-color: @separatorcolor; |
||||
padding: 1px ; |
||||
} |
||||
#textbox { |
||||
text-color: @foreground; |
||||
} |
||||
#listview { |
||||
fixed-height: 0; |
||||
border: 2px dash 0px 0px ; |
||||
border-color: @bordercolor; |
||||
spacing: 2px ; |
||||
scrollbar: false; |
||||
padding: 2px 0px 0px ; |
||||
} |
||||
#element { |
||||
border: 0; |
||||
padding: 1px ; |
||||
} |
||||
#element.normal.normal { |
||||
background-color: @background-color; |
||||
text-color: @foreground; |
||||
} |
||||
#element.normal.urgent { |
||||
background-color: @urgent-background; |
||||
text-color: @urgent-foreground; |
||||
} |
||||
#element.normal.active { |
||||
background-color: @active-background; |
||||
text-color: @foreground; |
||||
} |
||||
#element.selected.normal { |
||||
background-color: @selected-background; |
||||
text-color: @foreground; |
||||
} |
||||
#element.selected.urgent { |
||||
background-color: @selected-urgent-background; |
||||
text-color: @foreground; |
||||
} |
||||
#element.selected.active { |
||||
background-color: @selected-active-background; |
||||
text-color: @foreground; |
||||
} |
||||
#element.alternate.normal { |
||||
background-color: @background-color; |
||||
text-color: @foreground; |
||||
} |
||||
#element.alternate.urgent { |
||||
background-color: @urgent-background; |
||||
text-color: @foreground; |
||||
} |
||||
#element.alternate.active { |
||||
background-color: @active-background; |
||||
text-color: @foreground; |
||||
} |
||||
#scrollbar { |
||||
width: 2px ; |
||||
border: 0; |
||||
handle-width: 8px ; |
||||
padding: 0; |
||||
} |
||||
#sidebar { |
||||
border: 2px dash 0px 0px ; |
||||
border-color: @separatorcolor; |
||||
} |
||||
#button.selected { |
||||
background-color: @selected-background; |
||||
text-color: @foreground; |
||||
} |
||||
#inputbar { |
||||
spacing: 0; |
||||
text-color: @foreground; |
||||
padding: 1px ; |
||||
} |
||||
#case-indicator { |
||||
spacing: 0; |
||||
text-color: @foreground; |
||||
} |
||||
#entry { |
||||
spacing: 0; |
||||
text-color: @foreground; |
||||
} |
||||
#prompt { |
||||
spacing: 0; |
||||
text-color: @foreground; |
||||
} |
||||
#inputbar { |
||||
children: [ prompt,textbox-prompt-colon,entry,case-indicator ]; |
||||
} |
||||
#textbox-prompt-colon { |
||||
expand: false; |
||||
str: ":"; |
||||
margin: 0px 0.3em 0em 0em ; |
||||
text-color: @foreground; |
||||
} |
||||
element-text, element-icon { |
||||
background-color: inherit; |
||||
text-color: inherit; |
||||
} |
@ -0,0 +1,27 @@ |
||||
rofi.modi: window,run,ssh |
||||
rofi.width: 50 |
||||
rofi.lines: 15 |
||||
rofi.columns: 1 |
||||
rofi.font: roboto mono 10 |
||||
rofi.color-normal: #0e4470, #9dd9df, #0e4470, #016694, #9dd9df |
||||
rofi.color-urgent: #0e4470, #d6b48d, #22231D, #d6b48d, #9dd9df |
||||
rofi.color-active: #0e4470, #2db1b9, #0e4470, #2db1b9, #0e4470 |
||||
rofi.color-window: #0e4470, #016694, #014665 |
||||
rofi.bw: 5 |
||||
rofi.location: 0 |
||||
rofi.padding: 5 |
||||
rofi.yoffset: 0 |
||||
rofi.xoffset: 0 |
||||
rofi.fixed-num-lines: true |
||||
rofi.terminal: rofi-sensible-terminal |
||||
rofi.ssh-client: ssh |
||||
rofi.ssh-command: {terminal} -e {ssh-client} {host} |
||||
rofi.run-command: {cmd} |
||||
rofi.parse-hosts: true |
||||
rofi.matching: normal |
||||
rofi.separator-style: none |
||||
rofi.scrollbar-width: 0 |
||||
rofi.kb-mode-next: Shift+Right,Control+Tab,Alt+l |
||||
rofi.kb-mode-previous: Shift+Left,Control+Shift+Tab,Alt+h |
||||
rofi.kb-row-up: Up,Control+p,Shift+Tab,Shift+ISO_Left_Tab,Alt+k |
||||
rofi.kb-row-down: Down,Control+n,Alt+j |
Loading…
Reference in new issue