├── .gitignore ├── FakeDetector.py ├── README.md └── xmlrpclib_to └── __init__.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### JetBrains template 3 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 4 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 5 | 6 | # User-specific stuff 7 | .idea/ 8 | .pyc -------------------------------------------------------------------------------- /FakeDetector.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Fake detection script for NZBGet 4 | # 5 | # Copyright (C) 2014-2016 Andrey Prygunkov 6 | # Copyright (C) 2014 Clinton Hall 7 | # Copyright (C) 2014 JVM 8 | # 9 | # This program is free software; you can redistribute it and/or modify 10 | # it under the terms of the GNU General Public License as published by 11 | # the Free Software Foundation; either version 2 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # This program is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU General Public License 20 | # along with this program; if not, write to the Free Software 21 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 22 | # 23 | 24 | ############################################################################## 25 | ### NZBGET QUEUE/POST-PROCESSING SCRIPT ### 26 | ### QUEUE EVENTS: NZB_ADDED, NZB_DOWNLOADED, FILE_DOWNLOADED 27 | 28 | # Detect nzbs with fake media files. 29 | # 30 | # If a fake is detected the download is marked as bad. NZBGet removes 31 | # the download from queue and (if option "DeleteCleanupDisk" is active) the 32 | # downloaded files are deleted from disk. If duplicate handling is active 33 | # (option "DupeCheck") then another duplicate is chosen for download 34 | # if available. 35 | # 36 | # The status "FAILURE/BAD" is passed to other scripts and informs them 37 | # about failure. 38 | # 39 | # PP-Script version: 1.8. 40 | # 41 | # For more info and updates please visit forum topic at 42 | # http://nzbget.net/forum/viewtopic.php?f=8&t=1394. 43 | # 44 | # NOTE: This script requires Python to be installed on your system. 45 | 46 | 47 | ############################################################################## 48 | ### OPTIONS ### 49 | 50 | # Banned extensions. 51 | # 52 | # Downloads which contain files with any of the following extensions will be marked as fake. 53 | # Extensions must be separated by a comma (eg: .wmv, .divx). 54 | #BannedExtensions= 55 | 56 | 57 | ### NZBGET QUEUE/POST-PROCESSING SCRIPT ### 58 | ############################################################################## 59 | 60 | 61 | import os 62 | import sys 63 | import subprocess 64 | import re 65 | 66 | PY2 = 2 == sys.version_info[0] 67 | PY3 = 3 == sys.version_info[0] 68 | 69 | try: 70 | from urllib2 import Request, urlopen 71 | except ImportError: 72 | from urllib.request import Request, urlopen 73 | import base64 74 | from xmlrpclib_to import ServerProxy 75 | import shlex 76 | import traceback 77 | 78 | 79 | if PY3: 80 | text_type = str 81 | binary_type = bytes 82 | encodebytes = base64.encodebytes 83 | 84 | else: 85 | text_type = unicode 86 | binary_type = str 87 | encodebytes = base64.encodestring 88 | 89 | # Exit codes used by NZBGet for post-processing scripts. 90 | # Queue-scripts don't have any special exit codes. 91 | POSTPROCESS_SUCCESS=93 92 | POSTPROCESS_NONE=95 93 | POSTPROCESS_ERROR=94 94 | 95 | mediaExtensions = ['.mkv', '.avi', '.divx', '.xvid', '.mov', '.wmv', '.mp4', '.mpg', '.mpeg', '.vob', '.iso', '.m4v'] 96 | bannedMediaExtensions = os.environ.get('NZBPO_BANNEDEXTENSIONS').replace(' ', '').split(',') 97 | 98 | verbose = False 99 | 100 | 101 | def ensure_binary(s, encoding='utf-8', errors='strict'): 102 | """Coerce **s** to six.binary_type. 103 | 104 | For Python 2: 105 | - `unicode` -> encoded to `str` 106 | - `str` -> `str` 107 | 108 | For Python 3: 109 | - `str` -> encoded to `bytes` 110 | - `bytes` -> `bytes` 111 | """ 112 | if isinstance(s, binary_type): 113 | return s 114 | if isinstance(s, text_type): 115 | return s.encode(encoding, errors) 116 | raise TypeError("not expecting type '%s'" % type(s)) 117 | 118 | 119 | def ensure_str(s, encoding='utf-8', errors='strict'): 120 | """Coerce *s* to `str`. 121 | 122 | For Python 2: 123 | - `unicode` -> encoded to `str` 124 | - `str` -> `str` 125 | 126 | For Python 3: 127 | - `str` -> `str` 128 | - `bytes` -> decoded to `str` 129 | """ 130 | # Optimization: Fast return for the common case. 131 | if type(s) is str: 132 | return s 133 | if PY2 and isinstance(s, text_type): 134 | return s.encode(encoding, errors) 135 | elif PY3 and isinstance(s, binary_type): 136 | return s.decode(encoding, errors) 137 | elif not isinstance(s, (text_type, binary_type)): 138 | raise TypeError("not expecting type '%s'" % type(s)) 139 | return s 140 | 141 | 142 | # Start up checks 143 | def start_check(): 144 | # Check if the script is called from a compatible NZBGet version (as queue-script or as pp-script) 145 | if not ('NZBNA_EVENT' in os.environ or 'NZBPP_DIRECTORY' in os.environ) or not 'NZBOP_ARTICLECACHE' in os.environ: 146 | print('*** NZBGet queue script ***') 147 | print('This script is supposed to be called from nzbget (14.0 or later).') 148 | sys.exit(1) 149 | 150 | # This script processes only certain queue events. 151 | # For compatibility with newer NZBGet versions it ignores event types it doesn't know 152 | if os.environ.get('NZBNA_EVENT') not in ['NZB_ADDED', 'FILE_DOWNLOADED', 'NZB_DOWNLOADED', None]: 153 | sys.exit(0) 154 | 155 | # If nzb was already marked as bad don't do any further detection 156 | if os.environ.get('NZBPP_STATUS') == 'FAILURE/BAD': 157 | if os.environ.get('NZBPR_PPSTATUS_FAKE') == 'yes': 158 | # Print the message again during post-processing to add it into the post-processing log 159 | # (which is then can be used by notification scripts such as EMail.py) 160 | # Pp-parameter "NZBPR_PPSTATUS_FAKEBAN" contains more details (saved previously by our script) 161 | if os.environ.get('NZBPR_PPSTATUS_FAKEBAN') == None: 162 | print('[WARNING] Download has media files and executables') 163 | else: 164 | print('[WARNING] Download contains banned extension ' + os.environ.get('NZBPR_PPSTATUS_FAKEBAN')) 165 | clean_up() 166 | sys.exit(POSTPROCESS_SUCCESS) 167 | 168 | # If called via "Post-process again" from history details dialog the download may not exist anymore 169 | if 'NZBPP_DIRECTORY' in os.environ and not os.path.exists(os.environ.get('NZBPP_DIRECTORY')): 170 | print('Destination directory doesn\'t exist, exiting') 171 | clean_up() 172 | sys.exit(POSTPROCESS_NONE) 173 | 174 | # If nzb is already failed, don't do any further detection 175 | if os.environ.get('NZBPP_TOTALSTATUS') == 'FAILURE': 176 | clean_up() 177 | sys.exit(POSTPROCESS_NONE) 178 | 179 | # Check if media files present in the list of files 180 | def contains_media(list): 181 | for item in list: 182 | if os.path.splitext(item)[1] in mediaExtensions: 183 | return True 184 | else: 185 | continue 186 | return False 187 | 188 | # Check if banned media files present in the list of files 189 | def contains_banned_media(list): 190 | for item in list: 191 | if os.path.splitext(item)[1] in bannedMediaExtensions: 192 | print('[INFO] Found file with banned extension: ' + item) 193 | return os.path.splitext(item)[1] 194 | else: 195 | continue 196 | return '' 197 | 198 | # Check if executable files present in the list of files 199 | # Exception: rename.bat (.sh, .exe) are ignored, sometimes valid posts include them. 200 | def contains_executable(list): 201 | exExtensions = [ '.exe', '.bat', '.sh' ] 202 | allowNames = [ 'rename', 'file renamer', 'rarbg_do_not_mirror' ] 203 | excludePath = [ r'reverse', r'spiegelen' ] 204 | for item in list: 205 | ep = False 206 | for ap in excludePath: 207 | if re.search(ap, item, re.I): 208 | ep = True 209 | break 210 | if ep: 211 | continue 212 | name, ext = os.path.splitext(item) 213 | if os.path.split(name)[1] != "": 214 | name = os.path.split(name)[1] 215 | name = name.lower() 216 | if ext.lower() in exExtensions and not name in allowNames: 217 | print('[INFO] Found executable %s' % item) 218 | return True 219 | else: 220 | continue 221 | return False 222 | 223 | # Finds untested files, comparing all files and processed files in tmp_file 224 | def get_latest_file(dir): 225 | try: 226 | with open(tmp_file_name) as tmp_file: 227 | tested = tmp_file.read().splitlines() 228 | files = os.listdir(dir) 229 | return list(set(files)-set(tested)) 230 | except: 231 | # tmp_file doesn't exist, all files need testing 232 | temp_folder = os.path.dirname(tmp_file_name) 233 | if not os.path.exists(temp_folder): 234 | os.makedirs(temp_folder) 235 | print('[DETAIL] Created folder ' + temp_folder) 236 | with open(tmp_file_name, "w") as tmp_file: 237 | tmp_file.write('') 238 | print('[DETAIL] Created temp file ' + tmp_file_name) 239 | return os.listdir(dir) 240 | 241 | # Saves tested files so to not test again 242 | def save_tested(data): 243 | with open(tmp_file_name, "a") as tmp_file: 244 | tmp_file.write(data) 245 | 246 | # Extract path to unrar from NZBGet's global option "UnrarCmd"; 247 | # Since v15 "UnrarCmd" may contain extra parameters passed to unrar; 248 | # We have to strip these parameters because we need only the path to unrar. 249 | # Returns path to unrar executable. 250 | def unrar(): 251 | exe_name = 'unrar.exe' if os.name == 'nt' else 'unrar' 252 | UnrarCmd = os.environ['NZBOP_UNRARCMD'] 253 | if os.path.isfile(UnrarCmd) and UnrarCmd.lower().endswith(exe_name): 254 | return UnrarCmd 255 | args = shlex.split(UnrarCmd) 256 | for arg in args: 257 | if arg.lower().endswith(exe_name): 258 | return arg 259 | # We were unable to determine the path to unrar; 260 | # Let's use the exe name with a hope it's in the search path 261 | return exe_name 262 | 263 | # List contents of rar-files (without unpacking). 264 | # That's how we detect fakes during download, when the download is not completed yet. 265 | def list_all_rars(dir): 266 | files = get_latest_file(dir) 267 | tested = '' 268 | out = '' 269 | for file in files: 270 | # avoid .tmp files as corrupt 271 | if not "tmp" in file: 272 | try: 273 | command = [unrar(), "vb", dir + '/' + file] 274 | if verbose: 275 | print('command: %s' % command) 276 | proc = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 277 | out_tmp, err = proc.communicate() 278 | out += ensure_str(out_tmp) 279 | result = proc.returncode 280 | if verbose: 281 | print(out_tmp) 282 | except Exception as e: 283 | print('[ERROR] Failed %s: %s' % (file, e)) 284 | if verbose: 285 | traceback.print_exc() 286 | tested += file + '\n' 287 | save_tested(tested) 288 | return out.splitlines() 289 | 290 | # Detect fake nzbs. Returns True if a fake is detected. 291 | def detect_fake(name, dir): 292 | # Fake detection: 293 | # If download contains media files AND executables we consider it a fake. 294 | # QUEUE mode (called during download and before unpack): 295 | # - if directory contains archives list their content and use the file 296 | # names for detection; 297 | # POST-PROCESSING mode (called after unpack): 298 | # - scan directroy content and use file names for detection; 299 | # - TODO: check video files using ffprobe. 300 | # 301 | # It's actually not necessary to check the mode (QUEUE or POST-PROCESSING), we always do all checks. 302 | 303 | filelist = [] 304 | dir = os.path.normpath(dir) 305 | filelist.extend([ o for o in os.listdir(dir) if os.path.isfile(os.path.join(dir, o)) ]) 306 | dirlist = [ os.path.join(dir, o) for o in os.listdir(dir) if os.path.isdir(os.path.join(dir, o)) ] 307 | filelist.extend(list_all_rars(dir)) 308 | for subdir in dirlist: 309 | filelist.extend(list_all_rars(subdir)) 310 | if contains_media(filelist) and contains_executable(filelist): 311 | print('[WARNING] Download has media files and executables') 312 | # Remove info about banned extension from pp-parameter "NZBPR_PPSTATUS_FAKEBAN" 313 | # (in a case it was saved previously) 314 | print('[NZB] NZBPR_PPSTATUS_FAKEBAN=') 315 | return True 316 | banned_ext = contains_banned_media(filelist) 317 | if banned_ext != '': 318 | print('[WARNING] Download contains banned extension ' + banned_ext) 319 | # Save details about banned extension in pp-parameter "NZBPR_PPSTATUS_FAKEBAN" 320 | print('[NZB] NZBPR_PPSTATUS_FAKEBAN=' + banned_ext) 321 | return True 322 | return False 323 | 324 | # Establish connection to NZBGet via RPC-API 325 | def connect_to_nzbget(): 326 | # First we need to know connection info: host, port and password of NZBGet server. 327 | # NZBGet passes all configuration options to scripts as environment variables. 328 | host = os.environ['NZBOP_CONTROLIP'] 329 | if host == '0.0.0.0': host = '127.0.0.1' 330 | port = os.environ['NZBOP_CONTROLPORT'] 331 | username = os.environ['NZBOP_CONTROLUSERNAME'] 332 | password = os.environ['NZBOP_CONTROLPASSWORD'] 333 | 334 | # Build an URL for XML-RPC requests 335 | # TODO: encode username and password in URL-format 336 | xmlRpcUrl = 'http://%s:%s@%s:%s/xmlrpc' % (username, password, host, port) 337 | 338 | # Create remote server object 339 | nzbget = ServerProxy(xmlRpcUrl, timeout=500) 340 | return nzbget 341 | 342 | # Connect to NZBGet and call an RPC-API-method without using of python's XML-RPC. 343 | # XML-RPC is easy to use but it is slow for large amount of data 344 | def call_nzbget_direct(url_command): 345 | # First we need to know connection info: host, port and password of NZBGet server. 346 | # NZBGet passes all configuration options to scripts as environment variables. 347 | host = os.environ['NZBOP_CONTROLIP'] 348 | if host == '0.0.0.0': host = '127.0.0.1' 349 | port = os.environ['NZBOP_CONTROLPORT'] 350 | username = os.environ['NZBOP_CONTROLUSERNAME'] 351 | password = os.environ['NZBOP_CONTROLPASSWORD'] 352 | 353 | # Building http-URL to call the method 354 | httpUrl = 'http://%s:%s/jsonrpc/%s' % (host, port, url_command) 355 | request = Request(httpUrl) 356 | 357 | base64string = ensure_str(encodebytes(ensure_binary('%s:%s' % (username, password)))).replace('\n', '') 358 | request.add_header("Authorization", "Basic %s" % base64string) 359 | 360 | # Load data from NZBGet 361 | try: 362 | response = urlopen(request) 363 | data = ensure_str(response.read()) 364 | except Exception: 365 | print('Error connecting to NZBGet') 366 | sys.exit(1) 367 | 368 | # "data" is a JSON raw-string 369 | return data 370 | 371 | # Reorder inner files for earlier fake detection 372 | def sort_inner_files(): 373 | nzb_id = int(os.environ.get('NZBNA_NZBID')) 374 | 375 | # Building command-URL to call method "listfiles" passing three parameters: (0, 0, nzb_id) 376 | url_command = 'listfiles?1=0&2=0&3=%i' % nzb_id 377 | data = call_nzbget_direct(url_command) 378 | 379 | # The "data" is a raw json-string. We could use json.loads(data) to 380 | # parse it but json-module is slow. We parse it on our own. 381 | 382 | # Iterate through the list of files to find the last rar-file. 383 | # The last is the one with the highest XX in ".partXX.rar" or ".rXX" 384 | regex1 = re.compile('.*\.part(\d+)\.rar', re.IGNORECASE) 385 | regex2 = re.compile('.*\.r(\d+)', re.IGNORECASE) 386 | file_num = None 387 | file_id = None 388 | file_name = None 389 | 390 | for line in data.splitlines(): 391 | line = ensure_str(line) 392 | if line.startswith('"ID" : '): 393 | cur_id = int(line[7:len(line)-1]) 394 | if line.startswith('"Filename" : "'): 395 | cur_name = line[14:len(line)-2] 396 | match = regex1.match(cur_name) or regex2.match(cur_name) 397 | if (match): 398 | cur_num = int(match.group(1)) 399 | if not file_num or cur_num > file_num: 400 | file_num = cur_num 401 | file_id = cur_id 402 | file_name = cur_name 403 | 404 | # Move the last rar-file to the top of file list 405 | if (file_id): 406 | print('[INFO] Moving last rar-file to the top: %s' % file_name) 407 | # Create remote server object 408 | nzbget = connect_to_nzbget() 409 | # Using RPC-method "editqueue" of XML-RPC-object "nzbget". 410 | # we could use direct http access here too but the speed isn't 411 | # an issue here and XML-RPC is easier to use. 412 | nzbget.editqueue('FileMoveTop', 0, '', [file_id]) 413 | else: 414 | print('[INFO] Skipping sorting since could not find any rar-files') 415 | 416 | # Remove current and any old temp files 417 | def clean_up(): 418 | nzb_id = os.environ.get('NZBPP_NZBID') 419 | temp_folder = os.environ.get('NZBOP_TEMPDIR') + '/FakeDetector' 420 | 421 | nzbids = [] 422 | files = os.listdir(temp_folder) 423 | 424 | if len(files) > 1: 425 | # Create the list of nzbs in download queue 426 | data = call_nzbget_direct('listgroups?1=0') 427 | # The "data" is a raw json-string. We could use json.loads(data) to 428 | # parse it but json-module is slow. We parse it on our own. 429 | for line in data.splitlines(): 430 | line = ensure_str(line) 431 | if line.startswith('"NZBID" : '): 432 | cur_id = int(line[10:len(line)-1]) 433 | nzbids.append(str(cur_id)) 434 | 435 | old_temp_files = list(set(files)-set(nzbids)) 436 | if nzb_id in files and nzb_id not in old_temp_files: 437 | old_temp_files.append(nzb_id) 438 | 439 | for temp_id in old_temp_files: 440 | temp_file = temp_folder + '/' + str(temp_id) 441 | try: 442 | print('[DETAIL] Removing temp file ' + temp_file) 443 | os.remove(temp_file) 444 | except: 445 | print('[ERROR] Could not remove temp file ' + temp_file) 446 | 447 | # Script body 448 | def main(): 449 | # Globally define directory for storing list of tested files 450 | global tmp_file_name 451 | 452 | # Do start up check 453 | start_check() 454 | 455 | # That's how we determine if the download is still runnning or is completely downloaded. 456 | # We don't use this info in the fake detector (yet). 457 | Downloading = os.environ.get('NZBNA_EVENT') == 'FILE_DOWNLOADED' 458 | 459 | # Depending on the mode in which the script was called (queue-script 460 | # or post-processing-script) a different set of parameters (env. vars) 461 | # is passed. They also have different prefixes: 462 | # - NZBNA_ in queue-script mode; 463 | # - NZBPP_ in pp-script mode. 464 | Prefix = 'NZBNA_' if 'NZBNA_EVENT' in os.environ else 'NZBPP_' 465 | 466 | # Read context (what nzb is currently being processed) 467 | Category = os.environ[Prefix + 'CATEGORY'] 468 | Directory = os.environ[Prefix + 'DIRECTORY'] 469 | NzbName = os.environ[Prefix + 'NZBNAME'] 470 | 471 | # Directory for storing list of tested files 472 | tmp_file_name = os.environ.get('NZBOP_TEMPDIR') + '/FakeDetector/' + os.environ.get(Prefix + 'NZBID') 473 | 474 | # When nzb is added to queue - reorder inner files for earlier fake detection. 475 | # Also it is possible that nzb was added with a category which doesn't have 476 | # FakeDetector listed in the PostScript. In this case FakeDetector was not called 477 | # when adding nzb to queue but it is being called now and we can reorder 478 | # files now. 479 | if os.environ.get('NZBNA_EVENT') == 'NZB_ADDED' or \ 480 | (os.environ.get('NZBNA_EVENT') == 'FILE_DOWNLOADED' and \ 481 | os.environ.get('NZBPR_FAKEDETECTOR_SORTED') != 'yes'): 482 | print('[INFO] Sorting inner files for earlier fake detection for %s' % NzbName) 483 | sys.stdout.flush() 484 | sort_inner_files() 485 | print('[NZB] NZBPR_FAKEDETECTOR_SORTED=yes') 486 | if os.environ.get('NZBNA_EVENT') == 'NZB_ADDED': 487 | sys.exit(POSTPROCESS_NONE) 488 | 489 | print('[DETAIL] Detecting fake for %s' % NzbName) 490 | sys.stdout.flush() 491 | 492 | if detect_fake(NzbName, Directory): 493 | # A fake is detected 494 | # 495 | # Add post-processing parameter "PPSTATUS_FAKE" for nzb-file. 496 | # Scripts running after fake detector can check the parameter like this: 497 | # if os.environ.get('NZBPR_PPSTATUS_FAKE') == 'yes': 498 | # print('Marked as fake by another script') 499 | print('[NZB] NZBPR_PPSTATUS_FAKE=yes') 500 | 501 | # Special command telling NZBGet to mark nzb as bad. The nzb will 502 | # be removed from queue and become status "FAILURE/BAD". 503 | print('[NZB] MARK=BAD') 504 | else: 505 | # Not a fake or at least doesn't look like a fake (yet). 506 | # 507 | # When nzb is downloaded again (using "Download again" from history) 508 | # it may have been marked by our script as a fake. Since now the script 509 | # doesn't consider nzb as fake we remove the old marking. That's 510 | # of course a rare case that someone will redownload a fake but 511 | # at least during debugging of fake detector we do that all the time. 512 | if os.environ.get('NZBPR_PPSTATUS_FAKE') == 'yes': 513 | print('[NZB] NZBPR_PPSTATUS_FAKE=') 514 | 515 | print('[DETAIL] Detecting completed for %s' % NzbName) 516 | sys.stdout.flush() 517 | 518 | # Remove temp files in PP 519 | if Prefix == 'NZBPP_': 520 | clean_up() 521 | 522 | # Execute main script function 523 | main() 524 | 525 | # All OK, returning exit status 'POSTPROCESS_SUCCESS' (int <93>) to let NZBGet know 526 | # that our script has successfully completed (only for pp-script mode). 527 | sys.exit(POSTPROCESS_SUCCESS) 528 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FakeDetector 2 | Fake detection script for [NZBGet](http://nzbget.net). 3 | 4 | Authors: 5 | - Andrey Prygunkov 6 | - Clinton Hall 7 | - JVM 8 | 9 | Detects nzbs with fake media files. If a fake is detected the download is marked as bad. NZBGet removes the download from queue and (if option "DeleteCleanupDisk" is active) the downloaded files are deleted from disk. If duplicate handling is active (option "DupeCheck") then another duplicate is chosen for download if available. 10 | 11 | The status "FAILURE/BAD" is passed to other scripts and informs them about failure. 12 | 13 | For more info and support please visit forum topic [PP-Script FakeDetector](http://nzbget.net/forum/viewtopic.php?f=8&t=1394). 14 | 15 | NOTE: This script requires Python to be installed on your system 16 | -------------------------------------------------------------------------------- /xmlrpclib_to/__init__.py: -------------------------------------------------------------------------------- 1 | try: 2 | import xmlrpclib 3 | from xmlrpclib import * 4 | except ImportError: 5 | # Python 3.0 portability fix... 6 | import xmlrpc.client as xmlrpclib 7 | from xmlrpc.client import * 8 | 9 | try: 10 | import httplib 11 | except ImportError: 12 | import http.client as httplib 13 | import socket 14 | 15 | 16 | class ServerProxy(xmlrpclib.ServerProxy): 17 | 18 | def __init__(self, uri, transport=None, encoding=None, verbose=0, 19 | allow_none=0, use_datetime=0, timeout=None): 20 | if timeout is not None: 21 | if uri.startswith('http://'): 22 | secure = False 23 | elif uri.startswith('https://'): 24 | secure = True 25 | transport = TimeoutTransport(use_datetime, timeout, secure=secure) 26 | xmlrpclib.ServerProxy.__init__(self, uri, transport, encoding, verbose, 27 | allow_none, use_datetime) 28 | 29 | 30 | class TimeoutTransport(xmlrpclib.Transport): 31 | 32 | def __init__(self, use_datetime=0, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, 33 | secure=False): 34 | xmlrpclib.Transport.__init__(self, use_datetime) 35 | self.timeout = timeout 36 | self.secure = secure 37 | 38 | def make_connection(self, host): 39 | if self._connection and host == self._connection[0]: 40 | return self._connection[1] 41 | chost, self._extra_headers, x509 = self.get_host_info(host) 42 | if self.secure: 43 | self._connection = host, httplib.HTTPSConnection( 44 | chost, None, timeout=self.timeout, **(x509 or {}) 45 | ) 46 | else: 47 | self._connection = host, httplib.HTTPConnection( 48 | chost, timeout=self.timeout 49 | ) 50 | 51 | return self._connection[1] 52 | --------------------------------------------------------------------------------