From e18b9e807844ae2c2077d8020ed6f7b6962dda9f Mon Sep 17 00:00:00 2001 From: kris kechagia Date: Sat, 5 May 2012 18:22:33 +0200 Subject: [PATCH] upgrades a lot of stuff, new functions migrated from cjdns-tool, version bump --- lib/cjdns-lib.rb | 143 +------------------------- lib/cjdns-lib/host.rb | 117 +++++++++++++++++++++ lib/cjdns-lib/hypedns.rb | 60 +++++++++++ lib/cjdns-lib/interface.rb | 181 +++++++++++++++++++++++++++++++++ lib/cjdns-lib/route.rb | 37 +++++++ lib/cjdns-lib/routing_table.rb | 58 +++++++++++ lib/cjdns-lib/version.rb | 2 +- 7 files changed, 457 insertions(+), 141 deletions(-) create mode 100644 lib/cjdns-lib/host.rb create mode 100644 lib/cjdns-lib/hypedns.rb create mode 100644 lib/cjdns-lib/interface.rb create mode 100644 lib/cjdns-lib/route.rb create mode 100644 lib/cjdns-lib/routing_table.rb diff --git a/lib/cjdns-lib.rb b/lib/cjdns-lib.rb index e230de6..eae9acd 100644 --- a/lib/cjdns-lib.rb +++ b/lib/cjdns-lib.rb @@ -1,144 +1,7 @@ -require 'rubygems' -require 'bencode' -require 'socket' -require 'digest' +require 'cjdns-lib/interface' +require 'cjdns-lib/routing_table' +require 'cjdns-lib/hypedns' require 'cjdns-lib/version' module Cjdns - class Lib - - def initialize(options = {}) - options = { 'host' => 'localhost', 'port' => 11234, 'password' => nil, 'debug' => false }.merge options - - @password = options['password'] - @debug = options['debug'] - - puts "connecting to #{options['host']}:#{options['port']}" if @debug - @socket = TCPSocket.open(options['host'], options['port']) - raise "#{host}:#{port} doesn't appear to be a cjdns socket" unless ping_self - end - - def ping_self - return false unless auth_send('ping')['q'] == 'pong' - true - end - - def memory - return auth_send('memory')['bytes'] - end - - def ping_node(path, timeout = 5000) - response = auth_send('RouterModule_pingNode', { 'path' => path, 'timeout' => timeout } ) - return response['ms'] if response['result'] == 'pong' - false - end - - def dump_table - page = 0 - routing_table = [] - begin - response = auth_send('NodeStore_dumpTable', 'page' => page) - - # add received routing table - routing_table = routing_table + response['routingTable'] - - # if 'more' is set, there's more data to come, request next page - page += 1 - end while response['more'] - - routing_table - end - - def ping_switch(path, data = 'x', timeout = 5000) - response = auth_send('SwitchPinger_ping', { 'path' => path, - 'data' => data, - 'timeout' => timeout } ) - - return response['ms'] if response['result'] == 'pong' - false - end - - def lookup(address) - return auth_send('RouterModule_lookup', { 'address' => address } ) - end - - def authorized_passwords_add(password, auth_type = 1) - return auth_send('AuthorizedPasswords_add', { 'password' => password, - 'authType' => auth_type } ) - end - - def authorized_passwords_flush - return auth_send('AuthorizedPasswords_flush') - end - - def scramble_keys(xor_value) - return auth_send('UDPInterface_scrambleKeys', { 'xorValue' => xor_value } ) - end - - def begin_connection(public_key, address, password = nil) - return auth_send('UDPInterface_beginConnection', { 'publicKey' => public_key, - 'address' => address, - 'password' => password } ) - end - - - private - - def auth_send(funcname, args = nil) - txid = rand(36**8).to_s(36) - - # setup authenticated request if password given - if @password - cookie = get_cookie - - request = { - 'q' => 'auth', - 'aq' => funcname, - 'hash' => Digest::SHA256.hexdigest(@password + cookie), - 'cookie' => cookie, - 'txid' => txid - } - - request['args'] = args if args - request['hash'] = Digest::SHA256.hexdigest(request.bencode) - - # if no password is given, try request without auth - else - request = { 'q' => funcname, 'txid' => txid } - request['args'] = args if args - end - - response = send request - raise 'wrong txid in reply' if response['txid'] and response['txid'] != txid - response - end - - def get_cookie - txid = rand(36**8).to_s(36) - response = send('q' => 'cookie', 'txid' => txid) - raise 'wrong txid in reply' if response['txid'] and response['txid'] != txid - response['cookie'] - end - - def send(request) - # clear socket - puts "flushing socket" if @debug - @socket.flush - - puts "sending request: #{request.inspect}" if @debug - response = '' - @socket.puts request.bencode - - while r = @socket.recv(1024) - response << r - break if r.length < 1024 - end - - puts "bencoded reply: #{response.inspect}" if @debug - response = response.bdecode - - puts "bdecoded reply: #{response.inspect}" if @debug - response - end - end end diff --git a/lib/cjdns-lib/host.rb b/lib/cjdns-lib/host.rb new file mode 100644 index 0000000..2266426 --- /dev/null +++ b/lib/cjdns-lib/host.rb @@ -0,0 +1,117 @@ +require 'socket' + +module CJDNS + class Host + attr_reader :ip, :cjdns + + # @param [String] ip + # @param [Cjdns::Interface] cjdns + # @param [Hash] options options for CJDNS::Interface + def initialize(ip, cjdns = nil, options = {}) + @ip = ip + + # connect to cjdns socket, unless given + cjdns = CJDNS::Interface.new(options) unless cjdns + @cjdns = cjdns + end + + # TCP pings host (port 7) + # + # @param [Int] timeout + # @return [Hash] { 'time' => [Int] response_time } + # @return [Boolean] false if host is not responding + def ping_tcp(timeout = 5) + start = Time.new + + begin + s = connect(7, timeout) + return false unless s + s.close + rescue Errno::ECONNREFUSED + # connection refused means host is alive + rescue Errno::EHOSTUNREACH, Errno::ETIMEDOUT + return false + end + + return { 'time' => (Time.new - start) * 1000 } + end + + # HTTP ping (port 80) + # + # @param [Int] timeout + # @return [Hash] { 'time' => [Int] response_time, 'title' => [String] html_title (if found) } + # @return [Boolean] false if host is not replying to http + def ping_http(timeout = 5) + response = {} + start = Time.new + + begin + s = connect(80, timeout) + return false unless s + + s.write "GET / HTTP/1.1\r\nHost: [#{@ip}]\r\nConnection: close\r\n\r\n" + + s.read.each_line do |line| + line.force_encoding 'utf-8' unless RUBY_VERSION < '1.9' + if md = (/\s*(.*)\s*<\/title>/iu).match(line) + response['title'] = md[1] + end + end + + s.close + rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ETIMEDOUT, Errno::EPIPE, Errno::EINVAL + return false + end + + response['time'] = (Time.new - start) * 1000 + return response + end + + # cjdns internal ping + # + # @param [Int] timeout + # @return [Hash] { 'time' => [Int] response_time } + # @return [Boolean] false if host is not replying + ### TODO + def ping_cjdns(timeout = 1) + time = @cjdns.ping_node(@ip, timeout * 1000) + return false unless time + return { 'time' => time } + end + + + private + + # use nonblocking socket to connect to port, respecting timeout + # + # @param [Int] port + # @param [Int] timeout + # @return [Socket] on success, nil on failure + def connect(port, timeout = 5) + s = Socket.open(Socket::AF_INET6, Socket::SOCK_STREAM, 0) + + begin + s.connect_nonblock(Socket.sockaddr_in(port, @ip)) + rescue Errno::EINPROGRESS + # block until the socket is ready, then try again + IO.select([s], [s], [s], timeout) + + begin + s.connect_nonblock(Socket.sockaddr_in(port, @ip)) + rescue Errno::EISCONN + # already connected, do nothing + rescue Errno::EINPROGRESS, Errno::EALREADY + # connection still in progress, this means we timed out given + # our IO.select has returned. + s.close + return nil + rescue Errno::EINVAL, Errno::EACCES + # invalid argument errors or permission denied errors + # rise once in a while on the secoond connect, ignore + end + end + s + end + + end +end diff --git a/lib/cjdns-lib/hypedns.rb b/lib/cjdns-lib/hypedns.rb new file mode 100644 index 0000000..df99448 --- /dev/null +++ b/lib/cjdns-lib/hypedns.rb @@ -0,0 +1,60 @@ +require 'rubygems' +require 'resolv' +require 'ipaddress' + +module CJDNS + class HypeDNS + + # @param [String] nameserver (either ip, or 'via_internet' / 'via_cjdns' to use default) + def initialize(nameserver = 'via_cjdns') + nameserver = '216.150.225.240' if nameserver == 'via_internet' + nameserver = 'fc5d:baa5:61fc:6ffd:9554:67f0:e290:7535' if nameserver == 'via_cjdns' + + @hypedns = Resolv::DNS.new(:nameserver => nameserver) + end + + # get AAAA (ipv6) record of host + # + # @param [String] host + # @return [String] ip, nil on failure + def aaaa(host) + return nil if @disabled + + begin + @hypedns.getresource(host, Resolv::DNS::Resource::IN::AAAA).address.to_s.downcase + rescue Resolv::ResolvError + return nil + end + end + + # get PTR record for ip + # + # @param [String] ip + # @return [String] host, nil on failure + def ptr(ip) + return nil if @disabled + + begin + return @hypedns.getname(ip).to_s + rescue Resolv::ResolvError + return nil + end + end + + # resolv host unless it already a valid ipv6 address + # + # @param [String] host + # @return [String] ip|host, nil on failure or if host = nil + def aaaa_unless_ip(host = nil) + return nil unless host + + if IPAddress.valid_ipv6? host + return host + else + return aaaa host + end + end + + end +end + diff --git a/lib/cjdns-lib/interface.rb b/lib/cjdns-lib/interface.rb new file mode 100644 index 0000000..acef2f3 --- /dev/null +++ b/lib/cjdns-lib/interface.rb @@ -0,0 +1,181 @@ +require 'rubygems' +require 'bencode' +require 'socket' +require 'digest' + +module CJDNS + class Interface + + # @option options [String] host + # @option options [Int] port + # @option options [String] password + # @option options [Boolean] debug + # @raise [RuntimeError] if socket is not a valid cjdns socket + def initialize(options = {}) + options = { 'host' => 'localhost', 'port' => 11234, 'password' => nil, 'debug' => false }.merge options + + @password = options['password'] + @debug = options['debug'] + + puts "connecting to #{options['host']}:#{options['port']}" if @debug + @socket = TCPSocket.open(options['host'], options['port']) + raise "#{host}:#{port} doesn't appear to be a cjdns socket" unless ping_self + end + + # @return [Boolean] true if cjdns socket replies + def ping_self + return false unless auth_send('ping')['q'] == 'pong' + true + end + + # @return [Int] bytes + def memory + return auth_send('memory')['bytes'] + end + + # @param [String] path to node + # @param [Int] timeout + def ping_node(path, timeout = 5000) + response = auth_send('RouterModule_pingNode', { 'path' => path, 'timeout' => timeout } ) + return response['ms'] if response['result'] == 'pong' + false + end + + # @return [Hash] routing table + def dump_table + page = 0 + routing_table = [] + begin + response = auth_send('NodeStore_dumpTable', 'page' => page) + + # add received routing table + routing_table = routing_table + response['routingTable'] + + # if 'more' is set, there's more data to come, request next page + page += 1 + end while response['more'] + + routing_table + end + + # @param [String] path + # @param [String] data + # @param [Int] timeout + # @return [Boolean] true if path socket replies + def ping_switch(path, data = 'x', timeout = 5000) + response = auth_send('SwitchPinger_ping', { 'path' => path, + 'data' => data, + 'timeout' => timeout } ) + + return response['ms'] if response['result'] == 'pong' + false + end + + # @param [String] address + # @return [Hash] + def lookup(address) + return auth_send('RouterModule_lookup', { 'address' => address } ) + end + + # @param [String] password + # @param [Int] auth_type + # @return [Hash] + def authorized_passwords_add(password, auth_type = 1) + return auth_send('AuthorizedPasswords_add', { 'password' => password, + 'authType' => auth_type } ) + end + + # @return [Hash] + def authorized_passwords_flush + return auth_send('AuthorizedPasswords_flush') + end + + # @param [String] xor_value + # @return [Hash] + def scramble_keys(xor_value) + return auth_send('UDPInterface_scrambleKeys', { 'xorValue' => xor_value } ) + end + + # @param [String] publicKey + # @param [String] address + # @param [String] password + # @return [Hash] + def begin_connection(public_key, address, password = nil) + return auth_send('UDPInterface_beginConnection', { 'publicKey' => public_key, + 'address' => address, + 'password' => password } ) + end + + + private + + # send an authenticated 'funcname' request to the admin interface + # + # @param [String] funcname + # @param [Hash] args + # @return [Hash] + def auth_send(funcname, args = nil) + txid = rand(36**8).to_s(36) + + # setup authenticated request if password given + if @password + cookie = get_cookie + + request = { + 'q' => 'auth', + 'aq' => funcname, + 'hash' => Digest::SHA256.hexdigest(@password + cookie), + 'cookie' => cookie, + 'txid' => txid + } + + request['args'] = args if args + request['hash'] = Digest::SHA256.hexdigest(request.bencode) + + # if no password is given, try request without auth + else + request = { 'q' => funcname, 'txid' => txid } + request['args'] = args if args + end + + response = send request + raise 'wrong txid in reply' if response['txid'] and response['txid'] != txid + response + end + + # get a cookie from server + # + # @return [String] + def get_cookie + txid = rand(36**8).to_s(36) + response = send('q' => 'cookie', 'txid' => txid) + raise 'wrong txid in reply' if response['txid'] and response['txid'] != txid + response['cookie'] + end + + # send a request to the admin interface + # + # @param [Hash] request + # @return [Hash] + def send(request) + # clear socket + puts "flushing socket" if @debug + @socket.flush + + puts "sending request: #{request.inspect}" if @debug + response = '' + @socket.puts request.bencode + + while r = @socket.recv(1024) + response << r + break if r.length < 1024 + end + + puts "bencoded reply: #{response.inspect}" if @debug + response = response.bdecode + + puts "bdecoded reply: #{response.inspect}" if @debug + response + end + end +end diff --git a/lib/cjdns-lib/route.rb b/lib/cjdns-lib/route.rb new file mode 100644 index 0000000..2357b64 --- /dev/null +++ b/lib/cjdns-lib/route.rb @@ -0,0 +1,37 @@ +module CJDNS + class Route + attr_reader :path, :ip, :link, :quality, :routing_table + + # @param [Cjdns::RoutingTable] routing_table + # @param [String] ip + # @param [String] path + # @param [String] link + def initialize(routing_table, ip, path, link) + @routing_table = routing_table + @ip = ip + @link = link + + # convert path to binary + @path = path.gsub('.','').hex.to_s(2) + + # calculate quality using LINK_STATE_MULTIPLIER + @quality = @link / 5366870.0 + end + + # get all possible routes to a host + # + # @return [Hash] routes + def get_hops + hops = [] + @routing_table.routes.each do |r| + # for more information, read the switch section in the whitepaper + next if self == r + next unless @path.end_with? r.path[1..-1] + hops << r + end + + # puts hops in right order + hops.sort_by { |h| h.path.to_i } + end + end +end diff --git a/lib/cjdns-lib/routing_table.rb b/lib/cjdns-lib/routing_table.rb new file mode 100644 index 0000000..11787dd --- /dev/null +++ b/lib/cjdns-lib/routing_table.rb @@ -0,0 +1,58 @@ +require 'rubygems' +require 'bencode' + +require 'cjdns-lib/interface' +require 'cjdns-lib/host' +require 'cjdns-lib/route' + +module CJDNS + class RoutingTable + + attr_reader :routes, :hosts + + # @param [Cjdns::Interface] cjdns + # @param [Hash] options options for CJDNS::Interface + def initialize(cjdns = nil, options = {}) + # connect to cjdns socket, unless given + cjdns = CJDNS::Interface.new(options) unless cjdns + + routing_table = cjdns.dump_table + + # populate routes + @routes = [] + routing_table.each do |route| + @routes << Route.new(self, route['ip'], route['path'], route['link']) + end + + # populate hosts + @hosts = [] + @routes.each do |r| + next unless r.link > 0 + next unless @hosts.select { |h| h.ip == r.ip }.length == 0 + @hosts << Host.new(r.ip, cjdns) + end + end + + # get all routes (for host) + # + # @param [String] host get routes to this host only + # @param [Int] max_hops onyl get routes with up to max_hops hops + def get_routes(host = nil, max_hops = nil) + routes = {} + @routes.each do |r| + next if host and host != r.ip # skip if not requested + next unless r.link > 0 # skip dead links + hops = r.get_hops + + # skip if not enough hops + next if max_hops and hops.length > max_hops + + hops << r # add target as last hop + routes[r.ip] ||= [] + routes[r.ip] << hops + end + + routes + end + end +end diff --git a/lib/cjdns-lib/version.rb b/lib/cjdns-lib/version.rb index 91085af..dbfd737 100644 --- a/lib/cjdns-lib/version.rb +++ b/lib/cjdns-lib/version.rb @@ -1,5 +1,5 @@ module Cjdns class Lib - VERSION = "0.1.0" + VERSION = "0.2.0" end end