Chapi
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!