logo Raspberry Pi Python message-forwarder/proxy

Comments: 0

Introduction

NOTE: This code is a first attempt at Python 3 message-forwarder/proxy for the HTML/CSS/JS frontend on smartphones/tablets.

I will be migrating to a Phonegap app + Node.JS architecture in the coming weeks, so these notes will soon become irrelevant to my setup.

The main point of this blog post is to document what was done!

Overview of architecture

From a previous post the Raspberry Pi uses WebIOPi which has a built-in Python server that serves up the HTML/CSS/JS to smartphones/tablets. The JS sends HTML requests to the Python functions which then forwards/proxies the messages to relevant devices/services and returns responses where appliable.

The main reasons for using message forwarding are:

  1. JS on a smartphone is restricted by the Same Origin Policy (ie: JS can only talk to the same protocol/host/port that it was served up from)
  2. JS on a smartphone cannot communicate over UDP or cabled RS232

I have intentionally kept the message forwarding on the Pi fairly 'dumb' with all the 'smarts' residing in JS on the smartphones/tablets.

Main Functions

Coming up next

A closer look at the HTML/CSS/JS and how it sends requests to the Pi

The code in full

#!/usr/bin/python

import os
import webiopi
import http.client
import urllib.request
import urllib.parse
import socket
import select
import serial
import time
import ast

import sys
import cgi 
from os import getenv

import re
import subprocess

# debug stuff: use "pprint(getmembers(obj))"
from inspect import getmembers
from pprint import pprint

# global CONSTANTS
PUSHOVER_HOST = 'api.pushover.net'
PUSHOVER_PATH = '/1/messages.json'
PUSHOVER_APP_TOKEN = 'INSERT YOUR OWN HERE'
PUSHOVER_USER_KEY = 'INSERT YOUR OWN HERE'

# global vars
globalSerialPort = serial.Serial("/dev/ttyAMA0", baudrate=19200, timeout=0.5)
globalAction = ''

# start debug output
webiopi.setDebug()

# Called by WebIOPi at server startup
def setup():
  webiopi.debug('@ setup')
  timestamp = int(time.time())
  webiopi.debug('# timestamp='+repr(timestamp))

# continuous loop (show in debug console that script is still alive)
def loop():
  webiopi.debug('###')
  time.sleep(5)

# Called by WebIOPi at server shutdown
def destroy():
  webiopi.debug('@ destroy')

# get AV action (to show correct page on other devices)
@webiopi.macro
def getAction():
  webiopi.debug('@ getAction')
  webiopi.debug('# globalAction='+globalAction)
  return globalAction

# set AV action
@webiopi.macro
def setAction(action):
  global globalAction
  webiopi.debug('@ setAction')
  webiopi.debug('# action='+action)
  globalAction = action
  return 0

# proxy request to URL with data and headers
@webiopi.macro
def rPiHTTPproxy(httpVerb,protocol,host,path,data,headers):
  webiopi.debug('@ rPiHTTPproxy')
  # get & display parameters
  httpVerb = urllib.parse.unquote(httpVerb)
  protocol = urllib.parse.unquote(protocol)
  host = urllib.parse.unquote(host)
  path = urllib.parse.unquote(path)
  url = protocol + host + path
  data = urllib.parse.unquote(data)
  headers = urllib.parse.unquote(headers)
  webiopi.debug('# httpVerb='+httpVerb)
  webiopi.debug('# protocol='+protocol);
  webiopi.debug('# host='+host)
  webiopi.debug('# path='+path)
  webiopi.debug('# data='+data)
  webiopi.debug('# url='+url)
  webiopi.debug('# headers='+headers)
  # decide how to construct the message
  if not headers:
    if httpVerb == "GET":
      # construct GET request
      webiopi.debug('# GET w/o headers')
      req = urllib.request.Request(url)
    elif httpVerb == "PUT":
      # construct PUT request
      webiopi.debug('# PUT w/o headers')
      req = urllib.request.Request(url, data.encode('utf-8'))
      req.get_method = lambda: 'PUT'
    else:
      # construct POST request
      webiopi.debug('# POST w/o headers')
      req = urllib.request.Request(url, data.encode('utf-8'))
  else:
    headers = ast.literal_eval(headers)
    webiopi.debug('# headers object=')
    webiopi.debug(headers)
    if httpVerb == "GET":
      # construct GET request
      webiopi.debug('# GET with headers')
      req = urllib.request.Request(url, None, headers)
    elif httpVerb == "PUT":
      # construct PUT request
      webiopi.debug('# PUT with headers')
      if protocol == 'https://':
        conn = http.client.HTTPSConnection(host)
      else:
        conn = http.client.HTTPConnection(host)
      # PUT with headers requires extra attention, so send here & return response
      conn.connect()
      conn.request("PUT", path, body=data, headers=headers)
      resp = conn.getresponse()
      webiopi.debug(resp.status)
      webiopi.debug(resp.reason)
      responseBytes = resp.read()
      try:
        response = responseBytes.decode('utf-8')
        webiopi.debug('# rPiHTTPproxy:response')
        webiopi.debug(response)
        return response
      except:
        webiopi.debug('# rPiHTTPproxy:responseBytes')
        pprint(getmembers(responseBytes))
        return responseBytes
    else:
      # construct POST request
      webiopi.debug('# POST with headers')
      req = urllib.request.Request(url, data.encode('utf-8'), headers)
  # send the request, return response
  f = urllib.request.urlopen(req)
  responseBytes = f.read()
  try:
    response = responseBytes.decode('utf-8')
    webiopi.debug('# rPiHTTPproxy:response')
    webiopi.debug(response)
    return response
  except:
    webiopi.debug('# rPiHTTPproxy:responseBytes')
    webiopi.debug(responseBytes)
    return responseBytes

# open TCP socket with requested IP:PORT, send message, return response
@webiopi.macro
def rPiTCPsocketProxy(ip,port,data):
  webiopi.debug('@ rPiTCPsocketProxy')
  # get & display parameters
  ip = urllib.parse.unquote(ip)
  port = urllib.parse.unquote(port)
  data = urllib.parse.unquote(data)
  data = data+'\x0d'
  dataBytes = data.encode('utf-8')
  webiopi.debug('# ip='+ip)
  webiopi.debug('# port='+port)
  webiopi.debug('# data='+data)
  # open socket
  addr = (ip, int(port))
  sock = socket.socket()
  sock.settimeout(3)
  try:
    sock.connect(addr)
  except:
    webiopi.debug('### could not connect to TCP socket ip:port')
    return 'EXCEPTION: could not connect to TCP socket ip:port'
  sock.settimeout(1)
  # send message
  sock.sendall(dataBytes)
  time.sleep(0.1)
  return # don't bother with response, for now
  # get response
  try:
    responseBytes = sock.recv(1024)
  except:
    webiopi.debug('### no TCP socket response')
    return 'EXCEPTION: no TCP socket response'
  sock.close()
  response = responseBytes.decode('utf-8')
  webiopi.debug('# rPiTCPsocketProxy:response='+response)
  return response

# open UDP socket with requested IP:PORT, send message, return response
@webiopi.macro
def rPiUDPproxy(ip,port,msgNum,data):
  webiopi.debug('@ rPiUDPproxy')
  # get & display parameters
  ip = urllib.parse.unquote(ip)
  port = urllib.parse.unquote(port)
  msgNum = urllib.parse.unquote(msgNum)
  data = urllib.parse.unquote(data)
  data = msgNum+','+data+'\x0d'
  dataBytes = data.encode('utf-8')
  webiopi.debug('# ip='+ip)
  webiopi.debug('# port='+port)
  webiopi.debug('# msgNum='+msgNum)
  webiopi.debug('# data='+data)
  webiopi.debug(dataBytes)
  # open socket & send datagram
  addr = (ip, int(port))
  sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.SOL_UDP)
  sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
  sock.sendto(dataBytes, addr)
  sock.close()
  # av app has no need for response from UDP
  return 1

# send RS232 command, return response
@webiopi.macro
def rPiRS232proxy(command, value):
  global globalSerialPort
  webiopi.debug('@ rPiRS232proxy')
  # get & display parameters
  command = urllib.parse.unquote(command)
  webiopi.debug('# command='+command)
  value = urllib.parse.unquote(value)
  webiopi.debug('# value='+value)
  if value == '=':
    value = '?)'
  # send each character with a tiny gap (many serial devices need time to process each byte and don't buffer incoming bytes)
  data = command+value;
  webiopi.debug('# data='+data)
  for c in data
    b = bytearray(c,'ascii')
    webiopi.debug(b)
    globalSerialPort.write(b)
    time.sleep(0.05);
  # get the response & return it
  responseBytes = globalSerialPort.read(40)
  webiopi.debug(responseBytes)
  try:
    response = responseBytes.decode('utf-8')
    webiopi.debug('# rPiRS232proxy:response')
    webiopi.debug(response)
    return response
  except:
    webiopi.debug('# rPiRS232proxy:responseBytes')
    webiopi.debug(responseBytes)
    return responseBytes

# send push notification to smartphone using Pushover API https://pushover.net/api
@webiopi.macro
def rPiPushNotification(message):
  webiopi.debug('@ rPiPushNotification')
  # get & display parameters
  message = urllib.parse.unquote(message)
  webiopi.debug('# message='+message)
  # construct & send the notification
  data = 'token=' + PUSHOVER_APP_TOKEN + '&user=' + PUSHOVER_USER_KEY + '&message=' + message + '&title=AV'
  webiopi.debug('# data='+data)
  response = rPiHTTPproxy('POST','https://', PUSHOVER_HOST, PUSHOVER_PATH, data, '')
  return response

# get channel JSON from Sky's EPG service
@webiopi.macro
def rPiGetEPGchannelInfo(epgNumber):
  webiopi.debug('@ rPiGetEPGchannelInfo')
  # get & display parameters
  epgNumber = urllib.parse.unquote(epgNumber)
  webiopi.debug('# epgNumber='+epgNumber)
  # construct the message & send it, return response
  url = "http://epgservices.sky.com/5.1.1/api/2.0/channel/json/"+epgNumber+"/now/nnl/1"
  headers = {'User-agent':'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.66 Safari/537.36'}
  req = urllib.request.Request(url, None, headers)
  f = urllib.request.urlopen(req)
  responseBytes = f.read()
  try:
    response = responseBytes.decode('utf-8')
    webiopi.debug('# rPiGetEPGchannelInfo:response')
    webiopi.debug(response)
    return response
  except:
    webiopi.debug('# rPiGetEPGchannelInfo:responseBytes')
    webiopi.debug(responseBytes)
    return responseBytes

Comments

comments powered by Disqus