diff options
Diffstat (limited to 'lib/nickserver')
24 files changed, 536 insertions, 190 deletions
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  | 
