Beginning bumper's new journey #4

Merged
bmartin5692 merged 37 commits from dev into master 2019-02-22 14:34:25 +01:00
32 changed files with 2910 additions and 360 deletions

1
.gitignore vendored
View file

@ -3,3 +3,4 @@ __pycache__
Pipfile.lock
/bumper.sublime-project
/bumper.sublime-workspace
.vscode/

View file

@ -1,13 +1,11 @@
[[source]]
url = "https://pypi.python.org/simple"
verify_ssl = true
name = "pypi"
[packages]
hbmqtt = "*"
aiohttp = "*"
black = "*"
[dev-packages]

214
README.md
View file

@ -1,109 +1,201 @@
Bumper
=====
A standalone implementation of the central server used by Ecovacs
Deebot cleaning robots to relay data between the robot and client.
Tested on Ecovacs Deebot M81 Pro but should be compatible with
most wifi-enabled robots that use the Ecovacs Android app.
# Bumper
A standalone implementation of the central server used by Ecovacs Deebot cleaning robots to relay data between the robot and client.
*Please note: this software is experimental and not ready
for production use. Use at your own risk.*
A big thanks to the original project creator @torbjornaxelsson, without his work this project would have taken much longer to build. The project reached its original goal and remained in a stable, but stale state with the last commit in Dec 2017. Since the original release of bumper newer bots have been released that use different protocols, and a growing Ecovacs user base has sparked further interest in the project.
In early 2019 after making massive changes to the codebase and with ideas for additional feature development, it was decided that @bmartin5692 would take over active development. The fork was detached and all future development of bumper will take place here.
The current master branch is unstable and based on the work for adding D900 series support to bumper, along with other code changes. Additional testing is needed with bots that use the XMPP protocols to ensure compatibility, so at this time it can only be guaranteed for 900-series bots (900/901/907).
If you want a working version of bumper for XMPP bots (M81 Pro, and others), the original code base has been branched off as [v0.1.0](https://github.com/bmartin5692/bumper/tree/v0.1.0
) and will remain in the original state. Bumper needs users of models that use XMPP to assist with development efforts in order to ensure compatability as bumper moves forward.
*Please note: this software is experimental and not ready for production use. Use at your own risk.*
## Compatibility
As work to reverse the protocols and provide a self-hosted central server is still in progress, Bumper has had limited testing. There are a number of EcoVacs models and varying protocols that it hasn't been tested against. So far two different protocols have been seen, **XMPP** and **MQTT**. Bumper should be compatible with most wifi-enabled robots that use the Ecovacs Android app, but has only been reported to work on the below:
| Model | Protocol Used | Bumper Version |
|--|--|--|
| Deebot 900/901 | MQTT | master |
| Deebot M81 Pro | XMPP | v0.1.0 |
For more information about the protocols and how it works, see the [How does it work?](#how-does-it-work) section at the end. If you test against another model and it works, please report it so it can be added to the list.
## Why?
For fun, mostly :)
But seriously, there are a serveral reasons for
elminating the central server:
1. Convenience: It works without an internet connection or if
Ecovacs servers are down
2. Performance: No need for messages to travel to Ecovacs server
and back.
3. Security: We can completely isolate the robot from the public
Internet.
But seriously, there are a several reasons for eliminating the central server:
1. Convenience: It works without an internet connection or if Ecovacs servers are down
2. Performance: No need for messages to travel to Ecovacs server and back.
3. Security: We can completely isolate the robot from the public Internet.
## Requirements
- An Ecovacs wifi-enabled robot
- A computer on your local network to run the Bumper server
- Python 3 and pipenv
- A network router that has functionality for overriding DNS queries
- A client that can connect to Bumper and talk to the robot over the
Ecovacs protocol.
I recomend [Sucks](https://github.com/wpietri/sucks).
It can run on the same computer and requires only minimal modification
to work with Bumper.
- A client that can connect to Bumper and talk to the robot over the Ecovacs protocol.
- The Android or iOS apps can be used if configured properly. See [Using with the official Android/iOS App](#using-with-the-official-androidios-app) below.
- [Sucks](https://github.com/wpietri/sucks) can also be used, which can act as a client and control the robots via command-line.
## Usage
- Run `pipenv install` to install dependencies
- Configure your Ecovacs vacuum using the official mobile app (if you haven't done this already)
### DNS
You need to configure your router to point DNS locally to where Bumper is running.
The easiest way is overriding the main domains used by EcoVacs using DNSMasq/PiHole, by adding address entries in a custom config:
- Run `pipenv install` to install dependencies
address=/ecouser.net/{bumper server ip}
address=/ecovacs.com/{bumper server ip}
address=/ecovacs.net/{bumper server ip}
- Configure your Ecovacs vacuum using the official mobile app
(if you haven't done this already)
If this isn't an option, you'll need to configure your router DNS to point a number of domains used by the app/robot to the Bumper server.
**Note:** Depending on country, your phone may be using a different domain. Most of these domains contain country-specific placeholders.
- Example: If you see `eco-{countrycode}-api.ecovacs.com` and you live in the US/North America you would use: `eco-us-api.ecovacs.com`
- Configure your router DNS to point the domain lbo.ecovacs.net to
the machine that will run the Bumper server
| Address | Description |
|--|--|
| `lb-{countrycode}.ecovacs.net` | Load-balancer that is checked by the app/robot |
| `eco-{countrycode}-api.ecovacs.com` | Used for Login |
| `portal-{countrycode}.ecouser.net` | Used for Login and Rest API |
| `msg-{countrycode}.ecouser.net` | Used for XMPP |
| `mq-ww.ecouser.net` | Used for MQTT |
### Starting Bumper
- Start Bumper with `pipenv run python bumper.py`
- Reboot your robot (remove and re-insert the battery pack,
then power it on)
- Reboot your robot
- **Note:** Some models may require removing and re-inserting the battery pack.
- This doesn't seem to be required for models that don't have easily accessible batteries such as the 900/901.
- If your configuration is correct, the robot will connect to Bumper
within about 30 seconds. Bumper will output informaiton about the
connection status.
- If your configuration is correct, the robot will connect to Bumper within about 30 seconds. Bumper will output information about the connection status.
- Configure your client - see below.
## Using with Sucks
Instructions (verified to work with Sucks 0.8.3)
- Download and install [Sucks](https://github.com/wpietri/sucks)
- See the [example script](examples/sucks.py) for how to connect
## Using with the official Android/iOS App
Bumper *can* be used with the official app, but with limitations. Your phone needs to use your DNS server with custom settings, and you ***must*** import Bumper's CA cert and trust it before the app will work.
### DNS
- Configure your DNS server as described above in the [DNS](#dns) section.
### Import the Bumper CA Cert
- E-mail yourself the Bumper CA cert (located at `./certs/CA/cacert.crt`)
**Note:** Make sure you select the `cacert.crt` file, which is a DER encoded version that will work on either Android or iOS.
Bumper *can* be used with the official app, but with limitations. Your
phone needs to use your DNS server with custom settings, and the app
authenticates via Ecovacs central servers every time you start it.
![Example of emailing CA cert](docs/images/emailcert.png)
- Configure your DNS server to point the domains msg-na.ecouser.net and
msg-ww.ecouser.net to the machine running Bumpy. Note: Depending on
country, your phone may be using a different domain.
- Import the cert as a CA, and trust it
- Instructions here are different for [iOS](#importing-the-ca-cert-on-ios) vs [Android](#importing-the-ca-cert-on-android)
#### Importing the CA Cert on iOS
- Login to the app. It will authenticate and ask for a list of robots
from Ecovacs central servers.
1. Open the e-mail on your iOS device, and click the attached cert
- The app will now connect to Bumper and try to ping the robot. Bumper
responds to this ping to tell the app that the robot is online.
![Example of email on iOS device](docs/images/ios_email_cert.png)
- You should now be able to control the robot from the app.
2. Install the profile by clicking "Install", and entering your pass code if prompted
![Example of install profile on iOS device](docs/images/ios_install_profile.png)
3. Accept the certificate warning by clicking "Install" again
![Example of cert warning on iOS device](docs/images/ios_cert_warning_install.png)
4. Click "Done" to exit the profile installation
5. Go to Settings > General > About
6. Scroll to the bottom and click "Certificate Trust Settings"
7. Enable Full Trust for the Bumper CA Cert, by moving the slider to the right
![Example of enable trust cert on iOS device](docs/images/ios_cert_trust.png)
![Example of enable trust cert on iOS device 2](docs/images/ios_cert_trust_continue.png)
8. Click continue when prompted
9. That's it, you can now [Use the app](#use-the-app)
#### Importing the CA Cert on Android
1. Open the e-mail on your Android device
**Quick Method**
2. Click the cert, and if prompted provide a name
3. Under "Used for", select "VPN and apps"
**Long Method**
2. Save the attached cert file
3. Go to Settings > Lock screen and security > Other security settings
4. Under "Credential storage", click "Install from device storage"
5. Browse to the downloaded cert, select it, then click "Done"
6. Click the cert, and if prompted provide a name
7. Under "Used for", select "VPN and apps"
Now, start [using the app](#use-the-app).
### Use the app
- Open the app
- At this time there is no authentication layer, you can enter any e-mail address and password (as long as it is 6 characters) and you will be authenticated
- If your robot has already checked into Bumper, then it will be available in the list of robots
- The app now does a ping to the robot to make sure it is online, and if it is you can now control the robot
## How does it work?
**App/Authentication**
Ecovacs robots communicate over the XMPP (jabber) protocol. Messages
are relayed by a central XMPP server.
EcoVacs servers provide authentication of accounts and match those up to registered robots. Once authenticated, users can control the robots via the app. ***Bumper*** provides an implementation of the central servers providing authentication and matching.
Bumper exposes a simulated XMPP server that implements the nessecary
functions for relaying messages between a robot and a client.
**Robots**
When the robot boots up it sends a HTTP request to lbo.ecovacs.net:8007
asking for the IP address and port of the XMPP server. Because of our
DNS override, this request will be received by Bumper. We tell the robot
to connect over XMPP to our local machine. Voilá!
So far two protocols have been identified that various models of EcoVacs robots use: **XMPP** and **MQTT**. These appear to be mutually exclusive and your robot model will use one of these two protocols for communication. ***Bumper*** provides an implementation of both protocols handling communication between the app and robots.
----
### Login/Authentication/RestAPI
***Bumper*** provides a fully simulated central server that handles login/authentication for the app/clients.
The EcoVacs app encrypts the username/password with the public key of EcoVacs when authenticating. Since we don't have the private key to decrypt, there is no way to provide true security and authentication.
Future versions may add additional options here for security.
Bots have no authentication and once a robot has been configured to access your WiFi it never needs to communicate with EcoVac's servers again.
----
### XMPP
*Example Model:* Deebot M81 Pro
The robot utilizes XMPP for control. When the robot boots up it sends a HTTP request to `lbo.ecovacs.net:8007` asking for the IP address and port of the XMPP server. Because of our DNS override, this request will be received by Bumper. We tell the robot to connect over XMPP to our local machine.
Both the app and robot connect to the central XMPP server, which relays messages between the app and robot. The messages contain commands for the robot to execute or responses and statuses of the robot.
***Bumper*** exposes a simulated XMPP server that implements the necessary functions for relaying messages between a robot and a client, acting as the central server.
**Note:** It's been observed that the apps will attempt to utilize XMPP regardless of robot model. For models that utilize MQTT, no activity is performed over XMPP just pings.
----
### MQTT
*Example Model:* Deebot 900/901
The robot utilizes MQTT for control. On startup it connects to an MQTT broker (`mq-{countrycode}.ecovacs.com`) and subscribes to a `p2p` topic where commands are issued.
The app also connects to the MQTT broker and subscribes to a `attr` topic where the robot will periodically post status and location updates.
The app receives status updates, for example battery status or robot position information, periodically via the `attr` topic.
However, commands issued via the app/client are not published directly to the `p2p` topic that the robot subscribes to. When the app sends a command such as "Clean", this is sent via a Rest API to the server at `portal-{countrycode}.ecouser.net`.
The Rest API provided by `portal-{countrycode}.ecouser.net` receives the command and passes it to a "helper" bot that has permission to publish to the `p2p` topic the robot is subscribed to. The "helper" bot waits for a response to be published by the robot and then passes that response back to the server for the Rest API to send back as a response.
***Bumper*** provides a simulated RestAPI and "helper" bot, performing the same function as the central server above.
## Thanks
A big thanks to the original project creator @torbjornaxelsson, without his work this project would have taken much longer to build.
Bumper woulden't exist without [Sucks](https://github.com/wpietri/sucks),
an open source client for Ecovacs robots. Big thanks to @wpietri and
contributors!
Bumper wouldn't exist without [Sucks](https://github.com/wpietri/sucks), an open source client for Ecovacs robots. Big thanks to @wpietri and contributors!

116
bumper.py
View file

@ -3,17 +3,115 @@
import logging
import bumper
import sys, socket
import time
import platform
logging.basicConfig(level=logging.INFO,
format='%(asctime)s %(levelname)-8s %(message)s')
conf_address = (socket.gethostbyname(socket.gethostname()), 8007)
xmpp_address = (socket.gethostbyname(socket.gethostname()), 5223)
def main():
args = sys.argv
# start conf server (async)
conf_server = bumper.ConfServer(conf_address, ssl=False, async=True)
if len(args) > 0:
if "--debug" in args:
logging.basicConfig(
level=logging.DEBUG,
format="[%(asctime)s] :: %(levelname)s :: %(name)s :: %(module)s :: %(funcName)s :: %(lineno)d :: %(message)s",
)
else:
logging.basicConfig(
level=logging.INFO,
format="[%(asctime)s] :: %(levelname)s :: %(name)s :: %(message)s",
)
# format="[%(asctime)s] :: %(levelname)s :: %(name)s :: %(module)s :: %(funcName)s :: %(lineno)d :: %(message)s")
# start xmpp server (sync)
xmpp_server = bumper.XMPPServer(xmpp_address)
if platform.system() == "Darwin": # If a Mac, use 0.0.0.0 for listening
listen_host = "0.0.0.0"
else:
listen_host = socket.gethostbyname(socket.gethostname())
# listen_host = "localhost" #Try this if the above doesn't work
conf_server.disconnect()
conf_address_443 = (listen_host, 443)
conf_address_8007 = (listen_host, 8007)
xmpp_address = (listen_host, 5223)
mqtt_address = (listen_host, 8883)
xmpp_server = bumper.XMPPServer(
xmpp_address,
bumper_users=bumper.bumper_users_var,
bumper_bots=bumper.bumper_bots_var,
bumper_clients=bumper.bumper_clients_var,
)
mqtt_server = bumper.MQTTServer(
mqtt_address,
bumper_users=bumper.bumper_users_var,
bumper_bots=bumper.bumper_bots_var,
bumper_clients=bumper.bumper_clients_var,
)
mqtt_helperbot = bumper.MQTTHelperBot(
mqtt_address,
bumper_bots=bumper.bumper_bots_var,
bumper_clients=bumper.bumper_clients_var,
)
conf_server = bumper.ConfServer(
conf_address_443,
usessl=True,
bumper_users=bumper.bumper_users_var,
bumper_bots=bumper.bumper_bots_var,
bumper_clients=bumper.bumper_clients_var,
helperbot=mqtt_helperbot,
)
conf_server_2 = bumper.ConfServer(
conf_address_8007,
usessl=False,
bumper_users=bumper.bumper_users_var,
bumper_bots=bumper.bumper_bots_var,
bumper_clients=bumper.bumper_clients_var,
helperbot=mqtt_helperbot,
)
# add user
# users = bumper.bumper_users_var.get()
# user1 = bumper.BumperUser('user1')
# user1.add_device('devid')
# user1.add_bot('bot_did')
# users.append(user1)
# bumper.bumper_users_var.set(users)
# start xmpp server on port 5223 (sync)
xmpp_server.run(run_async=True) # Start in new thread
# start mqtt server on port 8883 (async)
mqtt_server.run(run_async=True) # Start in new thread
time.sleep(1.5) # Wait for broker startup
# start mqtt_helperbot (async)
mqtt_helperbot.run(run_async=True) # Start in new thread
# start conf server on port 443 (async) - Used for most https calls
conf_server.run(run_async=True) # Start in new thread
# start conf server on port 8007 (async) - Used for a load balancer request
conf_server_2.run(run_async=True) # Start in new thread
while True:
try:
time.sleep(0.25)
# WIP: Remove clients that have disconnected
# remove_clients = bumper.bumper_removeclients_var.get()
# if len(remove_clients) > 0:
# for uid in remove_clients:
# if uid != "":
# xmpp_server.remove_client_byuid(uid) #Remove clients from xmpp server
# remove_clients.remove(uid)
# bumper.bumper_removeclients_var.set(remove_clients)
except KeyboardInterrupt:
bumper.bumperlog.info("Bumper Exiting - Keyboard Interrupt")
print("Bumper Exiting")
exit(1)
if __name__ == "__main__":
main()

View file

@ -1,4 +1,213 @@
#!/usr/bin/env python3
from .confserver import ConfServer
from .mqttserver import MQTTServer
from .mqttserver import MQTTHelperBot
from .xmppserver import XMPPServer
import asyncio
import contextvars
import time
import logging
from base64 import b64decode, b64encode
bumper_users_var = contextvars.ContextVar("bumper_users", default=[])
bumper_clients_var = contextvars.ContextVar("bumper_clients", default=[])
bumper_bots_var = contextvars.ContextVar("bumper_bots", default=[])
ca_cert = "./certs/CA/cacert.pem"
server_cert = "./certs/cert.pem"
server_key = "./certs/key.pem"
use_auth = False
# Logs
bumperlog = logging.getLogger("bumper")
confserverlog = logging.getLogger("confserver")
# Override the logging level
# confserverlog.setLevel(logging.INFO)
mqttserverlog = logging.getLogger("mqttserver")
# Override the logging level
# mqttserverlog.setLevel(logging.INFO)
helperbotlog = logging.getLogger("helperbot")
# Override the logging level
# helperbotlog.setLevel(logging.INFO)
xmppserverlog = logging.getLogger("xmppserver")
# Override the logging level
# xmppserverlog.setLevel(logging.INFO)
def get_milli_time(timetoconvert):
return int(round(timetoconvert * 1000))
class BumperUser(object):
def __init__(self, userid=""):
self.userid = userid
self.devices = []
self.tokens = []
self.authcodes = []
self.bots = []
def add_device(self, devid):
if not devid in self.devices:
self.devices.append(devid)
def remove_device(self, devid):
if devid in self.devices:
self.devices.remove(devid)
def add_token(self, token):
if not token in self.tokens:
self.tokens.append(token)
def revoke_token(self, token):
if token in self.tokens:
self.tokens.remove(token)
def add_authcode(self, authcode):
if not authcode in self.authcodes:
self.authcodes.append(authcode)
def revoke_authcode(self, authcode):
if authcode in self.authcodes:
self.authcodes.remove(authcode)
def add_bot(self, botdid):
if not botdid in self.bots:
self.bots.append(botdid)
def remove_bot(self, botdid):
if botdid in self.bots:
self.bots.remove(botdid)
class VacBotDevice(object):
def __init__(
self,
did="",
vac_bot_device_class="",
resource="",
name="",
nick="",
company="eco-ng",
):
self.vac_bot_device_class = vac_bot_device_class
self.company = company
self.did = did
self.name = name
self.nick = nick
self.resource = resource
self.mqtt_connection = False
self.xmpp_connection = False
def asdict(self):
return {
"class": self.vac_bot_device_class,
"company": self.company,
"did": self.did,
"name": self.name,
"nick": self.nick,
"resource": self.resource,
}
class VacBotClient(object):
def __init__(self, userid="", realm="", token=""):
self.userid = userid
self.realm = realm
self.resource = token
self.mqtt_connection = False
self.xmpp_connection = False
def asdict(self):
return {"userid": self.userid, "realm": self.realm, "resource": self.resource}
def check_authcode(uid, authcode):
users = bumper_users_var.get()
for user in users:
if uid == "fuid_{}".format(user.userid) and authcode in user.authcodes:
return True
return False
def add_bot(sn, did, devclass, resource):
newbot = VacBotDevice()
newbot.did = did
newbot.name = sn
newbot.vac_bot_device_class = devclass
newbot.resource = resource
bots = bumper_bots_var.get()
existingbot = False
for bot in bots:
if bot.did == newbot.did:
existingbot = True
if existingbot == False:
bots.append(newbot)
bumperlog.info("new bot added SN: {} DID: {}".format(newbot.name, newbot.did))
bumper_bots_var.set(bots)
def add_client(userid, realm, resource):
newclient = VacBotClient()
newclient.userid = userid
newclient.realm = realm
newclient.resource = resource
clients = bumper_clients_var.get()
existingclient = False
for client in clients:
if client.userid == newclient.userid:
existingclient = True
if existingclient == False:
clients.append(newclient)
bumperlog.info("new client added {}".format(newclient.userid))
bumper_clients_var.set(clients)
RETURN_API_SUCCESS = "0000"
ERR_ACTIVATE_TOKEN_TIMEOUT = "1006"
ERR_COMMON = "0001"
ERR_DEFAULT = "9000"
ERR_EMAIL_NON_EXIST = "1002"
ERR_EMAIL_SEND_TIME_LIMIT = "1011"
ERR_EMAIL_USED = "1001"
ERR_INTERFACE_AUTH = "0002"
ERR_PARAM_INVALID = "0003"
ERR_PWD_WRONG = "1005"
ERR_RESET_PWD_TOKEN_TIMEOUT = "1007"
ERR_TIMESTAMP_INVALID = "0005"
ERR_TOKEN_INVALID = "0004"
ERR_USER_DISABLE = "1004"
ERR_USER_NOT_ACTIVATED = "1003"
ERR_WRONG_COMFIRM_PWD = "10010"
ERR_WRONG_EMAIL_ADDRESS = "1008"
ERR_WRONG_PWD_FROMATE = "1009"
API_ERRORS = {
RETURN_API_SUCCESS: "0000",
ERR_ACTIVATE_TOKEN_TIMEOUT: "1006",
ERR_COMMON: "0001",
ERR_DEFAULT: "9000",
ERR_EMAIL_NON_EXIST: "1002",
ERR_EMAIL_SEND_TIME_LIMIT: "1011",
ERR_EMAIL_USED: "1001",
ERR_INTERFACE_AUTH: "0002",
ERR_PARAM_INVALID: "0003",
ERR_PWD_WRONG: "1005",
ERR_RESET_PWD_TOKEN_TIMEOUT: "1007",
ERR_TIMESTAMP_INVALID: "0005",
ERR_TOKEN_INVALID: "0004",
ERR_USER_DISABLE: "1004",
ERR_USER_NOT_ACTIVATED: "1003",
ERR_WRONG_COMFIRM_PWD: "10010",
ERR_WRONG_EMAIL_ADDRESS: "1008",
ERR_WRONG_PWD_FROMATE: "1009",
}

View file

@ -1,88 +1,660 @@
#!/usr/bin/env python3
from http.server import HTTPServer
from http.server import BaseHTTPRequestHandler
from http import HTTPStatus
from threading import Thread
import socket, logging, ssl, json, sys
import socket, logging, ssl, json
import string
import random
import bumper
import time
from datetime import datetime, timedelta
import asyncio
import contextvars
from aiohttp import web
import uuid
class RequestHandler(BaseHTTPRequestHandler):
def do_POST(self):
class aiohttp_filter(logging.Filter):
def filter(self, record):
if (
record.name == "aiohttp.access" and record.levelno == 20
): # Filters aiohttp.access log to switch it from INFO to DEBUG
record.levelno = 10
record.levelname = "DEBUG"
if (
record.levelno == 10
and logging.getLogger("confserver").getEffectiveLevel() == 10
):
return True
else:
return False
confserverlog = logging.getLogger("confserver")
logging.getLogger("asyncio").setLevel(logging.CRITICAL + 1) # Ignore this logger
logging.getLogger("aiohttp.access").addFilter(aiohttp_filter())
class ConfServer:
bumper_clients = contextvars.ContextVar
bumper_bots = contextvars.ContextVar
def __init__(
self,
address,
usessl=False,
bumper_users=contextvars.ContextVar,
bumper_bots=contextvars.ContextVar,
bumper_clients=contextvars.ContextVar,
helperbot=None,
):
self.bumper_users = bumper_users
self.bumper_bots = bumper_bots
self.bumper_clients = bumper_clients
self.helperbot = helperbot
self.usessl = usessl
self.address = address
self.confthread = None
def run(self, run_async=False):
try:
self.protocol_version = 'HTTP/1.1'
content_length = int(self.headers['Content-Length'])
post_data = self.rfile.read(content_length)
logging.debug("Headers: " + str(self.headers))
request_body = post_data.decode('utf-8')
logging.debug("Request: " + request_body)
json_body = json.loads(request_body)
todo = json_body['todo']
if todo == 'FindBest':
service = json_body['service']
if service == 'EcoMsgNew':
body = '{{"result":"ok","ip":"{}","port":5223}}'.format(socket.gethostbyname(socket.gethostname()))
elif service == 'EcoUpdate':
body = '{"result":"ok","ip":"47.88.66.164","port":8005}'
elif todo == 'loginByItToken':
body = "{{'todo': 'result', 'result': 'ok', 'userId': '{}', 'resource': '{}', 'token': '{}'}}".format(json_body['userId'], json_body['resource'], json_body['token'])
elif todo == 'GetDeviceList':
body = "{'todo': 'result', 'result': 'ok', 'devices': [{'did': '{}', 'name': '{}', 'class': '{}', 'resource': 'atom', 'nick': None, 'company': 'eco'}]}"
logging.debug("Response: " + body)
body = body.encode()
self.send_response(HTTPStatus.OK)
self.send_header('Content-Type', 'application/json; charset=utf-8')
self.send_header('Connection', 'keep-alive')
self.send_header('Content-Length', len(body))
self.end_headers()
self.wfile.write(body)
except Exception as e:
logging.error('ConfServer: {}'.format(e))
if run_async:
confserverlog.debug("Starting ConfServer Thread: 1")
self.confthread = Thread(
name="ConfServer_{}_Thread".format(self.address[1]),
target=self.run_server,
)
self.confthread.setDaemon(True)
self.confthread.start()
class HTTPServerThread(HTTPServer, Thread):
def __init__(self, server_address):
Thread.__init__(self)
self.server_address = server_address
self.handler = RequestHandler
self.exit_flag = False
def handle_error(self, request, client_address):
self.close_request(request)
def run(self):
try:
HTTPServer.__init__(self, self.server_address, self.handler)
logging.info('ConfServer: listening on {}:{}'.format(self.server_address[0], self.server_address[1]))
while not self.exit_flag:
self.handle_request()
except Exception as e:
logging.error('ConfServer: {}'.format(e))
def disconnect(self):
self.exit_flag = True
# make a connection to
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(self.server_address)
s.close()
class ConfServer():
def __init__(self, address, ssl=False, async=True):
try:
self.async = async
self.server = HTTPServerThread(address)
if ssl:
self.server.socket = ssl.wrap_socket(self.server.socket, keyfile='./certs/key.pem', certfile='./certs/cert.pem', server_side=True)
if self.async:
self.server.start()
else:
try:
self.server.run()
self.run_server()
except KeyboardInterrupt:
self.disconnect()
except Exception as e:
logging.error('ConfServer: {}'.format(e))
confserverlog.exception("{}".format(e))
def run_server(self):
logging.info("Starting ConfServer at {}".format(self.address))
print("Starting ConfServer at {}".format(self.address))
try:
loop = asyncio.get_event_loop()
except:
loop = asyncio.new_event_loop()
try:
loop.run_until_complete(self.start_server())
loop.run_forever()
except Exception as e:
confserverlog.exception("{}".format(e))
async def start_server(self):
try:
app = web.Application()
app.add_routes(
[
web.get("", self.handle_base),
web.get(
"/{apiversion}/private/{country}/{language}/{devid}/{apptype}/{appversion}/{devtype}/{aid}/user/login",
self.handle_login,
),
web.get(
"/{apiversion}/private/{country}/{language}/{devid}/{apptype}/{appversion}/{devtype}/{aid}/user/checkLogin",
self.handle_login,
),
web.get(
"/{apiversion}/private/{country}/{language}/{devid}/{apptype}/{appversion}/{devtype}/{aid}/user/logout",
self.handle_logout,
),
web.get(
"/{apiversion}/private/{country}/{language}/{devid}/{apptype}/{appversion}/{devtype}/{aid}/user/getAuthCode",
self.handle_getAuthCode,
),
web.get(
"/{apiversion}/private/{country}/{language}/{devid}/{apptype}/{appversion}/{devtype}/{aid}/user/checkAgreement",
self.handle_checkAgreement,
),
web.get(
"/{apiversion}/private/{country}/{language}/{devid}/{apptype}/{appversion}/{devtype}/{aid}/common/checkVersion",
self.handle_checkVersion,
),
web.get(
"/{apiversion}/private/{country}/{language}/{devid}/{apptype}/{appversion}/{devtype}/{aid}/campaign/homePageAlert",
self.handle_homePageAlert,
),
web.post("/api/users/user.do", self.handle_usersapi),
web.get("/api/users/user.do", self.handle_usersapi),
web.post(
"/api/pim/product/getProductIotMap",
self.handle_getProductIotMap,
),
web.post(
"/api/iot/devmanager.do", self.handle_devmanager_botcommand
),
web.post("/lookup.do", self.handle_lookup),
]
)
# Direct register from app:
# /{apiversion}/private/{country}/{language}/{devid}/{apptype}/{appversion}/{devtype}/{aid}/user/directRegister
runner = web.AppRunner(app)
await runner.setup()
if self.usessl:
ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
ssl_ctx.load_cert_chain(bumper.server_cert, bumper.server_key)
site = web.TCPSite(
runner,
host=self.address[0],
port=self.address[1],
ssl_context=ssl_ctx,
)
else:
site = web.TCPSite(runner, host=self.address[0], port=self.address[1])
await site.start()
except PermissionError as e:
if "bind" in e.strerror:
confserverlog.exception(
"Error binding confserver, exiting. Try using a different hostname or IP - {}".format(
e
)
)
exit(1)
except Exception as e:
confserverlog.exception("{}".format(e))
exit(1)
async def handle_base(self, request):
try:
text = "Bumper!"
return web.json_response(text)
except Exception as e:
confserverlog.exception("{}".format(e))
async def handle_login(self, request):
try:
user_devid = request.match_info.get("devid", "")
countrycode = request.match_info.get("country", "us")
confserverlog.info(
"client with devid {} attempting login".format(user_devid)
)
if bumper.use_auth:
if (
not user_devid == ""
): # Performing basic "auth" using devid, super insecure
users = self.bumper_users.get()
for user in users:
if user_devid in user.devices:
tmpaccesstoken = ""
if "checkLogin" in request.path:
if request.query[
"accessToken"
] in user.tokens and request.query[
"uid"
] == "fuid_{}".format(
user.userid
):
tmpaccesstoken = request.query["accessToken"]
body = {
"code": bumper.RETURN_API_SUCCESS,
"data": {
"accessToken": tmpaccesstoken, # Random chars 32 length
"country": countrycode,
"email": "null@null.com",
"uid": "fuid_{}".format(user.userid),
"username": "fusername_{}".format(
user.userid
),
},
"msg": "操作成功",
"time": bumper.get_milli_time(time.time()),
}
else:
body = {
"code": bumper.ERR_TOKEN_INVALID,
"data": None,
"msg": "当前密码错误",
"time": bumper.get_milli_time(time.time()),
}
else:
if tmpaccesstoken == "":
tmpaccesstoken = uuid.uuid4().hex
user.add_token(tmpaccesstoken)
body = {
"code": bumper.RETURN_API_SUCCESS,
"data": {
"accessToken": tmpaccesstoken, # Random chars 32 length
"country": countrycode,
"email": "null@null.com",
"uid": "fuid_{}".format(user.userid),
"username": "fusername_{}".format(user.userid),
},
"msg": "操作成功",
"time": bumper.get_milli_time(time.time()),
}
self.bumper_users.set(users)
return web.json_response(body)
body = {
"code": bumper.ERR_USER_NOT_ACTIVATED,
"data": None,
"msg": "当前密码错误",
"time": bumper.get_milli_time(time.time()),
}
return web.json_response(body)
else:
return web.json_response(
self._auth_any(user_devid, countrycode, request)
)
except Exception as e:
confserverlog.exception("{}".format(e))
def _auth_any(self, devid, country, request):
try:
user_devid = devid
countrycode = country
tmpaccesstoken = ""
users = self.bumper_users.get()
bots = self.bumper_bots.get()
if len(users) > 0:
tmpuser = users[0]
tmpuser.add_device(user_devid)
else:
tmpuser = bumper.BumperUser("tmpuser")
users.append(tmpuser)
tmpuser.add_device(user_devid)
for bot in bots:
tmpuser.add_bot(bot.did)
if "checkLogin" in request.path:
tmpaccesstoken = request.query["accessToken"]
tmpuser.add_token(tmpaccesstoken)
body = {
"code": bumper.RETURN_API_SUCCESS,
"data": {
"accessToken": tmpaccesstoken, # Random chars 32 length
"country": countrycode,
"email": "null@null.com",
"uid": "fuid_{}".format(tmpuser.userid),
"username": "fusername_{}".format(tmpuser.userid),
},
"msg": "操作成功",
"time": bumper.get_milli_time(time.time()),
}
else:
if tmpaccesstoken == "":
tmpaccesstoken = uuid.uuid4().hex
tmpuser.add_token(tmpaccesstoken)
body = {
"code": bumper.RETURN_API_SUCCESS,
"data": {
"accessToken": tmpaccesstoken, # Random chars 32 length
"country": countrycode,
"email": "null@null.com",
"uid": "fuid_{}".format(tmpuser.userid),
"username": "fusername_{}".format(tmpuser.userid),
},
"msg": "操作成功",
"time": bumper.get_milli_time(time.time()),
}
self.bumper_users.set(users)
return body
except Exception as e:
confserverlog.exception("{}".format(e))
async def handle_logout(self, request):
try:
user_devid = request.match_info.get("devid", "")
if not user_devid == "":
users = self.bumper_users.get()
for user in users:
if user_devid in user.devices:
if (
request.query["uid"] == "fuid_{}".format(user.userid)
and request.query["accessToken"] in user.tokens
):
user.revoke_token(request.query["accessToken"])
self.bumper_users.set(users)
body = {
"code": bumper.RETURN_API_SUCCESS,
"data": None,
"msg": "操作成功",
"time": bumper.get_milli_time(time.time()),
}
return web.json_response(body)
except Exception as e:
confserverlog.exception("{}".format(e))
async def handle_getAuthCode(self, request):
try:
user_devid = request.match_info.get("devid", "")
if not user_devid == "":
users = self.bumper_users.get()
if len(users) > 0:
for user in users:
if (
user_devid in user.devices
and request.query["accessToken"] in user.tokens
):
countrycode = request.match_info.get("country", "us")
tmpauthcode = "{}_{}".format(countrycode, uuid.uuid4().hex)
user.add_authcode(tmpauthcode)
body = {
"code": bumper.RETURN_API_SUCCESS,
"data": {
"authCode": tmpauthcode,
"ecovacsUid": request.query["uid"],
},
"msg": "操作成功",
"time": bumper.get_milli_time(time.time()),
}
self.bumper_users.set(users)
return web.json_response(body)
body = {
"code": bumper.ERR_TOKEN_INVALID,
"data": None,
"msg": "当前密码错误",
"time": bumper.get_milli_time(time.time()),
}
return web.json_response(body)
except Exception as e:
confserverlog.exception("{}".format(e))
async def handle_checkVersion(self, request):
try:
body = {
"code": bumper.RETURN_API_SUCCESS,
"data": {
"c": None,
"img": None,
"r": 0,
"t": None,
"u": None,
"ut": 0,
"v": None,
},
"msg": "操作成功",
"time": bumper.get_milli_time(time.time()),
}
return web.json_response(body)
except Exception as e:
confserverlog.exception("{}".format(e))
async def handle_checkAgreement(self, request):
try:
body = {
"code": bumper.RETURN_API_SUCCESS,
"data": [],
"msg": "操作成功",
"time": bumper.get_milli_time(time.time()),
}
return web.json_response(body)
except Exception as e:
confserverlog.exception("{}".format(e))
async def handle_homePageAlert(self, request):
try:
nextAlert = bumper.get_milli_time(
(datetime.now() + timedelta(hours=12)).timestamp()
)
body = {
"code": bumper.RETURN_API_SUCCESS,
"data": {
"clickSchemeUrl": None,
"clickWebUrl": None,
"hasCampaign": "N",
"imageUrl": None,
"nextAlertTime": nextAlert,
"serverTime": bumper.get_milli_time(time.time()),
},
"msg": "操作成功",
"time": bumper.get_milli_time(time.time()),
}
return web.json_response(body)
except Exception as e:
confserverlog.exception("{}".format(e))
async def handle_getProductIotMap(self, request):
try:
body = {
"code": bumper.RETURN_API_SUCCESS,
"data": [
{
"classid": "dl8fht",
"product": {
"_id": "5acb0fa87c295c0001876ecf",
"name": "DEEBOT 600 Series",
"icon": "5acc32067c295c0001876eea",
"UILogicId": "dl8fht",
"ota": False,
"iconUrl": "https://portal-ww.ecouser.net/api/pim/file/get/5acc32067c295c0001876eea",
},
},
{
"classid": "02uwxm",
"product": {
"_id": "5ae1481e7ccd1a0001e1f69e",
"name": "DEEBOT OZMO Slim10 Series",
"icon": "5b1dddc48bc45700014035a1",
"UILogicId": "02uwxm",
"ota": False,
"iconUrl": "https://portal-ww.ecouser.net/api/pim/file/get/5b1dddc48bc45700014035a1",
},
},
{
"classid": "y79a7u",
"product": {
"_id": "5b04c0227ccd1a0001e1f6a8",
"name": "DEEBOT OZMO 900",
"icon": "5b04c0217ccd1a0001e1f6a7",
"UILogicId": "y79a7u",
"ota": True,
"iconUrl": "https://portal-ww.ecouser.net/api/pim/file/get/5b04c0217ccd1a0001e1f6a7",
},
},
{
"classid": "jr3pqa",
"product": {
"_id": "5b43077b8bc457000140363e",
"name": "DEEBOT 711",
"icon": "5b5ac4cc8d5a56000111e769",
"UILogicId": "jr3pqa",
"ota": True,
"iconUrl": "https://portal-ww.ecouser.net/api/pim/file/get/5b5ac4cc8d5a56000111e769",
},
},
{
"classid": "uv242z",
"product": {
"_id": "5b5149b4ac0b87000148c128",
"name": "DEEBOT 710",
"icon": "5b5ac4e45f21100001882bb9",
"UILogicId": "uv242z",
"ota": True,
"iconUrl": "https://portal-ww.ecouser.net/api/pim/file/get/5b5ac4e45f21100001882bb9",
},
},
{
"classid": "ls1ok3",
"product": {
"_id": "5b6561060506b100015c8868",
"name": "DEEBOT 900 Series",
"icon": "5ba4a2cb6c2f120001c32839",
"UILogicId": "ls1ok3",
"ota": True,
"iconUrl": "https://portal-ww.ecouser.net/api/pim/file/get/5ba4a2cb6c2f120001c32839",
},
},
],
}
return web.json_response(body)
except Exception as e:
confserverlog.exception("{}".format(e))
async def handle_usersapi(self, request):
try:
body = {}
postbody = {}
if request.content_type == "application/x-www-form-urlencoded":
postbody = await request.post()
else:
postbody = json.loads(await request.text())
todo = postbody["todo"]
if todo == "FindBest":
service = postbody["service"]
if service == "EcoMsgNew":
body = {
"result": "ok",
"ip": socket.gethostbyname(socket.gethostname()),
"port": 5223,
}
elif service == "EcoUpdate":
body = {"result": "ok", "ip": "47.88.66.164", "port": 8005}
elif todo == "loginByItToken":
users = self.bumper_users.get()
for user in users:
if (
postbody["userId"] == "fuid_{}".format(user.userid)
and postbody["token"] in user.authcodes
):
body = {
"resource": postbody["resource"],
"result": "ok",
"todo": "result",
"token": postbody["token"],
"userId": postbody["userId"],
}
elif todo == "GetDeviceList":
active_bots = self.bumper_bots.get()
bot_list = []
for bot in active_bots:
bot_list.append(bot.asdict())
body = {"devices": bot_list, "result": "ok", "todo": "result"}
elif todo == "SetDeviceNick":
bots = self.bumper_bots.get()
for bot in bots:
if postbody["did"] == bot.did:
bot.nick = postbody["nick"]
self.bumper_bots.set(bots)
body = {"result": "ok", "todo": "result"}
confserverlog.debug(
"\r\n POST: {} \r\n Response: {}".format(postbody, body)
)
return web.json_response(body)
except Exception as e:
confserverlog.exception("{}".format(e))
async def handle_lookup(self, request):
try:
body = {}
postbody = {}
if request.content_type == "application/x-www-form-urlencoded":
postbody = await request.post()
else:
postbody = json.loads(await request.text())
confserverlog.debug(postbody)
todo = postbody["todo"]
if todo == "FindBest":
service = postbody["service"]
if service == "EcoMsgNew":
body = {
"result": "ok",
"ip": socket.gethostbyname(socket.gethostname()),
"port": 5223,
}
elif service == "EcoUpdate":
body = {"result": "ok", "ip": "47.88.66.164", "port": 8005}
confserverlog.debug(
"\r\n POST: {} \r\n Response: {}".format(postbody, body)
)
return web.json_response(body)
except Exception as e:
confserverlog.exception("{}".format(e))
async def handle_devmanager_botcommand(self, request):
try:
json_body = json.loads(await request.text())
randomid = "".join(random.sample(string.ascii_letters, 6))
bots = self.bumper_bots.get()
for bot in bots:
if bot.did == json_body["toId"] and bot.mqtt_connection == True:
retcmd = await self.helperbot.send_command(json_body, randomid)
body = retcmd
confserverlog.debug(
"\r\n POST: {} \r\n Response: {}".format(json_body, body)
)
return web.json_response(body)
else:
confserverlog.error(
"No bots with DID: {} connected to MQTT".format(
json_body["toId"]
)
)
body = {"id": randomid, "errno": bumper.ERR_COMMON, "ret": "fail"}
return web.json_response(body)
except Exception as e:
confserverlog.exception("{}".format(e))
def disconnect(self):
logging.info('ConfServer: shutting down...')
self.server.disconnect()
if(self.async):
self.server.join()
logging.info('ConfServer: bye')
try:
confserverlog.info("shutting down")
if self.run_async:
self.confthread.join()
else:
self.confthread.disconnect()
except Exception as e:
confserverlog.exception("{}".format(e))

419
bumper/mqttserver.py Normal file
View file

@ -0,0 +1,419 @@
#!/usr/bin/env python3
import logging
import asyncio
import os
import hbmqtt
from hbmqtt.broker import Broker
from hbmqtt.client import MQTTClient
from hbmqtt.mqtt.constants import QOS_0, QOS_1, QOS_2
import pkg_resources
import contextvars
import time
from threading import Thread
import ssl
import bumper
import json
from datetime import datetime, timedelta
helperbotlog = logging.getLogger("helperbot")
mqttserverlog = logging.getLogger("mqttserver")
logging.getLogger("transitions").setLevel(logging.CRITICAL + 1) # Ignore this logger
logging.getLogger("passlib").setLevel(logging.CRITICAL + 1) # Ignore this logger
logging.getLogger("hbmqtt.broker").setLevel(
logging.CRITICAL + 1
) # Ignore this logger #There are some sublogs that could be set if needed (.plugins)
logging.getLogger("hbmqtt.mqtt.protocol").setLevel(
logging.CRITICAL + 1
) # Ignore this logger
logging.getLogger("hbmqtt.client").setLevel(logging.CRITICAL + 1) # Ignore this logger
class MQTTHelperBot:
Client = MQTTClient()
def __init__(
self,
address,
bumper_bots=contextvars.ContextVar,
bumper_clients=contextvars.ContextVar,
):
self.address = address
self.client_id = "helper1@bumper/helper1"
self.command_responses = contextvars.ContextVar("command_responses", default=[])
self.helperthread = None
def run(self, run_async=False):
if run_async:
hloop = asyncio.new_event_loop()
helperbotlog.debug("Starting MQTT HelperBot Thread: 1")
self.helperthread = Thread(
name="MQTTHelperBot_Thread", target=self.run_helperbot, args=(hloop,)
)
self.helperthread.setDaemon(True)
self.helperthread.start()
else:
self.run_helperbot()
def run_helperbot(self, loop):
logging.info("Starting MQTT HelperBot")
print("Starting MQTT HelperBot")
try:
asyncio.set_event_loop(loop)
self.Client = MQTTClient(
client_id=self.client_id, config={"check_hostname": False}
)
loop.run_until_complete(self.start_helper_bot())
loop.run_until_complete(self.get_msg())
loop.run_forever()
except Exception as e:
helperbotlog.exception("{}".format(e))
async def start_helper_bot(self):
try:
await self.Client.connect(
"mqtts://{}:{}/".format(self.address[0], self.address[1]),
cafile=bumper.ca_cert,
)
await self.Client.subscribe(
[
("iot/p2p/+/+/+/+/helper1/bumper/helper1/+/+/+", QOS_0),
("iot/p2p/+", QOS_0),
]
)
except Exception as e:
helperbotlog.exception("{}".format(e))
async def get_msg(self):
try:
while True:
message = await self.Client.deliver_message()
# helperbotlog.debug("HelperBot MQTT Received Message on Topic: {} - Message: {}".format(message.topic, str(message.payload.decode("utf-8"))))
cresp = self.command_responses.get()
if str(message.topic).split("/")[6] == "helper1":
cresp.append(
{
"time": time.time(),
"topic": message.topic,
"payload": str(message.data.decode("utf-8")),
}
)
# Cleanup "expired messages" > 60 seconds from time
for msg in cresp:
expire_time = (
datetime.fromtimestamp(msg["time"]) + timedelta(seconds=10)
).timestamp()
if time.time() > expire_time:
# helperbotlog.debug("Pruning Message Time: {}, MsgTime: {}, MsgTime+60: {}".format(time.time(), msg['time'], expire_time))
cresp.remove(msg)
self.command_responses.set(cresp)
# helperbotlog.debug("MQTT Command Response List Count: %s" %len(cresp))
except Exception as e:
helperbotlog.exception("{}".format(e))
async def wait_for_resp(self, requestid):
try:
t_end = (datetime.now() + timedelta(seconds=10)).timestamp()
while time.time() < t_end:
await asyncio.sleep(0.1)
responses = self.command_responses.get()
if len(responses) > 0:
for msg in responses:
topic = str(msg["topic"]).split("/")
if topic[6] == "helper1" and topic[10] == requestid:
# helperbotlog.debug('VacBot MQTT Response: Topic: %s Payload: %s' % (msg['topic'], msg['payload']))
if topic[11] == "j":
resppayload = json.loads(msg["payload"])
else:
resppayload = str(msg["payload"])
resp = {"id": requestid, "ret": "ok", "resp": resppayload}
cresp = self.command_responses.get()
cresp.remove(msg)
self.command_responses.set(cresp)
return resp
return {"id": requestid, "errno": "timeout", "ret": "fail"}
except asyncio.CancelledError as e:
helperbotlog.debug("wait_for_resp cancelled by asyncio")
except Exception as e:
helperbotlog.exception("{}".format(e))
async def send_command(self, cmdjson, requestid):
try:
ttopic = "iot/p2p/{}/helper1/bumper/helper1/{}/{}/{}/q/{}/{}".format(
cmdjson["cmdName"],
cmdjson["toId"],
cmdjson["toType"],
cmdjson["toRes"],
requestid,
cmdjson["payloadType"],
)
try:
await self.Client.publish(
ttopic, str(cmdjson["payload"]).encode(), QOS_0
)
except Exception as e:
helperbotlog.exception("{}".format(e))
resp = await self.wait_for_resp(requestid)
return resp
except Exception as e:
helperbotlog.exception("{}".format(e))
class MQTTServer:
default_config = {}
bumper_users = []
bumper_clients = []
bumper_bots = []
async def broker_coro(self):
try:
broker = hbmqtt.broker.Broker(config=self.default_config)
await broker.start()
except PermissionError as e:
if "bind" in e.strerror:
mqttserverlog.exception(
"Error binding mqttserver, exiting. Try using a different hostname or IP - {}".format(
e
)
)
exit(1)
except Exception as e:
mqttserverlog.exception("{}".format(e))
exit(1)
async def active_bot_listing(self):
try:
while True:
await asyncio.sleep(5)
mqttserverlog.debug("connected bots - %s" % self.bumper_bots.get())
except Exception as e:
mqttserverlog.exception("{}".format(e))
def __init__(
self,
address,
bumper_users=contextvars.ContextVar,
bumper_bots=contextvars.ContextVar,
bumper_clients=contextvars.ContextVar,
):
try:
self.bumper_users = bumper_users
self.bumper_bots = bumper_bots
self.bumper_clients = bumper_clients
self.mqttserverthread = None
self.address = address
# The below adds a plugin to the hbmqtt.broker.plugins without having to futz with setup.py
distribution = pkg_resources.Distribution("hbmqtt.broker.plugins")
bumper_plugin = pkg_resources.EntryPoint.parse(
"bumper = bumper.mqttserver:BumperMQTTServer_Plugin", dist=distribution
)
distribution._ep_map = {"hbmqtt.broker.plugins": {"bumper": bumper_plugin}}
pkg_resources.working_set.add(distribution)
# Initialize bot server
self.default_config = {
"listeners": {
"default": {"type": "tcp"},
"tls1": {
"bind": "{}:{}".format(address[0], address[1]),
"ssl": "on",
"certfile": bumper.server_cert,
"keyfile": bumper.server_key,
},
},
"sys_interval": 10,
"auth": {
"allow-anonymous": False,
"password-file": os.path.join(
os.path.dirname(os.path.realpath(__file__)), "passwd"
),
"plugins": ["bumper"], # No plugins == no auth
},
"topic-check": {"enabled": False},
"bumper": {
"bumper_users": self.bumper_users,
"bumper_bots": self.bumper_bots,
"bumper_clients": self.bumper_clients,
},
}
except Exception as e:
mqttserverlog.exception("{}".format(e))
def run(self, run_async=False):
if run_async:
sloop = asyncio.new_event_loop()
mqttserverlog.debug("Starting MQTTServer Thread: 1")
self.mqttserverthread = Thread(
name="MQTTServer_Thread", target=self.run_server, args=(sloop,)
)
self.mqttserverthread.setDaemon(True)
self.mqttserverthread.start()
else:
self.run_server()
def run_server(self, loop):
logging.info("Starting MQTT Server at {}".format(self.address))
print("Starting MQTT Server at {}".format(self.address))
try:
asyncio.set_event_loop(loop)
loop.run_until_complete(self.broker_coro())
# loop.run_until_complete(self.active_bot_listing())
loop.run_forever()
except Exception as e:
mqttserverlog.exception("{}".format(e))
class BumperMQTTServer_Plugin:
def __init__(self, context):
self.context = context
try:
self.bumper_config = self.context.config["bumper"]
self.auth_config = self.context.config["auth"]
except KeyError:
self.context.logger.warning(
"'bumper' section not found in context configuration"
)
except Exception as e:
mqttserverlog.exception("{}".format(e))
async def authenticate(self, *args, **kwargs):
if not self.auth_config:
# auth config section not found
self.context.logger.warning(
"'auth' section not found in context configuration"
)
return False
allow_anonymous = self.auth_config.get(
"allow-anonymous", True
) # allow anonymous by default
if allow_anonymous:
authenticated = True
self.context.logger.debug("Authentication success: config allows anonymous")
else:
try:
bumper_users = self.bumper_config["bumper_users"].get()
bumper_bots = self.bumper_config["bumper_bots"].get()
bumper_clients = self.bumper_config["bumper_clients"].get()
session = kwargs.get("session", None)
username = session.username
password = session.password
client_id = session.client_id
didsplit = str(client_id).split("@")
# If this isn't a fake user (fuid) then add as a bot
if not (
str(didsplit[0]).startswith("fuid")
or str(didsplit[0]).startswith("helper")
):
tmpbotdetail = str(didsplit[1]).split("/")
bumper.add_bot(
username, didsplit[0], tmpbotdetail[0], tmpbotdetail[1]
)
mqttserverlog.debug(
"new bot authenticated SN: {} DID: {}".format(
username, didsplit[0]
)
)
authenticated = True
else:
tmpclientdetail = str(didsplit[1]).split("/")
userid = didsplit[0]
realm = tmpclientdetail[0]
resource = tmpclientdetail[1]
if userid == "helper1":
authenticated = True
else:
auth = False
if bumper.check_authcode(didsplit[0], password):
auth = True
elif bumper.use_auth == False:
auth = True
if auth:
bumper.add_client(userid, realm, resource)
mqttserverlog.debug(
"client authenticated {}".format(userid)
)
authenticated = True
else:
authenticated = False
except Exception as e:
mqttserverlog.exception("{}".format(e))
authenticated = False
return authenticated
async def on_broker_client_connected(self, client_id):
try:
bumper_users = self.bumper_config["bumper_users"].get()
bumper_bots = self.bumper_config["bumper_bots"].get()
bumper_clients = self.bumper_config["bumper_clients"].get()
didsplit = str(client_id).split("@")
for bot in bumper_bots:
if didsplit[0] == bot.did:
bot.mqtt_connection = True
mqttserverlog.debug("bot connected {}".format(bot.did))
self.bumper_config["bumper_bots"].set(bumper_bots)
for client in bumper_clients:
if didsplit[0] == client.userid and client.userid != "helper1":
client.mqtt_connection = True
# mqttserverlog.info("client connected {}".format(client.userid))
self.bumper_config["bumper_clients"].set(bumper_clients)
except Exception as e:
mqttserverlog.exception("{}".format(e))
async def on_broker_client_disconnected(self, client_id):
try:
bumper_users = self.bumper_config["bumper_users"].get()
bumper_bots = self.bumper_config["bumper_bots"].get()
bumper_clients = self.bumper_config["bumper_clients"].get()
didsplit = str(client_id).split("@")
for bot in bumper_bots:
if didsplit[0] == bot.did:
bot.mqtt_connection = False
mqttserverlog.debug("bot disconnected {}".format(bot.did))
self.bumper_config["bumper_bots"].set(bumper_bots)
for client in bumper_clients:
if didsplit[0] == client.userid and client.userid != "helper1":
client.mqtt_connection = False
# mqttserverlog.info("client disconnected {}".format(client.userid))
self.bumper_config["bumper_clients"].set(bumper_clients)
except Exception as e:
mqttserverlog.exception("{}".format(e))

View file

@ -1,50 +1,168 @@
#!/usr/bin/env python3
from threading import Thread
import sys, socket, threading, re, time, logging, uuid, xml.etree.ElementTree as ET
import base64
import ssl
import contextvars
import bumper
xmppserverlog = logging.getLogger("xmppserver")
class XMPPServer():
server_id = 'bumper'
bot_id = 'bumpy'
class XMPPServer:
server_id = "bumper"
bot_id = "bumpy"
client_id = None
clients = []
exit_flag = False
def __init__(self, address):
try:
def __init__(
self,
address,
bumper_users=contextvars.ContextVar,
bumper_bots=contextvars.ContextVar,
bumper_clients=contextvars.ContextVar,
):
# Initialize bot server
self.address = address
self.bumper_users = bumper_users
self.bumper_bots = bumper_bots
self.bumper_clients = bumper_clients
def run(self, run_async=False):
if run_async:
xmppserverlog.debug("Starting XMPPServer Thread: 1")
self.xmppthread = Thread(name="XMPPServer_Thread", target=self.run_server)
self.xmppthread.setDaemon(True)
self.xmppthread.start()
else:
try:
self.run_server()
except KeyboardInterrupt:
self.disconnect()
def run_server(self):
logging.info("Starting XMPP Server at {}".format(self.address))
print("Starting XMPP Server at {}".format(self.address))
# xmppserverlog.setLevel(logging.DEBUG)
# Set SSL Context
self.ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
self.ssl_ctx.load_cert_chain(
certfile=bumper.server_cert, keyfile=bumper.server_key
)
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.socket.bind(address)
self.socket.listen(1)
logging.info('XMPPServer: listening on {}:{}'.format(address[0], address[1]))
try:
self.socket.bind(self.address)
self.socket.listen(5)
xmppserverlog.debug(
"listening on {}:{}".format(self.address[0], self.address[1])
)
while not self.exit_flag:
logging.info('XMPPServer: awaiting connection')
connection, client_address = self.socket.accept()
# disconnect any clients with this ip
for client in self.clients:
if client.address == client_address[0]:
client.disconnect()
xmppserverlog.debug(
"disconnecting existing client {} with resource {}".format(
client.address, client.clientresource
)
)
client._disconnect()
self.remove_client_byip(client.address)
xmppserverlog.debug(
"starting new client with ip {}".format(client_address[0])
)
thread_id = uuid.uuid4()
client = Client(thread_id, connection, client_address)
client = Client(
thread_id,
connection,
client_address,
self.bumper_users,
self.bumper_bots,
self.bumper_clients,
)
client.setDaemon(True)
client.start()
self.clients.append(client)
self.socket.close()
except PermissionError as e:
if "bind" in e.strerror:
xmppserverlog.exception(
"Error binding XMPPServer, exiting. Try using a different hostname or IP - {}".format(
e
)
)
exit(1)
except Exception as e:
logging.error('XMPPServer: {}'.format(e))
except KeyboardInterrupt:
logging.debug('XMPPServer: Keyboard interrupt')
xmppserverlog.exception("{}".format(e))
exit(1)
except KeyboardInterrupt as e:
xmppserverlog.exception("{}".format(e))
finally:
connection.shutdown(socket.SHUT_RDWR)
connection.close()
self.disconnect()
logging.info('XMPPServer: bye')
xmppserverlog.info("disconnecting")
self.socket.close()
def disconnect(self):
logging.info('XMPPServer: waiting for all client threads to exit')
try:
xmppserverlog.debug("waiting for all client threads to exit")
for client in self.clients:
client.disconnect()
client.join()
client._disconnect()
self.exit_flag = True
logging.info('XMPPServer: shutting down...')
xmppserverlog.debug("shutting down")
except Exception as e:
xmppserverlog.exception("{}".format(e))
def remove_client_byip(self, ip):
for client in self.clients:
if client.address == ip:
xmppserverlog.debug(
"removing client from client list with ip {} and resource {}".format(
client.address, client.clientresource
)
)
client._disconnect()
self.clients.remove(client)
def remove_client_byresource(self, resource):
for client in self.clients:
if str(client.clientresource).lower() == str(resource).lower():
xmppserverlog.debug(
"removing client from client list with ip {} and resource {}".format(
client.address, client.clientresource
)
)
client._disconnect()
self.clients.remove(client)
def remove_client_byuid(self, uid):
for client in self.clients:
if str(client.uid).lower() == str(uid).lower():
xmppserverlog.debug(
"removing client from client list with ip {} and resource {}".format(
client.address, client.clientresource
)
)
client._disconnect()
self.clients.remove(client)
class Client(threading.Thread):
@ -58,124 +176,598 @@ class Client(threading.Thread):
BOT = 1
CONTROLLER = 2
def __init__(self, thread_id, connection, client_address):
def __init__(
self,
thread_id,
connection,
client_address,
bumper_users=contextvars.ContextVar,
bumper_bots=contextvars.ContextVar,
bumper_clients=contextvars.ContextVar,
):
threading.Thread.__init__(self)
self.id = thread_id
self.name = "XMPP_Client_{}".format(client_address[0])
self.type = self.UNKNOWN
self.state = self.IDLE
self.connection = connection
self.address = client_address[0]
self.clientresource = ""
self.uid = ""
self.log_sent_message = False # Set to true to log sends
self.log_incoming_data = True # Set to true to log sends
self.bumper_users = bumper_users
self.bumper_bots = bumper_bots
self.bumper_clients = bumper_clients
xmppserverlog.debug(
"new client thread init for client with ip {}".format(self.address)
)
def send(self, command):
logging.debug('to {}: {}'.format(self.address, command))
try:
if not self.connection._closed:
if self.log_sent_message:
xmppserverlog.debug("send {} - {}".format(self.address, command))
self.connection.send(command.encode())
def disconnect(self):
logging.info('{} disconnecting'.format(self.address))
except OSError as e:
xmppserverlog.error("{}".format(e))
except BrokenPipeError as e:
xmppserverlog.error("{}".format(e))
# self._set_state('DISCONNECT')
except ConnectionResetError as e:
xmppserverlog.error("{}".format(e))
# self._set_state('DISCONNECT')
except Exception as e:
xmppserverlog.exception("{}".format(e))
def _disconnect(self):
try:
bumper_bots = self.bumper_bots.get()
bumper_clients = self.bumper_clients.get()
for bot in bumper_bots:
if self.uid == bot.did:
bot.xmpp_connection = False
# xmppserverlog.info("bot disconnected {}".format(bot.did))
self.bumper_bots.set(bumper_bots)
for client in bumper_clients:
if self.uid == client.userid and client.userid != "helper1":
client.xmpp_connection = False
# xmppserverlog.info("client disconnected {}".format(client.userid))
self.bumper_clients.set(bumper_clients)
# xmppserverlog.debug('client {} with resource {} disconnecting'.format(self.address, self.clientresource))
self.connection.close()
self._set_state('DISCONNECT')
except Exception as e:
xmppserverlog.exception("{}".format(e))
def _tag_strip_uri(self, tag):
if tag[0] == '{':
uri, ignore, tag = tag[1:].partition('}')
try:
if tag[0] == "{":
uri, ignore, tag = tag[1:].partition("}")
return tag
except Exception as e:
xmppserverlog.exception("{}".format(e))
def _set_state(self, state):
try:
new_state = getattr(Client, state)
if self.state > new_state:
raise Exception('{} illegal state change {}->{}'.format(self.address, self.state, new_state))
logging.info('{} state: {}'.format(self.address, state))
raise Exception(
"{} illegal state change {}->{}".format(
self.address, self.state, new_state
)
)
xmppserverlog.debug("{} state: {}".format(self.address, state))
self.state = new_state
if new_state == 5:
self._disconnect()
except Exception as e:
xmppserverlog.exception("{}".format(e))
def _handle_ctl(self, xml, data):
ctl = xml[0][0]
if ctl.get('admin') and self.type == self.BOT:
logging.info('admin username received from bot: {}'.format(ctl.get('admin')))
XMPPServer.client_id = ctl.get('admin')
try:
if data.decode("utf-8").find("roster") > -1:
# Return not-implemented for roster
self.send(
'<iq type="error" id="{}"><error type="cancel" code="501"><feature-not-implemented xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"/></error></iq>'.format(
xml.get("id")
)
)
return
if xml.get("type") == "set":
if (
data.decode("utf-8").find("com:sf") > -1
and xml.get("to") == "rl.ecorobot.net"
): # Android bind? Not sure what this does yet.
self.send(
'<iq id="{}" to="{}@{}/{}" from="rl.ecorobot.net" type="result"/>'.format(
xml.get("id"),
self.uid,
XMPPServer.bot_id,
self.clientresource,
)
)
else:
xmppserverlog.debug(
"Unknown set type: {}".format(data.decode("utf-8"))
)
if xml[0][0]:
ctl = xml[0][0]
if ctl.get("admin") and self.type == self.BOT:
xmppserverlog.debug(
"admin username received from bot: {}".format(ctl.get("admin"))
)
XMPPServer.client_id = ctl.get("admin")
return
# forward
for client in XMPPServer.clients:
if client.address != self.address and client.state == client.READY:
if client.type == self.BOT:
data = data.decode('utf-8')
id_index = data.find('id')
data = data.decode("utf-8")
id_index = data.find("id")
if id_index > -1:
data = data[:id_index] + 'from="' + XMPPServer.client_id + '" ' + data[id_index:]
data = (
data[:id_index]
+ 'from="'
+ XMPPServer.client_id
+ '" '
+ data[id_index:]
)
data = data.encode()
client.send(data.decode('utf-8'))
client.send(data.decode("utf-8"))
except Exception as e:
xmppserverlog.exception("{}".format(e))
def _handle_ping(self, xml, data):
if(xml.get('to').find('@') == -1):
try:
if xml.get("to").find("@") == -1:
# Ping to server - respond
self.send('<iq type="result" id="{}" from="{}" />'.format(xml.get('id'), xml.get('to')))
self.send(
'<iq type="result" id="{}" from="{}" />'.format(
xml.get("id"), xml.get("to")
)
)
else:
for client in XMPPServer.clients:
if client.address != self.address and client.state == client.READY:
client.send(data.decode('utf-8'))
client.send(data.decode("utf-8"))
except Exception as e:
xmppserverlog.exception("{}".format(e))
def _handle_result(self, data):
# forward
try:
for client in XMPPServer.clients:
if client.address != self.address and client.state == client.READY:
client.send(data.decode('utf-8'))
client.send(data.decode("utf-8"))
def run(self):
try:
logging.info('client connected: {}'.format(self.address))
self._set_state('CONNECT')
while True:
data = self.connection.recv(4096)
if data:
logging.debug('from {}: {}'.format(self.address, data.decode('utf-8')))
except Exception as e:
xmppserverlog.exception("{}".format(e))
def _handle_connect(self, data):
try:
if self.state == self.CONNECT:
if data.decode('utf-8').find('jabber:client') > -1:
self._set_state('INIT')
# Client first connecting, send our features
if data.decode("utf-8").find("jabber:client") > -1:
# ack jabbr:client
self.send('<stream:stream xmlns:stream="http://etherx.jabber.org/streams" xmlns="jabber:client" version="1.0" id="1" from="{}">'.format(XMPPServer.server_id))
time.sleep(0.5)
# no STARTTLS
self.send(
'<stream:stream xmlns:stream="http://etherx.jabber.org/streams" xmlns="jabber:client" version="1.0" id="1" from="{}">'.format(
XMPPServer.server_id
)
)
# with STARTTLS
# self.send('<stream:stream xmlns:stream="http://etherx.jabber.org/streams" xmlns:tls="http://www.ietf.org/rfc/rfc2595.txt" xmlns="jabber:client" version="1.0" id="1" from="{}">'.format(XMPPServer.server_id))
time.sleep(0.25)
# send authentication support for iq-auth (fallback) and SASL
self.send(
'<stream:features><auth xmlns="http://jabber.org/features/iq-auth"/><mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl"><mechanism>PLAIN</mechanism></mechanisms></stream:features>'
)
# self.send('<stream:features><auth xmlns="http://jabber.org/features/iq-auth"/></stream:features>')
elif data.decode("utf-8").find("jabber:iq:auth") > -1: # Handle iq-auth
self._handle_iq_auth(data)
elif (
data.decode("utf-8").find("urn:ietf:params:xml:ns:xmpp-sasl") > -1
): # Handle SASL auth
self._handle_sasl_auth(data)
elif self.state == self.INIT:
# Client getting session after authentication
if data.decode("utf-8").find("jabber:client") > -1:
# ack jabbr:client
self.send(
'<stream:stream xmlns:stream="http://etherx.jabber.org/streams" xmlns="jabber:client" version="1.0" id="1" from="{}">'.format(
XMPPServer.server_id
)
)
time.sleep(0.25)
# session
self.send('<stream:features><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"/><session xmlns="urn:ietf:params:xml:ns:xmpp-session"/></stream:features>')
continue
xml = ET.fromstring(data)
self.send(
'<stream:features><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"/><session xmlns="urn:ietf:params:xml:ns:xmpp-session"/></stream:features>'
)
else: # Handle init bind
xml = ET.fromstring(data.decode("utf-8"))
if len(xml):
child = self._tag_strip_uri(xml[0].tag)
else:
child = None
if xml.tag == 'iq':
res = None
if child == 'bind':
res = '<iq type="result" id="{}"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><jid>{}</jid></bind></iq>'.format(xml.get('id'), XMPPServer.bot_id)
self._set_state('BIND')
elif child == 'session':
res = '<iq type="result" id="{}" />'.format(xml.get('id'))
self._set_state('READY')
elif child == 'query':
self._handle_ctl(xml, data)
elif child == 'ping':
self._handle_ping(xml, data)
elif xml.get('type') == 'result':
self._handle_result(data)
if res:
if xml.tag == "iq":
if child == "bind":
self._handle_bind(xml)
except Exception as e:
xmppserverlog.exception("{}".format(e))
def _handle_iq_auth(self, data):
try:
xml = ET.fromstring(data.decode("utf-8"))
ctl = xml[0][0]
xmppserverlog.info("IQ AUTH XML: {}".format(xml))
# Received username and auth tag, send username/password requirement
if (
xml.get("type") == "get"
and "auth}username" in ctl.tag
and self.type == self.UNKNOWN
):
self.send(
'<iq type="result" id="{}"><query xmlns="jabber:iq:auth"><username/><password/></query></iq>'.format(
xml.get("id")
)
)
# Received username, password, resource - Handle auth here and return pass or fail
if (
xml.get("type") == "set"
and "auth}username" in ctl.tag
and self.type == self.UNKNOWN
):
xmlauth = xml[0].getchildren()
uid = ""
password = ""
resource = ""
for aitem in xmlauth:
if "username" in aitem.tag:
self.uid = aitem.text
elif "password" in aitem.tag:
password = aitem.text.split("/")[2]
authcode = password
elif "resource" in aitem.tag:
self.clientresource = aitem.text
resource = self.clientresource
if not self.uid.startswith("fuid"):
# Need sample data to see details here
bumper.add_bot("", self.uid, "", resource)
xmppserverlog.info("bot authenticated {}".format(self.uid))
# Client authenticated, move to next state
self._set_state("INIT")
# Successful auth
self.send('<iq type="result" id="{}"/>'.format(xml.get("id")))
else:
auth = False
if bumper.check_authcode(self.uid, authcode):
auth = True
elif bumper.use_auth == False:
auth = True
if auth:
bumper.add_client(self.uid, "bumper", self.clientresource)
xmppserverlog.debug("client authenticated {}".format(self.uid))
# Client authenticated, move to next state
self._set_state("INIT")
# Successful auth
self.send('<iq type="result" id="{}"/>'.format(xml.get("id")))
else:
# Failed auth
self.send(
'<iq type="error" id="{}"><error code="401" type="auth"><not-authorized xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"/></error></iq>'.format(
xml.get("id")
)
)
except ET.ParseError as e:
if "no element found" in e.msg:
xmppserverlog.debug(
"xml parse error - {} - {} - this is common with ecovac protocol".format(
data.decode("utf-8"), e
)
)
elif "not well-formed (invalid token)" in e.msg:
xmppserverlog.debug(
"xml parse error - {} - {}".format(data.decode("utf-8"), e)
)
else:
xmppserverlog.debug(
"xml parse error - {} - {}".format(data.decode("utf-8"), e)
)
except Exception as e:
xmppserverlog.exception("{}".format(e))
def _handle_sasl_auth(self, data):
try:
xml = ET.fromstring(data.decode("utf-8"))
saslauth = base64.b64decode(xml.text).decode("utf-8").split("/")
username = saslauth[0]
username = saslauth[0].split("\x00")[1]
self.uid = username
resource = saslauth[1]
self.clientresource = resource
authcode = saslauth[2]
if not self.uid.startswith("fuid"):
# Need sample data to see details here
bumper.add_bot("", self.uid, "", resource)
xmppserverlog.info("bot authenticated {}".format(self.uid))
# Send response
self.send(
'<success xmlns="urn:ietf:params:xml:ns:xmpp-sasl"/>'
) # Success
# Client authenticated, move to next state
self._set_state("INIT")
else:
auth = False
if bumper.check_authcode(self.uid, authcode):
auth = True
elif bumper.use_auth == False:
auth = True
if auth:
bumper.add_client(self.uid, "bumper", self.clientresource)
xmppserverlog.debug("client authenticated {}".format(self.uid))
# Client authenticated, move to next state
self._set_state("INIT")
# Send response
self.send(
'<success xmlns="urn:ietf:params:xml:ns:xmpp-sasl"/>'
) # Success
else:
# Failed to authenticate
self.send(
'<response xmlns="urn:ietf:params:xml:ns:xmpp-sasl"/>'
) # Fail
except ET.ParseError as e:
if "no element found" in e.msg:
xmppserverlog.debug(
"xml parse error - {} - {} - this is common with ecovac protocol".format(
data.decode("utf-8"), e
)
)
elif "not well-formed (invalid token)" in e.msg:
xmppserverlog.debug(
"xml parse error - {} - {}".format(data.decode("utf-8"), e)
)
else:
xmppserverlog.debug(
"xml parse error - {} - {}".format(data.decode("utf-8"), e)
)
except Exception as e:
xmppserverlog.exception("{}".format(e))
def _handle_bind(self, xml):
try:
bumper_bots = self.bumper_bots.get()
bumper_clients = self.bumper_clients.get()
for bot in bumper_bots:
if self.uid == bot.did:
bot.xmpp_connection = True
# xmppserverlog.info("bot connected {}".format(bot.did))
self.bumper_bots.set(bumper_bots)
for client in bumper_clients:
if self.uid == client.userid:
client.xmpp_connection = True
# xmppserverlog.info("client connected {}".format(client.userid))
self.bumper_clients.set(bumper_clients)
clientbindxml = xml.getchildren()
clientresourcexml = clientbindxml[0].getchildren()
if len(clientresourcexml) > 0:
self.clientresource = clientresourcexml[0].text
self.name = "XMPP_Client_{}".format(self.clientresource)
xmppserverlog.debug(
"new client {} using resource {}".format(
self.address, self.clientresource
)
)
res = '<iq type="result" id="{}"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><jid>{}@{}/{}</jid></bind></iq>'.format(
xml.get("id"), self.uid, XMPPServer.bot_id, self.clientresource
)
else:
xmppserverlog.debug("new client {}".format(self.address))
res = '<iq type="result" id="{}"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><jid>{}@{}</jid></bind></iq>'.format(
xml.get("id"), self.uid, XMPPServer.bot_id
)
self._set_state("BIND")
self.send(res)
elif xml.tag == 'presence':
if len(xml) and xml[0].tag == 'status':
except Exception as e:
xmppserverlog.exception("{}".format(e))
def _handle_session(self, xml):
try:
res = '<iq type="result" id="{}" />'.format(xml.get("id"))
self._set_state("READY")
self.send(res)
except Exception as e:
xmppserverlog.exception("{}".format(e))
def _handle_presence(self, xml):
try:
if len(xml) and xml[0].tag == "status":
# bot announcing arrival
self.type = self.BOT
logging.info('{} type set to BOT (based on presence tag)'.format(self.address))
xmppserverlog.debug(
"{} type set to BOT (based on presence tag)".format(self.address)
)
# send a command from an unknown user - the response will contain the correct admin username
self.send('<iq type="set" id="{}" from="{}" to="{}"><query xmlns="com:ctl"><ctl td="GetCleanState" /></query></iq>'.format(uuid.uuid4(), 'unknown@ecouser.net', XMPPServer.bot_id))
elif xml.get('type') == 'available':
self.send(
'<iq type="set" id="{}" from="{}" to="{}"><query xmlns="com:ctl"><ctl td="GetCleanState" /></query></iq>'.format(
uuid.uuid4(), "unknown@ecouser.net", XMPPServer.bot_id
)
)
else:
self.type = self.CONTROLLER
logging.info('{} type set to CONTROLLER (based on presence tag)'.format(self.address))
xmppserverlog.debug(
"{} type set to CONTROLLER (based on presence tag)".format(
self.address
)
)
self.send(
'<presence to="{}@{}/{}"> dummy </presence>'.format(
self.uid, XMPPServer.bot_id, self.clientresource
)
)
except Exception as e:
xmppserverlog.exception("{}".format(e))
def _parse_data(self, data):
if self.log_incoming_data:
xmppserverlog.debug(
"from {} - {}".format(self.address, data.decode("utf-8"))
)
try:
xml = ET.fromstring(data.decode("utf-8"))
self._handle_xml(xml, data)
except ET.ParseError as e:
logging.debug('error: {}'.format(e))
if (
"no element found" in e.msg
): # Element not closed or not all bytes received
# Happens wth connect stream often
if "<stream:stream " in data.decode("utf-8"):
if self.state == self.CONNECT or self.state == self.INIT:
self._handle_connect(data)
else:
if not (data.decode("utf-8") == "" or data.decode("utf-8") == " "):
xmppserverlog.error(
"xml parse error - {} - {}".format(data.decode("utf-8"), e)
)
elif "not well-formed (invalid token)" in e.msg:
# If a lone </stream:stream> - client is signalling end of session/disconnect
if not "</stream:stream>" in data.decode("utf-8"):
xmppserverlog.error(
"xml parse error - {} - {}".format(data.decode("utf-8"), e)
)
else:
self.send("</stream:stream>") # Close stream
elif (
"junk after document element" in e.msg
): # More than one xml doc in data
# try to split it
data0 = data.decode("utf-8")
data1 = data0[e.position[1] :]
data0 = data0[: e.position[1]]
# xmppserverlog.debug('xml parse error - {} - {} - split0: {} - split1: {}'.format(data.decode('utf-8'), e, data0, data1))
self._parse_data(data0.encode("utf-8"))
self._parse_data(data1.encode("utf-8"))
else:
xmppserverlog.debug(
"xml parse error - {} - {}".format(data.decode("utf-8"), e)
)
except Exception as e:
logging.error('XMPPServer: {}'.format(e))
self._set_state('DISCONNECT')
xmppserverlog.exception("{}".format(e))
def _handle_xml(self, xml, data):
try:
if self.state == self.CONNECT or self.state == self.INIT:
self._handle_connect(data)
if len(xml):
child = self._tag_strip_uri(xml[0].tag)
else:
child = None
if xml.tag == "iq":
if child == "bind":
self._handle_bind(xml)
elif child == "session":
self._handle_session(xml)
elif child == "query":
self._handle_ctl(xml, data)
elif child == "ping":
self._handle_ping(xml, data)
elif xml.get("type") == "result":
self._handle_result(data)
elif xml.tag == "presence":
self._handle_presence(xml)
except Exception as e:
logging.error('XMPPServer: {}'.format(e))
self._set_state('DISCONNECT')
finally:
self.disconnect()
xmppserverlog.exception("{}".format(e))
def run(self):
# xmppserverlog.info('client connected - {}'.format(self.address))
self._set_state("CONNECT")
while not self.state == self.DISCONNECT and not self.connection._closed:
data = b""
time.sleep(0.2)
if not self.connection._closed:
try:
data = self.connection.recv(4096)
except ConnectionResetError as e:
xmppserverlog.error("{}".format(e))
except OSError as e:
xmppserverlog.error("{}".format(e))
except Exception as e:
xmppserverlog.exception("{}".format(e))
if data != b"":
self._parse_data(data)

BIN
certs/CA/cacert.crt Normal file

Binary file not shown.

85
certs/CA/cacert.pem Normal file
View file

@ -0,0 +1,85 @@
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 11078087720677522868 (0x99bd46167599bdb4)
Signature Algorithm: sha1WithRSAEncryption
Issuer: C=US, ST=California, O=Bumper, OU=Bumper, CN=Bumper
Validity
Not Before: Feb 1 05:59:32 2019 GMT
Not After : Jan 29 05:59:32 2029 GMT
Subject: C=US, ST=California, O=Bumper, OU=Bumper, CN=Bumper
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (2048 bit)
Modulus:
00:aa:6e:af:15:99:3b:09:32:a1:a7:4b:ee:f7:13:
68:c1:94:4f:09:71:54:99:02:2d:27:c1:5f:16:2e:
45:d3:a6:44:24:fb:bd:f0:9b:a9:f6:6c:49:6f:09:
13:76:d6:5b:88:c2:fc:8b:8b:14:4c:39:df:8a:b5:
51:a8:83:c1:73:ca:aa:f1:b7:b4:38:e1:f0:91:35:
be:6f:c6:99:7c:e1:ef:d9:c0:ee:d5:fb:f8:f5:0f:
a7:96:b3:78:95:d6:2a:04:7d:3b:31:42:b6:33:c0:
36:2c:06:f1:6d:db:11:99:16:3e:77:c6:c6:df:ed:
95:40:b8:39:a2:82:40:fe:bb:f6:8b:63:0c:c6:96:
ae:fd:3b:c7:79:a4:f5:5f:4e:83:87:27:33:bb:6f:
cf:43:86:55:73:6c:0c:28:61:89:66:72:b3:90:3d:
d8:54:53:6a:3a:57:76:01:05:58:ac:78:3d:0d:0a:
52:b4:b7:be:2b:4e:9a:6a:f4:f8:a2:3f:21:99:b6:
b4:74:fb:d5:c0:23:7b:e8:51:f5:0a:ec:6a:11:4e:
9b:c3:f2:56:00:06:3b:58:e3:cf:67:b6:c3:26:50:
0e:f7:72:11:3f:39:2c:c9:b7:d5:69:1a:33:dc:8f:
81:66:c5:88:3f:c7:01:94:f0:3d:ac:aa:f3:a0:2a:
e8:c7
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Subject Key Identifier:
5E:39:C7:78:31:83:30:51:82:D9:88:9B:73:0B:72:EF:44:74:7B:D5
X509v3 Authority Key Identifier:
keyid:5E:39:C7:78:31:83:30:51:82:D9:88:9B:73:0B:72:EF:44:74:7B:D5
DirName:/C=US/ST=California/O=Bumper/OU=Bumper/CN=Bumper
serial:99:BD:46:16:75:99:BD:B4
X509v3 Basic Constraints:
CA:TRUE
X509v3 Subject Alternative Name:
email:null@null.com
Signature Algorithm: sha1WithRSAEncryption
5e:45:c7:34:7f:9b:a4:6b:cc:3c:e7:34:84:76:ee:ec:98:be:
0f:de:ab:b2:a5:6d:59:18:eb:20:de:a4:fc:2c:4d:e4:9a:9c:
63:99:be:16:6a:1d:c0:57:73:e8:7f:9e:78:4d:2a:e6:11:a3:
b6:57:18:84:6e:ae:a7:97:08:7e:1f:4d:33:fc:46:06:a7:d1:
6e:3a:d1:d9:ef:ba:fd:87:b8:e0:ea:51:85:d2:a6:de:49:e1:
a6:43:d1:26:15:e2:f9:8b:8c:f0:94:fe:a3:62:dd:cc:ab:be:
a5:dc:90:d8:28:0c:85:4b:fb:df:4a:f1:ac:35:86:7f:63:fd:
ca:c6:aa:29:0b:b0:e1:e3:08:e6:4c:71:d2:08:32:c9:09:22:
6c:61:ec:87:49:8d:77:48:7a:f7:75:86:bb:4e:17:b0:fe:43:
4d:2f:95:bd:d5:0f:c1:ef:ba:f7:3e:e0:b6:2f:fa:4a:c9:45:
36:97:56:a4:6d:af:86:88:ff:98:2f:b2:e4:04:4d:c4:02:20:
fc:66:8c:d5:2b:e9:5a:6c:1f:d2:67:34:c3:39:7e:f4:c7:37:
53:6b:11:99:a8:29:8b:3f:f6:3a:82:ae:f5:2c:d5:b7:23:88:
bb:03:84:ad:cb:d0:f6:8d:24:9f:54:08:d1:39:da:1a:a9:f6:
1b:c9:53:65
-----BEGIN CERTIFICATE-----
MIIEADCCAuigAwIBAgIJAJm9RhZ1mb20MA0GCSqGSIb3DQEBBQUAMFUxCzAJBgNV
BAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMQ8wDQYDVQQKEwZCdW1wZXIxDzAN
BgNVBAsTBkJ1bXBlcjEPMA0GA1UEAxMGQnVtcGVyMB4XDTE5MDIwMTA1NTkzMloX
DTI5MDEyOTA1NTkzMlowVTELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3Ju
aWExDzANBgNVBAoTBkJ1bXBlcjEPMA0GA1UECxMGQnVtcGVyMQ8wDQYDVQQDEwZC
dW1wZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCqbq8VmTsJMqGn
S+73E2jBlE8JcVSZAi0nwV8WLkXTpkQk+73wm6n2bElvCRN21luIwvyLixRMOd+K
tVGog8Fzyqrxt7Q44fCRNb5vxpl84e/ZwO7V+/j1D6eWs3iV1ioEfTsxQrYzwDYs
BvFt2xGZFj53xsbf7ZVAuDmigkD+u/aLYwzGlq79O8d5pPVfToOHJzO7b89DhlVz
bAwoYYlmcrOQPdhUU2o6V3YBBViseD0NClK0t74rTppq9PiiPyGZtrR0+9XAI3vo
UfUK7GoRTpvD8lYABjtY489ntsMmUA73chE/OSzJt9VpGjPcj4FmxYg/xwGU8D2s
qvOgKujHAgMBAAGjgdIwgc8wHQYDVR0OBBYEFF45x3gxgzBRgtmIm3MLcu9EdHvV
MIGFBgNVHSMEfjB8gBReOcd4MYMwUYLZiJtzC3LvRHR71aFZpFcwVTELMAkGA1UE
BhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExDzANBgNVBAoTBkJ1bXBlcjEPMA0G
A1UECxMGQnVtcGVyMQ8wDQYDVQQDEwZCdW1wZXKCCQCZvUYWdZm9tDAMBgNVHRME
BTADAQH/MBgGA1UdEQQRMA+BDW51bGxAbnVsbC5jb20wDQYJKoZIhvcNAQEFBQAD
ggEBAF5FxzR/m6RrzDznNIR27uyYvg/eq7KlbVkY6yDepPwsTeSanGOZvhZqHcBX
c+h/nnhNKuYRo7ZXGIRurqeXCH4fTTP8Rgan0W460dnvuv2HuODqUYXSpt5J4aZD
0SYV4vmLjPCU/qNi3cyrvqXckNgoDIVL+99K8aw1hn9j/crGqikLsOHjCOZMcdII
MskJImxh7IdJjXdIevd1hrtOF7D+Q00vlb3VD8Hvuvc+4LYv+krJRTaXVqRtr4aI
/5gvsuQETcQCIPxmjNUr6VpsH9JnNMM5fvTHN1NrEZmoKYs/9jqCrvUs1bcjiLsD
hK3L0PaNJJ9UCNE52hqp9hvJU2U=
-----END CERTIFICATE-----

18
certs/CA/careq.pem Normal file
View file

@ -0,0 +1,18 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIC2TCCAcECAQAwaTELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWEx
EjAQBgNVBAcTCUhhd3Rob3JuZTEPMA0GA1UEChMGQnVtcGVyMQ8wDQYDVQQLEwZC
dW1wZXIxDzANBgNVBAMTBkJ1bXBlcjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
AQoCggEBAKpurxWZOwkyoadL7vcTaMGUTwlxVJkCLSfBXxYuRdOmRCT7vfCbqfZs
SW8JE3bWW4jC/IuLFEw534q1UaiDwXPKqvG3tDjh8JE1vm/GmXzh79nA7tX7+PUP
p5azeJXWKgR9OzFCtjPANiwG8W3bEZkWPnfGxt/tlUC4OaKCQP679otjDMaWrv07
x3mk9V9Og4cnM7tvz0OGVXNsDChhiWZys5A92FRTajpXdgEFWKx4PQ0KUrS3vitO
mmr0+KI/IZm2tHT71cAje+hR9QrsahFOm8PyVgAGO1jjz2e2wyZQDvdyET85LMm3
1WkaM9yPgWbFiD/HAZTwPayq86Aq6McCAwEAAaArMCkGCSqGSIb3DQEJDjEcMBow
GAYDVR0RBBEwD4ENbnVsbEBudWxsLmNvbTANBgkqhkiG9w0BAQsFAAOCAQEAQU/v
8GRj5M3Qs0at2UGxz7uKZF5RKt6hE5WreS/joaMkK4+kTqA8FsfCGz0zUyyLWNKh
1o4b99yvYXG+rP8euMyBql/WSnEcF0KYrrGyUjH0qQQfTHqC0UqolvqoQCkPbvUO
sWZVzHeVxIH3nNtXgkIS+PHvTIhDLA3CxrDSe5jz4XLaACPoK/dawojAd0Qet0zZ
GfXk9Vm/5D0k/7MPkstOTAFCvfln2cdoYhA34WNkPW+uZNGBEdXTN3Tg/Oj8qAqV
RdIXR8joIzgAJnCit1IDyTPHR88UNTlFtk4AJ9Ott4v1hmUOf3kBqB0owXj+fCpq
3Mz7yL1DioePr8h7iw==
-----END CERTIFICATE REQUEST-----

View file

@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIC0DCCAbgCAQAwETEPMA0GA1UEAwwGQnVtcGVyMIIBIjANBgkqhkiG9w0BAQEF
AAOCAQ8AMIIBCgKCAQEA0eEqslnaJcbhqTalVMQTNT8/s6957rLSw78cZSVQEISN
MwyS4Q5tYYJzXXEmqyp8rBMWAArfWGaPO8sgKs6rC9UcYurVr1GbxtsVdRRRI/fg
tt2rtWQYdkBI7Dz9vjkcUF1tWwydWzPtKunzHGq9XNWY8NvJ4eq5dnO1oF1wyPWe
5bbKURJpA3gOXuCtEY+TOv++8PK3vdPQnkE1pFRnTfBP5rmatnReFlbfxCUCwcnq
X5a0+BeDanw4GSm4m0MCFXGWEb07P+8JUdUP+2rbMIZwUb42BBz/UKBeg410vgwh
PISyQetW4XGn3bLJk933GFO6gvOU9CVdzPdxRPPLPQIDAQABoHoweAYJKoZIhvcN
AQkOMWswaTAOBgNVHQ8BAf8EBAMCBaAwIAYDVR0lAQH/BBYwFAYIKwYBBQUHAwEG
CCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwJwYDVR0RBCAwHoINKi5lY291c2VyLm5l
dIINKi5lY292YWNzLmNvbTANBgkqhkiG9w0BAQsFAAOCAQEAloHqI4fjQEL0H0I0
TMr+DemH5GhYyVC7hs5bPfajqUbrQdTMvfUhfcUm0JgAmicKlAvVe1+75grkXeCF
sfGBl10f5dbO9CEHBT9kzDJWhFT/ZFzGlblAwB6Kad7QGpIMKncJCZBUwkE4zrwO
wEI1hAo0zvJr/SETte4YO9JianzT1Tu5f3nVgvKYVM+XJ0KZ7ReIdrT0Ev95mZhB
tmZ1R7WeYtO8yZXVHP28uOHZcHCKgKcnrpb13yuNd6mkR643xMoirwp9t7xayJAt
RnQgvb9o8HXNZoO1whr4ZF8gNs0MQGnT72MoV5E2KSZ9HMO4nQlzj9wIiEMls+ss
UTUhBg==
-----END CERTIFICATE REQUEST-----

View file

@ -0,0 +1,85 @@
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 11078087720677522868 (0x99bd46167599bdb4)
Signature Algorithm: sha1WithRSAEncryption
Issuer: C=US, ST=California, O=Bumper, OU=Bumper, CN=Bumper
Validity
Not Before: Feb 1 05:59:32 2019 GMT
Not After : Jan 29 05:59:32 2029 GMT
Subject: C=US, ST=California, O=Bumper, OU=Bumper, CN=Bumper
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (2048 bit)
Modulus:
00:aa:6e:af:15:99:3b:09:32:a1:a7:4b:ee:f7:13:
68:c1:94:4f:09:71:54:99:02:2d:27:c1:5f:16:2e:
45:d3:a6:44:24:fb:bd:f0:9b:a9:f6:6c:49:6f:09:
13:76:d6:5b:88:c2:fc:8b:8b:14:4c:39:df:8a:b5:
51:a8:83:c1:73:ca:aa:f1:b7:b4:38:e1:f0:91:35:
be:6f:c6:99:7c:e1:ef:d9:c0:ee:d5:fb:f8:f5:0f:
a7:96:b3:78:95:d6:2a:04:7d:3b:31:42:b6:33:c0:
36:2c:06:f1:6d:db:11:99:16:3e:77:c6:c6:df:ed:
95:40:b8:39:a2:82:40:fe:bb:f6:8b:63:0c:c6:96:
ae:fd:3b:c7:79:a4:f5:5f:4e:83:87:27:33:bb:6f:
cf:43:86:55:73:6c:0c:28:61:89:66:72:b3:90:3d:
d8:54:53:6a:3a:57:76:01:05:58:ac:78:3d:0d:0a:
52:b4:b7:be:2b:4e:9a:6a:f4:f8:a2:3f:21:99:b6:
b4:74:fb:d5:c0:23:7b:e8:51:f5:0a:ec:6a:11:4e:
9b:c3:f2:56:00:06:3b:58:e3:cf:67:b6:c3:26:50:
0e:f7:72:11:3f:39:2c:c9:b7:d5:69:1a:33:dc:8f:
81:66:c5:88:3f:c7:01:94:f0:3d:ac:aa:f3:a0:2a:
e8:c7
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Subject Key Identifier:
5E:39:C7:78:31:83:30:51:82:D9:88:9B:73:0B:72:EF:44:74:7B:D5
X509v3 Authority Key Identifier:
keyid:5E:39:C7:78:31:83:30:51:82:D9:88:9B:73:0B:72:EF:44:74:7B:D5
DirName:/C=US/ST=California/O=Bumper/OU=Bumper/CN=Bumper
serial:99:BD:46:16:75:99:BD:B4
X509v3 Basic Constraints:
CA:TRUE
X509v3 Subject Alternative Name:
email:null@null.com
Signature Algorithm: sha1WithRSAEncryption
5e:45:c7:34:7f:9b:a4:6b:cc:3c:e7:34:84:76:ee:ec:98:be:
0f:de:ab:b2:a5:6d:59:18:eb:20:de:a4:fc:2c:4d:e4:9a:9c:
63:99:be:16:6a:1d:c0:57:73:e8:7f:9e:78:4d:2a:e6:11:a3:
b6:57:18:84:6e:ae:a7:97:08:7e:1f:4d:33:fc:46:06:a7:d1:
6e:3a:d1:d9:ef:ba:fd:87:b8:e0:ea:51:85:d2:a6:de:49:e1:
a6:43:d1:26:15:e2:f9:8b:8c:f0:94:fe:a3:62:dd:cc:ab:be:
a5:dc:90:d8:28:0c:85:4b:fb:df:4a:f1:ac:35:86:7f:63:fd:
ca:c6:aa:29:0b:b0:e1:e3:08:e6:4c:71:d2:08:32:c9:09:22:
6c:61:ec:87:49:8d:77:48:7a:f7:75:86:bb:4e:17:b0:fe:43:
4d:2f:95:bd:d5:0f:c1:ef:ba:f7:3e:e0:b6:2f:fa:4a:c9:45:
36:97:56:a4:6d:af:86:88:ff:98:2f:b2:e4:04:4d:c4:02:20:
fc:66:8c:d5:2b:e9:5a:6c:1f:d2:67:34:c3:39:7e:f4:c7:37:
53:6b:11:99:a8:29:8b:3f:f6:3a:82:ae:f5:2c:d5:b7:23:88:
bb:03:84:ad:cb:d0:f6:8d:24:9f:54:08:d1:39:da:1a:a9:f6:
1b:c9:53:65
-----BEGIN CERTIFICATE-----
MIIEADCCAuigAwIBAgIJAJm9RhZ1mb20MA0GCSqGSIb3DQEBBQUAMFUxCzAJBgNV
BAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMQ8wDQYDVQQKEwZCdW1wZXIxDzAN
BgNVBAsTBkJ1bXBlcjEPMA0GA1UEAxMGQnVtcGVyMB4XDTE5MDIwMTA1NTkzMloX
DTI5MDEyOTA1NTkzMlowVTELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3Ju
aWExDzANBgNVBAoTBkJ1bXBlcjEPMA0GA1UECxMGQnVtcGVyMQ8wDQYDVQQDEwZC
dW1wZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCqbq8VmTsJMqGn
S+73E2jBlE8JcVSZAi0nwV8WLkXTpkQk+73wm6n2bElvCRN21luIwvyLixRMOd+K
tVGog8Fzyqrxt7Q44fCRNb5vxpl84e/ZwO7V+/j1D6eWs3iV1ioEfTsxQrYzwDYs
BvFt2xGZFj53xsbf7ZVAuDmigkD+u/aLYwzGlq79O8d5pPVfToOHJzO7b89DhlVz
bAwoYYlmcrOQPdhUU2o6V3YBBViseD0NClK0t74rTppq9PiiPyGZtrR0+9XAI3vo
UfUK7GoRTpvD8lYABjtY489ntsMmUA73chE/OSzJt9VpGjPcj4FmxYg/xwGU8D2s
qvOgKujHAgMBAAGjgdIwgc8wHQYDVR0OBBYEFF45x3gxgzBRgtmIm3MLcu9EdHvV
MIGFBgNVHSMEfjB8gBReOcd4MYMwUYLZiJtzC3LvRHR71aFZpFcwVTELMAkGA1UE
BhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExDzANBgNVBAoTBkJ1bXBlcjEPMA0G
A1UECxMGQnVtcGVyMQ8wDQYDVQQDEwZCdW1wZXKCCQCZvUYWdZm9tDAMBgNVHRME
BTADAQH/MBgGA1UdEQQRMA+BDW51bGxAbnVsbC5jb20wDQYJKoZIhvcNAQEFBQAD
ggEBAF5FxzR/m6RrzDznNIR27uyYvg/eq7KlbVkY6yDepPwsTeSanGOZvhZqHcBX
c+h/nnhNKuYRo7ZXGIRurqeXCH4fTTP8Rgan0W460dnvuv2HuODqUYXSpt5J4aZD
0SYV4vmLjPCU/qNi3cyrvqXckNgoDIVL+99K8aw1hn9j/crGqikLsOHjCOZMcdII
MskJImxh7IdJjXdIevd1hrtOF7D+Q00vlb3VD8Hvuvc+4LYv+krJRTaXVqRtr4aI
/5gvsuQETcQCIPxmjNUr6VpsH9JnNMM5fvTHN1NrEZmoKYs/9jqCrvUs1bcjiLsD
hK3L0PaNJJ9UCNE52hqp9hvJU2U=
-----END CERTIFICATE-----

View file

@ -0,0 +1,85 @@
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 11078087720677522869 (0x99bd46167599bdb5)
Signature Algorithm: sha1WithRSAEncryption
Issuer: C=US, ST=California, O=Bumper, OU=Bumper, CN=Bumper
Validity
Not Before: Feb 1 06:17:18 2019 GMT
Not After : Jan 29 06:17:18 2029 GMT
Subject: CN=Bumper
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (2048 bit)
Modulus:
00:d1:e1:2a:b2:59:da:25:c6:e1:a9:36:a5:54:c4:
13:35:3f:3f:b3:af:79:ee:b2:d2:c3:bf:1c:65:25:
50:10:84:8d:33:0c:92:e1:0e:6d:61:82:73:5d:71:
26:ab:2a:7c:ac:13:16:00:0a:df:58:66:8f:3b:cb:
20:2a:ce:ab:0b:d5:1c:62:ea:d5:af:51:9b:c6:db:
15:75:14:51:23:f7:e0:b6:dd:ab:b5:64:18:76:40:
48:ec:3c:fd:be:39:1c:50:5d:6d:5b:0c:9d:5b:33:
ed:2a:e9:f3:1c:6a:bd:5c:d5:98:f0:db:c9:e1:ea:
b9:76:73:b5:a0:5d:70:c8:f5:9e:e5:b6:ca:51:12:
69:03:78:0e:5e:e0:ad:11:8f:93:3a:ff:be:f0:f2:
b7:bd:d3:d0:9e:41:35:a4:54:67:4d:f0:4f:e6:b9:
9a:b6:74:5e:16:56:df:c4:25:02:c1:c9:ea:5f:96:
b4:f8:17:83:6a:7c:38:19:29:b8:9b:43:02:15:71:
96:11:bd:3b:3f:ef:09:51:d5:0f:fb:6a:db:30:86:
70:51:be:36:04:1c:ff:50:a0:5e:83:8d:74:be:0c:
21:3c:84:b2:41:eb:56:e1:71:a7:dd:b2:c9:93:dd:
f7:18:53:ba:82:f3:94:f4:25:5d:cc:f7:71:44:f3:
cb:3d
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Basic Constraints:
CA:FALSE
X509v3 Subject Key Identifier:
26:7E:4F:14:80:43:9E:EF:64:C2:61:6E:81:68:4B:6A:F2:4E:B5:40
X509v3 Authority Key Identifier:
keyid:5E:39:C7:78:31:83:30:51:82:D9:88:9B:73:0B:72:EF:44:74:7B:D5
X509v3 Subject Alternative Name:
DNS:*.ecouser.net, DNS:*.ecovacs.com
X509v3 Key Usage: critical
Digital Signature, Key Encipherment
X509v3 Extended Key Usage: critical
TLS Web Server Authentication, TLS Web Client Authentication
Signature Algorithm: sha1WithRSAEncryption
89:11:41:18:ac:22:0d:4a:08:31:ee:18:d4:cf:88:02:93:f0:
ef:53:f5:86:e1:b2:16:0e:8d:87:f9:e9:29:7a:aa:95:4e:d7:
89:96:ac:1c:9a:19:8c:84:62:41:7a:b8:8c:46:07:8f:56:ae:
18:31:52:cb:60:3f:01:57:4d:65:d6:5f:95:da:bc:c1:53:35:
fa:cc:c6:49:aa:fe:35:d3:05:15:2c:bd:9b:ac:f0:36:d7:71:
bc:ed:3e:f9:ac:c8:84:58:5e:f0:02:02:47:9c:0f:f3:f6:06:
35:4d:3b:69:2f:bb:81:cd:ae:61:84:75:97:b1:c2:e7:e1:74:
18:61:bc:c0:6b:2e:4d:f6:61:0d:36:72:3d:e2:ef:ef:e5:cf:
7a:17:af:d6:7c:d2:a4:88:ca:b3:4e:27:7e:f3:da:96:33:ef:
12:12:02:5a:31:70:82:e9:b6:e4:a9:57:d7:a4:b3:f6:3f:ea:
fa:63:6a:e6:b7:26:73:bb:c1:87:4e:e2:13:40:65:b9:04:3c:
c9:95:d7:8e:07:6f:9d:af:d2:36:ee:aa:5d:e5:f9:bb:cc:47:
9c:ce:62:cf:28:2e:a3:58:8b:63:a2:e3:c3:f4:b0:69:98:aa:
84:ff:a5:c2:52:8b:e8:b4:5c:2c:67:ef:63:b3:5c:42:95:6d:
6b:d7:5b:11
-----BEGIN CERTIFICATE-----
MIIDkzCCAnugAwIBAgIJAJm9RhZ1mb21MA0GCSqGSIb3DQEBBQUAMFUxCzAJBgNV
BAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMQ8wDQYDVQQKEwZCdW1wZXIxDzAN
BgNVBAsTBkJ1bXBlcjEPMA0GA1UEAxMGQnVtcGVyMB4XDTE5MDIwMTA2MTcxOFoX
DTI5MDEyOTA2MTcxOFowETEPMA0GA1UEAwwGQnVtcGVyMIIBIjANBgkqhkiG9w0B
AQEFAAOCAQ8AMIIBCgKCAQEA0eEqslnaJcbhqTalVMQTNT8/s6957rLSw78cZSVQ
EISNMwyS4Q5tYYJzXXEmqyp8rBMWAArfWGaPO8sgKs6rC9UcYurVr1GbxtsVdRRR
I/fgtt2rtWQYdkBI7Dz9vjkcUF1tWwydWzPtKunzHGq9XNWY8NvJ4eq5dnO1oF1w
yPWe5bbKURJpA3gOXuCtEY+TOv++8PK3vdPQnkE1pFRnTfBP5rmatnReFlbfxCUC
wcnqX5a0+BeDanw4GSm4m0MCFXGWEb07P+8JUdUP+2rbMIZwUb42BBz/UKBeg410
vgwhPISyQetW4XGn3bLJk933GFO6gvOU9CVdzPdxRPPLPQIDAQABo4GpMIGmMAkG
A1UdEwQCMAAwHQYDVR0OBBYEFCZ+TxSAQ57vZMJhboFoS2ryTrVAMB8GA1UdIwQY
MBaAFF45x3gxgzBRgtmIm3MLcu9EdHvVMCcGA1UdEQQgMB6CDSouZWNvdXNlci5u
ZXSCDSouZWNvdmFjcy5jb20wDgYDVR0PAQH/BAQDAgWgMCAGA1UdJQEB/wQWMBQG
CCsGAQUFBwMBBggrBgEFBQcDAjANBgkqhkiG9w0BAQUFAAOCAQEAiRFBGKwiDUoI
Me4Y1M+IApPw71P1huGyFg6Nh/npKXqqlU7XiZasHJoZjIRiQXq4jEYHj1auGDFS
y2A/AVdNZdZfldq8wVM1+szGSar+NdMFFSy9m6zwNtdxvO0++azIhFhe8AICR5wP
8/YGNU07aS+7gc2uYYR1l7HC5+F0GGG8wGsuTfZhDTZyPeLv7+XPehev1nzSpIjK
s04nfvPaljPvEhICWjFwgum25KlX16Sz9j/q+mNq5rcmc7vBh07iE0BluQQ8yZXX
jgdvna/SNu6qXeX5u8xHnM5izyguo1iLY6Ljw/SwaZiqhP+lwlKL6LRcLGfvY7Nc
QpVta9dbEQ==
-----END CERTIFICATE-----

2
certs/CA/index.txt Normal file
View file

@ -0,0 +1,2 @@
V 290129055932Z 99BD46167599BDB4 unknown /C=US/ST=California/O=Bumper/OU=Bumper/CN=Bumper
V 290129061718Z 99BD46167599BDB5 unknown /CN=Bumper

1
certs/CA/index.txt.attr Normal file
View file

@ -0,0 +1 @@
unique_subject = yes

View file

@ -0,0 +1 @@
unique_subject = yes

1
certs/CA/index.txt.old Normal file
View file

@ -0,0 +1 @@
V 290129055932Z 99BD46167599BDB4 unknown /C=US/ST=California/O=Bumper/OU=Bumper/CN=Bumper

180
certs/CA/openssl.cnf Normal file
View file

@ -0,0 +1,180 @@
HOME = .
RANDFILE = $ENV::HOME/.rnd
####################################################################
# CA Definition
[ ca ]
default_ca = CA_default # The default ca section
####################################################################
# Per the above, this is where we define CA values
[ CA_default ]
dir = . # Where everything is kept
certs = $dir/certsdb # Where the issued certs are kept
new_certs_dir = $certs # default place for new certs.
database = $dir/index.txt # database index file.
certificate = $dir/cacert.pem # The CA certificate
private_key = $dir/private/cakey.pem# The private key
serial = $dir/serial # The current serial number
RANDFILE = $dir/private/.rand # private random number file
crldir = $dir/crl
crlnumber = $dir/crlnumber # the current crl number
crl = $crldir/crl.pem # The current CRL
# By default we use "user certificate" extensions when signing
x509_extensions = usr_cert # The extentions to add to the cert
# Honor extensions requested of us
copy_extensions = copy
# Comment out the following two lines for the "traditional"
# (and highly broken) format.
name_opt = ca_default # Subject Name options
cert_opt = ca_default # Certificate field options
# Extensions to add to a CRL. Note: Netscape communicator chokes on V2 CRLs
# so this is commented out by default to leave a V1 CRL.
# crlnumber must also be commented out to leave a V1 CRL.
#crl_extensions = crl_ext
default_days = 3650 # how long to certify for
default_crl_days= 30 # how long before next CRL
default_md = sha1 # which md to use.
preserve = no # keep passed DN ordering
# A few difference way of specifying how similar the request should look
# For type CA, the listed attributes must be the same, and the optional
# and supplied fields are just that :-)
policy = policy_anything
####################################################################
# The default policy for the CA when signing requests, requires some
# resemblence to the CA cert
#
[ policy_match ]
countryName = match # Must be the same as the CA
stateOrProvinceName = match # Must be the same as the CA
organizationName = match # Must be the same as the CA
organizationalUnitName = optional # not required
commonName = supplied # must be there, whatever it is
emailAddress = optional # not required
####################################################################
# An alternative policy not referred to anywhere in this file. Can
# be used by specifying '-policy policy_anything' to ca(8).
#
[ policy_anything ]
countryName = optional
stateOrProvinceName = optional
localityName = optional
organizationName = optional
organizationalUnitName = optional
commonName = supplied
emailAddress = optional
####################################################################
# This is where we define how to generate CSRs
[ req ]
default_bits = 2048
default_keyfile = privkey.pem
distinguished_name = req_distinguished_name # where to get DN for reqs
attributes = req_attributes # req attributes
x509_extensions = v3_ca # The extentions to add to self signed certs
req_extensions = v3_req # The extensions to add to req's
prompt = no
# This sets a mask for permitted string types. There are several options.
# default: PrintableString, T61String, BMPString.
# pkix : PrintableString, BMPString.
# utf8only: only UTF8Strings.
# nombstr : PrintableString, T61String (no BMPStrings or UTF8Strings).
# MASK:XXXX a literal mask value.
# WARNING: current versions of Netscape crash on BMPStrings or UTF8Strings
# so use this option with caution!
string_mask = nombstr
####################################################################
# Per "req" section, this is where we define DN info
[ req_distinguished_name ]
countryName = Country Name (2 letter code)
countryName_default = US
countryName_min = 2
countryName_max = 2
stateOrProvinceName = State or Province Name (full name)
stateOrProvinceName_default = California
localityName = Locality Name (eg, city)
localityName_default = Hawthorne
0.organizationName = Organization Name (eg, company)
0.organizationName_default = PhilNet
organizationalUnitName = Organizational Unit Name (eg, section)
commonName = Common Name (eg, YOUR name)
commonName_max = 64
emailAddress = Email Address
emailAddress_max = 64
####################################################################
# We don't want these, but the section must exist
[ req_attributes ]
#challengePassword = A challenge password
#challengePassword_min = 4
#challengePassword_max = 20
#unstructuredName = An optional company name
####################################################################
[ v3_req ]
keyUsage=critical,digitalSignature,keyEncipherment
extendedKeyUsage=critical,serverAuth,clientAuth
basicConstraints=critical,CA:false
subjectAltName = @alt_names
[ alt_names ]
DNS.0 = *.ecouser.net
DNS.1 = *.ecovacs.com
####################################################################
# Extensions for when we sign normal certs (specified as default)
[ usr_cert ]
basicConstraints = CA:false
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
subjectAltName = @alt_names
[ alt_names ]
DNS.0 = *.ecouser.net
DNS.1 = *.ecovacs.com
####################################################################
# Same as above, but cert req already has SubjectAltName
[ usr_cert_has_san ]
basicConstraints = CA:false
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
subjectAltName = @alt_names
[ alt_names ]
DNS.0 = *.ecouser.net
DNS.1 = *.ecovacs.com
####################################################################
# Extensions to use when signing a CA
[ v3_ca ]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer:always
basicConstraints = CA:true
subjectAltName=email:move
####################################################################
# Same as above, but CA req already has SubjectAltName
[ v3_ca_has_san ]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer:always
basicConstraints = CA:true

View file

@ -0,0 +1,30 @@
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIFHzBJBgkqhkiG9w0BBQ0wPDAbBgkqhkiG9w0BBQwwDgQIzLScD02ODzQCAggA
MB0GCWCGSAFlAwQBKgQQzjzZ8LiXuLxb/Rx75kGXkwSCBNAmHUh4RTgy1bJmwZN7
uyb4gGHtY9+ZvKZikibq7LpiwmDAwjjwguJz/HRsquq8fsZDpB+rV4hD4lB3lHuj
jqETzKst5qLTgP0cC9BS0S0P9f0/L7MEcwtiBkds6dVs4Ot6IDr4napC+6wQCBfr
ISrTN8+ls9IVghiS1eOrarFTy2ZxkXFM6PJczLd15/pbTm91etLWl1vnPCJn3MTc
vUvneAChKFiwaX4Cbx8bhQP11OBBNshM1zANSoGAdijRpvRCGfeOIvgLsbBl9N0d
+lV6iJR0TzOr4GquBDXor/y2Mofn6wYRwtSLRUgHgAxy1USosWVuXwrtGO4htPoR
aGt8V3h4XQ90VGAh6ez43/nmkVDgIj8VU9qZ+skCz7Y4TxPZYXUIcnG14KFj9lsk
r+uV6HmHyBKSlSkAE+D9uukQb71oIW83tsSn6I4ulcWaax6VsOS5n/Jrhf9V+Nz0
e0CjrOJeLUSd2vTiyz03ho32nIrsGFeNDHhFmKzXNynjcW3z6Mx9/dSjqPHn8wFn
0dcVkq0GAZ+L8woNyDhr4e7Hhy6FEDhRX9bWz7FlswnBzym2AzU/zhagDrP2MIQ9
o/cjAp3AoX70IIF7kIha8OXG4ew4tHpQIAj9E5SG5uEeD6gVTg7HB93dH+NEeLNR
F65wzQHPhf+V2zR7qlpCg3gqzyE9mvvTN/DMAJwASXWXK37ITIjcXhErH/2shHSK
jRwYuQiAWuhPrAr04EUoeHQFM6EAKDqBsFRr+/Ot34DqhAwAOlmkyCM/mw7IDl73
pX/Ey9CQc2u2PS0zqYkDIzWE1Osek3FIF6JHSzaiRYODjaE6v15FLe7hZ56x6NMI
EDzpScp6HwMg6jsudbONrz3KwvXE6j4+yWpp8tgkZzwsIfnOMtWSrGgX4Oz9Y0Ep
jZ1yKDLQBjKZor2z4FBNze74tEnzJdBl59mQoN3ffFWHRlnkOhAZ+HsaXoIKB6AZ
3NPzlgGhOmtza6HKAwlseMgjpNH7EmeTLplr5nUlEMwwJ2+i8VGS4J9458vipcBm
50MbqaR8N8jAsanxyAbBF7gLPzsezN9Kzus99B6uB2nZIgZIsOeCeBLEZGzz9bUv
AiI3rM38nFkkKeHjJwqcYsO6LiLxdstnaHSveE7d6ugmN3+bNaZ+QCUDFlfELr4M
n5Eiwv3+hlCL8UBubacdQypfYvYI/z8rTdH+5eTVxBjeTCG0QmjDlYiuHcycL+q3
iE3WxQ0yGpeJL6V985Ej/0SHkudTI49T5o+jcp6HFNS7xQelBGndpXGq9Xyc+OV/
c+1zYWxz/sMFAbMkCF2pMMLfKL5EBkLBCpOdxHtWFt9JMxuhRfl3YzN81xSV1m0A
FtURjPUSwGqI7QFPl4R4MA1+0AdveZYprGo2nSUQpSoCW9WHl8U8tLIUNtew1MPA
v19h5DSqJ3tj+pPdxmFixoG+hBNk8FO1lJvH6P2an6lYVTlZ3/uDU5/DHTUTHuen
9x8FY3LQDQZA65QEnN092cUxXWw6cha+xXvwrwZGZiWXtuMBh/mfa4apxQ5+KY0N
/RCbIBctvLgijuC+xQw8GqBcG5L7urCOhnyZCXspWW7+loI7uZ0EfpNi7jY8BmAs
3S3MwMwwp+uvloikg3ZTUJmx/g==
-----END ENCRYPTED PRIVATE KEY-----

28
certs/CA/private/key.pem Normal file
View file

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDR4SqyWdolxuGp
NqVUxBM1Pz+zr3nustLDvxxlJVAQhI0zDJLhDm1hgnNdcSarKnysExYACt9YZo87
yyAqzqsL1Rxi6tWvUZvG2xV1FFEj9+C23au1ZBh2QEjsPP2+ORxQXW1bDJ1bM+0q
6fMcar1c1Zjw28nh6rl2c7WgXXDI9Z7ltspREmkDeA5e4K0Rj5M6/77w8re909Ce
QTWkVGdN8E/muZq2dF4WVt/EJQLByepflrT4F4NqfDgZKbibQwIVcZYRvTs/7wlR
1Q/7atswhnBRvjYEHP9QoF6DjXS+DCE8hLJB61bhcafdssmT3fcYU7qC85T0JV3M
93FE88s9AgMBAAECggEBAJ4N3aOWeuv8DDDsUDG3v2dYDFb7DO4viP2UzIN3SAZI
bDVHgM0vmjvWi56Q+bfpNR44+VZq2auvAKrc24qhPLqacNglVWrEIHXuwwf6luhp
YbcU1JyzlqBm5QPTIpvAUoxK73Gj3FS0lCS/Li/2SkjBKn4oZq5Vn/mORfdQpwEO
aY5YzNNCzCwbqL/XcwvN1XRpF9puhBb+o2TtfGPLBf5LiScJWxrtPM7q+dFG/Ef7
f9L28iX+W6e3lD0/d93XEtlh1IsaZTWkl4m01Etg0f2FFtcja38eG+Yys4YNSpPe
VUWtFL2eDX+u5wv7nbaG9GsZmithXL6xa0PdwOd0KAECgYEA/D7K2KcxS+HeBgcG
V3b0m6QH1bRbqRxzK22TKnCVLAlRW7+5bfK56sHqMrOnDp/PuyAdq93roeJBNnAg
DrUt9i/lFTZ2kSBTJIlKW7cHv2Hocw3sQSDY/PhAXtALULxa8NHadPGsntHQDLzq
TVUNhOU6+GqPcqTp8BeSy+XrGD0CgYEA1QDvbxh/5tQxCFXcKn5KQCpJhbCfKBSW
drd8YF3Gdpxt3d2vTFTOsQ3xyAvEhSV0Jy9WUXobmx+KK8OjZqsR4PoYpYN3IWIW
N9jBKE9bahPRFF3g3FxaeVrCBkZBVK+ZAv/E5gshCyCXOmgHT+Kgi1Xpb4X0J/Pj
T83O7y/DrwECgYEAlagfq4kOy2m307WpQo12NT2N1HIMi/owLoz1apGfP6BLn/bR
anU/bxQcAnbqX9fbZLd6QzUIj2Rwsn611mQSX+Ussng8wn/+NEgsUNv4AclyI4kY
Kc/2TahwcJBYlk8aWZT4WrfkZdov6M7Q6I7U1Ys3gJmggDFuBp/F3amM4TUCgYBv
IofKvUuukjPrF3FCMpZnGCk4Ockwzl0bH+0jEoqERGYGhq35853MP2xRZeJpXftW
npo49K8GptQetXrCDKvNaHAHm6pCnrzc8pkFgTI/DIvgRoMWNpF2E+Z8A5f+qYf6
SpzfMhtRhPWiobh+8820JWUgrRGQ7u1BLC62GYPOAQKBgG/rt2Xu42DJcQ/wm7wN
LB8zn54G/rKWq+mXQbenpr1lN4zBOGgZILrRb1HTcQ7a19056BHBmx3J52yQAnGH
cxuhOyppsH3WgUkUqYN4bzO1CJorX3cP9udBAcmN4BWLF7pLgT7zHrLkJg05sWnd
UHWm2y7vdXeTvdD381p0Ug0S
-----END PRIVATE KEY-----

1
certs/CA/serial Normal file
View file

@ -0,0 +1 @@
99BD46167599BDB6

1
certs/CA/serial.old Normal file
View file

@ -0,0 +1 @@
99BD46167599BDB5

View file

@ -1,33 +1,85 @@
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 11078087720677522869 (0x99bd46167599bdb5)
Signature Algorithm: sha1WithRSAEncryption
Issuer: C=US, ST=California, O=Bumper, OU=Bumper, CN=Bumper
Validity
Not Before: Feb 1 06:17:18 2019 GMT
Not After : Jan 29 06:17:18 2029 GMT
Subject: CN=Bumper
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (2048 bit)
Modulus:
00:d1:e1:2a:b2:59:da:25:c6:e1:a9:36:a5:54:c4:
13:35:3f:3f:b3:af:79:ee:b2:d2:c3:bf:1c:65:25:
50:10:84:8d:33:0c:92:e1:0e:6d:61:82:73:5d:71:
26:ab:2a:7c:ac:13:16:00:0a:df:58:66:8f:3b:cb:
20:2a:ce:ab:0b:d5:1c:62:ea:d5:af:51:9b:c6:db:
15:75:14:51:23:f7:e0:b6:dd:ab:b5:64:18:76:40:
48:ec:3c:fd:be:39:1c:50:5d:6d:5b:0c:9d:5b:33:
ed:2a:e9:f3:1c:6a:bd:5c:d5:98:f0:db:c9:e1:ea:
b9:76:73:b5:a0:5d:70:c8:f5:9e:e5:b6:ca:51:12:
69:03:78:0e:5e:e0:ad:11:8f:93:3a:ff:be:f0:f2:
b7:bd:d3:d0:9e:41:35:a4:54:67:4d:f0:4f:e6:b9:
9a:b6:74:5e:16:56:df:c4:25:02:c1:c9:ea:5f:96:
b4:f8:17:83:6a:7c:38:19:29:b8:9b:43:02:15:71:
96:11:bd:3b:3f:ef:09:51:d5:0f:fb:6a:db:30:86:
70:51:be:36:04:1c:ff:50:a0:5e:83:8d:74:be:0c:
21:3c:84:b2:41:eb:56:e1:71:a7:dd:b2:c9:93:dd:
f7:18:53:ba:82:f3:94:f4:25:5d:cc:f7:71:44:f3:
cb:3d
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Basic Constraints:
CA:FALSE
X509v3 Subject Key Identifier:
26:7E:4F:14:80:43:9E:EF:64:C2:61:6E:81:68:4B:6A:F2:4E:B5:40
X509v3 Authority Key Identifier:
keyid:5E:39:C7:78:31:83:30:51:82:D9:88:9B:73:0B:72:EF:44:74:7B:D5
X509v3 Subject Alternative Name:
DNS:*.ecouser.net, DNS:*.ecovacs.com
X509v3 Key Usage: critical
Digital Signature, Key Encipherment
X509v3 Extended Key Usage: critical
TLS Web Server Authentication, TLS Web Client Authentication
Signature Algorithm: sha1WithRSAEncryption
89:11:41:18:ac:22:0d:4a:08:31:ee:18:d4:cf:88:02:93:f0:
ef:53:f5:86:e1:b2:16:0e:8d:87:f9:e9:29:7a:aa:95:4e:d7:
89:96:ac:1c:9a:19:8c:84:62:41:7a:b8:8c:46:07:8f:56:ae:
18:31:52:cb:60:3f:01:57:4d:65:d6:5f:95:da:bc:c1:53:35:
fa:cc:c6:49:aa:fe:35:d3:05:15:2c:bd:9b:ac:f0:36:d7:71:
bc:ed:3e:f9:ac:c8:84:58:5e:f0:02:02:47:9c:0f:f3:f6:06:
35:4d:3b:69:2f:bb:81:cd:ae:61:84:75:97:b1:c2:e7:e1:74:
18:61:bc:c0:6b:2e:4d:f6:61:0d:36:72:3d:e2:ef:ef:e5:cf:
7a:17:af:d6:7c:d2:a4:88:ca:b3:4e:27:7e:f3:da:96:33:ef:
12:12:02:5a:31:70:82:e9:b6:e4:a9:57:d7:a4:b3:f6:3f:ea:
fa:63:6a:e6:b7:26:73:bb:c1:87:4e:e2:13:40:65:b9:04:3c:
c9:95:d7:8e:07:6f:9d:af:d2:36:ee:aa:5d:e5:f9:bb:cc:47:
9c:ce:62:cf:28:2e:a3:58:8b:63:a2:e3:c3:f4:b0:69:98:aa:
84:ff:a5:c2:52:8b:e8:b4:5c:2c:67:ef:63:b3:5c:42:95:6d:
6b:d7:5b:11
-----BEGIN CERTIFICATE-----
MIIFtTCCA52gAwIBAgIJAKLG5udzkR+zMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
aWRnaXRzIFB0eSBMdGQwHhcNMTcxMjA1MTkxNTQwWhcNMTgxMjA1MTkxNTQwWjBF
MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
CgKCAgEAu1z0Iw7KLJZD8CBrZWm9Jp7eqIAqdoTn3GWmNmzgIDnzmxZb70War74d
7aWDVtEVtOZQ8ShW15zRanr6hQkHdM5Li1KTMYW3P7TOmzJZkESNEZviOyvlfRPG
oLLbPIp+A7W8Zz3ST0cDzyy6N7LRAA0OywRgPhrxrN637Fx1+SFwqKQg0IUOuX3v
veZ1oAZSBUDzpDPUaak7jXugB11wt6XTEekwOcrH7WEDoFH87thtJyCM7IidW0bM
rkqxeiM18qMzn8cGqnm2nY+VHgK0t0/QZ/cftPVc2IHZLr/+eTGaJmPv7gL+7v/l
7fgk3RMTn0GrdTO+Vvnu1qNK7r+//KMgGOo6zwEQAI9Ml6y1LRuEXrymgsYY9qCE
ZtBV0FoZaVYAuGG4cvCDLLPgM2OivIcWgG+77MqMH/RkWg+BrUesn7ISEpF+7Q7+
jc6pFYr4y5fc2jJ9/c5Mxu6sv0Tc8N/c9WpcXsJ6Y1JrBbDcdGSwmTZ1JVyiog9O
6bU/16QTuU3w2oH9qjnc907tZuB66P53WJ2iHPvB8yF5V93FvtbPYsHpO7noURSI
LqvKoNy31ndv4kao2AIzrJuhBBkSorBEcoFerRRBJMHa85R5Tb6iuGg6RFv0BB8K
gv6mG/7oqmR8QDnRRd54U2pEsIkYmgoIU4/8L91r9Vd1eiEDEnsCAwEAAaOBpzCB
pDAdBgNVHQ4EFgQUqA/GBsZAepFIcbgw6gK/HTUWolswdQYDVR0jBG4wbIAUqA/G
BsZAepFIcbgw6gK/HTUWoluhSaRHMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpT
b21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGSCCQCi
xubnc5EfszAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4ICAQB7kHEHKNDQ
TcvwY9z3RscpHyWwjrGjUdjk8AkM9cTm/sT8G7JU/7+KZueu4elNOc4hir/+DMlO
/AN6IXRNrfPO8IVG6hKnK9K3wvceJzg6mT8F4peg4vMNdIsrZyQaPhWArUF7LG/Q
HF4Yk8jnw3NKIG5fnlagAGFDdDGSdDK/IQN+JA6sHJVQRrBLpksRUCWFY7QJGYVS
DmEAYr6Y6emftbflUjW6iC/E57lC5F7bQyoIEIBBHKjMaNTdM8TzOUZSOGLwAuVD
C1iAoZn7VWeGa4h/xwx0yio0yeI6gRh3fINBKoCNv6pLUm6sgrW8oqGMDJiH0T3H
sc8xC3bK2DPO4sIyisL3HUPcsSFB/hf0i8BOQrihrpZEn/fx4lTRyQYSKT5NVSl4
v/9hmDjWIXh9EInGfP8DdWp4Og8GiXcV9rUqvRBo5aBwqK/aTNRCk6ah5N958yFu
wHlrbQmPnw1AqJSG1oG+4dcXnrcgd/xGMAarTbgzqsd8icafg8v2x2pG5ZexE3uN
0METK9hvCrjN8/Rvo2ViTDa8WF4CXMuVFG4FdbeRrYPZl1j1jBYDMtKkVJJCsVQd
chYbqKgNDmMcbfpksBtF3XpYTtVNAKA9g5mrAhEJS88uy63PStLoBQNXv9y8ksql
VVCTFNGy5JRyvOeSRYSSdXjKbfj5Vgh1HQ==
MIIDkzCCAnugAwIBAgIJAJm9RhZ1mb21MA0GCSqGSIb3DQEBBQUAMFUxCzAJBgNV
BAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMQ8wDQYDVQQKEwZCdW1wZXIxDzAN
BgNVBAsTBkJ1bXBlcjEPMA0GA1UEAxMGQnVtcGVyMB4XDTE5MDIwMTA2MTcxOFoX
DTI5MDEyOTA2MTcxOFowETEPMA0GA1UEAwwGQnVtcGVyMIIBIjANBgkqhkiG9w0B
AQEFAAOCAQ8AMIIBCgKCAQEA0eEqslnaJcbhqTalVMQTNT8/s6957rLSw78cZSVQ
EISNMwyS4Q5tYYJzXXEmqyp8rBMWAArfWGaPO8sgKs6rC9UcYurVr1GbxtsVdRRR
I/fgtt2rtWQYdkBI7Dz9vjkcUF1tWwydWzPtKunzHGq9XNWY8NvJ4eq5dnO1oF1w
yPWe5bbKURJpA3gOXuCtEY+TOv++8PK3vdPQnkE1pFRnTfBP5rmatnReFlbfxCUC
wcnqX5a0+BeDanw4GSm4m0MCFXGWEb07P+8JUdUP+2rbMIZwUb42BBz/UKBeg410
vgwhPISyQetW4XGn3bLJk933GFO6gvOU9CVdzPdxRPPLPQIDAQABo4GpMIGmMAkG
A1UdEwQCMAAwHQYDVR0OBBYEFCZ+TxSAQ57vZMJhboFoS2ryTrVAMB8GA1UdIwQY
MBaAFF45x3gxgzBRgtmIm3MLcu9EdHvVMCcGA1UdEQQgMB6CDSouZWNvdXNlci5u
ZXSCDSouZWNvdmFjcy5jb20wDgYDVR0PAQH/BAQDAgWgMCAGA1UdJQEB/wQWMBQG
CCsGAQUFBwMBBggrBgEFBQcDAjANBgkqhkiG9w0BAQUFAAOCAQEAiRFBGKwiDUoI
Me4Y1M+IApPw71P1huGyFg6Nh/npKXqqlU7XiZasHJoZjIRiQXq4jEYHj1auGDFS
y2A/AVdNZdZfldq8wVM1+szGSar+NdMFFSy9m6zwNtdxvO0++azIhFhe8AICR5wP
8/YGNU07aS+7gc2uYYR1l7HC5+F0GGG8wGsuTfZhDTZyPeLv7+XPehev1nzSpIjK
s04nfvPaljPvEhICWjFwgum25KlX16Sz9j/q+mNq5rcmc7vBh07iE0BluQQ8yZXX
jgdvna/SNu6qXeX5u8xHnM5izyguo1iLY6Ljw/SwaZiqhP+lwlKL6LRcLGfvY7Nc
QpVta9dbEQ==
-----END CERTIFICATE-----

View file

@ -1,51 +1,28 @@
-----BEGIN RSA PRIVATE KEY-----
MIIJJwIBAAKCAgEAu1z0Iw7KLJZD8CBrZWm9Jp7eqIAqdoTn3GWmNmzgIDnzmxZb
70War74d7aWDVtEVtOZQ8ShW15zRanr6hQkHdM5Li1KTMYW3P7TOmzJZkESNEZvi
OyvlfRPGoLLbPIp+A7W8Zz3ST0cDzyy6N7LRAA0OywRgPhrxrN637Fx1+SFwqKQg
0IUOuX3vveZ1oAZSBUDzpDPUaak7jXugB11wt6XTEekwOcrH7WEDoFH87thtJyCM
7IidW0bMrkqxeiM18qMzn8cGqnm2nY+VHgK0t0/QZ/cftPVc2IHZLr/+eTGaJmPv
7gL+7v/l7fgk3RMTn0GrdTO+Vvnu1qNK7r+//KMgGOo6zwEQAI9Ml6y1LRuEXrym
gsYY9qCEZtBV0FoZaVYAuGG4cvCDLLPgM2OivIcWgG+77MqMH/RkWg+BrUesn7IS
EpF+7Q7+jc6pFYr4y5fc2jJ9/c5Mxu6sv0Tc8N/c9WpcXsJ6Y1JrBbDcdGSwmTZ1
JVyiog9O6bU/16QTuU3w2oH9qjnc907tZuB66P53WJ2iHPvB8yF5V93FvtbPYsHp
O7noURSILqvKoNy31ndv4kao2AIzrJuhBBkSorBEcoFerRRBJMHa85R5Tb6iuGg6
RFv0BB8Kgv6mG/7oqmR8QDnRRd54U2pEsIkYmgoIU4/8L91r9Vd1eiEDEnsCAwEA
AQKCAgBmUpJl3vrctMeviRT90mxhfhvda/lEtrqla8IFMRqJnvyCMwjMOHgNCEfx
9BUxQYiYPbvwp/YdVGAtxbGxG8g9qzgog1Utk3gLS6QVr76oAjgEaNx5MjTnRPiR
5HvEqWG+hu64d1O2xfK3nxanunlXjMoKZ35kDHT10sAhTjGDeBa9krz1A+WRucxH
KmgMl8cNZXZps4CKn8iNMPmpbAdV1DLcMjyLI0piQjwnvv7iNcIOv0EXIFjHc3fu
q44XRMl+lFBqLtheG5B2peZq+FFXVzhavuRLAEDPYMKKPIIuD7++vY9Xrb0qKwiY
GI+RSIzLyaJopHSLrX0ZLR6MGp4u3D47EwdN6Epir94lLW7lqeyLzXyQN9YWCCiD
f6+aa3CfO6892ToPFstliD1+3o2ghaTG2ypbfK+OaVWwaUzI2TXgFM08b3SPpQaS
xgsanJhVHBSVZgyTU++hSZJcmIFPm0creEdB2HFm/ef0nABoYqRMADOToZSOaIUU
eKy0bLGTkBo5F1lV9s8Bz9GsDihORiwMucJ2Q0k8i6kE26oudMm4dOD8m+Q9Q+i1
cuC2oujD6qGbQx0uagAiNE8OHsvTZ1N7LpB7Z8ipz6k5D5xELsieFzGkj+ZBUh7B
ilRpBO0DLie7e90HICpAuOckpcgU+2Ek/WgFzNl8eEtAl44VOQKCAQEA5HNF8Pni
rjadcOcYXzdVRQp44sjErbUjTiqzsoZrcDYExZQPKPjOIzACjiUzJT6uiuTJcaye
2Y4j4S11M9kKY4E41bmXsIBfioNLd5GfSCYm3qdMTy5hOSOUCLxSA2rQ+QGYMGGc
1EJwGnx5dxS8G1K9tyIC1aSiYJivvGstNEWy+2SIWWfHIg9s6edEKhLo+lMVb0aZ
JLrtE0XWKS9u5meZdKpib20WfNI2XCKH7bs8wKagHEvOv1xky1R0+5kJ61bUhuto
3kdUtd6RmcXRlAhxRUGta9PszKVBaUgMsKIaWG9ArgrNs3pwg6mAy+eo33DdREN6
AcFODJG+TVEGZwKCAQEA0fU8MbiQ1HtAYju36WPQYP4b8GT1RmaSsiRJRwexHD/q
HbFZ43CVWni03G4YU4fNjzUUg3gfJgV+ho6fdMJvTneCy0jJEQnjlo/RN4N9nbgL
fyrxhtsvfKus9NyxA3YzaPNc8b87f7c4kXgKCKJfiUyWGye0jBkK/1U1Se0R4eko
816ZktIPWjrJGItlsrv/XnhQVDjhvAIjvVnVfIvEtpbie1zOxTFtRm+DkZwkRBmn
2+0SjWjy3Wmi5GVne8LslS9nRk5uOw2//nXB/1rqWGNaiwW5PPeprv416OVxQGY4
MKbEp3H2CeTPW9MWaX4zCfI0d1TANXdGGNDjJ0w+zQKCAQAWRyN/725qnMrXssGO
r3+yPuYw/3Emm2549fMWKsOZarsmZVzhGvpHNixZnkkRjb5Jryvx2OqYvqLDF8LB
Lp4zu+tF9FZVkP9AK7OVUm8dAxNxTRFE+3nhr5WFEJBS4vzD+6LSvQIlNOFnw9G4
ih4Z8ASuMjPij66CCwHANxdubImauGAPj+48ZRKy8KmWDMgJeUay1kii61OpOHs9
AvImp92wIdl9kj7KJ1nDvF5TEZpIEzez6rSbtq/hoDfUPUVZXNtW2OgeHQimv7Cl
NURN0lDxdwkpZb5G6qqpUKxkITq+ewLstgL/SKZmQIZd3BZ8y63YNVhViXtKAIB1
W7xrAoIBACZi3cSBaAlRF25eWLuVAi4Zh2YmLynV6xAizjrsTxdJIDaZjEOm+8d0
tixu/IeZFp4OKFf5CEjdTEqxnLmuKsd/1ivwfMJeunt2g9vQcJD7ay6u3UW4oS/7
+z0xdDOb0r5ncCDKw8gP6Ym/FqaJeUoS9Ia2da3/jiEspPeFHnXdOR11uEI9oYzv
noCcS9WnxYKyII2lcK/8/CfMWtnQfACeCX5gc3VeGgp8rONcVs0Um7n72B5+h5iv
jZaQa6EW/EYySoFyKXkmhGmzgep75siOMamlPD7HUQCrfZO9Dz2k8rUCaIXssf0R
FlBFgZ178zzoSWtf1VxxGCHFXyJ6MCkCggEAPbaIna3rDPMQbw34Zhd1LrUvtiRG
G1kARdR/LTA+j8wXSI4ONlhHT50g3sxyIW1UcPTr4tlJRECQ97XaY5HPP+FovWVV
joOnFgVn9hklo8EdUbc2jFFtPZOwqikfocxf8H4XfR58Ak7/lKzwegwM7MciF5r7
19WVi/QQZa2gRTFrn78eElUnAu33AlW2SHELwg4Lh2zbJK2wYIfT3c7GB4qhFLch
+MF7GOzLmkBYb23IE+S7ddciH0OnAOLMHxxFUx+DdQ2hPobj/Y0oqUdEl5wqLvZ+
4TUdzCay4b1hBoXKD1dVv1VUtt1XGB8AZYOdVkbInDDWJuuJLgY93Qaeqg==
-----END RSA PRIVATE KEY-----
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDR4SqyWdolxuGp
NqVUxBM1Pz+zr3nustLDvxxlJVAQhI0zDJLhDm1hgnNdcSarKnysExYACt9YZo87
yyAqzqsL1Rxi6tWvUZvG2xV1FFEj9+C23au1ZBh2QEjsPP2+ORxQXW1bDJ1bM+0q
6fMcar1c1Zjw28nh6rl2c7WgXXDI9Z7ltspREmkDeA5e4K0Rj5M6/77w8re909Ce
QTWkVGdN8E/muZq2dF4WVt/EJQLByepflrT4F4NqfDgZKbibQwIVcZYRvTs/7wlR
1Q/7atswhnBRvjYEHP9QoF6DjXS+DCE8hLJB61bhcafdssmT3fcYU7qC85T0JV3M
93FE88s9AgMBAAECggEBAJ4N3aOWeuv8DDDsUDG3v2dYDFb7DO4viP2UzIN3SAZI
bDVHgM0vmjvWi56Q+bfpNR44+VZq2auvAKrc24qhPLqacNglVWrEIHXuwwf6luhp
YbcU1JyzlqBm5QPTIpvAUoxK73Gj3FS0lCS/Li/2SkjBKn4oZq5Vn/mORfdQpwEO
aY5YzNNCzCwbqL/XcwvN1XRpF9puhBb+o2TtfGPLBf5LiScJWxrtPM7q+dFG/Ef7
f9L28iX+W6e3lD0/d93XEtlh1IsaZTWkl4m01Etg0f2FFtcja38eG+Yys4YNSpPe
VUWtFL2eDX+u5wv7nbaG9GsZmithXL6xa0PdwOd0KAECgYEA/D7K2KcxS+HeBgcG
V3b0m6QH1bRbqRxzK22TKnCVLAlRW7+5bfK56sHqMrOnDp/PuyAdq93roeJBNnAg
DrUt9i/lFTZ2kSBTJIlKW7cHv2Hocw3sQSDY/PhAXtALULxa8NHadPGsntHQDLzq
TVUNhOU6+GqPcqTp8BeSy+XrGD0CgYEA1QDvbxh/5tQxCFXcKn5KQCpJhbCfKBSW
drd8YF3Gdpxt3d2vTFTOsQ3xyAvEhSV0Jy9WUXobmx+KK8OjZqsR4PoYpYN3IWIW
N9jBKE9bahPRFF3g3FxaeVrCBkZBVK+ZAv/E5gshCyCXOmgHT+Kgi1Xpb4X0J/Pj
T83O7y/DrwECgYEAlagfq4kOy2m307WpQo12NT2N1HIMi/owLoz1apGfP6BLn/bR
anU/bxQcAnbqX9fbZLd6QzUIj2Rwsn611mQSX+Ussng8wn/+NEgsUNv4AclyI4kY
Kc/2TahwcJBYlk8aWZT4WrfkZdov6M7Q6I7U1Ys3gJmggDFuBp/F3amM4TUCgYBv
IofKvUuukjPrF3FCMpZnGCk4Ockwzl0bH+0jEoqERGYGhq35853MP2xRZeJpXftW
npo49K8GptQetXrCDKvNaHAHm6pCnrzc8pkFgTI/DIvgRoMWNpF2E+Z8A5f+qYf6
SpzfMhtRhPWiobh+8820JWUgrRGQ7u1BLC62GYPOAQKBgG/rt2Xu42DJcQ/wm7wN
LB8zn54G/rKWq+mXQbenpr1lN4zBOGgZILrRb1HTcQ7a19056BHBmx3J52yQAnGH
cxuhOyppsH3WgUkUqYN4bzO1CJorX3cP9udBAcmN4BWLF7pLgT7zHrLkJg05sWnd
UHWm2y7vdXeTvdD381p0Ug0S
-----END PRIVATE KEY-----

BIN
docs/images/emailcert.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View file

@ -2,19 +2,22 @@
from sucks import *
class BumperVacBot(VacBot):
def __init__(self, server_address):
self.server_address = server_address
vacuum = { 'did':'none','class':'none' }
super().__init__('sucks', 'ecouser.net', '', '', vacuum, '')
vacuum = {"did": "none", "class": "none"}
super().__init__("sucks", "ecouser.net", "", "", vacuum, "")
def connect_and_wait_until_ready(self):
logging.info('connecting')
logging.info("connecting")
self.xmpp.connect(self.server_address)
self.xmpp.process()
self.xmpp.wait_until_ready()
logging.basicConfig(level=logging.DEBUG, format='%(levelname)-8s %(message)s')
server_address = ('xxx.xxx.xxx.xxx', 5223)
logging.basicConfig(level=logging.DEBUG, format="%(levelname)-8s %(message)s")
server_address = ("xxx.xxx.xxx.xxx", 5223)
# Initialize
vacbot = BumperVacBot(server_address)