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 |