class CFTunnel

Constants

HELPER_APP
HELPER_NAME
HELPER_VERSION

bump this AND the version info reported by HELPER_APP/server.rb this is to keep the helper in sync with any updates here

PORT_RANGE
TUNNEL_CHECK_LIMIT

Public Class Methods

new(client, service, port = 10000) click to toggle source
# File lib/tunnel/tunnel.rb, line 15
def initialize(client, service, port = 10000)
  @client = client
  @service = service
  @port = port
end

Public Instance Methods

open!() click to toggle source
# File lib/tunnel/tunnel.rb, line 21
def open!
  if helper
    auth = helper_auth

    unless helper_healthy?(auth)
      delete_helper
      auth = create_helper
    end
  else
    auth = create_helper
  end

  bind_to_helper if @service && !helper_already_binds?

  info = get_connection_info(auth)

  start_tunnel(info, auth)

  info
end
pick_port!(port = @port) click to toggle source
# File lib/tunnel/tunnel.rb, line 64
def pick_port!(port = @port)
  original = port

  PORT_RANGE.times do |n|
    begin
      TCPSocket.open("localhost", port)
      port += 1
    rescue
      return @port = port
    end
  end

  @port = grab_ephemeral_port
end
wait_for_end() click to toggle source
# File lib/tunnel/tunnel.rb, line 55
def wait_for_end
  if @local_tunnel_thread
    @local_tunnel_thread.join
  else
    raise "Tunnel wasn't started!"
  end
end
wait_for_start() click to toggle source
# File lib/tunnel/tunnel.rb, line 42
def wait_for_start
  10.times do |n|
    begin
      TCPSocket.open("localhost", @port).close
      return true
    rescue => e
      sleep 1
    end
  end

  raise "Could not connect to local tunnel."
end

Private Instance Methods

bind_to_helper() click to toggle source
# File lib/tunnel/tunnel.rb, line 174
def bind_to_helper
  helper.bind(@service)
  helper.restart!
end
create_helper() click to toggle source
# File lib/tunnel/tunnel.rb, line 85
def create_helper
  auth = UUIDTools::UUID.random_create.to_s
  push_helper(auth)
  start_helper
  auth
end
delete_helper() click to toggle source
# File lib/tunnel/tunnel.rb, line 148
def delete_helper
  helper.delete!
  invalidate_tunnel_app_info
end
get_connection_info(token) click to toggle source
# File lib/tunnel/tunnel.rb, line 206
def get_connection_info(token)
  response = nil
  10.times do
    begin
      response =
        RestClient.get(
          helper_url + "/" + safe_path("services", @service.name),
          "Auth-Token" => token)

      break
    rescue RestClient::Exception => e
      sleep 1
    end
  end

  unless response
    raise "Remote tunnel helper is unaware of #{@service.name}!"
  end

  is_v2 = @client.is_a?(CFoundry::V2::Client)

  info = JSON.parse(response)
  case (is_v2 ? @service.service_plan.service.label : @service.vendor)
  when "rabbitmq"
    uri = Addressable::URI.parse info["url"]
    info["hostname"] = uri.host
    info["port"] = uri.port
    info["vhost"] = uri.path[1..-1]
    info["user"] = uri.user
    info["password"] = uri.password
    info.delete "url"

  # we use "db" as the "name" for mongo
  # existing "name" is junk
  when "mongodb"
    info["name"] = info["db"]
    info.delete "db"

  # our "name" is irrelevant for redis
  when "redis"
    info.delete "name"

  when "filesystem"
    raise "Tunneling is not supported for this type of service"
  end

  ["hostname", "port", "password"].each do |k|
    raise "Could not determine #{k} for #{@service.name}" if info[k].nil?
  end

  info
end
grab_ephemeral_port() click to toggle source
# File lib/tunnel/tunnel.rb, line 287
def grab_ephemeral_port
  socket = TCPServer.new("0.0.0.0", 0)
  socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
  Socket.do_not_reverse_lookup = true
  socket.addr[1]
ensure
  socket.close
end
helper() click to toggle source
# File lib/tunnel/tunnel.rb, line 81
def helper
  @helper ||= @client.app_by_name(HELPER_NAME)
end
helper_already_binds?() click to toggle source
# File lib/tunnel/tunnel.rb, line 118
def helper_already_binds?
  helper.binds? @service
end
helper_auth() click to toggle source
# File lib/tunnel/tunnel.rb, line 92
def helper_auth
  helper.env["CALDECOTT_AUTH"]
end
helper_healthy?(token) click to toggle source
# File lib/tunnel/tunnel.rb, line 96
def helper_healthy?(token)
  return false unless helper.healthy?

  begin
    response = RestClient.get(
      "#{helper_url}/info",
      "Auth-Token" => token
    )

    info = JSON.parse(response)
    if info["version"] == HELPER_VERSION
      true
    else
      stop_helper
      false
    end
  rescue RestClient::Exception
    stop_helper
    false
  end
end
helper_url() click to toggle source
# File lib/tunnel/tunnel.rb, line 184
def helper_url
  return @helper_url if @helper_url

  tun_url = helper.url

  ["https", "http"].each do |scheme|
    url = "#{scheme}://#{tun_url}"
    begin
      RestClient.get(url)

    # https failed
    rescue Errno::ECONNREFUSED

    # we expect a 404 since this request isn't auth'd
    rescue RestClient::ResourceNotFound
      return @helper_url = url
    end
  end

  raise "Cannot determine URL for #{tun_url}"
end
invalidate_tunnel_app_info() click to toggle source
# File lib/tunnel/tunnel.rb, line 179
def invalidate_tunnel_app_info
  @helper_url = nil
  @helper = nil
end
push_helper(token) click to toggle source
# File lib/tunnel/tunnel.rb, line 122
def push_helper(token)
  app = @client.app
  app.name = HELPER_NAME
  app.command = "bundle exec ruby server.rb"
  app.total_instances = 1
  app.memory = 128
  app.env = {"CALDECOTT_AUTH" => token}

  space = app.space = @client.current_space
  app.create!

  app.bind(@service) if @service

  domain = @client.domains.find { |d| d.owning_organization == nil }

  app.create_route(:domain => domain, :space => space, :host => random_helper_url)

  begin
    app.upload(HELPER_APP)
    invalidate_tunnel_app_info
  rescue
    app.delete!
    raise
  end
end
random_helper_url() click to toggle source
# File lib/tunnel/tunnel.rb, line 276
def random_helper_url
  random = sprintf("%x", rand(1000000))
  "caldecott-#{random}"
end
safe_path(*segments) click to toggle source
# File lib/tunnel/tunnel.rb, line 281
def safe_path(*segments)
  segments.flatten.collect { |x|
    URI.encode x.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]")
  }.join("/")
end
start_helper() click to toggle source
# File lib/tunnel/tunnel.rb, line 159
def start_helper
  helper.start!

  seconds = 0
  until helper.healthy?
    sleep 1
    seconds += 1
    if seconds == TUNNEL_CHECK_LIMIT
      raise "Helper application failed to start."
    end
  end

  invalidate_tunnel_app_info
end
start_tunnel(conn_info, auth) click to toggle source
# File lib/tunnel/tunnel.rb, line 259
def start_tunnel(conn_info, auth)
  @local_tunnel_thread = Thread.new do
    Caldecott::Client.start({
      :local_port => @port,
      :tun_url => helper_url,
      :dst_host => conn_info["hostname"],
      :dst_port => conn_info["port"],
      :log_file => STDOUT,
      :log_level => ENV["CF_TUNNEL_DEBUG"] || "ERROR",
      :auth_token => auth,
      :quiet => true
    })
  end

  at_exit { @local_tunnel_thread.kill }
end
stop_helper() click to toggle source
# File lib/tunnel/tunnel.rb, line 153
def stop_helper
  helper.stop!
  invalidate_tunnel_app_info
end