I recently built a flask app that run on my raspberry pi. Then I turned it down. Why? In order to access the app from outside the home internet, I had to expose my pi to public. After only one day of exposure, my pi received several attack attempts and the flask app then always replied 301. Here are some attack logs:

139.162.83.10 - "GET / HTTP/1.1" 404 -
45.33.115.189 - "GET / HTTP/1.0" 404 -
79.55.11.21 - "GET / HTTP/1.0" 404 -
5.101.40.78 - code 400, message Bad HTTP/0.9 request type ('\x03\x00\x00/*à\x00\x00\x00\x00\x00Cookie:')
5.101.40.78 - "/*àCookie: mstshash=Administr" HTTPStatus.BAD_REQUEST -
123.151.42.61 - "GET http://www.baidu.com/ HTTP/1.1" 404 -
158.85.81.116 - "GET / HTTP/1.1" 404 -

To be more precise, the flask app was still running, but the home router seemed not forwarding the requests to my pi. Anyway, I have no idea of what have happened. I just shut the app down, flashed a fresh new image to my pi.

I want a way to communicate with my pi without exposing it. That is the motivation I start the project chapi, which means "chat with pi".

Ideas

My first thought is to use Google App Engine(GAE) as a communication hub: run a GAE app that receives clients' api requests and stores them, and a server will periodically asks the GAE app for api requests. This idea is fine for asking the server to do something that is not time sensitive. But the drawbacks are obvious. We need to write extra codes for the GAE app, it is not real-time request and response, it is hard for the server's APIs to return something to clients, and etc.

The second thought is to use WeChat as a communication hub. The idea is simple. Use ItChat to build api services on the server, and use WeChat clients on the phones to communicate with the server directly. This is really a good idea. However, ItChat under the hood is WeChat for Web, which means the server build from ItChat might not be able to run for a long period of time. This idea is worth trying. I probably will try this out in the future.

The third thought is similar to the second one: use Pusher service, more specifically, Pusher Channel service. To access its channel service in Python, we need two libraries: Pusher HTTP Python Library(call it Pusher from now on) and Pysher.

The core idea is to use a channel to link a server end and a client end. On the server end, it uses Pysher to subscribe this channel and waiting for request events. Once it receives a request event, the server will handle it. After the request event is done, the server trigger a response event using Pusher.

On the client end, it uses Pysher to subscribe the channel and waiting for response events. The data received from a response event is a response to some request event, and this data should contain enough information to reconstruct the request. To request, the client use Pusher to trigger a request event.

chapi

chapi is an implementation of the third idea. It requires Pusher HTTP Python Library(call it Pusher from now on) and Pysher, as mentioned. To install the requirements:

pip install pusher pysher

To install chapi,

pip install git+https://github.com/wormtooth/chapi.git

At this moment, chapi does not have any docstrings. But it should be easy to read the codes directly since the codes are simple. I will add docstrings in the near future.

Let's look at a piece of server codes.

from chapi import Server
import json

app_id = 'APP_ID'
key = 'KEY'
secret = 'SECRET'
cluster = 'CLUSTER'
server = Server(app_id, key, secret, cluster)

@server.api(name='echo')
def echo(**kwargs):
    return kwargs

def handler(data):
    data = json.loads(data)
    print('received api request {}: {}'.format(data['api'], data['params']))

server.add_request_handler(handler)

server.run()

The credentials app_id, key, secret and cluster can be found on your pusher app dashboard on Pusher. chapi.Server is a subclass of chapi.Pusher, which has the essential functionalities to build the server end. It takes credentials and an optional argument name (default "server") to establish connections. To register an API, we can use decorator chapi.Server.api(name). Internally, chapi.Server maintains a dict with pairs name: f, where f is the decorated function. When a client end requests API name, function f will be called.

chapi.Pusher has two methods enabling custom requests and responses handling: chapi.Pusher.add_request_handler(handler) and chapi.Pusher.add_response_handler(handler). A handler is a function with exactly one positional argument, and this argument is the raw data from an event within the channel. In the above server example, we have a handler handler(data) that prints the request information.

Finally, in order to keep server running, we need call chapi.Server.run(worker_num=1). It will start worker_num threads to run API requests and then block the main thread from existing.

Once the server is running, we can use chapi.Client to request API services as below.

from chapi import Client
import json
import time

app_id = 'APP_ID'
key = 'KEY'
secret = 'SECRET'
cluster = 'CLUSTER'
client = Client(app_id, key, secret, cluster)
wait_time = 2 # make wait_time longer if run in poor connection

def print_result(data):
    data = json.loads(data)
    msg = 'result of {}: {}'
    print(msg.format(data['api'], data['result']))

client.add_response_handler(print_result)

time.sleep(wait_time) # give some time to establish connections

client.request('echo', a='a', b=[1, 2, 3])
client.request('wrong_api', arg='test')

time.sleep(wait_time) # wait for responses

chapi.Client is also a subclass of chapi.Pusher. It has a specific method chapi.Client.request(api, **kwargs) designed to send requests. Argument api is the requested API name, and kwargs will be passed to this API in the server end. To see the results of requests, we need to write our own response handlers. In the client example, we have print_result(data) to print out the API name with its result.

Note that, it needs time to connect to Pusher, so we have wait_time in the example.

Improvements

I have used chapi to build a server on my raspberry pi. So far, it is satisfying. On my phone, I can use Pythonista to send requests to my pi and receive responses from it. For android devices, one can use QPython to build a client.

For now, I would like to improve chapi by finishing the following tasks.

  • Design a data class for requests and responses. Timestamp should be split into two attributes: request_time and response_time.

  • Add docstrings.

  • Enable chapi.Server to register, delete and manipulate APIs.

Any suggestions will be welcomed!