1 # Copyright (C) 2012 W. Trevor King <wking@drexel.edu>
3 # This file is part of pyassuan.
5 # pyassuan is free software: you can redistribute it and/or modify it under the
6 # terms of the GNU General Public License as published by the Free Software
7 # Foundation, either version 3 of the License, or (at your option) any later
10 # pyassuan is distributed in the hope that it will be useful, but WITHOUT ANY
11 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12 # A PARTICULAR PURPOSE. See the GNU General Public License for more details.
14 # You should have received a copy of the GNU General Public License along with
15 # pyassuan. If not, see <http://www.gnu.org/licenses/>.
17 """Items common to both the client and server
20 import array as _array
22 import socket as _socket
24 from . import LOG as _LOG
25 from . import error as _error
28 LINE_LENGTH = 1002 # 1000 + [CR,]LF
29 _ENCODE_PATTERN = '(' + '|'.join(['%', '\r', '\n']) + ')'
30 _ENCODE_STR_REGEXP = _re.compile(_ENCODE_PATTERN)
31 _ENCODE_BYTE_REGEXP = _re.compile(_ENCODE_PATTERN.encode('ascii'))
32 _DECODE_STR_REGEXP = _re.compile('(%[0-9A-Fa-f]{2})')
33 _DECODE_BYTE_REGEXP = _re.compile(b'(%[0-9A-Fa-f]{2})')
34 _REQUEST_REGEXP = _re.compile('^(\w+)( *)(.*)\Z')
40 >>> encode('It grew by 5%!\n')
42 >>> encode(b'It grew by 5%!\n')
43 b'It grew by 5%25!%0A'
45 if isinstance(data, bytes):
46 regexp = _ENCODE_BYTE_REGEXP
48 regexp = _ENCODE_STR_REGEXP
50 lambda x : to_hex(x.group()), data)
55 >>> decode('%22Look out!%22%0AWhere%3F')
57 >>> decode(b'%22Look out!%22%0AWhere%3F')
58 b'"Look out!"\nWhere?'
60 if isinstance(data, bytes):
61 regexp = _DECODE_BYTE_REGEXP
63 regexp = _DECODE_STR_REGEXP
65 lambda x : from_hex(x.group()), data)
77 c = chr(int(code[1:], 16))
78 if isinstance(code, bytes):
92 hx = '%{:02X}'.format(ord(char))
93 if isinstance(char, bytes):
94 hx = hx.encode('ascii')
98 class Request (object):
101 http://www.gnupg.org/documentation/manuals/assuan/Client-requests.html
103 >>> r = Request(command='BYE')
106 >>> r = Request(command='OPTION', parameters='testing at 5%')
108 'OPTION testing at 5%25'
110 b'OPTION testing at 5%25'
111 >>> r.from_bytes(b'BYE')
114 >>> print(r.parameters)
116 >>> r.from_bytes(b'OPTION testing at 5%25')
119 >>> print(r.parameters)
121 >>> r.from_bytes(b' invalid')
122 Traceback (most recent call last):
124 pyassuan.error.AssuanError: 170 Invalid request
125 >>> r.from_bytes(b'in-valid')
126 Traceback (most recent call last):
128 pyassuan.error.AssuanError: 170 Invalid request
130 def __init__(self, command=None, parameters=None, encoded=False):
131 self.command = command
132 self.parameters = parameters
133 self.encoded = encoded
138 encoded_parameters = self.parameters
140 encoded_parameters = encode(self.parameters)
141 return '{} {}'.format(self.command, encoded_parameters)
147 encoded_parameters = self.parameters
149 encoded_parameters = encode(self.parameters)
150 return '{} {}'.format(
151 self.command, encoded_parameters).encode('utf-8')
152 return self.command.encode('utf-8')
154 def from_bytes(self, line):
155 if len(line) > 1000: # TODO: byte-vs-str and newlines?
156 raise _error.AssuanError(message='Line too long')
157 line = str(line, encoding='utf-8')
158 match = _REQUEST_REGEXP.match(line)
160 raise _error.AssuanError(message='Invalid request')
161 self.command = match.group(1)
164 self.parameters = decode(match.group(3))
166 raise _error.AssuanError(message='Invalid request')
168 self.parameters = None
171 class Response (object):
174 http://www.gnupg.org/documentation/manuals/assuan/Server-responses.html
176 >>> r = Response(type='OK')
179 >>> r = Response(type='ERR', parameters='1 General error')
181 'ERR 1 General error'
183 b'ERR 1 General error'
184 >>> r.from_bytes(b'OK')
187 >>> print(r.parameters)
189 >>> r.from_bytes(b'ERR 1 General error')
192 >>> print(r.parameters)
194 >>> r.from_bytes(b' invalid')
195 Traceback (most recent call last):
197 pyassuan.error.AssuanError: 76 Invalid response
198 >>> r.from_bytes(b'in-valid')
199 Traceback (most recent call last):
201 pyassuan.error.AssuanError: 76 Invalid response
212 def __init__(self, type=None, parameters=None):
214 self.parameters = parameters
218 return '{} {}'.format(self.type, encode(self.parameters))
224 return b' '.join((b'D', self.parameters))
226 return '{} {}'.format(
227 self.type, encode(self.parameters)).encode('utf-8')
228 return self.type.encode('utf-8')
230 def from_bytes(self, line):
231 if len(line) > 1000: # TODO: byte-vs-str and newlines?
232 raise _error.AssuanError(message='Line too long')
233 if line.startswith(b'D'):
234 self.command = t = 'D'
236 line = str(line, encoding='utf-8')
241 raise _error.AssuanError(message='Invalid response')
243 if type == 'D': # data
244 self.parameters = decode(line[2:])
245 elif type == '#': # comment
246 self.parameters = decode(line[2:])
248 match = _REQUEST_REGEXP.match(line)
250 raise _error.AssuanError(message='Invalid request')
253 self.parameters = decode(match.group(3))
255 raise _error.AssuanError(message='Invalid request')
257 self.parameters = None
260 def error_response(error):
263 >>> from pyassuan.error import AssuanError
264 >>> error = AssuanError(1)
265 >>> response = error_response(error)
269 return Response(type='ERR', parameters=str(error))
272 def send_fds(socket, msg=None, fds=None, logger=_LOG):
273 """Send a file descriptor over a Unix socket using ``sendmsg``.
275 ``sendmsg`` suport requires Python >= 3.3.
278 http://docs.python.org/dev/library/socket.html#socket.socket.sendmsg
281 http://www.gnupg.org/documentation/manuals/assuan/Client-code.html#function-assuan_005fsendfd
285 [b'# descriptors in flight: ', str(fds).encode('ascii'), b'\n'])
286 if logger is not None:
287 logger.debug('sending file descriptors {} down {}'.format(fds, socket))
288 return socket.sendmsg(
290 [(_socket.SOL_SOCKET, _socket.SCM_RIGHTS, _array.array('i', fds))])
292 def receive_fds(socket, msglen=200, maxfds=10, logger=_LOG):
293 """Recieve file descriptors using ``recvmsg``.
295 ``recvmsg`` suport requires Python >= 3.3.
297 Code from http://docs.python.org/dev/library/socket.html
300 http://www.gnupg.org/documentation/manuals/assuan/Client-code.html#fun_002dassuan_005freceivedfd
302 fds = _array.array('i') # Array of ints
303 msg,ancdata,flags,addr = socket.recvmsg(
304 msglen, _socket.CMSG_LEN(maxfds * fds.itemsize))
305 for cmsg_level,cmsg_type,cmsg_data in ancdata:
306 if (cmsg_level == _socket.SOL_SOCKET and
307 cmsg_type == _socket.SCM_RIGHTS):
308 # Append data, ignoring any truncated integers at the end.
310 cmsg_data[:len(cmsg_data) - (len(cmsg_data) % fds.itemsize)])
311 if logger is not None:
312 logger.debug('receiving file descriptors {} from {} ({})'.format(
314 return (msg, list(fds))