@@ -944,6 +944,355 @@ def _copy_fileobj_to_path(fileobj, path, overwrite=False):
944944 pass
945945 shutil .copyfileobj (fileobj , out )
946946
947+ # TFTP opcodes
948+ OP_RRQ = 1
949+ OP_WRQ = 2
950+ OP_DATA = 3
951+ OP_ACK = 4
952+ OP_ERROR = 5
953+
954+ BLOCK_SIZE = 512
955+
956+
957+ class TFTPError (Exception ):
958+ pass
959+
960+
961+ def _make_rrq (filename , mode = b"octet" ):
962+ # RRQ: 2 bytes opcode, filename, 0, mode, 0
963+ return struct .pack ("!H" , OP_RRQ ) + _to_bytes (filename ) + b"\x00 " + _to_bytes (mode ) + b"\x00 "
964+
965+
966+ def _make_wrq (filename , mode = b"octet" ):
967+ return struct .pack ("!H" , OP_WRQ ) + _to_bytes (filename ) + b"\x00 " + _to_bytes (mode ) + b"\x00 "
968+
969+
970+ def _make_data (blockno , payload ):
971+ return struct .pack ("!HH" , OP_DATA , blockno ) + payload
972+
973+
974+ def _make_ack (blockno ):
975+ return struct .pack ("!HH" , OP_ACK , blockno )
976+
977+
978+ def _parse_packet (pkt ):
979+ if len (pkt ) < 2 :
980+ raise TFTPError ("Short packet" )
981+ op = struct .unpack ("!H" , pkt [:2 ])[0 ]
982+ return op
983+
984+
985+ def _parse_ack (pkt ):
986+ if len (pkt ) < 4 :
987+ raise TFTPError ("Short ACK" )
988+ op , blockno = struct .unpack ("!HH" , pkt [:4 ])
989+ if op != OP_ACK :
990+ raise TFTPError ("Expected ACK, got opcode %d" % op )
991+ return blockno
992+
993+
994+ def _parse_data (pkt ):
995+ if len (pkt ) < 4 :
996+ raise TFTPError ("Short DATA" )
997+ op , blockno = struct .unpack ("!HH" , pkt [:4 ])
998+ if op != OP_DATA :
999+ raise TFTPError ("Expected DATA, got opcode %d" % op )
1000+ return blockno , pkt [4 :]
1001+
1002+
1003+ def _parse_error (pkt ):
1004+ # ERROR: opcode(2) + errcode(2) + errmsg + 0
1005+ if len (pkt ) < 4 :
1006+ raise TFTPError ("Short ERROR" )
1007+ op , errcode = struct .unpack ("!HH" , pkt [:4 ])
1008+ if op != OP_ERROR :
1009+ raise TFTPError ("Not an ERROR packet" )
1010+ msg = pkt [4 :]
1011+ if b"\x00 " in msg :
1012+ msg = msg .split (b"\x00 " , 1 )[0 ]
1013+ try :
1014+ msg = msg .decode ("utf-8" , "replace" )
1015+ except Exception :
1016+ msg = repr (msg )
1017+ raise TFTPError ("TFTP ERROR %d: %s" % (errcode , msg ))
1018+
1019+
1020+ def _mk_sock (proxy , timeout ):
1021+ """
1022+ proxy: dict or None
1023+ If dict, expected keys:
1024+ host, port, username(optional), password(optional)
1025+ """
1026+ s = socks .socksocket (socket .AF_INET , socket .SOCK_DGRAM )
1027+ s .settimeout (timeout )
1028+
1029+ if proxy :
1030+ # Only SOCKS5 is realistic for UDP.
1031+ s .set_proxy (
1032+ proxy_type = socks .SOCKS5 ,
1033+ addr = proxy ["host" ],
1034+ port = int (proxy ["port" ]),
1035+ username = proxy .get ("username" ),
1036+ password = proxy .get ("password" ),
1037+ rdns = True ,
1038+ )
1039+ return s
1040+
1041+
1042+ def tftp_upload (server_host , remote_filename , fileobj ,
1043+ server_port = 69 , mode = "octet" ,
1044+ proxy = None , timeout = 5.0 , retries = 5 ):
1045+ """
1046+ Upload to a TFTP server using a file object opened for reading (binary).
1047+
1048+ Args:
1049+ server_host (str): TFTP server hostname/IP
1050+ remote_filename (str): destination filename on server
1051+ fileobj: readable file-like object (must return bytes)
1052+ proxy (dict|None): {"host": "...", "port": 1080, "username": "...", "password": "..."}
1053+ timeout (float): socket timeout seconds
1054+ retries (int): retransmit attempts per block
1055+
1056+ Returns:
1057+ None (raises TFTPError on failure)
1058+ """
1059+ sock = _mk_sock (proxy , timeout )
1060+
1061+ try :
1062+ # Send WRQ to well-known port
1063+ wrq = _make_wrq (remote_filename , mode = _to_bytes (mode ))
1064+ sock .sendto (wrq , (server_host , int (server_port )))
1065+
1066+ # Server should respond from a new ephemeral port with ACK(0)
1067+ for attempt in range (retries ):
1068+ try :
1069+ pkt , addr = sock .recvfrom (4 + 128 )
1070+ op = _parse_packet (pkt )
1071+ if op == OP_ERROR :
1072+ _parse_error (pkt )
1073+ if op != OP_ACK :
1074+ raise TFTPError ("Expected ACK(0), got opcode %d" % op )
1075+ ack_block = _parse_ack (pkt )
1076+ if ack_block != 0 :
1077+ raise TFTPError ("Expected ACK block 0, got %d" % ack_block )
1078+ server_tid = addr # (ip, port) for rest of transfer
1079+ break
1080+ except socket .timeout :
1081+ # Retransmit WRQ
1082+ sock .sendto (wrq , (server_host , int (server_port )))
1083+ else :
1084+ raise TFTPError ("Timeout waiting for ACK(0)" )
1085+
1086+ # Send DATA blocks starting at 1
1087+ blockno = 1
1088+ while True :
1089+ data = fileobj .read (BLOCK_SIZE )
1090+ if data is None :
1091+ data = b""
1092+ if not isinstance (data , (bytes , bytearray )):
1093+ raise TFTPError ("fileobj.read() must return bytes" )
1094+
1095+ data_pkt = _make_data (blockno , data )
1096+
1097+ # retransmit loop for this block
1098+ for attempt in range (retries ):
1099+ sock .sendto (data_pkt , server_tid )
1100+ try :
1101+ pkt , addr = sock .recvfrom (4 + 128 )
1102+ # TID check: ignore packets from other ports/hosts
1103+ if addr != server_tid :
1104+ continue
1105+ op = _parse_packet (pkt )
1106+ if op == OP_ERROR :
1107+ _parse_error (pkt )
1108+ ackb = _parse_ack (pkt )
1109+ if ackb == blockno :
1110+ break
1111+ except socket .timeout :
1112+ continue
1113+ else :
1114+ raise TFTPError ("Timeout waiting for ACK(%d)" % blockno )
1115+
1116+ # Last block is < 512 bytes (including 0 bytes if exact multiple requires final 0-length block)
1117+ if len (data ) < BLOCK_SIZE :
1118+ return
1119+
1120+ blockno = (blockno + 1 ) & 0xFFFF
1121+ if blockno == 0 :
1122+ # TFTP block rolls over after 65535; handling wrap robustly is more involved.
1123+ raise TFTPError ("Block number rollover not supported in this simple implementation." )
1124+
1125+ finally :
1126+ try :
1127+ sock .close ()
1128+ except Exception :
1129+ pass
1130+
1131+
1132+ def tftp_download (server_host , remote_filename ,
1133+ server_port = 69 , mode = "octet" ,
1134+ proxy = None , timeout = 5.0 , retries = 5 ):
1135+ """
1136+ Download from a TFTP server and return a file-like object containing bytes.
1137+
1138+ Args:
1139+ server_host (str): TFTP server hostname/IP
1140+ remote_filename (str): filename on server
1141+ proxy (dict|None): {"host": "...", "port": 1080, "username": "...", "password": "..."}
1142+ timeout (float): socket timeout seconds
1143+ retries (int): retransmit attempts
1144+
1145+ Returns:
1146+ io.BytesIO: file-like object positioned at start
1147+ """
1148+ sock = _mk_sock (proxy , timeout )
1149+ out = MkTempFile ()
1150+
1151+ rrq = _make_rrq (remote_filename , mode = _to_bytes (mode ))
1152+
1153+ try :
1154+ # Send RRQ to well-known port
1155+ sock .sendto (rrq , (server_host , int (server_port )))
1156+
1157+ expected = 1
1158+ server_tid = None
1159+ last_ack = 0
1160+
1161+ while True :
1162+ for attempt in range (retries ):
1163+ try :
1164+ pkt , addr = sock .recvfrom (4 + BLOCK_SIZE + 128 )
1165+ op = _parse_packet (pkt )
1166+
1167+ if op == OP_ERROR :
1168+ _parse_error (pkt )
1169+
1170+ if op != OP_DATA :
1171+ raise TFTPError ("Expected DATA, got opcode %d" % op )
1172+
1173+ blockno , payload = _parse_data (pkt )
1174+
1175+ # First DATA defines server TID
1176+ if server_tid is None :
1177+ server_tid = addr
1178+
1179+ # Ignore packets from unexpected TID
1180+ if addr != server_tid :
1181+ continue
1182+
1183+ if blockno == expected :
1184+ out .write (payload )
1185+ ack = _make_ack (blockno )
1186+ sock .sendto (ack , server_tid )
1187+ last_ack = blockno
1188+
1189+ # end condition
1190+ if len (payload ) < BLOCK_SIZE :
1191+ out .seek (0 )
1192+ return out
1193+
1194+ expected = (expected + 1 ) & 0xFFFF
1195+ if expected == 0 :
1196+ raise TFTPError ("Block number rollover not supported in this simple implementation." )
1197+ break
1198+
1199+ elif blockno == last_ack :
1200+ # Duplicate DATA; re-ACK to help server
1201+ sock .sendto (_make_ack (blockno ), server_tid )
1202+ break
1203+
1204+ else :
1205+ # Out-of-order: ACK last good block
1206+ sock .sendto (_make_ack (last_ack ), server_tid )
1207+ break
1208+
1209+ except socket .timeout :
1210+ # On timeout, retransmit RRQ initially, else retransmit last ACK
1211+ if server_tid is None :
1212+ sock .sendto (rrq , (server_host , int (server_port )))
1213+ else :
1214+ sock .sendto (_make_ack (last_ack ), server_tid )
1215+ else :
1216+ raise TFTPError ("Timeout receiving DATA block %d" % expected )
1217+
1218+ finally :
1219+ try :
1220+ sock .close ()
1221+ except Exception :
1222+ pass
1223+
1224+ def download_file_from_tftp_file (url , resumefile = None , timeout = 60 , returnstats = False ):
1225+ p = urlparse (url )
1226+ if p .scheme != "tftp" :
1227+ return False
1228+
1229+ host = p .hostname
1230+ port = p .port or 69
1231+ user = p .username
1232+ pw = p .password
1233+ path = p .path or "/"
1234+ file_dir = os .path .dirname (path )
1235+ start_time = time .time ()
1236+ socket .setdefaulttimeout (float (timeout ))
1237+ try :
1238+ bio = tftp_download (host , p .path , port , timeout = float (timeout ))
1239+ fulldatasize = bio .tell ()
1240+ bio .seek (0 , 0 )
1241+ end_time = time .time ()
1242+ total_time = end_time - start_time
1243+ if (returnstats ):
1244+ returnval = {'Type' : "Buffer" , 'Buffer' : bio , 'Contentsize' : fulldatasize , 'ContentsizeAlt' : {'IEC' : get_readable_size (fulldatasize , 2 , "IEC" ), 'SI' : get_readable_size (fulldatasize , 2 , "SI" )}, 'Headers' : None , 'Version' : None , 'Method' : None , 'HeadersSent' : None , 'URL' : url , 'Code' : None , 'RequestTime' : {'StartTime' : start_time , 'EndTime' : end_time , 'TotalTime' : total_time }, 'FTPLib' : 'pyftp' }
1245+ else :
1246+ return bio
1247+ except Exception :
1248+ try :
1249+ ftp .close ()
1250+ except Exception :
1251+ pass
1252+ return False
1253+
1254+ def download_file_from_tftp_string (url , resumefile = None , timeout = 60 , returnstats = False ):
1255+ fp = download_file_from_tftp_file (url , resumefile , timeout , returnstats )
1256+ return fp .read () if fp else False
1257+
1258+ def upload_file_to_tftp_file (fileobj , url , timeout = 60 ):
1259+ p = urlparse (url )
1260+ if p .scheme != "tftp" :
1261+ return False
1262+
1263+ socket .setdefaulttimeout (float (timeout ))
1264+ host = p .hostname
1265+ port = p .port or 21
1266+ user = p .username
1267+ pw = p .password
1268+ path = p .path or "/"
1269+ file_dir = os .path .dirname (path )
1270+ fname = os .path .basename (path ) or "upload.bin"
1271+
1272+ try :
1273+ try :
1274+ fileobj .seek (0 , 0 )
1275+ except Exception :
1276+ pass
1277+ tftp_upload (host , p .path , fileobj , port , timeout = float (timeout ))
1278+ try :
1279+ fileobj .seek (0 , 0 )
1280+ except Exception :
1281+ pass
1282+
1283+ return fileobj
1284+ except Exception :
1285+ return False
1286+
1287+ def upload_file_to_tftp_string (data , url , timeout = 60 ):
1288+ bio = MkTempFile (_to_bytes (data ))
1289+ out = upload_file_to_tftp_file (bio , url , timeout )
1290+ try :
1291+ bio .close ()
1292+ except Exception :
1293+ pass
1294+ return out
1295+
9471296# --------------------------
9481297# FTP helpers
9491298# --------------------------
@@ -5114,6 +5463,8 @@ def download_file_from_internet_file(url, **kwargs):
51145463 return download_file_from_http_file (url , ** kwargs )
51155464 if p .scheme in ("ftp" , "ftps" ):
51165465 return download_file_from_ftp_file (url , ** kwargs )
5466+ if p .scheme in ("tftp" , ):
5467+ return download_file_from_tftp_file (url , ** kwargs )
51175468 if p .scheme in ("sftp" , "scp" ):
51185469 if __use_pysftp__ and havepysftp :
51195470 return download_file_from_pysftp_file (url , ** kwargs )
@@ -5910,10 +6261,14 @@ def upload_file_to_internet_file(fileobj, url):
59106261 return _serve_file_over_http (fileobj , url )
59116262 if p .scheme in ("ftp" , "ftps" ):
59126263 return upload_file_to_ftp_file (fileobj , url )
6264+ if p .scheme in ("tftp" , ):
6265+ return upload_file_to_tftp_file (fileobj , url )
59136266 if p .scheme in ("sftp" , "scp" ):
59146267 if __use_pysftp__ and havepysftp :
59156268 return upload_file_to_pysftp_file (fileobj , url )
59166269 return upload_file_to_sftp_file (fileobj , url )
6270+ if p .scheme in ("data" , ):
6271+ return data_url_encode (fileobj )
59176272 if p .scheme in ("file" or "" ):
59186273 outfile = io .open (unquote (p .path ), "wb" )
59196274 try :
@@ -5923,8 +6278,6 @@ def upload_file_to_internet_file(fileobj, url):
59236278 with io .open (unquote (p .path ), "wb" ) as fdst :
59246279 shutil .copyfileobj (fileobj , fdst )
59256280 return fileobj
5926- if p .scheme in ("data" , ):
5927- return data_url_encode (fileobj )
59286281 if p .scheme in ("tcp" , "udp" ):
59296282 parts , o = _parse_net_url (url )
59306283 host = parts .hostname
0 commit comments