├── .gitignore ├── LICENSE ├── README.md ├── boot_esp32.py ├── boot_wipy.py ├── ftp.py ├── maes.py ├── md5.py ├── md5hash.py ├── ntptime.py ├── timeutils.py └── urllib └── urequest.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ESP32-MicroPython 2 | Basic functions/libraries for ESP32/ESP8266 and WiPy2.0/3.0 running MicroPython. 3 | 4 | More info: [lemariva.com](https://lemariva.com/micropython) 5 | 6 | ftp.py 7 | ------------ 8 | Small FTP server 9 | 10 | #### Limitations 11 | * Passive mode only 12 | * A single data connection at a time 13 | * Data transfer is assumed to be in binary mode (ascii setting is ignored) 14 | * Operation blocks the thread 15 | * No authentication support 16 | 17 | #### Supports 18 | * Changing directories (cd/CWD) 19 | * Directory listing (ls/LIST with no parameters) 20 | * File retrievals (get/RETR) 21 | * File uploads (put/STOR) 22 | 23 | 24 | ntptime.py 25 | ------------- 26 | Get the epoch time from time1.google.com 27 | 28 | 29 | timeutils.py 30 | ------------ 31 | Implements `machine.RTC()` with `ntp_sync()`, `gmtime()` and `formatdate()` -RFC 2822 date format- as functions 32 | 33 | 34 | md5.py 35 | ------------ 36 | Encode a string using an MD5 algorithm. 37 | ``` 38 | >>> import md5 39 | >>> md5.digest('foo') 40 | 'acbd18db4cc2f85cedef654fccc4a4d8 41 | ``` 42 | 43 | md5hash.py 44 | ------------ 45 | Encode a string using an MD5 algorithm. 46 | ``` 47 | >>> from md5hash import md5 48 | >>> m = md5() 49 | >>> m.update('foo') 50 | >>> m.hexdigest() 51 | 'acbd18db4cc2f85cedef654fccc4a4d8' 52 | ``` 53 | 54 | maes.py 55 | ------------ 56 | Simple AES cipher implementation in pure Python following PEP-272 API 57 | 58 | ``` 59 | import maes 60 | import ubinascii 61 | 62 | class AESCipher(): 63 | def __init__(self, key): 64 | self.bs = 16 65 | self.key = key 66 | 67 | def encrypt(self, raw): 68 | raw = self._pad(raw) 69 | cipher = maes.new(self.key, maes.MODE_ECB) 70 | crypted_text = cipher.encrypt(raw) 71 | crypted_text_b64 = ubinascii.b2a_base64(crypted_text) 72 | return crypted_text_b64 73 | 74 | def decrypt(self, enc): 75 | enc = ubinascii.a2b_base64(enc) 76 | cipher = maes.new(self.key, maes.MODE_ECB) 77 | raw = cipher.decrypt(enc) 78 | return self._unpad(raw).decode('utf-8') 79 | 80 | def _pad(self, s): 81 | padnum = self.bs - len(s) % self.bs 82 | return s + padnum * chr(padnum).encode() 83 | 84 | @staticmethod 85 | def _unpad(dec): 86 | s = bytes(bytearray(dec)) 87 | return s[:-ord(s[len(s)-1:])] 88 | 89 | 90 | >>> key = b'0cc103aaf3df5dff' 91 | >>> cypher = AESCipher(key) 92 | >>> enc = cypher.encrypt(b'foo') 93 | >>> print(enc) 94 | b'S8IpdJ+PpVYutZB5sWXGkA==\n' 95 | >>> cypher.decrypt(enc) 96 | 'foo' 97 | ``` 98 | 99 | Changelog 100 | ------------ 101 | Revision 0.3 102 | 103 | 104 | License 105 | ----------- 106 | Check files 107 | -------------------------------------------------------------------------------- /boot_esp32.py: -------------------------------------------------------------------------------- 1 | #Copyright [2017] [Mauro Riva ] 2 | 3 | #Licensed under the Apache License, Version 2.0 (the "License"); 4 | #you may not use this file except in compliance with the License. 5 | #You may obtain a copy of the License at 6 | 7 | #http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | #Unless required by applicable law or agreed to in writing, software 10 | #distributed under the License is distributed on an "AS IS" BASIS, 11 | #WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | #See the License for the specific language governing permissions and 13 | #limitations under the License. 14 | 15 | #The above copyright notice and this permission notice shall be 16 | #included in all copies or substantial portions of the Software. 17 | 18 | # boot.py -- run on boot-up 19 | 20 | # wlan access 21 | ssid_ = 22 | wp2_pass = 23 | 24 | ## ftp access 25 | #from ftp import ftpserver 26 | 27 | def do_connect(): 28 | import network 29 | sta_if = network.WLAN(network.STA_IF) 30 | if not sta_if.isconnected(): 31 | print('connecting to network...') 32 | sta_if.active(True) 33 | sta_if.connect(ssid_, wp2_pass) 34 | while not sta_if.isconnected(): 35 | pass 36 | print('network config:', sta_if.ifconfig()) 37 | 38 | do_connect() 39 | 40 | #ftp_server = ftpserver() 41 | #ftp_server.start_thread() 42 | -------------------------------------------------------------------------------- /boot_wipy.py: -------------------------------------------------------------------------------- 1 | #Copyright [2017] [Mauro Riva ] 2 | # 3 | #Licensed under the Apache License, Version 2.0 (the "License"); 4 | #you may not use this file except in compliance with the License. 5 | #You may obtain a copy of the License at 6 | # 7 | #http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | #Unless required by applicable law or agreed to in writing, software 10 | #distributed under the License is distributed on an "AS IS" BASIS, 11 | #WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | #See the License for the specific language governing permissions and 13 | #limitations under the License. 14 | # 15 | #The above copyright notice and this permission notice shall be 16 | #included in all copies or substantial portions of the Software. 17 | 18 | # boot.py -- run on boot-up 19 | import os 20 | import pycom 21 | from machine import UART 22 | from network import WLAN 23 | 24 | uart = UART(0, 115200) 25 | os.dupterm(uart) 26 | 27 | pycom.heartbeat(False) 28 | 29 | # wlan access 30 | ssid_ = 31 | wp2_pass = 32 | 33 | # configure the WLAN subsystem in station mode (the default is AP) 34 | wlan = WLAN(mode=WLAN.STA) 35 | 36 | wlan.scan() # scan for available networks 37 | wlan.connect(ssid=ssid_, auth=(WLAN.WPA2, wp2_pass)) 38 | 39 | while not wlan.isconnected(): 40 | pycom.rgbled(0xFF0000) 41 | pass 42 | 43 | pycom.rgbled(0x050505) 44 | -------------------------------------------------------------------------------- /ftp.py: -------------------------------------------------------------------------------- 1 | """ 2 | Small ftp server for ESP8266 and ESP32 Micropython 3 | 4 | Based on the work of chrisgp - Christopher Popp and pfalcon - Paul Sokolovsky 5 | 6 | The server accepts passive mode only. 7 | It runs in a separated thread 8 | 9 | Start the server with: 10 | from ftp import ftpserver 11 | u = ftpserver() 12 | u.start_thread() 13 | 14 | Distributed under MIT License 15 | Copyright (c) 2016 Christopher Popp (initial ftp server framework) 16 | Copyright (c) 2016 Robert Hammelrath (putting the pieces together and a few extensions) 17 | Copyright (c) 2017 Mauro Riva (lemariva.com) (ftp class, dbg option and threading (only ESP32)) 18 | Copyright (c) 2018 Gautier HUSSON (liberasys.com) (reentrancy) 19 | """ 20 | 21 | import socket 22 | import network 23 | import uos 24 | from utime import localtime 25 | import gc 26 | 27 | try: 28 | import _thread 29 | thread_available = True 30 | except: 31 | thread_available = False 32 | 33 | 34 | DEBUG = False 35 | #DEBUG = True 36 | 37 | def dbg(msg, msg_=""): 38 | global DEBUG 39 | if DEBUG: 40 | print (str(msg) + str(msg_)) 41 | 42 | 43 | class ftpserver(): 44 | def send_list_data(self, path, dataclient, full): 45 | try: 46 | # whether path is a directory name 47 | for fname in sorted(uos.listdir(path), key = str.lower): 48 | dataclient.sendall(self.make_description(path, fname, full)) 49 | except: 50 | # path may be a file name or pattern 51 | pattern = path.split("/")[-1] 52 | path = path[:-(len(pattern) + 1)] 53 | if path == "": path = "/" 54 | for fname in sorted(uos.listdir(path), key = str.lower): 55 | if fncmp(fname, pattern): 56 | dataclient.sendall(self.make_description(path, fname, full)) 57 | 58 | def make_description(self, path, fname, full): 59 | if full: 60 | stat = uos.stat(self.get_absolute_path(path,fname)) 61 | file_permissions = "drwxr-xr-x" if (stat[0] & 0o170000 == 0o040000) else "-rw-r--r--" 62 | file_size = stat[6] 63 | description = "{} 1 owner group {:>10} Jan 1 2000 {}\r\n".format(file_permissions, file_size, fname) 64 | else: 65 | description = fname + "\r\n" 66 | return description 67 | 68 | def send_file_data(self, path, dataclient): 69 | with open(path, "r") as file: 70 | chunk = file.read(512) 71 | while len(chunk) > 0: 72 | dataclient.sendall(chunk) 73 | chunk = file.read(512) 74 | 75 | def save_file_data(self, path, dataclient): 76 | with open(path, "w") as file: 77 | chunk = dataclient.recv(512) 78 | while len(chunk) > 0: 79 | file.write(chunk) 80 | chunk = dataclient.recv(512) 81 | 82 | def get_absolute_path(self, cwd, payload): 83 | # Just a few special cases "..", "." and "" 84 | # If payload start's with /, set cwd to / 85 | # and consider the remainder a relative path 86 | if payload.startswith('/'): 87 | cwd = "/" 88 | for token in payload.split("/"): 89 | if token == '..': 90 | if cwd != '/': 91 | cwd = '/'.join(cwd.split('/')[:-1]) 92 | if cwd == '': 93 | cwd = '/' 94 | elif token != '.' and token != '': 95 | if cwd == '/': 96 | cwd += token 97 | else: 98 | cwd = cwd + '/' + token 99 | return cwd 100 | 101 | # compare fname against pattern. Pattern may contain 102 | # wildcards ? and *. 103 | def fncmp(fname, pattern): 104 | pi = 0 105 | si = 0 106 | while pi < len(pattern) and si < len(fname): 107 | if (fname[si] == pattern[pi]) or (pattern[pi] == '?'): 108 | si += 1 109 | pi += 1 110 | else: 111 | if pattern[pi] == '*': # recurse 112 | if (pi + 1) == len(pattern): 113 | return True 114 | while si < len(fname): 115 | if fncmp(fname[si:], pattern[pi+1:]): 116 | return True 117 | else: 118 | si += 1 119 | return False 120 | else: 121 | return False 122 | if pi == len(pattern.rstrip("*")) and si == len(fname): 123 | return True 124 | else: 125 | return False 126 | 127 | def thread_ftp(self, arg): 128 | self.start() 129 | dbg('Thread ftp ended') 130 | 131 | def start_thread(self): 132 | if thread_available: 133 | dbg("Starting ftp server on separated thread\n") 134 | _thread.start_new_thread(self.thread_ftp, ('',)) 135 | else: 136 | self.start() 137 | 138 | def start(self): 139 | DATA_PORT = 13333 140 | 141 | self.ftpsocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 142 | self.datasocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 143 | 144 | self.ftpsocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 145 | self.datasocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 146 | 147 | self.ftpsocket.bind(socket.getaddrinfo("0.0.0.0", 21)[0][4]) 148 | self.datasocket.bind(socket.getaddrinfo("0.0.0.0", DATA_PORT)[0][4]) 149 | 150 | self.ftpsocket.listen(1) 151 | self.ftpsocket.settimeout(None) 152 | self.datasocket.listen(1) 153 | self.datasocket.settimeout(None) 154 | 155 | msg_250_OK = '250 OK\r\n' 156 | msg_550_fail = '550 Failed\r\n' 157 | self.wlan = network.WLAN(network.AP_IF) 158 | if self.wlan.active(): 159 | ifconfig = self.wlan.ifconfig() 160 | else: 161 | self.wlan = network.WLAN(network.STA_IF) 162 | if self.wlan.active(): 163 | ifconfig = self.wlan.ifconfig() 164 | else: 165 | dbg("No active connection") 166 | return 167 | addr = ifconfig[0] 168 | print("FTP Server started on ", addr) 169 | 170 | try: 171 | while True: 172 | dataclient = None 173 | fromname = None 174 | do_run = True 175 | while do_run: 176 | cl, remote_addr = self.ftpsocket.accept() 177 | cl.settimeout(300) 178 | cwd = '/' 179 | try: 180 | dbg("FTP connection from:", remote_addr) 181 | cl.sendall("220 Hello, this is the " + uos.uname()[4] + ".\r\n") 182 | while True: 183 | data = cl.readline().decode("utf-8").rstrip("\r\n") 184 | if len(data) <= 0: 185 | dbg("Client disappeared") 186 | do_run = False 187 | break 188 | 189 | command = data.split(" ")[0].upper() 190 | payload = data[len(command):].lstrip() 191 | 192 | path = self.get_absolute_path(cwd, payload) 193 | 194 | dbg("Command={}, Payload={}".format(command, payload)) 195 | 196 | if command == "USER": 197 | cl.sendall("230 Logged in.\r\n") 198 | elif command == "SYST": 199 | cl.sendall("215 UNIX Type: L8\r\n") 200 | elif command == "NOOP": 201 | cl.sendall("200 OK\r\n") 202 | elif command == "FEAT": 203 | cl.sendall("211 no-features\r\n") 204 | elif command == "PWD" or command == "XPWD": 205 | cl.sendall('257 "{}"\r\n'.format(cwd)) 206 | elif command == "CWD": 207 | try: 208 | files = uos.listdir(path) 209 | cwd = path 210 | cl.sendall(msg_250_OK) 211 | except: 212 | cl.sendall(msg_550_fail) 213 | elif command == "CDUP": 214 | cwd = self.get_absolute_path(cwd, "..") 215 | cl.sendall(msg_250_OK) 216 | elif command == "TYPE": 217 | # probably should switch between binary and not 218 | cl.sendall('200 Transfer mode set\r\n') 219 | elif command == "SIZE": 220 | try: 221 | size = uos.stat(path)[6] 222 | cl.sendall('213 {}\r\n'.format(size)) 223 | except: 224 | cl.sendall(msg_550_fail) 225 | elif command == "QUIT": 226 | cl.sendall('221 Bye.\r\n') 227 | do_run = False 228 | break 229 | elif command == "PASV": 230 | cl.sendall('227 Entering Passive Mode ({},{},{}).\r\n'.format( 231 | addr.replace('.',','), DATA_PORT>>8, DATA_PORT%256)) 232 | dataclient, data_addr = self.datasocket.accept() 233 | dbg("FTP Data connection from:", data_addr) 234 | elif command == "LIST" or command == "NLST": 235 | if not payload.startswith("-"): 236 | place = path 237 | else: 238 | place = cwd 239 | try: 240 | self.send_list_data(place, dataclient, command == "LIST" or payload == "-l") 241 | cl.sendall("150 Here comes the directory listing.\r\n") 242 | cl.sendall("226 Listed.\r\n") 243 | except: 244 | cl.sendall(msg_550_fail) 245 | if dataclient is not None: 246 | dataclient.close() 247 | dataclient = None 248 | elif command == "RETR": 249 | try: 250 | self.send_file_data(path, dataclient) 251 | cl.sendall("150 Opening data connection.\r\n") 252 | cl.sendall("226 Transfer complete.\r\n") 253 | except: 254 | cl.sendall(msg_550_fail) 255 | if dataclient is not None: 256 | dataclient.close() 257 | dataclient = None 258 | elif command == "STOR": 259 | try: 260 | cl.sendall("150 Ok to send data.\r\n") 261 | self.save_file_data(path, dataclient) 262 | cl.sendall("226 Transfer complete.\r\n") 263 | except: 264 | cl.sendall(msg_550_fail) 265 | if dataclient is not None: 266 | dataclient.close() 267 | dataclient = None 268 | elif command == "DELE": 269 | try: 270 | uos.remove(path) 271 | cl.sendall(msg_250_OK) 272 | except: 273 | cl.sendall(msg_550_fail) 274 | elif command == "RMD" or command == "XRMD": 275 | try: 276 | uos.rmdir(path) 277 | cl.sendall(msg_250_OK) 278 | except: 279 | cl.sendall(msg_550_fail) 280 | elif command == "MKD" or command == "XMKD": 281 | try: 282 | uos.mkdir(path) 283 | cl.sendall(msg_250_OK) 284 | except: 285 | cl.sendall(msg_550_fail) 286 | elif command == "RNFR": 287 | fromname = path 288 | cl.sendall("350 Rename from\r\n") 289 | elif command == "RNTO": 290 | if fromname is not None: 291 | try: 292 | uos.rename(fromname, path) 293 | cl.sendall(msg_250_OK) 294 | except: 295 | cl.sendall(msg_550_fail) 296 | else: 297 | cl.sendall(msg_550_fail) 298 | fromname = None 299 | elif command == "MDTM": 300 | try: 301 | tm=localtime(uos.stat(path)[8]) 302 | cl.sendall('213 {:04d}{:02d}{:02d}{:02d}{:02d}{:02d}\r\n'.format(*tm[0:6])) 303 | except: 304 | cl.sendall('550 Fail\r\n') 305 | elif command == "STAT": 306 | if payload == "": 307 | cl.sendall("211-Connected to ({})\r\n" 308 | " Data address ({})\r\n" 309 | "211 TYPE: Binary STRU: File MODE: Stream\r\n".format( 310 | remote_addr[0], addr)) 311 | else: 312 | cl.sendall("213-Directory listing:\r\n") 313 | self.send_list_data(path, cl, True) 314 | cl.sendall("213 Done.\r\n") 315 | else: 316 | cl.sendall("502 Unsupported command.\r\n") 317 | dbg("Unsupported command {} with payload {}".format(command, payload)) 318 | except Exception as err: 319 | dbg(err) 320 | finally: 321 | cl.close() 322 | cl = None 323 | if dataclient is not None: 324 | dataclient.close() 325 | 326 | finally: 327 | self.datasocket.close() 328 | self.ftpsocket.close() 329 | -------------------------------------------------------------------------------- /maes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """Simple AES cipher implementation in pure Python following PEP-272 API 5 | 6 | Based on: https://bitbucket.org/intgr/pyaes/ to compatible with PEP-8. 7 | 8 | The goal of this module is to be as fast as reasonable in Python while still 9 | being Pythonic and readable/understandable. It is licensed under the permissive 10 | MIT license. 11 | 12 | Hopefully the code is readable and commented enough that it can serve as an 13 | introduction to the AES cipher for Python coders. In fact, it should go along 14 | well with the Stick Figure Guide to AES: 15 | http://www.moserware.com/2009/09/stick-figure-guide-to-advanced.html 16 | 17 | Contrary to intuition, this implementation numbers the 4x4 matrices from top to 18 | bottom for efficiency reasons:: 19 | 20 | 0 4 8 12 21 | 1 5 9 13 22 | 2 6 10 14 23 | 3 7 11 15 24 | 25 | Effectively it's the transposition of what you'd expect. This actually makes 26 | the code simpler -- except the ShiftRows step, but hopefully the explanation 27 | there clears it up. 28 | 29 | """ 30 | 31 | #### 32 | # Copyright (c) 2010 Marti Raudsepp 33 | # 34 | # Permission is hereby granted, free of charge, to any person obtaining a copy 35 | # of this software and associated documentation files (the "Software"), to deal 36 | # in the Software without restriction, including without limitation the rights 37 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 38 | # copies of the Software, and to permit persons to whom the Software is 39 | # furnished to do so, subject to the following conditions: 40 | # 41 | # The above copyright notice and this permission notice shall be included in 42 | # all copies or substantial portions of the Software. 43 | # 44 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 45 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 46 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 47 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 48 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 49 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 50 | # THE SOFTWARE. 51 | #### 52 | 53 | import ubinascii 54 | from array import array 55 | 56 | # Globals mandated by PEP 272: 57 | # http://www.python.org/dev/peps/pep-0272/ 58 | MODE_ECB = 1 59 | MODE_CBC = 2 60 | #MODE_CTR = 6 61 | 62 | block_size = 16 63 | # variable length key: 16, 24 or 32 bytes 64 | key_size = None 65 | 66 | 67 | def new(key, mode, IV=None): 68 | if mode == MODE_ECB: 69 | return ECBMode(AES(key)) 70 | elif mode == MODE_CBC: 71 | if IV is None: 72 | raise ValueError("CBC mode needs an IV value!") 73 | return CBCMode(AES(key), IV) 74 | else: 75 | raise NotImplementedError 76 | 77 | 78 | #### AES cipher implementation 79 | class AES(object): 80 | block_size = 16 81 | 82 | def __init__(self, key): 83 | self.setkey(key) 84 | 85 | def setkey(self, key): 86 | """Sets the key and performs key expansion.""" 87 | 88 | self.key = key 89 | self.key_size = len(key) 90 | 91 | if self.key_size == 16: 92 | self.rounds = 10 93 | elif self.key_size == 24: 94 | self.rounds = 12 95 | elif self.key_size == 32: 96 | self.rounds = 14 97 | else: 98 | raise ValueError("Key length must be 16, 24 or 32 bytes") 99 | 100 | self.expand_key() 101 | 102 | def expand_key(self): 103 | """Performs AES key expansion on self.key and stores in self.exkey""" 104 | 105 | # The key schedule specifies how parts of the key are fed into the 106 | # cipher's round functions. "Key expansion" means performing this 107 | # schedule in advance. Almost all implementations do this. 108 | # 109 | # Here's a description of AES key schedule: 110 | # http://en.wikipedia.org/wiki/Rijndael_key_schedule 111 | 112 | # The expanded key starts with the actual key itself 113 | exkey = array('B', self.key) 114 | 115 | # extra key expansion steps 116 | if self.key_size == 16: 117 | extra_cnt = 0 118 | elif self.key_size == 24: 119 | extra_cnt = 2 120 | else: 121 | extra_cnt = 3 122 | 123 | # 4-byte temporary variable for key expansion 124 | word = exkey[-4:] 125 | # Each expansion cycle uses 'i' once for Rcon table lookup 126 | for i in range(1, 11): 127 | 128 | #### key schedule core: 129 | # left-rotate by 1 byte 130 | word = word[1:4] + word[0:1] 131 | 132 | # apply S-box to all bytes 133 | for j in range(4): 134 | word[j] = aes_sbox[word[j]] 135 | 136 | # apply the Rcon table to the leftmost byte 137 | word[0] ^= aes_Rcon[i] 138 | #### end key schedule core 139 | 140 | for z in range(4): 141 | for j in range(4): 142 | # mix in bytes from the last subkey 143 | word[j] ^= exkey[-self.key_size + j] 144 | exkey.extend(word) 145 | 146 | # Last key expansion cycle always finishes here 147 | if len(exkey) >= (self.rounds + 1) * self.block_size: 148 | break 149 | 150 | # Special substitution step for 256-bit key 151 | if self.key_size == 32: 152 | for j in range(4): 153 | # mix in bytes from the last subkey XORed with S-box of 154 | # current word bytes 155 | word[j] = aes_sbox[word[j]] ^ exkey[-self.key_size + j] 156 | exkey.extend(word) 157 | 158 | # Twice for 192-bit key, thrice for 256-bit key 159 | for z in range(extra_cnt): 160 | for j in range(4): 161 | # mix in bytes from the last subkey 162 | word[j] ^= exkey[-self.key_size + j] 163 | exkey.extend(word) 164 | 165 | self.exkey = exkey 166 | 167 | def add_round_key(self, block, round): 168 | """AddRoundKey step. This is where the key is mixed into plaintext""" 169 | 170 | offset = round * 16 171 | exkey = self.exkey 172 | 173 | for i in range(16): 174 | block[i] ^= exkey[offset + i] 175 | 176 | #print 'AddRoundKey:', block 177 | 178 | def sub_bytes(self, block, sbox): 179 | """ 180 | SubBytes step, apply S-box to all bytes 181 | 182 | Depending on whether encrypting or decrypting, a different sbox array 183 | is passed in. 184 | """ 185 | 186 | for i in range(16): 187 | block[i] = sbox[block[i]] 188 | 189 | #print 'SubBytes :', block 190 | 191 | def shift_rows(self, b): 192 | """ 193 | ShiftRows step in AES. 194 | 195 | Shifts 2nd row to left by 1, 3rd row by 2, 4th row by 3 196 | 197 | Since we're performing this on a transposed matrix, cells are numbered 198 | from top to bottom first:: 199 | 200 | 0 4 8 12 -> 0 4 8 12 -- 1st row doesn't change 201 | 1 5 9 13 -> 5 9 13 1 -- row shifted to left by 1 (wraps around) 202 | 2 6 10 14 -> 10 14 2 6 -- shifted by 2 203 | 3 7 11 15 -> 15 3 7 11 -- shifted by 3 204 | """ 205 | 206 | b[1], b[5], b[9], b[13] = b[5], b[9], b[13], b[1] 207 | b[2], b[6], b[10], b[14] = b[10], b[14], b[2], b[6] 208 | b[3], b[7], b[11], b[15] = b[15], b[3], b[7], b[11] 209 | 210 | #print 'ShiftRows :', b 211 | 212 | def shift_rows_inv(self, b): 213 | """ 214 | Similar to shift_rows above, but performed in inverse for decryption. 215 | """ 216 | b[5], b[9], b[13], b[1] = b[1], b[5], b[9], b[13] 217 | b[10], b[14], b[2], b[6] = b[2], b[6], b[10], b[14] 218 | b[15], b[3], b[7], b[11] = b[3], b[7], b[11], b[15] 219 | 220 | #print 'ShiftRows :', b 221 | 222 | def mix_columns(self, block): 223 | """MixColumns step. Mixes the values in each column""" 224 | 225 | # Cache global multiplication tables (see below) 226 | mul_by_2 = gf_mul_by_2 227 | mul_by_3 = gf_mul_by_3 228 | 229 | # Since we're dealing with a transposed matrix, columns are already 230 | # sequential 231 | for col in range(0, 16, 4): 232 | v0, v1, v2, v3 = block[col:col + 4] 233 | 234 | block[col] = mul_by_2[v0] ^ v3 ^ v2 ^ mul_by_3[v1] 235 | block[col + 1] = mul_by_2[v1] ^ v0 ^ v3 ^ mul_by_3[v2] 236 | block[col + 2] = mul_by_2[v2] ^ v1 ^ v0 ^ mul_by_3[v3] 237 | block[col + 3] = mul_by_2[v3] ^ v2 ^ v1 ^ mul_by_3[v0] 238 | 239 | #print 'MixColumns :', block 240 | 241 | def mix_columns_inv(self, block): 242 | """ 243 | Similar to mix_columns above, but performed in inverse for decryption. 244 | """ 245 | # Cache global multiplication tables (see below) 246 | mul_9 = gf_mul_by_9 247 | mul_11 = gf_mul_by_11 248 | mul_13 = gf_mul_by_13 249 | mul_14 = gf_mul_by_14 250 | 251 | # Since we're dealing with a transposed matrix, columns are already 252 | # sequential 253 | for col in range(0, 16, 4): 254 | v0, v1, v2, v3 = block[col:col + 4] 255 | 256 | block[col] = mul_14[v0] ^ mul_9[v3] ^ mul_13[v2] ^ mul_11[v1] 257 | block[col + 1] = mul_14[v1] ^ mul_9[v0] ^ mul_13[v3] ^ mul_11[v2] 258 | block[col + 2] = mul_14[v2] ^ mul_9[v1] ^ mul_13[v0] ^ mul_11[v3] 259 | block[col + 3] = mul_14[v3] ^ mul_9[v2] ^ mul_13[v1] ^ mul_11[v0] 260 | 261 | #print 'MixColumns :', block 262 | 263 | def encrypt_block(self, block): 264 | """Encrypts a single block. This is the main AES function""" 265 | 266 | # For efficiency reasons, the state between steps is transmitted via a 267 | # mutable array, not returned 268 | self.add_round_key(block, 0) 269 | 270 | for round in range(1, self.rounds): 271 | self.sub_bytes(block, aes_sbox) 272 | self.shift_rows(block) 273 | self.mix_columns(block) 274 | self.add_round_key(block, round) 275 | 276 | self.sub_bytes(block, aes_sbox) 277 | self.shift_rows(block) 278 | # no mix_columns step in the last round 279 | self.add_round_key(block, self.rounds) 280 | 281 | def decrypt_block(self, block): 282 | """Decrypts a single block. This is the main AES decryption function""" 283 | 284 | # For efficiency reasons, the state between steps is transmitted via a 285 | # mutable array, not returned 286 | self.add_round_key(block, self.rounds) 287 | 288 | # count rounds down from (self.rounds) ... 1 289 | for round in range(self.rounds - 1, 0, -1): 290 | self.shift_rows_inv(block) 291 | self.sub_bytes(block, aes_inv_sbox) 292 | self.add_round_key(block, round) 293 | self.mix_columns_inv(block) 294 | 295 | self.shift_rows_inv(block) 296 | self.sub_bytes(block, aes_inv_sbox) 297 | self.add_round_key(block, 0) 298 | # no mix_columns step in the last round 299 | 300 | 301 | #### ECB mode implementation 302 | 303 | class ECBMode(object): 304 | """Electronic CodeBook (ECB) mode encryption. 305 | 306 | Basically this mode applies the cipher function to each block individually; 307 | no feedback is done. NB! This is insecure for almost all purposes 308 | """ 309 | 310 | def __init__(self, cipher): 311 | self.cipher = cipher 312 | self.block_size = cipher.block_size 313 | 314 | def ecb(self, data, block_func): 315 | """Perform ECB mode with the given function""" 316 | 317 | if len(data) % self.block_size != 0: 318 | raise ValueError("Input length must be multiple of 16") 319 | 320 | block_size = self.block_size 321 | data = array('B', data) 322 | 323 | for offset in range(0, len(data), block_size): 324 | block = data[offset:offset + block_size] 325 | block_func(block) 326 | data[offset:offset + block_size] = block 327 | 328 | return data 329 | 330 | def encrypt(self, data): 331 | """Encrypt data in ECB mode""" 332 | 333 | return self.ecb(data, self.cipher.encrypt_block) 334 | 335 | def decrypt(self, data): 336 | """Decrypt data in ECB mode""" 337 | 338 | return self.ecb(data, self.cipher.decrypt_block) 339 | 340 | 341 | #### CBC mode 342 | class CBCMode(object): 343 | """ 344 | Cipher Block Chaining(CBC) mode encryption. This mode avoids content leaks. 345 | 346 | In CBC encryption, each plaintext block is XORed with the ciphertext block 347 | preceding it; decryption is simply the inverse. 348 | """ 349 | 350 | # A better explanation of CBC can be found here: 351 | # http://en.wikipedia.org/wiki/Block_cipher_modes_of_operation#- 352 | # Cipher-block_chaining_.28CBC.29 353 | 354 | def __init__(self, cipher, IV): 355 | self.cipher = cipher 356 | self.block_size = cipher.block_size 357 | self.IV = array('B', IV) 358 | 359 | def encrypt(self, data): 360 | """Encrypt data in CBC mode""" 361 | 362 | block_size = self.block_size 363 | if len(data) % block_size != 0: 364 | raise ValueError("Plaintext length must be multiple of 16") 365 | 366 | data = array('B', data) 367 | IV = self.IV 368 | 369 | for offset in range(0, len(data), block_size): 370 | block = data[offset:offset + block_size] 371 | 372 | # Perform CBC chaining 373 | for i in range(block_size): 374 | block[i] ^= IV[i] 375 | 376 | self.cipher.encrypt_block(block) 377 | data[offset:offset + block_size] = block 378 | IV = block 379 | 380 | self.IV = IV 381 | return data 382 | 383 | def decrypt(self, data): 384 | """Decrypt data in CBC mode""" 385 | 386 | block_size = self.block_size 387 | if len(data) % block_size != 0: 388 | raise ValueError("Ciphertext length must be multiple of 16") 389 | 390 | data = array('B', data) 391 | IV = self.IV 392 | 393 | for offset in range(0, len(data), block_size): 394 | ctext = data[offset:offset + block_size] 395 | block = ctext[:] 396 | self.cipher.decrypt_block(block) 397 | 398 | # Perform CBC chaining 399 | #for i in range(block_size): 400 | # data[offset + i] ^= IV[i] 401 | for i in range(block_size): 402 | block[i] ^= IV[i] 403 | data[offset:offset + block_size] = block 404 | 405 | IV = ctext 406 | #data[offset : offset+block_size] = block 407 | 408 | self.IV = IV 409 | return data 410 | 411 | 412 | def galois_multiply(a, b): 413 | """Galois Field multiplicaiton for AES""" 414 | p = 0 415 | while b: 416 | if b & 1: 417 | p ^= a 418 | a <<= 1 419 | if a & 0x100: 420 | a ^= 0x1b 421 | b >>= 1 422 | 423 | return p & 0xff 424 | 425 | # Precompute the multiplication tables for encryption 426 | gf_mul_by_2 = array('B', [galois_multiply(x, 2) for x in range(256)]) 427 | gf_mul_by_3 = array('B', [galois_multiply(x, 3) for x in range(256)]) 428 | # ... for decryption 429 | gf_mul_by_9 = array('B', [galois_multiply(x, 9) for x in range(256)]) 430 | gf_mul_by_11 = array('B', [galois_multiply(x, 11) for x in range(256)]) 431 | gf_mul_by_13 = array('B', [galois_multiply(x, 13) for x in range(256)]) 432 | gf_mul_by_14 = array('B', [galois_multiply(x, 14) for x in range(256)]) 433 | 434 | #### 435 | 436 | # The S-box is a 256-element array, that maps a single byte value to another 437 | # byte value. Since it's designed to be reversible, each value occurs only once 438 | # in the S-box 439 | # 440 | # More information: http://en.wikipedia.org/wiki/Rijndael_S-box 441 | 442 | aes_sbox = array( 443 | 'B', 444 | ubinascii.unhexlify('637c777bf26b6fc53001672bfed7ab76' 445 | 'ca82c97dfa5947f0add4a2af9ca472c0' 446 | 'b7fd9326363ff7cc34a5e5f171d83115' 447 | '04c723c31896059a071280e2eb27b275' 448 | '09832c1a1b6e5aa0523bd6b329e32f84' 449 | '53d100ed20fcb15b6acbbe394a4c58cf' 450 | 'd0efaafb434d338545f9027f503c9fa8' 451 | '51a3408f929d38f5bcb6da2110fff3d2' 452 | 'cd0c13ec5f974417c4a77e3d645d1973' 453 | '60814fdc222a908846eeb814de5e0bdb' 454 | 'e0323a0a4906245cc2d3ac629195e479' 455 | 'e7c8376d8dd54ea96c56f4ea657aae08' 456 | 'ba78252e1ca6b4c6e8dd741f4bbd8b8a' 457 | '703eb5664803f60e613557b986c11d9e' 458 | 'e1f8981169d98e949b1e87e9ce5528df' 459 | '8ca1890dbfe6426841992d0fb054bb16') 460 | ) 461 | 462 | # This is the inverse of the above. In other words: 463 | # aes_inv_sbox[aes_sbox[val]] == val 464 | 465 | aes_inv_sbox = array( 466 | 'B', 467 | ubinascii.unhexlify('52096ad53036a538bf40a39e81f3d7fb' 468 | '7ce339829b2fff87348e4344c4dee9cb' 469 | '547b9432a6c2233dee4c950b42fac34e' 470 | '082ea16628d924b2765ba2496d8bd125' 471 | '72f8f66486689816d4a45ccc5d65b692' 472 | '6c704850fdedb9da5e154657a78d9d84' 473 | '90d8ab008cbcd30af7e45805b8b34506' 474 | 'd02c1e8fca3f0f02c1afbd0301138a6b' 475 | '3a9111414f67dcea97f2cfcef0b4e673' 476 | '96ac7422e7ad3585e2f937e81c75df6e' 477 | '47f11a711d29c5896fb7620eaa18be1b' 478 | 'fc563e4bc6d279209adbc0fe78cd5af4' 479 | '1fdda8338807c731b11210592780ec5f' 480 | '60517fa919b54a0d2de57a9f93c99cef' 481 | 'a0e03b4dae2af5b0c8ebbb3c83539961' 482 | '172b047eba77d626e169146355210c7d') 483 | ) 484 | 485 | # The Rcon table is used in AES's key schedule (key expansion) 486 | # It's a pre-computed table of exponentation of 2 in AES's finite field 487 | # 488 | # More information: http://en.wikipedia.org/wiki/Rijndael_key_schedule 489 | 490 | # aes_Rcon = array( 491 | # 'B', 492 | # '8d01020408102040801b366cd8ab4d9a' 493 | # '2f5ebc63c697356ad4b37dfaefc59139' 494 | # '72e4d3bd61c29f254a943366cc831d3a' 495 | # '74e8cb8d01020408102040801b366cd8' 496 | # 'ab4d9a2f5ebc63c697356ad4b37dfaef' 497 | # 'c5913972e4d3bd61c29f254a943366cc' 498 | # '831d3a74e8cb8d01020408102040801b' 499 | # '366cd8ab4d9a2f5ebc63c697356ad4b3' 500 | # '7dfaefc5913972e4d3bd61c29f254a94' 501 | # '3366cc831d3a74e8cb8d010204081020' 502 | # '40801b366cd8ab4d9a2f5ebc63c69735' 503 | # '6ad4b37dfaefc5913972e4d3bd61c29f' 504 | # '254a943366cc831d3a74e8cb8d010204' 505 | # '08102040801b366cd8ab4d9a2f5ebc63' 506 | # 'c697356ad4b37dfaefc5913972e4d3bd' 507 | # '61c29f254a943366cc831d3a74e8cb'.decode('hex') 508 | # ) 509 | 510 | aes_Rcon = array( 511 | 'B', 512 | ubinascii.unhexlify('8d01020408102040801b366cd8ab4d9a' 513 | '2f5ebc63c697356ad4b37dfaefc59139' 514 | '72e4d3bd61c29f254a943366cc831d3a' 515 | '74e8cb8d01020408102040801b366cd8' 516 | 'ab4d9a2f5ebc63c697356ad4b37dfaef' 517 | 'c5913972e4d3bd61c29f254a943366cc' 518 | '831d3a74e8cb8d01020408102040801b' 519 | '366cd8ab4d9a2f5ebc63c697356ad4b3' 520 | '7dfaefc5913972e4d3bd61c29f254a94' 521 | '3366cc831d3a74e8cb8d010204081020' 522 | '40801b366cd8ab4d9a2f5ebc63c69735' 523 | '6ad4b37dfaefc5913972e4d3bd61c29f' 524 | '254a943366cc831d3a74e8cb8d010204' 525 | '08102040801b366cd8ab4d9a2f5ebc63' 526 | 'c697356ad4b37dfaefc5913972e4d3bd' 527 | '61c29f254a943366cc831d3a74e8cb') 528 | ) 529 | 530 | -------------------------------------------------------------------------------- /md5.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright [2018] [Mauro Riva ] 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | 16 | based on https://rosettacode.org/wiki/MD5/Implementation#Python 17 | adapted for MicroPython 18 | """ 19 | 20 | rotate_amounts = [7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 21 | 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 22 | 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 23 | 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21] 24 | 25 | #constants = [int(abs(math.sin(i+1)) * 2**32) & 0xFFFFFFFF for i in range(64)] # precision is not enough 26 | constants = [3614090360, 3905402710, 606105819, 3250441966, 4118548399, 1200080426, 2821735955, 4249261313, 27 | 1770035416, 2336552879, 4294925233, 2304563134, 1804603682, 4254626195, 2792965006, 1236535329, 28 | 4129170786, 3225465664, 643717713, 3921069994, 3593408605, 38016083, 3634488961, 3889429448, 29 | 568446438, 3275163606, 4107603335, 1163531501, 2850285829, 4243563512, 1735328473, 2368359562, 30 | 4294588738, 2272392833, 1839030562, 4259657740, 2763975236, 1272893353, 4139469664, 3200236656, 31 | 681279174, 3936430074, 3572445317, 76029189, 3654602809, 3873151461, 530742520, 3299628645, 32 | 4096336452, 1126891415, 2878612391, 4237533241, 1700485571, 2399980690, 4293915773, 2240044497, 33 | 1873313359, 4264355552, 2734768916, 1309151649, 4149444226, 3174756917, 718787259, 3951481745] 34 | 35 | init_values = [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476] 36 | 37 | functions = 16*[lambda b, c, d: (b & c) | (~b & d)] + \ 38 | 16*[lambda b, c, d: (d & b) | (~d & c)] + \ 39 | 16*[lambda b, c, d: b ^ c ^ d] + \ 40 | 16*[lambda b, c, d: c ^ (b | ~d)] 41 | 42 | index_functions = 16*[lambda i: i] + \ 43 | 16*[lambda i: (5*i + 1)%16] + \ 44 | 16*[lambda i: (3*i + 5)%16] + \ 45 | 16*[lambda i: (7*i)%16] 46 | 47 | def left_rotate(x, amount): 48 | x &= 0xFFFFFFFF 49 | return ((x<>(32-amount))) & 0xFFFFFFFF 50 | 51 | def md5(message): 52 | message = bytearray(message) #copy our input into a mutable buffer 53 | orig_len_in_bits = (8 * len(message)) & 0xffffffffffffffff 54 | message.append(0x80) 55 | while len(message)%64 != 56: 56 | message.append(0) 57 | message += orig_len_in_bits.to_bytes(8, 'little') 58 | #print (message) 59 | 60 | hash_pieces = init_values[:] 61 | 62 | for chunk_ofst in range(0, len(message), 64): 63 | a, b, c, d = hash_pieces 64 | chunk = message[chunk_ofst:chunk_ofst+64] 65 | #print(a, b, c, d) 66 | for i in range(64): 67 | f = functions[i](b, c, d) 68 | g = index_functions[i](i) 69 | #print(constants[i]) 70 | to_rotate = a + f + constants[i] + int.from_bytes(chunk[4*g:4*g+4], 'little') 71 | new_b = (b + left_rotate(to_rotate, rotate_amounts[i])) & 0xFFFFFFFF 72 | a, b, c, d = d, new_b, b, c 73 | #print(to_rotate) 74 | 75 | for i, val in enumerate([a, b, c, d]): 76 | hash_pieces[i] += val 77 | hash_pieces[i] &= 0xFFFFFFFF 78 | return sum(x<<(32*i) for i, x in enumerate(hash_pieces)) 79 | 80 | def digest(message): 81 | digest = md5(message) 82 | raw = digest.to_bytes(16, 'little') 83 | return '{:032x}'.format(int.from_bytes(raw, 'big')) 84 | #return raw 85 | -------------------------------------------------------------------------------- /md5hash.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright [2018] [Mauro Riva ] 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | Encode a string using an MD5 algorithm 16 | Usage: 17 | >>> from md5hash import md5 18 | >>> m = md5() 19 | >>> m.update('foo') 20 | >>> m.hexdigest() 21 | 'acbd18db4cc2f85cedef654fccc4a4d8' 22 | 23 | Based on https://rosettacode.org/wiki/MD5/Implementation#Python 24 | Adapted for MicroPython 25 | """ 26 | 27 | rotate_amounts = [7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 28 | 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 29 | 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 30 | 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21] 31 | 32 | #constants = [int(abs(math.sin(i+1)) * 2**32) & 0xFFFFFFFF for i in range(64)] # precision is not enough 33 | constants = [3614090360, 3905402710, 606105819, 3250441966, 4118548399, 1200080426, 2821735955, 4249261313, 34 | 1770035416, 2336552879, 4294925233, 2304563134, 1804603682, 4254626195, 2792965006, 1236535329, 35 | 4129170786, 3225465664, 643717713, 3921069994, 3593408605, 38016083, 3634488961, 3889429448, 36 | 568446438, 3275163606, 4107603335, 1163531501, 2850285829, 4243563512, 1735328473, 2368359562, 37 | 4294588738, 2272392833, 1839030562, 4259657740, 2763975236, 1272893353, 4139469664, 3200236656, 38 | 681279174, 3936430074, 3572445317, 76029189, 3654602809, 3873151461, 530742520, 3299628645, 39 | 4096336452, 1126891415, 2878612391, 4237533241, 1700485571, 2399980690, 4293915773, 2240044497, 40 | 1873313359, 4264355552, 2734768916, 1309151649, 4149444226, 3174756917, 718787259, 3951481745] 41 | 42 | init_values = [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476] 43 | 44 | functions = 16*[lambda b, c, d: (b & c) | (~b & d)] + \ 45 | 16*[lambda b, c, d: (d & b) | (~d & c)] + \ 46 | 16*[lambda b, c, d: b ^ c ^ d] + \ 47 | 16*[lambda b, c, d: c ^ (b | ~d)] 48 | 49 | index_functions = 16*[lambda i: i] + \ 50 | 16*[lambda i: (5*i + 1)%16] + \ 51 | 16*[lambda i: (3*i + 5)%16] + \ 52 | 16*[lambda i: (7*i)%16] 53 | 54 | class md5(): 55 | def __init__(self): 56 | return 57 | 58 | def left_rotate(self, x, amount): 59 | x &= 0xFFFFFFFF 60 | return ((x<>(32-amount))) & 0xFFFFFFFF 61 | 62 | def update(self, message): 63 | self.message = bytearray(message) #copy our input into a mutable buffer 64 | orig_len_in_bits = (8 * len(self.message)) & 0xffffffffffffffff 65 | self.message.append(0x80) 66 | while len(self.message)%64 != 56: 67 | self.message.append(0) 68 | self.message += orig_len_in_bits.to_bytes(8, 'little') 69 | 70 | hash_pieces = init_values[:] 71 | 72 | for chunk_ofst in range(0, len(self.message), 64): 73 | a, b, c, d = hash_pieces 74 | chunk = self.message[chunk_ofst:chunk_ofst+64] 75 | for i in range(64): 76 | f = functions[i](b, c, d) 77 | g = index_functions[i](i) 78 | to_rotate = a + f + constants[i] + int.from_bytes(chunk[4*g:4*g+4], 'little') 79 | new_b = (b + self.left_rotate(to_rotate, rotate_amounts[i])) & 0xFFFFFFFF 80 | a, b, c, d = d, new_b, b, c 81 | 82 | for i, val in enumerate([a, b, c, d]): 83 | hash_pieces[i] += val 84 | hash_pieces[i] &= 0xFFFFFFFF 85 | 86 | self.msg_digest = sum(x<<(32*i) for i, x in enumerate(hash_pieces)) 87 | 88 | def digest(self): 89 | return self.msg_digest 90 | 91 | def hexdigest(self): 92 | raw = self.msg_digest.to_bytes(16, 'little') 93 | return '{:032x}'.format(int.from_bytes(raw, 'big')) 94 | -------------------------------------------------------------------------------- /ntptime.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright [2017] [Mauro Riva ] 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | 16 | The above copyright notice and this permission notice shall be 17 | included in all copies or substantial portions of the Software. 18 | """ 19 | import gc 20 | 21 | try: 22 | import usocket as socket 23 | except: 24 | import socket 25 | try: 26 | import ustruct as struct 27 | except: 28 | import struct 29 | 30 | # (date(2000, 1, 1) - date(1900, 1, 1)).days * 24*60*60 31 | NTP_DELTA = 3155673600 32 | host = "time1.google.com" 33 | 34 | 35 | def epoch(): 36 | NTP_QUERY = bytearray(48) 37 | NTP_QUERY[0] = 0x1b 38 | addr = socket.getaddrinfo(host, 123)[0][-1] 39 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 40 | s.settimeout(1) 41 | s.sendto(NTP_QUERY, addr) 42 | msg = s.recv(48) 43 | s.close() 44 | val = struct.unpack("!I", msg[40:44])[0] 45 | gc.collect() 46 | return val - NTP_DELTA 47 | -------------------------------------------------------------------------------- /timeutils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright [2018] [Mauro Riva ] 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | 16 | The above copyright notice and this permission notice shall be 17 | included in all copies or substantial portions of the Software. 18 | """ 19 | 20 | 21 | import time 22 | import math 23 | import gc 24 | 25 | try: 26 | import usocket as socket 27 | except: 28 | import socket 29 | try: 30 | import ustruct as struct 31 | except: 32 | import struct 33 | 34 | NTP_PACKET_FORMAT = "!12I" 35 | 36 | # (70 * 365 + 17) * 24*60*60 37 | NTP_DELTA = 2208988800 38 | 39 | DaysPer4Years = 365*4+1 40 | EPOCH_DOW = 3 41 | SECS_PER_DAY = 60*60*24 42 | 43 | days = [[ 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335], 44 | [ 366, 397, 425, 46, 486, 517, 547, 578, 609, 639, 670, 700], 45 | [ 731, 762, 790, 821, 851, 882, 912, 943, 974,1004,1035,1065], 46 | [1096,1127,1155,1186,1216,1247,1277,1308,1339,1369,1400,1430], 47 | ] 48 | 49 | class RTC(): 50 | def __init__(self): 51 | self.ntp_tmp = 0 52 | self.ntp_epoch = 0 53 | self.ntp_sync_clk = 0 54 | self.delta_time = 0 55 | self.synced_ = False 56 | 57 | def synced(self): 58 | return self.synced_ 59 | 60 | def epoch_ntp(self): 61 | return self.ntp_epoch 62 | 63 | def ntp_sync(self, ntp_server = "pool.ntp.org"): 64 | try: 65 | NTP_QUERY = bytearray(48) 66 | NTP_QUERY[0] = 0x1b 67 | addr = socket.getaddrinfo(ntp_server, 123)[0][-1] 68 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 69 | s.settimeout(1) 70 | res = s.sendto(NTP_QUERY, addr) 71 | msg = s.recv(48) 72 | del(s) #s.close() 73 | unpacked = struct.unpack(NTP_PACKET_FORMAT, msg[0:struct.calcsize(NTP_PACKET_FORMAT)]) 74 | #self.ntp_epoch = int(unpacked[10] + float(unpacked[11]) / 2**32 - NTP_DELTA) 75 | self.ntp_epoch = int(unpacked[10] - NTP_DELTA) 76 | self.ntp_sync_clk = time.time() 77 | self.synced_ = True 78 | gc.collect() 79 | except Exception as e: 80 | print("Something wrong with the ntp-server: " + str(e)) 81 | return 82 | 83 | # updating ntp information with internal clock 84 | def clock_now(self): 85 | self.ntp_epoch = self.ntp_epoch + time.time() - self.ntp_sync_clk 86 | self.ntp_sync_clk = time.time() 87 | 88 | # year 2000-2099, no DST, no leap second, no timezone. 89 | # epoch = (((year/4*(365*4+1)+days[year%4][month]+day)*24+hour)*60+minute)*60+second; 90 | def gmtime(self, epoch = None): 91 | if not epoch: 92 | self.clock_now() 93 | epoch = self.ntp_epoch 94 | 95 | epoch_tmp = float(epoch) 96 | 97 | seconds = int(epoch%60) 98 | epoch = epoch / 60 99 | minutes = int(epoch%60) 100 | epoch = epoch / 60 101 | hours = int(epoch%24) 102 | epoch = epoch / 24 103 | 104 | years = epoch/(DaysPer4Years)*4 105 | epoch = epoch%(DaysPer4Years) 106 | 107 | for year in range(2,-1,-1): 108 | if (epoch >= days[year][0]): 109 | break 110 | for month in range(11,-1,-1): 111 | if (epoch >= days[year][month]): 112 | break 113 | 114 | day = int(epoch - days[year][month]) + 2 115 | year = 1970 + int(years) + year 116 | month = month + 1 117 | 118 | dow = math.ceil((epoch_tmp / (SECS_PER_DAY)) % 7 - EPOCH_DOW) 119 | #print("epoch:" + str(epoch_tmp) + "-> year: " + str(year) + " month: " + str(month) + " day: " + str(day) + " hour: " + str(hours) + " minutes: " + str(minutes) + " dow: " + str(dow)) 120 | return (year, month, day, hours, minutes, seconds, dow) 121 | 122 | def formatdate(self, time_epoch = None): 123 | """Returns a date string as specified by RFC 2822, e.g.: 124 | Fri, 09 Nov 2001 01:08:47 GMT 125 | """ 126 | # Note: we cannot use strftime() because that honors the locale and RFC 127 | # 2822 requires that day and month names be the English abbreviations. 128 | if not time_epoch: 129 | self.clock_now() 130 | time_epoch = self.ntp_epoch 131 | 132 | now = self.gmtime(time_epoch) 133 | zone = 'GMT' 134 | 135 | try: 136 | date_str = self.format_timetuple_and_zone(now, zone) 137 | except Exception as e: 138 | date_str = "none" 139 | print("Timeutils exception: " + str(e)) 140 | return date_str 141 | 142 | def utcnow(self, time_epoch=None, minutes=0, seconds=0): 143 | if(time_epoch == None): 144 | self.clock_now() 145 | time_epoch = self.ntp_epoch + minutes * 60 + seconds 146 | return time_epoch 147 | 148 | def utc_now(self, minutes=0, seconds=0): 149 | self.clock_now() 150 | tmp_gmtime = self.gmtime(self.ntp_epoch + minutes * 60 + seconds) 151 | return '%04d-%02d-%02d %02d:%02d:%02d.%04d' % ( 152 | tmp_gmtime[0], tmp_gmtime[1], tmp_gmtime[2], 153 | tmp_gmtime[3], tmp_gmtime[4], tmp_gmtime[5], tmp_gmtime[6]) 154 | def now(self): 155 | self.clock_now() 156 | tmp_gmtime = self.gmtime(self.ntp_epoch) 157 | return tmp_gmtime[:-1] + (0,) + ('None',) 158 | 159 | def format_timetuple_and_zone(self, timetuple, zone): 160 | return '%s, %02d %s %04d %02d:%02d:%02d %s' % ( 161 | ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'][timetuple[6]], 162 | timetuple[2], 163 | ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 164 | 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][timetuple[1] - 1], 165 | timetuple[0], timetuple[3], timetuple[4], timetuple[5], 166 | zone) 167 | -------------------------------------------------------------------------------- /urllib/urequest.py: -------------------------------------------------------------------------------- 1 | import usocket 2 | 3 | def urlopen(url, data=None, method="GET"): 4 | if data is not None and method == "GET": 5 | method = "POST" 6 | try: 7 | proto, dummy, host, path = url.split("/", 3) 8 | except ValueError: 9 | proto, dummy, host = url.split("/", 2) 10 | path = "" 11 | if proto == "http:": 12 | port = 80 13 | elif proto == "https:": 14 | import ussl 15 | port = 443 16 | else: 17 | raise ValueError("Unsupported protocol: " + proto) 18 | 19 | if ":" in host: 20 | host, port = host.split(":", 1) 21 | port = int(port) 22 | 23 | ai = usocket.getaddrinfo(host, port) 24 | addr = ai[0][4] 25 | 26 | s = usocket.socket() 27 | try: 28 | s.connect(addr) 29 | if proto == "https:": 30 | s = ussl.wrap_socket(s, server_hostname=host) 31 | 32 | s.write(method) 33 | s.write(b" /") 34 | s.write(path) 35 | s.write(b" HTTP/1.0\r\nHost: ") 36 | s.write(host) 37 | s.write(b"\r\n") 38 | 39 | if data: 40 | s.write(b"Content-Length: ") 41 | s.write(str(len(data))) 42 | s.write(b"\r\n") 43 | s.write(b"\r\n") 44 | if data: 45 | s.write(data) 46 | 47 | l = s.readline() 48 | protover, status, msg = l.split(None, 2) 49 | status = int(status) 50 | #print(protover, status, msg) 51 | while True: 52 | l = s.readline() 53 | if not l or l == b"\r\n": 54 | break 55 | #print(l) 56 | if l.startswith(b"Transfer-Encoding:"): 57 | if b"chunked" in l: 58 | raise ValueError("Unsupported " + l) 59 | elif l.startswith(b"Location:"): 60 | raise NotImplementedError("Redirects not yet supported") 61 | except OSError: 62 | s.close() 63 | raise 64 | 65 | return s 66 | --------------------------------------------------------------------------------