6
0
mirror of https://github.com/FirebirdSQL/firebird-qa.git synced 2025-01-22 13:33:07 +01:00

Added/Updated tests\bugs\gh_5995_test.py: Checked on 6.0.0.366, 5.0.1.1411, 4.0.5.3103

This commit is contained in:
pavel-zotov 2024-06-03 09:50:00 +03:00
parent 14035a7304
commit f7afa0199e

View File

@ -6,149 +6,199 @@ ISSUE: 5995
TITLE: Connection to server may hang when working with encrypted databases over non-TCP protocol
DESCRIPTION:
Test implemented only to be run on Windows.
It assumes that there are files keyholder.dll and keyholder.conf in the %FIREBIRD_HOME%\\plugins dir.
These files were provided by IBSurgeon and added during fbt_run prepare phase by batch scenario (qa_rundaily).
File keyholder.conf initially contains several keys.
Folder %FIREBIRD_HOME%/plugins/ must have files fbSampleKeyHolder.conf and fbSampleKeyHolder.dll which should be
copied there from %FIREBIRD_HOME%/examples/prebuilt/plugins/.
NB! These files ABSENT in FB 3.x but they can be taken from FB 4.x snapshot.
File fbSampleKeyHolder.conf must have following lines:
Auto = true
KeyRed=111
If we make this file EMPTY then usage of XNET and WNET protocols became improssible before this ticket was fixed.
If we encrypt database and then make file fbSampleKeyHolder.conf EMPTY then usage of XNET and WNET protocols became
impossible before this ticket was fixed.
Great thanks to Alex for suggestions.
Confirmed bug on 3.0.1.32609: ISQL hangs on attempt to connect to database when file plugins\\keyholder.conf is empty.
In order to properly finish test, we have to kill hanging ISQL and change DB state to full shutdown (with subsequent
returning it to online) - fortunately, connection using TCP remains avaliable in this case.
JIRA: CORE-5730
FBTEST: bugs.gh_5995
NOTES:
[03.06.2024] pzotov
Confirmed bug on 3.0.1.32609, 4.0.0.853: ISQL hangs on attempt to connect to database when file plugins/keyholder.conf is empty.
Checked on 6.0.0.366, 5.0.1.1411, 4.0.5.3103 (all of them were checked for ServerMode = SS and CS).
ATTENTION: 3.x raises different SQLSTATE and error text, depending on ServerMode!
For 3.x value of SQLSTATE and error text depends on Servermode.
On Classic FB 3.x output will be almost like in FB 4.x+:
Statement failed, SQLSTATE = 08004
Missing correct crypt key
-Plugin fbSampleDbCrypt:
-Crypt key Red not set
-IProvider::attachDatabase failed when loading mapping cache
On Super FB 3.x output is:
Statement failed, SQLSTATE = HY000
Missing correct crypt key
-Plugin fbSampleDbCrypt:
-Crypt key Red not set
Because of that, it was decided not to check FB 3.x as this version soon will be considered as obsolete.
"""
import shutil
import locale
import re
import time
import platform
import subprocess
import datetime as py_dt
from pathlib import Path
import pytest
from firebird.qa import *
from firebird.driver import DatabaseError, DbInfoCode, NetProtocol
import time
###########################
### S E T T I N G S ###
###########################
# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings
# from act.files_dir/'test_config.ini':
enc_settings = QA_GLOBALS['encryption']
# ACHTUNG: this must be carefully tuned on every new host:
#
MAX_WAITING_ENCR_FINISH = int(enc_settings['MAX_WAIT_FOR_ENCR_FINISH_WIN'])
assert MAX_WAITING_ENCR_FINISH > 0
ENCRYPTION_PLUGIN = enc_settings['encryption_plugin'] # fbSampleDbCrypt
ENCRYPTION_KEY = enc_settings['encryption_key'] # Red
db = db_factory()
act = python_act('db', substitutions = [('After line \\d+.*', ''),('[ \t]+', ' ')])
act = python_act('db')
kholder_cfg_bak = temp_file('fbSampleKeyHolder.bak')
tmp_sql = temp_file('tmp_5995.sql')
tmp_log = temp_file('tmp_5995.log')
expected_stdout = """
MON$REMOTE_PROTOCOL WNET
MON$REMOTE_PROTOCOL XNET
"""
#-----------------------------------------------------------------------
@pytest.mark.skip('FIXME: Not IMPLEMENTED')
@pytest.mark.version('>=3.0.4')
def run_encr_decr(act: Action, mode, max_wait_encr_thread_finish, capsys):
assert mode in ('encrypt', 'decrypt')
if mode == 'encrypt':
alter_db_sttm = f'alter database encrypt with "{ENCRYPTION_PLUGIN}" key "{ENCRYPTION_KEY}"'
wait_for_state = 'Database encrypted'
elif mode == 'decrypt':
alter_db_sttm = 'alter database decrypt'
wait_for_state = 'Database not encrypted'
e_thread_finished = False
d1 = py_dt.timedelta(0)
with act.db.connect() as con:
#cur = con.cursor()
#ps = cur.prepare('select mon$crypt_state from mon$database')
t1=py_dt.datetime.now()
try:
d1 = t1-t1
con.execute_immediate(alter_db_sttm)
con.commit()
# Pattern to check for completed encryption thread:
completed_encr_pattern = re.compile(f'Attributes\\s+encrypted,\\s+plugin\\s+{ENCRYPTION_PLUGIN}', re.IGNORECASE)
while not e_thread_finished:
t2=py_dt.datetime.now()
d1=t2-t1
if d1.seconds*1000 + d1.microseconds//1000 > max_wait_encr_thread_finish:
break
#############################################
### C H E C K G S T A T A T T R. ###
#############################################
# Invoke 'gstat -h' and read its ouput.
# Encryption can be considered as COMPLETED when we will found:
# "Attributes encrypted, plugin fbSampleDbCrypt"
#
act.gstat(switches=['-h'])
for line in act.stdout.splitlines():
if mode == 'encrypt' and completed_encr_pattern.match(line.strip()):
e_thread_finished = True
if mode == 'decrypt' and 'Attributes' in line and not completed_encr_pattern.search(line.strip()):
e_thread_finished = True
if e_thread_finished:
break
time.sleep(0.5)
except DatabaseError as e:
print( e.__str__() )
assert e_thread_finished, f'TIMEOUT EXPIRATION: {mode=} took {d1.seconds*1000 + d1.microseconds//1000} ms which greater than {max_wait_encr_thread_finish=} ms'
#-----------------------------------------------------------------------
@pytest.mark.encryption
@pytest.mark.version('>=4.0')
@pytest.mark.platform('Windows')
def test_1(act: Action):
pytest.fail("Not IMPLEMENTED")
def test_1(act: Action, kholder_cfg_bak: Path, tmp_sql: Path, tmp_log: Path, capsys):
kholder_cfg_file = act.vars['home-dir'] / 'plugins' / 'fbSampleKeyHolder.conf'
shutil.copy2(kholder_cfg_file, kholder_cfg_bak)
finish_encryption = False
# test_script_1
#---
#
# import os
# import subprocess
# from subprocess import Popen
# import datetime
# import time
# import shutil
# import re
# import fdb
#
# os.environ["ISC_USER"] = user_name
# os.environ["ISC_PASSWORD"] = user_password
# engine = db_conn.engine_version
# db_name = db_conn.database_name
# db_conn.close()
#
# svc = fdb.services.connect(host='localhost', user=user_name, password=user_password)
# FB_HOME = svc.get_home_directory()
# svc.close()
#
# #--------------------------------------------
#
# def flush_and_close( file_handle ):
# # https://docs.python.org/2/library/os.html#os.fsync
# # If you're starting with a Python file object f,
# # first do f.flush(), and
# # then do os.fsync(f.fileno()), to ensure that all internal buffers associated with f are written to disk.
# global os
#
# file_handle.flush()
# if file_handle.mode not in ('r', 'rb') and file_handle.name != os.devnull:
# # otherwise: "OSError: [Errno 9] Bad file descriptor"!
# os.fsync(file_handle.fileno())
# file_handle.close()
#
# #--------------------------------------------
#
# def cleanup( f_names_list ):
# global os
# for i in range(len( f_names_list )):
# if type(f_names_list[i]) == file:
# del_name = f_names_list[i].name
# elif type(f_names_list[i]) == str:
# del_name = f_names_list[i]
# else:
# print('Unrecognized type of element:', f_names_list[i], ' - can not be treated as file.')
# print('type(f_names_list[i])=',type(f_names_list[i]))
# del_name = None
#
# if del_name and os.path.isfile( del_name ):
# os.remove( del_name )
#
# #--------------------------------------------
#
#
# dts = datetime.datetime.now().strftime("%y%m%d_%H%M%S")
#
# kholder_cur = os.path.join( FB_HOME, 'plugins', 'keyholder.conf')
# kholder_bak = os.path.join( context['temp_directory'], 'keyholder'+dts+'.bak')
#
# shutil.copy2( kholder_cur, kholder_bak)
#
# # Make file %FB_HOME%\\plugins\\keyholder.conf empty:
# with open(kholder_cur,'w') as f:
# pass
#
# MAX_SECONDS_TO_WAIT = 3
#
# # Trying to establish connection to database using WNET and XNET protocols.
# # Async. launch of ISQL with check that it will finished within some reasonable time (and w/o errors).
# # If it will hang - kill (this is bug dexcribed in the ticket)
# for p in ('wnet', 'xnet'):
# f_isql_sql=open(os.path.join(context['temp_directory'],'tmp_gh_5995.'+p+'.sql'),'w')
# f_isql_sql.write('set list on; select mon$remote_protocol from mon$attachments where mon$attachment_id = current_connection;')
# flush_and_close( f_isql_sql )
#
# protocol_conn_string = ''.join( (p, '://', db_name) )
# f_isql_log=open( os.path.join(context['temp_directory'],'tmp_gh_5995.'+p+'.log'), 'w')
# p_isql = Popen([ context['isql_path'], protocol_conn_string, "-i", f_isql_sql.name], stdout=f_isql_log, stderr=subprocess.STDOUT )
#
# time.sleep(0.2)
# for i in range(0,MAX_SECONDS_TO_WAIT):
# # Check if child process has terminated. Set and return returncode attribute. Otherwise, returns None.
# p_isql.poll()
# if p_isql.returncode is None:
# # A None value indicates that the process has not terminated yet.
# time.sleep(1)
# if i < MAX_SECONDS_TO_WAIT-1:
# continue
# else:
# f_isql_log.write( '\\nISQL process %d hangs for %d seconds and is forcedly killed.' % (p_isql.pid, MAX_SECONDS_TO_WAIT) )
# p_isql.terminate()
#
# flush_and_close(f_isql_log)
#
# with open(f_isql_log.name,'r') as f:
# for line in f:
# if line:
# print(line)
#
# cleanup((f_isql_sql,f_isql_log))
#
# shutil.move( kholder_bak, kholder_cur)
#
# # ::: NOTE ::: We have to change DB state to full shutdown and bring it back online
# # in order to prevent "Object in use" while fbtest will try to drop this DB
# #####################################
# runProgram('gfix',[dsn,'-shut','full','-force','0'])
# runProgram('gfix',[dsn,'-online'])
#
#
#---
protocols_list = [ NetProtocol.INET, NetProtocol.XNET, ]
if act.is_version('<5'):
protocols_list.append(NetProtocol.WNET)
expected_output = actual_output = test_sql = ''
try:
run_encr_decr(act, 'encrypt', MAX_WAITING_ENCR_FINISH, capsys)
finish_encryption = True
with open(kholder_cfg_file,'w') as f:
pass
for protocol_name in protocols_list:
conn_str = f"connect {protocol_name.name.lower()}://{act.db.db_path} user {act.db.user} password '{act.db.password}'"
test_sql = f"""
set list on;
set bail on;
set echo on;
{conn_str};
select mon$remote_protocol from mon$attachments where mon$attachment_id = current_connection;
"""
tmp_sql.write_text(test_sql)
with open(tmp_log, 'w') as f_log:
# ISQL-4.x must issue:
# Statement failed, SQLSTATE = 08004
# Missing database encryption key for your attachment
# -Plugin fbSampleDbCrypt:
# -Crypt key Red not set
# Before fix, ISQL hanged on CONNECT, thus we have to use timeout here!
#
p = subprocess.run( [ act.vars['isql'],
'-q',
'-i', str(tmp_sql)
],
stdout = f_log, stderr = subprocess.STDOUT,
timeout = 3
)
actual_output += tmp_log.read_text()
expected_output += f"""
{conn_str};
Statement failed, SQLSTATE = 08004
Missing database encryption key for your attachment
-Plugin {ENCRYPTION_PLUGIN}:
-Crypt key {ENCRYPTION_KEY} not set
"""
except Exception as e:
actual_output += test_sql + '\n' + e.__str__()
finally:
shutil.copy2(kholder_cfg_bak, kholder_cfg_file)
if finish_encryption:
run_encr_decr(act, 'decrypt', MAX_WAITING_ENCR_FINISH, capsys)
act.expected_stdout = expected_output
act.stdout = actual_output # capsys.readouterr().out
assert act.clean_stdout == act.clean_expected_stdout