event-shipper 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/HISTORY ADDED
@@ -0,0 +1,4 @@
1
+
2
+ = 0.1
3
+
4
+ * First version.
data/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+
2
+ Copyright (c) 2013 Kaspar Schiess
3
+
4
+ Permission is hereby granted, free of charge, to any person
5
+ obtaining a copy of this software and associated documentation
6
+ files (the "Software"), to deal in the Software without
7
+ restriction, including without limitation the rights to use,
8
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the
10
+ Software is furnished to do so, subject to the following
11
+ conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
18
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
20
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
21
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
22
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
23
+ OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,57 @@
1
+
2
+ event_shipper reads log files and sends each line in logstash JSON format
3
+ via encrypted UDP to redis.
4
+
5
+ LINKS
6
+
7
+ logstash is awesome and here:
8
+ http://logstash.net/
9
+
10
+ this uses a CA and public/private key architecture:
11
+ https://github.com/jordansissel/lumberjack
12
+
13
+ and finally, here's woodchuck, which inspired this work:
14
+ https://github.com/danryan/woodchuck
15
+
16
+ SYNOPSIS
17
+
18
+ To run a local example installation, start redis. Then:
19
+
20
+ # Produce one log entry per second in the file 'test.log'
21
+ $ ./bin/producer 1
22
+
23
+ # Ship the log entries to localhost
24
+ $ ./bin/esshipper -c configs/test.yaml
25
+
26
+ # Receive entries and store in redis
27
+ $ ./bin/esproxy -c configs/proxy.yaml
28
+
29
+ PERFORMANCE
30
+
31
+ Good.
32
+
33
+ ENCRYPTION
34
+
35
+ Packets are encrypted using a simple AES256 symmetric encryption with a
36
+ preshared user/password tuple on the server and the client. This should be
37
+ good to keep people from reading your logs,
38
+
39
+ If someone with deep cryptographic knowledge wants to criticise my use of
40
+ algorithms, please, you're welcome! Just keep in mind that I've selected the
41
+ current method to be simple to set up.
42
+
43
+ CONTRIBUTE
44
+
45
+ This gem is hosted on bitbucket at
46
+ https://bitbucket.org/kschiess/event_shipper
47
+
48
+ Please file any issue you might have with the program there. Contributions
49
+ are welcome - you might shoot me an email before starting, just to check if
50
+ the direction you take is welcome in the code base.
51
+
52
+ LICENSE
53
+
54
+ This piece of software is under a MIT license, which means you can do pretty
55
+ much anything you want with it. Read the license, it's in the LICENSE file.
56
+
57
+ (c) 2013 Kaspar Schiess, Technology Astronauts
data/bin/esproxy ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Tails one or more log files and ships logs as events over the network
4
+ # via a custom encrypted protocol. Transport is UDP.
5
+
6
+ require 'bundler/setup'
7
+
8
+ $:.unshift File.dirname(__FILE__) + "/../lib"
9
+ require 'event_shipper/proxy'
10
+
11
+ EventShipper::Proxy.run
data/bin/esshipper ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Tails one or more log files and ships logs as events over the network
4
+ # via a custom encrypted protocol. Transport is UDP.
5
+
6
+ require 'bundler/setup'
7
+
8
+ $:.unshift File.dirname(__FILE__) + "/../lib"
9
+ require 'event_shipper/shipper'
10
+
11
+ EventShipper::Shipper.run
data/bin/producer ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # A trivial test script that produces a file called test.log and appends N
4
+ # messages per second to the file.
5
+
6
+ n = Integer(ARGV.first || 1)
7
+
8
+ sleep_seconds = (1 / Float(n))
9
+
10
+ File.open('test.log', 'a+') do |file|
11
+ file.sync = true
12
+
13
+ loop do
14
+ file.puts "A message (#{Time.now})"
15
+ sleep sleep_seconds
16
+ end
17
+ end
@@ -0,0 +1,12 @@
1
+
2
+ encrypt: true
3
+ users:
4
+ - test/MyPassword
5
+
6
+ listen:
7
+ host: 127.0.0.1
8
+ port: 5050
9
+
10
+ redis:
11
+ host: localhost
12
+ port: 6379
data/configs/test.yaml ADDED
@@ -0,0 +1,10 @@
1
+
2
+ encrypt: true
3
+ user: test
4
+ password: MyPassword
5
+
6
+ target: localhost:5050
7
+
8
+ logs:
9
+ - ./test.log
10
+
File without changes
@@ -0,0 +1,41 @@
1
+
2
+ require 'yajl'
3
+ require 'addressable/uri'
4
+
5
+ module EventShipper
6
+ class Event
7
+ def initialize host, path, line
8
+ @path = path
9
+ @line = line
10
+ @host = host
11
+ @timestamp = Time.now.utc.iso8601(6)
12
+ @source = Addressable::URI.new(
13
+ :scheme => 'file',
14
+ :host => host, :path => path)
15
+ @message = line.chomp.strip
16
+ @fields = {}
17
+ @tags = []
18
+ end
19
+
20
+ def to_hash
21
+ {
22
+ '@source' => @source.to_s,
23
+ '@type' => @source.scheme,
24
+ '@tags' => @tags,
25
+ '@fields' => @fields,
26
+ '@timestamp' => @timestamp,
27
+ '@source_host' => @host,
28
+ '@source_path' => @path,
29
+ '@message' => @message
30
+ }
31
+ end
32
+
33
+ def to_json
34
+ Yajl::Encoder.encode(to_hash)
35
+ end
36
+
37
+ def to_s
38
+ to_json
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,40 @@
1
+
2
+ require 'time'
3
+ require 'socket'
4
+ require 'file-tail'
5
+
6
+ require 'event_shipper/event'
7
+
8
+ module EventShipper
9
+ class LogTailer
10
+ def initialize path, transport
11
+ @path = path
12
+ @transport = transport
13
+ @host = Socket.gethostname
14
+ end
15
+
16
+ def start
17
+ @thread = Thread.new(&method(:thread_main))
18
+ @thread.abort_on_exception = true
19
+ end
20
+
21
+ def thread_main
22
+ File.open(@path) do |log|
23
+ log.seek 0, IO::SEEK_END
24
+ log.extend File::Tail
25
+
26
+ log.tail do |line|
27
+ issue line
28
+ end
29
+ end
30
+ end
31
+ def issue line
32
+ event = Event.new @host, @path, line
33
+ @transport.send "queue/#{event.to_json}"
34
+ end
35
+
36
+ def join
37
+ @thread.join
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,42 @@
1
+
2
+ require 'redis'
3
+ require 'yaml'
4
+ require 'clamp'
5
+
6
+ module EventShipper
7
+
8
+ require_relative 'transport/encrypt'
9
+ require_relative 'transport/udp'
10
+
11
+ class Proxy < Clamp::Command
12
+
13
+ option %w(-c --config), "FILE", "Location of the configuration file."
14
+
15
+ def execute
16
+ fail "Please specify a configuration file to use on the command line. (--config)" \
17
+ unless config
18
+
19
+ configuration = YAML.load_file(config)
20
+
21
+ listen = configuration['listen']
22
+ host, port = listen['host'], listen['port']
23
+ transport = Transport::UDP.new(host, port)
24
+
25
+ if configuration['encrypt']
26
+ users = configuration['users']
27
+ userdb = Hash[users.map { |str| str.split('/') }]
28
+
29
+ transport.filter = Transport::Decrypt.new(userdb)
30
+ end
31
+
32
+ redis_config = configuration['redis']
33
+ redis = Redis.new(
34
+ host: redis_config['host'],
35
+ port: redis_config['port'])
36
+
37
+ transport.dispatch { |queue, message|
38
+ # puts message
39
+ redis.lpush queue, message }
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,41 @@
1
+
2
+ require 'yaml'
3
+ require 'clamp'
4
+
5
+ module EventShipper
6
+
7
+ require 'event_shipper/transport/encrypt'
8
+ require 'event_shipper/transport/udp'
9
+ require 'event_shipper/log_tailer'
10
+
11
+ class Shipper < Clamp::Command
12
+ option %w(-c --config), "FILE", "Location of the configuration file."
13
+
14
+ def execute
15
+ fail "Please specify a configuration file to use on the command line. (--config)" \
16
+ unless config
17
+
18
+ configuration = YAML.load_file(config)
19
+
20
+ host, port = configuration['target'].split(':')
21
+ transport = Transport::UDP.new(host, port)
22
+
23
+ if configuration['encrypt']
24
+ user, password = configuration.values_at('user', 'password')
25
+ transport.filter = Transport::Encrypt.new(user, password)
26
+ end
27
+
28
+ tailers = configuration["logs"].map do |path|
29
+ tailer = LogTailer.new(path, transport)
30
+ tailer.start
31
+
32
+ tailer
33
+ end
34
+
35
+ # Block until everyone exits.
36
+ tailers.each do |tailer|
37
+ tailer.join
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,101 @@
1
+
2
+ require 'openssl'
3
+
4
+ module EventShipper; end
5
+ module EventShipper::Transport
6
+ class Encryption
7
+ def initialize password
8
+ @password = password
9
+ @salt = generate_salt
10
+
11
+ @key = generate_key(password, @salt)
12
+ end
13
+
14
+ def generate_salt
15
+ OpenSSL::Random.random_bytes(8)
16
+ end
17
+
18
+ def generate_key password, salt
19
+ if [password, salt] == @last_key_seed
20
+ return @last_key
21
+ end
22
+
23
+ iterations = 10000
24
+ key_length = 32
25
+
26
+ @last_key_seed = [password, salt]
27
+ @last_key = OpenSSL::PKCS5.pbkdf2_hmac_sha1(
28
+ password, salt, iterations, key_length)
29
+ end
30
+
31
+ def enc str
32
+ cipher = OpenSSL::Cipher::AES256.new(:CBC)
33
+ cipher.encrypt
34
+ # cipher.random_iv
35
+ cipher.key = @key
36
+
37
+ ciphertext = cipher.update(str) + cipher.final
38
+
39
+ "Salted__#{@salt}#{ciphertext}" #OpenSSL compatible
40
+ end
41
+ def dec str
42
+ salt = str[8..15]
43
+ str = str[16..-1]
44
+
45
+ cipher = OpenSSL::Cipher::AES256.new(:CBC)
46
+ cipher.decrypt
47
+ # cipher.random_iv
48
+ cipher.key = generate_key(@password, salt)
49
+
50
+ cipher.update(str) + cipher.final
51
+ end
52
+ end
53
+
54
+ class Encrypt
55
+ def initialize user, password
56
+ @user = user
57
+ @encryption = Encryption.new(password)
58
+ end
59
+
60
+ def call string
61
+ ciphertext = @encryption.enc string
62
+ "#{@user}/#{ciphertext}"
63
+ end
64
+ end
65
+
66
+ class Decrypt
67
+ def initialize userdb
68
+ @userdb = userdb.inject({}) do |hash, (key, value)|
69
+ hash[key] = Encryption.new(value)
70
+ hash
71
+ end
72
+ end
73
+
74
+ def call str
75
+ user, _, ciphertext = str.partition('/')
76
+
77
+ if encryption = @userdb[user]
78
+ encryption.dec(ciphertext)
79
+ end
80
+ end
81
+ end
82
+ end
83
+
84
+ if $0 == __FILE__
85
+ enc = EventShipper::Transport::Encryption.new 'password'
86
+ str = enc.enc 'A very secret text'
87
+ puts str
88
+
89
+ dec = EventShipper::Transport::Encryption.new 'password'
90
+ puts dec.dec(str)
91
+
92
+ require 'benchmark'
93
+ puts "Doing it a 1000 times"
94
+ msg = "foobar"*20
95
+ puts Benchmark.measure {
96
+ e = EventShipper::Transport::Encrypt.new('test', 'password')
97
+ d = EventShipper::Transport::Decrypt.new('test' => 'password')
98
+ 1000.times do d.call(e.call(msg)) end
99
+ }
100
+
101
+ end
@@ -0,0 +1,54 @@
1
+ require 'socket'
2
+
3
+ module EventShipper::Transport
4
+ class UDP
5
+ def initialize host, port
6
+ @host, @port = host, port
7
+ @socket = UDPSocket.new
8
+ @filter = nil
9
+
10
+ @messages_per_period = 0
11
+ @period_start = Time.now
12
+ Thread.start do
13
+ loop do
14
+ puts "total #{@messages_per_period / (Time.now - @period_start)} msgs/s"
15
+
16
+ @messages_per_period = 0
17
+ @period_start = Time.now
18
+
19
+ sleep 10
20
+ end
21
+ end
22
+ end
23
+
24
+ attr_writer :filter
25
+
26
+ def filter string
27
+ if @filter
28
+ @filter.call(string)
29
+ else
30
+ string
31
+ end
32
+ end
33
+
34
+ def send string
35
+ @messages_per_period += 1
36
+ @socket.send filter(string),
37
+ 0, # flags...
38
+ @host, @port
39
+ end
40
+
41
+ # Enters a loop, receiving messages, yielding them to the block.
42
+ #
43
+ def dispatch
44
+ @socket.bind @host, @port
45
+ loop do
46
+ datagram, source_info = @socket.recvfrom(10 * 1024)
47
+ @messages_per_period += 1
48
+
49
+ queue, _, message = filter(datagram).partition('/')
50
+ yield queue, message
51
+ end
52
+ end
53
+ end
54
+ end
metadata ADDED
@@ -0,0 +1,146 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: event-shipper
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Kaspar Schiess
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-05-03 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: redis
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: clamp
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: file-tail
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: addressable
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :runtime
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: yajl-ruby
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :runtime
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ description: ! "\n event_shipper reads log files and sends each line in logstash
95
+ JSON format \n via encrypted UDP to redis.\n "
96
+ email: kaspar.schiess@absurd.li
97
+ executables:
98
+ - esproxy
99
+ - esshipper
100
+ extensions: []
101
+ extra_rdoc_files:
102
+ - README
103
+ files:
104
+ - HISTORY
105
+ - LICENSE
106
+ - README
107
+ - lib/event_shipper/event.rb
108
+ - lib/event_shipper/log_tailer.rb
109
+ - lib/event_shipper/proxy.rb
110
+ - lib/event_shipper/shipper.rb
111
+ - lib/event_shipper/transport/encrypt.rb
112
+ - lib/event_shipper/transport/udp.rb
113
+ - lib/event_shipper.rb
114
+ - bin/esproxy
115
+ - bin/esshipper
116
+ - bin/producer
117
+ - configs/proxy.yaml
118
+ - configs/test.yaml
119
+ homepage: https://bitbucket.org/kschiess/event_shipper
120
+ licenses: []
121
+ post_install_message:
122
+ rdoc_options:
123
+ - --main
124
+ - README
125
+ require_paths:
126
+ - lib
127
+ required_ruby_version: !ruby/object:Gem::Requirement
128
+ none: false
129
+ requirements:
130
+ - - ! '>='
131
+ - !ruby/object:Gem::Version
132
+ version: '0'
133
+ required_rubygems_version: !ruby/object:Gem::Requirement
134
+ none: false
135
+ requirements:
136
+ - - ! '>='
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ requirements: []
140
+ rubyforge_project:
141
+ rubygems_version: 1.8.25
142
+ signing_key:
143
+ specification_version: 3
144
+ summary: Ships log events to logstash.
145
+ test_files: []
146
+ has_rdoc: