#!/usr/bin/python3
#
# Hippotat - Asinine IP Over HTTP program
# ./hippotat - client main program
#
# Copyright 2017 Ian Jackson
#
# GPLv3+
#
# 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 3 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, in the file GPLv3. If not,
# see .
from hippotatlib import *
import twisted.web
import twisted.web.client
import io
class GeneralResponseConsumer(twisted.internet.protocol.Protocol):
def __init__(self, cl, req, desc):
self._cl = cl
self._req = req
self._desc = desc
def _log(self, dflag, msg, **kwargs):
self._cl.log(dflag, '%s: %s' % (self._desc, msg), idof=self._req, **kwargs)
def connectionMade(self):
self._log(DBG.HTTP_CTRL, 'connectionMade')
class ResponseConsumer(GeneralResponseConsumer):
def __init__(self, cl, req):
super().__init__(cl, req, 'RC')
ssddesc = '[%s] %s' % (id(req), self._desc)
self._ssd = SlipStreamDecoder(ssddesc, partial(queue_inbound, cl.ipif))
self._log(DBG.HTTP_CTRL, '__init__')
def dataReceived(self, data):
self._log(DBG.HTTP, 'dataReceived', d=data)
try:
self._ssd.inputdata(data)
except Exception as e:
self._handleexception()
def connectionLost(self, reason):
self._log(DBG.HTTP_CTRL, 'connectionLost ' + str(reason))
if not reason.check(twisted.web.client.ResponseDone):
self.latefailure()
return
try:
self._log(DBG.HTTP, 'ResponseDone')
self._ssd.flush()
self._cl.req_fin(self._req)
except Exception as e:
self._handleexception()
self._cl.report_running()
def _handleexception(self):
self._latefailure(traceback.format_exc())
def _latefailure(self, reason):
self._log(DBG.HTTP_CTRL, '_latefailure ' + str(reason))
self._cl.req_err(self._req, reason)
class ErrorResponseConsumer(GeneralResponseConsumer):
def __init__(self, cl, req, resp):
super().__init__(cl, req, 'ERROR-RC')
self._resp = resp
self._m = b''
try:
self._phrase = resp.phrase.decode('utf-8')
except Exception:
self._phrase = repr(resp.phrase)
self._log(DBG.HTTP_CTRL, '__init__ %d %s' % (resp.code, self._phrase))
def dataReceived(self, data):
self._log(DBG.HTTP_CTRL, 'dataReceived ' + repr(data))
self._m += data
def connectionLost(self, reason):
try:
mbody = self._m.decode('utf-8')
except Exception:
mbody = repr(self._m)
if not reason.check(twisted.web.client.ResponseDone):
mbody += ' || ' + str(reason)
self._cl.req_err(self._req,
"FAILED %d %s | %s"
% (self._resp.code, self._phrase, mbody))
class Client():
def __init__(cl, c,ss,cs):
cl.c = c
cl.outstanding = { }
cl.desc = '[%s %s] ' % (ss,cs)
cl.running_reported = False
cl.log_info('setting up')
def log_info(cl, msg):
log.info(cl.desc + msg, dflag=False)
def report_running(cl):
if not cl.running_reported:
cl.log_info('running OK')
cl.running_reported = True
def log(cl, dflag, msg, **kwargs):
log_debug(dflag, cl.desc + msg, **kwargs)
def log_outstanding(cl):
cl.log(DBG.CTRL_DUMP, 'OS %s' % cl.outstanding)
def start(cl):
cl.queue = PacketQueue('up', cl.c.max_queue_time)
cl.agent = twisted.web.client.Agent(
reactor, connectTimeout = cl.c.http_timeout)
def outbound(cl, packet, saddr, daddr):
#print('OUT ', saddr, daddr, repr(packet))
cl.queue.append(packet)
cl.check_outbound()
def req_ok(cl, req, resp):
cl.log(DBG.HTTP_CTRL,
'req_ok %d %s %s' % (resp.code, repr(resp.phrase), str(resp)),
idof=req)
if resp.code == 200:
rc = ResponseConsumer(cl, req)
else:
rc = ErrorResponseConsumer(cl, req, resp)
resp.deliverBody(rc)
# now rc is responsible for calling req_fin
def req_err(cl, req, err):
# called when the Deferred fails, or (if it completes),
# later, by ResponsConsumer or ErrorResponsConsumer
try:
cl.log(DBG.HTTP_CTRL, 'req_err ' + str(err), idof=req)
if isinstance(err, twisted.python.failure.Failure):
err = err.getTraceback()
print('[%#x] %s' % (id(req), err), file=sys.stderr)
if not isinstance(cl.outstanding[req], int):
raise RuntimeError('[%#x] previously %s' %
(id(req), cl.outstanding[req]))
cl.outstanding[req] = err
cl.log_outstanding()
reactor.callLater(cl.c.http_retry, partial(cl.req_fin, req))
except Exception as e:
crash(traceback.format_exc() + '\n----- handling -----\n' + err)
def req_fin(cl, req):
del cl.outstanding[req]
cl.log(DBG.HTTP_CTRL, 'req_fin OS=%d' % len(cl.outstanding), idof=req)
cl.check_outbound()
def check_outbound(cl):
while True:
if len(cl.outstanding) >= cl.c.max_outstanding:
break
if (not cl.queue.nonempty() and
len(cl.outstanding) >= cl.c.target_requests_outstanding):
break
d = b''
def moredata(s): nonlocal d; d += s
cl.queue.process((lambda: len(d)),
moredata,
cl.c.max_batch_up)
d = mime_translate(d)
crlf = b'\r\n'
lf = b'\n'
mime = (b'--b' + crlf +
b'Content-Type: text/plain; charset="utf-8"' + crlf +
b'Content-Disposition: form-data; name="m"' + crlf + crlf +
str(cl.c.client) .encode('ascii') + crlf +
cl.c.password + crlf +
str(cl.c.target_requests_outstanding)
.encode('ascii') + crlf +
str(cl.c.http_timeout) .encode('ascii') + crlf +
((
b'--b' + crlf +
b'Content-Type: application/octet-stream' + crlf +
b'Content-Disposition: form-data; name="d"' + crlf + crlf +
d + crlf
) if len(d) else b'') +
b'--b--' + crlf)
#df = open('data.dump.dbg', mode='wb')
#df.write(mime)
#df.close()
# POST -use -c 'multipart/form-data; boundary="b"' http://localhost:8099/