ape 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|