##// END OF EJS Templates
Merge pull request #689 from minrk/auth...
Fernando Perez -
r4565:1d092172 merge
parent child Browse files
Show More
@@ -1,686 +1,700 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 """A simple interactive kernel that talks to a frontend over 0MQ.
2 """A simple interactive kernel that talks to a frontend over 0MQ.
3
3
4 Things to do:
4 Things to do:
5
5
6 * Implement `set_parent` logic. Right before doing exec, the Kernel should
6 * Implement `set_parent` logic. Right before doing exec, the Kernel should
7 call set_parent on all the PUB objects with the message about to be executed.
7 call set_parent on all the PUB objects with the message about to be executed.
8 * Implement random port and security key logic.
8 * Implement random port and security key logic.
9 * Implement control messages.
9 * Implement control messages.
10 * Implement event loop and poll version.
10 * Implement event loop and poll version.
11 """
11 """
12
12
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14 # Imports
14 # Imports
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 from __future__ import print_function
16 from __future__ import print_function
17
17
18 # Standard library imports.
18 # Standard library imports.
19 import __builtin__
19 import __builtin__
20 import atexit
20 import atexit
21 import sys
21 import sys
22 import time
22 import time
23 import traceback
23 import traceback
24 import logging
24 import logging
25 # System library imports.
25 # System library imports.
26 import zmq
26 import zmq
27
27
28 # Local imports.
28 # Local imports.
29 from IPython.config.configurable import Configurable
29 from IPython.config.configurable import Configurable
30 from IPython.config.application import boolean_flag
30 from IPython.config.application import boolean_flag
31 from IPython.core.application import ProfileDir
31 from IPython.core.application import ProfileDir
32 from IPython.core.shellapp import (
32 from IPython.core.shellapp import (
33 InteractiveShellApp, shell_flags, shell_aliases
33 InteractiveShellApp, shell_flags, shell_aliases
34 )
34 )
35 from IPython.utils import io
35 from IPython.utils import io
36 from IPython.utils.jsonutil import json_clean
36 from IPython.utils.jsonutil import json_clean
37 from IPython.lib import pylabtools
37 from IPython.lib import pylabtools
38 from IPython.utils.traitlets import (
38 from IPython.utils.traitlets import (
39 List, Instance, Float, Dict, Bool, Int, Unicode, CaselessStrEnum
39 List, Instance, Float, Dict, Bool, Int, Unicode, CaselessStrEnum
40 )
40 )
41
41
42 from entry_point import base_launch_kernel
42 from entry_point import base_launch_kernel
43 from kernelapp import KernelApp, kernel_flags, kernel_aliases
43 from kernelapp import KernelApp, kernel_flags, kernel_aliases
44 from iostream import OutStream
44 from iostream import OutStream
45 from session import Session, Message
45 from session import Session, Message
46 from zmqshell import ZMQInteractiveShell
46 from zmqshell import ZMQInteractiveShell
47
47
48
48
49
49
50 #-----------------------------------------------------------------------------
50 #-----------------------------------------------------------------------------
51 # Main kernel class
51 # Main kernel class
52 #-----------------------------------------------------------------------------
52 #-----------------------------------------------------------------------------
53
53
54 class Kernel(Configurable):
54 class Kernel(Configurable):
55
55
56 #---------------------------------------------------------------------------
56 #---------------------------------------------------------------------------
57 # Kernel interface
57 # Kernel interface
58 #---------------------------------------------------------------------------
58 #---------------------------------------------------------------------------
59
59
60 shell = Instance('IPython.core.interactiveshell.InteractiveShellABC')
60 shell = Instance('IPython.core.interactiveshell.InteractiveShellABC')
61 session = Instance(Session)
61 session = Instance(Session)
62 shell_socket = Instance('zmq.Socket')
62 shell_socket = Instance('zmq.Socket')
63 iopub_socket = Instance('zmq.Socket')
63 iopub_socket = Instance('zmq.Socket')
64 stdin_socket = Instance('zmq.Socket')
64 stdin_socket = Instance('zmq.Socket')
65 log = Instance(logging.Logger)
65 log = Instance(logging.Logger)
66
66
67 # Private interface
67 # Private interface
68
68
69 # Time to sleep after flushing the stdout/err buffers in each execute
69 # Time to sleep after flushing the stdout/err buffers in each execute
70 # cycle. While this introduces a hard limit on the minimal latency of the
70 # cycle. While this introduces a hard limit on the minimal latency of the
71 # execute cycle, it helps prevent output synchronization problems for
71 # execute cycle, it helps prevent output synchronization problems for
72 # clients.
72 # clients.
73 # Units are in seconds. The minimum zmq latency on local host is probably
73 # Units are in seconds. The minimum zmq latency on local host is probably
74 # ~150 microseconds, set this to 500us for now. We may need to increase it
74 # ~150 microseconds, set this to 500us for now. We may need to increase it
75 # a little if it's not enough after more interactive testing.
75 # a little if it's not enough after more interactive testing.
76 _execute_sleep = Float(0.0005, config=True)
76 _execute_sleep = Float(0.0005, config=True)
77
77
78 # Frequency of the kernel's event loop.
78 # Frequency of the kernel's event loop.
79 # Units are in seconds, kernel subclasses for GUI toolkits may need to
79 # Units are in seconds, kernel subclasses for GUI toolkits may need to
80 # adapt to milliseconds.
80 # adapt to milliseconds.
81 _poll_interval = Float(0.05, config=True)
81 _poll_interval = Float(0.05, config=True)
82
82
83 # If the shutdown was requested over the network, we leave here the
83 # If the shutdown was requested over the network, we leave here the
84 # necessary reply message so it can be sent by our registered atexit
84 # necessary reply message so it can be sent by our registered atexit
85 # handler. This ensures that the reply is only sent to clients truly at
85 # handler. This ensures that the reply is only sent to clients truly at
86 # the end of our shutdown process (which happens after the underlying
86 # the end of our shutdown process (which happens after the underlying
87 # IPython shell's own shutdown).
87 # IPython shell's own shutdown).
88 _shutdown_message = None
88 _shutdown_message = None
89
89
90 # This is a dict of port number that the kernel is listening on. It is set
90 # This is a dict of port number that the kernel is listening on. It is set
91 # by record_ports and used by connect_request.
91 # by record_ports and used by connect_request.
92 _recorded_ports = Dict()
92 _recorded_ports = Dict()
93
93
94
94
95
95
96 def __init__(self, **kwargs):
96 def __init__(self, **kwargs):
97 super(Kernel, self).__init__(**kwargs)
97 super(Kernel, self).__init__(**kwargs)
98
98
99 # Before we even start up the shell, register *first* our exit handlers
99 # Before we even start up the shell, register *first* our exit handlers
100 # so they come before the shell's
100 # so they come before the shell's
101 atexit.register(self._at_shutdown)
101 atexit.register(self._at_shutdown)
102
102
103 # Initialize the InteractiveShell subclass
103 # Initialize the InteractiveShell subclass
104 self.shell = ZMQInteractiveShell.instance(config=self.config)
104 self.shell = ZMQInteractiveShell.instance(config=self.config)
105 self.shell.displayhook.session = self.session
105 self.shell.displayhook.session = self.session
106 self.shell.displayhook.pub_socket = self.iopub_socket
106 self.shell.displayhook.pub_socket = self.iopub_socket
107 self.shell.display_pub.session = self.session
107 self.shell.display_pub.session = self.session
108 self.shell.display_pub.pub_socket = self.iopub_socket
108 self.shell.display_pub.pub_socket = self.iopub_socket
109
109
110 # TMP - hack while developing
110 # TMP - hack while developing
111 self.shell._reply_content = None
111 self.shell._reply_content = None
112
112
113 # Build dict of handlers for message types
113 # Build dict of handlers for message types
114 msg_types = [ 'execute_request', 'complete_request',
114 msg_types = [ 'execute_request', 'complete_request',
115 'object_info_request', 'history_request',
115 'object_info_request', 'history_request',
116 'connect_request', 'shutdown_request']
116 'connect_request', 'shutdown_request']
117 self.handlers = {}
117 self.handlers = {}
118 for msg_type in msg_types:
118 for msg_type in msg_types:
119 self.handlers[msg_type] = getattr(self, msg_type)
119 self.handlers[msg_type] = getattr(self, msg_type)
120
120
121 def do_one_iteration(self):
121 def do_one_iteration(self):
122 """Do one iteration of the kernel's evaluation loop.
122 """Do one iteration of the kernel's evaluation loop.
123 """
123 """
124 ident,msg = self.session.recv(self.shell_socket, zmq.NOBLOCK)
124 try:
125 ident,msg = self.session.recv(self.shell_socket, zmq.NOBLOCK)
126 except Exception:
127 self.log.warn("Invalid Message:", exc_info=True)
128 return
125 if msg is None:
129 if msg is None:
126 return
130 return
127
131
128 msg_type = msg['header']['msg_type']
132 msg_type = msg['header']['msg_type']
129
133
130 # This assert will raise in versions of zeromq 2.0.7 and lesser.
134 # This assert will raise in versions of zeromq 2.0.7 and lesser.
131 # We now require 2.0.8 or above, so we can uncomment for safety.
135 # We now require 2.0.8 or above, so we can uncomment for safety.
132 # print(ident,msg, file=sys.__stdout__)
136 # print(ident,msg, file=sys.__stdout__)
133 assert ident is not None, "Missing message part."
137 assert ident is not None, "Missing message part."
134
138
135 # Print some info about this message and leave a '--->' marker, so it's
139 # Print some info about this message and leave a '--->' marker, so it's
136 # easier to trace visually the message chain when debugging. Each
140 # easier to trace visually the message chain when debugging. Each
137 # handler prints its message at the end.
141 # handler prints its message at the end.
138 self.log.debug('\n*** MESSAGE TYPE:'+str(msg_type)+'***')
142 self.log.debug('\n*** MESSAGE TYPE:'+str(msg_type)+'***')
139 self.log.debug(' Content: '+str(msg['content'])+'\n --->\n ')
143 self.log.debug(' Content: '+str(msg['content'])+'\n --->\n ')
140
144
141 # Find and call actual handler for message
145 # Find and call actual handler for message
142 handler = self.handlers.get(msg_type, None)
146 handler = self.handlers.get(msg_type, None)
143 if handler is None:
147 if handler is None:
144 self.log.error("UNKNOWN MESSAGE TYPE:" +str(msg))
148 self.log.error("UNKNOWN MESSAGE TYPE:" +str(msg))
145 else:
149 else:
146 handler(ident, msg)
150 handler(ident, msg)
147
151
148 # Check whether we should exit, in case the incoming message set the
152 # Check whether we should exit, in case the incoming message set the
149 # exit flag on
153 # exit flag on
150 if self.shell.exit_now:
154 if self.shell.exit_now:
151 self.log.debug('\nExiting IPython kernel...')
155 self.log.debug('\nExiting IPython kernel...')
152 # We do a normal, clean exit, which allows any actions registered
156 # We do a normal, clean exit, which allows any actions registered
153 # via atexit (such as history saving) to take place.
157 # via atexit (such as history saving) to take place.
154 sys.exit(0)
158 sys.exit(0)
155
159
156
160
157 def start(self):
161 def start(self):
158 """ Start the kernel main loop.
162 """ Start the kernel main loop.
159 """
163 """
160 poller = zmq.Poller()
164 poller = zmq.Poller()
161 poller.register(self.shell_socket, zmq.POLLIN)
165 poller.register(self.shell_socket, zmq.POLLIN)
162 while True:
166 while True:
163 try:
167 try:
164 # scale by extra factor of 10, because there is no
168 # scale by extra factor of 10, because there is no
165 # reason for this to be anything less than ~ 0.1s
169 # reason for this to be anything less than ~ 0.1s
166 # since it is a real poller and will respond
170 # since it is a real poller and will respond
167 # to events immediately
171 # to events immediately
168
172
169 # double nested try/except, to properly catch KeyboardInterrupt
173 # double nested try/except, to properly catch KeyboardInterrupt
170 # due to pyzmq Issue #130
174 # due to pyzmq Issue #130
171 try:
175 try:
172 poller.poll(10*1000*self._poll_interval)
176 poller.poll(10*1000*self._poll_interval)
173 self.do_one_iteration()
177 self.do_one_iteration()
174 except:
178 except:
175 raise
179 raise
176 except KeyboardInterrupt:
180 except KeyboardInterrupt:
177 # Ctrl-C shouldn't crash the kernel
181 # Ctrl-C shouldn't crash the kernel
178 io.raw_print("KeyboardInterrupt caught in kernel")
182 io.raw_print("KeyboardInterrupt caught in kernel")
179
183
180 def record_ports(self, ports):
184 def record_ports(self, ports):
181 """Record the ports that this kernel is using.
185 """Record the ports that this kernel is using.
182
186
183 The creator of the Kernel instance must call this methods if they
187 The creator of the Kernel instance must call this methods if they
184 want the :meth:`connect_request` method to return the port numbers.
188 want the :meth:`connect_request` method to return the port numbers.
185 """
189 """
186 self._recorded_ports = ports
190 self._recorded_ports = ports
187
191
188 #---------------------------------------------------------------------------
192 #---------------------------------------------------------------------------
189 # Kernel request handlers
193 # Kernel request handlers
190 #---------------------------------------------------------------------------
194 #---------------------------------------------------------------------------
191
195
192 def _publish_pyin(self, code, parent):
196 def _publish_pyin(self, code, parent):
193 """Publish the code request on the pyin stream."""
197 """Publish the code request on the pyin stream."""
194
198
195 pyin_msg = self.session.send(self.iopub_socket, u'pyin',{u'code':code}, parent=parent)
199 pyin_msg = self.session.send(self.iopub_socket, u'pyin',{u'code':code}, parent=parent)
196
200
197 def execute_request(self, ident, parent):
201 def execute_request(self, ident, parent):
198
202
199 status_msg = self.session.send(self.iopub_socket,
203 status_msg = self.session.send(self.iopub_socket,
200 u'status',
204 u'status',
201 {u'execution_state':u'busy'},
205 {u'execution_state':u'busy'},
202 parent=parent
206 parent=parent
203 )
207 )
204
208
205 try:
209 try:
206 content = parent[u'content']
210 content = parent[u'content']
207 code = content[u'code']
211 code = content[u'code']
208 silent = content[u'silent']
212 silent = content[u'silent']
209 except:
213 except:
210 self.log.error("Got bad msg: ")
214 self.log.error("Got bad msg: ")
211 self.log.error(str(Message(parent)))
215 self.log.error(str(Message(parent)))
212 return
216 return
213
217
214 shell = self.shell # we'll need this a lot here
218 shell = self.shell # we'll need this a lot here
215
219
216 # Replace raw_input. Note that is not sufficient to replace
220 # Replace raw_input. Note that is not sufficient to replace
217 # raw_input in the user namespace.
221 # raw_input in the user namespace.
218 raw_input = lambda prompt='': self._raw_input(prompt, ident, parent)
222 raw_input = lambda prompt='': self._raw_input(prompt, ident, parent)
219 __builtin__.raw_input = raw_input
223 __builtin__.raw_input = raw_input
220
224
221 # Set the parent message of the display hook and out streams.
225 # Set the parent message of the display hook and out streams.
222 shell.displayhook.set_parent(parent)
226 shell.displayhook.set_parent(parent)
223 shell.display_pub.set_parent(parent)
227 shell.display_pub.set_parent(parent)
224 sys.stdout.set_parent(parent)
228 sys.stdout.set_parent(parent)
225 sys.stderr.set_parent(parent)
229 sys.stderr.set_parent(parent)
226
230
227 # Re-broadcast our input for the benefit of listening clients, and
231 # Re-broadcast our input for the benefit of listening clients, and
228 # start computing output
232 # start computing output
229 if not silent:
233 if not silent:
230 self._publish_pyin(code, parent)
234 self._publish_pyin(code, parent)
231
235
232 reply_content = {}
236 reply_content = {}
233 try:
237 try:
234 if silent:
238 if silent:
235 # run_code uses 'exec' mode, so no displayhook will fire, and it
239 # run_code uses 'exec' mode, so no displayhook will fire, and it
236 # doesn't call logging or history manipulations. Print
240 # doesn't call logging or history manipulations. Print
237 # statements in that code will obviously still execute.
241 # statements in that code will obviously still execute.
238 shell.run_code(code)
242 shell.run_code(code)
239 else:
243 else:
240 # FIXME: the shell calls the exception handler itself.
244 # FIXME: the shell calls the exception handler itself.
241 shell.run_cell(code)
245 shell.run_cell(code)
242 except:
246 except:
243 status = u'error'
247 status = u'error'
244 # FIXME: this code right now isn't being used yet by default,
248 # FIXME: this code right now isn't being used yet by default,
245 # because the run_cell() call above directly fires off exception
249 # because the run_cell() call above directly fires off exception
246 # reporting. This code, therefore, is only active in the scenario
250 # reporting. This code, therefore, is only active in the scenario
247 # where runlines itself has an unhandled exception. We need to
251 # where runlines itself has an unhandled exception. We need to
248 # uniformize this, for all exception construction to come from a
252 # uniformize this, for all exception construction to come from a
249 # single location in the codbase.
253 # single location in the codbase.
250 etype, evalue, tb = sys.exc_info()
254 etype, evalue, tb = sys.exc_info()
251 tb_list = traceback.format_exception(etype, evalue, tb)
255 tb_list = traceback.format_exception(etype, evalue, tb)
252 reply_content.update(shell._showtraceback(etype, evalue, tb_list))
256 reply_content.update(shell._showtraceback(etype, evalue, tb_list))
253 else:
257 else:
254 status = u'ok'
258 status = u'ok'
255
259
256 reply_content[u'status'] = status
260 reply_content[u'status'] = status
257
261
258 # Return the execution counter so clients can display prompts
262 # Return the execution counter so clients can display prompts
259 reply_content['execution_count'] = shell.execution_count -1
263 reply_content['execution_count'] = shell.execution_count -1
260
264
261 # FIXME - fish exception info out of shell, possibly left there by
265 # FIXME - fish exception info out of shell, possibly left there by
262 # runlines. We'll need to clean up this logic later.
266 # runlines. We'll need to clean up this logic later.
263 if shell._reply_content is not None:
267 if shell._reply_content is not None:
264 reply_content.update(shell._reply_content)
268 reply_content.update(shell._reply_content)
265 # reset after use
269 # reset after use
266 shell._reply_content = None
270 shell._reply_content = None
267
271
268 # At this point, we can tell whether the main code execution succeeded
272 # At this point, we can tell whether the main code execution succeeded
269 # or not. If it did, we proceed to evaluate user_variables/expressions
273 # or not. If it did, we proceed to evaluate user_variables/expressions
270 if reply_content['status'] == 'ok':
274 if reply_content['status'] == 'ok':
271 reply_content[u'user_variables'] = \
275 reply_content[u'user_variables'] = \
272 shell.user_variables(content[u'user_variables'])
276 shell.user_variables(content[u'user_variables'])
273 reply_content[u'user_expressions'] = \
277 reply_content[u'user_expressions'] = \
274 shell.user_expressions(content[u'user_expressions'])
278 shell.user_expressions(content[u'user_expressions'])
275 else:
279 else:
276 # If there was an error, don't even try to compute variables or
280 # If there was an error, don't even try to compute variables or
277 # expressions
281 # expressions
278 reply_content[u'user_variables'] = {}
282 reply_content[u'user_variables'] = {}
279 reply_content[u'user_expressions'] = {}
283 reply_content[u'user_expressions'] = {}
280
284
281 # Payloads should be retrieved regardless of outcome, so we can both
285 # Payloads should be retrieved regardless of outcome, so we can both
282 # recover partial output (that could have been generated early in a
286 # recover partial output (that could have been generated early in a
283 # block, before an error) and clear the payload system always.
287 # block, before an error) and clear the payload system always.
284 reply_content[u'payload'] = shell.payload_manager.read_payload()
288 reply_content[u'payload'] = shell.payload_manager.read_payload()
285 # Be agressive about clearing the payload because we don't want
289 # Be agressive about clearing the payload because we don't want
286 # it to sit in memory until the next execute_request comes in.
290 # it to sit in memory until the next execute_request comes in.
287 shell.payload_manager.clear_payload()
291 shell.payload_manager.clear_payload()
288
292
289 # Flush output before sending the reply.
293 # Flush output before sending the reply.
290 sys.stdout.flush()
294 sys.stdout.flush()
291 sys.stderr.flush()
295 sys.stderr.flush()
292 # FIXME: on rare occasions, the flush doesn't seem to make it to the
296 # FIXME: on rare occasions, the flush doesn't seem to make it to the
293 # clients... This seems to mitigate the problem, but we definitely need
297 # clients... This seems to mitigate the problem, but we definitely need
294 # to better understand what's going on.
298 # to better understand what's going on.
295 if self._execute_sleep:
299 if self._execute_sleep:
296 time.sleep(self._execute_sleep)
300 time.sleep(self._execute_sleep)
297
301
298 # Send the reply.
302 # Send the reply.
299 reply_msg = self.session.send(self.shell_socket, u'execute_reply',
303 reply_msg = self.session.send(self.shell_socket, u'execute_reply',
300 reply_content, parent, ident=ident)
304 reply_content, parent, ident=ident)
301 self.log.debug(str(reply_msg))
305 self.log.debug(str(reply_msg))
302
306
303 if reply_msg['content']['status'] == u'error':
307 if reply_msg['content']['status'] == u'error':
304 self._abort_queue()
308 self._abort_queue()
305
309
306 status_msg = self.session.send(self.iopub_socket,
310 status_msg = self.session.send(self.iopub_socket,
307 u'status',
311 u'status',
308 {u'execution_state':u'idle'},
312 {u'execution_state':u'idle'},
309 parent=parent
313 parent=parent
310 )
314 )
311
315
312 def complete_request(self, ident, parent):
316 def complete_request(self, ident, parent):
313 txt, matches = self._complete(parent)
317 txt, matches = self._complete(parent)
314 matches = {'matches' : matches,
318 matches = {'matches' : matches,
315 'matched_text' : txt,
319 'matched_text' : txt,
316 'status' : 'ok'}
320 'status' : 'ok'}
317 completion_msg = self.session.send(self.shell_socket, 'complete_reply',
321 completion_msg = self.session.send(self.shell_socket, 'complete_reply',
318 matches, parent, ident)
322 matches, parent, ident)
319 self.log.debug(str(completion_msg))
323 self.log.debug(str(completion_msg))
320
324
321 def object_info_request(self, ident, parent):
325 def object_info_request(self, ident, parent):
322 object_info = self.shell.object_inspect(parent['content']['oname'])
326 object_info = self.shell.object_inspect(parent['content']['oname'])
323 # Before we send this object over, we scrub it for JSON usage
327 # Before we send this object over, we scrub it for JSON usage
324 oinfo = json_clean(object_info)
328 oinfo = json_clean(object_info)
325 msg = self.session.send(self.shell_socket, 'object_info_reply',
329 msg = self.session.send(self.shell_socket, 'object_info_reply',
326 oinfo, parent, ident)
330 oinfo, parent, ident)
327 self.log.debug(msg)
331 self.log.debug(msg)
328
332
329 def history_request(self, ident, parent):
333 def history_request(self, ident, parent):
330 # We need to pull these out, as passing **kwargs doesn't work with
334 # We need to pull these out, as passing **kwargs doesn't work with
331 # unicode keys before Python 2.6.5.
335 # unicode keys before Python 2.6.5.
332 hist_access_type = parent['content']['hist_access_type']
336 hist_access_type = parent['content']['hist_access_type']
333 raw = parent['content']['raw']
337 raw = parent['content']['raw']
334 output = parent['content']['output']
338 output = parent['content']['output']
335 if hist_access_type == 'tail':
339 if hist_access_type == 'tail':
336 n = parent['content']['n']
340 n = parent['content']['n']
337 hist = self.shell.history_manager.get_tail(n, raw=raw, output=output,
341 hist = self.shell.history_manager.get_tail(n, raw=raw, output=output,
338 include_latest=True)
342 include_latest=True)
339
343
340 elif hist_access_type == 'range':
344 elif hist_access_type == 'range':
341 session = parent['content']['session']
345 session = parent['content']['session']
342 start = parent['content']['start']
346 start = parent['content']['start']
343 stop = parent['content']['stop']
347 stop = parent['content']['stop']
344 hist = self.shell.history_manager.get_range(session, start, stop,
348 hist = self.shell.history_manager.get_range(session, start, stop,
345 raw=raw, output=output)
349 raw=raw, output=output)
346
350
347 elif hist_access_type == 'search':
351 elif hist_access_type == 'search':
348 pattern = parent['content']['pattern']
352 pattern = parent['content']['pattern']
349 hist = self.shell.history_manager.search(pattern, raw=raw, output=output)
353 hist = self.shell.history_manager.search(pattern, raw=raw, output=output)
350
354
351 else:
355 else:
352 hist = []
356 hist = []
353 content = {'history' : list(hist)}
357 content = {'history' : list(hist)}
354 msg = self.session.send(self.shell_socket, 'history_reply',
358 msg = self.session.send(self.shell_socket, 'history_reply',
355 content, parent, ident)
359 content, parent, ident)
356 self.log.debug(str(msg))
360 self.log.debug(str(msg))
357
361
358 def connect_request(self, ident, parent):
362 def connect_request(self, ident, parent):
359 if self._recorded_ports is not None:
363 if self._recorded_ports is not None:
360 content = self._recorded_ports.copy()
364 content = self._recorded_ports.copy()
361 else:
365 else:
362 content = {}
366 content = {}
363 msg = self.session.send(self.shell_socket, 'connect_reply',
367 msg = self.session.send(self.shell_socket, 'connect_reply',
364 content, parent, ident)
368 content, parent, ident)
365 self.log.debug(msg)
369 self.log.debug(msg)
366
370
367 def shutdown_request(self, ident, parent):
371 def shutdown_request(self, ident, parent):
368 self.shell.exit_now = True
372 self.shell.exit_now = True
369 self._shutdown_message = self.session.msg(u'shutdown_reply', parent['content'], parent)
373 self._shutdown_message = self.session.msg(u'shutdown_reply', parent['content'], parent)
370 sys.exit(0)
374 sys.exit(0)
371
375
372 #---------------------------------------------------------------------------
376 #---------------------------------------------------------------------------
373 # Protected interface
377 # Protected interface
374 #---------------------------------------------------------------------------
378 #---------------------------------------------------------------------------
375
379
376 def _abort_queue(self):
380 def _abort_queue(self):
377 while True:
381 while True:
378 ident,msg = self.session.recv(self.shell_socket, zmq.NOBLOCK)
382 try:
383 ident,msg = self.session.recv(self.shell_socket, zmq.NOBLOCK)
384 except Exception:
385 self.log.warn("Invalid Message:", exc_info=True)
386 continue
379 if msg is None:
387 if msg is None:
380 break
388 break
381 else:
389 else:
382 assert ident is not None, \
390 assert ident is not None, \
383 "Unexpected missing message part."
391 "Unexpected missing message part."
384
392
385 self.log.debug("Aborting:\n"+str(Message(msg)))
393 self.log.debug("Aborting:\n"+str(Message(msg)))
386 msg_type = msg['header']['msg_type']
394 msg_type = msg['header']['msg_type']
387 reply_type = msg_type.split('_')[0] + '_reply'
395 reply_type = msg_type.split('_')[0] + '_reply'
388 reply_msg = self.session.send(self.shell_socket, reply_type,
396 reply_msg = self.session.send(self.shell_socket, reply_type,
389 {'status' : 'aborted'}, msg, ident=ident)
397 {'status' : 'aborted'}, msg, ident=ident)
390 self.log.debug(reply_msg)
398 self.log.debug(reply_msg)
391 # We need to wait a bit for requests to come in. This can probably
399 # We need to wait a bit for requests to come in. This can probably
392 # be set shorter for true asynchronous clients.
400 # be set shorter for true asynchronous clients.
393 time.sleep(0.1)
401 time.sleep(0.1)
394
402
395 def _raw_input(self, prompt, ident, parent):
403 def _raw_input(self, prompt, ident, parent):
396 # Flush output before making the request.
404 # Flush output before making the request.
397 sys.stderr.flush()
405 sys.stderr.flush()
398 sys.stdout.flush()
406 sys.stdout.flush()
399
407
400 # Send the input request.
408 # Send the input request.
401 content = dict(prompt=prompt)
409 content = dict(prompt=prompt)
402 msg = self.session.send(self.stdin_socket, u'input_request', content, parent)
410 msg = self.session.send(self.stdin_socket, u'input_request', content, parent)
403
411
404 # Await a response.
412 # Await a response.
405 ident, reply = self.session.recv(self.stdin_socket, 0)
413 while True:
414 try:
415 ident, reply = self.session.recv(self.stdin_socket, 0)
416 except Exception:
417 self.log.warn("Invalid Message:", exc_info=True)
418 else:
419 break
406 try:
420 try:
407 value = reply['content']['value']
421 value = reply['content']['value']
408 except:
422 except:
409 self.log.error("Got bad raw_input reply: ")
423 self.log.error("Got bad raw_input reply: ")
410 self.log.error(str(Message(parent)))
424 self.log.error(str(Message(parent)))
411 value = ''
425 value = ''
412 return value
426 return value
413
427
414 def _complete(self, msg):
428 def _complete(self, msg):
415 c = msg['content']
429 c = msg['content']
416 try:
430 try:
417 cpos = int(c['cursor_pos'])
431 cpos = int(c['cursor_pos'])
418 except:
432 except:
419 # If we don't get something that we can convert to an integer, at
433 # If we don't get something that we can convert to an integer, at
420 # least attempt the completion guessing the cursor is at the end of
434 # least attempt the completion guessing the cursor is at the end of
421 # the text, if there's any, and otherwise of the line
435 # the text, if there's any, and otherwise of the line
422 cpos = len(c['text'])
436 cpos = len(c['text'])
423 if cpos==0:
437 if cpos==0:
424 cpos = len(c['line'])
438 cpos = len(c['line'])
425 return self.shell.complete(c['text'], c['line'], cpos)
439 return self.shell.complete(c['text'], c['line'], cpos)
426
440
427 def _object_info(self, context):
441 def _object_info(self, context):
428 symbol, leftover = self._symbol_from_context(context)
442 symbol, leftover = self._symbol_from_context(context)
429 if symbol is not None and not leftover:
443 if symbol is not None and not leftover:
430 doc = getattr(symbol, '__doc__', '')
444 doc = getattr(symbol, '__doc__', '')
431 else:
445 else:
432 doc = ''
446 doc = ''
433 object_info = dict(docstring = doc)
447 object_info = dict(docstring = doc)
434 return object_info
448 return object_info
435
449
436 def _symbol_from_context(self, context):
450 def _symbol_from_context(self, context):
437 if not context:
451 if not context:
438 return None, context
452 return None, context
439
453
440 base_symbol_string = context[0]
454 base_symbol_string = context[0]
441 symbol = self.shell.user_ns.get(base_symbol_string, None)
455 symbol = self.shell.user_ns.get(base_symbol_string, None)
442 if symbol is None:
456 if symbol is None:
443 symbol = __builtin__.__dict__.get(base_symbol_string, None)
457 symbol = __builtin__.__dict__.get(base_symbol_string, None)
444 if symbol is None:
458 if symbol is None:
445 return None, context
459 return None, context
446
460
447 context = context[1:]
461 context = context[1:]
448 for i, name in enumerate(context):
462 for i, name in enumerate(context):
449 new_symbol = getattr(symbol, name, None)
463 new_symbol = getattr(symbol, name, None)
450 if new_symbol is None:
464 if new_symbol is None:
451 return symbol, context[i:]
465 return symbol, context[i:]
452 else:
466 else:
453 symbol = new_symbol
467 symbol = new_symbol
454
468
455 return symbol, []
469 return symbol, []
456
470
457 def _at_shutdown(self):
471 def _at_shutdown(self):
458 """Actions taken at shutdown by the kernel, called by python's atexit.
472 """Actions taken at shutdown by the kernel, called by python's atexit.
459 """
473 """
460 # io.rprint("Kernel at_shutdown") # dbg
474 # io.rprint("Kernel at_shutdown") # dbg
461 if self._shutdown_message is not None:
475 if self._shutdown_message is not None:
462 self.session.send(self.shell_socket, self._shutdown_message)
476 self.session.send(self.shell_socket, self._shutdown_message)
463 self.session.send(self.iopub_socket, self._shutdown_message)
477 self.session.send(self.iopub_socket, self._shutdown_message)
464 self.log.debug(str(self._shutdown_message))
478 self.log.debug(str(self._shutdown_message))
465 # A very short sleep to give zmq time to flush its message buffers
479 # A very short sleep to give zmq time to flush its message buffers
466 # before Python truly shuts down.
480 # before Python truly shuts down.
467 time.sleep(0.01)
481 time.sleep(0.01)
468
482
469
483
470 class QtKernel(Kernel):
484 class QtKernel(Kernel):
471 """A Kernel subclass with Qt support."""
485 """A Kernel subclass with Qt support."""
472
486
473 def start(self):
487 def start(self):
474 """Start a kernel with QtPy4 event loop integration."""
488 """Start a kernel with QtPy4 event loop integration."""
475
489
476 from IPython.external.qt_for_kernel import QtCore
490 from IPython.external.qt_for_kernel import QtCore
477 from IPython.lib.guisupport import get_app_qt4, start_event_loop_qt4
491 from IPython.lib.guisupport import get_app_qt4, start_event_loop_qt4
478
492
479 self.app = get_app_qt4([" "])
493 self.app = get_app_qt4([" "])
480 self.app.setQuitOnLastWindowClosed(False)
494 self.app.setQuitOnLastWindowClosed(False)
481 self.timer = QtCore.QTimer()
495 self.timer = QtCore.QTimer()
482 self.timer.timeout.connect(self.do_one_iteration)
496 self.timer.timeout.connect(self.do_one_iteration)
483 # Units for the timer are in milliseconds
497 # Units for the timer are in milliseconds
484 self.timer.start(1000*self._poll_interval)
498 self.timer.start(1000*self._poll_interval)
485 start_event_loop_qt4(self.app)
499 start_event_loop_qt4(self.app)
486
500
487
501
488 class WxKernel(Kernel):
502 class WxKernel(Kernel):
489 """A Kernel subclass with Wx support."""
503 """A Kernel subclass with Wx support."""
490
504
491 def start(self):
505 def start(self):
492 """Start a kernel with wx event loop support."""
506 """Start a kernel with wx event loop support."""
493
507
494 import wx
508 import wx
495 from IPython.lib.guisupport import start_event_loop_wx
509 from IPython.lib.guisupport import start_event_loop_wx
496
510
497 doi = self.do_one_iteration
511 doi = self.do_one_iteration
498 # Wx uses milliseconds
512 # Wx uses milliseconds
499 poll_interval = int(1000*self._poll_interval)
513 poll_interval = int(1000*self._poll_interval)
500
514
501 # We have to put the wx.Timer in a wx.Frame for it to fire properly.
515 # We have to put the wx.Timer in a wx.Frame for it to fire properly.
502 # We make the Frame hidden when we create it in the main app below.
516 # We make the Frame hidden when we create it in the main app below.
503 class TimerFrame(wx.Frame):
517 class TimerFrame(wx.Frame):
504 def __init__(self, func):
518 def __init__(self, func):
505 wx.Frame.__init__(self, None, -1)
519 wx.Frame.__init__(self, None, -1)
506 self.timer = wx.Timer(self)
520 self.timer = wx.Timer(self)
507 # Units for the timer are in milliseconds
521 # Units for the timer are in milliseconds
508 self.timer.Start(poll_interval)
522 self.timer.Start(poll_interval)
509 self.Bind(wx.EVT_TIMER, self.on_timer)
523 self.Bind(wx.EVT_TIMER, self.on_timer)
510 self.func = func
524 self.func = func
511
525
512 def on_timer(self, event):
526 def on_timer(self, event):
513 self.func()
527 self.func()
514
528
515 # We need a custom wx.App to create our Frame subclass that has the
529 # We need a custom wx.App to create our Frame subclass that has the
516 # wx.Timer to drive the ZMQ event loop.
530 # wx.Timer to drive the ZMQ event loop.
517 class IPWxApp(wx.App):
531 class IPWxApp(wx.App):
518 def OnInit(self):
532 def OnInit(self):
519 self.frame = TimerFrame(doi)
533 self.frame = TimerFrame(doi)
520 self.frame.Show(False)
534 self.frame.Show(False)
521 return True
535 return True
522
536
523 # The redirect=False here makes sure that wx doesn't replace
537 # The redirect=False here makes sure that wx doesn't replace
524 # sys.stdout/stderr with its own classes.
538 # sys.stdout/stderr with its own classes.
525 self.app = IPWxApp(redirect=False)
539 self.app = IPWxApp(redirect=False)
526 start_event_loop_wx(self.app)
540 start_event_loop_wx(self.app)
527
541
528
542
529 class TkKernel(Kernel):
543 class TkKernel(Kernel):
530 """A Kernel subclass with Tk support."""
544 """A Kernel subclass with Tk support."""
531
545
532 def start(self):
546 def start(self):
533 """Start a Tk enabled event loop."""
547 """Start a Tk enabled event loop."""
534
548
535 import Tkinter
549 import Tkinter
536 doi = self.do_one_iteration
550 doi = self.do_one_iteration
537 # Tk uses milliseconds
551 # Tk uses milliseconds
538 poll_interval = int(1000*self._poll_interval)
552 poll_interval = int(1000*self._poll_interval)
539 # For Tkinter, we create a Tk object and call its withdraw method.
553 # For Tkinter, we create a Tk object and call its withdraw method.
540 class Timer(object):
554 class Timer(object):
541 def __init__(self, func):
555 def __init__(self, func):
542 self.app = Tkinter.Tk()
556 self.app = Tkinter.Tk()
543 self.app.withdraw()
557 self.app.withdraw()
544 self.func = func
558 self.func = func
545
559
546 def on_timer(self):
560 def on_timer(self):
547 self.func()
561 self.func()
548 self.app.after(poll_interval, self.on_timer)
562 self.app.after(poll_interval, self.on_timer)
549
563
550 def start(self):
564 def start(self):
551 self.on_timer() # Call it once to get things going.
565 self.on_timer() # Call it once to get things going.
552 self.app.mainloop()
566 self.app.mainloop()
553
567
554 self.timer = Timer(doi)
568 self.timer = Timer(doi)
555 self.timer.start()
569 self.timer.start()
556
570
557
571
558 class GTKKernel(Kernel):
572 class GTKKernel(Kernel):
559 """A Kernel subclass with GTK support."""
573 """A Kernel subclass with GTK support."""
560
574
561 def start(self):
575 def start(self):
562 """Start the kernel, coordinating with the GTK event loop"""
576 """Start the kernel, coordinating with the GTK event loop"""
563 from .gui.gtkembed import GTKEmbed
577 from .gui.gtkembed import GTKEmbed
564
578
565 gtk_kernel = GTKEmbed(self)
579 gtk_kernel = GTKEmbed(self)
566 gtk_kernel.start()
580 gtk_kernel.start()
567
581
568
582
569 #-----------------------------------------------------------------------------
583 #-----------------------------------------------------------------------------
570 # Aliases and Flags for the IPKernelApp
584 # Aliases and Flags for the IPKernelApp
571 #-----------------------------------------------------------------------------
585 #-----------------------------------------------------------------------------
572
586
573 flags = dict(kernel_flags)
587 flags = dict(kernel_flags)
574 flags.update(shell_flags)
588 flags.update(shell_flags)
575
589
576 addflag = lambda *args: flags.update(boolean_flag(*args))
590 addflag = lambda *args: flags.update(boolean_flag(*args))
577
591
578 flags['pylab'] = (
592 flags['pylab'] = (
579 {'IPKernelApp' : {'pylab' : 'auto'}},
593 {'IPKernelApp' : {'pylab' : 'auto'}},
580 """Pre-load matplotlib and numpy for interactive use with
594 """Pre-load matplotlib and numpy for interactive use with
581 the default matplotlib backend."""
595 the default matplotlib backend."""
582 )
596 )
583
597
584 aliases = dict(kernel_aliases)
598 aliases = dict(kernel_aliases)
585 aliases.update(shell_aliases)
599 aliases.update(shell_aliases)
586
600
587 # it's possible we don't want short aliases for *all* of these:
601 # it's possible we don't want short aliases for *all* of these:
588 aliases.update(dict(
602 aliases.update(dict(
589 pylab='IPKernelApp.pylab',
603 pylab='IPKernelApp.pylab',
590 ))
604 ))
591
605
592 #-----------------------------------------------------------------------------
606 #-----------------------------------------------------------------------------
593 # The IPKernelApp class
607 # The IPKernelApp class
594 #-----------------------------------------------------------------------------
608 #-----------------------------------------------------------------------------
595
609
596 class IPKernelApp(KernelApp, InteractiveShellApp):
610 class IPKernelApp(KernelApp, InteractiveShellApp):
597 name = 'ipkernel'
611 name = 'ipkernel'
598
612
599 aliases = Dict(aliases)
613 aliases = Dict(aliases)
600 flags = Dict(flags)
614 flags = Dict(flags)
601 classes = [Kernel, ZMQInteractiveShell, ProfileDir, Session]
615 classes = [Kernel, ZMQInteractiveShell, ProfileDir, Session]
602 # configurables
616 # configurables
603 pylab = CaselessStrEnum(['tk', 'qt', 'wx', 'gtk', 'osx', 'inline', 'auto'],
617 pylab = CaselessStrEnum(['tk', 'qt', 'wx', 'gtk', 'osx', 'inline', 'auto'],
604 config=True,
618 config=True,
605 help="""Pre-load matplotlib and numpy for interactive use,
619 help="""Pre-load matplotlib and numpy for interactive use,
606 selecting a particular matplotlib backend and loop integration.
620 selecting a particular matplotlib backend and loop integration.
607 """
621 """
608 )
622 )
609 pylab_import_all = Bool(True, config=True,
623 pylab_import_all = Bool(True, config=True,
610 help="""If true, an 'import *' is done from numpy and pylab,
624 help="""If true, an 'import *' is done from numpy and pylab,
611 when using pylab"""
625 when using pylab"""
612 )
626 )
613 def initialize(self, argv=None):
627 def initialize(self, argv=None):
614 super(IPKernelApp, self).initialize(argv)
628 super(IPKernelApp, self).initialize(argv)
615 self.init_shell()
629 self.init_shell()
616 self.init_extensions()
630 self.init_extensions()
617 self.init_code()
631 self.init_code()
618
632
619 def init_kernel(self):
633 def init_kernel(self):
620 kernel_factory = Kernel
634 kernel_factory = Kernel
621
635
622 kernel_map = {
636 kernel_map = {
623 'qt' : QtKernel,
637 'qt' : QtKernel,
624 'qt4': QtKernel,
638 'qt4': QtKernel,
625 'inline': Kernel,
639 'inline': Kernel,
626 'osx': TkKernel,
640 'osx': TkKernel,
627 'wx' : WxKernel,
641 'wx' : WxKernel,
628 'tk' : TkKernel,
642 'tk' : TkKernel,
629 'gtk': GTKKernel,
643 'gtk': GTKKernel,
630 }
644 }
631
645
632 if self.pylab:
646 if self.pylab:
633 key = None if self.pylab == 'auto' else self.pylab
647 key = None if self.pylab == 'auto' else self.pylab
634 gui, backend = pylabtools.find_gui_and_backend(key)
648 gui, backend = pylabtools.find_gui_and_backend(key)
635 kernel_factory = kernel_map.get(gui)
649 kernel_factory = kernel_map.get(gui)
636 if kernel_factory is None:
650 if kernel_factory is None:
637 raise ValueError('GUI is not supported: %r' % gui)
651 raise ValueError('GUI is not supported: %r' % gui)
638 pylabtools.activate_matplotlib(backend)
652 pylabtools.activate_matplotlib(backend)
639
653
640 kernel = kernel_factory(config=self.config, session=self.session,
654 kernel = kernel_factory(config=self.config, session=self.session,
641 shell_socket=self.shell_socket,
655 shell_socket=self.shell_socket,
642 iopub_socket=self.iopub_socket,
656 iopub_socket=self.iopub_socket,
643 stdin_socket=self.stdin_socket,
657 stdin_socket=self.stdin_socket,
644 log=self.log
658 log=self.log
645 )
659 )
646 self.kernel = kernel
660 self.kernel = kernel
647 kernel.record_ports(self.ports)
661 kernel.record_ports(self.ports)
648
662
649 if self.pylab:
663 if self.pylab:
650 import_all = self.pylab_import_all
664 import_all = self.pylab_import_all
651 pylabtools.import_pylab(kernel.shell.user_ns, backend, import_all,
665 pylabtools.import_pylab(kernel.shell.user_ns, backend, import_all,
652 shell=kernel.shell)
666 shell=kernel.shell)
653
667
654 def init_shell(self):
668 def init_shell(self):
655 self.shell = self.kernel.shell
669 self.shell = self.kernel.shell
656
670
657
671
658 #-----------------------------------------------------------------------------
672 #-----------------------------------------------------------------------------
659 # Kernel main and launch functions
673 # Kernel main and launch functions
660 #-----------------------------------------------------------------------------
674 #-----------------------------------------------------------------------------
661
675
662 def launch_kernel(*args, **kwargs):
676 def launch_kernel(*args, **kwargs):
663 """Launches a localhost IPython kernel, binding to the specified ports.
677 """Launches a localhost IPython kernel, binding to the specified ports.
664
678
665 This function simply calls entry_point.base_launch_kernel with the right first
679 This function simply calls entry_point.base_launch_kernel with the right first
666 command to start an ipkernel. See base_launch_kernel for arguments.
680 command to start an ipkernel. See base_launch_kernel for arguments.
667
681
668 Returns
682 Returns
669 -------
683 -------
670 A tuple of form:
684 A tuple of form:
671 (kernel_process, shell_port, iopub_port, stdin_port, hb_port)
685 (kernel_process, shell_port, iopub_port, stdin_port, hb_port)
672 where kernel_process is a Popen object and the ports are integers.
686 where kernel_process is a Popen object and the ports are integers.
673 """
687 """
674 return base_launch_kernel('from IPython.zmq.ipkernel import main; main()',
688 return base_launch_kernel('from IPython.zmq.ipkernel import main; main()',
675 *args, **kwargs)
689 *args, **kwargs)
676
690
677
691
678 def main():
692 def main():
679 """Run an IPKernel as an application"""
693 """Run an IPKernel as an application"""
680 app = IPKernelApp.instance()
694 app = IPKernelApp.instance()
681 app.initialize()
695 app.initialize()
682 app.start()
696 app.start()
683
697
684
698
685 if __name__ == '__main__':
699 if __name__ == '__main__':
686 main()
700 main()
@@ -1,697 +1,698 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 """Session object for building, serializing, sending, and receiving messages in
2 """Session object for building, serializing, sending, and receiving messages in
3 IPython. The Session object supports serialization, HMAC signatures, and
3 IPython. The Session object supports serialization, HMAC signatures, and
4 metadata on messages.
4 metadata on messages.
5
5
6 Also defined here are utilities for working with Sessions:
6 Also defined here are utilities for working with Sessions:
7 * A SessionFactory to be used as a base class for configurables that work with
7 * A SessionFactory to be used as a base class for configurables that work with
8 Sessions.
8 Sessions.
9 * A Message object for convenience that allows attribute-access to the msg dict.
9 * A Message object for convenience that allows attribute-access to the msg dict.
10
10
11 Authors:
11 Authors:
12
12
13 * Min RK
13 * Min RK
14 * Brian Granger
14 * Brian Granger
15 * Fernando Perez
15 * Fernando Perez
16 """
16 """
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18 # Copyright (C) 2010-2011 The IPython Development Team
18 # Copyright (C) 2010-2011 The IPython Development Team
19 #
19 #
20 # Distributed under the terms of the BSD License. The full license is in
20 # Distributed under the terms of the BSD License. The full license is in
21 # the file COPYING, distributed as part of this software.
21 # the file COPYING, distributed as part of this software.
22 #-----------------------------------------------------------------------------
22 #-----------------------------------------------------------------------------
23
23
24 #-----------------------------------------------------------------------------
24 #-----------------------------------------------------------------------------
25 # Imports
25 # Imports
26 #-----------------------------------------------------------------------------
26 #-----------------------------------------------------------------------------
27
27
28 import hmac
28 import hmac
29 import logging
29 import logging
30 import os
30 import os
31 import pprint
31 import pprint
32 import uuid
32 import uuid
33 from datetime import datetime
33 from datetime import datetime
34
34
35 try:
35 try:
36 import cPickle
36 import cPickle
37 pickle = cPickle
37 pickle = cPickle
38 except:
38 except:
39 cPickle = None
39 cPickle = None
40 import pickle
40 import pickle
41
41
42 import zmq
42 import zmq
43 from zmq.utils import jsonapi
43 from zmq.utils import jsonapi
44 from zmq.eventloop.ioloop import IOLoop
44 from zmq.eventloop.ioloop import IOLoop
45 from zmq.eventloop.zmqstream import ZMQStream
45 from zmq.eventloop.zmqstream import ZMQStream
46
46
47 from IPython.config.configurable import Configurable, LoggingConfigurable
47 from IPython.config.configurable import Configurable, LoggingConfigurable
48 from IPython.utils.importstring import import_item
48 from IPython.utils.importstring import import_item
49 from IPython.utils.jsonutil import extract_dates, squash_dates, date_default
49 from IPython.utils.jsonutil import extract_dates, squash_dates, date_default
50 from IPython.utils.traitlets import (CBytes, Unicode, Bool, Any, Instance, Set,
50 from IPython.utils.traitlets import (CBytes, Unicode, Bool, Any, Instance, Set,
51 DottedObjectName)
51 DottedObjectName)
52
52
53 #-----------------------------------------------------------------------------
53 #-----------------------------------------------------------------------------
54 # utility functions
54 # utility functions
55 #-----------------------------------------------------------------------------
55 #-----------------------------------------------------------------------------
56
56
57 def squash_unicode(obj):
57 def squash_unicode(obj):
58 """coerce unicode back to bytestrings."""
58 """coerce unicode back to bytestrings."""
59 if isinstance(obj,dict):
59 if isinstance(obj,dict):
60 for key in obj.keys():
60 for key in obj.keys():
61 obj[key] = squash_unicode(obj[key])
61 obj[key] = squash_unicode(obj[key])
62 if isinstance(key, unicode):
62 if isinstance(key, unicode):
63 obj[squash_unicode(key)] = obj.pop(key)
63 obj[squash_unicode(key)] = obj.pop(key)
64 elif isinstance(obj, list):
64 elif isinstance(obj, list):
65 for i,v in enumerate(obj):
65 for i,v in enumerate(obj):
66 obj[i] = squash_unicode(v)
66 obj[i] = squash_unicode(v)
67 elif isinstance(obj, unicode):
67 elif isinstance(obj, unicode):
68 obj = obj.encode('utf8')
68 obj = obj.encode('utf8')
69 return obj
69 return obj
70
70
71 #-----------------------------------------------------------------------------
71 #-----------------------------------------------------------------------------
72 # globals and defaults
72 # globals and defaults
73 #-----------------------------------------------------------------------------
73 #-----------------------------------------------------------------------------
74 key = 'on_unknown' if jsonapi.jsonmod.__name__ == 'jsonlib' else 'default'
74 key = 'on_unknown' if jsonapi.jsonmod.__name__ == 'jsonlib' else 'default'
75 json_packer = lambda obj: jsonapi.dumps(obj, **{key:date_default})
75 json_packer = lambda obj: jsonapi.dumps(obj, **{key:date_default})
76 json_unpacker = lambda s: extract_dates(jsonapi.loads(s))
76 json_unpacker = lambda s: extract_dates(jsonapi.loads(s))
77
77
78 pickle_packer = lambda o: pickle.dumps(o,-1)
78 pickle_packer = lambda o: pickle.dumps(o,-1)
79 pickle_unpacker = pickle.loads
79 pickle_unpacker = pickle.loads
80
80
81 default_packer = json_packer
81 default_packer = json_packer
82 default_unpacker = json_unpacker
82 default_unpacker = json_unpacker
83
83
84
84
85 DELIM=b"<IDS|MSG>"
85 DELIM=b"<IDS|MSG>"
86
86
87 #-----------------------------------------------------------------------------
87 #-----------------------------------------------------------------------------
88 # Classes
88 # Classes
89 #-----------------------------------------------------------------------------
89 #-----------------------------------------------------------------------------
90
90
91 class SessionFactory(LoggingConfigurable):
91 class SessionFactory(LoggingConfigurable):
92 """The Base class for configurables that have a Session, Context, logger,
92 """The Base class for configurables that have a Session, Context, logger,
93 and IOLoop.
93 and IOLoop.
94 """
94 """
95
95
96 logname = Unicode('')
96 logname = Unicode('')
97 def _logname_changed(self, name, old, new):
97 def _logname_changed(self, name, old, new):
98 self.log = logging.getLogger(new)
98 self.log = logging.getLogger(new)
99
99
100 # not configurable:
100 # not configurable:
101 context = Instance('zmq.Context')
101 context = Instance('zmq.Context')
102 def _context_default(self):
102 def _context_default(self):
103 return zmq.Context.instance()
103 return zmq.Context.instance()
104
104
105 session = Instance('IPython.zmq.session.Session')
105 session = Instance('IPython.zmq.session.Session')
106
106
107 loop = Instance('zmq.eventloop.ioloop.IOLoop', allow_none=False)
107 loop = Instance('zmq.eventloop.ioloop.IOLoop', allow_none=False)
108 def _loop_default(self):
108 def _loop_default(self):
109 return IOLoop.instance()
109 return IOLoop.instance()
110
110
111 def __init__(self, **kwargs):
111 def __init__(self, **kwargs):
112 super(SessionFactory, self).__init__(**kwargs)
112 super(SessionFactory, self).__init__(**kwargs)
113
113
114 if self.session is None:
114 if self.session is None:
115 # construct the session
115 # construct the session
116 self.session = Session(**kwargs)
116 self.session = Session(**kwargs)
117
117
118
118
119 class Message(object):
119 class Message(object):
120 """A simple message object that maps dict keys to attributes.
120 """A simple message object that maps dict keys to attributes.
121
121
122 A Message can be created from a dict and a dict from a Message instance
122 A Message can be created from a dict and a dict from a Message instance
123 simply by calling dict(msg_obj)."""
123 simply by calling dict(msg_obj)."""
124
124
125 def __init__(self, msg_dict):
125 def __init__(self, msg_dict):
126 dct = self.__dict__
126 dct = self.__dict__
127 for k, v in dict(msg_dict).iteritems():
127 for k, v in dict(msg_dict).iteritems():
128 if isinstance(v, dict):
128 if isinstance(v, dict):
129 v = Message(v)
129 v = Message(v)
130 dct[k] = v
130 dct[k] = v
131
131
132 # Having this iterator lets dict(msg_obj) work out of the box.
132 # Having this iterator lets dict(msg_obj) work out of the box.
133 def __iter__(self):
133 def __iter__(self):
134 return iter(self.__dict__.iteritems())
134 return iter(self.__dict__.iteritems())
135
135
136 def __repr__(self):
136 def __repr__(self):
137 return repr(self.__dict__)
137 return repr(self.__dict__)
138
138
139 def __str__(self):
139 def __str__(self):
140 return pprint.pformat(self.__dict__)
140 return pprint.pformat(self.__dict__)
141
141
142 def __contains__(self, k):
142 def __contains__(self, k):
143 return k in self.__dict__
143 return k in self.__dict__
144
144
145 def __getitem__(self, k):
145 def __getitem__(self, k):
146 return self.__dict__[k]
146 return self.__dict__[k]
147
147
148
148
149 def msg_header(msg_id, msg_type, username, session):
149 def msg_header(msg_id, msg_type, username, session):
150 date = datetime.now()
150 date = datetime.now()
151 return locals()
151 return locals()
152
152
153 def extract_header(msg_or_header):
153 def extract_header(msg_or_header):
154 """Given a message or header, return the header."""
154 """Given a message or header, return the header."""
155 if not msg_or_header:
155 if not msg_or_header:
156 return {}
156 return {}
157 try:
157 try:
158 # See if msg_or_header is the entire message.
158 # See if msg_or_header is the entire message.
159 h = msg_or_header['header']
159 h = msg_or_header['header']
160 except KeyError:
160 except KeyError:
161 try:
161 try:
162 # See if msg_or_header is just the header
162 # See if msg_or_header is just the header
163 h = msg_or_header['msg_id']
163 h = msg_or_header['msg_id']
164 except KeyError:
164 except KeyError:
165 raise
165 raise
166 else:
166 else:
167 h = msg_or_header
167 h = msg_or_header
168 if not isinstance(h, dict):
168 if not isinstance(h, dict):
169 h = dict(h)
169 h = dict(h)
170 return h
170 return h
171
171
172 class Session(Configurable):
172 class Session(Configurable):
173 """Object for handling serialization and sending of messages.
173 """Object for handling serialization and sending of messages.
174
174
175 The Session object handles building messages and sending them
175 The Session object handles building messages and sending them
176 with ZMQ sockets or ZMQStream objects. Objects can communicate with each
176 with ZMQ sockets or ZMQStream objects. Objects can communicate with each
177 other over the network via Session objects, and only need to work with the
177 other over the network via Session objects, and only need to work with the
178 dict-based IPython message spec. The Session will handle
178 dict-based IPython message spec. The Session will handle
179 serialization/deserialization, security, and metadata.
179 serialization/deserialization, security, and metadata.
180
180
181 Sessions support configurable serialiization via packer/unpacker traits,
181 Sessions support configurable serialiization via packer/unpacker traits,
182 and signing with HMAC digests via the key/keyfile traits.
182 and signing with HMAC digests via the key/keyfile traits.
183
183
184 Parameters
184 Parameters
185 ----------
185 ----------
186
186
187 debug : bool
187 debug : bool
188 whether to trigger extra debugging statements
188 whether to trigger extra debugging statements
189 packer/unpacker : str : 'json', 'pickle' or import_string
189 packer/unpacker : str : 'json', 'pickle' or import_string
190 importstrings for methods to serialize message parts. If just
190 importstrings for methods to serialize message parts. If just
191 'json' or 'pickle', predefined JSON and pickle packers will be used.
191 'json' or 'pickle', predefined JSON and pickle packers will be used.
192 Otherwise, the entire importstring must be used.
192 Otherwise, the entire importstring must be used.
193
193
194 The functions must accept at least valid JSON input, and output *bytes*.
194 The functions must accept at least valid JSON input, and output *bytes*.
195
195
196 For example, to use msgpack:
196 For example, to use msgpack:
197 packer = 'msgpack.packb', unpacker='msgpack.unpackb'
197 packer = 'msgpack.packb', unpacker='msgpack.unpackb'
198 pack/unpack : callables
198 pack/unpack : callables
199 You can also set the pack/unpack callables for serialization directly.
199 You can also set the pack/unpack callables for serialization directly.
200 session : bytes
200 session : bytes
201 the ID of this Session object. The default is to generate a new UUID.
201 the ID of this Session object. The default is to generate a new UUID.
202 username : unicode
202 username : unicode
203 username added to message headers. The default is to ask the OS.
203 username added to message headers. The default is to ask the OS.
204 key : bytes
204 key : bytes
205 The key used to initialize an HMAC signature. If unset, messages
205 The key used to initialize an HMAC signature. If unset, messages
206 will not be signed or checked.
206 will not be signed or checked.
207 keyfile : filepath
207 keyfile : filepath
208 The file containing a key. If this is set, `key` will be initialized
208 The file containing a key. If this is set, `key` will be initialized
209 to the contents of the file.
209 to the contents of the file.
210
210
211 """
211 """
212
212
213 debug=Bool(False, config=True, help="""Debug output in the Session""")
213 debug=Bool(False, config=True, help="""Debug output in the Session""")
214
214
215 packer = DottedObjectName('json',config=True,
215 packer = DottedObjectName('json',config=True,
216 help="""The name of the packer for serializing messages.
216 help="""The name of the packer for serializing messages.
217 Should be one of 'json', 'pickle', or an import name
217 Should be one of 'json', 'pickle', or an import name
218 for a custom callable serializer.""")
218 for a custom callable serializer.""")
219 def _packer_changed(self, name, old, new):
219 def _packer_changed(self, name, old, new):
220 if new.lower() == 'json':
220 if new.lower() == 'json':
221 self.pack = json_packer
221 self.pack = json_packer
222 self.unpack = json_unpacker
222 self.unpack = json_unpacker
223 elif new.lower() == 'pickle':
223 elif new.lower() == 'pickle':
224 self.pack = pickle_packer
224 self.pack = pickle_packer
225 self.unpack = pickle_unpacker
225 self.unpack = pickle_unpacker
226 else:
226 else:
227 self.pack = import_item(str(new))
227 self.pack = import_item(str(new))
228
228
229 unpacker = DottedObjectName('json', config=True,
229 unpacker = DottedObjectName('json', config=True,
230 help="""The name of the unpacker for unserializing messages.
230 help="""The name of the unpacker for unserializing messages.
231 Only used with custom functions for `packer`.""")
231 Only used with custom functions for `packer`.""")
232 def _unpacker_changed(self, name, old, new):
232 def _unpacker_changed(self, name, old, new):
233 if new.lower() == 'json':
233 if new.lower() == 'json':
234 self.pack = json_packer
234 self.pack = json_packer
235 self.unpack = json_unpacker
235 self.unpack = json_unpacker
236 elif new.lower() == 'pickle':
236 elif new.lower() == 'pickle':
237 self.pack = pickle_packer
237 self.pack = pickle_packer
238 self.unpack = pickle_unpacker
238 self.unpack = pickle_unpacker
239 else:
239 else:
240 self.unpack = import_item(str(new))
240 self.unpack = import_item(str(new))
241
241
242 session = CBytes(b'', config=True,
242 session = CBytes(b'', config=True,
243 help="""The UUID identifying this session.""")
243 help="""The UUID identifying this session.""")
244 def _session_default(self):
244 def _session_default(self):
245 return bytes(uuid.uuid4())
245 return bytes(uuid.uuid4())
246
246
247 username = Unicode(os.environ.get('USER',u'username'), config=True,
247 username = Unicode(os.environ.get('USER',u'username'), config=True,
248 help="""Username for the Session. Default is your system username.""")
248 help="""Username for the Session. Default is your system username.""")
249
249
250 # message signature related traits:
250 # message signature related traits:
251 key = CBytes(b'', config=True,
251 key = CBytes(b'', config=True,
252 help="""execution key, for extra authentication.""")
252 help="""execution key, for extra authentication.""")
253 def _key_changed(self, name, old, new):
253 def _key_changed(self, name, old, new):
254 if new:
254 if new:
255 self.auth = hmac.HMAC(new)
255 self.auth = hmac.HMAC(new)
256 else:
256 else:
257 self.auth = None
257 self.auth = None
258 auth = Instance(hmac.HMAC)
258 auth = Instance(hmac.HMAC)
259 digest_history = Set()
259 digest_history = Set()
260
260
261 keyfile = Unicode('', config=True,
261 keyfile = Unicode('', config=True,
262 help="""path to file containing execution key.""")
262 help="""path to file containing execution key.""")
263 def _keyfile_changed(self, name, old, new):
263 def _keyfile_changed(self, name, old, new):
264 with open(new, 'rb') as f:
264 with open(new, 'rb') as f:
265 self.key = f.read().strip()
265 self.key = f.read().strip()
266
266
267 pack = Any(default_packer) # the actual packer function
267 pack = Any(default_packer) # the actual packer function
268 def _pack_changed(self, name, old, new):
268 def _pack_changed(self, name, old, new):
269 if not callable(new):
269 if not callable(new):
270 raise TypeError("packer must be callable, not %s"%type(new))
270 raise TypeError("packer must be callable, not %s"%type(new))
271
271
272 unpack = Any(default_unpacker) # the actual packer function
272 unpack = Any(default_unpacker) # the actual packer function
273 def _unpack_changed(self, name, old, new):
273 def _unpack_changed(self, name, old, new):
274 # unpacker is not checked - it is assumed to be
274 # unpacker is not checked - it is assumed to be
275 if not callable(new):
275 if not callable(new):
276 raise TypeError("unpacker must be callable, not %s"%type(new))
276 raise TypeError("unpacker must be callable, not %s"%type(new))
277
277
278 def __init__(self, **kwargs):
278 def __init__(self, **kwargs):
279 """create a Session object
279 """create a Session object
280
280
281 Parameters
281 Parameters
282 ----------
282 ----------
283
283
284 debug : bool
284 debug : bool
285 whether to trigger extra debugging statements
285 whether to trigger extra debugging statements
286 packer/unpacker : str : 'json', 'pickle' or import_string
286 packer/unpacker : str : 'json', 'pickle' or import_string
287 importstrings for methods to serialize message parts. If just
287 importstrings for methods to serialize message parts. If just
288 'json' or 'pickle', predefined JSON and pickle packers will be used.
288 'json' or 'pickle', predefined JSON and pickle packers will be used.
289 Otherwise, the entire importstring must be used.
289 Otherwise, the entire importstring must be used.
290
290
291 The functions must accept at least valid JSON input, and output
291 The functions must accept at least valid JSON input, and output
292 *bytes*.
292 *bytes*.
293
293
294 For example, to use msgpack:
294 For example, to use msgpack:
295 packer = 'msgpack.packb', unpacker='msgpack.unpackb'
295 packer = 'msgpack.packb', unpacker='msgpack.unpackb'
296 pack/unpack : callables
296 pack/unpack : callables
297 You can also set the pack/unpack callables for serialization
297 You can also set the pack/unpack callables for serialization
298 directly.
298 directly.
299 session : bytes
299 session : bytes
300 the ID of this Session object. The default is to generate a new
300 the ID of this Session object. The default is to generate a new
301 UUID.
301 UUID.
302 username : unicode
302 username : unicode
303 username added to message headers. The default is to ask the OS.
303 username added to message headers. The default is to ask the OS.
304 key : bytes
304 key : bytes
305 The key used to initialize an HMAC signature. If unset, messages
305 The key used to initialize an HMAC signature. If unset, messages
306 will not be signed or checked.
306 will not be signed or checked.
307 keyfile : filepath
307 keyfile : filepath
308 The file containing a key. If this is set, `key` will be
308 The file containing a key. If this is set, `key` will be
309 initialized to the contents of the file.
309 initialized to the contents of the file.
310 """
310 """
311 super(Session, self).__init__(**kwargs)
311 super(Session, self).__init__(**kwargs)
312 self._check_packers()
312 self._check_packers()
313 self.none = self.pack({})
313 self.none = self.pack({})
314
314
315 @property
315 @property
316 def msg_id(self):
316 def msg_id(self):
317 """always return new uuid"""
317 """always return new uuid"""
318 return str(uuid.uuid4())
318 return str(uuid.uuid4())
319
319
320 def _check_packers(self):
320 def _check_packers(self):
321 """check packers for binary data and datetime support."""
321 """check packers for binary data and datetime support."""
322 pack = self.pack
322 pack = self.pack
323 unpack = self.unpack
323 unpack = self.unpack
324
324
325 # check simple serialization
325 # check simple serialization
326 msg = dict(a=[1,'hi'])
326 msg = dict(a=[1,'hi'])
327 try:
327 try:
328 packed = pack(msg)
328 packed = pack(msg)
329 except Exception:
329 except Exception:
330 raise ValueError("packer could not serialize a simple message")
330 raise ValueError("packer could not serialize a simple message")
331
331
332 # ensure packed message is bytes
332 # ensure packed message is bytes
333 if not isinstance(packed, bytes):
333 if not isinstance(packed, bytes):
334 raise ValueError("message packed to %r, but bytes are required"%type(packed))
334 raise ValueError("message packed to %r, but bytes are required"%type(packed))
335
335
336 # check that unpack is pack's inverse
336 # check that unpack is pack's inverse
337 try:
337 try:
338 unpacked = unpack(packed)
338 unpacked = unpack(packed)
339 except Exception:
339 except Exception:
340 raise ValueError("unpacker could not handle the packer's output")
340 raise ValueError("unpacker could not handle the packer's output")
341
341
342 # check datetime support
342 # check datetime support
343 msg = dict(t=datetime.now())
343 msg = dict(t=datetime.now())
344 try:
344 try:
345 unpacked = unpack(pack(msg))
345 unpacked = unpack(pack(msg))
346 except Exception:
346 except Exception:
347 self.pack = lambda o: pack(squash_dates(o))
347 self.pack = lambda o: pack(squash_dates(o))
348 self.unpack = lambda s: extract_dates(unpack(s))
348 self.unpack = lambda s: extract_dates(unpack(s))
349
349
350 def msg_header(self, msg_type):
350 def msg_header(self, msg_type):
351 return msg_header(self.msg_id, msg_type, self.username, self.session)
351 return msg_header(self.msg_id, msg_type, self.username, self.session)
352
352
353 def msg(self, msg_type, content=None, parent=None, subheader=None, header=None):
353 def msg(self, msg_type, content=None, parent=None, subheader=None, header=None):
354 """Return the nested message dict.
354 """Return the nested message dict.
355
355
356 This format is different from what is sent over the wire. The
356 This format is different from what is sent over the wire. The
357 serialize/unserialize methods converts this nested message dict to the wire
357 serialize/unserialize methods converts this nested message dict to the wire
358 format, which is a list of message parts.
358 format, which is a list of message parts.
359 """
359 """
360 msg = {}
360 msg = {}
361 msg['header'] = self.msg_header(msg_type) if header is None else header
361 msg['header'] = self.msg_header(msg_type) if header is None else header
362 msg['parent_header'] = {} if parent is None else extract_header(parent)
362 msg['parent_header'] = {} if parent is None else extract_header(parent)
363 msg['content'] = {} if content is None else content
363 msg['content'] = {} if content is None else content
364 sub = {} if subheader is None else subheader
364 sub = {} if subheader is None else subheader
365 msg['header'].update(sub)
365 msg['header'].update(sub)
366 return msg
366 return msg
367
367
368 def sign(self, msg_list):
368 def sign(self, msg_list):
369 """Sign a message with HMAC digest. If no auth, return b''.
369 """Sign a message with HMAC digest. If no auth, return b''.
370
370
371 Parameters
371 Parameters
372 ----------
372 ----------
373 msg_list : list
373 msg_list : list
374 The [p_header,p_parent,p_content] part of the message list.
374 The [p_header,p_parent,p_content] part of the message list.
375 """
375 """
376 if self.auth is None:
376 if self.auth is None:
377 return b''
377 return b''
378 h = self.auth.copy()
378 h = self.auth.copy()
379 for m in msg_list:
379 for m in msg_list:
380 h.update(m)
380 h.update(m)
381 return h.hexdigest()
381 return h.hexdigest()
382
382
383 def serialize(self, msg, ident=None):
383 def serialize(self, msg, ident=None):
384 """Serialize the message components to bytes.
384 """Serialize the message components to bytes.
385
385
386 This is roughly the inverse of unserialize. The serialize/unserialize
386 This is roughly the inverse of unserialize. The serialize/unserialize
387 methods work with full message lists, whereas pack/unpack work with
387 methods work with full message lists, whereas pack/unpack work with
388 the individual message parts in the message list.
388 the individual message parts in the message list.
389
389
390 Parameters
390 Parameters
391 ----------
391 ----------
392 msg : dict or Message
392 msg : dict or Message
393 The nexted message dict as returned by the self.msg method.
393 The nexted message dict as returned by the self.msg method.
394
394
395 Returns
395 Returns
396 -------
396 -------
397 msg_list : list
397 msg_list : list
398 The list of bytes objects to be sent with the format:
398 The list of bytes objects to be sent with the format:
399 [ident1,ident2,...,DELIM,HMAC,p_header,p_parent,p_content,
399 [ident1,ident2,...,DELIM,HMAC,p_header,p_parent,p_content,
400 buffer1,buffer2,...]. In this list, the p_* entities are
400 buffer1,buffer2,...]. In this list, the p_* entities are
401 the packed or serialized versions, so if JSON is used, these
401 the packed or serialized versions, so if JSON is used, these
402 are uft8 encoded JSON strings.
402 are uft8 encoded JSON strings.
403 """
403 """
404 content = msg.get('content', {})
404 content = msg.get('content', {})
405 if content is None:
405 if content is None:
406 content = self.none
406 content = self.none
407 elif isinstance(content, dict):
407 elif isinstance(content, dict):
408 content = self.pack(content)
408 content = self.pack(content)
409 elif isinstance(content, bytes):
409 elif isinstance(content, bytes):
410 # content is already packed, as in a relayed message
410 # content is already packed, as in a relayed message
411 pass
411 pass
412 elif isinstance(content, unicode):
412 elif isinstance(content, unicode):
413 # should be bytes, but JSON often spits out unicode
413 # should be bytes, but JSON often spits out unicode
414 content = content.encode('utf8')
414 content = content.encode('utf8')
415 else:
415 else:
416 raise TypeError("Content incorrect type: %s"%type(content))
416 raise TypeError("Content incorrect type: %s"%type(content))
417
417
418 real_message = [self.pack(msg['header']),
418 real_message = [self.pack(msg['header']),
419 self.pack(msg['parent_header']),
419 self.pack(msg['parent_header']),
420 content
420 content
421 ]
421 ]
422
422
423 to_send = []
423 to_send = []
424
424
425 if isinstance(ident, list):
425 if isinstance(ident, list):
426 # accept list of idents
426 # accept list of idents
427 to_send.extend(ident)
427 to_send.extend(ident)
428 elif ident is not None:
428 elif ident is not None:
429 to_send.append(ident)
429 to_send.append(ident)
430 to_send.append(DELIM)
430 to_send.append(DELIM)
431
431
432 signature = self.sign(real_message)
432 signature = self.sign(real_message)
433 to_send.append(signature)
433 to_send.append(signature)
434
434
435 to_send.extend(real_message)
435 to_send.extend(real_message)
436
436
437 return to_send
437 return to_send
438
438
439 def send(self, stream, msg_or_type, content=None, parent=None, ident=None,
439 def send(self, stream, msg_or_type, content=None, parent=None, ident=None,
440 buffers=None, subheader=None, track=False, header=None):
440 buffers=None, subheader=None, track=False, header=None):
441 """Build and send a message via stream or socket.
441 """Build and send a message via stream or socket.
442
442
443 The message format used by this function internally is as follows:
443 The message format used by this function internally is as follows:
444
444
445 [ident1,ident2,...,DELIM,HMAC,p_header,p_parent,p_content,
445 [ident1,ident2,...,DELIM,HMAC,p_header,p_parent,p_content,
446 buffer1,buffer2,...]
446 buffer1,buffer2,...]
447
447
448 The serialize/unserialize methods convert the nested message dict into this
448 The serialize/unserialize methods convert the nested message dict into this
449 format.
449 format.
450
450
451 Parameters
451 Parameters
452 ----------
452 ----------
453
453
454 stream : zmq.Socket or ZMQStream
454 stream : zmq.Socket or ZMQStream
455 The socket-like object used to send the data.
455 The socket-like object used to send the data.
456 msg_or_type : str or Message/dict
456 msg_or_type : str or Message/dict
457 Normally, msg_or_type will be a msg_type unless a message is being
457 Normally, msg_or_type will be a msg_type unless a message is being
458 sent more than once. If a header is supplied, this can be set to
458 sent more than once. If a header is supplied, this can be set to
459 None and the msg_type will be pulled from the header.
459 None and the msg_type will be pulled from the header.
460
460
461 content : dict or None
461 content : dict or None
462 The content of the message (ignored if msg_or_type is a message).
462 The content of the message (ignored if msg_or_type is a message).
463 header : dict or None
463 header : dict or None
464 The header dict for the message (ignores if msg_to_type is a message).
464 The header dict for the message (ignores if msg_to_type is a message).
465 parent : Message or dict or None
465 parent : Message or dict or None
466 The parent or parent header describing the parent of this message
466 The parent or parent header describing the parent of this message
467 (ignored if msg_or_type is a message).
467 (ignored if msg_or_type is a message).
468 ident : bytes or list of bytes
468 ident : bytes or list of bytes
469 The zmq.IDENTITY routing path.
469 The zmq.IDENTITY routing path.
470 subheader : dict or None
470 subheader : dict or None
471 Extra header keys for this message's header (ignored if msg_or_type
471 Extra header keys for this message's header (ignored if msg_or_type
472 is a message).
472 is a message).
473 buffers : list or None
473 buffers : list or None
474 The already-serialized buffers to be appended to the message.
474 The already-serialized buffers to be appended to the message.
475 track : bool
475 track : bool
476 Whether to track. Only for use with Sockets, because ZMQStream
476 Whether to track. Only for use with Sockets, because ZMQStream
477 objects cannot track messages.
477 objects cannot track messages.
478
478
479 Returns
479 Returns
480 -------
480 -------
481 msg : dict
481 msg : dict
482 The constructed message.
482 The constructed message.
483 (msg,tracker) : (dict, MessageTracker)
483 (msg,tracker) : (dict, MessageTracker)
484 if track=True, then a 2-tuple will be returned,
484 if track=True, then a 2-tuple will be returned,
485 the first element being the constructed
485 the first element being the constructed
486 message, and the second being the MessageTracker
486 message, and the second being the MessageTracker
487
487
488 """
488 """
489
489
490 if not isinstance(stream, (zmq.Socket, ZMQStream)):
490 if not isinstance(stream, (zmq.Socket, ZMQStream)):
491 raise TypeError("stream must be Socket or ZMQStream, not %r"%type(stream))
491 raise TypeError("stream must be Socket or ZMQStream, not %r"%type(stream))
492 elif track and isinstance(stream, ZMQStream):
492 elif track and isinstance(stream, ZMQStream):
493 raise TypeError("ZMQStream cannot track messages")
493 raise TypeError("ZMQStream cannot track messages")
494
494
495 if isinstance(msg_or_type, (Message, dict)):
495 if isinstance(msg_or_type, (Message, dict)):
496 # We got a Message or message dict, not a msg_type so don't
496 # We got a Message or message dict, not a msg_type so don't
497 # build a new Message.
497 # build a new Message.
498 msg = msg_or_type
498 msg = msg_or_type
499 else:
499 else:
500 msg = self.msg(msg_or_type, content=content, parent=parent,
500 msg = self.msg(msg_or_type, content=content, parent=parent,
501 subheader=subheader, header=header)
501 subheader=subheader, header=header)
502
502
503 buffers = [] if buffers is None else buffers
503 buffers = [] if buffers is None else buffers
504 to_send = self.serialize(msg, ident)
504 to_send = self.serialize(msg, ident)
505 flag = 0
505 flag = 0
506 if buffers:
506 if buffers:
507 flag = zmq.SNDMORE
507 flag = zmq.SNDMORE
508 _track = False
508 _track = False
509 else:
509 else:
510 _track=track
510 _track=track
511 if track:
511 if track:
512 tracker = stream.send_multipart(to_send, flag, copy=False, track=_track)
512 tracker = stream.send_multipart(to_send, flag, copy=False, track=_track)
513 else:
513 else:
514 tracker = stream.send_multipart(to_send, flag, copy=False)
514 tracker = stream.send_multipart(to_send, flag, copy=False)
515 for b in buffers[:-1]:
515 for b in buffers[:-1]:
516 stream.send(b, flag, copy=False)
516 stream.send(b, flag, copy=False)
517 if buffers:
517 if buffers:
518 if track:
518 if track:
519 tracker = stream.send(buffers[-1], copy=False, track=track)
519 tracker = stream.send(buffers[-1], copy=False, track=track)
520 else:
520 else:
521 tracker = stream.send(buffers[-1], copy=False)
521 tracker = stream.send(buffers[-1], copy=False)
522
522
523 # omsg = Message(msg)
523 # omsg = Message(msg)
524 if self.debug:
524 if self.debug:
525 pprint.pprint(msg)
525 pprint.pprint(msg)
526 pprint.pprint(to_send)
526 pprint.pprint(to_send)
527 pprint.pprint(buffers)
527 pprint.pprint(buffers)
528
528
529 msg['tracker'] = tracker
529 msg['tracker'] = tracker
530
530
531 return msg
531 return msg
532
532
533 def send_raw(self, stream, msg_list, flags=0, copy=True, ident=None):
533 def send_raw(self, stream, msg_list, flags=0, copy=True, ident=None):
534 """Send a raw message via ident path.
534 """Send a raw message via ident path.
535
535
536 This method is used to send a already serialized message.
536 This method is used to send a already serialized message.
537
537
538 Parameters
538 Parameters
539 ----------
539 ----------
540 stream : ZMQStream or Socket
540 stream : ZMQStream or Socket
541 The ZMQ stream or socket to use for sending the message.
541 The ZMQ stream or socket to use for sending the message.
542 msg_list : list
542 msg_list : list
543 The serialized list of messages to send. This only includes the
543 The serialized list of messages to send. This only includes the
544 [p_header,p_parent,p_content,buffer1,buffer2,...] portion of
544 [p_header,p_parent,p_content,buffer1,buffer2,...] portion of
545 the message.
545 the message.
546 ident : ident or list
546 ident : ident or list
547 A single ident or a list of idents to use in sending.
547 A single ident or a list of idents to use in sending.
548 """
548 """
549 to_send = []
549 to_send = []
550 if isinstance(ident, bytes):
550 if isinstance(ident, bytes):
551 ident = [ident]
551 ident = [ident]
552 if ident is not None:
552 if ident is not None:
553 to_send.extend(ident)
553 to_send.extend(ident)
554
554
555 to_send.append(DELIM)
555 to_send.append(DELIM)
556 to_send.append(self.sign(msg_list))
556 to_send.append(self.sign(msg_list))
557 to_send.extend(msg_list)
557 to_send.extend(msg_list)
558 stream.send_multipart(msg_list, flags, copy=copy)
558 stream.send_multipart(msg_list, flags, copy=copy)
559
559
560 def recv(self, socket, mode=zmq.NOBLOCK, content=True, copy=True):
560 def recv(self, socket, mode=zmq.NOBLOCK, content=True, copy=True):
561 """Receive and unpack a message.
561 """Receive and unpack a message.
562
562
563 Parameters
563 Parameters
564 ----------
564 ----------
565 socket : ZMQStream or Socket
565 socket : ZMQStream or Socket
566 The socket or stream to use in receiving.
566 The socket or stream to use in receiving.
567
567
568 Returns
568 Returns
569 -------
569 -------
570 [idents], msg
570 [idents], msg
571 [idents] is a list of idents and msg is a nested message dict of
571 [idents] is a list of idents and msg is a nested message dict of
572 same format as self.msg returns.
572 same format as self.msg returns.
573 """
573 """
574 if isinstance(socket, ZMQStream):
574 if isinstance(socket, ZMQStream):
575 socket = socket.socket
575 socket = socket.socket
576 try:
576 try:
577 msg_list = socket.recv_multipart(mode)
577 msg_list = socket.recv_multipart(mode)
578 except zmq.ZMQError as e:
578 except zmq.ZMQError as e:
579 if e.errno == zmq.EAGAIN:
579 if e.errno == zmq.EAGAIN:
580 # We can convert EAGAIN to None as we know in this case
580 # We can convert EAGAIN to None as we know in this case
581 # recv_multipart won't return None.
581 # recv_multipart won't return None.
582 return None,None
582 return None,None
583 else:
583 else:
584 raise
584 raise
585 # split multipart message into identity list and message dict
585 # split multipart message into identity list and message dict
586 # invalid large messages can cause very expensive string comparisons
586 # invalid large messages can cause very expensive string comparisons
587 idents, msg_list = self.feed_identities(msg_list, copy)
587 idents, msg_list = self.feed_identities(msg_list, copy)
588 try:
588 try:
589 return idents, self.unserialize(msg_list, content=content, copy=copy)
589 return idents, self.unserialize(msg_list, content=content, copy=copy)
590 except Exception as e:
590 except Exception as e:
591 print (idents, msg_list)
592 # TODO: handle it
591 # TODO: handle it
593 raise e
592 raise e
594
593
595 def feed_identities(self, msg_list, copy=True):
594 def feed_identities(self, msg_list, copy=True):
596 """Split the identities from the rest of the message.
595 """Split the identities from the rest of the message.
597
596
598 Feed until DELIM is reached, then return the prefix as idents and
597 Feed until DELIM is reached, then return the prefix as idents and
599 remainder as msg_list. This is easily broken by setting an IDENT to DELIM,
598 remainder as msg_list. This is easily broken by setting an IDENT to DELIM,
600 but that would be silly.
599 but that would be silly.
601
600
602 Parameters
601 Parameters
603 ----------
602 ----------
604 msg_list : a list of Message or bytes objects
603 msg_list : a list of Message or bytes objects
605 The message to be split.
604 The message to be split.
606 copy : bool
605 copy : bool
607 flag determining whether the arguments are bytes or Messages
606 flag determining whether the arguments are bytes or Messages
608
607
609 Returns
608 Returns
610 -------
609 -------
611 (idents, msg_list) : two lists
610 (idents, msg_list) : two lists
612 idents will always be a list of bytes, each of which is a ZMQ
611 idents will always be a list of bytes, each of which is a ZMQ
613 identity. msg_list will be a list of bytes or zmq.Messages of the
612 identity. msg_list will be a list of bytes or zmq.Messages of the
614 form [HMAC,p_header,p_parent,p_content,buffer1,buffer2,...] and
613 form [HMAC,p_header,p_parent,p_content,buffer1,buffer2,...] and
615 should be unpackable/unserializable via self.unserialize at this
614 should be unpackable/unserializable via self.unserialize at this
616 point.
615 point.
617 """
616 """
618 if copy:
617 if copy:
619 idx = msg_list.index(DELIM)
618 idx = msg_list.index(DELIM)
620 return msg_list[:idx], msg_list[idx+1:]
619 return msg_list[:idx], msg_list[idx+1:]
621 else:
620 else:
622 failed = True
621 failed = True
623 for idx,m in enumerate(msg_list):
622 for idx,m in enumerate(msg_list):
624 if m.bytes == DELIM:
623 if m.bytes == DELIM:
625 failed = False
624 failed = False
626 break
625 break
627 if failed:
626 if failed:
628 raise ValueError("DELIM not in msg_list")
627 raise ValueError("DELIM not in msg_list")
629 idents, msg_list = msg_list[:idx], msg_list[idx+1:]
628 idents, msg_list = msg_list[:idx], msg_list[idx+1:]
630 return [m.bytes for m in idents], msg_list
629 return [m.bytes for m in idents], msg_list
631
630
632 def unserialize(self, msg_list, content=True, copy=True):
631 def unserialize(self, msg_list, content=True, copy=True):
633 """Unserialize a msg_list to a nested message dict.
632 """Unserialize a msg_list to a nested message dict.
634
633
635 This is roughly the inverse of serialize. The serialize/unserialize
634 This is roughly the inverse of serialize. The serialize/unserialize
636 methods work with full message lists, whereas pack/unpack work with
635 methods work with full message lists, whereas pack/unpack work with
637 the individual message parts in the message list.
636 the individual message parts in the message list.
638
637
639 Parameters:
638 Parameters:
640 -----------
639 -----------
641 msg_list : list of bytes or Message objects
640 msg_list : list of bytes or Message objects
642 The list of message parts of the form [HMAC,p_header,p_parent,
641 The list of message parts of the form [HMAC,p_header,p_parent,
643 p_content,buffer1,buffer2,...].
642 p_content,buffer1,buffer2,...].
644 content : bool (True)
643 content : bool (True)
645 Whether to unpack the content dict (True), or leave it packed
644 Whether to unpack the content dict (True), or leave it packed
646 (False).
645 (False).
647 copy : bool (True)
646 copy : bool (True)
648 Whether to return the bytes (True), or the non-copying Message
647 Whether to return the bytes (True), or the non-copying Message
649 object in each place (False).
648 object in each place (False).
650
649
651 Returns
650 Returns
652 -------
651 -------
653 msg : dict
652 msg : dict
654 The nested message dict with top-level keys [header, parent_header,
653 The nested message dict with top-level keys [header, parent_header,
655 content, buffers].
654 content, buffers].
656 """
655 """
657 minlen = 4
656 minlen = 4
658 message = {}
657 message = {}
659 if not copy:
658 if not copy:
660 for i in range(minlen):
659 for i in range(minlen):
661 msg_list[i] = msg_list[i].bytes
660 msg_list[i] = msg_list[i].bytes
662 if self.auth is not None:
661 if self.auth is not None:
663 signature = msg_list[0]
662 signature = msg_list[0]
663 if not signature:
664 raise ValueError("Unsigned Message")
664 if signature in self.digest_history:
665 if signature in self.digest_history:
665 raise ValueError("Duplicate Signature: %r"%signature)
666 raise ValueError("Duplicate Signature: %r"%signature)
666 self.digest_history.add(signature)
667 self.digest_history.add(signature)
667 check = self.sign(msg_list[1:4])
668 check = self.sign(msg_list[1:4])
668 if not signature == check:
669 if not signature == check:
669 raise ValueError("Invalid Signature: %r"%signature)
670 raise ValueError("Invalid Signature: %r"%signature)
670 if not len(msg_list) >= minlen:
671 if not len(msg_list) >= minlen:
671 raise TypeError("malformed message, must have at least %i elements"%minlen)
672 raise TypeError("malformed message, must have at least %i elements"%minlen)
672 message['header'] = self.unpack(msg_list[1])
673 message['header'] = self.unpack(msg_list[1])
673 message['parent_header'] = self.unpack(msg_list[2])
674 message['parent_header'] = self.unpack(msg_list[2])
674 if content:
675 if content:
675 message['content'] = self.unpack(msg_list[3])
676 message['content'] = self.unpack(msg_list[3])
676 else:
677 else:
677 message['content'] = msg_list[3]
678 message['content'] = msg_list[3]
678
679
679 message['buffers'] = msg_list[4:]
680 message['buffers'] = msg_list[4:]
680 return message
681 return message
681
682
682 def test_msg2obj():
683 def test_msg2obj():
683 am = dict(x=1)
684 am = dict(x=1)
684 ao = Message(am)
685 ao = Message(am)
685 assert ao.x == am['x']
686 assert ao.x == am['x']
686
687
687 am['y'] = dict(z=1)
688 am['y'] = dict(z=1)
688 ao = Message(am)
689 ao = Message(am)
689 assert ao.y.z == am['y']['z']
690 assert ao.y.z == am['y']['z']
690
691
691 k1, k2 = 'y', 'z'
692 k1, k2 = 'y', 'z'
692 assert ao[k1][k2] == am[k1][k2]
693 assert ao[k1][k2] == am[k1][k2]
693
694
694 am2 = dict(ao)
695 am2 = dict(ao)
695 assert am['x'] == am2['x']
696 assert am['x'] == am2['x']
696 assert am['y']['z'] == am2['y']['z']
697 assert am['y']['z'] == am2['y']['z']
697
698
@@ -1,255 +1,254 b''
1 .. _parallelsecurity:
1 .. _parallelsecurity:
2
2
3 ===========================
3 ===========================
4 Security details of IPython
4 Security details of IPython
5 ===========================
5 ===========================
6
6
7 .. note::
7 .. note::
8
8
9 This section is not thorough, and IPython.zmq needs a thorough security
9 This section is not thorough, and IPython.zmq needs a thorough security
10 audit.
10 audit.
11
11
12 IPython's :mod:`IPython.zmq` package exposes the full power of the
12 IPython's :mod:`IPython.zmq` package exposes the full power of the
13 Python interpreter over a TCP/IP network for the purposes of parallel
13 Python interpreter over a TCP/IP network for the purposes of parallel
14 computing. This feature brings up the important question of IPython's security
14 computing. This feature brings up the important question of IPython's security
15 model. This document gives details about this model and how it is implemented
15 model. This document gives details about this model and how it is implemented
16 in IPython's architecture.
16 in IPython's architecture.
17
17
18 Process and network topology
18 Process and network topology
19 ============================
19 ============================
20
20
21 To enable parallel computing, IPython has a number of different processes that
21 To enable parallel computing, IPython has a number of different processes that
22 run. These processes are discussed at length in the IPython documentation and
22 run. These processes are discussed at length in the IPython documentation and
23 are summarized here:
23 are summarized here:
24
24
25 * The IPython *engine*. This process is a full blown Python
25 * The IPython *engine*. This process is a full blown Python
26 interpreter in which user code is executed. Multiple
26 interpreter in which user code is executed. Multiple
27 engines are started to make parallel computing possible.
27 engines are started to make parallel computing possible.
28 * The IPython *hub*. This process monitors a set of
28 * The IPython *hub*. This process monitors a set of
29 engines and schedulers, and keeps track of the state of the processes. It listens
29 engines and schedulers, and keeps track of the state of the processes. It listens
30 for registration connections from engines and clients, and monitor connections
30 for registration connections from engines and clients, and monitor connections
31 from schedulers.
31 from schedulers.
32 * The IPython *schedulers*. This is a set of processes that relay commands and results
32 * The IPython *schedulers*. This is a set of processes that relay commands and results
33 between clients and engines. They are typically on the same machine as the controller,
33 between clients and engines. They are typically on the same machine as the controller,
34 and listen for connections from engines and clients, but connect to the Hub.
34 and listen for connections from engines and clients, but connect to the Hub.
35 * The IPython *client*. This process is typically an
35 * The IPython *client*. This process is typically an
36 interactive Python process that is used to coordinate the
36 interactive Python process that is used to coordinate the
37 engines to get a parallel computation done.
37 engines to get a parallel computation done.
38
38
39 Collectively, these processes are called the IPython *cluster*, and the hub and schedulers
39 Collectively, these processes are called the IPython *cluster*, and the hub and schedulers
40 together are referred to as the *controller*.
40 together are referred to as the *controller*.
41
41
42
42
43 These processes communicate over any transport supported by ZeroMQ (tcp,pgm,infiniband,ipc)
43 These processes communicate over any transport supported by ZeroMQ (tcp,pgm,infiniband,ipc)
44 with a well defined topology. The IPython hub and schedulers listen on sockets. Upon
44 with a well defined topology. The IPython hub and schedulers listen on sockets. Upon
45 starting, an engine connects to a hub and registers itself, which then informs the engine
45 starting, an engine connects to a hub and registers itself, which then informs the engine
46 of the connection information for the schedulers, and the engine then connects to the
46 of the connection information for the schedulers, and the engine then connects to the
47 schedulers. These engine/hub and engine/scheduler connections persist for the
47 schedulers. These engine/hub and engine/scheduler connections persist for the
48 lifetime of each engine.
48 lifetime of each engine.
49
49
50 The IPython client also connects to the controller processes using a number of socket
50 The IPython client also connects to the controller processes using a number of socket
51 connections. As of writing, this is one socket per scheduler (4), and 3 connections to the
51 connections. As of writing, this is one socket per scheduler (4), and 3 connections to the
52 hub for a total of 7. These connections persist for the lifetime of the client only.
52 hub for a total of 7. These connections persist for the lifetime of the client only.
53
53
54 A given IPython controller and set of engines engines typically has a relatively
54 A given IPython controller and set of engines engines typically has a relatively
55 short lifetime. Typically this lifetime corresponds to the duration of a single parallel
55 short lifetime. Typically this lifetime corresponds to the duration of a single parallel
56 simulation performed by a single user. Finally, the hub, schedulers, engines, and client
56 simulation performed by a single user. Finally, the hub, schedulers, engines, and client
57 processes typically execute with the permissions of that same user. More specifically, the
57 processes typically execute with the permissions of that same user. More specifically, the
58 controller and engines are *not* executed as root or with any other superuser permissions.
58 controller and engines are *not* executed as root or with any other superuser permissions.
59
59
60 Application logic
60 Application logic
61 =================
61 =================
62
62
63 When running the IPython kernel to perform a parallel computation, a user
63 When running the IPython kernel to perform a parallel computation, a user
64 utilizes the IPython client to send Python commands and data through the
64 utilizes the IPython client to send Python commands and data through the
65 IPython schedulers to the IPython engines, where those commands are executed
65 IPython schedulers to the IPython engines, where those commands are executed
66 and the data processed. The design of IPython ensures that the client is the
66 and the data processed. The design of IPython ensures that the client is the
67 only access point for the capabilities of the engines. That is, the only way
67 only access point for the capabilities of the engines. That is, the only way
68 of addressing the engines is through a client.
68 of addressing the engines is through a client.
69
69
70 A user can utilize the client to instruct the IPython engines to execute
70 A user can utilize the client to instruct the IPython engines to execute
71 arbitrary Python commands. These Python commands can include calls to the
71 arbitrary Python commands. These Python commands can include calls to the
72 system shell, access the filesystem, etc., as required by the user's
72 system shell, access the filesystem, etc., as required by the user's
73 application code. From this perspective, when a user runs an IPython engine on
73 application code. From this perspective, when a user runs an IPython engine on
74 a host, that engine has the same capabilities and permissions as the user
74 a host, that engine has the same capabilities and permissions as the user
75 themselves (as if they were logged onto the engine's host with a terminal).
75 themselves (as if they were logged onto the engine's host with a terminal).
76
76
77 Secure network connections
77 Secure network connections
78 ==========================
78 ==========================
79
79
80 Overview
80 Overview
81 --------
81 --------
82
82
83 ZeroMQ provides exactly no security. For this reason, users of IPython must be very
83 ZeroMQ provides exactly no security. For this reason, users of IPython must be very
84 careful in managing connections, because an open TCP/IP socket presents access to
84 careful in managing connections, because an open TCP/IP socket presents access to
85 arbitrary execution as the user on the engine machines. As a result, the default behavior
85 arbitrary execution as the user on the engine machines. As a result, the default behavior
86 of controller processes is to only listen for clients on the loopback interface, and the
86 of controller processes is to only listen for clients on the loopback interface, and the
87 client must establish SSH tunnels to connect to the controller processes.
87 client must establish SSH tunnels to connect to the controller processes.
88
88
89 .. warning::
89 .. warning::
90
90
91 If the controller's loopback interface is untrusted, then IPython should be considered
91 If the controller's loopback interface is untrusted, then IPython should be considered
92 vulnerable, and this extends to the loopback of all connected clients, which have
92 vulnerable, and this extends to the loopback of all connected clients, which have
93 opened a loopback port that is redirected to the controller's loopback port.
93 opened a loopback port that is redirected to the controller's loopback port.
94
94
95
95
96 SSH
96 SSH
97 ---
97 ---
98
98
99 Since ZeroMQ provides no security, SSH tunnels are the primary source of secure
99 Since ZeroMQ provides no security, SSH tunnels are the primary source of secure
100 connections. A connector file, such as `ipcontroller-client.json`, will contain
100 connections. A connector file, such as `ipcontroller-client.json`, will contain
101 information for connecting to the controller, possibly including the address of an
101 information for connecting to the controller, possibly including the address of an
102 ssh-server through with the client is to tunnel. The Client object then creates tunnels
102 ssh-server through with the client is to tunnel. The Client object then creates tunnels
103 using either [OpenSSH]_ or [Paramiko]_, depending on the platform. If users do not wish to
103 using either [OpenSSH]_ or [Paramiko]_, depending on the platform. If users do not wish to
104 use OpenSSH or Paramiko, or the tunneling utilities are insufficient, then they may
104 use OpenSSH or Paramiko, or the tunneling utilities are insufficient, then they may
105 construct the tunnels themselves, and simply connect clients and engines as if the
105 construct the tunnels themselves, and simply connect clients and engines as if the
106 controller were on loopback on the connecting machine.
106 controller were on loopback on the connecting machine.
107
107
108 .. note::
108 .. note::
109
109
110 There is not currently tunneling available for engines.
110 There is not currently tunneling available for engines.
111
111
112 Authentication
112 Authentication
113 --------------
113 --------------
114
114
115 To protect users of shared machines, [HMAC]_ digests are used to sign messages, using a
115 To protect users of shared machines, [HMAC]_ digests are used to sign messages, using a
116 shared key.
116 shared key.
117
117
118 The Session object that handles the message protocol uses a unique key to verify valid
118 The Session object that handles the message protocol uses a unique key to verify valid
119 messages. This can be any value specified by the user, but the default behavior is a
119 messages. This can be any value specified by the user, but the default behavior is a
120 pseudo-random 128-bit number, as generated by `uuid.uuid4()`. This key is used to
120 pseudo-random 128-bit number, as generated by `uuid.uuid4()`. This key is used to
121 initialize an HMAC object, which digests all messages, and includes that digest as a
121 initialize an HMAC object, which digests all messages, and includes that digest as a
122 signature and part of the message. Every message that is unpacked (on Controller, Engine,
122 signature and part of the message. Every message that is unpacked (on Controller, Engine,
123 and Client) will also be digested by the receiver, ensuring that the sender's key is the
123 and Client) will also be digested by the receiver, ensuring that the sender's key is the
124 same as the receiver's. No messages that do not contain this key are acted upon in any
124 same as the receiver's. No messages that do not contain this key are acted upon in any
125 way. The key itself is never sent over the network.
125 way. The key itself is never sent over the network.
126
126
127 There is exactly one shared key per cluster - it must be the same everywhere. Typically,
127 There is exactly one shared key per cluster - it must be the same everywhere. Typically,
128 the controller creates this key, and stores it in the private connection files
128 the controller creates this key, and stores it in the private connection files
129 `ipython-{engine|client}.json`. These files are typically stored in the
129 `ipython-{engine|client}.json`. These files are typically stored in the
130 `~/.ipython/profile_<name>/security` directory, and are maintained as readable only by the
130 `~/.ipython/profile_<name>/security` directory, and are maintained as readable only by the
131 owner, just as is common practice with a user's keys in their `.ssh` directory.
131 owner, just as is common practice with a user's keys in their `.ssh` directory.
132
132
133 .. warning::
133 .. warning::
134
134
135 It is important to note that the key authentication, as emphasized by the use of
135 It is important to note that the signatures protect against unauthorized messages,
136 a uuid rather than generating a key with a cryptographic library, provides a
136 but, as there is no encryption, provide exactly no protection of data privacy. It is
137 defense against *accidental* messages more than it does against malicious attacks.
137 possible, however, to use a custom serialization scheme (via Session.packer/unpacker
138 If loopback is compromised, it would be trivial for an attacker to intercept messages
138 traits) that does incorporate your own encryption scheme.
139 and deduce the key, as there is no encryption.
140
139
141
140
142
141
143 Specific security vulnerabilities
142 Specific security vulnerabilities
144 =================================
143 =================================
145
144
146 There are a number of potential security vulnerabilities present in IPython's
145 There are a number of potential security vulnerabilities present in IPython's
147 architecture. In this section we discuss those vulnerabilities and detail how
146 architecture. In this section we discuss those vulnerabilities and detail how
148 the security architecture described above prevents them from being exploited.
147 the security architecture described above prevents them from being exploited.
149
148
150 Unauthorized clients
149 Unauthorized clients
151 --------------------
150 --------------------
152
151
153 The IPython client can instruct the IPython engines to execute arbitrary
152 The IPython client can instruct the IPython engines to execute arbitrary
154 Python code with the permissions of the user who started the engines. If an
153 Python code with the permissions of the user who started the engines. If an
155 attacker were able to connect their own hostile IPython client to the IPython
154 attacker were able to connect their own hostile IPython client to the IPython
156 controller, they could instruct the engines to execute code.
155 controller, they could instruct the engines to execute code.
157
156
158
157
159 On the first level, this attack is prevented by requiring access to the controller's
158 On the first level, this attack is prevented by requiring access to the controller's
160 ports, which are recommended to only be open on loopback if the controller is on an
159 ports, which are recommended to only be open on loopback if the controller is on an
161 untrusted local network. If the attacker does have access to the Controller's ports, then
160 untrusted local network. If the attacker does have access to the Controller's ports, then
162 the attack is prevented by the capabilities based client authentication of the execution
161 the attack is prevented by the capabilities based client authentication of the execution
163 key. The relevant authentication information is encoded into the JSON file that clients
162 key. The relevant authentication information is encoded into the JSON file that clients
164 must present to gain access to the IPython controller. By limiting the distribution of
163 must present to gain access to the IPython controller. By limiting the distribution of
165 those keys, a user can grant access to only authorized persons, just as with SSH keys.
164 those keys, a user can grant access to only authorized persons, just as with SSH keys.
166
165
167 It is highly unlikely that an execution key could be guessed by an attacker
166 It is highly unlikely that an execution key could be guessed by an attacker
168 in a brute force guessing attack. A given instance of the IPython controller
167 in a brute force guessing attack. A given instance of the IPython controller
169 only runs for a relatively short amount of time (on the order of hours). Thus
168 only runs for a relatively short amount of time (on the order of hours). Thus
170 an attacker would have only a limited amount of time to test a search space of
169 an attacker would have only a limited amount of time to test a search space of
171 size 2**128. For added security, users can have arbitrarily long keys.
170 size 2**128. For added security, users can have arbitrarily long keys.
172
171
173 .. warning::
172 .. warning::
174
173
175 If the attacker has gained enough access to intercept loopback connections on *either* the
174 If the attacker has gained enough access to intercept loopback connections on *either* the
176 controller or client, then a duplicate message can be sent. To protect against this,
175 controller or client, then a duplicate message can be sent. To protect against this,
177 recipients only allow each signature once, and consider duplicates invalid. However,
176 recipients only allow each signature once, and consider duplicates invalid. However,
178 the duplicate message could be sent to *another* recipient using the same key,
177 the duplicate message could be sent to *another* recipient using the same key,
179 and it would be considered valid.
178 and it would be considered valid.
180
179
181
180
182 Unauthorized engines
181 Unauthorized engines
183 --------------------
182 --------------------
184
183
185 If an attacker were able to connect a hostile engine to a user's controller,
184 If an attacker were able to connect a hostile engine to a user's controller,
186 the user might unknowingly send sensitive code or data to the hostile engine.
185 the user might unknowingly send sensitive code or data to the hostile engine.
187 This attacker's engine would then have full access to that code and data.
186 This attacker's engine would then have full access to that code and data.
188
187
189 This type of attack is prevented in the same way as the unauthorized client
188 This type of attack is prevented in the same way as the unauthorized client
190 attack, through the usage of the capabilities based authentication scheme.
189 attack, through the usage of the capabilities based authentication scheme.
191
190
192 Unauthorized controllers
191 Unauthorized controllers
193 ------------------------
192 ------------------------
194
193
195 It is also possible that an attacker could try to convince a user's IPython
194 It is also possible that an attacker could try to convince a user's IPython
196 client or engine to connect to a hostile IPython controller. That controller
195 client or engine to connect to a hostile IPython controller. That controller
197 would then have full access to the code and data sent between the IPython
196 would then have full access to the code and data sent between the IPython
198 client and the IPython engines.
197 client and the IPython engines.
199
198
200 Again, this attack is prevented through the capabilities in a connection file, which
199 Again, this attack is prevented through the capabilities in a connection file, which
201 ensure that a client or engine connects to the correct controller. It is also important to
200 ensure that a client or engine connects to the correct controller. It is also important to
202 note that the connection files also encode the IP address and port that the controller is
201 note that the connection files also encode the IP address and port that the controller is
203 listening on, so there is little chance of mistakenly connecting to a controller running
202 listening on, so there is little chance of mistakenly connecting to a controller running
204 on a different IP address and port.
203 on a different IP address and port.
205
204
206 When starting an engine or client, a user must specify the key to use
205 When starting an engine or client, a user must specify the key to use
207 for that connection. Thus, in order to introduce a hostile controller, the
206 for that connection. Thus, in order to introduce a hostile controller, the
208 attacker must convince the user to use the key associated with the
207 attacker must convince the user to use the key associated with the
209 hostile controller. As long as a user is diligent in only using keys from
208 hostile controller. As long as a user is diligent in only using keys from
210 trusted sources, this attack is not possible.
209 trusted sources, this attack is not possible.
211
210
212 .. note::
211 .. note::
213
212
214 I may be wrong, the unauthorized controller may be easier to fake than this.
213 I may be wrong, the unauthorized controller may be easier to fake than this.
215
214
216 Other security measures
215 Other security measures
217 =======================
216 =======================
218
217
219 A number of other measures are taken to further limit the security risks
218 A number of other measures are taken to further limit the security risks
220 involved in running the IPython kernel.
219 involved in running the IPython kernel.
221
220
222 First, by default, the IPython controller listens on random port numbers.
221 First, by default, the IPython controller listens on random port numbers.
223 While this can be overridden by the user, in the default configuration, an
222 While this can be overridden by the user, in the default configuration, an
224 attacker would have to do a port scan to even find a controller to attack.
223 attacker would have to do a port scan to even find a controller to attack.
225 When coupled with the relatively short running time of a typical controller
224 When coupled with the relatively short running time of a typical controller
226 (on the order of hours), an attacker would have to work extremely hard and
225 (on the order of hours), an attacker would have to work extremely hard and
227 extremely *fast* to even find a running controller to attack.
226 extremely *fast* to even find a running controller to attack.
228
227
229 Second, much of the time, especially when run on supercomputers or clusters,
228 Second, much of the time, especially when run on supercomputers or clusters,
230 the controller is running behind a firewall. Thus, for engines or client to
229 the controller is running behind a firewall. Thus, for engines or client to
231 connect to the controller:
230 connect to the controller:
232
231
233 * The different processes have to all be behind the firewall.
232 * The different processes have to all be behind the firewall.
234
233
235 or:
234 or:
236
235
237 * The user has to use SSH port forwarding to tunnel the
236 * The user has to use SSH port forwarding to tunnel the
238 connections through the firewall.
237 connections through the firewall.
239
238
240 In either case, an attacker is presented with additional barriers that prevent
239 In either case, an attacker is presented with additional barriers that prevent
241 attacking or even probing the system.
240 attacking or even probing the system.
242
241
243 Summary
242 Summary
244 =======
243 =======
245
244
246 IPython's architecture has been carefully designed with security in mind. The
245 IPython's architecture has been carefully designed with security in mind. The
247 capabilities based authentication model, in conjunction with SSH tunneled
246 capabilities based authentication model, in conjunction with SSH tunneled
248 TCP/IP channels, address the core potential vulnerabilities in the system,
247 TCP/IP channels, address the core potential vulnerabilities in the system,
249 while still enabling user's to use the system in open networks.
248 while still enabling user's to use the system in open networks.
250
249
251 .. [RFC5246] <http://tools.ietf.org/html/rfc5246>
250 .. [RFC5246] <http://tools.ietf.org/html/rfc5246>
252
251
253 .. [OpenSSH] <http://www.openssh.com/>
252 .. [OpenSSH] <http://www.openssh.com/>
254 .. [Paramiko] <http://www.lag.net/paramiko/>
253 .. [Paramiko] <http://www.lag.net/paramiko/>
255 .. [HMAC] <http://tools.ietf.org/html/rfc2104.html>
254 .. [HMAC] <http://tools.ietf.org/html/rfc2104.html>
General Comments 0
You need to be logged in to leave comments. Login now