shodan-ruby 0.1.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.
@@ -0,0 +1,250 @@
1
+ #
2
+ # shodan-ruby - A Ruby interface to SHODAN, a computer search engine.
3
+ #
4
+ # Copyright (c) 2009 Hal Brodigan (postmodern.mod3 at gmail.com)
5
+ #
6
+ # This program is free software; you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License as published by
8
+ # the Free Software Foundation; either version 2 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU General Public License
17
+ # along with this program; if not, write to the Free Software
18
+ # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
19
+ #
20
+
21
+ require 'shodan/extensions/uri'
22
+ require 'shodan/countries'
23
+ require 'shodan/has_pages'
24
+ require 'shodan/page'
25
+ require 'shodan/shodan'
26
+
27
+ require 'net/http'
28
+ require 'nokogiri'
29
+
30
+ module Shodan
31
+ class Query
32
+
33
+ include HasPages
34
+
35
+ # Search URL
36
+ SEARCH_URL = 'http://shodan.surtri.com/'
37
+
38
+ # Results per page
39
+ RESULTS_PER_PAGE = 20
40
+
41
+ # Search query
42
+ attr_accessor :query
43
+
44
+ # Countries to search within
45
+ attr_accessor :countries
46
+
47
+ # Hostnames to search for
48
+ attr_accessor :hostnames
49
+
50
+ # CIDR Network blocks to search within
51
+ attr_accessor :networks
52
+
53
+ # Ports to search for
54
+ attr_accessor :ports
55
+
56
+ #
57
+ # Creates a new Query object.
58
+ #
59
+ # @param [Hash] options
60
+ # Additional options.
61
+ #
62
+ # @option options [String] :query
63
+ # The query expression.
64
+ #
65
+ # @option options [Array<String>] :countries
66
+ # The Country Codes to search within.
67
+ #
68
+ # @option options [String] :country
69
+ # A Country Code to search within.
70
+ #
71
+ # @option options [Array<String>] :hostnames
72
+ # The host names to search for.
73
+ #
74
+ # @option options [String] :hostname
75
+ # A host name to search for.
76
+ #
77
+ # @option options [Array<String>] :networks
78
+ # The CIDR network blocks to search within.
79
+ #
80
+ # @option options [String] :network
81
+ # A CIDR network blocks to search within.
82
+ #
83
+ # @option options [Array<Integer>] :ports
84
+ # The ports to search for.
85
+ #
86
+ # @option options [Integer] :port
87
+ # A port to search for.
88
+ #
89
+ # @yield [query]
90
+ # If a block is given, it will be passed the newly created Query
91
+ # object.
92
+ #
93
+ # @yieldparam [Query] query
94
+ # The newly created Query object.
95
+ #
96
+ def initialize(options={},&block)
97
+ @agent = Shodan.web_agent
98
+ @query = options[:query]
99
+
100
+ @countries = []
101
+
102
+ if options[:countries]
103
+ @countries += options[:countries]
104
+ elsif options[:country]
105
+ @countries << option[:country]
106
+ end
107
+
108
+ @hostnames = []
109
+
110
+ if options[:hostnames]
111
+ @hostnames += options[:hostnames]
112
+ elsif options[:hostname]
113
+ @hostnames << options[:hostname]
114
+ end
115
+
116
+ @networks = []
117
+
118
+ if options[:networks]
119
+ @networks += options[:networks]
120
+ elsif options[:network]
121
+ @networks << options[:network]
122
+ end
123
+
124
+ @ports = []
125
+
126
+ if options[:ports]
127
+ @ports += options[:ports]
128
+ elsif options[:port]
129
+ @ports << options[:port]
130
+ end
131
+
132
+ block.call(self) if block
133
+ end
134
+
135
+ #
136
+ # Converts a given URL into a Query object.
137
+ #
138
+ # @param [URI::HTTP, String] url
139
+ # The query URL.
140
+ #
141
+ # @return [Query]
142
+ # The Query object created from the URL.
143
+ #
144
+ def self.from_url(url)
145
+ url = URI(url.to_s)
146
+
147
+ return self.new(
148
+ :query => url.query_params['q'].gsub('+',' ')
149
+ )
150
+ end
151
+
152
+ #
153
+ # The results per page.
154
+ #
155
+ # @return [Integer]
156
+ # The resutls per page.
157
+ #
158
+ # @see RESULTS_PER_PAGE
159
+ #
160
+ def results_per_page
161
+ RESULTS_PER_PAGE
162
+ end
163
+
164
+ #
165
+ # The query expression from the query.
166
+ #
167
+ # @return [String]
168
+ # The query expression.
169
+ #
170
+ def expression
171
+ expr = []
172
+
173
+ expr << @query if @query
174
+
175
+ expr += @countries.map { |code| "country:#{code}" }
176
+ expr += @hostnames.map { |host| "hostname:#{host}" }
177
+ expr += @networks.map { |net| "net:#{net}" }
178
+ expr += @ports.map { |port| "port:#{port}" }
179
+
180
+ return expr.join(' ')
181
+ end
182
+
183
+ #
184
+ # The search URL for the query.
185
+ #
186
+ # @return [URI::HTTP]
187
+ # The search URL.
188
+ #
189
+ def search_url
190
+ url = URI(SEARCH_URL)
191
+
192
+ url.query_params['q'] = expression
193
+ return url
194
+ end
195
+
196
+ #
197
+ # The search URL for the query and given page index.
198
+ #
199
+ # @param [Integer] index
200
+ # The page index to lookup.
201
+ #
202
+ # @retrun [URI::HTTP]
203
+ # The search URL for the query and the specified page index.
204
+ #
205
+ def page_url(index)
206
+ url = search_url
207
+
208
+ unless index == 1
209
+ url.query_params['page'] = index
210
+ end
211
+
212
+ return url
213
+ end
214
+
215
+ #
216
+ # Requests a page at the given index.
217
+ #
218
+ # @param [Integer] index
219
+ # The index of the page.
220
+ #
221
+ # @return [Page]
222
+ # The page at the specified index.
223
+ #
224
+ def page(index)
225
+ Page.new do |new_page|
226
+ doc = @agent.get(page_url(index))
227
+
228
+ doc.search('#search/div.result').each do |result|
229
+ div = result.at('div')
230
+
231
+ ip = if (a = div.at('a'))
232
+ a.inner_text
233
+ else
234
+ div.children.first.inner_text
235
+ end
236
+
237
+ hostname = if (host_node = result.at('div/a:last'))
238
+ host_node.inner_text
239
+ end
240
+
241
+ date = result.at('div/span').inner_text.scan(/\d+\.\d+\.\d/).first
242
+ response = result.at('p').inner_text.strip
243
+
244
+ new_page << Host.new(ip,date,response,hostname)
245
+ end
246
+ end
247
+ end
248
+
249
+ end
250
+ end
@@ -0,0 +1,216 @@
1
+ #
2
+ # shodan-ruby - A Ruby interface to SHODAN, a computer search engine.
3
+ #
4
+ # Copyright (c) 2009 Hal Brodigan (postmodern.mod3 at gmail.com)
5
+ #
6
+ # This program is free software; you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License as published by
8
+ # the Free Software Foundation; either version 2 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU General Public License
17
+ # along with this program; if not, write to the Free Software
18
+ # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
19
+ #
20
+
21
+ require 'shodan/query'
22
+
23
+ require 'mechanize'
24
+
25
+ module Shodan
26
+ # Common proxy port.
27
+ COMMON_PROXY_PORT = 8080
28
+
29
+ #
30
+ # The proxy information used by {Query}.
31
+ #
32
+ # @return [Hash]
33
+ # The proxy information.
34
+ #
35
+ def Shodan.proxy
36
+ @@shodan_proxy ||= {:host => nil, :port => COMMON_PROXY_PORT, :user => nil, :password => nil}
37
+ end
38
+
39
+ #
40
+ # Creates a HTTP URI based from the given proxy information.
41
+ #
42
+ # @param [Hash] proxy_info (Shodan.proxy)
43
+ # The proxy information.
44
+ #
45
+ # @option proxy_info [String] :host
46
+ # The host of the proxy.
47
+ #
48
+ # @option proxy_info [Integer] :port (COMMON_PROXY_PORT)
49
+ # The port of the proxy.
50
+ #
51
+ # @option proxy_info [String] :user
52
+ # The user name to authenticate as.
53
+ #
54
+ # @option proxy_info [String] :password
55
+ # The password to authenticate with.
56
+ #
57
+ def Shodan.proxy_uri(proxy_info=Shodan.proxy)
58
+ if Shodan.proxy[:host]
59
+ return URI::HTTP.build(
60
+ :host => Shodan.proxy[:host],
61
+ :port => Shodan.proxy[:port],
62
+ :userinfo => "#{Shodan.proxy[:user]}:#{Shodan.proxy[:password]}",
63
+ :path => '/'
64
+ )
65
+ end
66
+ end
67
+
68
+ #
69
+ # The supported Shodan User-Agent Aliases.
70
+ #
71
+ # @return [Hash{String => String}]
72
+ # The User-Agent aliases and strings.
73
+ #
74
+ def Shodan.user_agent_aliases
75
+ WWW::Mechanize::AGENT_ALIASES
76
+ end
77
+
78
+ #
79
+ # The default Shodan User-Agent
80
+ #
81
+ # @return [String]
82
+ # The default User-Agent string used by Shodan.
83
+ #
84
+ def Shodan.user_agent
85
+ @@shodan_user_agent ||= Shodan.user_agent_aliases['Windows IE 6']
86
+ end
87
+
88
+ #
89
+ # Sets the default Shodan User-Agent.
90
+ #
91
+ # @param [String] agent
92
+ # The User-Agent string to use.
93
+ #
94
+ # @return [String]
95
+ # The new User-Agent string.
96
+ #
97
+ def Shodan.user_agent=(agent)
98
+ @@shodan_user_agent = agent
99
+ end
100
+
101
+ #
102
+ # Sets the default Shodan User-Agent alias.
103
+ #
104
+ # @param [String] name
105
+ # The alias of the User-Agent string to use.
106
+ #
107
+ # @return [String]
108
+ # The new User-Agent string.
109
+ #
110
+ def Shodan.user_agent_alias=(name)
111
+ @@shodan_user_agent = Shodan.user_agent_aliases[name.to_s]
112
+ end
113
+
114
+ #
115
+ # Creates a new WWW::Mechanize agent.
116
+ #
117
+ # @param [Hash] options
118
+ # Additional options.
119
+ #
120
+ # @option options [String] :user_agent_alias
121
+ # The User-Agent alias to use.
122
+ #
123
+ # @option options [String] :user_agent (Shodan.user_agent)
124
+ # The User-Agent string to use.
125
+ #
126
+ # @option options [Hash] :proxy (Shodan.proxy)
127
+ # The proxy information to use.
128
+ #
129
+ # @option :proxy [String] :host
130
+ # The host of the proxy.
131
+ #
132
+ # @option :proxy [Integer] :port
133
+ # The port of the proxy.
134
+ #
135
+ # @option :proxy [String] :user
136
+ # The user to authenticate as.
137
+ #
138
+ # @option :proxy [String] :password
139
+ # The password to authenticate with.
140
+ #
141
+ # @example Creating a new Mechanize agent.
142
+ # Shodan.web_agent
143
+ #
144
+ # @example Creating a new Mechanize agent with a User-Agent alias.
145
+ # Shodan.web_agent(:user_agent_alias => 'Linux Mozilla')
146
+ #
147
+ # @example Creating a new Mechanize agent with a User-Agent string.
148
+ # Shodan.web_agent(:user_agent => 'Google Bot')
149
+ #
150
+ def Shodan.web_agent(options={},&block)
151
+ agent = WWW::Mechanize.new
152
+
153
+ if options[:user_agent_alias]
154
+ agent.user_agent_alias = options[:user_agent_alias]
155
+ elsif options[:user_agent]
156
+ agent.user_agent = options[:user_agent]
157
+ elsif Shodan.user_agent
158
+ agent.user_agent = Shodan.user_agent
159
+ end
160
+
161
+ proxy = (options[:proxy] || Shodan.proxy)
162
+ if proxy[:host]
163
+ agent.set_proxy(proxy[:host],proxy[:port],proxy[:user],proxy[:password])
164
+ end
165
+
166
+ block.call(agent) if block
167
+ return agent
168
+ end
169
+
170
+ #
171
+ # Creates a new Query object.
172
+ #
173
+ # @param [Hash] options
174
+ # Additional options.
175
+ #
176
+ # @option options [String] :query
177
+ # The query expression.
178
+ #
179
+ # @option options [Array<String>] :countries
180
+ # The Country Codes to search within.
181
+ #
182
+ # @option options [String] :country
183
+ # A Country Code to search within.
184
+ #
185
+ # @option options [Array<String>] :hostnames
186
+ # The host names to search for.
187
+ #
188
+ # @option options [String] :hostname
189
+ # A host name to search for.
190
+ #
191
+ # @option options [Array<String>] :networks
192
+ # The CIDR network blocks to search within.
193
+ #
194
+ # @option options [String] :network
195
+ # A CIDR network blocks to search within.
196
+ #
197
+ # @option options [Array<Integer>] :ports
198
+ # The ports to search for.
199
+ #
200
+ # @option options [Integer] :port
201
+ # A port to search for.
202
+ #
203
+ # @yield [query]
204
+ # If a block is given, it will be passed the newly created Query
205
+ # object.
206
+ #
207
+ # @yieldparam [Query] query
208
+ # The newly created Query object.
209
+ #
210
+ # @return [Query]
211
+ # The new Query object.
212
+ #
213
+ def Shodan.query(options={},&block)
214
+ Query.new(options,&block)
215
+ end
216
+ end
@@ -0,0 +1,24 @@
1
+ #
2
+ # shodan-ruby - A Ruby interface to SHODAN, a computer search engine.
3
+ #
4
+ # Copyright (c) 2009 Hal Brodigan (postmodern.mod3 at gmail.com)
5
+ #
6
+ # This program is free software; you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License as published by
8
+ # the Free Software Foundation; either version 2 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU General Public License
17
+ # along with this program; if not, write to the Free Software
18
+ # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
19
+ #
20
+
21
+ module Shodan
22
+ # shodanrb version
23
+ VERSION = '0.1.0'
24
+ end
data/lib/shodan.rb ADDED
@@ -0,0 +1,23 @@
1
+ #
2
+ # shodan-ruby - A Ruby interface to SHODAN, a computer search engine.
3
+ #
4
+ # Copyright (c) 2009 Hal Brodigan (postmodern.mod3 at gmail.com)
5
+ #
6
+ # This program is free software; you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License as published by
8
+ # the Free Software Foundation; either version 2 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU General Public License
17
+ # along with this program; if not, write to the Free Software
18
+ # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
19
+ #
20
+
21
+ require 'shodan/query'
22
+ require 'shodan/shodan'
23
+ require 'shodan/version'
@@ -0,0 +1,17 @@
1
+ require 'spec_helper'
2
+
3
+ shared_examples_for "has Pages" do
4
+ it "should have a first page" do
5
+ @query.first_page.should_not be_nil
6
+ end
7
+
8
+ it "should allow indexed access" do
9
+ @query[1].should_not be_nil
10
+ end
11
+
12
+ it "should allow accessing multiple pages" do
13
+ pages = @query.pages(1..2)
14
+ pages.should_not be_nil
15
+ pages.length.should == 2
16
+ end
17
+ end
data/spec/host_spec.rb ADDED
@@ -0,0 +1,115 @@
1
+ require 'shodan/host'
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Host do
6
+ describe "non-HTTP host" do
7
+ before(:all) do
8
+ @host = Host.new('127.0.0.1','16.11.2009','SSH-2.0-OpenSSH_4.2')
9
+ end
10
+
11
+ it "should parse the date" do
12
+ @host.date.class.should == Date
13
+ end
14
+
15
+ it "should not have a HTTP version" do
16
+ @host.http_version.should be_nil
17
+ end
18
+
19
+ it "should not have a HTTP status code" do
20
+ @host.http_code.should be_nil
21
+ end
22
+
23
+ it "should not have a HTTP status message" do
24
+ @host.http_status.should be_nil
25
+ end
26
+
27
+ it "should not have HTTP response headers" do
28
+ @host.http_headers.should == {}
29
+ end
30
+
31
+ it "should not have a HTTP response body" do
32
+ @host.http_body.should be_nil
33
+ end
34
+ end
35
+
36
+ describe "HTTP host" do
37
+ before(:all) do
38
+ @host = Host.new(
39
+ '127.0.0.1',
40
+ '16.11.2009',
41
+ %{
42
+ HTTP/1.0 200 OK
43
+ X-powered-by: PHP/5.3.0
44
+ Transfer-encoding: chunked
45
+ X-gpg-key: http://chrislea.com/gpgkey.txt http://chrislea.com/gpgkey-virb.txt
46
+ Vary: Cookie
47
+ X-ssh-key: http://chrislea.com/sshkey.txt
48
+ Server: nginx/0.8.24
49
+ Connection: keep-alive
50
+ Date: Fri, 20 Nov 2009 00:52:24 GMT
51
+ X-the-question: Quis custodiet ipsos custodes?
52
+ Content-type: text/html; charset=UTF-8
53
+ X-pingback: http://chrislea.com/xmlrpc.php
54
+
55
+ <HTML>
56
+ </HTML>
57
+ }.strip
58
+ )
59
+ end
60
+
61
+ it "should parse the date" do
62
+ @host.date.class.should == Date
63
+ end
64
+
65
+ it "should not have a HTTP version" do
66
+ @host.http_version.should == '1.0'
67
+ end
68
+
69
+ it "should not have a HTTP status code" do
70
+ @host.http_code.should == 200
71
+ end
72
+
73
+ it "should not have a HTTP status message" do
74
+ @host.http_status.should == 'OK'
75
+ end
76
+
77
+ it "should not have HTTP response headers" do
78
+ @host.http_headers.should == {
79
+ 'X-powered-by' => 'PHP/5.3.0',
80
+ 'Transfer-encoding' => 'chunked',
81
+ 'X-gpg-key' => 'http://chrislea.com/gpgkey.txt http://chrislea.com/gpgkey-virb.txt',
82
+ 'Vary' => 'Cookie',
83
+ 'X-ssh-key' => 'http://chrislea.com/sshkey.txt',
84
+ 'Server' => 'nginx/0.8.24',
85
+ 'Connection' => 'keep-alive',
86
+ 'Date' => 'Fri, 20 Nov 2009 00:52:24 GMT',
87
+ 'X-the-question' => 'Quis custodiet ipsos custodes?',
88
+ 'Content-type' => 'text/html; charset=UTF-8',
89
+ 'X-pingback' => 'http://chrislea.com/xmlrpc.php'
90
+ }
91
+ end
92
+
93
+ it "should provide a server name" do
94
+ @host.server_name.should == 'nginx'
95
+ end
96
+
97
+ it "should provide a server version" do
98
+ @host.server_version.should == '0.8.24'
99
+ end
100
+
101
+ it "should provide transparent access to the HTTP response headers" do
102
+ @host.x_the_question.should == 'Quis custodiet ipsos custodes?'
103
+ end
104
+
105
+ it "should raise a NoMethodError when accessing missing headers" do
106
+ lambda {
107
+ @host.lol
108
+ }.should raise_error(NoMethodError)
109
+ end
110
+
111
+ it "should not have a HTTP response body" do
112
+ @host.http_body.should == "<HTML>\n</HTML>"
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,31 @@
1
+ require 'shodan/page'
2
+
3
+ require 'spec_helper'
4
+
5
+ shared_examples_for "Page has Hosts" do
6
+ it "should have hosts" do
7
+ @page.length.should_not == 0
8
+ end
9
+
10
+ it "should have the maximum amount of hosts per page" do
11
+ @page.length.should == @query.results_per_page
12
+ end
13
+
14
+ it "should have IP addresses" do
15
+ @page.ips.should_not be_empty
16
+ end
17
+
18
+ it "should have valid IP addresses" do
19
+ @page.each_ip do |ip|
20
+ ip =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/
21
+ end
22
+ end
23
+
24
+ it "should have dates" do
25
+ @page.dates.should_not be_empty
26
+ end
27
+
28
+ it "should have responses" do
29
+ @page.responses.should_not be_empty
30
+ end
31
+ end