diff options
44 files changed, 852 insertions, 255 deletions
| @@ -95,6 +95,6 @@ To run the test suite, run:      rake -The tests that actually make real network calls are disabled by default. To run these tests to, do this: +The tests that actually make real network calls are enabled by default. To only run local, do this: -    REAL_NET=true rake +    ONLY_LOCAL=true rake diff --git a/lib/kernel_ext.rb b/lib/kernel_ext.rb new file mode 100644 index 0000000..b5b58e0 --- /dev/null +++ b/lib/kernel_ext.rb @@ -0,0 +1,28 @@ +module Kernel +  # Sets $VERBOSE to nil for the duration of the block and back to its original +  # value afterwards. +  # +  #   silence_warnings do +  #     value = noisy_call # no warning voiced +  #   end +  # +  #   noisy_call # warning voiced +  def silence_warnings +    with_warnings(nil) { yield } +  end + +  # Sets $VERBOSE to +true+ for the duration of the block and back to its +  # original value afterwards. +  def enable_warnings +    with_warnings(true) { yield } +  end + +  # Sets $VERBOSE for the duration of the block and back to its original +  # value afterwards. +  def with_warnings(flag) +    old_verbose, $VERBOSE = $VERBOSE, flag +    yield +  ensure +    $VERBOSE = old_verbose +  end +end diff --git a/lib/nickserver.rb b/lib/nickserver.rb index eb7eddb..8843acd 100644 --- a/lib/nickserver.rb +++ b/lib/nickserver.rb @@ -3,12 +3,5 @@ require "nickserver/version"  require "nickserver/config"  require "nickserver/email_address" -require "nickserver/couch/fetch_key" - -require "nickserver/hkp/fetch_key" -require "nickserver/hkp/fetch_key_info" -require "nickserver/hkp/parse_key_info" -require "nickserver/hkp/key_info" -  require "nickserver/server"  require "nickserver/daemon" diff --git a/lib/nickserver/adapters.rb b/lib/nickserver/adapters.rb new file mode 100644 index 0000000..c87cb63 --- /dev/null +++ b/lib/nickserver/adapters.rb @@ -0,0 +1,4 @@ +module Nickserver +  module Adapters +  end +end diff --git a/lib/nickserver/adapters/em_http.rb b/lib/nickserver/adapters/em_http.rb new file mode 100644 index 0000000..16db5ae --- /dev/null +++ b/lib/nickserver/adapters/em_http.rb @@ -0,0 +1,24 @@ +require 'nickserver/adapters' +require 'em-http' + +module Nickserver::Adapters +  class EmHttp + +    def initialize +      @timeout = 5 +    end + +    def get(url, options = {}) +      get_request(url, options).callback {|http| +        yield http.response_header.status, http.response +      }.errback {|http| +        yield 0, http.error +      } +    end + +    def get_request(url, options = {}) +      @request = EventMachine::HttpRequest.new(url) +      @request.get timeout: @timeout, query: options[:query] +    end +  end +end diff --git a/lib/nickserver/adapters/local.rb b/lib/nickserver/adapters/local.rb new file mode 100644 index 0000000..d6210c3 --- /dev/null +++ b/lib/nickserver/adapters/local.rb @@ -0,0 +1,8 @@ +require 'nickserver/adapters' + +class Nickserver::Adapters::Local + +  def query(nick) +  end + +end diff --git a/lib/nickserver/adapters/remote.rb b/lib/nickserver/adapters/remote.rb new file mode 100644 index 0000000..e12bd26 --- /dev/null +++ b/lib/nickserver/adapters/remote.rb @@ -0,0 +1,4 @@ +require 'nickserver/adapters' + +class Nickserver::Adapters::Remote +end diff --git a/lib/nickserver/config.rb b/lib/nickserver/config.rb index 46b6bef..b1def7c 100644 --- a/lib/nickserver/config.rb +++ b/lib/nickserver/config.rb @@ -36,6 +36,21 @@ module Nickserver        self.validate      end +    def self.couch_url +      [ 'http://', +        couch_auth, +        couch_host, +        ':', +        couch_port, +        '/', +        couch_database +      ].join +    end + +    def self.couch_auth +      "#{couch_user}:#{couch_password}@" if couch_user +    end +      private      def self.validate @@ -59,7 +74,7 @@ module Nickserver          YAML.load(File.read(file_path)).each do |key, value|            begin              self.send("#{key}=", value) -          rescue NoMethodError => exc +          rescue NoMethodError              STDERR.puts "ERROR in file #{file_path}, '#{key}' is not a valid option"              exit(1)            end diff --git a/lib/nickserver/couch/fetch_key.rb b/lib/nickserver/couch/fetch_key.rb deleted file mode 100644 index 3fe2a63..0000000 --- a/lib/nickserver/couch/fetch_key.rb +++ /dev/null @@ -1,63 +0,0 @@ -require 'em-http' -require 'json' - -module Nickserver; module Couch -  class FetchKey -    include EM::Deferrable - -    VIEW = "_design/Identity/_view/pgp_key_by_email" - -    def initialize(options={}) -      @timeout = 5 -    end - -    def get(uid) -      couch_request(uid) -      self -    end - -    protected - -    # -    # For example: -    # curl "$COUCH/identities/_design/Identity/_view/pgp_key_by_email?key=\"test1@bitmask.net\"" -    # -    def couch_request(uid) -      query = {"reduce" => "false", "key" => "\"#{uid}\""} -      request = EventMachine::HttpRequest.new(FetchKey.couch_url).get(timeout: @timeout, query: query) -      request.callback {|http| -        if http.response_header.status != 200 -          self.fail http.response_header.status, 'Unknown Error' -        else -          self.succeed parse_key_from_response(uid, http.response) -        end -      }.errback {|http| -        self.fail 0, http.error -      } -    end - -    def parse_key_from_response(uid, response) -      json = JSON.load(response) -      if json["rows"].empty? -        self.fail 404, "Not Found" -      else -        return json["rows"].first["value"] -      end -    rescue Exception -      self.fail 0, "Error parsing CouchDB reply" -    end - -    def self.couch_url -      @couch_url ||= begin -        url = ['http://'] -        if Config.couch_user -          url.push Config.couch_user, ':', Config.couch_password, '@' -        end -        url.push Config.couch_host, ':', Config.couch_port, '/', Config.couch_database -        url.push '/', VIEW -        url.join -      end -    end - -  end -end; end
\ No newline at end of file diff --git a/lib/nickserver/couch_db.rb b/lib/nickserver/couch_db.rb new file mode 100644 index 0000000..5c63fc1 --- /dev/null +++ b/lib/nickserver/couch_db.rb @@ -0,0 +1,4 @@ +module Nickserver +  module CouchDB +  end +end diff --git a/lib/nickserver/couch_db/response.rb b/lib/nickserver/couch_db/response.rb new file mode 100644 index 0000000..c6afe03 --- /dev/null +++ b/lib/nickserver/couch_db/response.rb @@ -0,0 +1,51 @@ +require 'nickserver/couch_db' +require 'json' + +module Nickserver::CouchDB +  class Response + +    def initialize(nick, couch_response = {}) +      @nick = nick +      @couch_status = couch_response[:status] +      @json = JSON.load(couch_response[:body]) if couch_status == 200 +    end + +    def status +      if ok? && empty? then 404 +      else couch_status +      end +    end + +    def content +      key_response if ok? && !empty? +    end + +    protected + +    def key_response +      format address: nick.to_s, openpgp: key +    end + +    def format(response) +      response.to_json +    end + +    def key +      rows.first["value"] +    end + +    def ok? +      couch_status == 200 +    end + +    def empty? +      rows.empty? +    end + +    def rows +      json["rows"] +    end + +    attr_reader :couch_status, :json, :nick +  end +end diff --git a/lib/nickserver/couch_db/source.rb b/lib/nickserver/couch_db/source.rb new file mode 100644 index 0000000..874fe4f --- /dev/null +++ b/lib/nickserver/couch_db/source.rb @@ -0,0 +1,39 @@ +# +# This class allows querying couch for public keys. +# +require 'nickserver/couch_db/response' +require 'nickserver/config' + +module Nickserver::CouchDB +  class Source + +    VIEW = '/_design/Identity/_view/pgp_key_by_email' + +    def initialize(adapter) +      @adapter = adapter +    end + +    def query(nick) +      adapter.get url, query: query_for(nick) do |status, body| +        yield Response.new(nick, status: status, body: body) +      end +    end + +    protected + +    def url +      Nickserver::Config.couch_url + VIEW +    end + +    def query_for(nick) +      { reduce: "false", key: "\"#{nick}\"" } +    end + +    def adapter +      @adapter +      # Nickserver::Adapters::Http.new(config) +    end + +    attr_reader :config +  end +end diff --git a/lib/nickserver/daemon.rb b/lib/nickserver/daemon.rb index 7d0f02a..9b06a3c 100644 --- a/lib/nickserver/daemon.rb +++ b/lib/nickserver/daemon.rb @@ -60,7 +60,7 @@ module Nickserver      def daemonize        return bail("Process is already started") if daemon_running? -      pid = fork do +      _pid = fork do          exit if fork          Process.setsid          exit if fork @@ -219,7 +219,7 @@ module Nickserver      end      def override_default_config(flag, value) -      flag = flag.sub /^--/, '' +      flag = flag.sub(/^--/, '')        if Config.respond_to?("#{flag}=")          Config.send("#{flag}=", value)        else diff --git a/lib/nickserver/hkp.rb b/lib/nickserver/hkp.rb new file mode 100644 index 0000000..bb82a20 --- /dev/null +++ b/lib/nickserver/hkp.rb @@ -0,0 +1,4 @@ +module Nickserver +  module Hkp +  end +end diff --git a/lib/nickserver/hkp/client.rb b/lib/nickserver/hkp/client.rb new file mode 100644 index 0000000..6bd239d --- /dev/null +++ b/lib/nickserver/hkp/client.rb @@ -0,0 +1,44 @@ +require 'nickserver/hkp' + +# +# Client for the HKP protocol. +# +# This is not a complete implementation - only the parts we need. +# Instantiate with an adapter that will take care of the http requests. +# +# For each request we yield http_status and the response content just +# like the adapter does. + + +module Nickserver; module Hkp +  class Client + +    def initialize(adapter) +      @adapter = adapter +    end + +    # +    # used to fetch an array of KeyInfo objects that match the given email +    # +    def get_key_infos_by_email(email, &block) +      get op: 'vindex', search: email, fingerprint: 'on', &block +    end + +    # +    # fetches ascii armored OpenPGP public key from the keyserver +    # +    def get_key_by_fingerprint(fingerprint, &block) +      get op: 'get', search: "0x" + fingerprint, &block +    end + +    protected + +    attr_reader :adapter + +    def get(query, &block) +      # in practice, exact=on seems to have no effect +      query = {exact: 'on', options: 'mr'}.merge query +      adapter.get Config.hkp_url, query: query, &block +    end +  end +end; end diff --git a/lib/nickserver/hkp/fetch_key.rb b/lib/nickserver/hkp/fetch_key.rb deleted file mode 100644 index 44621d3..0000000 --- a/lib/nickserver/hkp/fetch_key.rb +++ /dev/null @@ -1,56 +0,0 @@ -require 'em-http' - -# -# Fetch keys via HKP -# http://tools.ietf.org/html/draft-shaw-openpgp-hkp-00 -# - -module Nickserver; module HKP - -  class FetchKey -    include EM::Deferrable - -    def get(uid) -      FetchKeyInfo.new.search(uid).callback {|key_info_list| -        best = pick_best_key(key_info_list) -        get_key_by_fingerprint(best.keyid) {|key| -          self.succeed key -        } -      }.errback {|status, msg| -        self.fail status, msg -      } -      self -    end - -    # -    # fetches ascii armored OpenPGP public key from the keyserver -    # -    def get_key_by_fingerprint(key_id) -      params = {op: 'get', search: "0x" + key_id, exact: 'on', options: 'mr'} -      http = EventMachine::HttpRequest.new(Config.hkp_url).get(query: params) -      http.callback { -        if http.response_header.status != 200 -          self.fail http.response_header.status, "HKP Request failed" -        else -          yield http.response -        end -      } -      http.errback { -        self.fail 500, http.error -      } -    end - -    protected - -    # -    # for now, just pick the newest key. -    # -    # in the future, we should perhaps pick the newest key -    # that is signed by the oldest key. -    # -    def pick_best_key(key_info_list) -      key_info_list.sort {|a,b| a.creationdate <=> b.creationdate}.last -    end -  end - -end; end
\ No newline at end of file diff --git a/lib/nickserver/hkp/fetch_key_info.rb b/lib/nickserver/hkp/fetch_key_info.rb deleted file mode 100644 index 2448bb1..0000000 --- a/lib/nickserver/hkp/fetch_key_info.rb +++ /dev/null @@ -1,30 +0,0 @@ -require 'em-http' - -# -# used to fetch an array of KeyInfo objects that match the given uid. -# - -module Nickserver; module HKP -  class FetchKeyInfo -    include EM::Deferrable - -    def search(uid) -      # in practice, exact=on seems to have no effect -      params = {op: 'vindex', search: uid, exact: 'on', options: 'mr', fingerprint: 'on'} -      EventMachine::HttpRequest.new(Config.hkp_url).get(query: params).callback {|http| -        parser = ParseKeyInfo.new http.response_header, http.response -        keys = parser.keys(uid) -        if keys.any? -          self.succeed keys -        else -          self.fail parser.status(uid), parser.msg(uid) -        end -      }.errback {|http| -        self.fail 500, http.error -      } -      self -    end - -  end - -end; end diff --git a/lib/nickserver/hkp/key_info.rb b/lib/nickserver/hkp/key_info.rb index adb75d8..d4ecf10 100644 --- a/lib/nickserver/hkp/key_info.rb +++ b/lib/nickserver/hkp/key_info.rb @@ -1,4 +1,5 @@  require 'cgi' +require 'nickserver/hkp'  #  # Class to represent the key information result from a query to a key server @@ -9,9 +10,9 @@ require 'cgi'  # format definition of machine readable index output is here:  # http://tools.ietf.org/html/draft-shaw-openpgp-hkp-00#section-5.2  # -module Nickserver; module HKP +module Nickserver::Hkp    class KeyInfo -    attr_accessor :uids, :keyid, :algo, :keylen, :creationdate, :expirationdate, :flags +    attr_accessor :uids, :keyid, :algo, :flags      def initialize(hkp_record)        uid_lines = hkp_record.split("\n") @@ -19,7 +20,7 @@ module Nickserver; module HKP        @keyid, @algo, @keylen_s, @creationdate_s, @expirationdate_s, @flags = pub_line.split(':')[1..-1]        @uids = []        uid_lines.each do |uid_line| -        uid, creationdate, expirationdate, flags = uid_line.split(':')[1..-1] +        uid, _creationdate, _expirationdate, _flags = uid_line.split(':')[1..-1]          # for now, ignore the expirationdate and flags of uids. sks does return them anyway          @uids << CGI.unescape(uid.sub(/.*<(.+)>.*/, '\1'))        end @@ -66,4 +67,4 @@ module Nickserver; module HKP      end    end -end; end +end diff --git a/lib/nickserver/hkp/parse_key_info.rb b/lib/nickserver/hkp/parse_key_info.rb index 8934829..9d59d6b 100644 --- a/lib/nickserver/hkp/parse_key_info.rb +++ b/lib/nickserver/hkp/parse_key_info.rb @@ -1,29 +1,37 @@  # -# Simple parser for HKP KeyInfo responses. +# Simple parser for Hkp KeyInfo responses.  #  # Focus is on simple here. Trying to avoid state and sideeffects.  # Parsing a response with 12 keys and validating them takes 2ms.  # So no need for memoization and making things more complex.  # -module Nickserver; module HKP +module Nickserver; module Hkp    class ParseKeyInfo      # for this regexp to work, the source text must end in a trailing "\n",      # which the output of sks does.      MATCH_PUB_KEY = /(^pub:.+?\n(^uid:.+?\n)+)/m -    #  header        -- header of the hkp response +    #  status        -- http status of the hkp response      #  vindex_result -- raw output from a vindex hkp query (machine readable) -    def initialize(header, vindex_result) -      @header = header +    def initialize(status, vindex_result) +      @status = status        @vindex_result = vindex_result      end -    def status(uid) +    def status_for(uid)        if hkp_ok? && keys(uid).empty?          error_status(uid)        else -        header.status +        status +      end +    end + +    def response_for(uid) +      if keys(uid).any? +        keys(uid) +      else +        msg(uid)        end      end @@ -41,7 +49,7 @@ module Nickserver; module HKP      protected -    attr_reader :header +    attr_reader :status      attr_reader :vindex_result      def error_status(uid) @@ -78,7 +86,7 @@ module Nickserver; module HKP      end      def hkp_ok? -      header.status == 200 +      status == 200      end      def error_message(uid, key, err) diff --git a/lib/nickserver/hkp/response.rb b/lib/nickserver/hkp/response.rb new file mode 100644 index 0000000..c52e25f --- /dev/null +++ b/lib/nickserver/hkp/response.rb @@ -0,0 +1,18 @@ +module Nickserver::Hkp +  class Response + +    attr_reader :status, :content + +    def initialize(uid, key) +      @content = format_response(address: uid, openpgp: key) +      @status = 200 +    end + +    protected + +    def format_response(map) +      map.to_json +    end + +  end +end diff --git a/lib/nickserver/hkp/source.rb b/lib/nickserver/hkp/source.rb new file mode 100644 index 0000000..cae3e01 --- /dev/null +++ b/lib/nickserver/hkp/source.rb @@ -0,0 +1,66 @@ +require 'nickserver/response' +require 'nickserver/hkp/response' +require 'nickserver/hkp/client' +require "nickserver/hkp/parse_key_info" +require "nickserver/hkp/key_info" + + +# +# Fetch keys via HKP +# http://tools.ietf.org/html/draft-shaw-openpgp-hkp-00 +# + +module Nickserver; module Hkp +  class Source + +    def initialize(adapter) +      @adapter = adapter +    end + +    def query(nick, &block) +      search(nick) do |status, response| +        if status == 200 +          best = pick_best_key(response) +          get_key_by_fingerprint(nick, best.keyid, &block) +        else +          yield Nickserver::Response.new(status, response) +        end +      end +    end + +    def search(nick, &block) +      client.get_key_infos_by_email(nick) do |status, response| +        parser = ParseKeyInfo.new status, response +        yield parser.status_for(nick), parser.response_for(nick) +      end +    end + +    protected + +    attr_reader :adapter + +    # +    # for now, just pick the newest key. +    # +    # in the future, we should perhaps pick the newest key +    # that is signed by the oldest key. +    # +    def pick_best_key(key_info_list) +      key_info_list.sort {|a,b| a.creationdate <=> b.creationdate}.last +    end + +    def get_key_by_fingerprint(nick, fingerprint) +      client.get_key_by_fingerprint fingerprint do |status, response| +        if status == 200 +          yield Response.new nick, response +        else +          yield Nickserver::Response.new status, "HKP Request failed" +        end +      end +    end + +    def client +      @client ||= Client.new(adapter) +    end +  end +end; end diff --git a/lib/nickserver/hkp/v_index_response.rb b/lib/nickserver/hkp/v_index_response.rb new file mode 100644 index 0000000..865d476 --- /dev/null +++ b/lib/nickserver/hkp/v_index_response.rb @@ -0,0 +1,96 @@ +require 'nickserver/hkp' +require 'nickserver/hkp/key_info' + +# +# Simple parser for Hkp KeyInfo responses. +# +# Focus is on simple here. Trying to avoid state and sideeffects. +# Parsing a response with 12 keys and validating them takes 2ms. +# So no need for memoization and making things more complex. +module Nickserver::Hkp +  class VIndexResponse + +    # for this regexp to work, the source text must end in a trailing "\n", +    # which the output of sks does. +    MATCH_PUB_KEY = /(^pub:.+?\n(^uid:.+?\n)+)/m + +    #  hkp_response -- raw output from a vindex hkp query (machine readable) +    def initialize(nick, hkp_response) +      @nick = nick.to_s +      @vindex_result = hkp_response[:body] +    end + +    def status +      if keys.empty? +        error_status +      else +        200 +      end +    end + +    def keys +      key_infos.reject { |key| error_for_key(key) } +    end + +    def msg +      if errors.any? +        error_messages.join "\n" +      else +        "Could not fetch keyinfo." +      end +    end + +    protected + +    attr_reader :vindex_result, :nick + +    def error_status +      if errors.any? +        500 +      else +        404 +      end +    end + +    def errors +      key_infos.map{|key| error_for_key(key) }.compact +    end + +    def error_messages +      key_infos.map do |key| +        err = error_for_key(key) +        error_message(key, err) +      end.compact +    end + +    def key_infos +      all_key_infos.select do |key_info| +        key_info.uids.include?(nick) +      end +    end + +    def all_key_infos +      @all_key_infos ||= vindex_result.scan(MATCH_PUB_KEY).map do |match| +        KeyInfo.new(match[0]) +      end +    end + +    def error_message(key, err) +      "Ignoring key #{key.keyid} for #{nick}: #{err}" if err +    end + +    def error_for_key(key) +      if key.keylen < 2048 +        "key length is too short." +      elsif key.expired? +        "key expired." +      elsif key.revoked? +        "key revoked." +      elsif key.disabled? +        "key disabled." +      elsif key.expirationdate && key.expirationdate < Time.now +        "key expired" +      end +    end +  end +end diff --git a/lib/nickserver/invalid_source.rb b/lib/nickserver/invalid_source.rb new file mode 100644 index 0000000..dac245a --- /dev/null +++ b/lib/nickserver/invalid_source.rb @@ -0,0 +1,14 @@ +# +# This is a dummy source for invalid queries. +# It simply always returns 500 and "Not a valid address" +# + +module Nickserver +  class InvalidSource + +    def query(nick) +      yield 500, "Not a valid address" +    end + +  end +end diff --git a/lib/nickserver/lookup.rb b/lib/nickserver/lookup.rb new file mode 100644 index 0000000..105e77e --- /dev/null +++ b/lib/nickserver/lookup.rb @@ -0,0 +1,31 @@ +require 'nickserver/invalid_source' + +module Nickserver +  class Lookup + +    attr_reader :nick + +    def initialize(nick) +      @nick = nick +    end + +    def respond_with(responder) +      query do |status, content| +        responder.send_response status: status, content: content +      end +    end + +    protected + +    def query(&block) +      source.query nick, &block +    end + +    def source +      if nick.invalid?  then Nickserver::InvalidSource +      elsif nick.local? then Nickserver::Config.local_source +      else Nickserver::Config.remote_source +      end +    end +  end +end diff --git a/lib/nickserver/nickname.rb b/lib/nickserver/nickname.rb new file mode 100644 index 0000000..938d4a4 --- /dev/null +++ b/lib/nickserver/nickname.rb @@ -0,0 +1,51 @@ +module Nickserver +  class Nickname + +    EmailAddress = begin +                     qtext = '[^\\x0d\\x22\\x5c\\x80-\\xff]' +                     dtext = '[^\\x0d\\x5b-\\x5d\\x80-\\xff]' +                     atom = '[^\\x00-\\x20\\x22\\x28\\x29\\x2c\\x2e\\x3a-\\x3c\\x3e\\x40\\x5b-\\x5d\\x7f-\\xff]+' +                     quoted_pair = '\\x5c[\\x00-\\x7f]' +                     domain_literal = "\\x5b(?:#{dtext}|#{quoted_pair})*\\x5d" +                     quoted_string = "\\x22(?:#{qtext}|#{quoted_pair})*\\x22" +                     domain_ref = atom +                     sub_domain = "(?:#{domain_ref}|#{domain_literal})" +                     word = "(?:#{atom}|#{quoted_string})" +                     domain = "#{sub_domain}(?:\\x2e#{sub_domain})*" +                     local_part = "#{word}(?:\\x2e#{word})*" +                     addr_spec = "#{local_part}\\x40#{domain}" +                     /\A#{addr_spec}\z/n +                   end + +    LOCAL_DOMAIN = 'test.me' + +    def initialize(address) +      @address = address.to_s +    end + +    def valid? +      address =~ EmailAddress +    end + +    def invalid? +      !valid? +    end + +    def local? +      address.end_with? LOCAL_DOMAIN +    end + +    def remote? +      !local? +    end + +    def to_s +      address +    end + +    protected + +    attr_reader :address + +  end +end diff --git a/lib/nickserver/response.rb b/lib/nickserver/response.rb new file mode 100644 index 0000000..c55d359 --- /dev/null +++ b/lib/nickserver/response.rb @@ -0,0 +1,15 @@ +module Nickserver +  class Response + +    attr_reader :status, :message + +    def initialize(status, message) +      @status = status +      @message = message +    end + +    def content +      "#{status} #{message}" +    end +  end +end diff --git a/lib/nickserver/server.rb b/lib/nickserver/server.rb index 32afdae..2453f94 100644 --- a/lib/nickserver/server.rb +++ b/lib/nickserver/server.rb @@ -1,6 +1,13 @@ +require 'kernel_ext'  require 'eventmachine' -require 'evma_httpserver' +silence_warnings do +  require 'evma_httpserver' +end  require 'json' +require 'nickserver/couch_db/source' +require 'nickserver/hkp/source' +require 'nickserver/adapters/em_http' +  #  # This is the main HTTP server that clients connect to in order to fetch keys @@ -64,12 +71,8 @@ module Nickserver        response.status = options[:status]        response.content_type options[:content_type]        response.content = options[:content] -      response.send_response -    end - -    def send_key(uid) -      get_key_from_uid(uid) do |key| -        send_response content: format_response(address: uid, openpgp: key) +      silence_warnings do +        response.send_response        end      end @@ -86,25 +89,15 @@ module Nickserver        end      end -    def get_key_from_uid(uid) -      fetcher = if local_address?(uid) -        Nickserver::Couch::FetchKey.new +    def send_key(uid) +      if local_address?(uid) +        source = Nickserver::CouchDB::Source.new(adapter)        else -        Nickserver::HKP::FetchKey.new +        source = Nickserver::Hkp::Source.new(adapter) +      end +      source.query(uid) do |response| +        send_response(status: response.status, content: response.content)        end -      fetcher.get(uid).callback {|key| -        yield key -      }.errback {|status, msg| -        if status == 404 -          send_not_found -        else -          send_response(status: status, content: msg) -        end -      } -    end - -    def format_response(map) -      map.to_json      end      # @@ -127,8 +120,13 @@ module Nickserver            return uid_domain == host          end        end -    rescue +    rescue # XXX what are we rescueing here?        return false      end + +    def adapter +      @adapter ||= Nickserver::Adapters::EmHttp.new +    end +    end  end diff --git a/lib/server.rb b/lib/server.rb new file mode 100644 index 0000000..4e7cf51 --- /dev/null +++ b/lib/server.rb @@ -0,0 +1,19 @@ +class Server + +  def process_http_request +    lookup.respond_with(Responder) +  end + +  def lookup +    LookupFactory.lookup_for(nick) +  end + +  def nick +    Nickname.new(request.address) +  end + +  def request +    Request.new(params) +  end + +end diff --git a/test/file_content.rb b/test/file_content.rb new file mode 100644 index 0000000..0d0ac21 --- /dev/null +++ b/test/file_content.rb @@ -0,0 +1,11 @@ +module FileContent + +  def file_content(filename) +    (@file_contents ||= {})[filename] ||= File.read(file_path(filename)) +  end + +  def file_path(filename) +    "%s/files/%s" % [File.dirname(__FILE__), filename] +  end + +end diff --git a/test/helpers/test_adapter.rb b/test/helpers/test_adapter.rb new file mode 100644 index 0000000..46d4713 --- /dev/null +++ b/test/helpers/test_adapter.rb @@ -0,0 +1,10 @@ +class TestAdapter +  def initialize(status, content) +    @status = status +    @content = content +  end + +  def get(url, opts) +    yield @status, @content +  end +end diff --git a/test/integration/couch_db/source_test.rb b/test/integration/couch_db/source_test.rb new file mode 100644 index 0000000..21e3642 --- /dev/null +++ b/test/integration/couch_db/source_test.rb @@ -0,0 +1,19 @@ +require 'test_helper' +require 'file_content' +require 'helpers/test_adapter' +require 'nickserver/couch_db/source' + +module Nickserver::CouchDB +  class SourceTest < Minitest::Test +  include FileContent + +    def test_couch_query_and_response +      adapter = TestAdapter.new 200, file_content(:blue_couchdb_result) +      source = Source.new adapter +      source.query 'blue@example.org' do |response| +        assert_equal 200, response.status +        assert_equal file_content(:blue_nickserver_result), response.content +      end +    end +  end +end diff --git a/test/unit/hkp_test.rb b/test/integration/hkp_test.rb index 0ecde2d..a824a3f 100644 --- a/test/unit/hkp_test.rb +++ b/test/integration/hkp_test.rb @@ -1,4 +1,6 @@ -require File.expand_path('test_helper', File.dirname(__FILE__)) +require 'test_helper' +require 'nickserver/hkp/source' +require 'nickserver/adapters/em_http'  class HkpTest < Minitest::Test @@ -29,17 +31,13 @@ class HkpTest < Minitest::Test    def test_key_info_not_found      uid = 'leaping_lemur@leap.se'      stub_sks_vindex_reponse(uid, status: 404) -    test_em_errback "Nickserver::HKP::FetchKeyInfo.new.search '#{uid}'" do |error| -      assert_equal 404, error -    end +    assert_response_status_for_uid uid, 404    end    def test_no_matching_key_found      uid = 'leaping_lemur@leap.se'      stub_sks_vindex_reponse(uid, status: 200) -    test_em_errback "Nickserver::HKP::FetchKeyInfo.new.search '#{uid}'" do |error| -      assert_equal 404, error -    end +    assert_response_status_for_uid uid, 404    end    def test_fetch_key @@ -48,8 +46,9 @@ class HkpTest < Minitest::Test      stub_sks_vindex_reponse(uid, body: file_content(:leap_vindex_result))      stub_sks_get_reponse(key_id, body: file_content(:leap_public_key)) -    test_em_callback "Nickserver::HKP::FetchKey.new.get '#{uid}'" do |key_text| -      assert_equal file_content(:leap_public_key), key_text +    assert_response_for_uid(uid) do |response| +      content = JSON.parse response.content +      assert_equal file_content(:leap_public_key), content['openpgp']      end    end @@ -60,19 +59,14 @@ class HkpTest < Minitest::Test      stub_sks_vindex_reponse(uid, body: file_content(:leap_vindex_result))      stub_sks_get_reponse(key_id, status: 404) -    test_em_errback "Nickserver::HKP::FetchKey.new.get '#{uid}'" do |error| -      assert_equal 404, error -    end +    assert_response_status_for_uid uid, 404    end    def test_fetch_key_too_short      uid    = 'chiiph@leap.se' -    key_id = '9A753A6B'      stub_sks_vindex_reponse(uid, body: file_content(:short_key_vindex_result)) -    test_em_errback "Nickserver::HKP::FetchKey.new.get '#{uid}'" do |error| -      assert_equal 500, error -    end +    assert_response_status_for_uid uid, 500    end    # @@ -83,7 +77,7 @@ class HkpTest < Minitest::Test    def test_key_info_real_network      real_network do        uid = 'elijah@riseup.net' -      test_em_callback "Nickserver::HKP::FetchKeyInfo.new.search '#{uid}'" do |keys| +      assert_key_info_for_uid uid do |keys|          assert_equal 1, keys.size          assert keys.first.keyid =~ /00440025$/        end @@ -100,7 +94,7 @@ class HkpTest < Minitest::Test          #stub_config(:hkp_ca_file, file_path('autistici-ca.pem')) do            assert File.exist?(Nickserver::Config.hkp_ca_file)            uid = 'elijah@riseup.net' -          test_em_callback "Nickserver::HKP::FetchKeyInfo.new.search '#{uid}'" do |keys| +          assert_key_info_for_uid uid do |keys|              assert_equal 1, keys.size              assert keys.first.keyid =~ /00440025$/            end @@ -111,53 +105,38 @@ class HkpTest < Minitest::Test    protected -  # -  # Takes a code snippet that returns a Deferrable, and yields the callback result. -  # Assertion fails if errback is called instead of callback. -  # -  # This method takes care of the calls to EM.run and EM.stop. It works kind of like EM.run_block, -  # except I couldn't get run_block to work with multiple nested HTTP requests. -  # -  def test_em_callback(code, &block) +  def assert_response_status_for_uid(uid, status) +    assert_response_for_uid(uid) do |response| +      assert_equal status, response.status +    end +  end + +  def assert_response_for_uid(uid, &block)      EM.run do -      deferrable = instance_eval(code) -      deferrable.callback {|response| -        EM.stop +      Nickserver::Hkp::Source.new(adapter).query uid do |response|          yield response -        return -      } -      deferrable.errback {|response, msg|          EM.stop -        puts caller.join("\n") -        flunk "Expecting callback, but errback invoked with response: #{response} #{msg}\n\n#{caller.join("\n")}" -      } +      end      end -    assert false, 'should not get here'    end -  # -  # like test_em_callback, except value yielded is the result of errback, and -  # we raise an exception if errback was not called. -  # -  def test_em_errback(code, &block) +  def assert_key_info_for_uid(uid, &block)      EM.run do -      deferrable = instance_eval(code) -      deferrable.callback {|response| -        EM.stop -        flunk "Expecting errback, but callback invoked with response: #{response}" -      } -      deferrable.errback {|response| +      Nickserver::Hkp::Source.new(adapter).search uid do |status, keys| +        assert_equal 200, status +        yield keys          EM.stop -        yield response -        return -      } +      end      end -    assert false, 'should not get here' +  end + +  def adapter +    Nickserver::Adapters::EmHttp.new    end    def fetch_key_info(body_source, uid, &block)      stub_sks_vindex_reponse(uid, body: file_content(body_source)) -    test_em_callback "Nickserver::HKP::FetchKeyInfo.new.search '#{uid}'", &block +    assert_key_info_for_uid(uid, &block)    end  end diff --git a/test/unit/nickserver_test.rb b/test/integration/nickserver_test.rb index 65ade8c..b4ff4da 100644 --- a/test/unit/nickserver_test.rb +++ b/test/integration/nickserver_test.rb @@ -1,4 +1,4 @@ -require File.expand_path('test_helper', File.dirname(__FILE__)) +require 'test_helper'  require 'json'  # diff --git a/test/test_helper.rb b/test/test_helper.rb index d4765bc..afdd3f9 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,10 +1,15 @@  $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')  require 'rubygems' +require 'kernel_ext'  require 'bundler/setup'  require 'minitest/autorun' -require 'webmock/minitest' +silence_warnings do +  require 'webmock/minitest' +end  require 'nickserver' +require 'minitest/pride' +require 'minitest/hell'  TESTING = true @@ -27,7 +32,7 @@ class Minitest::Test    end    def real_network -    if ENV['REAL_NET'] == 'true' +    unless ENV['ONLY_LOCAL'] == 'true'        WebMock.allow_net_connect!        yield        WebMock.disable_net_connect! @@ -53,7 +58,7 @@ class Minitest::Test      Nickserver::Config.stub :couch_host, 'notlocalhost' do        options = {status: 200, body: ""}.merge(opts)        query = "\?key=#{"%22#{uid}%22"}&reduce=false" -      stub_http_request(:get, /#{Regexp.escape(Nickserver::Couch::FetchKey.couch_url)}.*#{query}/).to_return(options) +      stub_http_request(:get, /#{Regexp.escape(Nickserver::Config.couch_url)}.*#{query}/).to_return(options)        yield      end    end diff --git a/test/unit/adapters/couch_db.rb b/test/unit/adapters/couch_db.rb new file mode 100644 index 0000000..339e4f4 --- /dev/null +++ b/test/unit/adapters/couch_db.rb @@ -0,0 +1,15 @@ +require 'testhelper' +require 'nickserver/adapters/couch_db' + +class Nickserver::Adapters::CouchDBTest < Minitest::Test + +  def test_query_404 +    adapter.query(nil) do |status, content| +      assert_equal 404, status +    end +  end + +  def adapter +    @adapter ||= Nickserver::Adapters::CouchDB.new +  end +end diff --git a/test/unit/adapters/em_http_test.rb b/test/unit/adapters/em_http_test.rb new file mode 100644 index 0000000..659ff1b --- /dev/null +++ b/test/unit/adapters/em_http_test.rb @@ -0,0 +1,25 @@ +require 'test_helper' +require 'nickserver/adapters/em_http' + +class Nickserver::Adapters::EmHttpTest < Minitest::Test + +  def test_successful_request +    url = 'http://url.to' +    stub_http_request(:get, url) +      .with(query: {key: :value}) +      .to_return status: 200, body: 'body' +    EM.run do +      adapter.get(url, query: {key: :value}) do |status, body| +        assert_equal 200, status +        assert_equal 'body', body +        EM.stop +      end +    end +  end + +  protected + +  def adapter +    Nickserver::Adapters::EmHttp.new +  end +end diff --git a/test/unit/adapters/local_test.rb b/test/unit/adapters/local_test.rb new file mode 100644 index 0000000..97d394f --- /dev/null +++ b/test/unit/adapters/local_test.rb @@ -0,0 +1,5 @@ +require 'test_helper' +require 'nickserver/adapters/local' + +class Nickserver::Adapters::LocalTest < Minitest::Test +end diff --git a/test/unit/couch_db/response_test.rb b/test/unit/couch_db/response_test.rb new file mode 100644 index 0000000..d44760d --- /dev/null +++ b/test/unit/couch_db/response_test.rb @@ -0,0 +1,30 @@ +require 'test_helper' +require 'file_content' +require 'nickserver/couch_db/response' + +class Nickserver::CouchDB::ResponseTest < Minitest::Test +  include FileContent + +  def test_404 +    response = response_for "bananas@example.org", +      status: 404, body: "{}" +    assert_equal 404, response.status +  end + +  def test_200_with_empty_response +    response = response_for "stompy@example.org", +      status: 200, body: file_content(:empty_couchdb_result) +    assert_equal 404, response.status +  end + +  def test_200_with_success +    response = response_for "blue@example.org", +      status: 200, body: file_content(:blue_couchdb_result) +    assert_equal 200, response.status +    assert_equal file_content(:blue_nickserver_result), response.content +  end + +  def response_for(uid, couch_response = {}) +    Nickserver::CouchDB::Response.new uid, couch_response +  end +end diff --git a/test/unit/couch_db/source_unit_test.rb b/test/unit/couch_db/source_unit_test.rb new file mode 100644 index 0000000..19ea9bc --- /dev/null +++ b/test/unit/couch_db/source_unit_test.rb @@ -0,0 +1,17 @@ +require 'test_helper' +require 'nickserver/couch_db/source' + +module Nickserver::CouchDB +  class SourceUnitTest < Minitest::Test + +    def test_query +      address = "nick@domain.tl" +      adapter = Minitest::Mock.new +      adapter.expect :get, nil, +        [String,  {query: { reduce: "false", key: "\"#{address}\"" }}] +      query = Source.new(adapter) +      query.query address +      adapter.verify +    end +  end +end diff --git a/test/unit/hkp/v_index_response_test.rb b/test/unit/hkp/v_index_response_test.rb new file mode 100644 index 0000000..d909520 --- /dev/null +++ b/test/unit/hkp/v_index_response_test.rb @@ -0,0 +1,17 @@ +require 'test_helper' +require 'file_content' +require 'nickserver/hkp/v_index_response' + +class Nickserver::Hkp::VIndexResponseTest < Minitest::Test +  include FileContent + +  def test_leap_public_key +    response = response_for 'cloudadmin@leap.se', +      body: file_content(:leap_vindex_result) +    assert_equal 'E818C478D3141282F7590D29D041EB11B1647490', response.keys.first.keyid +  end + +  def response_for(uid, hkp_response = {}) +    Nickserver::Hkp::VIndexResponse.new uid, hkp_response +  end +end diff --git a/test/unit/invalid_source_test.rb b/test/unit/invalid_source_test.rb new file mode 100644 index 0000000..37a38fc --- /dev/null +++ b/test/unit/invalid_source_test.rb @@ -0,0 +1,16 @@ +require 'test_helper' +require 'nickserver/invalid_source' + +class Nickserver::InvalidSourceTest < Minitest::Test + +  def test_query +    adapter.query(nil) do |status, content| +      assert_equal 500, status +      assert_equal "Not a valid address", content +    end +  end + +  def adapter +    Nickserver::InvalidSource.new +  end +end diff --git a/test/unit/lookup_test.rb b/test/unit/lookup_test.rb new file mode 100644 index 0000000..ac827e0 --- /dev/null +++ b/test/unit/lookup_test.rb @@ -0,0 +1,23 @@ +require 'test_helper' +require 'minitest/mock' +require 'nickserver/lookup' + +class TestLookup < Nickserver::Lookup + +  def query +    yield 200, 'yeah' +  end + +end + +class LookupTest < Minitest::Test + +  def test_responding +    responder = Minitest::Mock.new +    responder.expect :send_response, nil, +      [{status: 200, content: 'yeah'}] +    lookup = TestLookup.new nil +    lookup.respond_with responder +    responder.verify +  end +end diff --git a/test/unit/nickname_test.rb b/test/unit/nickname_test.rb new file mode 100644 index 0000000..8681545 --- /dev/null +++ b/test/unit/nickname_test.rb @@ -0,0 +1,28 @@ +require 'test_helper' +require 'nickserver/nickname' + +class NicknameTest < Minitest::Test + +  def test_local +    nick = Nickserver::Nickname.new 'nick@test.me' +    assert nick.local? +    assert !nick.remote? +  end + +  def test_remote +    nick = Nickserver::Nickname.new 'nick@remote.domain' +    assert !nick.local? +    assert nick.remote? +  end + +  def test_valid +    nick = Nickserver::Nickname.new 'nick@remote.domain' +    assert nick.valid? +  end + +  def test_invalid +    nick = Nickserver::Nickname.new 'asdf' +    assert nick.invalid? +  end + +end diff --git a/test/unit/response_test.rb b/test/unit/response_test.rb new file mode 100644 index 0000000..8a53066 --- /dev/null +++ b/test/unit/response_test.rb @@ -0,0 +1,11 @@ +require 'test_helper' +require 'nickserver/response' + +class ResponseTest < Minitest::Test + +  def test_content +    response = Nickserver::Response.new 500, "Not a valid address" +    assert_equal "500 Not a valid address", response.content +  end + +end | 
