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 Pipfile.lock
/bumper.sublime-project /bumper.sublime-project
/bumper.sublime-workspace /bumper.sublime-workspace
.vscode/

View file

@ -1,13 +1,11 @@
[[source]] [[source]]
url = "https://pypi.python.org/simple" url = "https://pypi.python.org/simple"
verify_ssl = true verify_ssl = true
name = "pypi" name = "pypi"
[packages] [packages]
hbmqtt = "*"
aiohttp = "*"
black = "*"
[dev-packages] [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 # Bumper
most wifi-enabled robots that use the Ecovacs Android app. 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 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.
for production use. Use at your own risk.*
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? ## Why?
For fun, mostly :) For fun, mostly :)
But seriously, there are a serveral reasons for But seriously, there are a several reasons for eliminating the central server:
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.
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 ## Requirements
- An Ecovacs wifi-enabled robot - An Ecovacs wifi-enabled robot
- A computer on your local network to run the Bumper server - A computer on your local network to run the Bumper server
- Python 3 and pipenv - Python 3 and pipenv
- A network router that has functionality for overriding DNS queries - A network router that has functionality for overriding DNS queries
- A client that can connect to Bumper and talk to the robot over the - A client that can connect to Bumper and talk to the robot over the Ecovacs protocol.
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.
I recomend [Sucks](https://github.com/wpietri/sucks). - [Sucks](https://github.com/wpietri/sucks) can also be used, which can act as a client and control the robots via command-line.
It can run on the same computer and requires only minimal modification
to work with Bumper.
## Usage ## 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 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.
(if you haven't done this already) **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 | Address | Description |
the machine that will run the Bumper server |--|--|
| `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` - Start Bumper with `pipenv run python bumper.py`
- Reboot your robot (remove and re-insert the battery pack, - Reboot your robot
then power it on) - **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 - If your configuration is correct, the robot will connect to Bumper within about 30 seconds. Bumper will output information about the connection status.
within about 30 seconds. Bumper will output informaiton about the
connection status.
- Configure your client - see below. - Configure your client - see below.
## Using with Sucks ## Using with Sucks
Instructions (verified to work with Sucks 0.8.3) Instructions (verified to work with Sucks 0.8.3)
- Download and install [Sucks](https://github.com/wpietri/sucks) - Download and install [Sucks](https://github.com/wpietri/sucks)
- See the [example script](examples/sucks.py) for how to connect - See the [example script](examples/sucks.py) for how to connect
## Using with the official Android/iOS App ## 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 ![Example of emailing CA cert](docs/images/emailcert.png)
phone needs to use your DNS server with custom settings, and the app
authenticates via Ecovacs central servers every time you start it.
- Configure your DNS server to point the domains msg-na.ecouser.net and - Import the cert as a CA, and trust it
msg-ww.ecouser.net to the machine running Bumpy. Note: Depending on - Instructions here are different for [iOS](#importing-the-ca-cert-on-ios) vs [Android](#importing-the-ca-cert-on-android)
country, your phone may be using a different domain. #### Importing the CA Cert on iOS
- Login to the app. It will authenticate and ask for a list of robots 1. Open the e-mail on your iOS device, and click the attached cert
from Ecovacs central servers.
- The app will now connect to Bumper and try to ping the robot. Bumper ![Example of email on iOS device](docs/images/ios_email_cert.png)
responds to this ping to tell the app that the robot is online.
- 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? ## How does it work?
**App/Authentication**
Ecovacs robots communicate over the XMPP (jabber) protocol. Messages 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.
are relayed by a central XMPP server.
Bumper exposes a simulated XMPP server that implements the nessecary **Robots**
functions for relaying messages between a robot and a client.
When the robot boots up it sends a HTTP request to lbo.ecovacs.net:8007 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.
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á!
----
### 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 ## 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), Bumper wouldn't exist without [Sucks](https://github.com/wpietri/sucks), an open source client for Ecovacs robots. Big thanks to @wpietri and contributors!
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 logging
import bumper import bumper
import sys, socket 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) def main():
xmpp_address = (socket.gethostbyname(socket.gethostname()), 5223) args = sys.argv
# start conf server (async) if len(args) > 0:
conf_server = bumper.ConfServer(conf_address, ssl=False, async=True) 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) if platform.system() == "Darwin": # If a Mac, use 0.0.0.0 for listening
xmpp_server = bumper.XMPPServer(xmpp_address) 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 #!/usr/bin/env python3
from .confserver import ConfServer from .confserver import ConfServer
from .mqttserver import MQTTServer
from .mqttserver import MQTTHelperBot
from .xmppserver import XMPPServer 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 #!/usr/bin/env python3
from http.server import HTTPServer
from http.server import BaseHTTPRequestHandler
from http import HTTPStatus
from threading import Thread 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): class aiohttp_filter(logging.Filter):
def do_POST(self): 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: try:
self.protocol_version = 'HTTP/1.1' if run_async:
content_length = int(self.headers['Content-Length']) confserverlog.debug("Starting ConfServer Thread: 1")
post_data = self.rfile.read(content_length) self.confthread = Thread(
logging.debug("Headers: " + str(self.headers)) name="ConfServer_{}_Thread".format(self.address[1]),
request_body = post_data.decode('utf-8') target=self.run_server,
logging.debug("Request: " + request_body) )
json_body = json.loads(request_body) self.confthread.setDaemon(True)
todo = json_body['todo'] self.confthread.start()
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))
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: else:
try: try:
self.server.run() self.run_server()
except KeyboardInterrupt: except KeyboardInterrupt:
self.disconnect() self.disconnect()
except Exception as e: 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): def disconnect(self):
logging.info('ConfServer: shutting down...') try:
self.server.disconnect() confserverlog.info("shutting down")
if(self.async): if self.run_async:
self.server.join() self.confthread.join()
logging.info('ConfServer: bye') 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 #!/usr/bin/env python3
from threading import Thread
import sys, socket, threading, re, time, logging, uuid, xml.etree.ElementTree as ET 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(): class XMPPServer:
server_id = 'bumper' server_id = "bumper"
bot_id = 'bumpy' bot_id = "bumpy"
client_id = None client_id = None
clients = [] clients = []
exit_flag = False exit_flag = False
def __init__(self, address): 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)
try: try:
# Initialize bot server self.socket.bind(self.address)
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.socket.listen(5)
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.socket.bind(address) xmppserverlog.debug(
self.socket.listen(1) "listening on {}:{}".format(self.address[0], self.address[1])
logging.info('XMPPServer: listening on {}:{}'.format(address[0], address[1])) )
while not self.exit_flag: while not self.exit_flag:
logging.info('XMPPServer: awaiting connection')
connection, client_address = self.socket.accept() connection, client_address = self.socket.accept()
# disconnect any clients with this ip # disconnect any clients with this ip
for client in self.clients: for client in self.clients:
if client.address == client_address[0]: 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() 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() client.start()
self.clients.append(client) 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: except Exception as e:
logging.error('XMPPServer: {}'.format(e)) xmppserverlog.exception("{}".format(e))
except KeyboardInterrupt: exit(1)
logging.debug('XMPPServer: Keyboard interrupt')
except KeyboardInterrupt as e:
xmppserverlog.exception("{}".format(e))
finally: finally:
connection.shutdown(socket.SHUT_RDWR)
connection.close()
self.disconnect() self.disconnect()
logging.info('XMPPServer: bye') xmppserverlog.info("disconnecting")
self.socket.close()
def disconnect(self): 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()
self.exit_flag = True
xmppserverlog.debug("shutting down")
except Exception as e:
xmppserverlog.exception("{}".format(e))
def remove_client_byip(self, ip):
for client in self.clients: for client in self.clients:
client.disconnect() if client.address == ip:
client.join() xmppserverlog.debug(
self.exit_flag = True "removing client from client list with ip {} and resource {}".format(
logging.info('XMPPServer: shutting down...') 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): class Client(threading.Thread):
@ -58,124 +176,598 @@ class Client(threading.Thread):
BOT = 1 BOT = 1
CONTROLLER = 2 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) threading.Thread.__init__(self)
self.id = thread_id self.id = thread_id
self.name = "XMPP_Client_{}".format(client_address[0])
self.type = self.UNKNOWN self.type = self.UNKNOWN
self.state = self.IDLE self.state = self.IDLE
self.connection = connection self.connection = connection
self.address = client_address[0] 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): def send(self, command):
logging.debug('to {}: {}'.format(self.address, command)) try:
self.connection.send(command.encode()) 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): except OSError as e:
logging.info('{} disconnecting'.format(self.address)) xmppserverlog.error("{}".format(e))
self.connection.close()
self._set_state('DISCONNECT') 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()
except Exception as e:
xmppserverlog.exception("{}".format(e))
def _tag_strip_uri(self, tag): def _tag_strip_uri(self, tag):
if tag[0] == '{': try:
uri, ignore, tag = tag[1:].partition('}') if tag[0] == "{":
return tag uri, ignore, tag = tag[1:].partition("}")
return tag
except Exception as e:
xmppserverlog.exception("{}".format(e))
def _set_state(self, state): def _set_state(self, state):
new_state = getattr(Client, state) try:
if self.state > new_state: new_state = getattr(Client, state)
raise Exception('{} illegal state change {}->{}'.format(self.address, self.state, new_state)) if self.state > new_state:
logging.info('{} state: {}'.format(self.address, state)) raise Exception(
self.state = new_state "{} 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): def _handle_ctl(self, xml, data):
ctl = xml[0][0] try:
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')
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')
if id_index > -1:
data = data[:id_index] + 'from="' + XMPPServer.client_id + '" ' + data[id_index:]
data = data.encode()
client.send(data.decode('utf-8'))
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
def _handle_ping(self, xml, data): if xml.get("type") == "set":
if(xml.get('to').find('@') == -1): if (
# Ping to server - respond data.decode("utf-8").find("com:sf") > -1
self.send('<iq type="result" id="{}" from="{}" />'.format(xml.get('id'), xml.get('to'))) and xml.get("to") == "rl.ecorobot.net"
else: ): # 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: for client in XMPPServer.clients:
if client.address != self.address and client.state == client.READY: if client.address != self.address and client.state == client.READY:
client.send(data.decode('utf-8')) if client.type == self.BOT:
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.encode()
client.send(data.decode("utf-8"))
except Exception as e:
xmppserverlog.exception("{}".format(e))
def _handle_ping(self, xml, data):
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")
)
)
else:
for client in XMPPServer.clients:
if client.address != self.address and client.state == client.READY:
client.send(data.decode("utf-8"))
except Exception as e:
xmppserverlog.exception("{}".format(e))
def _handle_result(self, data): def _handle_result(self, data):
# forward # forward
for client in XMPPServer.clients: try:
if client.address != self.address and client.state == client.READY: for client in XMPPServer.clients:
client.send(data.decode('utf-8')) if client.address != self.address and client.state == client.READY:
client.send(data.decode("utf-8"))
except Exception as e:
xmppserverlog.exception("{}".format(e))
def _handle_connect(self, data):
try:
if self.state == self.CONNECT:
# Client first connecting, send our features
if data.decode("utf-8").find("jabber:client") > -1:
# ack jabbr:client
# 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>'
)
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":
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)
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
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
)
)
else:
self.type = self.CONTROLLER
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:
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:
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:
xmppserverlog.exception("{}".format(e))
def run(self): def run(self):
try: # xmppserverlog.info('client connected - {}'.format(self.address))
logging.info('client connected: {}'.format(self.address)) self._set_state("CONNECT")
self._set_state('CONNECT')
while True: while not self.state == self.DISCONNECT and not self.connection._closed:
data = self.connection.recv(4096) data = b""
if data:
logging.debug('from {}: {}'.format(self.address, data.decode('utf-8'))) time.sleep(0.2)
try: if not self.connection._closed:
if self.state == self.CONNECT: try:
if data.decode('utf-8').find('jabber:client') > -1: data = self.connection.recv(4096)
self._set_state('INIT')
# ack jabbr:client except ConnectionResetError as e:
self.send('<stream:stream xmlns:stream="http://etherx.jabber.org/streams" xmlns="jabber:client" version="1.0" id="1" from="{}">'.format(XMPPServer.server_id)) xmppserverlog.error("{}".format(e))
time.sleep(0.5) except OSError as e:
# session xmppserverlog.error("{}".format(e))
self.send('<stream:features><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"/><session xmlns="urn:ietf:params:xml:ns:xmpp-session"/></stream:features>') except Exception as e:
continue xmppserverlog.exception("{}".format(e))
xml = ET.fromstring(data)
if len(xml): if data != b"":
child = self._tag_strip_uri(xml[0].tag) self._parse_data(data)
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:
self.send(res)
elif xml.tag == 'presence':
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))
# 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.type = self.CONTROLLER
logging.info('{} type set to CONTROLLER (based on presence tag)'.format(self.address))
except ET.ParseError as e:
logging.debug('error: {}'.format(e))
except Exception as e:
logging.error('XMPPServer: {}'.format(e))
self._set_state('DISCONNECT')
except Exception as e:
logging.error('XMPPServer: {}'.format(e))
self._set_state('DISCONNECT')
finally:
self.disconnect()

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----- -----BEGIN CERTIFICATE-----
MIIFtTCCA52gAwIBAgIJAKLG5udzkR+zMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV MIIDkzCCAnugAwIBAgIJAJm9RhZ1mb21MA0GCSqGSIb3DQEBBQUAMFUxCzAJBgNV
BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX BAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMQ8wDQYDVQQKEwZCdW1wZXIxDzAN
aWRnaXRzIFB0eSBMdGQwHhcNMTcxMjA1MTkxNTQwWhcNMTgxMjA1MTkxNTQwWjBF BgNVBAsTBkJ1bXBlcjEPMA0GA1UEAxMGQnVtcGVyMB4XDTE5MDIwMTA2MTcxOFoX
MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50 DTI5MDEyOTA2MTcxOFowETEPMA0GA1UEAwwGQnVtcGVyMIIBIjANBgkqhkiG9w0B
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC AQEFAAOCAQ8AMIIBCgKCAQEA0eEqslnaJcbhqTalVMQTNT8/s6957rLSw78cZSVQ
CgKCAgEAu1z0Iw7KLJZD8CBrZWm9Jp7eqIAqdoTn3GWmNmzgIDnzmxZb70War74d EISNMwyS4Q5tYYJzXXEmqyp8rBMWAArfWGaPO8sgKs6rC9UcYurVr1GbxtsVdRRR
7aWDVtEVtOZQ8ShW15zRanr6hQkHdM5Li1KTMYW3P7TOmzJZkESNEZviOyvlfRPG I/fgtt2rtWQYdkBI7Dz9vjkcUF1tWwydWzPtKunzHGq9XNWY8NvJ4eq5dnO1oF1w
oLLbPIp+A7W8Zz3ST0cDzyy6N7LRAA0OywRgPhrxrN637Fx1+SFwqKQg0IUOuX3v yPWe5bbKURJpA3gOXuCtEY+TOv++8PK3vdPQnkE1pFRnTfBP5rmatnReFlbfxCUC
veZ1oAZSBUDzpDPUaak7jXugB11wt6XTEekwOcrH7WEDoFH87thtJyCM7IidW0bM wcnqX5a0+BeDanw4GSm4m0MCFXGWEb07P+8JUdUP+2rbMIZwUb42BBz/UKBeg410
rkqxeiM18qMzn8cGqnm2nY+VHgK0t0/QZ/cftPVc2IHZLr/+eTGaJmPv7gL+7v/l vgwhPISyQetW4XGn3bLJk933GFO6gvOU9CVdzPdxRPPLPQIDAQABo4GpMIGmMAkG
7fgk3RMTn0GrdTO+Vvnu1qNK7r+//KMgGOo6zwEQAI9Ml6y1LRuEXrymgsYY9qCE A1UdEwQCMAAwHQYDVR0OBBYEFCZ+TxSAQ57vZMJhboFoS2ryTrVAMB8GA1UdIwQY
ZtBV0FoZaVYAuGG4cvCDLLPgM2OivIcWgG+77MqMH/RkWg+BrUesn7ISEpF+7Q7+ MBaAFF45x3gxgzBRgtmIm3MLcu9EdHvVMCcGA1UdEQQgMB6CDSouZWNvdXNlci5u
jc6pFYr4y5fc2jJ9/c5Mxu6sv0Tc8N/c9WpcXsJ6Y1JrBbDcdGSwmTZ1JVyiog9O ZXSCDSouZWNvdmFjcy5jb20wDgYDVR0PAQH/BAQDAgWgMCAGA1UdJQEB/wQWMBQG
6bU/16QTuU3w2oH9qjnc907tZuB66P53WJ2iHPvB8yF5V93FvtbPYsHpO7noURSI CCsGAQUFBwMBBggrBgEFBQcDAjANBgkqhkiG9w0BAQUFAAOCAQEAiRFBGKwiDUoI
LqvKoNy31ndv4kao2AIzrJuhBBkSorBEcoFerRRBJMHa85R5Tb6iuGg6RFv0BB8K Me4Y1M+IApPw71P1huGyFg6Nh/npKXqqlU7XiZasHJoZjIRiQXq4jEYHj1auGDFS
gv6mG/7oqmR8QDnRRd54U2pEsIkYmgoIU4/8L91r9Vd1eiEDEnsCAwEAAaOBpzCB y2A/AVdNZdZfldq8wVM1+szGSar+NdMFFSy9m6zwNtdxvO0++azIhFhe8AICR5wP
pDAdBgNVHQ4EFgQUqA/GBsZAepFIcbgw6gK/HTUWolswdQYDVR0jBG4wbIAUqA/G 8/YGNU07aS+7gc2uYYR1l7HC5+F0GGG8wGsuTfZhDTZyPeLv7+XPehev1nzSpIjK
BsZAepFIcbgw6gK/HTUWoluhSaRHMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpT s04nfvPaljPvEhICWjFwgum25KlX16Sz9j/q+mNq5rcmc7vBh07iE0BluQQ8yZXX
b21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGSCCQCi jgdvna/SNu6qXeX5u8xHnM5izyguo1iLY6Ljw/SwaZiqhP+lwlKL6LRcLGfvY7Nc
xubnc5EfszAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4ICAQB7kHEHKNDQ QpVta9dbEQ==
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==
-----END CERTIFICATE----- -----END CERTIFICATE-----

View file

@ -1,51 +1,28 @@
-----BEGIN RSA PRIVATE KEY----- -----BEGIN PRIVATE KEY-----
MIIJJwIBAAKCAgEAu1z0Iw7KLJZD8CBrZWm9Jp7eqIAqdoTn3GWmNmzgIDnzmxZb MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDR4SqyWdolxuGp
70War74d7aWDVtEVtOZQ8ShW15zRanr6hQkHdM5Li1KTMYW3P7TOmzJZkESNEZvi NqVUxBM1Pz+zr3nustLDvxxlJVAQhI0zDJLhDm1hgnNdcSarKnysExYACt9YZo87
OyvlfRPGoLLbPIp+A7W8Zz3ST0cDzyy6N7LRAA0OywRgPhrxrN637Fx1+SFwqKQg yyAqzqsL1Rxi6tWvUZvG2xV1FFEj9+C23au1ZBh2QEjsPP2+ORxQXW1bDJ1bM+0q
0IUOuX3vveZ1oAZSBUDzpDPUaak7jXugB11wt6XTEekwOcrH7WEDoFH87thtJyCM 6fMcar1c1Zjw28nh6rl2c7WgXXDI9Z7ltspREmkDeA5e4K0Rj5M6/77w8re909Ce
7IidW0bMrkqxeiM18qMzn8cGqnm2nY+VHgK0t0/QZ/cftPVc2IHZLr/+eTGaJmPv QTWkVGdN8E/muZq2dF4WVt/EJQLByepflrT4F4NqfDgZKbibQwIVcZYRvTs/7wlR
7gL+7v/l7fgk3RMTn0GrdTO+Vvnu1qNK7r+//KMgGOo6zwEQAI9Ml6y1LRuEXrym 1Q/7atswhnBRvjYEHP9QoF6DjXS+DCE8hLJB61bhcafdssmT3fcYU7qC85T0JV3M
gsYY9qCEZtBV0FoZaVYAuGG4cvCDLLPgM2OivIcWgG+77MqMH/RkWg+BrUesn7IS 93FE88s9AgMBAAECggEBAJ4N3aOWeuv8DDDsUDG3v2dYDFb7DO4viP2UzIN3SAZI
EpF+7Q7+jc6pFYr4y5fc2jJ9/c5Mxu6sv0Tc8N/c9WpcXsJ6Y1JrBbDcdGSwmTZ1 bDVHgM0vmjvWi56Q+bfpNR44+VZq2auvAKrc24qhPLqacNglVWrEIHXuwwf6luhp
JVyiog9O6bU/16QTuU3w2oH9qjnc907tZuB66P53WJ2iHPvB8yF5V93FvtbPYsHp YbcU1JyzlqBm5QPTIpvAUoxK73Gj3FS0lCS/Li/2SkjBKn4oZq5Vn/mORfdQpwEO
O7noURSILqvKoNy31ndv4kao2AIzrJuhBBkSorBEcoFerRRBJMHa85R5Tb6iuGg6 aY5YzNNCzCwbqL/XcwvN1XRpF9puhBb+o2TtfGPLBf5LiScJWxrtPM7q+dFG/Ef7
RFv0BB8Kgv6mG/7oqmR8QDnRRd54U2pEsIkYmgoIU4/8L91r9Vd1eiEDEnsCAwEA f9L28iX+W6e3lD0/d93XEtlh1IsaZTWkl4m01Etg0f2FFtcja38eG+Yys4YNSpPe
AQKCAgBmUpJl3vrctMeviRT90mxhfhvda/lEtrqla8IFMRqJnvyCMwjMOHgNCEfx VUWtFL2eDX+u5wv7nbaG9GsZmithXL6xa0PdwOd0KAECgYEA/D7K2KcxS+HeBgcG
9BUxQYiYPbvwp/YdVGAtxbGxG8g9qzgog1Utk3gLS6QVr76oAjgEaNx5MjTnRPiR V3b0m6QH1bRbqRxzK22TKnCVLAlRW7+5bfK56sHqMrOnDp/PuyAdq93roeJBNnAg
5HvEqWG+hu64d1O2xfK3nxanunlXjMoKZ35kDHT10sAhTjGDeBa9krz1A+WRucxH DrUt9i/lFTZ2kSBTJIlKW7cHv2Hocw3sQSDY/PhAXtALULxa8NHadPGsntHQDLzq
KmgMl8cNZXZps4CKn8iNMPmpbAdV1DLcMjyLI0piQjwnvv7iNcIOv0EXIFjHc3fu TVUNhOU6+GqPcqTp8BeSy+XrGD0CgYEA1QDvbxh/5tQxCFXcKn5KQCpJhbCfKBSW
q44XRMl+lFBqLtheG5B2peZq+FFXVzhavuRLAEDPYMKKPIIuD7++vY9Xrb0qKwiY drd8YF3Gdpxt3d2vTFTOsQ3xyAvEhSV0Jy9WUXobmx+KK8OjZqsR4PoYpYN3IWIW
GI+RSIzLyaJopHSLrX0ZLR6MGp4u3D47EwdN6Epir94lLW7lqeyLzXyQN9YWCCiD N9jBKE9bahPRFF3g3FxaeVrCBkZBVK+ZAv/E5gshCyCXOmgHT+Kgi1Xpb4X0J/Pj
f6+aa3CfO6892ToPFstliD1+3o2ghaTG2ypbfK+OaVWwaUzI2TXgFM08b3SPpQaS T83O7y/DrwECgYEAlagfq4kOy2m307WpQo12NT2N1HIMi/owLoz1apGfP6BLn/bR
xgsanJhVHBSVZgyTU++hSZJcmIFPm0creEdB2HFm/ef0nABoYqRMADOToZSOaIUU anU/bxQcAnbqX9fbZLd6QzUIj2Rwsn611mQSX+Ussng8wn/+NEgsUNv4AclyI4kY
eKy0bLGTkBo5F1lV9s8Bz9GsDihORiwMucJ2Q0k8i6kE26oudMm4dOD8m+Q9Q+i1 Kc/2TahwcJBYlk8aWZT4WrfkZdov6M7Q6I7U1Ys3gJmggDFuBp/F3amM4TUCgYBv
cuC2oujD6qGbQx0uagAiNE8OHsvTZ1N7LpB7Z8ipz6k5D5xELsieFzGkj+ZBUh7B IofKvUuukjPrF3FCMpZnGCk4Ockwzl0bH+0jEoqERGYGhq35853MP2xRZeJpXftW
ilRpBO0DLie7e90HICpAuOckpcgU+2Ek/WgFzNl8eEtAl44VOQKCAQEA5HNF8Pni npo49K8GptQetXrCDKvNaHAHm6pCnrzc8pkFgTI/DIvgRoMWNpF2E+Z8A5f+qYf6
rjadcOcYXzdVRQp44sjErbUjTiqzsoZrcDYExZQPKPjOIzACjiUzJT6uiuTJcaye SpzfMhtRhPWiobh+8820JWUgrRGQ7u1BLC62GYPOAQKBgG/rt2Xu42DJcQ/wm7wN
2Y4j4S11M9kKY4E41bmXsIBfioNLd5GfSCYm3qdMTy5hOSOUCLxSA2rQ+QGYMGGc LB8zn54G/rKWq+mXQbenpr1lN4zBOGgZILrRb1HTcQ7a19056BHBmx3J52yQAnGH
1EJwGnx5dxS8G1K9tyIC1aSiYJivvGstNEWy+2SIWWfHIg9s6edEKhLo+lMVb0aZ cxuhOyppsH3WgUkUqYN4bzO1CJorX3cP9udBAcmN4BWLF7pLgT7zHrLkJg05sWnd
JLrtE0XWKS9u5meZdKpib20WfNI2XCKH7bs8wKagHEvOv1xky1R0+5kJ61bUhuto UHWm2y7vdXeTvdD381p0Ug0S
3kdUtd6RmcXRlAhxRUGta9PszKVBaUgMsKIaWG9ArgrNs3pwg6mAy+eo33DdREN6 -----END PRIVATE KEY-----
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-----

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 * from sucks import *
class BumperVacBot(VacBot): class BumperVacBot(VacBot):
def __init__(self, server_address): def __init__(self, server_address):
self.server_address = server_address self.server_address = server_address
vacuum = { 'did':'none','class':'none' } vacuum = {"did": "none", "class": "none"}
super().__init__('sucks', 'ecouser.net', '', '', vacuum, '') super().__init__("sucks", "ecouser.net", "", "", vacuum, "")
def connect_and_wait_until_ready(self): def connect_and_wait_until_ready(self):
logging.info('connecting') logging.info("connecting")
self.xmpp.connect(self.server_address) self.xmpp.connect(self.server_address)
self.xmpp.process() self.xmpp.process()
self.xmpp.wait_until_ready() 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 # Initialize
vacbot = BumperVacBot(server_address) vacbot = BumperVacBot(server_address)