mikehale-akismet 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/VERSION.yml +4 -0
- data/lib/akismet.rb +64 -0
- data/spec/akismet_spec.rb +83 -0
- data/spec/spec.opts +6 -0
- data/spec/spec_helper.rb +26 -0
- data/spec/spec_http.rb +164 -0
- metadata +59 -0
data/VERSION.yml
ADDED
data/lib/akismet.rb
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'uri'
|
3
|
+
|
4
|
+
class Akismet
|
5
|
+
USER_AGENT = "Akismet-rb/1.0 | Akismet/1.11"
|
6
|
+
|
7
|
+
def initialize(key, url)
|
8
|
+
@key = key
|
9
|
+
@url = url
|
10
|
+
end
|
11
|
+
|
12
|
+
def verify_key
|
13
|
+
response = Net::HTTP.start('rest.akismet.com', 80) do |http|
|
14
|
+
http.post('/1.1/verify-key', post_data(:key => @key, :blog => @url), {'User-Agent' => USER_AGENT})
|
15
|
+
end
|
16
|
+
|
17
|
+
case response.body
|
18
|
+
when "invalid"
|
19
|
+
raise Akismet::VerifyException, response.to_hash["x-akismet-debug-help"], caller
|
20
|
+
when "valid"
|
21
|
+
true
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def submit_spam(args)
|
26
|
+
call_akismet('submit-spam', args)
|
27
|
+
end
|
28
|
+
|
29
|
+
def submit_ham(args)
|
30
|
+
call_akismet('submit-ham', args)
|
31
|
+
end
|
32
|
+
|
33
|
+
def spam?(args)
|
34
|
+
call_akismet('comment-check', args)
|
35
|
+
end
|
36
|
+
|
37
|
+
def ham?(args)
|
38
|
+
!spam?(args)
|
39
|
+
end
|
40
|
+
|
41
|
+
def call_akismet(method, args)
|
42
|
+
args.update(:blog => @url)
|
43
|
+
|
44
|
+
response = Net::HTTP.start("#{@key}.rest.akismet.com", 80) do |http|
|
45
|
+
http.post("/1.1/#{method}", post_data(args), {'User-Agent' => USER_AGENT})
|
46
|
+
end
|
47
|
+
|
48
|
+
case response.body
|
49
|
+
when "true"
|
50
|
+
true
|
51
|
+
when "false"
|
52
|
+
false
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def post_data(hash)
|
57
|
+
hash.inject([]) do |memo, hash|
|
58
|
+
k, v = hash
|
59
|
+
memo << "#{k}=#{URI.escape(v)}"
|
60
|
+
end.join('&')
|
61
|
+
end
|
62
|
+
|
63
|
+
class VerifyException < Exception; end
|
64
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
describe "Akismet" do
|
4
|
+
include SpecHttp
|
5
|
+
|
6
|
+
def params
|
7
|
+
{
|
8
|
+
:user_ip => "1.2.3.4",
|
9
|
+
:referrer => "http://othersite.com",
|
10
|
+
:permalink => "http://example.com/post",
|
11
|
+
:comment_type => "comment",
|
12
|
+
:comment_author => "joe smith",
|
13
|
+
:comment_author_email => "joe@smith.com",
|
14
|
+
:comment_author_url => "blog.smith.com"
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
before do
|
19
|
+
@akismet = Akismet.new('thekey', 'http://example.com')
|
20
|
+
app = lambda do |env|
|
21
|
+
spam = env["rack.input"].include?('viagra')
|
22
|
+
[200, {}, [spam ? "true" : "false"]]
|
23
|
+
end
|
24
|
+
map Rack::URLMap.new("http://thekey.rest.akismet.com/1.1/comment-check" => app)
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should verify the key" do
|
28
|
+
map Rack::URLMap.new("http://rest.akismet.com/" => lambda { |env| [200, {}, ["valid"]]})
|
29
|
+
|
30
|
+
@akismet.verify_key.should == true
|
31
|
+
request.post?.should == true
|
32
|
+
request.env['HTTP_USER_AGENT'].should == "Akismet-rb/1.0 | Akismet/1.11"
|
33
|
+
request.body.should include('http://example.com')
|
34
|
+
response.status.should == 200
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should not verify an invalid key" do
|
38
|
+
map Rack::URLMap.new("http://rest.akismet.com/" => lambda { |env| [200, {'x-akismet-debug-help' => 'sorry!'}, ["invalid"]]})
|
39
|
+
lambda {@akismet.verify_key}.should raise_error Akismet::VerifyException
|
40
|
+
response['x-akismet-debug-help'].should == 'sorry!'
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should detect spam" do
|
44
|
+
@akismet.spam?(params.update(:comment_content => "viagra-test-123")).should == true
|
45
|
+
request.env.has_key?('HTTP_USER_AGENT').should == true
|
46
|
+
request.body.should include("blog=http://example.com")
|
47
|
+
request.body.should include("user_ip=1.2.3.4")
|
48
|
+
request.body.should include("referrer=http://othersite.com")
|
49
|
+
request.body.should include("permalink=http://example.com/post")
|
50
|
+
request.body.should include("comment_type=comment")
|
51
|
+
request.body.should include("comment_author=joe%20smith")
|
52
|
+
request.body.should include("comment_author_email=joe@smith.com")
|
53
|
+
request.body.should include("comment_author_url=blog.smith.com")
|
54
|
+
request.body.should include("comment_content=viagra-test-123")
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should detect ham" do
|
58
|
+
@akismet.ham?(params.update(:comment_content => "not spam")).should == true
|
59
|
+
end
|
60
|
+
|
61
|
+
it "should submit spam" do
|
62
|
+
map Rack::URLMap.new("http://thekey.rest.akismet.com/1.1/submit-spam" => lambda { |env| [200, {}, "true"]})
|
63
|
+
@akismet.submit_spam(params.update(:comment_content => "this-is-spam"))
|
64
|
+
request.script_name.should == "/1.1/submit-spam"
|
65
|
+
|
66
|
+
request.env.has_key?('HTTP_USER_AGENT').should == true
|
67
|
+
request.body.should include("blog=http://example.com")
|
68
|
+
request.body.should include("user_ip=1.2.3.4")
|
69
|
+
request.body.should include("referrer=http://othersite.com")
|
70
|
+
request.body.should include("permalink=http://example.com/post")
|
71
|
+
request.body.should include("comment_type=comment")
|
72
|
+
request.body.should include("comment_author=joe%20smith")
|
73
|
+
request.body.should include("comment_author_email=joe@smith.com")
|
74
|
+
request.body.should include("comment_author_url=blog.smith.com")
|
75
|
+
request.body.should include("comment_content=this-is-spam")
|
76
|
+
end
|
77
|
+
|
78
|
+
it "should submit ham" do
|
79
|
+
map Rack::URLMap.new("http://thekey.rest.akismet.com/1.1/submit-ham" => lambda { |env| [200, {}, "true"]})
|
80
|
+
@akismet.submit_ham(params.update(:comment_content => "this-is-ham"))
|
81
|
+
request.script_name.should == "/1.1/submit-ham"
|
82
|
+
end
|
83
|
+
end
|
data/spec/spec.opts
ADDED
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'spec'
|
2
|
+
require 'rr'
|
3
|
+
require 'rack/urlmap'
|
4
|
+
require File.dirname(__FILE__) + '/spec_http'
|
5
|
+
require 'ruby-debug'
|
6
|
+
|
7
|
+
class MethodSpy
|
8
|
+
def initialize(delegate, &block)
|
9
|
+
@delegate = delegate
|
10
|
+
@filter = block
|
11
|
+
end
|
12
|
+
|
13
|
+
def method_missing(symbol, *args, &block)
|
14
|
+
result = @delegate.send(symbol, *args, &block)
|
15
|
+
p [symbol, args, result, block]
|
16
|
+
result
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
21
|
+
|
22
|
+
require 'akismet'
|
23
|
+
|
24
|
+
Spec::Runner.configure do |config|
|
25
|
+
config.mock_with :rr
|
26
|
+
end
|
data/spec/spec_http.rb
ADDED
@@ -0,0 +1,164 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'rack/request'
|
3
|
+
require 'rack/response'
|
4
|
+
require 'rack/utils'
|
5
|
+
|
6
|
+
module SpecHttp
|
7
|
+
def map(map)
|
8
|
+
@socket = SocketInterceptor.new(map)
|
9
|
+
stub(TCPSocket).open(anything, numeric) { @socket }
|
10
|
+
end
|
11
|
+
|
12
|
+
def request
|
13
|
+
Rack::Request.new(@socket.env)
|
14
|
+
end
|
15
|
+
|
16
|
+
def response
|
17
|
+
@socket.response
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class SocketInterceptor < StringIO
|
22
|
+
attr_reader :env, :response
|
23
|
+
|
24
|
+
def initialize(map)
|
25
|
+
@map = map
|
26
|
+
super()
|
27
|
+
end
|
28
|
+
|
29
|
+
def write(string)
|
30
|
+
raise "Already responded" if self.string.size > 0
|
31
|
+
@written = '' unless @written
|
32
|
+
@written << string
|
33
|
+
string.size
|
34
|
+
end
|
35
|
+
|
36
|
+
# this depends on Net::HTTP calling socket.sysread after it has finished all writes
|
37
|
+
alias :orig_sysread :sysread
|
38
|
+
def sysread(size)
|
39
|
+
set_string unless self.string.size > 0
|
40
|
+
orig_sysread(size)
|
41
|
+
end
|
42
|
+
|
43
|
+
def set_string
|
44
|
+
response_io = StringIO.new
|
45
|
+
@env = Rack::RequestStringParser.env(@written)
|
46
|
+
Rack::Handler::StringIO.run(app(@env), @env, response_io)
|
47
|
+
@response = Rack::Handler::StringIO.response
|
48
|
+
self.string = response_io.string
|
49
|
+
end
|
50
|
+
|
51
|
+
def app(env)
|
52
|
+
app_to_use = @map if !@map.respond_to? :mapping
|
53
|
+
unless app_to_use
|
54
|
+
path = env["PATH_INFO"].to_s.squeeze("/")
|
55
|
+
hHost, sName, sPort = env.values_at('HTTP_HOST','SERVER_NAME','SERVER_PORT')
|
56
|
+
@map.mapping.each { |host, location, app|
|
57
|
+
next unless (hHost == host || sName == host \
|
58
|
+
|| (host.nil? && (hHost == sName || hHost == sName+':'+sPort)))
|
59
|
+
next unless location == path[0, location.size]
|
60
|
+
next unless path[location.size] == nil || path[location.size] == ?/
|
61
|
+
|
62
|
+
env["SCRIPT_NAME"] += location
|
63
|
+
env["PATH_INFO"] = path[location.size..-1]
|
64
|
+
app_to_use = app
|
65
|
+
}
|
66
|
+
raise "No application associated with #{env['rack.url_scheme']}://#{hHost}#{path}" unless app_to_use
|
67
|
+
end
|
68
|
+
app_to_use
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
module Rack
|
73
|
+
class URLMap
|
74
|
+
attr_reader :mapping
|
75
|
+
end
|
76
|
+
|
77
|
+
class RequestStringParser
|
78
|
+
def self.env_key(key)
|
79
|
+
key = key.split('-').join('_').upcase
|
80
|
+
key = "HTTP_#{key}" unless key =~ /content/i
|
81
|
+
key
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.env(input)
|
85
|
+
lines = input.split("\r\n")
|
86
|
+
|
87
|
+
# find blank line which seperates the headers from the body
|
88
|
+
index_of_blank = nil
|
89
|
+
lines.each_with_index{|e,i|
|
90
|
+
index_of_blank = i if e == ""
|
91
|
+
}
|
92
|
+
|
93
|
+
type, uri = lines.first.split(/\s+/)
|
94
|
+
|
95
|
+
if index_of_blank
|
96
|
+
headers = lines[1..index_of_blank]
|
97
|
+
body = lines[(index_of_blank + 1)..-1].first
|
98
|
+
else
|
99
|
+
headers = lines[1..-1]
|
100
|
+
end
|
101
|
+
|
102
|
+
headers = headers.inject({}){|h,e|
|
103
|
+
k,v = e.split(/:\s+/)
|
104
|
+
h.merge! env_key(k) => v if k
|
105
|
+
h
|
106
|
+
}
|
107
|
+
|
108
|
+
uri = URI(uri)
|
109
|
+
env = headers.dup
|
110
|
+
env["SERVER_NAME"] = uri.host
|
111
|
+
env["SERVER_PORT"] = uri.port ? uri.port.to_s : "80"
|
112
|
+
env["QUERY_STRING"] = uri.query.to_s
|
113
|
+
env["PATH_INFO"] = (!uri.path || uri.path.empty?) ? "/" : uri.path
|
114
|
+
env["SCRIPT_NAME"] = ""
|
115
|
+
env["REQUEST_METHOD"] = type
|
116
|
+
env["rack.url_scheme"] = uri.scheme || "http"
|
117
|
+
env["rack.input"] = body
|
118
|
+
env
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
module Handler
|
123
|
+
class StringIO
|
124
|
+
def self.response
|
125
|
+
@@response
|
126
|
+
end
|
127
|
+
|
128
|
+
def self.run(app, env={}, output=StringIO.new)
|
129
|
+
env["SCRIPT_NAME"] = "" if env["SCRIPT_NAME"] == "/"
|
130
|
+
env["QUERY_STRING"] ||= ""
|
131
|
+
env["HTTP_VERSION"] ||= env["SERVER_PROTOCOL"]
|
132
|
+
env["REQUEST_PATH"] ||= "/"
|
133
|
+
|
134
|
+
status, headers, body = app.call(env)
|
135
|
+
@@response = Rack::Response.new(body, status, headers)
|
136
|
+
begin
|
137
|
+
send_headers output, status, headers
|
138
|
+
send_body output, body
|
139
|
+
output.rewind
|
140
|
+
ensure
|
141
|
+
body.close if body.respond_to? :close
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def self.send_headers(output, status, headers)
|
146
|
+
output.print "HTTP/1.1 #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]}\r\n"
|
147
|
+
headers.each { |k, vs|
|
148
|
+
vs.each { |v|
|
149
|
+
output.print "#{k}: #{v}\r\n"
|
150
|
+
}
|
151
|
+
}
|
152
|
+
output.print "\r\n"
|
153
|
+
output.flush
|
154
|
+
end
|
155
|
+
|
156
|
+
def self.send_body(output, body)
|
157
|
+
body.each { |part|
|
158
|
+
output.print part
|
159
|
+
output.flush
|
160
|
+
}
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
metadata
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: mikehale-akismet
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Michael Hale
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-04-07 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: TODO
|
17
|
+
email: mikehale@gmail.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files: []
|
23
|
+
|
24
|
+
files:
|
25
|
+
- VERSION.yml
|
26
|
+
- lib/akismet.rb
|
27
|
+
- spec/akismet_spec.rb
|
28
|
+
- spec/spec.opts
|
29
|
+
- spec/spec_helper.rb
|
30
|
+
- spec/spec_http.rb
|
31
|
+
has_rdoc: true
|
32
|
+
homepage: http://github.com/mikehale/akismet
|
33
|
+
post_install_message:
|
34
|
+
rdoc_options:
|
35
|
+
- --inline-source
|
36
|
+
- --charset=UTF-8
|
37
|
+
require_paths:
|
38
|
+
- lib
|
39
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: "0"
|
44
|
+
version:
|
45
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: "0"
|
50
|
+
version:
|
51
|
+
requirements: []
|
52
|
+
|
53
|
+
rubyforge_project:
|
54
|
+
rubygems_version: 1.2.0
|
55
|
+
signing_key:
|
56
|
+
specification_version: 2
|
57
|
+
summary: TODO
|
58
|
+
test_files: []
|
59
|
+
|