-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathgpgremote_client.py
executable file
·834 lines (665 loc) · 28.7 KB
/
gpgremote_client.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
""" GPG Remote Client Module
copyright: 2015, Vlad "SATtva" Miller, http://vladmiller.info
license: GNU GPL, see COPYING for details.
This module is intended as a drop-in replacement for unprivileged GnuPG
executable. Its purpose is to grab command line arguments, parse them
into an internal format, get STDIN, load files data, and tranfer all
that as a request package to the GPG Remote Server. The server, in
turn, filters command line arguments of inaproprite input, calls gpg
with provided data, and returns output (namely, newly created files,
STDOUT, STDERR, and exit code) back to the client.
The client reads configuration data (specifically, server listening
host:port) from gpgremote_client.conf file located in ~/.gnupg
directory unless path is overridden with GNUPGHOME environment
variable.
See README for additional details.
"""
import sys, os, json, io, socket, signal
__version__ = '1.3'
MIN_PYTHON = (3, 3)
CONFIG = {
'host': 'localhost',
'port': 29797,
'conn_timeout': 310}
CONF_NAME = 'gpgremote_client.conf'
PACKAGE_ERROR = 'error'
PACKAGE_GPG = 'gpg'
PACKAGE_PIN = 'pin'
STAGE_REQUEST = 'request'
STAGE_RESPONSE = 'response'
OUTPUT_OPTS = ['-o', '--output']
CONN_BUF = 4096
HEADER_LEN = 8
class GPGRemoteException(Exception):
pass
class TransmissionError(GPGRemoteException):
pass
class StreamLenError(GPGRemoteException):
pass
class PackageError(GPGRemoteException):
pass
class VersionMismatchError(GPGRemoteException):
pass
class AuthenticationError(GPGRemoteException):
pass
class FileSystemError(GPGRemoteException):
pass
class ResponseError(GPGRemoteException):
pass
class PyassuanImportError(GPGRemoteException):
pass
##########################
# Common functions begin #
##########################
# This section is identical for both client and server but duplicated
# in order to keep modules self-contained.
try:
# Use Standard Library secure comparison function if available (with
# Python 3.3+) instead of own implementation.
from hmac import compare_digest
secure_compare = compare_digest # Make IDE happy.
except ImportError:
def secure_compare(a, b):
"""Constant-time string/bytes comparison. Only reveals information
about values length, and whether values types and their lengths are
same/equal."""
return len(a) == len(b) and isinstance(a, type(b)) \
and sum([int(a[i] != b[i]) for i in range(len(a))]) < 1
def update_config(path, storage, silent=True):
""" Update application configuration from the conf file (if exists).
The function attempts to read the conf file at provided path, and
update configuration dict with its parsed contents. The format for
the configuration file is "key = value" for str/int value, or "key"
for bool values; blank lines and lines starting with hash sign are
ignored.
Args:
path (str): Configuration file pathname.
storage (dict): Configuration storage to update in-place.
silent (bool): Supress any errors.
Raises:
ValueError: In case of file read/parse errors in non-silent
mode.
"""
try:
with open(path, 'r') as file:
for line in file.readlines():
line = line.strip()
if line.startswith('#') or not line:
continue
key, separator, value = line.partition('=')
key, value = key.strip(), value.strip()
if value.isdecimal():
storage[key] = int(value)
else:
storage[key] = value if separator else True
except:
if silent:
return
else:
raise ValueError
def pack(identifier, *fields, files=None, auth_key=None):
""" Prepare package for sending.
Args:
identifier (str): Package type identifier.
*fields: Data fields to include in the package. Data types
must be JSON-compatible.
files (dict): Mapping of files binary data to filenames.
auth_key (bytes): Package contents authentication key (not used
for binary data). Must be bool(auth_key) == True to enable
authentication.
Returns:
(tuple) Package ready for transmission: its length (int) and
its contents as a binary stream (io.BytesIO).
The package is a structure of the following format:
header|JSON([auth, version], identifier, fields, files_meta)|binary,
where header is a 64-bit (8 bytes) JSON packet length header, and
binary is a concatenated binary data of all included files.
Filename and size of each file is included in the last item of JSON
packet as a list of (filename, file size) 2-tuples.
Package is authenticated (if auth_key is provided) with HMAC-SHA256
over JSON-encoded flat list of package content elements except for
binary data. Auth token is in hex notation.
"""
length = 0
output = io.BytesIO()
files = files or {}
files_meta = [(name, len(data)) for name, data in files.items()]
hmac_local = ''
if auth_key:
import hashlib, hmac
contents = json.dumps([__version__, identifier, fields, files_meta])
hmac_local = hmac.new(auth_key, contents.encode(),
hashlib.sha256).hexdigest()
header = [hmac_local, __version__]
package = json.dumps([header, identifier, fields, files_meta],
ensure_ascii=False, separators=(',', ':')).encode()
# Writing header.
length += output.write(int.to_bytes(len(package), HEADER_LEN, 'big'))
# Writing JSON packet.
length += output.write(package)
# Writing files binary data.
for name, data in files.items():
length += output.write(data)
files[name] = b'' # Trimming to keep memory reqs in bounds.
output.seek(0)
return length, output
def unpack(package, auth_key=None):
""" Unpack received package.
Args:
package (io.BytesIO): Received package stream.
auth_key (bytes): Package contents authentication key (not used
for binary data). Must be bool(auth_key) == True to enable
authentication.
Returns:
(tuple) Package type identifier (str), packed fields (list),
and {filename: binary_data} mapping (dict).
Raises:
PackageError: In case of malformed package.
VersionMismatchError: In case the package was created with a
different application version. Packer version is passed as
exception's first argument.
AuthenticationError: In case of authenticated package
verification failure.
"""
try:
length = int.from_bytes(package.read(HEADER_LEN), 'big')
header, identifier, fields, files_meta = json.loads(
package.read(length).decode())
hmac_received, version = header
if auth_key:
import hashlib, hmac
contents = json.dumps([version, identifier, fields, files_meta])
hmac_local = hmac.new(auth_key, contents.encode(),
hashlib.sha256).hexdigest()
if not secure_compare(hmac_local, hmac_received):
raise AuthenticationError
if version != __version__:
raise VersionMismatchError
files = {filename: package.read(length)
for filename, length in files_meta}
return identifier, fields, files
except Exception as exc:
if isinstance(exc, (VersionMismatchError, AuthenticationError)):
raise
else:
raise PackageError
def send(length, data, conn, _override_length=None):
""" Send binary data over a socket connection.
The stream is prefixed with 64-bit (8 bytes) data length header.
Args:
length (int): Stream length.
data (io.BytesIO): Data to be sent.
conn (socket.socket): Connection instance.
_override_length (int): For debugging only.
Raises:
TransmissionError: In case of abruptly terminated connection.
socket.timeout: In case of transmission timeout.
"""
def send_chunk(_data, offset):
sent = conn.send(_data.getbuffer()[offset:].tobytes())
if sent == 0:
raise TransmissionError
return sent
sent_total = 0
header_sent = False
header = io.BytesIO(int.to_bytes(_override_length or length,
HEADER_LEN, 'big'))
length += HEADER_LEN
while sent_total < length:
if not header_sent and sent_total >= HEADER_LEN:
header_sent = True
sent = send_chunk(header if not header_sent else data,
sent_total if not header_sent
else sent_total - HEADER_LEN)
sent_total += sent
def receive(conn, len_limit=None):
""" Receive binary data over a socket connection.
Data is read from a stream up to the length defined by the 8-byte
header regardless of the actual stream length.
Args:
conn (socket.socket): Connection instance.
len_limit (int): Length limit imposed on a stream. None or 0 to
disable length check.
Returns:
(io.BytesIO) Binary stream object with received data contents
excluding length header.
Raises:
TransmissionError: In case of abruptly terminated connection.
StreamLenError: In case data length defined in the header
exceeds len_limit. Defined length is passed as exception's
first argument.
socket.timeout: In case of transmission timeout.
"""
def receive_chunk():
received = conn.recv(CONN_BUF)
if received == b'':
raise TransmissionError
return received
received_total = 0
output = io.BytesIO()
header_received = False
length = CONN_BUF
while True:
if not header_received and received_total >= HEADER_LEN:
header_received = True
# Read package length from the header.
output.seek(0)
header = output.read(HEADER_LEN)
length = int.from_bytes(header, 'big')
# Length limit check, if any.
if len_limit and length > len_limit:
raise StreamLenError(length)
# Reinitialize output stream with its current contents except
# for the header, and set write pointer to its end.
output = io.BytesIO(output.read(length))
output.seek(0, io.SEEK_END)
length += HEADER_LEN
if received_total >= length:
break
data_left = length - received_total
received = receive_chunk()
received_total += len(received)
# Slice the receive buffer in order to not write its tail
# (exceeding the package length) on the last iteration.
output.write(received if not header_received else
received[:data_left])
output.seek(0)
return output
########################
# Common functions end #
########################
def parse_options(argv):
""" Parse arguments list.
Args:
argv (list): Command line arguments with application name
excluded (e.g. sys.argv[1:]).
Returns:
(list) Arguments are restructured into a list of two-tuples
of (option, parameter); None as the second element means no
parameters were provided for an option. Shortened options,
if given in chained form (multiple options following a
single dash sign), are separated in distinct elements. The
trailing command line arguments are assigned to the last
list elements with None instead of an option name.
"""
output = []
trailing_args = []
args_only = False
opt_name = None
for arg in argv:
# All the following are arguments only.
if arg == '--':
args_only = True
opt_name = None
continue
# Trailing argument.
if args_only:
trailing_args.append(arg)
else:
# An option.
if arg.startswith('-') and len(arg) > 1:
# Remove non-POSIX arguments if those were provided.
if trailing_args:
trailing_args = []
# Split shortened options.
if not arg.startswith('--') and len(arg) > 2:
short_args = ['-' + short for short
in list(arg.lstrip('-'))]
opt_name = short_args[-1]
for short_arg in short_args:
output.append((short_arg, None))
else:
opt_name = arg
output.append((opt_name, None))
# Argument.
elif opt_name is None or output[-1][1] is not None:
trailing_args.append(arg)
# Option parameter.
else:
output[-1] = (opt_name, arg)
return output + [(None, arg) for arg in trailing_args]
def get_filenames(args):
""" Get filenames of existing files provided in command line arguments.
Output file(s) (given as -o option parameter) is explicitly
ignored.
Args:
args (list): Parsed command line arguments (output of
parse_options() function).
Returns:
(list) Filenames of existing files.
"""
return [param for opt, param in args
if opt not in OUTPUT_OPTS
and param and os.path.isfile(param)]
def error_exit(msg, code=1):
"""Print error message and send exit code."""
print(msg)
sys.exit(code)
class ErrorHandler(object):
""" Exceptions handler.
A handler method must conform to "handle_<stage>_<exc_name>" naming
template and accept the actual exception as the only argument.
Attributes:
stage (str): STAGE_* constant corresponding to the current
protocol stage.
"""
stage = None
def __init__(self, stage):
self.stage = stage
def __call__(self, exc):
"""Handle exception."""
name = exc.__class__.__name__
try:
handle_error = getattr(self, 'handle_{}_{}'.format(self.stage,
name))
except AttributeError:
import traceback
error_exit("Undefined error '{}' at {} stage:\n{}".
format(name, self.stage,
traceback.format_exc().rstrip()))
handle_error(exc)
def handle_request_FileSystemError(self, exc):
error_exit("Unable to access or read file '{}'".format(exc.args[1]))
def handle_response_FileSystemError(self, exc):
error_exit("Unable to write file '{}'".format(exc.args[1]))
def handle_request_TransmissionError(self, exc):
error_exit('Server has abruptly terminated connection while '
'sending request')
def handle_response_TransmissionError(self, exc):
error_exit('Server has abruptly terminated connection while '
'receiving response')
def handle_request_ConnectionResetError(self, exc):
self.handle_request_TransmissionError()
def handle_response_ConnectionResetError(self, exc):
self.handle_response_TransmissionError()
def handle_request_timeout(self, exc):
error_exit('Timed out while sending request to GPG Remote')
def handle_response_timeout(self, exc):
error_exit('Timed out while awaiting response from GPG Remote')
def handle_request_BrokenPipeError(self, exc):
# Optimistically ignore exception to later read from
# socket as server may have sent a reason for connection
# termination.
return
def handle_response_BrokenPipeError(self, exc):
self.handle_response_TransmissionError()
def handle_response_AttributeError(self, exc):
error_exit('Unknown type response received from GPG Remote Server')
def handle_response_ResponseError(self, exc):
error_exit('Malformed response received from GPG Remote Server')
def handle_response_PackageError(self, exc):
self.handle_response_ResponseError()
def handle_response_TypeError(self, exc):
error_exit('Unable to unpack GPG Remote Server response')
def handle_response_ValueError(self, exc):
self.handle_response_TypeError()
def handle_response_PyassuanImportError(self, exc):
error_exit('Error importing pyassuan library. Make sure the '
'library is installed')
class PackageHandler(object):
""" Packages dispatcher/handler.
The dispatcher is a simple single-threaded server listening on the
provided connection. Once a package is received, it gets passed to
the corresponding handler method. The server runs until its 'stop'
attribute is set to True, or until a handler terminates the
interpreter by calling sys.exit().
Attributes:
stop (bool): Stop execution flag. If set to True the dispatcher
will stop listening for new packages once the current
handler has finished.
conn (socket.socket): Server connection instance.
conn_timeout (int): Connection timeout interval (in seconds).
"""
def __init__(self, conn):
""" Server initialization.
Args:
conn (socket.socket): Server connection instance.
"""
self.stop = False
self.conn = conn
self.conn_timeout = int(CONFIG['conn_timeout'])
@staticmethod
def timeout(signum, frame):
"""Timeout signal. Raises socket.timeout exception."""
raise socket.timeout
def run(self):
""" Start dispatcher.
Raises:
AttributeError: In case of an unknown package type.
"""
signal.signal(signal.SIGALRM, self.timeout)
signal.alarm(self.conn_timeout)
while not self.stop:
ErrorHandler.stage = STAGE_RESPONSE
identifier, *response = unpack(receive(self.conn))
handle_package = getattr(self, 'handle_package_' + identifier)
handle_package(*response)
signal.alarm(0)
def send_package_gpg(self, args, stdin, filenames):
""" Generate and send request package for GPG execution.
Args:
args (list): Parsed command line arguments (output of
parse_options() function).
stdin (bytes): STDIN data.
filenames (list): Filenames of existing files in command
line arguments (as per get_filenames() function).
Raises:
FileSystemError: In case of errors from the file system
layer during files packing. The filename is passed as
the first argument.
TransmissionError: Passed unhandled from the lower level.
socket.timeout: Passed unhandled from the lower level.
"""
ErrorHandler.stage = STAGE_REQUEST
files = {}
for filename in filenames:
try:
with open(filename, 'rb') as file:
files[filename] = file.read()
except:
raise FileSystemError(filename)
# Adding STDIN binary stream as None-keyed file.
if stdin is not None:
files[None] = stdin
package = pack(PACKAGE_GPG, *args, files=files)
send(*package, conn=self.conn)
def send_package_pin(self, responses):
""" Prepare and send pinentry output data.
Args:
responses (list): A list of pyassuan response objects.
Raises:
TransmissionError: Passed unhandled from the lower level.
socket.timeout: Passed unhandled from the lower level.
"""
import base64
ErrorHandler.stage = STAGE_REQUEST
data = []
for response in responses:
command = response.type
param = response.parameters
param = base64.b64encode(param if isinstance(param, bytes)
else param.encode()).decode() \
if param is not None and command != 'ERR' else param
data.append((command, param))
package = pack(PACKAGE_PIN, *data)
send(*package, conn=self.conn)
def handle_package_gpg(self, *data):
""" Process 'gpg' type response package.
Args:
data (list): Package contents returned by unpack() with
identifier element discarded. Packed fields are assumed
to be a list of [stderr, exit_code]. All GPG-generated
files are passed as the corresponding data element (see
unpack() for details) with the None-keyed element being
STDOUT binary stream. Files are written to the
specified filenames.
Raises:
ResponseError: In case of malformed package.
FileSystemError: In case any output file cannot be written.
File name is passed as exception's first argument.
"""
self.stop = True
try:
fields, files = data
stderr, exit_code = fields
# Writing output files. At the very least STDOUT should be
# contained here.
try:
for filename, data in files.items():
if filename is not None:
with open(filename, 'wb') as file:
file.write(data)
else:
stdout = data
except:
raise FileSystemError(filename)
# Writing STDERR/STDOUT, sending exit code.
sys.stderr.write(stderr)
sys.stdout.buffer.write(stdout)
sys.exit(exit_code)
except (TypeError, ValueError):
raise ResponseError
def handle_package_pin(self, *data):
""" Process 'pin' type response package. (Technically, it is
a request from the remote pinentry but backwards-named for
consistency reasons.)
The method does not returns, instead it sends pinentry response
back to the server.
Args:
data (list): Package contents returned by unpack() with
identifier element discarded. Packed fields are assumed
to be a list of [strings, options, OTP flag, OTP ID].
The first two are lists of two-tuples.
Raises:
PyassuanImportError: If pyassuan library is not installed.
"""
# Expect server data to be format conformant, so not handle errors.
fields, _ = data
strings, options, otp, otp_id = fields
# Update description string.
error_idx = None
for i, element in enumerate(strings):
if element[0] == 'SETDESC':
text = 'GPG Remote Server:\n\n' + element[1]
if otp and otp_id:
text += '\nOTP is enabled, append password {} to ' \
'the private key passphrase\n'.format(otp_id)
strings[i] = (element[0], text)
elif element[0] == 'SETERROR':
error_idx = i
# Updating error string if necessary.
if otp and not otp_id:
text = 'OTP list is exhausted, private key operation ' \
'will fail\n'
if error_idx is not None:
strings[error_idx] = ('SETERROR', text)
else:
strings.append(('SETERROR', text))
# Update ttyname option.
for i, element in enumerate(options):
if element[1] is not None \
and element[1].split(' ')[0] == 'ttyname':
try:
ttyname = os.ttyname(sys.stdin.fileno())
except OSError:
continue
options[i] = (element[0], ' '.join(['ttyname', ttyname]))
response = self._get_pin(options, strings)
self.send_package_pin(response)
def _get_pin(self, options, strings):
""" Run pinentry and ask for client passphrase.
Args:
options (list): List of Assuan options two-tuples.
strings (list): List of Assuan text strings two-tuples.
Returns:
(list) List of pyassuan response objects.
Raises:
PyassuanImportError: If pyassuan library is not installed.
"""
try:
from pyassuan import client as assuan_client
from pyassuan import common as assuan_common
from pyassuan import error as assuan_error
except ImportError:
raise PyassuanImportError
from subprocess import Popen, PIPE
client = assuan_client.AssuanClient(name='pin_client',
close_on_disconnect=True)
try:
use_curses = os.getenv('PINENTRY_USER_DATA', '').\
startswith('USE_CURSES=')
executable = 'pinentry' if not use_curses else 'pinentry-curses'
with Popen([executable], stdin=PIPE, stdout=PIPE) as pinentry:
client.input = pinentry.stdout
client.output = pinentry.stdin
client.connect()
try:
if client.read_response().type != 'OK':
error_exit('Pinentry protocol failed')
for opt, param in options:
client.make_request(assuan_common.Request(opt,
param))
for string, contents in strings:
client.make_request(assuan_common.Request(string,
contents))
return client.make_request(
assuan_common.Request('GETPIN'))[0]
except assuan_error.AssuanError as exc:
return exc.responses
finally:
client.make_request(assuan_common.Request('BYE'))
client.disconnect()
except FileNotFoundError:
error_exit("Pinentry executable '{}' not found".
format(executable))
def handle_package_error(self, *data):
""" Process 'error' type response package.
Args:
data (list): Package contents returned by unpack() with
identifier element discarded. Packed fields are assumed
to be a list of [message, exit_code].
Raises:
ResponseError: In case of malformed package.
"""
self.stop = True
try:
fields, _ = data
message, code = fields
error_exit(message, code)
except (TypeError, ValueError):
raise ResponseError
if __name__ == '__main__':
if sys.version_info[:2] < MIN_PYTHON:
error_exit("Python interpreter version {} or higher is required".
format('.'.join([str(i) for i in MIN_PYTHON])))
conf_path = os.path.join(os.getenv('GNUPGHOME', '~/.gnupg'), CONF_NAME)
update_config(conf_path, CONFIG)
args = parse_options(sys.argv[1:])
stdin = sys.stdin.buffer.read() if not sys.stdin.isatty() else None
filenames = get_filenames(args)
try:
conn = socket.create_connection(
(CONFIG['host'], int(CONFIG['port'])),
timeout=float(CONFIG['conn_timeout'] or 1))
package_handler = PackageHandler(conn)
error_handler = ErrorHandler(STAGE_REQUEST)
# Sending request to the server.
try:
package_handler.send_package_gpg(args, stdin, filenames)
except Exception as exc:
error_handler(exc)
# Handling server response.
try:
package_handler.run()
except Exception as exc:
error_handler(exc)
except ConnectionRefusedError:
error_exit('Connection to GPG Remote Server refused. '
'Probably no one is listening on the other end')
finally:
try:
conn.close()
except NameError:
pass