simple-gnupg-keyserver 1.3.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -1
- data/History.txt +0 -2
- data/Manifest.txt +4 -1
- data/README.rdoc +45 -40
- data/Rakefile +1 -0
- data/lib/simpleHKP/echo.rb +49 -8
- data/lib/simpleHKP/identities.rb +240 -0
- data/lib/simpleHKP/keys.rb +239 -0
- data/lib/simpleHKP/server.rb +349 -0
- data/lib/simpleHKP.rb +10 -489
- metadata +13 -9
data/.gitignore
CHANGED
data/Manifest.txt
CHANGED
data/README.rdoc
CHANGED
@@ -1,32 +1,22 @@
|
|
1
1
|
= rGem-simple-gnupg-keyserver
|
2
2
|
|
3
|
-
|
3
|
+
code :: https://github.com/stephengaito/rGem-simple-gnupg-keyserver
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
SimpleHKP is a simple Ruby/rack based GnuPG HKP key server.
|
8
|
-
|
9
|
-
Its sole task is to supply a limited number of GnuPG _public_ keys
|
10
|
-
to/from MonkeySphere on a limited number of machines/servers. It is
|
11
|
-
_not_ meant to server hundreds of keys, so it is not meant as a truely
|
12
|
-
public key server.
|
13
|
-
|
14
|
-
If you need to run your own _large_ scale production Key Sever then use
|
15
|
-
{SKS}[https://bitbucket.org/skskeyserver/sks-keyserver/wiki/Home]. You
|
16
|
-
might want to read Paul Bauer's {How To Setup A Free PGP Key Server in
|
17
|
-
Ubuntu}[http://www.bauer-power.net/2010/05/how-to-setup-free-pgp-key-server-in.html].
|
5
|
+
docs :: https://stephengaito.github.io/rGem-simple-gnupg-keyserver
|
18
6
|
|
19
|
-
|
7
|
+
== DESCRIPTION:
|
20
8
|
|
21
|
-
|
22
|
-
|
9
|
+
SimpleHKP is a simple Ruby/rack based GnuPG extendedHKP key and
|
10
|
+
identity server packaged as a standard Ruby Gem.
|
23
11
|
|
24
|
-
|
12
|
+
As such it conforms to an {extended
|
13
|
+
version}[http://stephengaito.github.io/rGem-simple-gnupg-keyserver/] of
|
14
|
+
{the OpenPGP HTTP Keyserver Protocol (HKP)
|
15
|
+
(draft-shaw-openpgp-hkp-00.txt)}[http://tools.ietf.org/html/draft-shaw-openpgp-hkp-00].
|
25
16
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
which are matched against each key's key data.
|
17
|
+
The {associated
|
18
|
+
documentation}[https://stephengaito.github.io/rGem-simple-gnupg-keyserver]
|
19
|
+
provides more detail.
|
30
20
|
|
31
21
|
=== Security:
|
32
22
|
|
@@ -47,6 +37,13 @@ This rack application is intentionally as self contained and simple as
|
|
47
37
|
possible. If you have any security concerns, this rack application is
|
48
38
|
easily readable.
|
49
39
|
|
40
|
+
When used with SimpleMonkey, this SimpleHKP acts like a distributed
|
41
|
+
password file. All important (non-public identity) details are
|
42
|
+
encrypted, however, the collection of servers, users and their
|
43
|
+
associated roles are visibly in the "public domain". It is important to
|
44
|
+
run your own collection of SimpleHKP servers *inside* a controlled
|
45
|
+
domain or VPN.
|
46
|
+
|
50
47
|
== SYNOPSIS:
|
51
48
|
|
52
49
|
A typical rackup.ru file might be:
|
@@ -68,13 +65,14 @@ key/value pairs.
|
|
68
65
|
The current default options are:
|
69
66
|
|
70
67
|
defaultOptions = {
|
71
|
-
'debug' => false,
|
72
|
-
'title' => 'SimpleHKP'
|
73
|
-
'simpleHKPdir' => 'simpleHKP',
|
74
|
-
'keyDir' => 'keys',
|
75
|
-
'
|
76
|
-
'
|
77
|
-
'
|
68
|
+
'debug' => false, # should debug output to logged?
|
69
|
+
'title' => 'SimpleHKP' # the title used by the default headerHTML
|
70
|
+
'simpleHKPdir' => 'simpleHKP', # base disk path to simpleHKP disk space
|
71
|
+
'keyDir' => 'keys', # subdir to key storage directory
|
72
|
+
'idDir' => 'identities', # subdir to identity storage directory
|
73
|
+
'mediaDir' => 'media', # subdir to any css, js, images etc
|
74
|
+
'htmlDir' => 'html', # subdir to html partials
|
75
|
+
'mimeMap' => { # a file ext to mime mapping
|
78
76
|
'css' => 'text/css',
|
79
77
|
'html' => 'text/html',
|
80
78
|
'js' => 'text/javascript'
|
@@ -86,14 +84,16 @@ the use of humans:
|
|
86
84
|
|
87
85
|
* header.html
|
88
86
|
* defaultBody.html
|
89
|
-
*
|
90
|
-
*
|
87
|
+
* lookupKeysForm.html
|
88
|
+
* lookupIdentitiesForm.html
|
89
|
+
* uploadKeyForm.html
|
90
|
+
* uploadIdentityForm.html
|
91
91
|
* footer.html
|
92
92
|
|
93
93
|
== SYNCHRONIZATION
|
94
94
|
|
95
|
-
A 'simpleHKP/echo' class has been added which knows how to echo
|
96
|
-
one key server to another.
|
95
|
+
A 'simpleHKP/echo' class has been added which knows how to echo
|
96
|
+
keys/identities from one key server to another.
|
97
97
|
|
98
98
|
So that for example the following ruby script could be placed into one
|
99
99
|
of your machine's /etc/daily directories and the script would ensure
|
@@ -101,13 +101,13 @@ all key servers are synchronized daily.
|
|
101
101
|
|
102
102
|
#!/usr/bin/env ruby
|
103
103
|
|
104
|
-
# A simple key server sychronization example
|
104
|
+
# A simple key/identity server sychronization example
|
105
105
|
#
|
106
106
|
# We use a hub-spoke model. We choose one "hub" key server to act as
|
107
|
-
# the master key server and amalgamate all of the keys from
|
108
|
-
# key server back to the hub key server. We then push the
|
109
|
-
# keys in the hub back to the spokes, so that
|
110
|
-
# servers have the same keys.
|
107
|
+
# the master key server and amalgamate all of the keys/identities from
|
108
|
+
# the spoke key server back to the hub key server. We then push the
|
109
|
+
# amalgamated keys/identities in the hub back to the spokes, so that
|
110
|
+
# after two passes, all key servers have the same keys/identities.
|
111
111
|
|
112
112
|
require 'simpleHKP/echo'
|
113
113
|
|
@@ -118,13 +118,13 @@ all key servers are synchronized daily.
|
|
118
118
|
'spokeKeyServer3'
|
119
119
|
]
|
120
120
|
|
121
|
-
# Start by bringing all spoke keys into the hub
|
121
|
+
# Start by bringing all spoke keys/identities into the hub
|
122
122
|
#
|
123
123
|
spokeKeyServers.each do | aKeyServer |
|
124
124
|
SimpleHKPEcho.echoFromTo(aKeyServer, hubKeyServer)
|
125
125
|
end
|
126
126
|
|
127
|
-
# now send the amalgamated hub keys back to each spoke
|
127
|
+
# now send the amalgamated hub keys/identities back to each spoke
|
128
128
|
#
|
129
129
|
spokeKeyServers.each do | aKeyServer |
|
130
130
|
SimpleHKPEcho.echoFromTo(hubKeyServer, aKeyServer)
|
@@ -137,6 +137,10 @@ The SimpleHKPEcho.echoFromTo method fails gracefully if a given key
|
|
137
137
|
server is offline, by simply returning. In the example above, all other
|
138
138
|
echo pairs (from, to) will be tried.
|
139
139
|
|
140
|
+
To just echo either the keys or identities you can use
|
141
|
+
SimpleHKPEcho.echoKeysFromTo or SimpleHKPEcho.echoIdentitiesFromTo
|
142
|
+
respectively.
|
143
|
+
|
140
144
|
== REQUIREMENTS:
|
141
145
|
|
142
146
|
There are explicitly no external Ruby requirements other than Ruby and
|
@@ -175,3 +179,4 @@ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
175
179
|
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
176
180
|
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
177
181
|
|
182
|
+
|
data/Rakefile
CHANGED
data/lib/simpleHKP/echo.rb
CHANGED
@@ -2,10 +2,11 @@ require 'uri'
|
|
2
2
|
require 'net/http'
|
3
3
|
require 'pp'
|
4
4
|
|
5
|
-
# This code provides a simple way to ensure keys are
|
6
|
-
# between a pair of key servers.
|
5
|
+
# This code provides a simple way to ensure keys/identities are
|
6
|
+
# synchronized between a pair of key/identity servers.
|
7
7
|
|
8
|
-
# It conforms to
|
8
|
+
# It conforms to our simple extension to:
|
9
|
+
# The OpenPGP HTTP Keyserver Protocol (HKP)
|
9
10
|
# draft-shaw-openpgp-hkp-00.txt
|
10
11
|
# http://tools.ietf.org/html/draft-shaw-openpgp-hkp-00
|
11
12
|
|
@@ -48,7 +49,7 @@ class SimpleHKPEcho
|
|
48
49
|
aKeyServerStr
|
49
50
|
end
|
50
51
|
|
51
|
-
def
|
52
|
+
def echoKeysFromTo(fromKeyServer, toKeyServer, options = {})
|
52
53
|
begin
|
53
54
|
debug = options.delete('debug')
|
54
55
|
puts fromKeyServer if debug
|
@@ -59,7 +60,7 @@ class SimpleHKPEcho
|
|
59
60
|
puts toKeyServer if debug
|
60
61
|
|
61
62
|
keys = Array.new
|
62
|
-
url = URI.parse(fromKeyServer+'/lookup?search=&op=index&options=mr')
|
63
|
+
url = URI.parse(fromKeyServer+'/pks/lookup?search=&op=index&options=mr')
|
63
64
|
response = Net::HTTP.get_response(url)
|
64
65
|
response.body.each_line do | aLine |
|
65
66
|
next unless aLine =~ /^pub/
|
@@ -68,19 +69,59 @@ class SimpleHKPEcho
|
|
68
69
|
pp keys if debug
|
69
70
|
keys.each do | aKey |
|
70
71
|
keyData = ""
|
71
|
-
url = URI.parse(fromKeyServer+"/lookup?op=get&options=mr&search=#{aKey}")
|
72
|
+
url = URI.parse(fromKeyServer+"/pks/lookup?op=get&options=mr&search=#{aKey}")
|
72
73
|
response = Net::HTTP.get_response(url)
|
73
74
|
keyData = response.body
|
74
75
|
puts aKey if debug
|
75
76
|
puts keyData if debug
|
76
|
-
url = URI.parse(toKeyServer+'/add')
|
77
|
+
url = URI.parse(toKeyServer+'/pks/add')
|
77
78
|
Net::HTTP.post_form(url, { 'keytext' => keyData })
|
78
79
|
end
|
79
|
-
rescue
|
80
|
+
rescue Exception => ex
|
80
81
|
puts "Cound not echo keys from #{fromKeyServer} to #{toKeyServer}"
|
82
|
+
puts ex
|
81
83
|
end
|
82
84
|
end
|
83
85
|
|
86
|
+
def echoIdentitiesFromTo(fromKeyServer, toKeyServer, options = {})
|
87
|
+
begin
|
88
|
+
debug = options.delete('debug')
|
89
|
+
puts fromKeyServer if debug
|
90
|
+
fromKeyServer = convertToHttp(fromKeyServer)
|
91
|
+
puts fromKeyServer if debug
|
92
|
+
puts toKeyServer if debug
|
93
|
+
toKeyServer = convertToHttp(toKeyServer)
|
94
|
+
puts toKeyServer if debug
|
95
|
+
|
96
|
+
identities = Array.new
|
97
|
+
url = URI.parse(fromKeyServer+'/pis/lookup?search=&op=index&options=mr')
|
98
|
+
response = Net::HTTP.get_response(url)
|
99
|
+
response.body.each_line do | aLine |
|
100
|
+
next unless aLine =~ /^idn/
|
101
|
+
identities.push(aLine.split(/:/)[1])
|
102
|
+
end
|
103
|
+
pp identities if debug
|
104
|
+
identities.each do | anIdFile |
|
105
|
+
idData = ""
|
106
|
+
url = URI.parse(fromKeyServer+"/pis/lookup?op=get&options=mr&search=#{anIdFile}")
|
107
|
+
response = Net::HTTP.get_response(url)
|
108
|
+
idData = response.body
|
109
|
+
puts anIdFile if debug
|
110
|
+
puts idData if debug
|
111
|
+
url = URI.parse(toKeyServer+'/pis/add')
|
112
|
+
Net::HTTP.post_form(url, { 'keytext' => idData })
|
113
|
+
end
|
114
|
+
rescue Exception => ex
|
115
|
+
puts "Cound not echo identities from #{fromKeyServer} to #{toKeyServer}"
|
116
|
+
puts ex
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def echoFromTo(fromKeyServer, toKeyServer, options = {})
|
121
|
+
echoKeysFromTo(fromKeyServer, toKeyServer, options)
|
122
|
+
echoIdentitiesFromTo(fromKeyServer, toKeyServer, options)
|
123
|
+
end
|
124
|
+
|
84
125
|
end
|
85
126
|
|
86
127
|
end
|
@@ -0,0 +1,240 @@
|
|
1
|
+
|
2
|
+
module SimpleHKP
|
3
|
+
|
4
|
+
module Identities
|
5
|
+
|
6
|
+
def loadIdentityFile(fileName)
|
7
|
+
identityContents = ''
|
8
|
+
begin
|
9
|
+
identityContents = File.open(fileName, 'r').read
|
10
|
+
rescue Exception => e
|
11
|
+
puts "Error reading file #{fileName}: #{e.message}"
|
12
|
+
end
|
13
|
+
identityContents
|
14
|
+
end
|
15
|
+
|
16
|
+
def getIdentityMetaData(fileContents)
|
17
|
+
metaData = Hash.new
|
18
|
+
begin
|
19
|
+
if fileContents =~ /\A(---\s*\n.*?\n?)^((---|\.\.\.)\s*$\n?)/m
|
20
|
+
metaData = SafeYAML.load($1)
|
21
|
+
end
|
22
|
+
rescue SyntaxError => e
|
23
|
+
puts "YAML Exception reading #{fileName}: #{e.message}"
|
24
|
+
end
|
25
|
+
|
26
|
+
if !metaData.has_key?('role') ||
|
27
|
+
!metaData.has_key?('userKeyID') ||
|
28
|
+
metaData['userKeyID'] !~ /^\h+$/ then
|
29
|
+
puts "WARNING the identity file has NO role or userKeyID or a malformed userKeyID"
|
30
|
+
return ''
|
31
|
+
end
|
32
|
+
|
33
|
+
#
|
34
|
+
# normalize the meta data with any missing (but useful) values
|
35
|
+
#
|
36
|
+
metaData['userID'] = metaData['userKeyID'] unless
|
37
|
+
metaData.has_key?('userID')
|
38
|
+
metaData['algorithm'] = 'unknown' unless metaData.has_key?('algorithm')
|
39
|
+
metaData['created'] = Date.today.strftime("%Y%m%d") unless
|
40
|
+
metaData.has_key?('created')
|
41
|
+
metaData['expires'] = Date.today.strftime("%Y%m%d") unless
|
42
|
+
metaData.has_key?('expires')
|
43
|
+
metaData['flags'] = '' unless metaData.has_key?('flags')
|
44
|
+
#
|
45
|
+
# compute the idFile for searching
|
46
|
+
#
|
47
|
+
metaData['identity'] = metaData['role']+'-'+metaData['userKeyID']
|
48
|
+
#
|
49
|
+
# wrap the whole meta data into a searchable string
|
50
|
+
#
|
51
|
+
metaData['searchData'] = metaData.to_s
|
52
|
+
metaData
|
53
|
+
end
|
54
|
+
|
55
|
+
def indexIdentityFiles
|
56
|
+
puts "SimpleHKP: indexing identities" if @debug
|
57
|
+
@idMetaData = Hash.new
|
58
|
+
Dir.glob(@idDir+'/**/*.asc') do | aFile |
|
59
|
+
idFile = File.basename(aFile, '.*')
|
60
|
+
puts aFile if @debug
|
61
|
+
puts idFile if @debug
|
62
|
+
identityFile = loadIdentityFile(aFile)
|
63
|
+
metaData = getIdentityMetaData(identityFile)
|
64
|
+
if metaData.empty? then
|
65
|
+
puts "WARNING the identity file [#{aFile}] is malformed!"
|
66
|
+
else
|
67
|
+
@idMetaData[idFile] = metaData
|
68
|
+
end
|
69
|
+
puts @idMetaData[idFile] if @debug
|
70
|
+
end
|
71
|
+
# File.open(@idIndex, 'w') do | indexFile |
|
72
|
+
# indexFile.puts "# identity metaData index created: #{DateTime.now}"
|
73
|
+
# indexFile.write(YAML.dump(@idMetaData))
|
74
|
+
# end
|
75
|
+
puts "SimpleHKP: finished indexing identities" if @debug
|
76
|
+
end
|
77
|
+
|
78
|
+
def loadIdentities
|
79
|
+
@idLookupData = Hash.new
|
80
|
+
puts "SimpleHKP: loading identities" if @debug
|
81
|
+
indexIdentityFiles
|
82
|
+
pp @idMetaData
|
83
|
+
puts "SimpleHKP: finished loading identities" if @debug
|
84
|
+
end
|
85
|
+
|
86
|
+
def storeIdentity(env)
|
87
|
+
#
|
88
|
+
# decode the post data
|
89
|
+
#
|
90
|
+
keys = URI.decode_www_form(env['rack.input'].read)
|
91
|
+
#
|
92
|
+
# ensure we are being sent a key
|
93
|
+
#
|
94
|
+
return replyBadRequest("No keytext field in post data") unless
|
95
|
+
keys[0][0] =~ /keytext/
|
96
|
+
pp keys[0][1] if @debug
|
97
|
+
metaData = getIdentityMetaData(keys[0][1])
|
98
|
+
pp metaData
|
99
|
+
return replyBadRequest("malformed identity metaData") if metaData.empty?
|
100
|
+
#
|
101
|
+
# store this key as a flat file with the name of the keyID.asc
|
102
|
+
#
|
103
|
+
idFile = metaData['role']+'-'+metaData['userKeyID']
|
104
|
+
idFileName = @idDir+'/'+idFile+'.asc'
|
105
|
+
idChanged = 'unchanged'
|
106
|
+
if !File.exists?(idFileName) || keys[0][1] != File.read(idFileName) then
|
107
|
+
idChanged = 'changed'
|
108
|
+
File.write(idFileName, keys[0][1])
|
109
|
+
@idMetaData[idFile] = metaData
|
110
|
+
end
|
111
|
+
pp @idMetaData
|
112
|
+
#
|
113
|
+
# return OK
|
114
|
+
#
|
115
|
+
@statusCode = 200
|
116
|
+
@body << @headerHtml
|
117
|
+
@body << "<h1 class=\"simpleHKP-storedIdentity\">Stored identity: [#{idFile}]</h1>"
|
118
|
+
@body << "<p>identity #{idChanged}</p>"
|
119
|
+
@body << "<h2 class=\"simpleHKP-identityDataID\">Identity data for identity: [#{idFile}]</h2>"
|
120
|
+
@body << '<pre class="simpleHKP-identityData">'+keys[0][1]+"</pre>\n"
|
121
|
+
@body << @footer
|
122
|
+
end
|
123
|
+
|
124
|
+
def lookUpIdentity(queryString)
|
125
|
+
return replyBadRequest("No search field in queryString") unless
|
126
|
+
queryString.has_key?('search')
|
127
|
+
#
|
128
|
+
# normalize the search string
|
129
|
+
#
|
130
|
+
searchString = queryString['search']
|
131
|
+
searchRegexp = Regexp.new(searchString,
|
132
|
+
Regexp::IGNORECASE | Regexp::MULTILINE)
|
133
|
+
puts searchRegexp if @debug
|
134
|
+
#
|
135
|
+
# (linearly) look through the hash of known identities
|
136
|
+
# looking for the FIRST match
|
137
|
+
#
|
138
|
+
idFile = nil
|
139
|
+
pp @idMetaData
|
140
|
+
@idMetaData.each_pair do | idKey, idData |
|
141
|
+
next unless idData['searchData'] =~ searchRegexp
|
142
|
+
puts "FOUND #{idKey} (#{idData})" if @debug
|
143
|
+
idFile = idKey
|
144
|
+
break
|
145
|
+
end
|
146
|
+
return replyNotFound if idFile.nil?
|
147
|
+
|
148
|
+
idFileName = @idDir+'/'+idFile+'.asc'
|
149
|
+
if queryString.has_key?('options') &&
|
150
|
+
queryString['options'] == 'mr' then
|
151
|
+
#
|
152
|
+
# return the key data in machine readable format
|
153
|
+
#
|
154
|
+
@header = {
|
155
|
+
'Content-Type' => 'text/plain; charset=utf-8',
|
156
|
+
'Content-Disposition' =>
|
157
|
+
'attachment; filename=' + File.basename(idFileName)
|
158
|
+
}
|
159
|
+
puts @header if @debug
|
160
|
+
@body << File.read(idFileName)
|
161
|
+
else
|
162
|
+
#
|
163
|
+
# return the key data for a human to read
|
164
|
+
#
|
165
|
+
@body << @headerHtml
|
166
|
+
@body << @lookupIdentitiesForm
|
167
|
+
@body << '<h1 class="simpleHKP-identityAscIdFile">Identity: '+idFile+'</h1>'
|
168
|
+
@body << '<h2 class="simpleHKP-identityAscTitle">Identity contents:</h2>'
|
169
|
+
@body << '<pre class="simpleHKP-identityAsc">'
|
170
|
+
@body << File.read(idFileName)
|
171
|
+
@body << '</pre>'
|
172
|
+
@body << @footer
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def indexIdentities(queryString)
|
177
|
+
return replyBadRequest("No search field in queryString") unless
|
178
|
+
queryString.has_key?('search')
|
179
|
+
#
|
180
|
+
# normalize the search string
|
181
|
+
#
|
182
|
+
searchString = queryString['search']
|
183
|
+
searchRegexp = Regexp.new(searchString,
|
184
|
+
Regexp::IGNORECASE | Regexp::MULTILINE)
|
185
|
+
puts searchRegexp if @debug
|
186
|
+
#
|
187
|
+
# accumulate the idFiles of any identities that match the search
|
188
|
+
#
|
189
|
+
idFiles = Array.new
|
190
|
+
pp @idMetaData
|
191
|
+
@idMetaData.each_pair do | idKey, idData |
|
192
|
+
next unless idData['searchData'] =~ searchRegexp
|
193
|
+
puts "FOUND #{idKey} (#{idData})" if @debug
|
194
|
+
idFiles.push(idKey)
|
195
|
+
end
|
196
|
+
|
197
|
+
if queryString.has_key?('options') &&
|
198
|
+
queryString['options'] == 'mr' then
|
199
|
+
#
|
200
|
+
# return a machine readable list of the dFiles found
|
201
|
+
#
|
202
|
+
@header = { 'Content-Type' => 'text/plain' }
|
203
|
+
@body << "info:1:#{idFiles.size}\n"
|
204
|
+
idFiles.each do | anIdFile |
|
205
|
+
next unless @idMetaData.has_key?(anIdFile)
|
206
|
+
idData = @idMetaData[anIdFile]
|
207
|
+
idnData = [ "idn" ]
|
208
|
+
idnData << anIdFile
|
209
|
+
idnData << idData['role']
|
210
|
+
idnData << idData['userID'].gsub(/:/, '\x3a')
|
211
|
+
idnData << idData['algorithm']
|
212
|
+
idnData << idData['created']
|
213
|
+
idnData << idData['expires']
|
214
|
+
idnData << idData['flags']
|
215
|
+
@body << idnData.join(':')
|
216
|
+
@body << "\n"
|
217
|
+
end
|
218
|
+
else
|
219
|
+
#
|
220
|
+
# return a (simple) human readable list of the keys found
|
221
|
+
#
|
222
|
+
@body << @headerHtml
|
223
|
+
@body << @lookupIdentitiesForm
|
224
|
+
@body << '<h1 class="simpleHKP-searchString">Identities matching: ['+searchString+']</h1><ul class="simpleHKP-searchList">'
|
225
|
+
idFiles.each do | anIdFile |
|
226
|
+
next unless @idMetaData.has_key?(anIdFile)
|
227
|
+
idData = @idMetaData[anIdFile]
|
228
|
+
idStr = " <li class=\"simpleHKP-searchItem\"><a href=\"/pis/lookup?op=get&search=#{anIdFile}\">"
|
229
|
+
idStr << anIdFile+': '+idData['userID']+' ('+idData['role']+')'
|
230
|
+
idStr << "</a></li>\n"
|
231
|
+
@body << idStr
|
232
|
+
end
|
233
|
+
@body << '</ul>'
|
234
|
+
@body << @footer
|
235
|
+
end
|
236
|
+
pp @body if @debug
|
237
|
+
end
|
238
|
+
|
239
|
+
end
|
240
|
+
end
|