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 os 

24import errno 

25import sys 

26from hashlib import md5, sha1 

27 

28from paramiko import util 

29from paramiko.sftp import ( 

30 BaseSFTP, 

31 Message, 

32 SFTP_FAILURE, 

33 SFTP_PERMISSION_DENIED, 

34 SFTP_NO_SUCH_FILE, 

35 int64, 

36) 

37from paramiko.sftp_si import SFTPServerInterface 

38from paramiko.sftp_attr import SFTPAttributes 

39from paramiko.common import DEBUG 

40from paramiko.server import SubsystemHandler 

41from paramiko.util import b 

42 

43 

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

45from paramiko.sftp import ( 

46 CMD_HANDLE, 

47 SFTP_DESC, 

48 CMD_STATUS, 

49 SFTP_EOF, 

50 CMD_NAME, 

51 SFTP_BAD_MESSAGE, 

52 CMD_EXTENDED_REPLY, 

53 SFTP_FLAG_READ, 

54 SFTP_FLAG_WRITE, 

55 SFTP_FLAG_APPEND, 

56 SFTP_FLAG_CREATE, 

57 SFTP_FLAG_TRUNC, 

58 SFTP_FLAG_EXCL, 

59 CMD_NAMES, 

60 CMD_OPEN, 

61 CMD_CLOSE, 

62 SFTP_OK, 

63 CMD_READ, 

64 CMD_DATA, 

65 CMD_WRITE, 

66 CMD_REMOVE, 

67 CMD_RENAME, 

68 CMD_MKDIR, 

69 CMD_RMDIR, 

70 CMD_OPENDIR, 

71 CMD_READDIR, 

72 CMD_STAT, 

73 CMD_ATTRS, 

74 CMD_LSTAT, 

75 CMD_FSTAT, 

76 CMD_SETSTAT, 

77 CMD_FSETSTAT, 

78 CMD_READLINK, 

79 CMD_SYMLINK, 

80 CMD_REALPATH, 

81 CMD_EXTENDED, 

82 SFTP_OP_UNSUPPORTED, 

83) 

84 

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

86 

87 

88class SFTPServer(BaseSFTP, SubsystemHandler): 

89 """ 

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

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

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

93 """ 

94 

95 def __init__( 

96 self, 

97 channel, 

98 name, 

99 server, 

100 sftp_si=SFTPServerInterface, 

101 *args, 

102 **kwargs 

103 ): 

104 """ 

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

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

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

108 `.Transport.set_subsystem_handler`. 

109 

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

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

112 :param .ServerInterface server: 

113 the server object associated with this channel and subsystem 

114 :param sftp_si: 

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

116 requests. 

117 """ 

118 BaseSFTP.__init__(self) 

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

120 transport = channel.get_transport() 

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

122 self.ultra_debug = transport.get_hexdump() 

123 self.next_handle = 1 

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

125 self.file_table = {} 

126 self.folder_table = {} 

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

128 

129 def _log(self, level, msg): 

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

131 for m in msg: 

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

133 else: 

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

135 

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

137 self.sock = channel 

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

139 self._send_server_version() 

140 self.server.session_started() 

141 while True: 

142 try: 

143 t, data = self._read_packet() 

144 except EOFError: 

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

146 return 

147 except Exception as e: 

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

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

150 return 

151 msg = Message(data) 

152 request_number = msg.get_int() 

153 try: 

154 self._process(t, request_number, msg) 

155 except Exception as e: 

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

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

158 # send some kind of failure message, at least 

159 try: 

160 self._send_status(request_number, SFTP_FAILURE) 

161 except: 

162 pass 

163 

164 def finish_subsystem(self): 

165 self.server.session_ended() 

166 super().finish_subsystem() 

167 # close any file handles that were left open 

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

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

170 f.close() 

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

172 f.close() 

173 self.file_table = {} 

174 self.folder_table = {} 

175 

176 @staticmethod 

177 def convert_errno(e): 

178 """ 

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

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

181 exceptions in server code and returning an appropriate result. 

182 

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

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

185 """ 

186 if e == errno.EACCES: 

187 # permission denied 

188 return SFTP_PERMISSION_DENIED 

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

190 # no such file 

191 return SFTP_NO_SUCH_FILE 

192 else: 

193 return SFTP_FAILURE 

194 

195 @staticmethod 

196 def set_file_attr(filename, attr): 

197 """ 

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

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

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

201 attributes are present in ``attr``. 

202 

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

204 requests into local file operations. 

205 

206 :param str filename: 

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

208 :param .SFTPAttributes attr: attributes to change. 

209 """ 

210 if sys.platform != "win32": 

211 # mode operations are meaningless on win32 

212 if attr._flags & attr.FLAG_PERMISSIONS: 

213 os.chmod(filename, attr.st_mode) 

214 if attr._flags & attr.FLAG_UIDGID: 

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

216 if attr._flags & attr.FLAG_AMTIME: 

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

218 if attr._flags & attr.FLAG_SIZE: 

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

220 f.truncate(attr.st_size) 

221 

222 # ...internals... 

223 

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

225 msg = Message() 

226 msg.add_int(request_number) 

227 for item in args: 

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

229 if isinstance(item, int64): 

230 msg.add_int64(item) 

231 elif isinstance(item, int): 

232 msg.add_int(item) 

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

234 msg.add_string(item) 

235 elif type(item) is SFTPAttributes: 

236 item._pack(msg) 

237 else: 

238 raise Exception( 

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

240 ) 

241 self._send_packet(t, msg) 

242 

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

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

245 # must be error code 

246 self._send_status(request_number, handle) 

247 return 

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

249 self.next_handle += 1 

250 if folder: 

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

252 else: 

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

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

255 

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

257 if desc is None: 

258 try: 

259 desc = SFTP_DESC[code] 

260 except IndexError: 

261 desc = "Unknown" 

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

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

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

265 

266 def _open_folder(self, request_number, path): 

267 resp = self.server.list_folder(path) 

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

269 # got an actual list of filenames in the folder 

270 folder = SFTPHandle() 

271 folder._set_files(resp) 

272 self._send_handle_response(request_number, folder, True) 

273 return 

274 # must be an error code 

275 self._send_status(request_number, resp) 

276 

277 def _read_folder(self, request_number, folder): 

278 flist = folder._get_next_files() 

279 if len(flist) == 0: 

280 self._send_status(request_number, SFTP_EOF) 

281 return 

282 msg = Message() 

283 msg.add_int(request_number) 

284 msg.add_int(len(flist)) 

285 for attr in flist: 

286 msg.add_string(attr.filename) 

287 msg.add_string(attr) 

288 attr._pack(msg) 

289 self._send_packet(CMD_NAME, msg) 

290 

291 def _check_file(self, request_number, msg): 

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

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

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

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

296 handle = msg.get_binary() 

297 alg_list = msg.get_list() 

298 start = msg.get_int64() 

299 length = msg.get_int64() 

300 block_size = msg.get_int() 

301 if handle not in self.file_table: 

302 self._send_status( 

303 request_number, SFTP_BAD_MESSAGE, "Invalid handle" 

304 ) 

305 return 

306 f = self.file_table[handle] 

307 for x in alg_list: 

308 if x in _hash_class: 

309 algname = x 

310 alg = _hash_class[x] 

311 break 

312 else: 

313 self._send_status( 

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

315 ) 

316 return 

317 if length == 0: 

318 st = f.stat() 

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

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

321 return 

322 length = st.st_size - start 

323 if block_size == 0: 

324 block_size = length 

325 if block_size < 256: 

326 self._send_status( 

327 request_number, SFTP_FAILURE, "Block size too small" 

328 ) 

329 return 

330 

331 sum_out = bytes() 

332 offset = start 

333 while offset < start + length: 

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

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

336 chunklen = min(blocklen, 65536) 

337 count = 0 

338 hash_obj = alg() 

339 while count < blocklen: 

340 data = f.read(offset, chunklen) 

341 if not isinstance(data, bytes): 

342 self._send_status( 

343 request_number, data, "Unable to hash file" 

344 ) 

345 return 

346 hash_obj.update(data) 

347 count += len(data) 

348 offset += count 

349 sum_out += hash_obj.digest() 

350 

351 msg = Message() 

352 msg.add_int(request_number) 

353 msg.add_string("check-file") 

354 msg.add_string(algname) 

355 msg.add_bytes(sum_out) 

356 self._send_packet(CMD_EXTENDED_REPLY, msg) 

357 

358 def _convert_pflags(self, pflags): 

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

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

361 flags = os.O_RDWR 

362 elif pflags & SFTP_FLAG_WRITE: 

363 flags = os.O_WRONLY 

364 else: 

365 flags = os.O_RDONLY 

366 if pflags & SFTP_FLAG_APPEND: 

367 flags |= os.O_APPEND 

368 if pflags & SFTP_FLAG_CREATE: 

369 flags |= os.O_CREAT 

370 if pflags & SFTP_FLAG_TRUNC: 

371 flags |= os.O_TRUNC 

372 if pflags & SFTP_FLAG_EXCL: 

373 flags |= os.O_EXCL 

374 return flags 

375 

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

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

378 if t == CMD_OPEN: 

379 path = msg.get_text() 

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

381 attr = SFTPAttributes._from_msg(msg) 

382 self._send_handle_response( 

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

384 ) 

385 elif t == CMD_CLOSE: 

386 handle = msg.get_binary() 

387 if handle in self.folder_table: 

388 del self.folder_table[handle] 

389 self._send_status(request_number, SFTP_OK) 

390 return 

391 if handle in self.file_table: 

392 self.file_table[handle].close() 

393 del self.file_table[handle] 

394 self._send_status(request_number, SFTP_OK) 

395 return 

396 self._send_status( 

397 request_number, SFTP_BAD_MESSAGE, "Invalid handle" 

398 ) 

399 elif t == CMD_READ: 

400 handle = msg.get_binary() 

401 offset = msg.get_int64() 

402 length = msg.get_int() 

403 if handle not in self.file_table: 

404 self._send_status( 

405 request_number, SFTP_BAD_MESSAGE, "Invalid handle" 

406 ) 

407 return 

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

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

410 if len(data) == 0: 

411 self._send_status(request_number, SFTP_EOF) 

412 else: 

413 self._response(request_number, CMD_DATA, data) 

414 else: 

415 self._send_status(request_number, data) 

416 elif t == CMD_WRITE: 

417 handle = msg.get_binary() 

418 offset = msg.get_int64() 

419 data = msg.get_binary() 

420 if handle not in self.file_table: 

421 self._send_status( 

422 request_number, SFTP_BAD_MESSAGE, "Invalid handle" 

423 ) 

424 return 

425 self._send_status( 

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

427 ) 

428 elif t == CMD_REMOVE: 

429 path = msg.get_text() 

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

431 elif t == CMD_RENAME: 

432 oldpath = msg.get_text() 

433 newpath = msg.get_text() 

434 self._send_status( 

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

436 ) 

437 elif t == CMD_MKDIR: 

438 path = msg.get_text() 

439 attr = SFTPAttributes._from_msg(msg) 

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

441 elif t == CMD_RMDIR: 

442 path = msg.get_text() 

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

444 elif t == CMD_OPENDIR: 

445 path = msg.get_text() 

446 self._open_folder(request_number, path) 

447 return 

448 elif t == CMD_READDIR: 

449 handle = msg.get_binary() 

450 if handle not in self.folder_table: 

451 self._send_status( 

452 request_number, SFTP_BAD_MESSAGE, "Invalid handle" 

453 ) 

454 return 

455 folder = self.folder_table[handle] 

456 self._read_folder(request_number, folder) 

457 elif t == CMD_STAT: 

458 path = msg.get_text() 

459 resp = self.server.stat(path) 

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

461 self._response(request_number, CMD_ATTRS, resp) 

462 else: 

463 self._send_status(request_number, resp) 

464 elif t == CMD_LSTAT: 

465 path = msg.get_text() 

466 resp = self.server.lstat(path) 

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

468 self._response(request_number, CMD_ATTRS, resp) 

469 else: 

470 self._send_status(request_number, resp) 

471 elif t == CMD_FSTAT: 

472 handle = msg.get_binary() 

473 if handle not in self.file_table: 

474 self._send_status( 

475 request_number, SFTP_BAD_MESSAGE, "Invalid handle" 

476 ) 

477 return 

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

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

480 self._response(request_number, CMD_ATTRS, resp) 

481 else: 

482 self._send_status(request_number, resp) 

483 elif t == CMD_SETSTAT: 

484 path = msg.get_text() 

485 attr = SFTPAttributes._from_msg(msg) 

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

487 elif t == CMD_FSETSTAT: 

488 handle = msg.get_binary() 

489 attr = SFTPAttributes._from_msg(msg) 

490 if handle not in self.file_table: 

491 self._response( 

492 request_number, SFTP_BAD_MESSAGE, "Invalid handle" 

493 ) 

494 return 

495 self._send_status( 

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

497 ) 

498 elif t == CMD_READLINK: 

499 path = msg.get_text() 

500 resp = self.server.readlink(path) 

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

502 self._response( 

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

504 ) 

505 else: 

506 self._send_status(request_number, resp) 

507 elif t == CMD_SYMLINK: 

508 # the sftp 2 draft is incorrect here! 

509 # path always follows target_path 

510 target_path = msg.get_text() 

511 path = msg.get_text() 

512 self._send_status( 

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

514 ) 

515 elif t == CMD_REALPATH: 

516 path = msg.get_text() 

517 rpath = self.server.canonicalize(path) 

518 self._response( 

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

520 ) 

521 elif t == CMD_EXTENDED: 

522 tag = msg.get_text() 

523 if tag == "check-file": 

524 self._check_file(request_number, msg) 

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

526 oldpath = msg.get_text() 

527 newpath = msg.get_text() 

528 self._send_status( 

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

530 ) 

531 else: 

532 self._send_status(request_number, SFTP_OP_UNSUPPORTED) 

533 else: 

534 self._send_status(request_number, SFTP_OP_UNSUPPORTED) 

535 

536 

537from paramiko.sftp_handle import SFTPHandle