Add cache, command to delete all passwords, re-use sessions, add proxy

This commit is contained in:
Antonio J. Delgado 2024-11-11 22:52:43 +02:00
parent 6cb7783bed
commit a9f44fd140

View file

@ -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={})