text: #********************************************************************************
  #*                               Dionaea
  #*                           - catches bugs -
  #*
  #*
  #*
  #* Copyright (C) 2010  Mark Schloesser
  #* 
  #* This program is free software; you can redistribute it and/or
  #* modify it under the terms of the GNU General Public License
  #* as published by the Free Software Foundation; either version 2
  #* of the License, or (at your option) any later version.
  #* 
  #* This program is distributed in the hope that it will be useful,
  #* but WITHOUT ANY WARRANTY; without even the implied warranty of
  #* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  #* GNU General Public License for more details.
  #* 
  #* You should have received a copy of the GNU General Public License
  #* along with this program; if not, write to the Free Software
  #* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  #* 
  #* 
  #*             contact nepenthesdev@gmail.com  
  #*
  #*******************************************************************************/
  
  from dionaea.core import ihandler, incident, g_dionaea, connection
  from dionaea.util import sha512file
  
  import os
  import logging
  import struct
  import hashlib
  import json
  try: import pyev
  except: pyev = None
  
  logger = logging.getLogger('hpfeeds')
  logger.setLevel(logging.DEBUG)
  
  #def DEBUGPERF(msg):
  #	print(msg)
  #logger.debug = DEBUGPERF
  #logger.critical = DEBUGPERF
  
  BUFSIZ = 16384
  
  OP_ERROR        = 0
  OP_INFO         = 1
  OP_AUTH         = 2
  OP_PUBLISH      = 3
  OP_SUBSCRIBE    = 4
  
  MAXBUF = 1024**2
  SIZES = {
  	OP_ERROR: 5+MAXBUF,
  	OP_INFO: 5+256+20,
  	OP_AUTH: 5+256+20,
  	OP_PUBLISH: 5+MAXBUF,
  	OP_SUBSCRIBE: 5+256*2,
  }
  
  CONNCHAN = 'dionaea.connections'
  CAPTURECHAN = 'dionaea.capture'
  DCECHAN = 'dionaea.dcerpcrequests'
  SCPROFCHAN = 'dionaea.shellcodeprofiles'
  UNIQUECHAN = 'mwbinary.dionaea.sensorunique'
  
  class BadClient(Exception):
          pass
  
  # packs a string with 1 byte length field
  def strpack8(x):
  	if isinstance(x, str): x = x.encode('latin1')
  	return struct.pack('!B', len(x)%0xff) + x
  
  # unpacks a string with 1 byte length field
  def strunpack8(x):
  	l = x[0]
  	return x[1:1+l], x[1+l:]
  	
  def msghdr(op, data):
  	return struct.pack('!iB', 5+len(data), op) + data
  def msgpublish(ident, chan, data):
  	return msghdr(OP_PUBLISH, strpack8(ident) + strpack8(chan) + data)
  def msgsubscribe(ident, chan):
  	if isinstance(chan, str): chan = chan.encode('latin1')
  	return msghdr(OP_SUBSCRIBE, strpack8(ident) + chan)
  def msgauth(rand, ident, secret):
  	hash = hashlib.sha1(bytes(rand)+secret).digest()
  	return msghdr(OP_AUTH, strpack8(ident) + hash)
  
  class FeedUnpack(object):
  	def __init__(self):
  		self.buf = bytearray()
  	def __iter__(self):
  		return self
  	def __next__(self):
  		return self.unpack()
  	def feed(self, data):
  		self.buf.extend(data)
  	def unpack(self):
  		if len(self.buf) < 5:
  			raise StopIteration('No message.')
  
  		ml, opcode = struct.unpack('!iB', self.buf[:5])
  		if ml > SIZES.get(opcode, MAXBUF):
  			raise BadClient('Not respecting MAXBUF.')
  
  		if len(self.buf) < ml:
  			raise StopIteration('No message.')
  
  		data = self.buf[5:ml]
  		del self.buf[:ml]
  		return opcode, data
  
  class hpclient(connection):
  	def __init__(self, server, port, ident, secret):
  		logger.debug('hpclient init')
  		connection.__init__(self, 'tcp')
  		self.unpacker = FeedUnpack()
  		self.ident, self.secret = ident.encode('latin1'), secret.encode('latin1')
  
  		self.connect(server, port)
  		self.timeouts.reconnect = 10.0
  		self.sendfiles = []
  		self.msgqueue = []
  		self.filehandle = None
  		self.connected = False
  
  	def handle_established(self):
  		self.connected = True
  		logger.debug('hpclient established')
  
  	def handle_io_in(self, indata):
  		self.unpacker.feed(indata)
  
  		# if we are currently streaming a file, delay handling incoming messages
  		if self.filehandle:
  			return len(indata)
  
  		try:
  			for opcode, data in self.unpacker:
  				logger.debug('hpclient msg opcode {0} data {1}'.format(opcode, data))
  				if opcode == OP_INFO:
  					name, rand = strunpack8(data)
  					logger.debug('hpclient server name {0} rand {1}'.format(name, rand))
  					self.send(msgauth(rand, self.ident, self.secret))
  
  				elif opcode == OP_PUBLISH:
  					ident, data = strunpack8(data)
  					chan, data = strunpack8(data)
  					logger.debug('publish to {0} by {1}: {2}'.format(chan, ident, data))
  
  				elif opcode == OP_ERROR:
  					logger.debug('errormessage from server: {0}'.format(data))
  				else:
  					logger.debug('unknown opcode message: {0}'.format(opcode))
  		except BadClient:
  			logger.critical('unpacker error, disconnecting.')
  			self.close()
  
  		return len(indata)
  
  	def handle_io_out(self):
  		if self.filehandle: self.sendfiledata()
  		else:
  			if self.msgqueue:
  				m = self.msgqueue.pop(0)
  				self.send(m)
  
  	def publish(self, channel, **kwargs):
  		if self.filehandle: self.msgqueue.append(msgpublish(self.ident, channel, json.dumps(kwargs).encode('latin1')))
  		else: self.send(msgpublish(self.ident, channel, json.dumps(kwargs).encode('latin1')))
  
  	def sendfile(self, filepath):
  		# does not read complete binary into memory, read and send chunks
  		if not self.filehandle:
  			self.sendfileheader(filepath)
  			self.sendfiledata()
  		else: self.sendfiles.append(filepath)
  
  	def sendfileheader(self, filepath):
  		self.filehandle = open(filepath, 'rb')
  		fsize = os.stat(filepath).st_size
  		headc = strpack8(self.ident) + strpack8(UNIQUECHAN)
  		headh = struct.pack('!iB', 5+len(headc)+fsize, OP_PUBLISH)
  		self.send(headh + headc)
  
  	def sendfiledata(self):
  		tmp = self.filehandle.read(BUFSIZ)
  		if not tmp:
  			if self.sendfiles:
  				fp = self.sendfiles.pop(0)
  				self.sendfileheader(fp)
  			else:
  				self.filehandle = None
  				self.handle_io_in(b'')
  		else:
  			self.send(tmp)
  
  	def handle_timeout_idle(self):
  		pass
  
  	def handle_disconnect(self):
  		logger.info('hpclient disconnect')
  		self.connected = False
  		return 1
  
  	def handle_error(self, err):
  		logger.warn('hpclient error {0}'.format(err))
  		self.connected = False
  		return 1
  
  class hpfeedihandler(ihandler):
  	def __init__(self, config):
  		logger.debug('hpfeedhandler init')
  		self.client = hpclient(config['server'], int(config['port']), config['ident'], config['secret'])
  		ihandler.__init__(self, '*')
  
  		self.dynip_resolve = config.get('dynip_resolve', '')
  		self.dynip_timer = None
  		self.ownip = None
  		if self.dynip_resolve and 'http' in self.dynip_resolve:
  			if pyev == None:
  				logger.debug('You are missing the python pyev binding in your dionaea installation.')
  			else:
  				logger.debug('hpfeedihandler will use dynamic IP resolving!')
  				self.loop = pyev.default_loop()
  				self.dynip_timer = pyev.Timer(2., 300, self.loop, self._dynip_resolve)
  				self.dynip_timer.start()
  
  	def stop(self):
  		if self.dynip_timer:
  			self.dynip_timer.stop()
  			self.dynip_timer = None
  			self.loop = None
  
  	def _ownip(self, icd):
  		if self.dynip_resolve and 'http' in self.dynip_resolve and pyev != None:
  			if self.ownip: return self.ownip
  			else: raise Exception('Own IP not yet resolved!')
  		return icd.con.local.host
  
  	def __del__(self):
  		#self.client.close()
  		pass
  
  	def connection_publish(self, icd, con_type):
  		try:
  			con=icd.con
  			self.client.publish(CONNCHAN, connection_type=con_type, connection_transport=con.transport, connection_protocol=con.protocol, remote_host=con.remote.host, remote_port=con.remote.port, remote_hostname=con.remote.hostname, local_host=self._ownip(icd), local_port=con.local.port)
  		except Exception as e:
  			logger.warn('exception when publishing: {0}'.format(e))
  
  	def handle_incident(self, i):
  		pass
  	
  	def handle_incident_dionaea_connection_tcp_listen(self, icd):
  		self.connection_publish(icd, 'listen')
  		con=icd.con
  		logger.info("listen connection on %s:%i" % 
  			(con.remote.host, con.remote.port))
  
  	def handle_incident_dionaea_connection_tls_listen(self, icd):
  		self.connection_publish(icd, 'listen')
  		con=icd.con
  		logger.info("listen connection on %s:%i" % 
  			(con.remote.host, con.remote.port))
  
  	def handle_incident_dionaea_connection_tcp_connect(self, icd):
  		self.connection_publish(icd, 'connect')
  		con=icd.con
  		logger.info("connect connection to %s/%s:%i from %s:%i" % 
  			(con.remote.host, con.remote.hostname, con.remote.port, self._ownip(icd), con.local.port))
  
  	def handle_incident_dionaea_connection_tls_connect(self, icd):
  		self.connection_publish(icd, 'connect')
  		con=icd.con
  		logger.info("connect connection to %s/%s:%i from %s:%i" % 
  			(con.remote.host, con.remote.hostname, con.remote.port, self._ownip(icd), con.local.port))
  
  	def handle_incident_dionaea_connection_udp_connect(self, icd):
  		self.connection_publish(icd, 'connect')
  		con=icd.con
  		logger.info("connect connection to %s/%s:%i from %s:%i" % 
  			(con.remote.host, con.remote.hostname, con.remote.port, self._ownip(icd), con.local.port))
  
  	def handle_incident_dionaea_connection_tcp_accept(self, icd):
  		self.connection_publish(icd, 'accept')
  		con=icd.con
  		logger.info("accepted connection from  %s:%i to %s:%i" %
  			(con.remote.host, con.remote.port, self._ownip(icd), con.local.port))
  
  	def handle_incident_dionaea_connection_tls_accept(self, icd):
  		self.connection_publish(icd, 'accept')
  		con=icd.con
  		logger.info("accepted connection from %s:%i to %s:%i" % 
  			(con.remote.host, con.remote.port, self._ownip(icd), con.local.port))
  
  
  	def handle_incident_dionaea_connection_tcp_reject(self, icd):
  		self.connection_publish(icd, 'reject')
  		con=icd.con
  		logger.info("reject connection from %s:%i to %s:%i" % 
  			(con.remote.host, con.remote.port, self._ownip(icd), con.local.port))
  
  	def handle_incident_dionaea_connection_tcp_pending(self, icd):
  		self.connection_publish(icd, 'pending')
  		con=icd.con
  		logger.info("pending connection from %s:%i to %s:%i" % 
  			(con.remote.host, con.remote.port, self._ownip(icd), con.local.port))
  	
  	def handle_incident_dionaea_download_complete_unique(self, i):
  		self.handle_incident_dionaea_download_complete_again(i)
  		if not hasattr(i, 'con') or not self.client.connected: return
  		logger.debug('unique complete, publishing md5 {0}, path {1}'.format(i.md5hash, i.file))
  		try:
  			self.client.sendfile(i.file)
  		except Exception as e:
  			logger.warn('exception when publishing: {0}'.format(e))
  
  	def handle_incident_dionaea_download_complete_again(self, i):
  		if not hasattr(i, 'con') or not self.client.connected: return
  		logger.debug('hash complete, publishing md5 {0}, path {1}'.format(i.md5hash, i.file))
  		try:
  			sha512 = sha512file(i.file)
  			self.client.publish(CAPTURECHAN, saddr=i.con.remote.host, 
  				sport=str(i.con.remote.port), daddr=self._ownip(i),
  				dport=str(i.con.local.port), md5=i.md5hash, sha512=sha512,
  				url=i.url
  			)
  		except Exception as e:
  			logger.warn('exception when publishing: {0}'.format(e))
  
  	def handle_incident_dionaea_modules_python_smb_dcerpc_request(self, i):
  		if not hasattr(i, 'con') or not self.client.connected: return
  		logger.debug('dcerpc request, publishing uuid {0}, opnum {1}'.format(i.uuid, i.opnum))
  		try:
  			self.client.publish(DCECHAN, uuid=i.uuid, opnum=i.opnum,
  				saddr=i.con.remote.host, sport=str(i.con.remote.port),
  				daddr=self._ownip(i), dport=str(i.con.local.port),
  			)
  		except Exception as e:
  			logger.warn('exception when publishing: {0}'.format(e))
  
  	def handle_incident_dionaea_module_emu_profile(self, icd):
  		if not hasattr(icd, 'con') or not self.client.connected: return
  		logger.debug('emu profile, publishing length {0}'.format(len(icd.profile)))
  		try:
  			self.client.publish(SCPROFCHAN, profile=icd.profile)
  		except Exception as e:
  			logger.warn('exception when publishing: {0}'.format(e))
  
  	def _dynip_resolve(self, events, data):
  		i = incident("dionaea.upload.request")
  		i._url = self.dynip_resolve
  		i._callback = "dionaea.modules.python.hpfeeds.dynipresult"
  		i.report()
  
  	def handle_incident_dionaea_modules_python_hpfeeds_dynipresult(self, icd):
  		fh = open(icd.path, mode="rb")
  		self.ownip = fh.read().strip().decode('latin1')
  		logger.debug('resolved own IP to: {0}'.format(self.ownip))
  		fh.close()
  