315 lines
9.1 KiB
Python
315 lines
9.1 KiB
Python
# Copyright (C) 2012 W. Trevor King <wking@tremily.us>
|
|
#
|
|
# This file is part of pyassuan.
|
|
#
|
|
# pyassuan is free software: you can redistribute it and/or modify it under the
|
|
# terms of the GNU General Public License as published by the Free Software
|
|
# Foundation, either version 3 of the License, or (at your option) any later
|
|
# version.
|
|
#
|
|
# pyassuan is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License along with
|
|
# pyassuan. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
"""Items common to both the client and server
|
|
"""
|
|
|
|
import array as _array
|
|
import re as _re
|
|
import socket as _socket
|
|
|
|
from . import LOG as _LOG
|
|
from . import error as _error
|
|
|
|
|
|
LINE_LENGTH = 1002 # 1000 + [CR,]LF
|
|
_ENCODE_PATTERN = '(' + '|'.join(['%', '\r', '\n']) + ')'
|
|
_ENCODE_STR_REGEXP = _re.compile(_ENCODE_PATTERN)
|
|
_ENCODE_BYTE_REGEXP = _re.compile(_ENCODE_PATTERN.encode('ascii'))
|
|
_DECODE_STR_REGEXP = _re.compile('(%[0-9A-Fa-f]{2})')
|
|
_DECODE_BYTE_REGEXP = _re.compile(b'(%[0-9A-Fa-f]{2})')
|
|
_REQUEST_REGEXP = _re.compile('^(\w+)( *)(.*)\Z')
|
|
|
|
|
|
def encode(data):
|
|
r"""
|
|
|
|
>>> encode('It grew by 5%!\n')
|
|
'It grew by 5%25!%0A'
|
|
>>> encode(b'It grew by 5%!\n')
|
|
b'It grew by 5%25!%0A'
|
|
"""
|
|
if isinstance(data, bytes):
|
|
regexp = _ENCODE_BYTE_REGEXP
|
|
else:
|
|
regexp = _ENCODE_STR_REGEXP
|
|
return regexp.sub(
|
|
lambda x : to_hex(x.group()), data)
|
|
|
|
def decode(data):
|
|
r"""
|
|
|
|
>>> decode('%22Look out!%22%0AWhere%3F')
|
|
'"Look out!"\nWhere?'
|
|
>>> decode(b'%22Look out!%22%0AWhere%3F')
|
|
b'"Look out!"\nWhere?'
|
|
"""
|
|
if isinstance(data, bytes):
|
|
regexp = _DECODE_BYTE_REGEXP
|
|
else:
|
|
regexp = _DECODE_STR_REGEXP
|
|
return regexp.sub(
|
|
lambda x : from_hex(x.group()), data)
|
|
|
|
def from_hex(code):
|
|
r"""
|
|
|
|
>>> from_hex('%22')
|
|
'"'
|
|
>>> from_hex('%0A')
|
|
'\n'
|
|
>>> from_hex(b'%0A')
|
|
b'\n'
|
|
"""
|
|
c = chr(int(code[1:], 16))
|
|
if isinstance(code, bytes):
|
|
c =c.encode('ascii')
|
|
return c
|
|
|
|
def to_hex(char):
|
|
r"""
|
|
|
|
>>> to_hex('"')
|
|
'%22'
|
|
>>> to_hex('\n')
|
|
'%0A'
|
|
>>> to_hex(b'\n')
|
|
b'%0A'
|
|
"""
|
|
hx = '%{:02X}'.format(ord(char))
|
|
if isinstance(char, bytes):
|
|
hx = hx.encode('ascii')
|
|
return hx
|
|
|
|
|
|
class Request (object):
|
|
"""A client request
|
|
|
|
http://www.gnupg.org/documentation/manuals/assuan/Client-requests.html
|
|
|
|
>>> r = Request(command='BYE')
|
|
>>> str(r)
|
|
'BYE'
|
|
>>> r = Request(command='OPTION', parameters='testing at 5%')
|
|
>>> str(r)
|
|
'OPTION testing at 5%25'
|
|
>>> bytes(r)
|
|
b'OPTION testing at 5%25'
|
|
>>> r.from_bytes(b'BYE')
|
|
>>> r.command
|
|
'BYE'
|
|
>>> print(r.parameters)
|
|
None
|
|
>>> r.from_bytes(b'OPTION testing at 5%25')
|
|
>>> r.command
|
|
'OPTION'
|
|
>>> print(r.parameters)
|
|
testing at 5%
|
|
>>> r.from_bytes(b' invalid')
|
|
Traceback (most recent call last):
|
|
...
|
|
pyassuan.error.AssuanError: 170 Invalid request
|
|
>>> r.from_bytes(b'in-valid')
|
|
Traceback (most recent call last):
|
|
...
|
|
pyassuan.error.AssuanError: 170 Invalid request
|
|
"""
|
|
def __init__(self, command=None, parameters=None, encoded=False):
|
|
self.command = command
|
|
self.parameters = parameters
|
|
self.encoded = encoded
|
|
|
|
def __str__(self):
|
|
if self.parameters:
|
|
if self.encoded:
|
|
encoded_parameters = self.parameters
|
|
else:
|
|
encoded_parameters = encode(self.parameters)
|
|
return '{} {}'.format(self.command, encoded_parameters)
|
|
return self.command
|
|
|
|
def __bytes__(self):
|
|
if self.parameters:
|
|
if self.encoded:
|
|
encoded_parameters = self.parameters
|
|
else:
|
|
encoded_parameters = encode(self.parameters)
|
|
return '{} {}'.format(
|
|
self.command, encoded_parameters).encode('utf-8')
|
|
return self.command.encode('utf-8')
|
|
|
|
def from_bytes(self, line):
|
|
if len(line) > 1000: # TODO: byte-vs-str and newlines?
|
|
raise _error.AssuanError(message='Line too long')
|
|
line = str(line, encoding='utf-8')
|
|
match = _REQUEST_REGEXP.match(line)
|
|
if not match:
|
|
raise _error.AssuanError(message='Invalid request')
|
|
self.command = match.group(1)
|
|
if match.group(3):
|
|
if match.group(2):
|
|
self.parameters = decode(match.group(3))
|
|
else:
|
|
raise _error.AssuanError(message='Invalid request')
|
|
else:
|
|
self.parameters = None
|
|
|
|
|
|
class Response (object):
|
|
"""A server response
|
|
|
|
http://www.gnupg.org/documentation/manuals/assuan/Server-responses.html
|
|
|
|
>>> r = Response(type='OK')
|
|
>>> str(r)
|
|
'OK'
|
|
>>> r = Response(type='ERR', parameters='1 General error')
|
|
>>> str(r)
|
|
'ERR 1 General error'
|
|
>>> bytes(r)
|
|
b'ERR 1 General error'
|
|
>>> r.from_bytes(b'OK')
|
|
>>> r.type
|
|
'OK'
|
|
>>> print(r.parameters)
|
|
None
|
|
>>> r.from_bytes(b'ERR 1 General error')
|
|
>>> r.type
|
|
'ERR'
|
|
>>> print(r.parameters)
|
|
1 General error
|
|
>>> r.from_bytes(b' invalid')
|
|
Traceback (most recent call last):
|
|
...
|
|
pyassuan.error.AssuanError: 76 Invalid response
|
|
>>> r.from_bytes(b'in-valid')
|
|
Traceback (most recent call last):
|
|
...
|
|
pyassuan.error.AssuanError: 76 Invalid response
|
|
"""
|
|
types = {
|
|
'O': 'OK',
|
|
'E': 'ERR',
|
|
'S': 'S',
|
|
'#': '#',
|
|
'D': 'D',
|
|
'I': 'INQUIRE',
|
|
}
|
|
|
|
def __init__(self, type=None, parameters=None):
|
|
self.type = type
|
|
self.parameters = parameters
|
|
|
|
def __str__(self):
|
|
if self.parameters:
|
|
return '{} {}'.format(self.type, encode(self.parameters))
|
|
return self.type
|
|
|
|
def __bytes__(self):
|
|
if self.parameters:
|
|
if self.type == 'D':
|
|
return b' '.join((b'D', self.parameters))
|
|
else:
|
|
return '{} {}'.format(
|
|
self.type, encode(self.parameters)).encode('utf-8')
|
|
return self.type.encode('utf-8')
|
|
|
|
def from_bytes(self, line):
|
|
if len(line) > 1000: # TODO: byte-vs-str and newlines?
|
|
raise _error.AssuanError(message='Line too long')
|
|
if line.startswith(b'D'):
|
|
self.command = t = 'D'
|
|
else:
|
|
line = str(line, encoding='utf-8')
|
|
t = line[0]
|
|
try:
|
|
type = self.types[t]
|
|
except KeyError:
|
|
raise _error.AssuanError(message='Invalid response')
|
|
self.type = type
|
|
if type == 'D': # data
|
|
self.parameters = decode(line[2:])
|
|
elif type == '#': # comment
|
|
self.parameters = decode(line[2:])
|
|
else:
|
|
match = _REQUEST_REGEXP.match(line)
|
|
if not match:
|
|
raise _error.AssuanError(message='Invalid request')
|
|
if match.group(3):
|
|
if match.group(2):
|
|
self.parameters = decode(match.group(3))
|
|
else:
|
|
raise _error.AssuanError(message='Invalid request')
|
|
else:
|
|
self.parameters = None
|
|
|
|
|
|
def error_response(error):
|
|
"""
|
|
|
|
>>> from pyassuan.error import AssuanError
|
|
>>> error = AssuanError(1)
|
|
>>> response = error_response(error)
|
|
>>> print(response)
|
|
ERR 1 General error
|
|
"""
|
|
return Response(type='ERR', parameters=str(error))
|
|
|
|
|
|
def send_fds(socket, msg=None, fds=None, logger=_LOG):
|
|
"""Send a file descriptor over a Unix socket using ``sendmsg``.
|
|
|
|
``sendmsg`` suport requires Python >= 3.3.
|
|
|
|
Code from
|
|
http://docs.python.org/dev/library/socket.html#socket.socket.sendmsg
|
|
|
|
Assuan equivalent is
|
|
http://www.gnupg.org/documentation/manuals/assuan/Client-code.html#function-assuan_005fsendfd
|
|
"""
|
|
if msg is None:
|
|
msg = b''.join(
|
|
[b'# descriptors in flight: ', str(fds).encode('ascii'), b'\n'])
|
|
if logger is not None:
|
|
logger.debug('sending file descriptors {} down {}'.format(fds, socket))
|
|
return socket.sendmsg(
|
|
[msg],
|
|
[(_socket.SOL_SOCKET, _socket.SCM_RIGHTS, _array.array('i', fds))])
|
|
|
|
def receive_fds(socket, msglen=200, maxfds=10, logger=_LOG):
|
|
"""Recieve file descriptors using ``recvmsg``.
|
|
|
|
``recvmsg`` suport requires Python >= 3.3.
|
|
|
|
Code from http://docs.python.org/dev/library/socket.html
|
|
|
|
Assuan equivalent is
|
|
http://www.gnupg.org/documentation/manuals/assuan/Client-code.html#fun_002dassuan_005freceivedfd
|
|
"""
|
|
fds = _array.array('i') # Array of ints
|
|
msg,ancdata,flags,addr = socket.recvmsg(
|
|
msglen, _socket.CMSG_LEN(maxfds * fds.itemsize))
|
|
for cmsg_level,cmsg_type,cmsg_data in ancdata:
|
|
if (cmsg_level == _socket.SOL_SOCKET and
|
|
cmsg_type == _socket.SCM_RIGHTS):
|
|
# Append data, ignoring any truncated integers at the end.
|
|
fds.fromstring(
|
|
cmsg_data[:len(cmsg_data) - (len(cmsg_data) % fds.itemsize)])
|
|
if logger is not None:
|
|
logger.debug('receiving file descriptors {} from {} ({})'.format(
|
|
fds, socket, msg))
|
|
return (msg, list(fds))
|