sftp_server 0.5.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,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: e6579fc72d42db229fdc52951724810a74e81661
4
+ data.tar.gz: a73bb16cb690daa050997e727c195740e70c841b
5
+ SHA512:
6
+ metadata.gz: c27a18a11ed53b0c762c48d6c43f2ba495bcb8ca18f85ce561ae8550be0d84df7d1220bd4ec0d1795dc32fb2abcd42cdacfc3ce20d4b346b574c456f20def542
7
+ data.tar.gz: 840fa2cfe4cc9ae5de894a94b03e07d1d8b98679d1d8f996226db9ff382feb025fb5fd561555b79f3a278789e6e7807f72b25d18a6256ffac70babcdd2783d8b
@@ -0,0 +1,15 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
15
+ *.gem
@@ -0,0 +1 @@
1
+ 2.1.5
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in sftp_server.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 M. Scott Ford
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following 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 OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,33 @@
1
+ # SFTPServer
2
+
3
+ A simple SFTP server for testing clients:
4
+
5
+ Uses libssh via FFI to create an SFTP server. Useful for testing interactions with a remote SFTP server.
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'sftp_server'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install sftp_server
22
+
23
+ ## Usage
24
+
25
+ TODO: Write usage instructions here
26
+
27
+ ## Contributing
28
+
29
+ 1. Fork it ( https://github.com/corgibytes/sftp_server/fork )
30
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
31
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
32
+ 4. Push to the branch (`git push origin my-new-feature`)
33
+ 5. Create a new Pull Request
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,2 @@
1
+ require 'sftp_server/version'
2
+ require 'sftp_server/server'
@@ -0,0 +1,13 @@
1
+ require 'ffi'
2
+
3
+ module SFTPServer
4
+ module C
5
+ module API
6
+ extend FFI::Library
7
+ ffi_lib_flags :now, :global
8
+ ffi_lib 'c'
9
+
10
+ attach_function :opendir, [:string], :pointer
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,417 @@
1
+ require 'sftp_server/ssh/api'
2
+ require 'sftp_server/c/api'
3
+
4
+ require 'pry'
5
+
6
+ module SFTPServer
7
+ class Server
8
+ attr_accessor :user_name
9
+ attr_accessor :password
10
+ attr_accessor :rsa_key
11
+ attr_accessor :dsa_key
12
+ attr_accessor :port
13
+ attr_accessor :listen_address
14
+ attr_accessor :verbose
15
+
16
+ def initialize(options = {})
17
+ @user_name = options[:user_name]
18
+ @password = options[:password]
19
+ @rsa_key = options[:rsa_key]
20
+ @dsa_key = options[:dsa_key]
21
+ @port = options[:port]
22
+ @listen_address = options[:listen_address]
23
+ @verbose = options[:verbose]
24
+
25
+ @authenticated = false
26
+ @handles = {}
27
+ end
28
+
29
+ def set_bind_option(sshbind, key_type, key_value, value_type, value_value)
30
+ result = SSH::API.ssh_bind_options_set(
31
+ sshbind, key_type, key_value, value_type, value_value
32
+ )
33
+ fail SSH::API.ssh_get_error(sshbind) if result < 0
34
+ end
35
+
36
+ def bind_listen(bind)
37
+ result = SSH::API.ssh_bind_listen(bind)
38
+ fail SSH::API.ssh_get_error(bind) if result < 0
39
+ end
40
+
41
+ def bind_accept(bind, session)
42
+ result = SSH::API.ssh_bind_accept(bind, session)
43
+ fail SSH::API.ssh_get_error(bind) if result < 0
44
+ end
45
+
46
+ def handle_key_exchange(session)
47
+ result = SSH::API.ssh_handle_key_exchange(session)
48
+ fail SSH::API.ssh_get_error(session) if result < 0
49
+ end
50
+
51
+ def handle_auth(message)
52
+ end
53
+
54
+ def respond_auth_required(message)
55
+ SSH::API.ssh_message_auth_set_methods(message, SSH::API::MessageAuthTypes::SSH_AUTH_METHOD_PASSWORD)
56
+ SSH::API.ssh_message_reply_default(message)
57
+ end
58
+
59
+ def sftp_channel_request(session)
60
+ sftp_channel_requested = false
61
+ while !sftp_channel_requested
62
+ message = SSH::API.ssh_message_get(session)
63
+ if message
64
+ message_type = SSH::API.ssh_message_type(message)
65
+ log "open sftp session message_type: #{message_type}"
66
+ if message_type == SSH::API::MessageTypes::SSH_REQUEST_CHANNEL
67
+ message_subtype = SSH::API.ssh_message_subtype(message)
68
+ log "open sftp session message_subtype: #{message_subtype}"
69
+
70
+ case message_subtype
71
+ when SSH::API::ChannelRequestTypes::SSH_CHANNEL_REQUEST_ENV
72
+ env_name = SSH::API.ssh_message_channel_request_env_name(message)
73
+ log "request env name: #{env_name}"
74
+
75
+ env_value = SSH::API.ssh_message_channel_request_env_value(message)
76
+ log "request env value: #{env_value}"
77
+ when SSH::API::ChannelRequestTypes::SSH_CHANNEL_REQUEST_SUBSYSTEM
78
+ subsystem_name = SSH::API.ssh_message_channel_request_subsystem(message)
79
+ log "request subsystem: #{subsystem_name}"
80
+ if subsystem_name == 'sftp'
81
+ SSH::API.ssh_message_channel_request_reply_success(message)
82
+ sftp_channel_requested = true
83
+ end
84
+ end
85
+ end
86
+ end
87
+
88
+ unless sftp_channel_requested
89
+ SSH::API.ssh_message_reply_default(message)
90
+ end
91
+ SSH::API.ssh_message_free(message)
92
+ end
93
+ sftp_channel_requested
94
+ end
95
+
96
+ def log(message)
97
+ puts(message) if verbose
98
+ end
99
+
100
+ def open_channel(session)
101
+ channel = nil
102
+ while channel.nil?
103
+ message = SSH::API.ssh_message_get(session)
104
+ next unless message
105
+
106
+ message_type = SSH::API.ssh_message_type(message)
107
+ log "channel message_type: #{message_type}"
108
+ next unless message_type > -1
109
+
110
+ case message_type
111
+ when SSH::API::MessageTypes::SSH_REQUEST_CHANNEL_OPEN
112
+ message_subtype = SSH::API.ssh_message_subtype(message)
113
+ log "channel message_subtype: #{message_subtype}"
114
+ if message_subtype == SSH::API::ChannelTypes::SSH_CHANNEL_SESSION
115
+ channel = SSH::API.ssh_message_channel_request_open_reply_accept(message)
116
+ break
117
+ end
118
+ else
119
+ SSH::API.ssh_message_reply_default(message)
120
+ end
121
+ SSH::API.ssh_message_free(message)
122
+ end
123
+ channel
124
+ end
125
+
126
+ def authenticate(session)
127
+ authenticated = false
128
+ while !authenticated
129
+ message = SSH::API.ssh_message_get(session)
130
+ next unless message
131
+
132
+ message_type = SSH::API.ssh_message_type(message)
133
+ log "message_type: #{message_type}"
134
+ next unless message_type > -1
135
+
136
+ case message_type
137
+ when SSH::API::MessageTypes::SSH_REQUEST_AUTH
138
+ log "auth"
139
+ message_subtype = SSH::API.ssh_message_subtype(message)
140
+ log "auth message_subtype: #{message_subtype}"
141
+ case message_subtype
142
+ when SSH::API::MessageAuthTypes::SSH_AUTH_METHOD_PASSWORD
143
+ request_user_name = SSH::API.ssh_message_auth_user(message)
144
+ request_password = SSH::API.ssh_message_auth_password(message)
145
+ log user_name
146
+ log password
147
+ if user_name == request_user_name && password == request_password
148
+ SSH::API.ssh_message_auth_reply_success(message, 0)
149
+ SSH::API.ssh_message_free(message)
150
+ authenticated = true
151
+ break
152
+ else
153
+ SSH::API.ssh_message_reply_default(message)
154
+ next
155
+ end
156
+ else
157
+ respond_auth_required(message) unless @authenticated
158
+ end
159
+ else
160
+ SSH::API.ssh_message_reply_default(message)
161
+ end
162
+ end
163
+ authenticated
164
+ end
165
+
166
+ def init_sftp_session(sftp_session)
167
+ result = SSH::API.sftp_server_init(sftp_session)
168
+ fail SSH::API.ssh_get_error(sftp_session) unless result == 0
169
+ end
170
+
171
+ def sftp_message_loop(sftp_session)
172
+ while true
173
+ client_message = SSH::API.sftp_get_client_message(sftp_session)
174
+ log "client_message: #{client_message}"
175
+ return if client_message.null?
176
+
177
+ client_message_type = SSH::API.sftp_client_message_get_type(client_message)
178
+ next unless client_message_type
179
+ log "client_message_type: #{client_message_type}"
180
+
181
+ case client_message_type
182
+ when SSH::API::SFTPCommands::SSH_FXP_REALPATH
183
+ log "realpath"
184
+
185
+ file_name = SSH::API.sftp_client_message_get_filename(client_message)
186
+ log "file_name: #{file_name}"
187
+
188
+ long_file_name = File.expand_path(file_name)
189
+
190
+ SSH::API.sftp_reply_names_add(client_message, long_file_name, long_file_name, SSH::API::SFTPAttributes.new.to_ptr)
191
+ SSH::API.sftp_reply_names(client_message)
192
+ when SSH::API::SFTPCommands::SSH_FXP_OPENDIR
193
+ log "opendir"
194
+
195
+ dir_name = SSH::API.sftp_client_message_get_filename(client_message)
196
+ long_dir_name = File.expand_path(dir_name)
197
+ log "long_dir_name: #{long_dir_name}"
198
+
199
+ @handles[long_dir_name] = :open
200
+
201
+ long_dir_name_pointer = FFI::MemoryPointer.from_string(long_dir_name)
202
+ handle = SSH::API.sftp_handle_alloc(sftp_session, long_dir_name_pointer)
203
+
204
+ SSH::API.sftp_reply_handle(client_message, handle)
205
+ when SSH::API::SFTPCommands::SSH_FXP_READDIR
206
+ log "readdir"
207
+
208
+ client_message_data = SSH::API::SFTPClientMessage.new(client_message)
209
+ handle = SSH::API.sftp_handle(sftp_session, client_message_data[:handle])
210
+ long_dir_name = handle.read_string
211
+ log "long_dir_name: #{long_dir_name}"
212
+
213
+ if @handles[long_dir_name] == :open
214
+ Dir.entries(long_dir_name).each do |entry|
215
+ SSH::API.sftp_reply_names_add(
216
+ client_message,
217
+ entry,
218
+ entry,
219
+ SSH::API::SFTPAttributes.new.to_ptr
220
+ )
221
+ end
222
+ @handles[long_dir_name] = :read
223
+ SSH::API.sftp_reply_names(client_message)
224
+ else
225
+ SSH::API.sftp_reply_status(client_message, SSH::API::SFTPStatus::SSH_FX_EOF, 'End-of-file encountered')
226
+ end
227
+ when SSH::API::SFTPCommands::SSH_FXP_CLOSE
228
+ log 'close'
229
+
230
+ client_message_data = SSH::API::SFTPClientMessage.new(client_message)
231
+ handle = SSH::API.sftp_handle(sftp_session, client_message_data[:handle])
232
+ long_dir_name = handle.read_string
233
+ log "long_dir_name: #{long_dir_name}"
234
+
235
+ @handles.delete(long_dir_name)
236
+
237
+ SSH::API.sftp_reply_status(client_message, SSH::API::SFTPStatus::SSH_FX_OK, 'Success')
238
+ when SSH::API::SFTPCommands::SSH_FXP_LSTAT
239
+ log 'lstat'
240
+
241
+ file_name = SSH::API.sftp_client_message_get_filename(client_message)
242
+ log "file_name: #{file_name}"
243
+
244
+ long_file_name = File.expand_path(file_name)
245
+
246
+ file_stat = File.lstat(long_file_name)
247
+
248
+ attributes = SSH::API::SFTPAttributes.new
249
+
250
+ attributes[:flags] = 0
251
+ attributes[:flags] |= SSH::API::Attributes::SSH_FILEXFER_ATTR_SIZE
252
+ attributes[:size] = file_stat.size
253
+ attributes[:flags] |= SSH::API::Attributes::SSH_FILEXFER_ATTR_UIDGID
254
+ attributes[:uid] = file_stat.uid
255
+ attributes[:gid] = file_stat.gid
256
+ attributes[:flags] |= SSH::API::Attributes::SSH_FILEXFER_ATTR_PERMISSIONS
257
+ attributes[:permissions] = file_stat.mode
258
+ attributes[:flags] |= SSH::API::Attributes::SSH_FILEXFER_ATTR_ACMODTIME
259
+ attributes[:atime] = file_stat.atime.to_i
260
+ attributes[:mtime] = file_stat.mtime.to_i
261
+
262
+ SSH::API.sftp_reply_attr(client_message, attributes.to_ptr)
263
+ when SSH::API::SFTPCommands::SSH_FXP_STAT
264
+ log 'stat'
265
+
266
+ file_name = SSH::API.sftp_client_message_get_filename(client_message)
267
+ log "file_name: #{file_name}"
268
+
269
+ long_file_name = File.expand_path(file_name)
270
+
271
+ file_stat = File.stat(long_file_name)
272
+
273
+ attributes = SSH::API::SFTPAttributes.new
274
+
275
+ attributes[:flags] = 0
276
+ attributes[:flags] |= SSH::API::Attributes::SSH_FILEXFER_ATTR_SIZE
277
+ attributes[:size] = file_stat.size
278
+ attributes[:flags] |= SSH::API::Attributes::SSH_FILEXFER_ATTR_UIDGID
279
+ attributes[:uid] = file_stat.uid
280
+ attributes[:gid] = file_stat.gid
281
+ attributes[:flags] |= SSH::API::Attributes::SSH_FILEXFER_ATTR_PERMISSIONS
282
+ attributes[:permissions] = file_stat.mode
283
+ attributes[:flags] |= SSH::API::Attributes::SSH_FILEXFER_ATTR_ACMODTIME
284
+ attributes[:atime] = file_stat.atime.to_i
285
+ attributes[:mtime] = file_stat.mtime.to_i
286
+
287
+ SSH::API.sftp_reply_attr(client_message, attributes.to_ptr)
288
+ when SSH::API::SFTPCommands::SSH_FXP_OPEN
289
+ log 'open'
290
+
291
+ file_name = SSH::API.sftp_client_message_get_filename(client_message)
292
+ long_file_name = File.expand_path(file_name)
293
+ log "long_file_name: #{long_file_name}"
294
+
295
+ client_message_data = SSH::API::SFTPClientMessage.new(client_message)
296
+ message_flags = client_message_data[:flags]
297
+ flags = 0
298
+ if (message_flags & SSH::API::Flags::SSH_FXF_READ == SSH::API::Flags::SSH_FXF_READ) &&
299
+ (message_flags & SSH::API::Flags::SSH_FXF_WRITE == SSH::API::Flags::SSH_FXF_WRITE)
300
+ flags = File::Constants::RDWR
301
+ elsif (message_flags & SSH::API::Flags::SSH_FXF_READ == SSH::API::Flags::SSH_FXF_READ)
302
+ flags = File::Constants::RDONLY
303
+ elsif (message_flags & SSH::API::Flags::SSH_FXF_WRITE == SSH::API::Flags::SSH_FXF_WRITE)
304
+ flags = File::Constants::WRONLY
305
+ end
306
+
307
+ if (message_flags & SSH::API::Flags::SSH_FXF_APPEND == SSH::API::Flags::SSH_FXF_APPEND)
308
+ flags |= File::Constants::APPEND
309
+ end
310
+
311
+ if (message_flags & SSH::API::Flags::SSH_FXF_CREAT == SSH::API::Flags::SSH_FXF_CREAT)
312
+ flags |= File::Constants::CREAT
313
+ end
314
+
315
+ if (message_flags & SSH::API::Flags::SSH_FXF_TRUNC == SSH::API::Flags::SSH_FXF_TRUNC)
316
+ flags |= File::Constants::TRUNC
317
+ end
318
+
319
+ if (message_flags & SSH::API::Flags::SSH_FXF_EXCL == SSH::API::Flags::SSH_FXF_EXCL)
320
+ flags |= File::Constants::EXCL
321
+ end
322
+
323
+ @handles[long_file_name] = File.open(long_file_name, flags)
324
+
325
+ long_file_name_pointer = FFI::MemoryPointer.from_string(long_file_name)
326
+ handle = SSH::API.sftp_handle_alloc(sftp_session, long_file_name_pointer)
327
+
328
+ SSH::API.sftp_reply_handle(client_message, handle)
329
+ when SSH::API::SFTPCommands::SSH_FXP_READ
330
+ log 'read'
331
+
332
+ client_message_data = SSH::API::SFTPClientMessage.new(client_message)
333
+ handle = SSH::API.sftp_handle(sftp_session, client_message_data[:handle])
334
+ long_file_name = handle.read_string
335
+ log "long_file_name: #{long_file_name}"
336
+
337
+ file = @handles[long_file_name]
338
+ if file
339
+ file.seek(client_message_data[:offset])
340
+ data = file.read(client_message_data[:len])
341
+ if data
342
+ buffer = FFI::MemoryPointer.new(:char, data.size)
343
+ buffer.put_bytes(0, data)
344
+ SSH::API.sftp_reply_data(client_message, buffer, data.size)
345
+ else
346
+ SSH::API.sftp_reply_status(client_message, SSH::API::SFTPStatus::SSH_FX_EOF, 'End-of-file encountered')
347
+ end
348
+ end
349
+ when SSH::API::SFTPCommands::SSH_FXP_WRITE
350
+ log 'write'
351
+
352
+ client_message_data = SSH::API::SFTPClientMessage.new(client_message)
353
+ handle = SSH::API.sftp_handle(sftp_session, client_message_data[:handle])
354
+ long_file_name = handle.read_string
355
+ log "long_file_name: #{long_file_name}"
356
+
357
+ file = @handles[long_file_name]
358
+ if file
359
+ file.seek(client_message_data[:offset])
360
+ buffer = SSH::API.sftp_client_message_get_data(client_message)
361
+ file.write(buffer.read_string)
362
+ SSH::API.sftp_reply_status(client_message, SSH::API::SFTPStatus::SSH_FX_OK, 'Success')
363
+ end
364
+ end
365
+
366
+ SSH::API.sftp_client_message_free(client_message)
367
+ end
368
+ end
369
+
370
+ def close_channel(channel)
371
+ result = SSH::API.ssh_channel_close(channel)
372
+ fail SSH::API.ssh_get_error(channel) if result < 0
373
+ end
374
+
375
+ def free_channel(channel)
376
+ result = SSH::API.ssh_channel_free(channel)
377
+ fail SSH::API.ssh_get_error(channel) if result < 0
378
+ end
379
+
380
+ def disconnect_session(session)
381
+ result = SSH::API.ssh_disconnect(session)
382
+ fail SSH::API.ssh_get_error(session) if result < 0
383
+ end
384
+
385
+ def free_bind(bind)
386
+ result = SSH::API.ssh_bind_free(bind)
387
+ fail SSH::API.ssh_get_error(bind) if result < 0
388
+ end
389
+
390
+ def open
391
+ ssh_bind = SSH::API.ssh_bind_new
392
+ session = SSH::API.ssh_new
393
+
394
+ set_bind_option(ssh_bind, :int, SSH::API::BindOptions::SSH_BIND_OPTIONS_BINDADDR, :string, listen_address) if listen_address
395
+ set_bind_option(ssh_bind, :int, SSH::API::BindOptions::SSH_BIND_OPTIONS_BINDPORT_STR, :string, port) if port
396
+ set_bind_option(ssh_bind, :int, SSH::API::BindOptions::SSH_BIND_OPTIONS_RSAKEY, :string, rsa_key) if rsa_key
397
+ set_bind_option(ssh_bind, :int, SSH::API::BindOptions::SSH_BIND_OPTIONS_DSAKEY, :string, dsa_key) if dsa_key
398
+
399
+ bind_listen(ssh_bind)
400
+ bind_accept(ssh_bind, session)
401
+ handle_key_exchange(session)
402
+
403
+ if authenticate(session)
404
+ channel = open_channel(session)
405
+ if channel
406
+ if sftp_channel_request(session)
407
+ sftp_session = SSH::API.sftp_server_new(session, channel)
408
+ init_sftp_session(sftp_session)
409
+ sftp_message_loop(sftp_session)
410
+ end
411
+ end
412
+ close_channel(channel)
413
+ free_channel(channel)
414
+ end
415
+ end
416
+ end
417
+ end