Module: PryRemoteEm::Server

Includes:
Proto
Defined in:
lib/pry-remote-em/server.rb,
lib/pry-remote-em/server/shell_cmd.rb

Defined Under Namespace

Modules: ShellCmd

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Proto

#receive_banner, #receive_data, #receive_object, #receive_prompt, #receive_proxy_connection, #receive_register_server, #receive_server_list, #receive_server_reload_list, #receive_shell_result, #receive_start_tls, #receive_unregister_server, #send_auth, #send_banner, #send_clear_buffer, #send_completion, #send_heatbeat, #send_msg, #send_msg_bcast, #send_object, #send_prompt, #send_proxy_connection, #send_raw, #send_register_server, #send_server_list, #send_server_reload_list, #send_shell_cmd, #send_shell_data, #send_shell_result, #send_shell_sig, #send_start_tls, #send_unregister_server

Class Method Details

.peers(obj = nil) ⇒ Object

The list of pry-remote-em connections for a given object, or the list of all pry-remote-em connections for this process. The peer list is used when broadcasting messages between connections.



114
115
116
117
# File 'lib/pry-remote-em/server.rb', line 114

def peers(obj = nil)
  @peers ||= {}
  obj.nil? ? @peers.values.flatten : (@peers[obj] ||= [])
end

.register(obj, peer) ⇒ Object

Record the association between a given object and a given pry-remote-em connection.



120
121
122
# File 'lib/pry-remote-em/server.rb', line 120

def register(obj, peer)
  peers(obj).tap { |plist| plist.include?(peer) || plist.push(peer) }
end

.run(options = {}, &block) ⇒ Object

Start a pry-remote-em server

Parameters:

  • options (Hash) (defaults to: {})

Options Hash (options):

  • :target (Object)

    Object to bind Pry session, default - PryRemoteEm::Sandbox instance

  • :host (String)

    The IP-address to listen on, default - 127.0.0.1 (same as PRYEMHOST environment variable)

  • :port (Fixnum, String, Symbol)

    The port to listen on - if :auto or ‘auto’ the next available port will be taken, default - 6463 (same as PRYEMPORT environment variable)

  • :id (String)

    Server’s UUID, will be generated automatically unless you pass it explicitly

  • :tls (Boolean)

    require SSL encryption, default - false

  • :logger (Logger)

    Logger for Pry Server, default - STDERR

  • :auth (Proc, Object)

    require user authentication - see README

  • :allow_shell_cmds (Boolean)

    Allow shell commands or not, default - true

  • :port_fail (Integer, Symbol)

    set to :auto to search for available port in range from given port to port + 100, or pass explicit integer to use instaed of 100, default - 1

  • :name (String)

    Server name to show in broker list, default - object’s inspect (same as PRYEMNAME environment variable)

  • :external_url (String)

    External URL to connect behind firewall, NAT, Docket etc. in form “pryem://my.host:6463”, default - use given host and port and expand it to all interfaces in case of 0.0.0.0 (same as PRYEMURL environment variable)

  • :heartbeat_interval (Integer)

    Interval to send heartbeats and updated details to broker, default - 15 (same as PRYEMHBSEND environment variable)

  • :remote_broker (Boolean)

    Connect to remote broker instead of starting local one, default - false (same as PRYEMREMOTEBROKER environment variable)

  • :broker_host (String)

    Broker host to connect to, default - localhost

  • :broker_port (String)

    Broker port to connect to, default - 6462

  • :details (Hash)

    Optional details to pass to broker and show in table (should consist of string/symbol keys and simple serializable values)



91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/pry-remote-em/server.rb', line 91

def run(options = {}, &block)
  description = options.dup
  description[:target] ||= PryRemoteEm::Sandbox.new
  description[:host] ||= ENV['PRYEMHOST'].nil? || ENV['PRYEMHOST'].empty? ? DEFAULT_SERVER_HOST : ENV['PRYEMHOST']
  determine_port_and_tries(description)
  determine_name(description)
  description[:id] ||= SecureRandom.uuid
  description[:logger] ||= ::Logger.new(STDERR)
  description[:external_url] ||= ENV['PRYEMURL'] || "#{description[:tls] ? 'pryems' : 'pryem'}://#{description[:host]}:#{description[:port]}/"
  description[:details] ||= {}
  description[:allow_shell_cmds] = true if description[:allow_shell_cmds].nil?
  description[:heartbeat_interval] ||= ENV['PRYEMHBSEND'].nil? || ENV['PRYEMHBSEND'].empty? ? HEARTBEAT_SEND_INTERVAL : ENV['PRYEMHBSEND']
  description[:urls] = expand_url(description[:external_url])
  description[:server] = start_server(description, &block)
  description[:logger].info("[pry-remote-em] listening for connections on #{description[:external_url]}")
  PryRemoteEm.servers[description[:id]] = description
  register_in_broker(description)
  return description
end

.unregister(obj, peer) ⇒ Object

Remove the association between a given object and a given pry-remote-em connection.



125
126
127
# File 'lib/pry-remote-em/server.rb', line 125

def unregister(obj, peer)
  peers(obj).tap {|plist| true while plist.delete(peer) }
end

Instance Method Details

#after_auth(&blk) ⇒ Object



420
421
422
423
# File 'lib/pry-remote-em/server.rb', line 420

def after_auth(&blk)
  # TODO perhaps replace with #auth_ok
  @after_auth.push(blk)
end

#auth_attempt {|user, ip| ... } ⇒ Object

Registers a block to call when authentication is attempted.

Yields:

  • (user, ip)

    a block to call on each authentication attempt

Yield Parameters:

  • user (String)
  • ip (String)


444
445
446
447
448
449
# File 'lib/pry-remote-em/server.rb', line 444

def auth_attempt(*args, &blk)
  block_given? ? @auth_attempt_cbs << blk : @auth_attempts.push(args)
  while (auth_data = @auth_attempts.shift)
    @auth_attempt_cbs.each { |cb| cb.call(*auth_data) }
  end
end

#auth_fail {|user, ip| ... } ⇒ Object

Registers a block to call when authentication fails.

Yields:

  • (user, ip)

    a block to call after each failed authentication attempt

Yield Parameters:

  • user (String)
  • ip (String)


456
457
458
459
460
461
# File 'lib/pry-remote-em/server.rb', line 456

def auth_fail(*args, &blk)
  block_given? ? @auth_fail_cbs << blk : @auth_fails.push(args)
  while (fail_data = @auth_fails.shift)
    @auth_fail_cbs.each { |cb| cb.call(*fail_data) }
  end
end

#auth_ok {|user, ip| ... } ⇒ Object

Registers a block to call when authentication succeeds.

Yields:

  • (user, ip)

    a block to call after each successful authentication attempt

Yield Parameters:

  • user (String)
  • ip (String)


468
469
470
471
472
473
# File 'lib/pry-remote-em/server.rb', line 468

def auth_ok(*args, &blk)
  block_given? ? @auth_ok_cbs << blk : @auth_oks.push(args)
  while (ok_data = @auth_oks.shift)
    @auth_ok_cbs.each { |cb| cb.call(*ok_data) }
  end
end

#authenticated!Object



399
400
401
402
403
# File 'lib/pry-remote-em/server.rb', line 399

def authenticated!
  while (aa = @after_auth.shift)
    aa.call
  end
end

#completion_proc=(compl) ⇒ Object



499
500
501
# File 'lib/pry-remote-em/server.rb', line 499

def completion_proc=(compl)
  @compl_proc = compl
end

#flushObject



507
508
509
# File 'lib/pry-remote-em/server.rb', line 507

def flush
  true
end

#initialize(opts = {}) ⇒ Object

class << self



214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
# File 'lib/pry-remote-em/server.rb', line 214

def initialize(opts = {})
  @obj              = opts[:target]
  @opts             = opts
  @allow_shell_cmds = opts[:allow_shell_cmds]
  @log              = opts[:logger] || ::Logger.new(STDERR)
  # Blocks that will be called on each authentication attempt, prior checking the credentials
  @auth_attempt_cbs = []
  # All authentication attempts that occured before an auth callback was registered
  @auth_attempts    = []
  # Blocks that will be called on each failed authentication attempt
  @auth_fail_cbs    = []
  # All failed authentication attempts that occured before an auth callback was reigstered
  @auth_fails       = []
  # Blocks that will be called on successful authentication attempt
  @auth_ok_cbs      = []
  # All successful authentication attemps that occured before the auth ok callbacks were registered
  @auth_oks         = []

  # The number maximum number of authentication attempts that are permitted
  @auth_tries       = 5
  # Data to be sent after the user successfully authenticates if authentication is required
  @after_auth       = []
  return unless (a = opts[:auth])
  if a.is_a?(Proc)
    return fail("auth handler Procs must take two arguments not (#{a.arity})") unless a.arity == 2
    @auth = a
  elsif a.respond_to?(:call)
    return fail("auth handler must take two arguments not (#{a.method(:call).arity})") unless a.method(:call).arity == 2
    @auth = a
  else
    return error('auth handler objects must respond to :call, or :[]') unless a.respond_to?(:[])
    @auth = lambda {|u,p| a[u] && a[u] == p }
  end
end

#peer_ipObject



286
287
288
289
290
291
# File 'lib/pry-remote-em/server.rb', line 286

def peer_ip
  return @peer_ip if @peer_ip
  return '' if get_peername.nil?
  @peer_port, @peer_ip = Socket.unpack_sockaddr_in(get_peername)
  @peer_ip
end

#peer_portObject



293
294
295
296
297
298
# File 'lib/pry-remote-em/server.rb', line 293

def peer_port
  return @peer_port if @peer_port
  return '' if get_peername.nil?
  @peer_port, @peer_ip = Socket.unpack_sockaddr_in(get_peername)
  @peer_port
end

#peers(all = false) ⇒ Object



410
411
412
413
414
# File 'lib/pry-remote-em/server.rb', line 410

def peers(all = false)
  plist = (all ? PryRemoteEm::Server.peers : PryRemoteEm::Server.peers(@obj)).clone
  plist.delete(self)
  plist
end

#post_initObject



254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
# File 'lib/pry-remote-em/server.rb', line 254

def post_init
  @lines = []
  @auth_required  = @auth
  @port, @ip        = Socket.unpack_sockaddr_in(get_peername)
  @log.info("[pry-remote-em] received client connection from #{@ip}:#{@port}")
  # TODO include first level prompt in banner
  send_banner("PryRemoteEm #{VERSION} #{@opts[:tls] ? 'pryems' : 'pryem'}")
  need_new_line = false
  if @opts[:details].any?
    send_raw("\nServer details:\n#{@opts[:details].map { |key, value| "  #{key}: #{value}" } * "\n"}\n")
    need_new_line = true
  end
  if PryRemoteEm::Metrics.any?
    send_raw("\nServer metrics:\n#{PryRemoteEm::Metrics.list.map { |key, value| "  #{key}: #{value}" } * "\n"}\n")
    need_new_line = true
  end
  send_raw("\n") if need_new_line
  @log.info("#{url} PryRemoteEm #{VERSION} #{@opts[:tls] ? 'pryems' : 'pryem'}")
  @opts[:tls] ? start_tls : (@auth_required && send_auth(false))
  PryRemoteEm::Server.register(@obj, self)
end


489
490
491
# File 'lib/pry-remote-em/server.rb', line 489

def print(val)
  @auth_required ? (after_auth { send_raw(val) }) : send_raw(val)
end

#puts(data = '') ⇒ Object



494
495
496
497
# File 'lib/pry-remote-em/server.rb', line 494

def puts(data = '')
  s = data.to_s
  print(s[0] == "\n" ? s : s + "\n")
end

#readline(prompt) ⇒ Object

Methods that make Server compatible with Pry



481
482
483
484
485
486
487
# File 'lib/pry-remote-em/server.rb', line 481

def readline(prompt)
  @last_prompt = prompt
  @auth_required ? (after_auth { send_prompt(prompt) }) : send_prompt(prompt)
  return @lines.shift unless @lines.empty?
  @waiting = Fiber.current
  return Fiber.yield
end

#receive_auth(user, pass) ⇒ Object



330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
# File 'lib/pry-remote-em/server.rb', line 330

def receive_auth(user, pass)
  return send_auth(true) if !@auth || !@auth_required
  return send_auth('auth data must include a user and pass') if user.nil? || pass.nil?
  auth_attempt(user, peer_ip)
  unless (@auth_required = !@auth.call(user, pass))
    @user = user
    auth_ok(user, peer_ip)
    authenticated!
  else
   auth_fail(user, peer_ip)
    if @auth_tries <= 0
      msg = 'max authentication attempts reached'
      send_auth(msg)
      @log.debug("[pry-remote-em] #{msg} (#{peer_ip}:#{peer_port})")
      return close_connection_after_writing
    end
    @auth_tries -= 1
  end
  return send_auth(!@auth_required)
end

#receive_clear_bufferObject



300
301
302
303
304
# File 'lib/pry-remote-em/server.rb', line 300

def receive_clear_buffer
  @opts[:pry].eval_string.replace('')
  @last_prompt = @opts[:pry].select_prompt
  send_last_prompt
end

#receive_completion(c) ⇒ Object

tab completion request



325
326
327
328
# File 'lib/pry-remote-em/server.rb', line 325

def receive_completion(c)
  return if require_auth
  send_completion(@compl_proc.call(c))
end

#receive_msg(m) ⇒ Object



351
352
353
354
355
# File 'lib/pry-remote-em/server.rb', line 351

def receive_msg(m)
  return if require_auth
  peers.each { |peer| peer.send_message(m, @user) }
  send_last_prompt
end

#receive_msg_bcast(mb) ⇒ Object



357
358
359
360
361
# File 'lib/pry-remote-em/server.rb', line 357

def receive_msg_bcast(mb)
  return if require_auth
  peers(:all).each { |peer| peer.send_bmessage(mb, @user) }
  send_last_prompt
end

#receive_raw(d) ⇒ Object



306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
# File 'lib/pry-remote-em/server.rb', line 306

def receive_raw(d)
  return if require_auth

  return send_last_prompt if d.nil?

  if d.empty?
    @lines.push('')
  else
    lines = d.split("\n")
    @lines.push(*lines)
  end

  if @waiting
    f, @waiting = @waiting, nil
    f.resume(@lines.shift)
  end
end

#receive_shell_cmd(cmd) ⇒ Object



363
364
365
366
367
368
369
370
371
372
373
374
# File 'lib/pry-remote-em/server.rb', line 363

def receive_shell_cmd(cmd)
  return if require_auth
  unless @allow_shell_cmds
    puts "\033[1mshell commands are not allowed by this server\033[0m"
    @log.error("refused to execute shell command '#{cmd}' for #{@user} (#{peer_ip}:#{peer_port})")
    send_shell_result(-1)
    send_last_prompt
  else
    @log.warn("executing shell command '#{cmd}' for #{@user} (#{peer_ip}:#{peer_port})")
    @shell_cmd = EM.popen3(cmd, ShellCmd, self)
  end
end

#receive_shell_data(d) ⇒ Object



376
377
378
379
# File 'lib/pry-remote-em/server.rb', line 376

def receive_shell_data(d)
  return if require_auth
  @shell_cmd.send_data(d)
end

#receive_shell_sig(type) ⇒ Object



381
382
383
384
# File 'lib/pry-remote-em/server.rb', line 381

def receive_shell_sig(type)
  return if require_auth
  @shell_cmd.close_connection if type == :int
end

#receive_unknown(j) ⇒ Object



386
387
388
389
390
391
# File 'lib/pry-remote-em/server.rb', line 386

def receive_unknown(j)
  return if require_auth
  warn "received unexpected data: #{j.inspect}"
  send_error("received unexpected data: #{j.inspect}")
  send_last_prompt
end

#require_authObject



393
394
395
396
397
# File 'lib/pry-remote-em/server.rb', line 393

def require_auth
  return false if !@auth_required
  send_auth(false)
  true
end

#send_bmessage(msg, from = nil) ⇒ Object

Sends a chat message to the client.



432
433
434
435
# File 'lib/pry-remote-em/server.rb', line 432

def send_bmessage(msg, from = nil)
  msg = "#{msg} (@#{from})" unless from.nil?
  @auth_required ? (after_auth {send_msg_bcast(msg)}) : send_msg_bcast(msg)
end

#send_error(msg) ⇒ Object

auth_fail(*args, &blk)



475
476
477
# File 'lib/pry-remote-em/server.rb', line 475

def send_error(msg)
  puts "\033[31m#{msg}\033[0m"
end

#send_last_promptObject



416
417
418
# File 'lib/pry-remote-em/server.rb', line 416

def send_last_prompt
  @auth_required ? (after_auth { send_prompt(@last_prompt) }) : send_prompt(@last_prompt)
end

#send_message(msg, from = nil) ⇒ Object

Sends a chat message to the client.



426
427
428
429
# File 'lib/pry-remote-em/server.rb', line 426

def send_message(msg, from = nil)
  msg = "#{msg} (@#{from})" unless from.nil?
  @auth_required ? (after_auth {send_msg(msg)}) : send_msg(msg)
end

#ssl_handshake_completedObject



281
282
283
284
# File 'lib/pry-remote-em/server.rb', line 281

def ssl_handshake_completed
  @log.info("[pry-remote-em] TLS connection established (#{peer_ip}:#{peer_port})")
  send_auth(false) if @auth_required
end

#start_tlsObject



276
277
278
279
# File 'lib/pry-remote-em/server.rb', line 276

def start_tls
  @log.debug("[pry-remote-em] starting TLS (#{peer_ip}:#{peer_port})")
  super(@opts[:tls].is_a?(Hash) ? @opts[:tls] : {})
end

#tty?Boolean

Returns:

  • (Boolean)


503
504
505
# File 'lib/pry-remote-em/server.rb', line 503

def tty?
  true # might be a very bad idea ....
end

#unbindObject



405
406
407
408
# File 'lib/pry-remote-em/server.rb', line 405

def unbind
  PryRemoteEm::Server.unregister(@obj, self)
  @log.debug("[pry-remote-em] remote session terminated (#{@ip}:#{@port})")
end

#urlObject



249
250
251
252
# File 'lib/pry-remote-em/server.rb', line 249

def url
  port, host = Socket.unpack_sockaddr_in(get_sockname)
  "#{@opts[:tls] ? 'pryems' : 'pryem'}://#{host}:#{port}/"
end