Add cache, command to delete all passwords, re-use sessions, add proxy
This commit is contained in:
parent
6cb7783bed
commit
a9f44fd140
1 changed files with 112 additions and 28 deletions
|
@ -13,6 +13,7 @@ import traceback
|
||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
|
import time
|
||||||
import logging
|
import logging
|
||||||
from logging.handlers import SysLogHandler
|
from logging.handlers import SysLogHandler
|
||||||
from xml.dom import minidom
|
from xml.dom import minidom
|
||||||
|
@ -87,6 +88,15 @@ class NextcloudHandler:
|
||||||
self.http = 'https'
|
self.http = 'https'
|
||||||
self.timeout = params.get('timeout', 3)
|
self.timeout = params.get('timeout', 3)
|
||||||
self.ssl = True
|
self.ssl = True
|
||||||
|
self.cached_passwords = []
|
||||||
|
self.last_cache = -1
|
||||||
|
self.session = requests.Session()
|
||||||
|
proxies = {
|
||||||
|
'http': params.get('https_proxy', ''),
|
||||||
|
'https': params.get('https_proxy', ''),
|
||||||
|
}
|
||||||
|
self.session.proxies.update(proxies)
|
||||||
|
self.cache_duration = params.get('cache_duration', 300)
|
||||||
if params.get('ssl_mode') == 'http':
|
if params.get('ssl_mode') == 'http':
|
||||||
self.http = 'http'
|
self.http = 'http'
|
||||||
elif params.get('ssl_mode') == 'skip':
|
elif params.get('ssl_mode') == 'skip':
|
||||||
|
@ -187,9 +197,16 @@ class NextcloudHandler:
|
||||||
|
|
||||||
def error(self, obj):
|
def error(self, obj):
|
||||||
'''Show error information'''
|
'''Show error information'''
|
||||||
|
try:
|
||||||
self._log.error(
|
self._log.error(
|
||||||
json.dumps(obj, indent=2)
|
json.dumps(obj, indent=2)
|
||||||
)
|
)
|
||||||
|
except TypeError as error:
|
||||||
|
self._log.error(
|
||||||
|
'Additional error showing the error from %s. %s',
|
||||||
|
obj,
|
||||||
|
error
|
||||||
|
)
|
||||||
|
|
||||||
def _init_log(self, params):
|
def _init_log(self, params):
|
||||||
''' Initialize log object '''
|
''' Initialize log object '''
|
||||||
|
@ -242,9 +259,9 @@ class NextcloudHandler:
|
||||||
|
|
||||||
def get(self, path):
|
def get(self, path):
|
||||||
'''Do a GET request'''
|
'''Do a GET request'''
|
||||||
self.debug({ "action": "get", "message": "Requesting {path}" })
|
self.debug({ "action": "get", "message": f"Requesting {path}" })
|
||||||
try:
|
try:
|
||||||
r = requests.get(
|
r = self.session.get(
|
||||||
f'{self.http}://{self.host}/{path}',
|
f'{self.http}://{self.host}/{path}',
|
||||||
auth=(self.user, self.token), verify=self.ssl, headers=self.headers(),
|
auth=(self.user, self.token), verify=self.ssl, headers=self.headers(),
|
||||||
timeout=self.timeout
|
timeout=self.timeout
|
||||||
|
@ -277,15 +294,15 @@ class NextcloudHandler:
|
||||||
{
|
{
|
||||||
"action": "get",
|
"action": "get",
|
||||||
"message": f"Timeout ({self.timeout} sec) error doing GET request. %s",
|
"message": f"Timeout ({self.timeout} sec) error doing GET request. %s",
|
||||||
"error": error
|
"error": f"{error}"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
sys.exit(5)
|
# sys.exit(5)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def propfind(self, path):
|
def propfind(self, path):
|
||||||
'''Do a PROPFIND request'''
|
'''Do a PROPFIND request'''
|
||||||
s = requests.Session()
|
s = self.session
|
||||||
s.auth = (self.user, self.token)
|
s.auth = (self.user, self.token)
|
||||||
try:
|
try:
|
||||||
r = s.request(
|
r = s.request(
|
||||||
|
@ -369,7 +386,7 @@ class NextcloudHandler:
|
||||||
'''Do a PUT request'''
|
'''Do a PUT request'''
|
||||||
try:
|
try:
|
||||||
if src:
|
if src:
|
||||||
r = requests.put(
|
r = self.session.put(
|
||||||
f'{self.http}://{self.host}/{path}',
|
f'{self.http}://{self.host}/{path}',
|
||||||
data=open(src, 'rb'),
|
data=open(src, 'rb'),
|
||||||
auth=(self.user, self.token),
|
auth=(self.user, self.token),
|
||||||
|
@ -377,7 +394,7 @@ class NextcloudHandler:
|
||||||
timeout=self.timeout
|
timeout=self.timeout
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
r = requests.put(
|
r = self.session.put(
|
||||||
f'{self.http}://{self.host}/{path}',
|
f'{self.http}://{self.host}/{path}',
|
||||||
headers=self.headers,
|
headers=self.headers,
|
||||||
auth=(self.user, self.token),
|
auth=(self.user, self.token),
|
||||||
|
@ -405,7 +422,7 @@ class NextcloudHandler:
|
||||||
def delete(self, path):
|
def delete(self, path):
|
||||||
'''Do a DELETE request'''
|
'''Do a DELETE request'''
|
||||||
try:
|
try:
|
||||||
r = requests.delete(
|
r = self.session.delete(
|
||||||
f'{self.http}://{self.host}/{path}',
|
f'{self.http}://{self.host}/{path}',
|
||||||
auth=(self.user, self.token), verify=self.ssl, timeout=self.timeout
|
auth=(self.user, self.token), verify=self.ssl, timeout=self.timeout
|
||||||
)
|
)
|
||||||
|
@ -440,7 +457,7 @@ class NextcloudHandler:
|
||||||
|
|
||||||
spreed_v1_path = "ocs/v2.php/apps/spreed/api/v1/chat"
|
spreed_v1_path = "ocs/v2.php/apps/spreed/api/v1/chat"
|
||||||
try:
|
try:
|
||||||
r = requests.post(
|
r = self.session.post(
|
||||||
f'{self.http}://{self.host}/{spreed_v1_path}/{channel}',
|
f'{self.http}://{self.host}/{spreed_v1_path}/{channel}',
|
||||||
data=body,
|
data=body,
|
||||||
headers=self.headers(),
|
headers=self.headers(),
|
||||||
|
@ -478,7 +495,7 @@ class NextcloudHandler:
|
||||||
'challenge': password_hash
|
'challenge': password_hash
|
||||||
}
|
}
|
||||||
|
|
||||||
r = requests.post(
|
r = self.session.post(
|
||||||
f'{self.http}://{self.host}/index.php/apps/passwords/api/1.0/session/open',
|
f'{self.http}://{self.host}/index.php/apps/passwords/api/1.0/session/open',
|
||||||
data=post_obj,
|
data=post_obj,
|
||||||
headers=self.headers(),
|
headers=self.headers(),
|
||||||
|
@ -503,7 +520,18 @@ class NextcloudHandler:
|
||||||
|
|
||||||
def list_passwords(self):
|
def list_passwords(self):
|
||||||
'''List all passwords'''
|
'''List all passwords'''
|
||||||
return self.get("index.php/apps/passwords/api/1.0/password/list")
|
if self.last_cache < (time.time() - self.cache_duration):
|
||||||
|
self.debug(
|
||||||
|
{ "action": "list_passwords", "message": "Updating cached list of password" }
|
||||||
|
)
|
||||||
|
self.cached_passwords = self.get("index.php/apps/passwords/api/1.0/password/list")
|
||||||
|
if self.cached_passwords:
|
||||||
|
self.last_cache = time.time()
|
||||||
|
else:
|
||||||
|
self.debug(
|
||||||
|
{ "action": "list_passwords", "message": "Reusing cached list of password" }
|
||||||
|
)
|
||||||
|
return self.cached_passwords
|
||||||
|
|
||||||
def list_passwords_folders(self):
|
def list_passwords_folders(self):
|
||||||
'''List passwords folders'''
|
'''List passwords folders'''
|
||||||
|
@ -525,7 +553,7 @@ class NextcloudHandler:
|
||||||
post_obj = {
|
post_obj = {
|
||||||
'id': folder_id
|
'id': folder_id
|
||||||
}
|
}
|
||||||
r = requests.delete(
|
r = self.session.delete(
|
||||||
f'{self.http}://{self.host}/index.php/apps/passwords/api/1.0/folder/delete',
|
f'{self.http}://{self.host}/index.php/apps/passwords/api/1.0/folder/delete',
|
||||||
data=post_obj,
|
data=post_obj,
|
||||||
headers=self.headers(),
|
headers=self.headers(),
|
||||||
|
@ -566,7 +594,7 @@ class NextcloudHandler:
|
||||||
'label': name
|
'label': name
|
||||||
}
|
}
|
||||||
|
|
||||||
r = requests.post(
|
r = self.session.post(
|
||||||
f'{self.http}://{self.host}/index.php/apps/passwords/api/1.0/folder/create',
|
f'{self.http}://{self.host}/index.php/apps/passwords/api/1.0/folder/create',
|
||||||
data=post_obj,
|
data=post_obj,
|
||||||
headers=self.headers(),
|
headers=self.headers(),
|
||||||
|
@ -637,7 +665,9 @@ class NextcloudHandler:
|
||||||
self.debug(
|
self.debug(
|
||||||
{ "action": "check_exists_password", "object": obj }
|
{ "action": "check_exists_password", "object": obj }
|
||||||
)
|
)
|
||||||
for password in self.list_passwords():
|
all_passwords = self.list_passwords()
|
||||||
|
if all_passwords:
|
||||||
|
for password in all_passwords:
|
||||||
if self.is_same_password(obj, password):
|
if self.is_same_password(obj, password):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
@ -659,7 +689,7 @@ class NextcloudHandler:
|
||||||
self.debug(
|
self.debug(
|
||||||
{ "action": "create_password", "object": safer_obj }
|
{ "action": "create_password", "object": safer_obj }
|
||||||
)
|
)
|
||||||
r = requests.post(
|
r = self.session.post(
|
||||||
f'{self.http}://{self.host}/index.php/apps/passwords/api/1.0/password/create',
|
f'{self.http}://{self.host}/index.php/apps/passwords/api/1.0/password/create',
|
||||||
data=post_obj,
|
data=post_obj,
|
||||||
headers=self.headers(),
|
headers=self.headers(),
|
||||||
|
@ -668,6 +698,10 @@ class NextcloudHandler:
|
||||||
)
|
)
|
||||||
|
|
||||||
if r.status_code == 201:
|
if r.status_code == 201:
|
||||||
|
if self.cached_passwords:
|
||||||
|
self.cached_passwords.append(post_obj)
|
||||||
|
else:
|
||||||
|
self.cached_passwords = [ post_obj ]
|
||||||
return r.json()
|
return r.json()
|
||||||
self.error(r.json())
|
self.error(r.json())
|
||||||
self.error(
|
self.error(
|
||||||
|
@ -699,7 +733,7 @@ class NextcloudHandler:
|
||||||
def delete_password(self, post_obj):
|
def delete_password(self, post_obj):
|
||||||
'''Delete a password'''
|
'''Delete a password'''
|
||||||
try:
|
try:
|
||||||
r = requests.delete(
|
r = self.session.delete(
|
||||||
f'{self.http}://{self.host}/index.php/apps/passwords/api/1.0/password/delete',
|
f'{self.http}://{self.host}/index.php/apps/passwords/api/1.0/password/delete',
|
||||||
data=post_obj,
|
data=post_obj,
|
||||||
headers=self.headers(),
|
headers=self.headers(),
|
||||||
|
@ -728,7 +762,7 @@ class NextcloudHandler:
|
||||||
def update_password(self, post_obj):
|
def update_password(self, post_obj):
|
||||||
'''Update a password'''
|
'''Update a password'''
|
||||||
try:
|
try:
|
||||||
r = requests.patch(
|
r = self.session.patch(
|
||||||
f'{self.http}://{self.host}/index.php/apps/passwords/api/1.0/password/update',
|
f'{self.http}://{self.host}/index.php/apps/passwords/api/1.0/password/update',
|
||||||
data=post_obj,
|
data=post_obj,
|
||||||
headers=self.headers(),
|
headers=self.headers(),
|
||||||
|
@ -795,7 +829,10 @@ class NextcloudHandler:
|
||||||
class NcPasswordClient:
|
class NcPasswordClient:
|
||||||
'''Nextcloud Password Client'''
|
'''Nextcloud Password Client'''
|
||||||
|
|
||||||
def __init__(self, debug_level, log_file, host, user, api_token, cse_password, timeout):
|
def __init__(
|
||||||
|
self, debug_level, log_file, host, user, api_token, cse_password,
|
||||||
|
timeout, cache_duration, https_proxy
|
||||||
|
):
|
||||||
self.config = {}
|
self.config = {}
|
||||||
self.config['debug_level'] = debug_level
|
self.config['debug_level'] = debug_level
|
||||||
if log_file is None:
|
if log_file is None:
|
||||||
|
@ -817,6 +854,8 @@ class NcPasswordClient:
|
||||||
"api_token": api_token,
|
"api_token": api_token,
|
||||||
"cse_password": cse_password,
|
"cse_password": cse_password,
|
||||||
"timeout": timeout,
|
"timeout": timeout,
|
||||||
|
"cache_duration": cache_duration,
|
||||||
|
"https_proxy": https_proxy,
|
||||||
}
|
}
|
||||||
self.nc = NextcloudHandler(params)
|
self.nc = NextcloudHandler(params)
|
||||||
|
|
||||||
|
@ -922,11 +961,30 @@ class NcPasswordClient:
|
||||||
if field == 'login':
|
if field == 'login':
|
||||||
field = 'username'
|
field = 'username'
|
||||||
obj[field] = value
|
obj[field] = value
|
||||||
|
new_password = self.nc.create_password(obj)
|
||||||
|
if new_password:
|
||||||
self.debug(
|
self.debug(
|
||||||
{ "action": "created_password", "object": self.nc.create_password(obj) }
|
{ "action": "created_password", "object": new_password }
|
||||||
)
|
)
|
||||||
count += 1
|
count += 1
|
||||||
|
|
||||||
|
def delete_all_passwords(self, yes_i_am_sure):
|
||||||
|
'''DANGEROUS! Delete ALL passwords from your Nextcloud Password instance'''
|
||||||
|
if not yes_i_am_sure:
|
||||||
|
answer = input(
|
||||||
|
'WARNING! Are you completely sure you want to delete ALL your passwords? (Y/n)'
|
||||||
|
)
|
||||||
|
if answer != "Y":
|
||||||
|
self.info(
|
||||||
|
{ "action": "delete_all_passwords", "message": "Aborting after answering something different from 'Y'" }
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
for item in self.nc.list_passwords():
|
||||||
|
self.debug(
|
||||||
|
{ "action": "delete_all_passwords", "message": "Deleting password", "object": item }
|
||||||
|
)
|
||||||
|
self.nc.delete_password(item)
|
||||||
|
|
||||||
def _init_log(self):
|
def _init_log(self):
|
||||||
''' Initialize log object '''
|
''' Initialize log object '''
|
||||||
self._log = logging.getLogger("nc_password_client")
|
self._log = logging.getLogger("nc_password_client")
|
||||||
|
@ -1006,13 +1064,27 @@ class NcPasswordClient:
|
||||||
type=click.INT,
|
type=click.INT,
|
||||||
help='Nextcloud user\'s end-to-end encryption password'
|
help='Nextcloud user\'s end-to-end encryption password'
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
'--cache-duration', '-c',
|
||||||
|
default=300,
|
||||||
|
type=click.INT,
|
||||||
|
help='Number of seconds to hold the list of passwords'
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
'--https-proxy', '-P',
|
||||||
|
help='HTTPS proxy to use to connect to the Nextcloud instance'
|
||||||
|
)
|
||||||
@click_config_file.configuration_option()
|
@click_config_file.configuration_option()
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def cli(ctx, debug_level, log_file, host, user, api_token, cse_password, timeout):
|
def cli(
|
||||||
|
ctx, debug_level, log_file, host, user, api_token, cse_password,
|
||||||
|
timeout, cache_duration, https_proxy
|
||||||
|
):
|
||||||
'''Client function to pass context'''
|
'''Client function to pass context'''
|
||||||
ctx.ensure_object(dict)
|
ctx.ensure_object(dict)
|
||||||
ctx.obj['NcPasswordClient'] = NcPasswordClient(
|
ctx.obj['NcPasswordClient'] = NcPasswordClient(
|
||||||
debug_level, log_file, host, user, api_token, cse_password, timeout
|
debug_level, log_file, host, user, api_token, cse_password,
|
||||||
|
timeout, cache_duration, https_proxy
|
||||||
)
|
)
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
|
@ -1087,5 +1159,17 @@ def migrate_pass(ctx, limit):
|
||||||
'''Migrate password store passwords to Nextcloud Passwords'''
|
'''Migrate password store passwords to Nextcloud Passwords'''
|
||||||
ctx.obj['NcPasswordClient'].migrate_pass(limit)
|
ctx.obj['NcPasswordClient'].migrate_pass(limit)
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.option(
|
||||||
|
'--yes-I-am-sure',
|
||||||
|
default=False,
|
||||||
|
help="DAGEROUS! Don't prompt for confirmation before deleting all passwords."
|
||||||
|
)
|
||||||
|
@click_config_file.configuration_option()
|
||||||
|
@click.pass_context
|
||||||
|
def delete_all_passwords(ctx, yes_i_am_sure):
|
||||||
|
'''Delete all passwords'''
|
||||||
|
ctx.obj['NcPasswordClient'].delete_all_passwords(yes_i_am_sure)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
cli(obj={})
|
cli(obj={})
|
||||||
|
|
Loading…
Reference in a new issue