Coverage for /pythoncovmergedfiles/medio/medio/src/paramiko/paramiko/sftp_server.py: 10%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

323 statements  

1# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> 

2# 

3# This file is part of paramiko. 

4# 

5# Paramiko is free software; you can redistribute it and/or modify it under the 

6# terms of the GNU Lesser General Public License as published by the Free 

7# Software Foundation; either version 2.1 of the License, or (at your option) 

8# any later version. 

9# 

10# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY 

11# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 

12# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 

13# details. 

14# 

15# You should have received a copy of the GNU Lesser General Public License 

16# along with Paramiko; if not, write to the Free Software Foundation, Inc., 

17# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 

18 

19""" 

20Server-mode SFTP support. 

21""" 

22 

23import errno 

24import os 

25import sys 

26from hashlib import md5, sha1 

27 

28from paramiko import util 

29from paramiko.common import DEBUG 

30from paramiko.server import SubsystemHandler 

31 

32# known hash algorithms for the "check-file" extension 

33from paramiko.sftp import ( 

34 CMD_ATTRS, 

35 CMD_CLOSE, 

36 CMD_DATA, 

37 CMD_EXTENDED, 

38 CMD_EXTENDED_REPLY, 

39 CMD_FSETSTAT, 

40 CMD_FSTAT, 

41 CMD_HANDLE, 

42 CMD_LSTAT, 

43 CMD_MKDIR, 

44 CMD_NAME, 

45 CMD_NAMES, 

46 CMD_OPEN, 

47 CMD_OPENDIR, 

48 CMD_READ, 

49 CMD_READDIR, 

50 CMD_READLINK, 

51 CMD_REALPATH, 

52 CMD_REMOVE, 

53 CMD_RENAME, 

54 CMD_RMDIR, 

55 CMD_SETSTAT, 

56 CMD_STAT, 

57 CMD_STATUS, 

58 CMD_SYMLINK, 

59 CMD_WRITE, 

60 SFTP_BAD_MESSAGE, 

61 SFTP_DESC, 

62 SFTP_EOF, 

63 SFTP_FAILURE, 

64 SFTP_FLAG_APPEND, 

65 SFTP_FLAG_CREATE, 

66 SFTP_FLAG_EXCL, 

67 SFTP_FLAG_READ, 

68 SFTP_FLAG_TRUNC, 

69 SFTP_FLAG_WRITE, 

70 SFTP_NO_SUCH_FILE, 

71 SFTP_OK, 

72 SFTP_OP_UNSUPPORTED, 

73 SFTP_PERMISSION_DENIED, 

74 BaseSFTP, 

75 Message, 

76 int64, 

77) 

78from paramiko.sftp_attr import SFTPAttributes 

79from paramiko.sftp_si import SFTPServerInterface 

80from paramiko.util import b 

81 

82# TODO: nuke or update w/ newer algs, see below 

83_hash_class = {"sha1": sha1, "md5": md5} 

84 

85 

86class SFTPServer(BaseSFTP, SubsystemHandler): 

87 """ 

88 Server-side SFTP subsystem support. Since this is a `.SubsystemHandler`, 

89 it can be (and is meant to be) set as the handler for ``"sftp"`` requests. 

90 Use `.Transport.set_subsystem_handler` to activate this class. 

91 """ 

92 

93 def __init__( 

94 self, 

95 channel, 

96 name, 

97 server, 

98 sftp_si=SFTPServerInterface, 

99 *args, 

100 **kwargs, 

101 ): 

102 """ 

103 The constructor for SFTPServer is meant to be called from within the 

104 `.Transport` as a subsystem handler. ``server`` and any additional 

105 parameters or keyword parameters are passed from the original call to 

106 `.Transport.set_subsystem_handler`. 

107 

108 :param .Channel channel: channel passed from the `.Transport`. 

109 :param str name: name of the requested subsystem. 

110 :param .ServerInterface server: 

111 the server object associated with this channel and subsystem 

112 :param sftp_si: 

113 a subclass of `.SFTPServerInterface` to use for handling individual 

114 requests. 

115 """ 

116 BaseSFTP.__init__(self) 

117 SubsystemHandler.__init__(self, channel, name, server) 

118 transport = channel.get_transport() 

119 self.logger = util.get_logger(transport.get_log_channel() + ".sftp") 

120 self.ultra_debug = transport.get_hexdump() 

121 self.next_handle = 1 

122 # map of handle-string to SFTPHandle for files & folders: 

123 self.file_table = {} 

124 self.folder_table = {} 

125 self.server = sftp_si(server, *args, **kwargs) 

126 

127 def _log(self, level, msg): 

128 if issubclass(type(msg), list): 

129 for m in msg: 

130 super()._log(level, "[chan " + self.sock.get_name() + "] " + m) 

131 else: 

132 super()._log(level, "[chan " + self.sock.get_name() + "] " + msg) 

133 

134 def start_subsystem(self, name, transport, channel): 

135 self.sock = channel 

136 self._log(DEBUG, "Started sftp server on channel {!r}".format(channel)) 

137 self._send_server_version() 

138 self.server.session_started() 

139 while True: 

140 try: 

141 t, data = self._read_packet() 

142 except EOFError: 

143 self._log(DEBUG, "EOF -- end of session") 

144 return 

145 except Exception as e: 

146 self._log(DEBUG, "Exception on channel: " + str(e)) 

147 self._log(DEBUG, util.tb_strings()) 

148 return 

149 msg = Message(data) 

150 request_number = msg.get_int() 

151 try: 

152 self._process(t, request_number, msg) 

153 except Exception as e: 

154 self._log(DEBUG, "Exception in server processing: " + str(e)) 

155 self._log(DEBUG, util.tb_strings()) 

156 # send some kind of failure message, at least 

157 try: 

158 self._send_status(request_number, SFTP_FAILURE) 

159 except: 

160 pass 

161 

162 def finish_subsystem(self): 

163 self.server.session_ended() 

164 super().finish_subsystem() 

165 # close any file handles that were left open 

166 # (so we can return them to the OS quickly) 

167 for f in self.file_table.values(): 

168 f.close() 

169 for f in self.folder_table.values(): 

170 f.close() 

171 self.file_table = {} 

172 self.folder_table = {} 

173 

174 @staticmethod 

175 def convert_errno(e): 

176 """ 

177 Convert an errno value (as from an ``OSError`` or ``IOError``) into a 

178 standard SFTP result code. This is a convenience function for trapping 

179 exceptions in server code and returning an appropriate result. 

180 

181 :param int e: an errno code, as from ``OSError.errno``. 

182 :return: an `int` SFTP error code like ``SFTP_NO_SUCH_FILE``. 

183 """ 

184 if e == errno.EACCES: 

185 # permission denied 

186 return SFTP_PERMISSION_DENIED 

187 elif (e == errno.ENOENT) or (e == errno.ENOTDIR): 

188 # no such file 

189 return SFTP_NO_SUCH_FILE 

190 else: 

191 return SFTP_FAILURE 

192 

193 @staticmethod 

194 def set_file_attr(filename, attr): 

195 """ 

196 Change a file's attributes on the local filesystem. The contents of 

197 ``attr`` are used to change the permissions, owner, group ownership, 

198 and/or modification & access time of the file, depending on which 

199 attributes are present in ``attr``. 

200 

201 This is meant to be a handy helper function for translating SFTP file 

202 requests into local file operations. 

203 

204 :param str filename: 

205 name of the file to alter (should usually be an absolute path). 

206 :param .SFTPAttributes attr: attributes to change. 

207 """ 

208 if sys.platform != "win32": 

209 # mode operations are meaningless on win32 

210 if attr._flags & attr.FLAG_PERMISSIONS: 

211 os.chmod(filename, attr.st_mode) 

212 if attr._flags & attr.FLAG_UIDGID: 

213 os.chown(filename, attr.st_uid, attr.st_gid) 

214 if attr._flags & attr.FLAG_AMTIME: 

215 os.utime(filename, (attr.st_atime, attr.st_mtime)) 

216 if attr._flags & attr.FLAG_SIZE: 

217 with open(filename, "w+") as f: 

218 f.truncate(attr.st_size) 

219 

220 # ...internals... 

221 

222 def _response(self, request_number, t, *args): 

223 msg = Message() 

224 msg.add_int(request_number) 

225 for item in args: 

226 # NOTE: this is a very silly tiny class used for SFTPFile mostly 

227 if isinstance(item, int64): 

228 msg.add_int64(item) 

229 elif isinstance(item, int): 

230 msg.add_int(item) 

231 elif isinstance(item, (str, bytes)): 

232 msg.add_string(item) 

233 elif type(item) is SFTPAttributes: 

234 item._pack(msg) 

235 else: 

236 raise Exception( 

237 "unknown type for {!r} type {!r}".format(item, type(item)) 

238 ) 

239 self._send_packet(t, msg) 

240 

241 def _send_handle_response(self, request_number, handle, folder=False): 

242 if not issubclass(type(handle), SFTPHandle): 

243 # must be error code 

244 self._send_status(request_number, handle) 

245 return 

246 handle._set_name(b("hx{:d}".format(self.next_handle))) 

247 self.next_handle += 1 

248 if folder: 

249 self.folder_table[handle._get_name()] = handle 

250 else: 

251 self.file_table[handle._get_name()] = handle 

252 self._response(request_number, CMD_HANDLE, handle._get_name()) 

253 

254 def _send_status(self, request_number, code, desc=None): 

255 if desc is None: 

256 try: 

257 desc = SFTP_DESC[code] 

258 except IndexError: 

259 desc = "Unknown" 

260 # some clients expect a "language" tag at the end 

261 # (but don't mind it being blank) 

262 self._response(request_number, CMD_STATUS, code, desc, "") 

263 

264 def _open_folder(self, request_number, path): 

265 resp = self.server.list_folder(path) 

266 if issubclass(type(resp), list): 

267 # got an actual list of filenames in the folder 

268 folder = SFTPHandle() 

269 folder._set_files(resp) 

270 self._send_handle_response(request_number, folder, True) 

271 return 

272 # must be an error code 

273 self._send_status(request_number, resp) 

274 

275 def _read_folder(self, request_number, folder): 

276 flist = folder._get_next_files() 

277 if len(flist) == 0: 

278 self._send_status(request_number, SFTP_EOF) 

279 return 

280 msg = Message() 

281 msg.add_int(request_number) 

282 msg.add_int(len(flist)) 

283 for attr in flist: 

284 msg.add_string(attr.filename) 

285 msg.add_string(attr) 

286 attr._pack(msg) 

287 self._send_packet(CMD_NAME, msg) 

288 

289 def _check_file(self, request_number, msg): 

290 # this extension actually comes from v6 protocol, but since it's an 

291 # extension, i feel like we can reasonably support it backported. 

292 # it's very useful for verifying uploaded files or checking for 

293 # rsync-like differences between local and remote files. 

294 handle = msg.get_binary() 

295 alg_list = msg.get_list() 

296 start = msg.get_int64() 

297 length = msg.get_int64() 

298 block_size = msg.get_int() 

299 if handle not in self.file_table: 

300 self._send_status( 

301 request_number, SFTP_BAD_MESSAGE, "Invalid handle" 

302 ) 

303 return 

304 f = self.file_table[handle] 

305 for x in alg_list: 

306 # TODO: this only contains sha1 and md5 so uh, is this extension 

307 # actually supported anymore? do we need to update the map for 

308 # newer algos instead? other? 

309 if x in _hash_class: 

310 algname = x 

311 alg = _hash_class[x] 

312 break 

313 else: 

314 self._send_status( 

315 request_number, SFTP_FAILURE, "No supported hash types found" 

316 ) 

317 return 

318 if length == 0: 

319 st = f.stat() 

320 if not issubclass(type(st), SFTPAttributes): 

321 self._send_status(request_number, st, "Unable to stat file") 

322 return 

323 length = st.st_size - start 

324 if block_size == 0: 

325 block_size = length 

326 if block_size < 256: 

327 self._send_status( 

328 request_number, SFTP_FAILURE, "Block size too small" 

329 ) 

330 return 

331 

332 sum_out = bytes() 

333 offset = start 

334 while offset < start + length: 

335 blocklen = min(block_size, start + length - offset) 

336 # don't try to read more than about 64KB at a time 

337 chunklen = min(blocklen, 65536) 

338 count = 0 

339 hash_obj = alg() 

340 while count < blocklen: 

341 data = f.read(offset, chunklen) 

342 if not isinstance(data, bytes): 

343 self._send_status( 

344 request_number, data, "Unable to hash file" 

345 ) 

346 return 

347 hash_obj.update(data) 

348 count += len(data) 

349 offset += count 

350 sum_out += hash_obj.digest() 

351 

352 msg = Message() 

353 msg.add_int(request_number) 

354 msg.add_string("check-file") 

355 msg.add_string(algname) 

356 msg.add_bytes(sum_out) 

357 self._send_packet(CMD_EXTENDED_REPLY, msg) 

358 

359 def _convert_pflags(self, pflags): 

360 """convert SFTP-style open() flags to Python's os.open() flags""" 

361 if (pflags & SFTP_FLAG_READ) and (pflags & SFTP_FLAG_WRITE): 

362 flags = os.O_RDWR 

363 elif pflags & SFTP_FLAG_WRITE: 

364 flags = os.O_WRONLY 

365 else: 

366 flags = os.O_RDONLY 

367 if pflags & SFTP_FLAG_APPEND: 

368 flags |= os.O_APPEND 

369 if pflags & SFTP_FLAG_CREATE: 

370 flags |= os.O_CREAT 

371 if pflags & SFTP_FLAG_TRUNC: 

372 flags |= os.O_TRUNC 

373 if pflags & SFTP_FLAG_EXCL: 

374 flags |= os.O_EXCL 

375 return flags 

376 

377 def _process(self, t, request_number, msg): 

378 self._log(DEBUG, "Request: {}".format(CMD_NAMES[t])) 

379 if t == CMD_OPEN: 

380 path = msg.get_text() 

381 flags = self._convert_pflags(msg.get_int()) 

382 attr = SFTPAttributes._from_msg(msg) 

383 self._send_handle_response( 

384 request_number, self.server.open(path, flags, attr) 

385 ) 

386 elif t == CMD_CLOSE: 

387 handle = msg.get_binary() 

388 if handle in self.folder_table: 

389 del self.folder_table[handle] 

390 self._send_status(request_number, SFTP_OK) 

391 return 

392 if handle in self.file_table: 

393 self.file_table[handle].close() 

394 del self.file_table[handle] 

395 self._send_status(request_number, SFTP_OK) 

396 return 

397 self._send_status( 

398 request_number, SFTP_BAD_MESSAGE, "Invalid handle" 

399 ) 

400 elif t == CMD_READ: 

401 handle = msg.get_binary() 

402 offset = msg.get_int64() 

403 length = msg.get_int() 

404 if handle not in self.file_table: 

405 self._send_status( 

406 request_number, SFTP_BAD_MESSAGE, "Invalid handle" 

407 ) 

408 return 

409 data = self.file_table[handle].read(offset, length) 

410 if isinstance(data, (bytes, str)): 

411 if len(data) == 0: 

412 self._send_status(request_number, SFTP_EOF) 

413 else: 

414 self._response(request_number, CMD_DATA, data) 

415 else: 

416 self._send_status(request_number, data) 

417 elif t == CMD_WRITE: 

418 handle = msg.get_binary() 

419 offset = msg.get_int64() 

420 data = msg.get_binary() 

421 if handle not in self.file_table: 

422 self._send_status( 

423 request_number, SFTP_BAD_MESSAGE, "Invalid handle" 

424 ) 

425 return 

426 self._send_status( 

427 request_number, self.file_table[handle].write(offset, data) 

428 ) 

429 elif t == CMD_REMOVE: 

430 path = msg.get_text() 

431 self._send_status(request_number, self.server.remove(path)) 

432 elif t == CMD_RENAME: 

433 oldpath = msg.get_text() 

434 newpath = msg.get_text() 

435 self._send_status( 

436 request_number, self.server.rename(oldpath, newpath) 

437 ) 

438 elif t == CMD_MKDIR: 

439 path = msg.get_text() 

440 attr = SFTPAttributes._from_msg(msg) 

441 self._send_status(request_number, self.server.mkdir(path, attr)) 

442 elif t == CMD_RMDIR: 

443 path = msg.get_text() 

444 self._send_status(request_number, self.server.rmdir(path)) 

445 elif t == CMD_OPENDIR: 

446 path = msg.get_text() 

447 self._open_folder(request_number, path) 

448 return 

449 elif t == CMD_READDIR: 

450 handle = msg.get_binary() 

451 if handle not in self.folder_table: 

452 self._send_status( 

453 request_number, SFTP_BAD_MESSAGE, "Invalid handle" 

454 ) 

455 return 

456 folder = self.folder_table[handle] 

457 self._read_folder(request_number, folder) 

458 elif t == CMD_STAT: 

459 path = msg.get_text() 

460 resp = self.server.stat(path) 

461 if issubclass(type(resp), SFTPAttributes): 

462 self._response(request_number, CMD_ATTRS, resp) 

463 else: 

464 self._send_status(request_number, resp) 

465 elif t == CMD_LSTAT: 

466 path = msg.get_text() 

467 resp = self.server.lstat(path) 

468 if issubclass(type(resp), SFTPAttributes): 

469 self._response(request_number, CMD_ATTRS, resp) 

470 else: 

471 self._send_status(request_number, resp) 

472 elif t == CMD_FSTAT: 

473 handle = msg.get_binary() 

474 if handle not in self.file_table: 

475 self._send_status( 

476 request_number, SFTP_BAD_MESSAGE, "Invalid handle" 

477 ) 

478 return 

479 resp = self.file_table[handle].stat() 

480 if issubclass(type(resp), SFTPAttributes): 

481 self._response(request_number, CMD_ATTRS, resp) 

482 else: 

483 self._send_status(request_number, resp) 

484 elif t == CMD_SETSTAT: 

485 path = msg.get_text() 

486 attr = SFTPAttributes._from_msg(msg) 

487 self._send_status(request_number, self.server.chattr(path, attr)) 

488 elif t == CMD_FSETSTAT: 

489 handle = msg.get_binary() 

490 attr = SFTPAttributes._from_msg(msg) 

491 if handle not in self.file_table: 

492 self._response( 

493 request_number, SFTP_BAD_MESSAGE, "Invalid handle" 

494 ) 

495 return 

496 self._send_status( 

497 request_number, self.file_table[handle].chattr(attr) 

498 ) 

499 elif t == CMD_READLINK: 

500 path = msg.get_text() 

501 resp = self.server.readlink(path) 

502 if isinstance(resp, (bytes, str)): 

503 self._response( 

504 request_number, CMD_NAME, 1, resp, "", SFTPAttributes() 

505 ) 

506 else: 

507 self._send_status(request_number, resp) 

508 elif t == CMD_SYMLINK: 

509 # the sftp 2 draft is incorrect here! 

510 # path always follows target_path 

511 target_path = msg.get_text() 

512 path = msg.get_text() 

513 self._send_status( 

514 request_number, self.server.symlink(target_path, path) 

515 ) 

516 elif t == CMD_REALPATH: 

517 path = msg.get_text() 

518 rpath = self.server.canonicalize(path) 

519 self._response( 

520 request_number, CMD_NAME, 1, rpath, "", SFTPAttributes() 

521 ) 

522 elif t == CMD_EXTENDED: 

523 tag = msg.get_text() 

524 if tag == "check-file": 

525 self._check_file(request_number, msg) 

526 elif tag == "posix-rename@openssh.com": 

527 oldpath = msg.get_text() 

528 newpath = msg.get_text() 

529 self._send_status( 

530 request_number, self.server.posix_rename(oldpath, newpath) 

531 ) 

532 else: 

533 self._send_status(request_number, SFTP_OP_UNSUPPORTED) 

534 else: 

535 self._send_status(request_number, SFTP_OP_UNSUPPORTED) 

536 

537 

538from paramiko.sftp_handle import SFTPHandle