diff --git a/ElementTree.py b/ElementTree.py index 254c5d1c25e40af35835596cda380fea84f58f8c..8ec120328f1c012006896a7c2fc7d4c660ef0679 100644 --- a/ElementTree.py +++ b/ElementTree.py @@ -55,6 +55,12 @@ class Element: def get(self, key, default=None): return self.attrib.get(key, default) + def find_first_by_tag(self, tag): + for child in self: + if child.tag == tag: + return child + return None + def set(self, key, value): self.attrib[key] = value diff --git a/main.py b/main.py new file mode 100644 index 0000000000000000000000000000000000000000..c45a97edc61aff117535748a411e29830ea2107a --- /dev/null +++ b/main.py @@ -0,0 +1,8 @@ +import sdcard +import uNextcloud +import gc + +sd_spi = machine.SPI(0, sck=machine.Pin(18, machine.Pin.OUT), mosi=machine.Pin(19, machine.Pin.OUT), miso=machine.Pin(16, machine.Pin.OUT)) +sd = sdcard.SDCard(sd_spi, machine.Pin(22)) +uos.mount(sd, "/sd") +gc.collect() diff --git a/uNextcloud.py b/uNextcloud.py index 7feff82c88cdcdc5af667de2e49c5cd7d6f1aa19..a7cfe359fae683b4bbd766e5bc46b5af652e4cd6 100644 --- a/uNextcloud.py +++ b/uNextcloud.py @@ -1,25 +1,51 @@ import ElementTree -from urllib import urequest +import urequests +from urequests import auth import gc import uos class uNextcloud: - def __init__(self): - self.sd_spi = machine.SPI(0, sck=machine.Pin(18, machine.Pin.OUT), mosi=machine.Pin(19, machine.Pin.OUT), miso=machine.Pin(16, machine.Pin.OUT)) - self.sd = sdcard.SDCard(sd_spi, machine.Pin(22)) + class File: + + def __init__(self, url_path, mimetype): + self.url_path = url_path + self.mimetype = mimetype + + def get_url(self): + return self.url_path + + def get_mimetype(self): + return self.mimetype + + def __init__(self, nextcloud_url): + self.url = nextcloud_url self.username = None self.password = None - uos.mount(sd, "/sd") - gc.collect() def set_auth(self, username, password): self.username = username self.password = password - def get_folder_items(self, folder_path): - pass + def get_folder_items(self, folder_path=""): + response = urequests.request("PROPFIND", self.url+"/remote.php/dav/files/"+self.username+"/"+folder_path, auth=urequests.auth.HTTPBasicAuth(self.username, self.password)) + if 200 <= response.status_code < 300: + file_list = [] + try: + xml = ElementTree.fromstring(response.text) + for resp in xml: + href = resp.find_first_by_tag("href") + propstat = resp.find_first_by_tag("propstat") + prop = propstat.find_first_by_tag("prop") + content_type = prop.find_first_by_tag("getcontenttype") + if content_type != None: + file_list.append(self.File(href.text, content_type.text)) + return file_list + except AttributeError: + return None + else: + return None def download_file_to_path(self, folder_path, file, destination_path): pass diff --git a/urequests/__init__.py b/urequests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..237c4c70c3f186e2caf8d10692b2608ba4bf520c --- /dev/null +++ b/urequests/__init__.py @@ -0,0 +1,169 @@ +# (c) 2016-2021 Paul Sokolovsky, MIT license, https://github.com/pfalcon/pycopy-lib +import usocket + +class Request: + pass + + +class Response: + + def __init__(self, f): + self.raw = f + self.encoding = "utf-8" + self._cached = None + + def close(self): + if self.raw: + self.raw.close() + self.raw = None + self._cached = None + + @property + def content(self): + if self._cached is None: + try: + self._cached = self.raw.read() + finally: + self.raw.close() + self.raw = None + return self._cached + + @property + def text(self): + return str(self.content, self.encoding) + + def json(self): + import ujson + return ujson.loads(self.content) + + +def request(method, url, data=None, json=None, headers={}, auth=None, stream=None, parse_headers=True): + redir_cnt = 1 + if json is not None: + assert data is None + import ujson + data = ujson.dumps(json) + + while True: + try: + proto, dummy, host, path = url.split("/", 3) + except ValueError: + proto, dummy, host = url.split("/", 2) + path = "" + if proto == "http:": + port = 80 + elif proto == "https:": + import ussl + port = 443 + else: + raise ValueError("Unsupported protocol: " + proto) + + if ":" in host: + host, port = host.split(":", 1) + port = int(port) + + if auth is not None: + req = Request() + req.method = method + req.url = url + if not headers: + # Fresh local dict, not a copy of anything. + headers = {} + req.headers = headers + req = auth(req) + headers = req.headers + + ai = usocket.getaddrinfo(host, port, 0, usocket.SOCK_STREAM) + ai = ai[0] + + resp_d = None + if parse_headers is not False: + resp_d = {} + + s = usocket.socket(ai[0], ai[1], ai[2]) + try: + s.connect(ai[-1]) + if proto == "https:": + s = ussl.wrap_socket(s, server_hostname=host) + s.write(b"%s /%s HTTP/1.0\r\n" % (method, path)) + if not "Host" in headers: + s.write(b"Host: %s\r\n" % host) + # Iterate over keys to avoid tuple alloc + for k in headers: + s.write(k) + s.write(b": ") + s.write(headers[k]) + s.write(b"\r\n") + if json is not None: + s.write(b"Content-Type: application/json\r\n") + if data: + s.write(b"Content-Length: %d\r\n" % len(data)) + s.write(b"Connection: close\r\n\r\n") + if data: + s.write(data) + + l = s.readline() + #print(l) + l = l.split(None, 2) + status = int(l[1]) + reason = "" + if len(l) > 2: + reason = l[2].rstrip() + while True: + l = s.readline() + if not l or l == b"\r\n": + break + #print(l) + + if l.startswith(b"Transfer-Encoding:"): + if b"chunked" in l: + raise ValueError("Unsupported " + l.decode()) + elif l.startswith(b"Location:") and 300 <= status <= 399: + if not redir_cnt: + raise ValueError("Too many redirects") + redir_cnt -= 1 + url = l[9:].decode().strip() + #print("redir to:", url) + status = 300 + break + + if parse_headers is False: + pass + elif parse_headers is True: + l = l.decode() + k, v = l.split(":", 1) + resp_d[k] = v.strip() + else: + parse_headers(l, resp_d) + except OSError: + s.close() + raise + + if status != 300: + break + + resp = Response(s) + resp.status_code = status + resp.reason = reason + if resp_d is not None: + resp.headers = resp_d + return resp + + +def head(url, **kw): + return request("HEAD", url, **kw) + +def get(url, **kw): + return request("GET", url, **kw) + +def post(url, **kw): + return request("POST", url, **kw) + +def put(url, **kw): + return request("PUT", url, **kw) + +def patch(url, **kw): + return request("PATCH", url, **kw) + +def delete(url, **kw): + return request("DELETE", url, **kw) diff --git a/urequests/auth.py b/urequests/auth.py new file mode 100644 index 0000000000000000000000000000000000000000..4cbb29d0da5339f14b0dff24c0bd0f323b813c76 --- /dev/null +++ b/urequests/auth.py @@ -0,0 +1,11 @@ +# (c) 2021 Paul Sokolovsky, MIT license, https://github.com/pfalcon/pycopy-lib +import uwwwauth + + +class HTTPBasicAuth: + def __init__(self, user, passwd): + self.auth = uwwwauth.basic_resp(user, passwd) + + def __call__(self, r): + r.headers["Authorization"] = self.auth + return r \ No newline at end of file diff --git a/uwwwauth/__init__.py b/uwwwauth/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..c88685ca93a33f6836429e049f6547ad73405bd2 --- /dev/null +++ b/uwwwauth/__init__.py @@ -0,0 +1,108 @@ +# RFC2617, WWW-Authenticate: Basic/Digest module +# (c) 2018 Paul Sokolovsky, MIT license +import uhashlib +import ubinascii + + +# Private functions - do not use, will change + +def md5_concat(arg1, arg2, arg3): + h = uhashlib.md5(arg1) + h.update(b":") + h.update(arg2) + if arg3 is not None: + h.update(b":") + h.update(arg3) + return ubinascii.hexlify(h.digest()).decode() + + +def make_digest_ha1(a1, method, uri, nonce): + a2 = md5_concat(method, uri, None) + digest = md5_concat(a1, nonce, a2) + return digest + + +def make_digest(realm, username, passwd, method, uri, nonce): + a1 = md5_concat(username, realm, passwd) + return make_digest_ha1(a1, method, uri, nonce) + + +def parse_auth_req(line): + typ, line = line.split(None, 1) + d = {"type": typ} + for kv in line.split(","): + k, v = kv.split("=", 1) + assert v[0] == '"' and v[-1] == '"' + d[k.strip()] = v[1:-1] + return d + + +def format_resp(resp_d): + fields = [] + for k, v in resp_d.items(): + if k in ("type", "passwd"): + continue + fields.append('%s="%s"' % (k, v)) + resp_auth = ", ".join(fields) + + resp_auth = "Digest " + resp_auth + return resp_auth + + +def _digest_resp(auth_d, username, passwd, method, URL): + #print(auth_d) + + resp_d = {} + resp_d["username"] = username + resp_d["uri"] = URL + resp_d["realm"] = auth_d["realm"] + resp_d["nonce"] = auth_d["nonce"] + + digest = make_digest(auth_d["realm"], username, passwd, method, URL, auth_d["nonce"]) + resp_d["response"] = digest + #print(resp_d) + + return format_resp(resp_d) + + +# Helper functions - may change + +def basic_resp(username, passwd): + return "Basic " + ubinascii.b2a_base64("%s:%s" % (username, passwd))[:-1].decode() + + +def auth_resp(auth_line, username, passwd, method=None, URL=None): + auth_d = parse_auth_req(auth_line) + if auth_d["type"] == "Basic": + return basic_resp(username, passwd) + elif auth_d["type"] == "Digest": + assert method and URL + return _digest_resp(auth_d, username, passwd, method, URL) + else: + raise ValueError(auth_d["type"]) + + +# Public interface + +class WWWAuth: + + def __init__(self, username, passwd): + self.username = username + self.passwd = passwd + self.cached_auth_line = None + + def resp(self, auth_line, method, URL): + if auth_line.startswith("Basic"): + return basic_resp(self.username, self.passwd) + elif auth_line.startswith("Digest"): + auth_d = parse_auth_req(auth_line) + if auth_line != self.cached_auth_line: + self.ha1 = md5_concat(self.username, auth_d["realm"], self.passwd) + self.cached_auth_line = auth_line + digest = make_digest_ha1(self.ha1, method, URL, auth_d["nonce"]) + auth_d["username"] = self.username + auth_d["uri"] = URL + auth_d["response"] = digest + return format_resp(auth_d) + else: + raise ValueError("Unsupported auth: " + auth_line)