class Snxvpn::CLI

Attributes

config[R]
http[R]

Public Class Methods

new(profile, config_path: File.join(ENV['HOME'], '.snxvpn')) click to toggle source
# File lib/snxvpn/cli.rb, line 16
def initialize(profile, config_path: File.join(ENV['HOME'], '.snxvpn'))
  @config = Config.new(config_path, profile)
  @http = Net::HTTP.new(@config[:host], 443)
  @http.use_ssl = true
  @cookies = ["selected_realm=#{@config[:realm]}"]
end

Public Instance Methods

run(retries = 10) click to toggle source
# File lib/snxvpn/cli.rb, line 23
def run(retries = 10)
  # find RSA.js
  resp       = get(config[:entry_path])
  rsa_path   = resp.body.match(/<script .*?src *\= *["'](.*RSA.js)["']/).to_a[1]
  raise RetryableError, "Unable to detect a RSA.js script reference on login page" unless rsa_path

  # store paths
  login_path = resp.uri
  rsa_path   = File.expand_path(rsa_path, File.dirname(login_path))

  # fetch RSA.js and parse RSA
  rsa  = RSA.parse(get(rsa_path).body)
  raise RetryableError "Unable to detect modulus/exponent in RSA.js script" unless rsa

  # post to login
  resp = post(login_path,
    password: rsa.hex_encrypt(config[:password]),
    userName: config[:username],
    selectedRealm: config[:realm],
    loginType: config[:login_type],
    HeightData: '',
    vpid_prefix: '',
  )
  raise RetryableError, "Expected redirect to multi-challenge, but got #{resp.uri}" unless resp.uri.include?('MultiChallenge')

  # request OTP until successful
  inputs  = resp.body.scan(/<input.*?type="hidden".*?name="(username|params|HeightData)"(?:.*?value="(.+?)")?/)
  payload = Hash[inputs]
  while resp.uri.include?('MultiChallenge')
    print " + Enter one-time password: "
    otp  = gets.strip
    payload['password'] = rsa.hex_encrypt(otp)
    resp = post(resp.uri, payload)
  end

  # request extender info
  ext_info = ExtInfo.new get("/SNX/extender").body
  raise RetryableError, "Unable to retrieve extender information" if ext_info.empty?

  output, status = Open3.capture2(config[:snx_path], '-Z')
  raise RetryableError, "Unable to start snx: #{output}" unless status.success?

  Socket.tcp('127.0.0.1', 7776) do |sock|
    sock.write(ext_info.payload)
    sock.recv(4096) # read answer
    puts ' = Connected! Please leave this running to keep VPN open.'
    sock.recv(4096) # block until snx process dies
  end

  puts ' ! Connection closed. Exiting...'
rescue RetryableError
  raise if retries < 1

  puts ' ! #{e.message}. Retrying...'
  sleep 1
  run(retries - 1)
end

Private Instance Methods

get(path, retries = 10) click to toggle source
# File lib/snxvpn/cli.rb, line 83
def get(path, retries = 10)
  raise ArgumentError, 'too many HTTP redirects' if retries.zero?

  resp = @http.get(path, headers)
  handle_response(path, resp)

  case resp
  when Net::HTTPSuccess then
    resp
  when Net::HTTPRedirection then
    get(resp['location'], retries - 1)
  when Net::HTTPNotFound then
    raise "unexpected response from GET #{path} - #{resp.code} #{resp.message}" unless path.include?(config[:entry_path])
    resp
  else
    raise "unexpected response from GET #{path} - #{resp.code} #{resp.message}"
  end
end
handle_response(path, resp) click to toggle source
# File lib/snxvpn/cli.rb, line 120
def handle_response(path, resp)
  resp.uri = path
  @cookies += Array(resp.get_fields('set-cookie')).map do |str|
    str.split('; ')[0]
  end
  @cookies.uniq!
end
headers() click to toggle source
# File lib/snxvpn/cli.rb, line 116
def headers
  {'Cookie' => @cookies.join('; ')} unless @cookies.empty?
end
post(path, payload) click to toggle source
# File lib/snxvpn/cli.rb, line 102
def post(path, payload)
  resp = @http.post(path, URI.encode_www_form(payload), headers)
  handle_response(path, resp)

  case resp
  when Net::HTTPSuccess then
    resp
  when Net::HTTPRedirection then
    get(resp['location'])
  else
    raise "unexpected response from POST #{path} - #{resp.code} #{resp.message}"
  end
end