boat 0.2
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.
- data/.gitignore +7 -0
- data/Gemfile +4 -0
- data/README +0 -0
- data/Rakefile +2 -0
- data/bin/boat +55 -0
- data/boat.gemspec +20 -0
- data/lib/boat.rb +11 -0
- data/lib/boat/client.rb +126 -0
- data/lib/boat/put.rb +46 -0
- data/lib/boat/server.rb +291 -0
- data/lib/boat/version.rb +3 -0
- data/server.conf +11 -0
- metadata +80 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README
ADDED
File without changes
|
data/Rakefile
ADDED
data/bin/boat
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'boat'
|
5
|
+
|
6
|
+
case command = ARGV.shift
|
7
|
+
when 'server'
|
8
|
+
Boat::Server.new.run
|
9
|
+
|
10
|
+
when 'put'
|
11
|
+
Boat::Put.new.run
|
12
|
+
|
13
|
+
when 'configure', 'config'
|
14
|
+
config_file = ARGV.first || Boat::DEFAULT_CLIENT_CONFIGURATION_FILE
|
15
|
+
|
16
|
+
configuration = File.exists?(config_file) ? YAML.load(IO.read(config_file)) : {}
|
17
|
+
puts "Configuring #{config_file}\n\n"
|
18
|
+
|
19
|
+
[["Username", "username"], ["Key", "key"], ["Hostname", "host"]].each do |title, key|
|
20
|
+
print "#{title} [#{configuration[key]}] "
|
21
|
+
input = STDIN.gets.strip
|
22
|
+
configuration[key] = input unless input.empty?
|
23
|
+
end
|
24
|
+
|
25
|
+
File.open(config_file, "w") {|file| file.write configuration.to_yaml}
|
26
|
+
|
27
|
+
|
28
|
+
else
|
29
|
+
puts "Unknown command #{command}\n\n" if command && !command.empty? && command != 'help'
|
30
|
+
|
31
|
+
puts <<-EOT
|
32
|
+
Boat #{Boat::VERSION}
|
33
|
+
Copyright 2011 Roger Nesbitt
|
34
|
+
|
35
|
+
Boat is a file transfer server and client, made for backing up files.
|
36
|
+
|
37
|
+
Server usage:
|
38
|
+
|
39
|
+
boat server [-c config_file]
|
40
|
+
Starts the boat server.
|
41
|
+
Uses /etc/boat.conf if config_file is not specified.
|
42
|
+
|
43
|
+
Client usage:
|
44
|
+
|
45
|
+
boat configure [config_file]
|
46
|
+
Configures the username, key and hostname to use. If no config
|
47
|
+
file is specified, ~/.boat.yml is used by default.
|
48
|
+
|
49
|
+
boat put [-v] [-c config_file] source_filename [destination_filename]
|
50
|
+
Uploads source_filename to the remote server.
|
51
|
+
source_filename may be '-' to upload from stdin, but in this case
|
52
|
+
a destination_filename must be specified.
|
53
|
+
|
54
|
+
EOT
|
55
|
+
end
|
data/boat.gemspec
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path("../lib/boat/version", __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = "boat"
|
6
|
+
s.version = Boat::VERSION
|
7
|
+
s.platform = Gem::Platform::RUBY
|
8
|
+
s.authors = ["Roger Nesbitt"]
|
9
|
+
s.email = []
|
10
|
+
s.homepage = "http://rubygems.org/gems/boat"
|
11
|
+
s.summary = "File upload client and server specifically aimed at transferring already-encrypted backups"
|
12
|
+
s.description = s.summary
|
13
|
+
|
14
|
+
s.required_rubygems_version = ">= 1.3.6"
|
15
|
+
s.rubyforge_project = "boat"
|
16
|
+
|
17
|
+
s.files = `git ls-files`.split("\n")
|
18
|
+
s.executables = `git ls-files`.split("\n").map{|f| f =~ /^bin\/(.*)/ ? $1 : nil}.compact
|
19
|
+
s.require_path = 'lib'
|
20
|
+
end
|
data/lib/boat.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
module Boat
|
2
|
+
DEFAULT_PORT = 19184
|
3
|
+
DEFAULT_SERVER_CONFIGURATION_FILE = "/etc/boat.conf"
|
4
|
+
DEFAULT_CLIENT_CONFIGURATION_FILE = "#{ENV['HOME']}/.boat.yml"
|
5
|
+
DEFAULT_STORAGE_DIRECTORY = "/var/lib/boat"
|
6
|
+
end
|
7
|
+
|
8
|
+
require 'boat/version'
|
9
|
+
require 'boat/client'
|
10
|
+
require 'boat/server'
|
11
|
+
require 'boat/put'
|
data/lib/boat/client.rb
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
require 'hmac/sha2'
|
2
|
+
require 'socket'
|
3
|
+
|
4
|
+
class Boat::Client
|
5
|
+
Error = Class.new(StandardError)
|
6
|
+
|
7
|
+
def initialize(username, key, host, opts = {})
|
8
|
+
port = opts.fetch(:port, Boat::DEFAULT_PORT)
|
9
|
+
@key = key
|
10
|
+
@debug = opts.fetch(:debug, false)
|
11
|
+
@chunk_size = opts.fetch(:chunk_size, 1048576)
|
12
|
+
|
13
|
+
puts "[debug] connecting to #{host} port #{port}" if @debug
|
14
|
+
@socket = TCPSocket.new(host, port)
|
15
|
+
response = socket_gets.to_s
|
16
|
+
raise Error, response unless response =~ /^220/
|
17
|
+
|
18
|
+
puts "[debug] sending username" if @debug
|
19
|
+
socket_puts "user #{username}"
|
20
|
+
response = socket_gets.to_s
|
21
|
+
raise Error, response unless response =~ /^251 HMAC-SHA256 (.+)/
|
22
|
+
|
23
|
+
puts "[debug] sending password" if @debug
|
24
|
+
password_hash = HMAC::SHA256.hexdigest(key, $1)
|
25
|
+
socket_puts "pass #{password_hash}"
|
26
|
+
response = socket_gets.to_s
|
27
|
+
raise Error, response unless response =~ /^250/
|
28
|
+
end
|
29
|
+
|
30
|
+
def put(io, filename, size = nil, hash = nil)
|
31
|
+
encoded_filename = CGI.escape(filename)
|
32
|
+
puts "[debug] sending put command with filename #{encoded_filename}" if @debug
|
33
|
+
socket_puts "put #{encoded_filename}"
|
34
|
+
response = socket_gets.to_s
|
35
|
+
raise Error, response unless response =~ /^250/
|
36
|
+
server_salt = response.strip[4..-1]
|
37
|
+
|
38
|
+
size ||= io.respond_to?(:stat) ? io.stat.size : io.length
|
39
|
+
|
40
|
+
hash ||= if io.respond_to?(:path)
|
41
|
+
Digest::SHA256.file(io.path)
|
42
|
+
elsif !io.respond_to?(:read)
|
43
|
+
Digest::SHA256.hexdigest(io)
|
44
|
+
else
|
45
|
+
"-"
|
46
|
+
end
|
47
|
+
|
48
|
+
client_salt = [Digest::SHA256.digest((0..64).inject("") {|r, i| r << rand(256).chr})].pack("m").strip
|
49
|
+
signature = HMAC::SHA256.hexdigest(@key, "#{server_salt}#{encoded_filename}#{size}#{hash}#{client_salt}")
|
50
|
+
|
51
|
+
puts "[debug] sending data command" if @debug
|
52
|
+
socket_puts "data #{size} #{hash} #{client_salt} #{signature}"
|
53
|
+
response = socket_gets.to_s
|
54
|
+
|
55
|
+
# The server might already have the file with this hash - if so it'll return 255 at this point.
|
56
|
+
if matches = response.strip.match(/\A255 accepted ([0-9a-f]{64})\z/i)
|
57
|
+
confirm_hash = HMAC::SHA256.hexdigest(@key, "#{client_salt}#{hash}")
|
58
|
+
if matches[1] != confirm_hash
|
59
|
+
raise Error, "Incorrect server signature; the srver may be faking that it received the upload"
|
60
|
+
end
|
61
|
+
return size
|
62
|
+
end
|
63
|
+
|
64
|
+
raise Error, response unless response =~ /^253/
|
65
|
+
|
66
|
+
if io.respond_to?(:read)
|
67
|
+
digest = Digest::SHA256.new if hash == '-'
|
68
|
+
written = 0
|
69
|
+
while data = io.read(@chunk_size)
|
70
|
+
if @debug
|
71
|
+
print "[debug] sending data (#{written} / #{size} bytes)\r"
|
72
|
+
STDOUT.flush
|
73
|
+
end
|
74
|
+
digest << data if hash == '-'
|
75
|
+
@socket.write(data)
|
76
|
+
written += data.length
|
77
|
+
end
|
78
|
+
else
|
79
|
+
puts "[debug] sending data" if @debug
|
80
|
+
@socket.write(io)
|
81
|
+
digest << io
|
82
|
+
end
|
83
|
+
|
84
|
+
puts "[debug] data sent (#{size} bytes); waiting for response" if @debug
|
85
|
+
response = socket_gets.to_s
|
86
|
+
|
87
|
+
if response =~ /^254/ # we need to send the hash of the file because we didn't on the DATA line
|
88
|
+
hash = digest.to_s
|
89
|
+
signature = HMAC::SHA256.hexdigest(@key, "#{server_salt}#{encoded_filename}#{size}#{hash}#{client_salt}")
|
90
|
+
|
91
|
+
puts "[debug] sending confirm command" if @debug
|
92
|
+
socket_puts "confirm #{hash} #{signature}\n"
|
93
|
+
response = socket_gets.to_s
|
94
|
+
end
|
95
|
+
|
96
|
+
raise Error, response unless response && matches = response.strip.match(/\A255 accepted ([0-9a-f]{64})\z/i)
|
97
|
+
|
98
|
+
confirm_hash = HMAC::SHA256.hexdigest(@key, "#{client_salt}#{hash}")
|
99
|
+
if matches[1] != confirm_hash
|
100
|
+
raise Error, "Incorrect server signature; the srver may be faking that it received the upload"
|
101
|
+
end
|
102
|
+
|
103
|
+
size
|
104
|
+
end
|
105
|
+
|
106
|
+
def quit
|
107
|
+
puts "[debug] sending quit" if @debug
|
108
|
+
socket_puts "quit"
|
109
|
+
response = socket_gets
|
110
|
+
@socket.close
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
def socket_gets
|
115
|
+
data = @socket.gets
|
116
|
+
puts "[debug] < #{data}" if @debug
|
117
|
+
data
|
118
|
+
end
|
119
|
+
|
120
|
+
def socket_puts(data)
|
121
|
+
result = @socket.puts(data)
|
122
|
+
puts "[debug] > #{data}" if @debug
|
123
|
+
result
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
data/lib/boat/put.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
class Boat::Put
|
5
|
+
def run
|
6
|
+
while ARGV.first && ARGV.first[0..0] == '-' && ARGV.first.length > 1
|
7
|
+
case opt = ARGV.shift
|
8
|
+
when '-v' then debug = true
|
9
|
+
when '-c' then config_file = ARGV.shift
|
10
|
+
else raise "unknown commandline option #{opt}"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
filename, destination_filename = ARGV
|
15
|
+
|
16
|
+
if filename == '-' && destination_filename.to_s.empty?
|
17
|
+
raise "you must specify a destination_filename if you are uploading from stdin"
|
18
|
+
end
|
19
|
+
|
20
|
+
destination_filename ||= File.basename(filename)
|
21
|
+
config_file ||= Boat::DEFAULT_CLIENT_CONFIGURATION_FILE
|
22
|
+
|
23
|
+
unless File.exists?(config_file)
|
24
|
+
raise "#{config_file} does not exist. run boat configure"
|
25
|
+
end
|
26
|
+
|
27
|
+
configuration = YAML.load(IO.read(config_file))
|
28
|
+
|
29
|
+
if filename != '-' && !File.exists?(filename)
|
30
|
+
raise "#{filename} doesn't exist"
|
31
|
+
end
|
32
|
+
|
33
|
+
begin
|
34
|
+
client = Boat::Client.new(configuration["username"], configuration["key"], configuration["host"], :debug => debug)
|
35
|
+
if filename == '-'
|
36
|
+
client.put(STDIN, destination_filename)
|
37
|
+
else
|
38
|
+
File.open(filename, "r") {|file| client.put(file, destination_filename)}
|
39
|
+
end
|
40
|
+
client.quit
|
41
|
+
rescue Boat::Client::Error => e
|
42
|
+
STDERR.puts e.message
|
43
|
+
exit(1)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/lib/boat/server.rb
ADDED
@@ -0,0 +1,291 @@
|
|
1
|
+
require 'hmac/sha2'
|
2
|
+
require 'eventmachine'
|
3
|
+
require 'syslog'
|
4
|
+
require 'digest'
|
5
|
+
require 'fileutils'
|
6
|
+
|
7
|
+
class Boat::Server
|
8
|
+
ConfigurationError = Class.new(StandardError)
|
9
|
+
|
10
|
+
attr_reader :configuration
|
11
|
+
|
12
|
+
module BoatServer
|
13
|
+
include EventMachine::Protocols::LineText2
|
14
|
+
NextCommand = Class.new(StandardError)
|
15
|
+
@@last_connection_id = 0
|
16
|
+
|
17
|
+
def initialize(configuration)
|
18
|
+
@configuration = configuration
|
19
|
+
end
|
20
|
+
|
21
|
+
def post_init
|
22
|
+
@@last_connection_id += 1
|
23
|
+
@connection_id = @@last_connection_id
|
24
|
+
@temporary_files = []
|
25
|
+
send_data "220 Boat Server #{Boat::VERSION}\n"
|
26
|
+
end
|
27
|
+
|
28
|
+
def receive_line(line)
|
29
|
+
match = line.match(/\A(\S*)(.*)?/)
|
30
|
+
command = match[1].downcase
|
31
|
+
args = match[2].strip if match[2] && !match[2].strip.empty?
|
32
|
+
|
33
|
+
begin
|
34
|
+
if %w(user pass put get data confirm quit).include?(command)
|
35
|
+
send("command_#{command}", args)
|
36
|
+
else
|
37
|
+
send_data "500 unknown command\n"
|
38
|
+
end
|
39
|
+
rescue NextCommand
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def command_user(args)
|
44
|
+
if @authenticated
|
45
|
+
send_data "500 already authenticated\n"
|
46
|
+
elsif args.empty? || args.match(/[^a-z0-9_]/i)
|
47
|
+
send_data "500 invalid username\n"
|
48
|
+
else
|
49
|
+
@username = args
|
50
|
+
@login_salt = random_salt
|
51
|
+
send_data "251 HMAC-SHA256 #{@login_salt}\n"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def command_pass(args)
|
56
|
+
if @authenticated
|
57
|
+
send_data "500 already authenticated\n"
|
58
|
+
elsif @username.nil? || @login_salt.nil?
|
59
|
+
send_data "500 USER first\n"
|
60
|
+
else
|
61
|
+
user = @configuration.fetch("users", {}).fetch(@username, nil)
|
62
|
+
expected = HMAC::SHA256.hexdigest(user["key"], @login_salt) if user
|
63
|
+
if user && expected && args == expected
|
64
|
+
send_data "250 OK\n"
|
65
|
+
@user = user
|
66
|
+
@authenticated = true
|
67
|
+
else
|
68
|
+
@username = @login_salt = nil
|
69
|
+
send_data "401 invalid username or password\n"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def command_put(args)
|
75
|
+
check_authenticated!
|
76
|
+
|
77
|
+
if @user["access"] == "r"
|
78
|
+
send_data "400 no write access\n"
|
79
|
+
elsif @put
|
80
|
+
send_data "500 PUT already sent\n"
|
81
|
+
elsif !args.match(/\A[a-z0-9_.%+-]+\z/i) # filenames should be urlencoded
|
82
|
+
send_data "500 invalid filename\n"
|
83
|
+
else
|
84
|
+
if @user.fetch("versioning", true) == false && File.exists?("#{repository_path}/current.#{args}")
|
85
|
+
send_data "500 file already exists\n"
|
86
|
+
else
|
87
|
+
@put = {:state => "PUT", :filename => args, :server_salt => random_salt}
|
88
|
+
send_data "250 #{@put[:server_salt]}\n"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def command_data(args)
|
94
|
+
check_authenticated!
|
95
|
+
|
96
|
+
if @put.nil?
|
97
|
+
send_data "500 PUT first\n"
|
98
|
+
elsif @put[:state] != "PUT"
|
99
|
+
send_data "500 DATA already sent\n"
|
100
|
+
elsif (matches = args.match(/\A([0-9]+) ([0-9a-f]{64}|-) (\S+) ([0-9a-f]{64})\z/i)).nil?
|
101
|
+
send_data "500 invalid DATA command line; requires size, hash, new salt and signature\n"
|
102
|
+
else
|
103
|
+
size = matches[1].to_i
|
104
|
+
file_hash = matches[2].downcase
|
105
|
+
client_salt = matches[3]
|
106
|
+
signature = matches[4].downcase
|
107
|
+
|
108
|
+
if size >= 1<<31
|
109
|
+
send_data "500 size too large\n"
|
110
|
+
elsif signature != HMAC::SHA256.hexdigest(@user["key"], "#{@put.fetch(:server_salt)}#{@put.fetch(:filename)}#{size}#{file_hash}#{client_salt}")
|
111
|
+
send_data "500 signature is invalid\n"
|
112
|
+
elsif File.exists?(current_filename = "#{repository_path}/current.#{@put.fetch(:filename)}") && Digest::SHA256.file(current_filename).to_s == file_hash
|
113
|
+
signature = HMAC::SHA256.hexdigest(@user["key"], "#{client_salt}#{file_hash}")
|
114
|
+
send_data "255 accepted #{signature}\n"
|
115
|
+
else
|
116
|
+
@put[:temporary_id] = "#{Time.now.to_i}.#{Process.pid}.#{@connection_id}"
|
117
|
+
@put[:temporary_filename] = "#{@configuration["storage_path"]}/tmp/#{@put.fetch(:temporary_id)}"
|
118
|
+
@put.merge!(
|
119
|
+
:state => "DATA",
|
120
|
+
:size => size,
|
121
|
+
:hash => (file_hash unless file_hash == '-'),
|
122
|
+
:client_salt => client_salt,
|
123
|
+
:file_handle => File.open(@put[:temporary_filename], "w"),
|
124
|
+
:digest => Digest::SHA256.new)
|
125
|
+
|
126
|
+
@temporary_files << @put[:temporary_filename]
|
127
|
+
|
128
|
+
send_data "253 send #{size} bytes now\n"
|
129
|
+
set_binary_mode size
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def receive_binary_data(data)
|
135
|
+
@put[:file_handle].write data
|
136
|
+
@put[:digest] << data
|
137
|
+
end
|
138
|
+
|
139
|
+
def receive_end_of_binary_data
|
140
|
+
@put[:file_handle].close
|
141
|
+
|
142
|
+
if @put.fetch(:hash).nil?
|
143
|
+
@put[:state] = "awaiting CONFIRM"
|
144
|
+
send_data "254 send hash confirmation\n"
|
145
|
+
else
|
146
|
+
complete_put
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def command_confirm(args)
|
151
|
+
if @put.nil? || @put[:state] != "awaiting CONFIRM"
|
152
|
+
send_data "500 no need to send CONFIRM\n"
|
153
|
+
elsif (matches = args.match(/\A([0-9a-f]{64}) ([0-9a-f]{64})\z/i)).nil?
|
154
|
+
send_data "500 invalid CONFIRM command line; requires hash and signature\n"
|
155
|
+
else
|
156
|
+
file_hash = matches[1].downcase
|
157
|
+
signature = matches[2].downcase
|
158
|
+
|
159
|
+
if signature != HMAC::SHA256.hexdigest(@user["key"], "#{@put.fetch(:server_salt)}#{@put.fetch(:filename)}#{@put.fetch(:size)}#{file_hash}#{@put.fetch(:client_salt)}")
|
160
|
+
send_data "500 signature is invalid\n"
|
161
|
+
@put = nil
|
162
|
+
else
|
163
|
+
@put[:hash] = file_hash
|
164
|
+
complete_put
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def complete_put
|
170
|
+
calculated_hash = @put.fetch(:digest).to_s
|
171
|
+
|
172
|
+
if @put.fetch(:hash) != calculated_hash
|
173
|
+
send_data "500 file hash does not match hash supplied by client\n"
|
174
|
+
File.unlink(@put.fetch(:temporary_filename))
|
175
|
+
@temporary_files.delete(@put.fetch(:temporary_filename))
|
176
|
+
return
|
177
|
+
end
|
178
|
+
|
179
|
+
FileUtils.mkdir_p(repository_path)
|
180
|
+
version_filename = "#{repository_path}/#{@put.fetch(:temporary_id)}.#{@put.fetch(:filename)}"
|
181
|
+
symlink_name = "#{repository_path}/current.#{@put.fetch(:filename)}"
|
182
|
+
|
183
|
+
if @user.fetch("versioning", true) == false && File.exists?(symlink_name)
|
184
|
+
send_data "500 file with same filename was uploaded before this upload completed\n"
|
185
|
+
File.unlink(@put.fetch(:temporary_filename))
|
186
|
+
@temporary_files.delete(@put.fetch(:temporary_filename))
|
187
|
+
return
|
188
|
+
end
|
189
|
+
|
190
|
+
File.rename(@put.fetch(:temporary_filename), version_filename)
|
191
|
+
@temporary_files.delete(@put.fetch(:temporary_filename))
|
192
|
+
begin
|
193
|
+
File.unlink(symlink_name) if File.symlink?(symlink_name)
|
194
|
+
rescue Errno::ENOENT
|
195
|
+
end
|
196
|
+
File.symlink(version_filename, symlink_name)
|
197
|
+
|
198
|
+
signature = HMAC::SHA256.hexdigest(@user["key"], "#{@put.fetch(:client_salt)}#{@put.fetch(:hash)}")
|
199
|
+
send_data "255 accepted #{signature}\n"
|
200
|
+
ensure
|
201
|
+
@put = nil
|
202
|
+
end
|
203
|
+
|
204
|
+
def command_get(args)
|
205
|
+
check_authenticated!
|
206
|
+
send_data "500 not implemented\n"
|
207
|
+
end
|
208
|
+
|
209
|
+
def command_quit(args)
|
210
|
+
send_data "221 bye\n"
|
211
|
+
close_connection_after_writing
|
212
|
+
end
|
213
|
+
|
214
|
+
def check_authenticated!
|
215
|
+
unless @authenticated
|
216
|
+
send_data "500 not authenticated\n"
|
217
|
+
raise NextCommand
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
def unbind
|
222
|
+
@temporary_files.each do |filename|
|
223
|
+
begin
|
224
|
+
File.unlink(filename)
|
225
|
+
rescue Errno::ENOENT
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
def random_salt
|
231
|
+
[Digest::SHA256.digest((0..64).inject("") {|r, i| r << rand(256).chr})].pack("m").strip
|
232
|
+
end
|
233
|
+
|
234
|
+
def repository_path
|
235
|
+
@user && "#{@configuration.fetch("storage_path")}/repositories/#{@user.fetch("repository")}"
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
def load_configuration
|
240
|
+
unless File.exists?(@config_file)
|
241
|
+
raise "configuration file #{config_file} does not exist"
|
242
|
+
end
|
243
|
+
|
244
|
+
configuration = YAML.load(IO.read(@config_file))
|
245
|
+
if configuration["users"].nil? || configuration["users"].empty?
|
246
|
+
raise "configuration file does not have any users defined in it"
|
247
|
+
end
|
248
|
+
|
249
|
+
configuration["storage_path"] ||= Boat::DEFAULT_STORAGE_DIRECTORY
|
250
|
+
FileUtils.mkdir_p("#{configuration["storage_path"]}/tmp")
|
251
|
+
|
252
|
+
@configuration.update(configuration)
|
253
|
+
rescue => e
|
254
|
+
raise ConfigurationError, e.message, $@
|
255
|
+
end
|
256
|
+
|
257
|
+
def run
|
258
|
+
trap('SIGINT') { exit }
|
259
|
+
trap('SIGTERM') { exit }
|
260
|
+
|
261
|
+
while ARGV.first && ARGV.first[0..0] == '-' && ARGV.first.length > 1
|
262
|
+
case opt = ARGV.shift
|
263
|
+
when '-c' then @config_file = ARGV.shift
|
264
|
+
else raise "unknown commandline option #{opt}"
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
@config_file ||= Boat::DEFAULT_SERVER_CONFIGURATION_FILE
|
269
|
+
@configuration = {}
|
270
|
+
load_configuration
|
271
|
+
|
272
|
+
trap('SIGHUP') do
|
273
|
+
begin
|
274
|
+
load_configuration
|
275
|
+
rescue ConfigurationError => e
|
276
|
+
STDERR.puts "Could not reload configuration file: #{e.message}"
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
#Syslog.open 'boat'
|
281
|
+
|
282
|
+
File.umask(0077)
|
283
|
+
EventMachine.run do
|
284
|
+
EventMachine.start_server(
|
285
|
+
@configuration.fetch("listen_address", "localhost"),
|
286
|
+
@configuration.fetch("listen_port", Boat::DEFAULT_PORT),
|
287
|
+
BoatServer,
|
288
|
+
@configuration)
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|
data/lib/boat/version.rb
ADDED
data/server.conf
ADDED
metadata
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: boat
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 15
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 2
|
9
|
+
version: "0.2"
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Roger Nesbitt
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2011-05-30 00:00:00 +12:00
|
18
|
+
default_executable:
|
19
|
+
dependencies: []
|
20
|
+
|
21
|
+
description: File upload client and server specifically aimed at transferring already-encrypted backups
|
22
|
+
email: []
|
23
|
+
|
24
|
+
executables:
|
25
|
+
- boat
|
26
|
+
extensions: []
|
27
|
+
|
28
|
+
extra_rdoc_files: []
|
29
|
+
|
30
|
+
files:
|
31
|
+
- .gitignore
|
32
|
+
- Gemfile
|
33
|
+
- README
|
34
|
+
- Rakefile
|
35
|
+
- bin/boat
|
36
|
+
- boat.gemspec
|
37
|
+
- lib/boat.rb
|
38
|
+
- lib/boat/client.rb
|
39
|
+
- lib/boat/put.rb
|
40
|
+
- lib/boat/server.rb
|
41
|
+
- lib/boat/version.rb
|
42
|
+
- server.conf
|
43
|
+
has_rdoc: true
|
44
|
+
homepage: http://rubygems.org/gems/boat
|
45
|
+
licenses: []
|
46
|
+
|
47
|
+
post_install_message:
|
48
|
+
rdoc_options: []
|
49
|
+
|
50
|
+
require_paths:
|
51
|
+
- lib
|
52
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
53
|
+
none: false
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
hash: 3
|
58
|
+
segments:
|
59
|
+
- 0
|
60
|
+
version: "0"
|
61
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
62
|
+
none: false
|
63
|
+
requirements:
|
64
|
+
- - ">="
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
hash: 23
|
67
|
+
segments:
|
68
|
+
- 1
|
69
|
+
- 3
|
70
|
+
- 6
|
71
|
+
version: 1.3.6
|
72
|
+
requirements: []
|
73
|
+
|
74
|
+
rubyforge_project: boat
|
75
|
+
rubygems_version: 1.4.2
|
76
|
+
signing_key:
|
77
|
+
specification_version: 3
|
78
|
+
summary: File upload client and server specifically aimed at transferring already-encrypted backups
|
79
|
+
test_files: []
|
80
|
+
|