Skip to content

Commit

Permalink
[app][feat] whitelist clients
Browse files Browse the repository at this point in the history
  • Loading branch information
M3ssman committed Oct 1, 2024
1 parent 1b8435b commit a325900
Show file tree
Hide file tree
Showing 2 changed files with 76 additions and 34 deletions.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "digiflow"
version = "5.3.6"
version = "5.4.6"
description = "Father's Little Digitization Workflow Helper"
readme = "README.md"
requires-python = ">=3.8"
Expand Down
108 changes: 75 additions & 33 deletions src/digiflow/record/record_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,16 @@
import digiflow.record as df_r


_MIME_TXT = "text/plain"
_MIME_JSON = "application/json"
DEFAULT_HEADER = {
"Content-Type": _MIME_JSON
}
TEXT_HEADER = {
"Content-Type": _MIME_TXT
}
DEFAULT_COMMAND_NEXT = 'next'
DEFAULT_COMMAND_UPDATE = 'update'
_MIME_TXT = 'text/plain'
DEFAULT_MARK_BUSY = 'busy'

X_HEADER_GET_STATE = 'X-GET-STATE'
Expand All @@ -37,6 +44,7 @@ class HandlerInformation:

data_path: Path
logger: logging.Logger
client_ips = []

def __init__(self, data_path, logger):
"""Enforce proper types"""
Expand All @@ -57,6 +65,8 @@ class RecordsServiceException(df_rc.RecordDataException):
"""Mark generic exception state"""


# make pylint accept names like "do_POST"
# pylint:disable=invalid-name
class RecordRequestHandler(http.server.SimpleHTTPRequestHandler,
df.FallbackLogger):
"""Simple handler for POST and GET requests
Expand All @@ -67,6 +77,7 @@ def __init__(self, start_info: HandlerInformation,
*args,
**kwargs):
self.record_list_directory: Path = start_info.data_path
self.client_ips = start_info.client_ips
self.command_next = DEFAULT_COMMAND_NEXT
self.command_update = DEFAULT_COMMAND_UPDATE
self._logger = start_info.logger
Expand All @@ -76,60 +87,86 @@ def _parse_request_path(self):
try:
_, file_name, command = self.path.split('/')
return command, file_name
except ValueError:
self._set_headers(state=400, mime_type=_MIME_TXT)
except (ValueError, TypeError):
self._respond(state=400, headers=TEXT_HEADER)
self.wfile.write(
b'provide file name and command, e.g.: /<file_name>/<command>')
self.log("unable to parse '%s'", self.path, level=logging.ERROR)
return None

def _client_allowed(self):
"""check incomming requests verso list of allowed
client ips if available"""
if self.client_ips is not None and len(self.client_ips) > 0:
client_name = self.address_string()
is_allowed = client_name in self.client_ips
if not is_allowed:
self._respond(state=404, headers={})
# self.wfile.write()
return False
return True

def do_GET(self):
"""handle GET request"""
client_name = self.address_string()
if not self._client_allowed():
self.log("request from %s rejected", self.address_string(), level=logging.WARNING)
return
get_record_state = self.headers.get(X_HEADER_GET_STATE)
set_record_state = self.headers.get(X_HEADER_SET_STATE)
command, file_name = self._parse_request_path()
if command is not None and command == DEFAULT_COMMAND_NEXT:
state, data = self.get_next_record(file_name, client_name,
parsed_request = self._parse_request_path()
if isinstance(parsed_request, tuple):
command, file_name = parsed_request
if command is not None and command == DEFAULT_COMMAND_NEXT:
state, data = self.get_next_record(file_name, client_name,
get_record_state, set_record_state)
if isinstance(data, str):
self._set_headers(state, _MIME_TXT)
self.wfile.write(data.encode('utf-8'))
else:
self._set_headers(state)
self.wfile.write(json.dumps(data, default=df_r.Record.dict).encode('utf-8'))
if isinstance(data, str):
self._respond(state, _MIME_TXT)
self.wfile.write(data.encode('utf-8'))
else:
self._respond(state)
self.wfile.write(json.dumps(data, default=df_r.Record.dict).encode('utf-8'))

def log_request(self, _):
"""silence internal logger"""

def do_POST(self):
"""handle POST request"""
client_name = self.address_string()
if not self._client_allowed():
self.log("request from %s rejected", self.address_string(), level=logging.WARNING)
return
self.log('url path %s from %s', self.path, client_name,
level=logging.INFO)
command, file_name = self._parse_request_path()
if command is None:
return
content_length = int(self.headers['Content-Length'])
post_data = self.rfile.read(content_length)
data_dict = json.loads(post_data)
ident = data_dict.get(df_r.FIELD_IDENTIFIER)
if command == DEFAULT_COMMAND_UPDATE:
self.log('update %s in %s: %s', ident, self.path, data_dict)
if ident:
state, data = self.update_record(file_name, data_dict)
if isinstance(data, str):
self._set_headers(state, _MIME_TXT)
self.wfile.write(data.encode('utf-8'))
parsed_request = self._parse_request_path()
if isinstance(parsed_request, tuple):
command, file_name = parsed_request
if command is None:
return
content_length = int(self.headers['Content-Length'])
post_data = self.rfile.read(content_length)
data_dict = json.loads(post_data)
ident = data_dict.get(df_r.FIELD_IDENTIFIER)
if DEFAULT_COMMAND_UPDATE == command:
self.log('update %s in %s: %s', ident, self.path, data_dict)
if ident:
state, data = self.update_record(file_name, data_dict)
if isinstance(data, str):
self._respond(state, TEXT_HEADER)
self.wfile.write(data.encode('utf-8'))
else:
self._respond(state)
self.wfile.write(json.dumps(data, default=data.dict).encode('utf-8'))
else:
self._set_headers(state)
self.wfile.write(json.dumps(data, default=data.dict).encode('utf-8'))
else:
self._set_headers(404, _MIME_TXT)
self.wfile.write(f"no {ident} in {file_name}!".encode('utf-8'))
self._respond(404, TEXT_HEADER)
self.wfile.write(f"no {ident} in {file_name}!".encode('utf-8'))

def _set_headers(self, state=200, mime_type='application/json') -> None:
def _respond(self, state=200, headers=None) -> None:
self.send_response(state)
self.send_header('Content-type', mime_type)
if headers is None:
headers = DEFAULT_HEADER
for k, v in headers.items():
self.send_header(k, v)
self.end_headers()

def get_data_file(self, file_name: str):
Expand All @@ -146,6 +183,7 @@ def get_data_file(self, file_name: str):
return data_file
self.log("no file %s in %s", file_name, self.record_list_directory,
level=logging.CRITICAL)
return None

def get_next_record(self, file_name, client_name, requested_state, set_state) -> tuple:
"""Deliver next record data if both
Expand Down Expand Up @@ -238,6 +276,8 @@ def get_record(self, get_record_state, set_record_state):
raise RecordsExhaustedException(result.decode(encoding='utf-8'))

if status != 200:
if result is None or len(result) == 0:
result = df.UNSET_LABEL
self.log("server connection status: %s -> %s", status, result,
level=logging.ERROR)
raise RecordsServiceException(f"Record service error {status} - {result}")
Expand Down Expand Up @@ -272,6 +312,8 @@ def run_server(host, port, start_data: HandlerInformation):

the_logger = start_data.logger
the_logger.info("listen at: %s:%s for files from %s", host, port, start_data.data_path)
if start_data.client_ips and len(start_data.client_ips) > 0:
the_logger.info("accept requests only from %s", start_data.client_ips)
the_logger.info("next data: %s:%s/<record-file>/next", host, port)
the_logger.info("post data: %s:%s/<record-file>/update", host, port)
the_logger.info("to stop server, press CTRL+C")
Expand Down

0 comments on commit a325900

Please sign in to comment.