Module: Boat::Server::BoatServer

Includes:
EventMachine::Protocols::LineText2
Defined in:
lib/boat/server.rb

Constant Summary collapse

NextCommand =
Class.new(StandardError)
@@last_connection_id =
0

Instance Method Summary collapse

Instance Method Details

#check_authenticated!Object



213
214
215
216
217
218
# File 'lib/boat/server.rb', line 213

def check_authenticated!
  unless @authenticated
    send_data "500 not authenticated\n"
    raise NextCommand
  end
end

#command_confirm(args) ⇒ Object



149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/boat/server.rb', line 149

def command_confirm(args)
  if @put.nil? || @put[:state] != "awaiting CONFIRM"
    send_data "500 no need to send CONFIRM\n"
  elsif (matches = args.match(/\A([0-9a-f]{64}) ([0-9a-f]{64})\z/i)).nil?
    send_data "500 invalid CONFIRM command line; requires hash and signature\n"
  else
    file_hash = matches[1].downcase
    signature = matches[2].downcase

    if signature != OpenSSL::HMAC.hexdigest(@digest, @user["key"], "#{@put.fetch(:server_salt)}#{@put.fetch(:filename)}#{@put.fetch(:size)}#{file_hash}#{@put.fetch(:client_salt)}")
      send_data "500 signature is invalid\n"
      @put = nil
    else
      @put[:hash] = file_hash
      complete_put
    end
  end
end

#command_data(args) ⇒ Object



92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/boat/server.rb', line 92

def command_data(args)
  check_authenticated!

  if @put.nil?
    send_data "500 PUT first\n"
  elsif @put[:state] != "PUT"
    send_data "500 DATA already sent\n"
  elsif (matches = args.match(/\A([0-9]+) ([0-9a-f]{64}|-) (\S+) ([0-9a-f]{64})\z/i)).nil?
    send_data "500 invalid DATA command line; requires size, hash, new salt and signature\n"
  else
    size = matches[1].to_i
    file_hash = matches[2].downcase
    client_salt = matches[3]
    signature = matches[4].downcase

    if size >= 1<<31
      send_data "500 size too large\n"
    elsif signature != OpenSSL::HMAC.hexdigest(@digest, @user["key"], "#{@put.fetch(:server_salt)}#{@put.fetch(:filename)}#{size}#{file_hash}#{client_salt}")
      send_data "500 signature is invalid\n"
    elsif File.exists?(current_filename = "#{repository_path}/current.#{@put.fetch(:filename)}") && OpenSSL::Digest.new('sha256').file(current_filename).to_s == file_hash
      signature = OpenSSL::HMAC.hexdigest(@digest, @user["key"], "#{client_salt}#{file_hash}")
      send_data "255 accepted #{signature}\n"
    else
      @put[:temporary_id] = "#{Time.now.to_i}.#{Process.pid}.#{@connection_id}"
      @put[:temporary_filename] = "#{@configuration["storage_path"]}/tmp/#{@put.fetch(:temporary_id)}"
      @put.merge!(
        :state => "DATA",
        :size => size,
        :hash => (file_hash unless file_hash == '-'),
        :client_salt => client_salt,
        :file_handle => File.open(@put[:temporary_filename], "w"),
        :digest => OpenSSL::Digest.new('sha256'))

      @temporary_files << @put[:temporary_filename]

      send_data "253 send #{size} bytes now\n"
      set_binary_mode size
    end
  end
end

#command_get(args) ⇒ Object



203
204
205
206
# File 'lib/boat/server.rb', line 203

def command_get(args)
  check_authenticated!
  send_data "500 not implemented\n"
end

#command_pass(args) ⇒ Object



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/boat/server.rb', line 54

def command_pass(args)
  if @authenticated
    send_data "500 already authenticated\n"
  elsif @username.nil? || @login_salt.nil?
    send_data "500 USER first\n"
  else
    user = @configuration.fetch("users", {}).fetch(@username, nil)
    expected = OpenSSL::HMAC.hexdigest(@digest, user["key"], @login_salt) if user
    if user && expected && args == expected
      send_data "250 OK\n"
      @user = user
      @authenticated = true
    else
      @username = @login_salt = nil
      send_data "401 invalid username or password\n"
    end
  end
end

#command_put(args) ⇒ Object



73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/boat/server.rb', line 73

def command_put(args)
  check_authenticated!

  if @user["access"] == "r"
    send_data "400 no write access\n"
  elsif @put
    send_data "500 PUT already sent\n"
  elsif !args.match(/\A[a-z0-9_.%+-]+\z/i) # filenames should be urlencoded
    send_data "500 invalid filename\n"
  else
    if @user.fetch("versioning", true) == false && File.exists?("#{repository_path}/current.#{args}")
      send_data "500 file already exists\n"
    else
      @put = {:state => "PUT", :filename => args, :server_salt => random_salt}
      send_data "250 #{@put[:server_salt]}\n"
    end
  end
end

#command_quit(args) ⇒ Object



208
209
210
211
# File 'lib/boat/server.rb', line 208

def command_quit(args)
  send_data "221 bye\n"
  close_connection_after_writing
end

#command_user(args) ⇒ Object



42
43
44
45
46
47
48
49
50
51
52
# File 'lib/boat/server.rb', line 42

def command_user(args)
  if @authenticated
    send_data "500 already authenticated\n"
  elsif args.empty? || args.match(/[^a-z0-9_.]/i)
    send_data "500 invalid username\n"
  else
    @username = args
    @login_salt = random_salt
    send_data "251 HMAC-SHA256 #{@login_salt}\n"
  end
end

#complete_putObject



168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
# File 'lib/boat/server.rb', line 168

def complete_put
  calculated_hash = @put.fetch(:digest).hexdigest

  if @put.fetch(:hash) != calculated_hash
    send_data "500 file hash does not match hash supplied by client\n"
    File.unlink(@put.fetch(:temporary_filename))
    @temporary_files.delete(@put.fetch(:temporary_filename))
    return
  end

  FileUtils.mkdir_p(repository_path)
  version_filename = "#{repository_path}/#{@put.fetch(:temporary_id)}.#{@put.fetch(:filename)}"
  symlink_name = "#{repository_path}/current.#{@put.fetch(:filename)}"

  if @user.fetch("versioning", true) == false && File.exists?(symlink_name)
    send_data "500 file with same filename was uploaded before this upload completed\n"
    File.unlink(@put.fetch(:temporary_filename))
    @temporary_files.delete(@put.fetch(:temporary_filename))
    return
  end

  File.rename(@put.fetch(:temporary_filename), version_filename)
  @temporary_files.delete(@put.fetch(:temporary_filename))
  begin
    File.unlink(symlink_name) if File.symlink?(symlink_name)
  rescue Errno::ENOENT
  end
  File.symlink(version_filename, symlink_name)

  signature = OpenSSL::HMAC.hexdigest(@digest, @user["key"], "#{@put.fetch(:client_salt)}#{@put.fetch(:hash)}")
  send_data "255 accepted #{signature}\n"
ensure
  @put = nil
end

#initialize(configuration) ⇒ Object



15
16
17
# File 'lib/boat/server.rb', line 15

def initialize(configuration)
  @configuration = configuration
end

#post_initObject



19
20
21
22
23
24
25
# File 'lib/boat/server.rb', line 19

def post_init
  @@last_connection_id += 1
  @connection_id = @@last_connection_id
  @temporary_files = []
  @digest = OpenSSL::Digest::Digest.new('sha256')
  send_data "220 Boat Server #{Boat::VERSION}\n"
end

#random_saltObject



229
230
231
# File 'lib/boat/server.rb', line 229

def random_salt
  [OpenSSL::Digest.new('sha256').digest((0..64).inject("") {|r, i| r << rand(256).chr})].pack("m").strip
end

#receive_binary_data(data) ⇒ Object



133
134
135
136
# File 'lib/boat/server.rb', line 133

def receive_binary_data(data)
  @put[:file_handle].write data
  @put[:digest] << data
end

#receive_end_of_binary_dataObject



138
139
140
141
142
143
144
145
146
147
# File 'lib/boat/server.rb', line 138

def receive_end_of_binary_data
  @put[:file_handle].close

  if @put.fetch(:hash).nil?
    @put[:state] = "awaiting CONFIRM"
    send_data "254 send hash confirmation\n"
  else
    complete_put
  end
end

#receive_line(line) ⇒ Object



27
28
29
30
31
32
33
34
35
36
37
38
39
40
# File 'lib/boat/server.rb', line 27

def receive_line(line)
  match = line.match(/\A(\S*)(.*)?/)
  command = match[1].downcase
  args = match[2].strip if match[2] && !match[2].strip.empty?

  begin
    if %w(user pass put get data confirm quit).include?(command)
      send("command_#{command}", args)
    else
      send_data "500 unknown command\n"
    end
  rescue NextCommand
  end
end

#repository_pathObject



233
234
235
# File 'lib/boat/server.rb', line 233

def repository_path
  @user && "#{@configuration.fetch("storage_path")}/repositories/#{@user.fetch("repository")}"
end

#unbindObject



220
221
222
223
224
225
226
227
# File 'lib/boat/server.rb', line 220

def unbind
  @temporary_files.each do |filename|
    begin
      File.unlink(filename)
    rescue Errno::ENOENT
    end
  end
end