ape 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG +1 -0
- data/LICENSE +19 -0
- data/Manifest +45 -0
- data/README +12 -0
- data/ape.gemspec +57 -0
- data/bin/ape_server +28 -0
- data/lib/ape.rb +982 -0
- data/lib/ape/atomURI.rb +73 -0
- data/lib/ape/auth/google_login_credentials.rb +96 -0
- data/lib/ape/auth/wsse_credentials.rb +25 -0
- data/lib/ape/authent.rb +42 -0
- data/lib/ape/categories.rb +95 -0
- data/lib/ape/collection.rb +51 -0
- data/lib/ape/crumbs.rb +39 -0
- data/lib/ape/entry.rb +151 -0
- data/lib/ape/escaper.rb +29 -0
- data/lib/ape/feed.rb +117 -0
- data/lib/ape/handler.rb +34 -0
- data/lib/ape/html.rb +17 -0
- data/lib/ape/invoker.rb +54 -0
- data/lib/ape/invokers/deleter.rb +31 -0
- data/lib/ape/invokers/getter.rb +80 -0
- data/lib/ape/invokers/poster.rb +57 -0
- data/lib/ape/invokers/putter.rb +46 -0
- data/lib/ape/layout/ape.css +56 -0
- data/lib/ape/layout/ape_logo.png +0 -0
- data/lib/ape/layout/index.html +54 -0
- data/lib/ape/layout/info.png +0 -0
- data/lib/ape/names.rb +24 -0
- data/lib/ape/print_writer.rb +21 -0
- data/lib/ape/samples.rb +180 -0
- data/lib/ape/samples/atom_schema.txt +338 -0
- data/lib/ape/samples/basic_entry.eruby +16 -0
- data/lib/ape/samples/categories_schema.txt +69 -0
- data/lib/ape/samples/mini_entry.eruby +8 -0
- data/lib/ape/samples/service_schema.txt +187 -0
- data/lib/ape/samples/unclean_xhtml_entry.eruby +21 -0
- data/lib/ape/server.rb +32 -0
- data/lib/ape/service.rb +12 -0
- data/lib/ape/validator.rb +65 -0
- data/lib/ape/version.rb +9 -0
- data/scripts/go.rb +29 -0
- data/test/test_helper.rb +17 -0
- data/test/unit/authent_test.rb +35 -0
- data/test/unit/invoker_test.rb +25 -0
- data/test/unit/samples_test.rb +36 -0
- metadata +111 -0
data/lib/ape/escaper.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# Copyright © 2006 Sun Microsystems, Inc. All rights reserved
|
2
|
+
# Use is subject to license terms - see file "LICENSE"
|
3
|
+
module Ape
|
4
|
+
class Escaper
|
5
|
+
def Escaper.escape(text)
|
6
|
+
text.gsub(/([&<'">])/) do
|
7
|
+
case $1
|
8
|
+
when '&' then '&'
|
9
|
+
when '<' then '<'
|
10
|
+
when "'" then '''
|
11
|
+
when '"' then '"'
|
12
|
+
when '>' then '>'
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def Escaper.unescape(text)
|
18
|
+
text.gsub(/&([^;]*);/) do
|
19
|
+
case $1
|
20
|
+
when 'lt' then '<'
|
21
|
+
when 'amp' then '&'
|
22
|
+
when 'gt' then '>'
|
23
|
+
when 'apos' then "'"
|
24
|
+
when 'quot' then '"'
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/ape/feed.rb
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
# Copyright © 2006 Sun Microsystems, Inc. All rights reserved
|
2
|
+
# Use is subject to license terms - see file "LICENSE"
|
3
|
+
|
4
|
+
require 'rexml/document'
|
5
|
+
require 'rexml/xpath'
|
6
|
+
|
7
|
+
module Ape
|
8
|
+
class Feed
|
9
|
+
# Load up a collection feed from its URI. Return an array of <entry> objects.
|
10
|
+
# follow <link rel="next" /> pointers as required to get the whole
|
11
|
+
# collection
|
12
|
+
def Feed.read(uri, name, ape, report=true)
|
13
|
+
|
14
|
+
entries = []
|
15
|
+
uris = []
|
16
|
+
next_page = uri
|
17
|
+
page_num = 1
|
18
|
+
|
19
|
+
while (next_page) && (page_num < 10) do
|
20
|
+
|
21
|
+
label = "Page #{page_num} of #{name}"
|
22
|
+
uris << next_page
|
23
|
+
page = ape.check_resource(next_page, label, Names::AtomMediaType, report)
|
24
|
+
break unless page
|
25
|
+
|
26
|
+
# * Validate it
|
27
|
+
Validator.validate(Samples.atom_RNC, page.body, label, ape) if report
|
28
|
+
|
29
|
+
# XML-parse the feed
|
30
|
+
error = "not well-formed"
|
31
|
+
feed = nil
|
32
|
+
begin
|
33
|
+
feed = REXML::Document.new(page.body, { :raw => nil })
|
34
|
+
rescue Exception
|
35
|
+
error = $!.to_s
|
36
|
+
feed = nil
|
37
|
+
end
|
38
|
+
if feed == nil
|
39
|
+
ape.error "Can't parse #{label} at #{next_page}, Parser said: #{$!}" if report
|
40
|
+
break
|
41
|
+
end
|
42
|
+
|
43
|
+
feed = feed.root
|
44
|
+
if feed == nil
|
45
|
+
ape.warning "#{label} is empty."
|
46
|
+
break
|
47
|
+
end
|
48
|
+
|
49
|
+
page_entries = REXML::XPath.match(feed, "./atom:entry", Names::XmlNamespaces)
|
50
|
+
if page_entries.empty? && report
|
51
|
+
ape.info "#{label} has no entries."
|
52
|
+
end
|
53
|
+
|
54
|
+
entries += page_entries.map { |e| Entry.new(e, next_page)}
|
55
|
+
|
56
|
+
next_link = REXML::XPath.first(feed, "./atom:link[@rel=\"next\"]", Names::XmlNamespaces)
|
57
|
+
if next_link
|
58
|
+
next_link = next_link.attributes['href']
|
59
|
+
base = AtomURI.new(next_page)
|
60
|
+
next_link = base.absolutize(next_link, feed)
|
61
|
+
if uris.index(next_link)
|
62
|
+
ape.error "Collection contains circular 'next' linkage: #{next_link}" if report
|
63
|
+
break
|
64
|
+
end
|
65
|
+
page_num += 1
|
66
|
+
end
|
67
|
+
next_page = next_link
|
68
|
+
end
|
69
|
+
|
70
|
+
if report && next_page
|
71
|
+
ape.warning "Stopped reading collection after #{page_num} pages."
|
72
|
+
end
|
73
|
+
|
74
|
+
# all done unless we're error-checking
|
75
|
+
return entries unless report
|
76
|
+
|
77
|
+
# Ensure that entries are ordered by app:edited
|
78
|
+
last_date = nil
|
79
|
+
with_app_date = 0
|
80
|
+
clean = true
|
81
|
+
entries.each do |e|
|
82
|
+
datestr = e.child_content("edited", Names::AppNamespace)
|
83
|
+
error = nil
|
84
|
+
if datestr
|
85
|
+
if datestr =~ /\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d+)?(Z|([-+]\d\d:\d\d))/
|
86
|
+
begin
|
87
|
+
date = Time.parse(datestr)
|
88
|
+
with_app_date += 1
|
89
|
+
if last_date && (date > last_date)
|
90
|
+
error = "app:edited values out of order, d #{date} ld #{last_date}"
|
91
|
+
end
|
92
|
+
last_date = date
|
93
|
+
rescue ArgumentError
|
94
|
+
error = "invalid app:edited value: #{datestr}"
|
95
|
+
end
|
96
|
+
else
|
97
|
+
error = "invalid app:edited child: #{datestr}"
|
98
|
+
end
|
99
|
+
if error
|
100
|
+
title = e.child_content "title"
|
101
|
+
ape.error "In entry with title '#{title}', #{error}."
|
102
|
+
clean = false
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
if with_app_date < entries.size
|
107
|
+
ape.error "#{entries.size - with_app_date} of #{entries.size} entries in #{name} lack app:edited elements."
|
108
|
+
clean = false
|
109
|
+
end
|
110
|
+
|
111
|
+
ape.good "#{name} has correct app:edited value order." if clean
|
112
|
+
|
113
|
+
entries
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
data/lib/ape/handler.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'mongrel'
|
3
|
+
require 'ape'
|
4
|
+
|
5
|
+
module Ape
|
6
|
+
class Handler < Mongrel::HttpHandler
|
7
|
+
def process(request, response)
|
8
|
+
cgi = Mongrel::CGIWrapper.new(request, response)
|
9
|
+
|
10
|
+
uri = cgi['uri'].strip
|
11
|
+
user = cgi['username'].strip
|
12
|
+
pass = cgi['password'].strip
|
13
|
+
|
14
|
+
# invoke_ape uri, user, pass, request, response
|
15
|
+
|
16
|
+
if uri.empty?
|
17
|
+
response.start(200, true) do |header, body|
|
18
|
+
header['Content-Type'] = 'text/plain'
|
19
|
+
body << 'URI argument is required'
|
20
|
+
end
|
21
|
+
return
|
22
|
+
end
|
23
|
+
|
24
|
+
format = request.params['HTTP_ACCEPT'] == 'text/plain' ? 'text' : 'html'
|
25
|
+
ape = Ape.new({ :crumbs => true, :output => format })
|
26
|
+
(user && pass) ? ape.check(uri, user, pass) : ape.check(uri)
|
27
|
+
|
28
|
+
response.start(200, true) do |header, body|
|
29
|
+
header['Content-Type'] = 'text/html'
|
30
|
+
ape.report(body)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/ape/html.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# Copyright © 2006 Sun Microsystems, Inc. All rights reserved
|
2
|
+
# Use is subject to license terms - see file "LICENSE"
|
3
|
+
|
4
|
+
module Ape
|
5
|
+
class HTML
|
6
|
+
def HTML.error(message, output=STDOUT)
|
7
|
+
headers(output)
|
8
|
+
output.puts <<EndOfText
|
9
|
+
<title>Error: #{message}</title>
|
10
|
+
</head>
|
11
|
+
<body>
|
12
|
+
<h2>Error</h2>
|
13
|
+
<p>#{message}.</p>
|
14
|
+
EndOfText
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/ape/invoker.rb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
# Copyright © 2006 Sun Microsystems, Inc. All rights reserved
|
2
|
+
# Use is subject to license terms - see file "LICENSE"
|
3
|
+
require 'net/https'
|
4
|
+
|
5
|
+
module Ape
|
6
|
+
class Invoker
|
7
|
+
attr_reader :last_error, :crumbs, :response
|
8
|
+
|
9
|
+
def initialize(uriString, authent)
|
10
|
+
@last_error = nil
|
11
|
+
@crumbs = Crumbs.new
|
12
|
+
@uri = AtomURI.check(uriString)
|
13
|
+
if (@uri.class == String)
|
14
|
+
@last_error = @uri
|
15
|
+
end
|
16
|
+
@authent = authent
|
17
|
+
@authent_checker = 0
|
18
|
+
end
|
19
|
+
|
20
|
+
def header(which)
|
21
|
+
@response[which]
|
22
|
+
end
|
23
|
+
|
24
|
+
def prepare_http
|
25
|
+
http = Net::HTTP.new(@uri.host, @uri.port)
|
26
|
+
if @uri.scheme == 'https'
|
27
|
+
http.use_ssl = true
|
28
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
29
|
+
end
|
30
|
+
http.set_debug_output @crumbs if @crumbs
|
31
|
+
http
|
32
|
+
end
|
33
|
+
|
34
|
+
def need_authentication?(req)
|
35
|
+
if @response.instance_of?(Net::HTTPUnauthorized) && @authent
|
36
|
+
#tries to authenticate just two times in order to avoid infinite loops
|
37
|
+
raise AuthenticationError, 'Authentication is required' unless @authent_checker <= 1
|
38
|
+
@authent_checker += 1
|
39
|
+
|
40
|
+
@authent.add_to req, header('WWW-Authenticate')
|
41
|
+
#clean the request body attribute, if we don't do it http.request(req, body) will raise an exception
|
42
|
+
req.body = nil unless req.body.nil?
|
43
|
+
return true
|
44
|
+
end
|
45
|
+
return false
|
46
|
+
end
|
47
|
+
|
48
|
+
def restart_authent_checker
|
49
|
+
@authent_checker = 0
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
Dir[File.dirname(__FILE__) + '/invokers/*.rb'].each { |l| require l }
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# Copyright © 2006 Sun Microsystems, Inc. All rights reserved
|
2
|
+
# Use is subject to license terms - see file "LICENSE"
|
3
|
+
require 'net/http'
|
4
|
+
|
5
|
+
module Ape
|
6
|
+
class Deleter < Invoker
|
7
|
+
|
8
|
+
def delete( req = nil )
|
9
|
+
req = Net::HTTP::Delete.new(AtomURI.on_the_wire(@uri)) unless req
|
10
|
+
|
11
|
+
begin
|
12
|
+
http = prepare_http
|
13
|
+
|
14
|
+
http.start do |connection|
|
15
|
+
@response = connection.request(req)
|
16
|
+
|
17
|
+
return delete(req) if need_authentication?(req)
|
18
|
+
restart_authent_checker
|
19
|
+
|
20
|
+
return true if @response.kind_of? Net::HTTPSuccess
|
21
|
+
|
22
|
+
@last_error = @response.message
|
23
|
+
return false
|
24
|
+
end
|
25
|
+
rescue Exception
|
26
|
+
@last_error = "Can't connect to #{@uri.host} on port #{@uri.port}: #{$!}"
|
27
|
+
return nil
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# Copyright © 2006 Sun Microsystems, Inc. All rights reserved
|
2
|
+
# Use is subject to license terms - see file "LICENSE"
|
3
|
+
require 'net/http'
|
4
|
+
|
5
|
+
module Ape
|
6
|
+
class Getter < Invoker
|
7
|
+
attr_reader :contentType, :charset, :body, :security_warning
|
8
|
+
|
9
|
+
def get(contentType = nil, depth = 0, req = nil)
|
10
|
+
req = Net::HTTP::Get.new(AtomURI.on_the_wire(@uri)) unless req
|
11
|
+
@last_error = nil
|
12
|
+
|
13
|
+
return false if document_failed?(depth, req)
|
14
|
+
|
15
|
+
begin
|
16
|
+
http = prepare_http
|
17
|
+
|
18
|
+
http.start do |connection|
|
19
|
+
@response = connection.request(req)
|
20
|
+
|
21
|
+
if need_authentication?(req)
|
22
|
+
@security_warning = true unless http.use_ssl?
|
23
|
+
return get(contentType, depth + 1, req)
|
24
|
+
end
|
25
|
+
restart_authent_checker
|
26
|
+
|
27
|
+
case @response
|
28
|
+
when Net::HTTPSuccess
|
29
|
+
return getBody(contentType)
|
30
|
+
|
31
|
+
when Net::HTTPRedirection
|
32
|
+
redirect_to = @uri.merge(@response['location'])
|
33
|
+
@uri = AtomURI.check(redirect_to)
|
34
|
+
return get(contentType, depth + 1)
|
35
|
+
|
36
|
+
else
|
37
|
+
@last_error = @response.message
|
38
|
+
return false
|
39
|
+
end
|
40
|
+
end
|
41
|
+
rescue Exception
|
42
|
+
@last_error = "Can't connect to #{@uri.host} on port #{@uri.port}: #{$!}"
|
43
|
+
return false
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def document_failed?(depth, req)
|
48
|
+
if (depth > 10)
|
49
|
+
if need_authentication?(req)
|
50
|
+
#Authentication required
|
51
|
+
@last_error = "Authentication is required"
|
52
|
+
else
|
53
|
+
# too many redirects
|
54
|
+
@last_error = "Too many redirects"
|
55
|
+
end
|
56
|
+
return true
|
57
|
+
end
|
58
|
+
return false
|
59
|
+
end
|
60
|
+
|
61
|
+
def getBody contentType
|
62
|
+
|
63
|
+
if contentType
|
64
|
+
@contentType = @response['Content-Type']
|
65
|
+
# XXX TODO - better regex
|
66
|
+
if @contentType =~ /^([^;]*);/
|
67
|
+
@contentType = $1
|
68
|
+
end
|
69
|
+
|
70
|
+
if contentType != @contentType
|
71
|
+
@last_error = "Content-type must be '#{contentType}', not '#{@contentType}'"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
@body = @response.body
|
76
|
+
return true
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# Copyright © 2006 Sun Microsystems, Inc. All rights reserved
|
2
|
+
# Use is subject to license terms - see file "LICENSE"
|
3
|
+
require 'net/http'
|
4
|
+
|
5
|
+
module Ape
|
6
|
+
class Poster < Invoker
|
7
|
+
attr_reader :entry, :uri
|
8
|
+
|
9
|
+
def initialize(uriString, authent)
|
10
|
+
super uriString, authent
|
11
|
+
@headers = {}
|
12
|
+
@entry = nil
|
13
|
+
end
|
14
|
+
|
15
|
+
def set_header(name, val)
|
16
|
+
@headers[name] = val
|
17
|
+
end
|
18
|
+
|
19
|
+
def post(contentType, body, req = nil)
|
20
|
+
req = Net::HTTP::Post.new(AtomURI.on_the_wire(@uri)) if req.nil?
|
21
|
+
req.set_content_type contentType
|
22
|
+
@headers.each { |k, v| req[k]= v }
|
23
|
+
|
24
|
+
begin
|
25
|
+
http = prepare_http
|
26
|
+
|
27
|
+
http.start do |connection|
|
28
|
+
@response = connection.request(req, body)
|
29
|
+
|
30
|
+
return post(contentType, body, req) if need_authentication?(req)
|
31
|
+
restart_authent_checker
|
32
|
+
|
33
|
+
if @response.code != '201'
|
34
|
+
@last_error = @response.message
|
35
|
+
return false
|
36
|
+
end
|
37
|
+
|
38
|
+
if (!((@response['Content-type'] =~ %r{^application/atom\+xml}) ||
|
39
|
+
(@response['Content-type'] =~ %r{^application/atom\+xml;type=entry})))
|
40
|
+
return true
|
41
|
+
end
|
42
|
+
|
43
|
+
begin
|
44
|
+
@entry = Entry.new(@response.body)
|
45
|
+
return true
|
46
|
+
rescue ArgumentError
|
47
|
+
@last_error = @entry.broken
|
48
|
+
return false
|
49
|
+
end
|
50
|
+
end
|
51
|
+
rescue Exception
|
52
|
+
@last_error = "Can't connect to #{@uri.host} on port #{@uri.port}: #{$!}"
|
53
|
+
return false
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# Copyright © 2006 Sun Microsystems, Inc. All rights reserved
|
2
|
+
# Use is subject to license terms - see file "LICENSE"
|
3
|
+
require 'net/http'
|
4
|
+
|
5
|
+
module Ape
|
6
|
+
class Putter < Invoker
|
7
|
+
attr_reader :headers
|
8
|
+
|
9
|
+
def initialize(uriString, authent)
|
10
|
+
super uriString, authent
|
11
|
+
@headers = {}
|
12
|
+
end
|
13
|
+
|
14
|
+
def set_header(name, val)
|
15
|
+
@headers[name] = val
|
16
|
+
end
|
17
|
+
|
18
|
+
def put(contentType, body, req = nil)
|
19
|
+
req = Net::HTTP::Put.new(AtomURI.on_the_wire(@uri)) unless req
|
20
|
+
|
21
|
+
req.set_content_type contentType
|
22
|
+
@headers.each { |k, v| req[k]= v }
|
23
|
+
|
24
|
+
begin
|
25
|
+
http = prepare_http
|
26
|
+
|
27
|
+
http.start do |connection|
|
28
|
+
@response = connection.request(req, body)
|
29
|
+
|
30
|
+
return put(contentType, body, req) if need_authentication?(req)
|
31
|
+
restart_authent_checker
|
32
|
+
|
33
|
+
unless @response.kind_of? Net::HTTPSuccess
|
34
|
+
@last_error = @response.message
|
35
|
+
return false
|
36
|
+
end
|
37
|
+
|
38
|
+
return true
|
39
|
+
end
|
40
|
+
rescue Exception
|
41
|
+
@last_error = "Can't connect to #{@uri.host} on port #{@uri.port}: #{$!}"
|
42
|
+
return nil
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|