mongo-proxy 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 82257f694a4c0d087fd02010aa37c2a160ef2cff
4
+ data.tar.gz: c7fd83f1ba64daa944a0a55cbe462f5f30c8683f
5
+ SHA512:
6
+ metadata.gz: ac74381e8f4d526f3a24441fb6a21c7a04eedd4473be89edf2f0aedcfb6e0f1accb5a0c5f1218f7ea39fda50b5eb3263067d9478e3d7bd03144e14d40fd08d7a
7
+ data.tar.gz: 6afb35ffac434d0ab07dc444fc0930cec5a2e80814764ce618e1d59988ae4365688d6d20db1f3e4feb40232f211f5aa6966fbb12463d52ef5c6f7184b8ec0941
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --colour
2
+ --backtrace
3
+
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
3
+
4
+
data/LICENSE.md ADDED
@@ -0,0 +1,9 @@
1
+ Copyright (C) 2014 Peter Bakkum
2
+
3
+ (MIT License)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,151 @@
1
+ Mongo Proxy
2
+ ===========
3
+
4
+ A gem for proxying MongoDB at the wire-protocol level. The proxy intercepts MongoDB communication, presents it in an easy-to-manipulate format, and forwards only desired messages. This approach allows you to filter MongoDB traffic, such as writes, or even inject new messages, such as a message-of-the-day that appears on new connections.
5
+
6
+ mongo-proxy includes a command-line interface and an API for running it in a Ruby application. You can write your own hooks to manipulate MongoDB wire traffic as it arrives from the client.
7
+
8
+ Installation
9
+ ------------
10
+
11
+ `gem install mongo-proxy`
12
+
13
+ More information at the [gem page](http://rubygems.org/gems/mongo-proxy).
14
+
15
+ Command Line
16
+ ------------
17
+
18
+ You can run mongo-proxy from the command line:
19
+
20
+ `mongo-proxy [options]`
21
+
22
+ Here is an example:
23
+
24
+ `mongo-proxy --client-port 29017 --read-only --motd 'Connected to proxy!'`
25
+
26
+ This will proxy local client connections to port 29017 to a running MongoDB server at port 27017. Only read queries will be allowed and the client will be told on startup that he is connecting to a proxy server. With the proxy running, a connection looks something like:
27
+
28
+ ```
29
+ > mongo --port 29017
30
+ MongoDB shell version: 2.6.0
31
+ connecting to: 127.0.0.1:27018/test
32
+ Server has startup warnings:
33
+ Connected to proxy!
34
+ ```
35
+
36
+ #### Command Line Options
37
+
38
+ The following flags can be used when connecting:
39
+ ```
40
+ --client-host, -h <s>: Set the host to bind the proxy socket on
41
+ (default: 127.0.0.1)
42
+ --client-port, -p <i>: Set the port to bind the proxy socket on
43
+ (default: 27018)
44
+ --server-host, -H <s>: Set the backend hostname which we proxy
45
+ (default: 127.0.0.1)
46
+ --server-port, -P <i>: Set the backend port which we proxy (default:
47
+ 27017)
48
+ --read-only, -r: Prevent any traffic that writes to the
49
+ database
50
+ --motd, -m: Set a message-of-the-day to display to clients
51
+ when they connect
52
+ --verbose, --no-verbose, -v: Print out MongoDB wire traffic (default: true)
53
+ --debug, --no-debug, -d: Print log lines in a more human-readible
54
+ format (default: true)
55
+ --version, -e: Print version and exit
56
+ --help, -l: Show this message
57
+
58
+ ```
59
+
60
+ API
61
+ ---
62
+
63
+ You can also use the mongo-proxy functionality directly in Ruby. This approach allows you to add your own hooks to manipulate MongoDB traffic. Here is an example:
64
+
65
+ ```ruby
66
+ require 'mongo-proxy'
67
+
68
+ front = 0
69
+ back = 0
70
+ config = {
71
+ :client_port => 29017,
72
+ :server_port => 27017,
73
+ :read_only => true,
74
+ :debug => true
75
+ }
76
+
77
+ proxy = MongoProxy.new(config)
78
+
79
+ m.add_callback_to_front do |conn, msg|
80
+ front += 1
81
+ puts "received #{front} client messages so far"
82
+ msg
83
+ end
84
+
85
+ m.add_callback_to_back do |conn, msg|
86
+ back += 1
87
+ puts "forwarded #{back} client messages so far"
88
+ msg
89
+ end
90
+
91
+ m.start
92
+ ```
93
+
94
+ This example opens up a read-only proxy at port 29017. There are two stacks of callbacks: the 'front' callbacks are executed before applying the read-only authorization, while the 'back' callbacks are called after. Thus, the back callbacks will only receive messages that passed authorization. Calling `add_callback_to_front` adds a hook to the front of the front stack, while calling `add_callback_to_back` adds a hook to the back of the back stack. Once you call `start` the proxy will run and show both the messages it receives (because of the debug flag) and the messages from the callback.
95
+
96
+ Callbacks are executed and passed the client connection on which a message was received and the message itself as a `Hash`. The callback can make changes to this message, if desired. The callback is expected to return a message to pass along through the stack of callbacks and eventually forwarded to the backend MongoDB server. If a callback returns `nil` then the message will dropped.
97
+
98
+ mongo-proxy is a thin layer on top of [em-proxy](https://github.com/igrigorik/em-proxy), and the connection object passed to your hook is the same as the em-proxy connection object. This means that you can call the `send_data` method on it to send a raw message to the client.
99
+
100
+ #### MongoProxy Options
101
+
102
+ You can use the following options (shown with their default values), when creating a `MongoProxy` object.
103
+
104
+ ```ruby
105
+ @config = {
106
+ :client_host => '127.0.0.1', # Set the host to bind the proxy socket on.
107
+ :client_port => 29017, # Set the port to bind the proxy socket on.
108
+ :server_host => '127.0.0.1', # Set the backend host which we proxy.
109
+ :server_port => 27017, # Set the backend port which we proxy.
110
+ :motd => nil, # Set a message-of-the-day to display to clients when they connect. nil for none.
111
+ :read_only => false, # Prevent any traffic that writes to the database.
112
+ :verbose => false, # Print out MongoDB wire traffic.
113
+ :logger => nil, # Use this object as the logger instead of creating one.
114
+ :debug => false # Print log lines in a more human-readible format.
115
+ }
116
+ ```
117
+
118
+ #### Message Format
119
+
120
+ Here is an example message passed to a hook:
121
+ ```ruby
122
+ {
123
+ :flags => 0,
124
+ :database => 'test',
125
+ :collection => 'testcoll',
126
+ :numberToSkip => 0,
127
+ :numberToReturn => 4294967295,
128
+ :query => {
129
+ "foo" => "bar"
130
+ },
131
+ :returnFieldSelector => nil,
132
+ :header => {
133
+ :messageLength => 58,
134
+ :requestID => 1,
135
+ :responseTo => 0,
136
+ :opCode => :query
137
+ }
138
+ }
139
+ ```
140
+
141
+ This format comes from our [wire parsing code](lib/mongo-proxy/wire.rb), and will look similar, but differ slightly, for other operations such as inserts or deletes.
142
+
143
+ Testing
144
+ -------
145
+
146
+ Running the unit tests requires a MongoDB instance at port 27018 (nonstandard) and nothing at port 27017. The tests use rspec, so you can run with `bundle exec rspec`.
147
+
148
+ License
149
+ -------
150
+
151
+ [The MIT License](LICENSE.md) - Copyright 2014 Peter Bakkum
data/bin/mongo-proxy ADDED
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'mongo-proxy'
4
+ require 'trollop'
5
+
6
+ opts = Trollop::options do
7
+ version "mongo-proxy #{MongoProxy::VERSION} (c) Peter Bakkum"
8
+ banner <<-END
9
+ mongo-proxy runs a local proxy server for MongoDB. It allows you to watch the MongoDB wire traffic, run hooks on it, and force traffic to be read-only. This command is intended for easy access, you can also access the same functionality with the MongoProxy class.
10
+
11
+ Usage:
12
+ mongo-proxy [options]
13
+
14
+ Examples:
15
+
16
+ mongo-proxy -P 27017 -p 29017 -r -d
17
+ (runs a proxy at port 29017 to connect to a local mongo in read-only and debug mode)
18
+
19
+ More info can be found at the gem website at github.com/bakks/mongo-proxy
20
+
21
+ Command line options:
22
+
23
+ END
24
+
25
+ opt :client_host, 'Set the host to bind the proxy socket on.', :default => '127.0.0.1', :short => 'h'
26
+ opt :client_port, 'Set the port to bind the proxy socket on.', :default => 27018, :short => 'p'
27
+ opt :server_host, 'Set the backend hostname which we proxy.', :default => '127.0.0.1', :short => 'H'
28
+ opt :server_port, 'Set the backend port which we proxy.', :default => 27017, :short => 'P'
29
+ opt :read_only, 'Prevent any traffic that writes to the database.', :default => false
30
+ opt :motd, 'Set a message-of-the-day to display to clients when they connect.', :default => nil
31
+ opt :verbose, 'Print out MongoDB wire traffic.', :default => true
32
+ opt :debug, 'Print log lines in a more human-readible format.', :default => true
33
+ end
34
+
35
+ keys = [:client_host, :client_port, :server_host, :server_port, :read_only, :motd, :verbose, :debug]
36
+
37
+ args = {}
38
+ keys.each { |key| args[key] = opts[key] }
39
+
40
+ m = MongoProxy.new(args)
41
+ m.start
42
+
@@ -0,0 +1,3 @@
1
+ require_relative 'mongo-proxy/wire'
2
+ require_relative 'mongo-proxy/proxy'
3
+ require_relative 'mongo-proxy/auth'
@@ -0,0 +1,151 @@
1
+ require 'securerandom'
2
+
3
+ class AuthMongo
4
+ @@admin_cmd_whitelist = [
5
+ { 'ismaster' => 1 },
6
+ { 'isMaster' => 1 },
7
+ { 'listDatabases' => 1},
8
+ {
9
+ 'replSetGetStatus' => 1,
10
+ 'forShell' => 1
11
+ }
12
+ ]
13
+
14
+ def initialize(config = nil)
15
+ @config = config
16
+ @log = @config[:logger]
17
+ @request_id = 20
18
+ @last_error = {}
19
+ end
20
+
21
+ def wire_auth(conn, msg)
22
+ return nil unless msg
23
+
24
+ authed, response = auth(conn, msg)
25
+
26
+ if !authed
27
+ @last_error[conn] = true
28
+ else
29
+ @last_error[conn] = false
30
+ end
31
+
32
+ if response
33
+ return WireMongo::build_reply(response, get_request_id, msg[:header][:requestID])
34
+ else
35
+ return authed
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def reply_motd
42
+ motd = @config[:motd]
43
+ motd = motd.split("\n")
44
+ return true, {
45
+ 'totalLinesWritten' => motd.size,
46
+ 'log' => motd,
47
+ 'ok' => 1.0
48
+ }
49
+ end
50
+
51
+ def reply_unauth(db, coll)
52
+ @log.info "replying unauthed for collection #{db}.#{coll}"
53
+ return false, {
54
+ 'ok' => 0,
55
+ 'n' => 0,
56
+ 'code' => 2,
57
+ 'errmsg' => 'Writes and Javascript execution are disallowed in this interface.',
58
+ 'writeErrors' => {
59
+ 'index' => 0,
60
+ 'code' => 2,
61
+ 'errmsg' => 'Writes and Javascript execution are disallowed in this interface.'
62
+ }
63
+ }
64
+ end
65
+
66
+ def reply_error(err)
67
+ return false, {
68
+ 'errmsg' => err,
69
+ 'ok' => 0.0
70
+ }
71
+ end
72
+
73
+ def reply_ok
74
+ return true, {
75
+ 'ok' => 1.0
76
+ }
77
+ end
78
+
79
+ def get_request_id
80
+ x = @request_id
81
+ @request_id += 1
82
+ return x
83
+ end
84
+
85
+
86
+ def auth(conn, msg)
87
+ op = msg[:header][:opCode]
88
+
89
+ if op == WireMongo::OP_KILL_CURSORS
90
+ return true, nil
91
+ end
92
+
93
+ db = msg[:database]
94
+ coll = msg[:collection]
95
+ query = (msg[:query] or {})
96
+
97
+ case op
98
+ when WireMongo::OP_QUERY, WireMongo::OP_GET_MORE
99
+ return reply_unauth(db, coll) unless db and coll
100
+ return reply_unauth(db, coll) if query['$where'] != nil
101
+
102
+ # handle authentication process
103
+ if coll == '$cmd'
104
+
105
+ if query['count']
106
+ # fields key can be nil but must exist
107
+ unless query.size == 3 and query['query'] and query.has_key? 'fields'
108
+ return reply_unauth(db, coll)
109
+ end
110
+ return true, nil
111
+
112
+ elsif query['getlasterror'] == 1
113
+ if @last_error[conn]
114
+ return reply_unauth(db, coll)
115
+ @last_error[conn] = false
116
+ else
117
+ return true, nil
118
+ end
119
+
120
+ # allow ismaster query, listDatabases query
121
+ elsif db == 'admin'
122
+ if @@admin_cmd_whitelist.include?(query)
123
+ return true, nil
124
+ elsif query['getLog'] == 'startupWarnings'
125
+ if @config[:motd]
126
+ return reply_motd
127
+ else
128
+ return true
129
+ end
130
+ end
131
+
132
+ end # if db == 'admin'
133
+ return reply_unauth(db, coll)
134
+ end # if coll == '$cmd'
135
+
136
+ return true, nil if coll == 'system.namespaces' # list collections
137
+ return reply_unauth(db, coll) if coll[0] == '$' #other command
138
+
139
+ return true, nil
140
+
141
+ when WireMongo::OP_UPDATE, WireMongo::OP_INSERT, WireMongo::OP_DELETE
142
+ #return reply_unauth(db, coll)
143
+ return false, nil
144
+
145
+ else
146
+ return reply_unauth(db, coll)
147
+
148
+ end
149
+ end
150
+ end
151
+
@@ -0,0 +1,172 @@
1
+ require 'em-proxy'
2
+ require 'logger'
3
+ require 'json'
4
+ require 'pp'
5
+ require 'socket'
6
+ require 'timeout'
7
+
8
+
9
+ # This class uses em-proxy to help listen to MongoDB traffic, with some
10
+ # parsing and filtering capabilities that allow you to enforce a read-only
11
+ # mode, or use your own arbitrary logic.
12
+ class MongoProxy
13
+ VERSION = '1.0.0'
14
+
15
+ def initialize(config = nil)
16
+ # default config values
17
+ @config = {
18
+ :client_host => '127.0.0.1', # Set the host to bind the proxy socket on.
19
+ :client_port => 29017, # Set the port to bind the proxy socket on.
20
+ :server_host => '127.0.0.1', # Set the backend host which we proxy.
21
+ :server_port => 27017, # Set the backend port which we proxy.
22
+ :motd => nil, # Set a message-of-the-day to display to clients when they connect. nil for none.
23
+ :read_only => false, # Prevent any traffic that writes to the database.
24
+ :verbose => false, # Print out MongoDB wire traffic.
25
+ :logger => nil, # Use this object as the logger instead of creating one.
26
+ :debug => false # Print log lines in a more human-readible format.
27
+ }
28
+ @front_callbacks = []
29
+ @back_callbacks = []
30
+
31
+ # apply argument config to the default values
32
+ (config || []).each do |k, v|
33
+ if @config.include?(k)
34
+ @config[k] = v
35
+ else
36
+ raise "Unrecognized configuration value: #{k}"
37
+ end
38
+ end
39
+
40
+ # debug implies verbose
41
+ @config[:verbose] = true if @config[:debug]
42
+
43
+ # Set up the logger for mongo proxy. Users can also pass their own
44
+ # logger in with the :logger config value.
45
+ unless @config[:logger]
46
+ @config[:logger] = Logger.new(STDOUT)
47
+ @config[:logger].level = (@config[:verbose] ? Logger::DEBUG : Logger::WARN)
48
+
49
+ if @config[:debug]
50
+ @config[:logger].formatter = proc do |_, _, _, msg|
51
+ if msg.is_a?(Hash)
52
+ "#{JSON::pretty_generate(msg)}\n\n"
53
+ else
54
+ "#{msg}\n\n"
55
+ end
56
+ end
57
+ end
58
+ end
59
+
60
+ unless port_open?(@config[:server_host], @config[:server_port])
61
+ raise "Could not connect to MongoDB server at #{@config[:server_host]}.#{@config[:server_port]}"
62
+ end
63
+
64
+ @log = @config[:logger]
65
+ @auth = AuthMongo.new(@config)
66
+ end
67
+
68
+ def start
69
+ # em proxy launches a thread, this is the error handler for it
70
+ EM.error_handler do |e|
71
+ @log.error [e.inspect, e.backtrace.first]
72
+ raise e
73
+ end
74
+
75
+ # kick off em-proxy
76
+ Proxy.start({
77
+ :host => @config[:client_host],
78
+ :port => @config[:client_port],
79
+ :debug => false
80
+ },
81
+ &method(:callbacks))
82
+ end
83
+
84
+ def add_callback_to_front(&block)
85
+ @front_callbacks.insert(0, block)
86
+ end
87
+
88
+ def add_callback_to_back(&block)
89
+ @back_callbacks << block
90
+ end
91
+
92
+ private
93
+
94
+ def callbacks(conn)
95
+ conn.server(:srv, {
96
+ :host => @config[:server_host],
97
+ :port => @config[:server_port]})
98
+
99
+ conn.on_data do |data|
100
+ # parse the raw binary message
101
+ raw_msg, msg = WireMongo.receive(data)
102
+
103
+ @log.info 'from client'
104
+ @log.info msg
105
+
106
+ if raw_msg == nil
107
+ @log.info "Client disconnected"
108
+ return
109
+ end
110
+
111
+ @front_callbacks.each do |cb|
112
+ msg = cb.call(conn, msg)
113
+ break unless msg
114
+ end
115
+ next unless msg
116
+
117
+ # get auth response about client query
118
+ authed = (@config[:read_only] == true ? @auth.wire_auth(conn, msg) : true)
119
+ r = nil
120
+
121
+ if authed == true # auth succeeded
122
+ @back_callbacks.each do |cb|
123
+ msg = cb.call(conn, msg)
124
+ break unless msg
125
+ end
126
+ next unless msg
127
+
128
+ r = WireMongo::write(msg)
129
+
130
+ elsif authed.is_a?(Hash) # auth had a direct response
131
+ response = WireMongo::write(authed)
132
+ conn.send_data(response)
133
+
134
+ else # otherwise drop the message
135
+ @log.info 'dropping message'
136
+
137
+ end
138
+
139
+ r
140
+ end
141
+
142
+ # messages back from the server
143
+ conn.on_response do |backend, resp|
144
+ if @config[:verbose]
145
+ _, msg = WireMongo::receive(resp)
146
+ @log.info 'from server'
147
+ @log.info msg
148
+ end
149
+
150
+ resp
151
+ end
152
+
153
+ conn.on_finish do |backend, name|
154
+ @log.info "closing client connection #{name}"
155
+ end
156
+ end
157
+
158
+ # http://stackoverflow.com/questions/517219/ruby-see-if-a-port-is-open
159
+ def port_open?(ip, port, seconds=1)
160
+ Timeout::timeout(seconds) do
161
+ begin
162
+ TCPSocket.new(ip, port).close
163
+ true
164
+ rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
165
+ false
166
+ end
167
+ end
168
+ rescue Timeout::Error
169
+ false
170
+ end
171
+ end
172
+