~/paste/2765
~/paste/2765
~/paste/2765

  1. # Sonota - Flashing Sonoff devices running orig fw with custom fw via OTA
  2. # Copyright (C) 2017  Mirko Vogt <dev-sonota@nanl.de>
  3. #
  4. # This file is part of Sonota.
  5. #
  6. # Sonota is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation, either version 2 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # Sonota is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with Sonota.  If not, see <http://www.gnu.org/licenses/>.
  18.  
  19. # using tornado as it has also support for websockets
  20. import tornado.ioloop
  21. import tornado.web
  22. import tornado.httpserver
  23. import tornado.websocket
  24. import json
  25. from datetime import datetime
  26. from time import sleep, time
  27. from uuid import uuid4
  28. from hashlib import sha256
  29.  
  30. upgrade_file_user1 = "sonata_tasmota_user1.bin"
  31. upgrade_file_user2 = "sonata_tasmota_user2.bin"
  32. serving_host = "10.23.42.5"
  33.  
  34. class OTAUpdate(tornado.web.StaticFileHandler):
  35.     # Override tornado.web.StaticHandler to debug-print GET request
  36.     # FIXME: For some reason that fails horribly!
  37.     #   What did I do wrong trying to override and eventually calling the
  38.     #   original get method?request!
  39.     def get(self, path, include_body=True):
  40.         print("~~ HTTP GET")
  41.         print(">> %s" % path)
  42.         super(OTAUpdate, self).get(path, include_body)
  43.  
  44. class DispatchDevice(tornado.web.RequestHandler):
  45.     def post(self):
  46.         # telling device where to connect to in order to establish a WebSocket
  47.         #   channel
  48.         # as the initial request goes to port 443 anyway, we will just continue
  49.         #   on this port
  50.         print("~~ HTTP POST")
  51.         data = {
  52.             "error":    0,
  53.             "reason":   "ok",
  54.             "IP":       serving_host,
  55.             "port":     443
  56.         }
  57.         print(">> %s" % json.dumps(data, indent=4))
  58.         self.write(data)
  59.         self.finish()
  60.  
  61. class WebSocketHandler(tornado.websocket.WebSocketHandler):
  62.     def open(self, *args):
  63.         print("~~ WEBSOCKET OPEN")
  64.         # the device expects the server to generate and consistently provide
  65.         #   an API key which equals the UUID format
  66.         # it *must not* be the same apikey which the device uses in its requests
  67.         self.uuid = str(uuid4())
  68.         self.setup_completed = False
  69.         self.test = False
  70.         self.upgrade = False
  71.         self.stream.set_nodelay(True)
  72.  
  73.     def on_message(self, message):
  74.         print("~~ WEBSOCKET INPUT")
  75.         dct = json.loads(message)
  76.         print("<< %s" % json.dumps(dct, indent=4))
  77.         if dct.has_key("action"):
  78.             print("~~~ device sent action request, " \
  79.                     "acknowledging / answering...")
  80.             if dct['action'] == "register":
  81.                 print("~~~~ register")
  82.                 data = {
  83.                     "error":    0,
  84.                     "deviceid": dct['deviceid'],
  85.                     "apikey":   self.uuid,
  86.                     "config":   {
  87.                         "hb":           1,
  88.                         "hbInterval":   145
  89.                     }
  90.                 }
  91.                 print(">> %s" % json.dumps(data, indent=4))
  92.                 self.write_message(data)
  93.             if dct['action'] == "date":
  94.                 print("~~~~ date")
  95.                 data = {
  96.                     "error":    0,
  97.                     "deviceid": dct['deviceid'],
  98.                     "apikey":   self.uuid,
  99.                     "date":     datetime.isoformat(datetime.today())[:-3] + 'Z'
  100.                 }
  101.                 print(">> %s" % json.dumps(data, indent=4))
  102.                 self.write_message(data)
  103.             if dct['action'] == "query":
  104.                 print("~~~~ query")
  105.                 data = {
  106.                     "error":    0,
  107.                     "deviceid": dct['deviceid'],
  108.                     "apikey":   self.uuid,
  109.                     "params":0
  110.                 }
  111.                 print(">> %s" % json.dumps(data, indent=4))
  112.                 self.write_message(data)
  113.             if dct['action'] == "update":
  114.                 print("~~~~ update")
  115.                 data = {
  116.                     "error":    0,
  117.                     "deviceid": dct['deviceid'],
  118.                     "apikey":   self.uuid
  119.                 }
  120.                 print(">> %s" % json.dumps(data, indent=4))
  121.                 self.write_message(data)
  122.                 self.setup_completed = True
  123.         elif dct.has_key("sequence") and dct.has_key("error"):
  124.             print("~~~ device acknowledged our action request (seq %s) " \
  125.                     "with error code %d" % (dct['sequence'], dct['error']))
  126.         else:
  127.             print("### MOEP! Unknown request/answer from device!")
  128.  
  129.         if self.setup_completed and not self.test:
  130.             # switching relais on and off - for fun and profit!
  131.             data = {
  132.                 "action":       "update",
  133.                 "deviceid":     dct['deviceid'],
  134.                 "apikey":       self.uuid,
  135.                 "userAgent":    "app",
  136.                 "sequence":     str(int(time() * 1000)),
  137.                 "ts":           0,
  138.                 "params":       {
  139.                     "switch":       "off"
  140.                 },
  141.                 "from":         "hackepeter"
  142.             }
  143.             print(">> %s" % json.dumps(data, indent=4))
  144.             self.write_message(data)
  145.             data = {
  146.                 "action":       "update",
  147.                 "deviceid":     dct['deviceid'],
  148.                 "apikey":       self.uuid,
  149.                 "userAgent":    "app",
  150.                 "sequence":     str(int(time() * 1000)),
  151.                 "ts":           0,
  152.                 "params":       {
  153.                     "switch":       "on"
  154.                 },
  155.                 "from":         "hackepeter"
  156.             }
  157.             print(">> %s" % json.dumps(data, indent=4))
  158.             self.write_message(data)
  159.             data = {
  160.                 "action":       "update",
  161.                 "deviceid":     dct['deviceid'],
  162.                 "apikey":       self.uuid,
  163.                 "userAgent":    "app",
  164.                 "sequence":     str(int(time() * 1000)),
  165.                 "ts":           0,
  166.                 "params":       {
  167.                     "switch":       "off"
  168.                 },
  169.                 "from":         "hackepeter"
  170.             }
  171.             print(">> %s" % json.dumps(data, indent=4))
  172.             self.write_message(data)
  173.             data = {
  174.                 "action":       "update",
  175.                 "deviceid":     dct['deviceid'],
  176.                 "apikey":       self.uuid,
  177.                 "userAgent":    "app",
  178.                 "sequence":     str(int(time() * 1000)),
  179.                 "ts":           0,
  180.                 "params":       {
  181.                     "switch":       "on"
  182.                 },
  183.                 "from":         "hackepeter"
  184.             }
  185.  
  186.             print(">> %s" % json.dumps(data, indent=4))
  187.             self.write_message(data)
  188.             data = {
  189.                 "action":       "update",
  190.                 "deviceid":     dct['deviceid'],
  191.                 "apikey":       self.uuid,
  192.                 "userAgent":    "app",
  193.                 "sequence":     str(int(time() * 1000)),
  194.                 "ts":           0,
  195.                 "params":       {
  196.                     "switch":       "off"
  197.                 },
  198.                 "from":         "hackepeter"
  199.             }
  200.             print(">> %s" % json.dumps(data, indent=4))
  201.             self.write_message(data)
  202.             self.test = True
  203.  
  204.         if self.setup_completed and self.test and not self.upgrade:
  205.             fd = open("static/%s" % upgrade_file_user1, "r")
  206.             hash_user1 = sha256(fd.read()).hexdigest()
  207.             fd.close()
  208.             fd = open("static/%s" % upgrade_file_user2, "r")
  209.             hash_user2 = sha256(fd.read()).hexdigest()
  210.             fd.close()
  211.             data = {
  212.                 "action":       "upgrade",
  213.                 "deviceid":     dct['deviceid'],
  214.                 "apikey":       self.uuid,
  215.                 "userAgent":    "app",
  216.                 "sequence":     str(int(time() * 1000)),
  217.                 "ts":           0,
  218.                 "params":       {
  219.                     # the device expects two available images, as the original
  220.                     #   firmware splits the flash into two halfs and flashes
  221.                     #   the inactive partition (ping-pong).
  222.                     # as we don't know which partition is (in)active, we
  223.                     # provide our custom image as user1 as well as user2.
  224.                     # unfortunately this also means that our firmware image
  225.                     # must noch exceed FLASH_SIZE / 2 - (bootloader - spiffs)
  226.                     # TODO: make sure to always flash to user1 (off 0x1000)
  227.                     # TODO: check whether orig. bootloader can load and boot
  228.                     #   code bigger than FLASH_SIZE / 2 - (bootloader - spiffs)
  229.                     "binList":      [
  230.                         {
  231.                             "downloadUrl":  "http://%s:8080/ota/%s" %
  232.                                 (serving_host, upgrade_file_user1),
  233.                             # the device expects and checks the sha256 hash of
  234.                             #   the transmitted file
  235.                             "digest":       hash_user1,
  236.                             "name":         "user1.bin"
  237.                         },
  238.                         {
  239.                             "downloadUrl":  "http://%s:8080/ota/%s" %
  240.                                 (serving_host, upgrade_file_user2),
  241.                             # the device expects and checks the sha256 hash of
  242.                             #   the transmitted file
  243.                             "digest":       hash_user2,
  244.                             "name":         "user2.bin"
  245.                         }
  246.                     ],
  247.                     # if `model` is set to sth. else the websocket gets closed
  248.                     #   in the middle of the JSON transmission
  249.                     "model":        "ITA-GZ1-GL",
  250.                     # the `version` field doesn't seem to have any effect;
  251.                     #   nevertheless set it to a ridiculously high number
  252.                     #   to always be newer than the existing firmware
  253.                     "version":      "23.42.5"
  254.                 }
  255.             }
  256.             print(">> %s" % json.dumps(data, indent=4))
  257.             self.write_message(data)
  258.             self.upgrade = True
  259.  
  260.     def on_close(self):
  261.         print("~~ websocket close")
  262.  
  263. app = tornado.web.Application([
  264.     # handling initial dispatch SHTTP POST call to eu-disp.coolkit.cc
  265.     (r'/dispatch/device', DispatchDevice),
  266.     # handling actual payload communication on WebSockets
  267.     (r'/api/ws', WebSocketHandler),
  268.     # serving upgrade files via HTTP
  269.     # overriding get method of tornado.web.StaticFileHandler has some weird
  270.     #   side effects - as we don't necessarily need our adjustments use default
  271.     #(r'/ota/(.*)', OTAUpdate, {'path': "static/"})
  272.     (r'/ota/(.*)', tornado.web.StaticFileHandler, {'path': "static/"})
  273. ])
  274.  
  275. if __name__ == '__main__':
  276.     # listening on port 8080 for serving upgrade files
  277.     print("----- This script only works if host 'eu-disp.coolkit.cc' is being " \
  278.             "resolved to the machine this script is running on -----")
  279.     app.listen(8080)
  280.     app_ssl = tornado.httpserver.HTTPServer(app, ssl_options = {
  281.         "certfile": "ssl/server.crt",
  282.         "keyfile":  "ssl/server.key",
  283.     })
  284.     # listening on port 443 to catch initial POST request to eu-disp.coolkit.cc
  285.     app_ssl.listen(443)
  286.     tornado.ioloop.IOLoop.instance().start() 
Language: python
Posted by mirko at 27 May 2017, 07:32:03 UTC