chrysn / aiocoap

The Python CoAP library
Other
264 stars 119 forks source link

Option "Uri-Path" is missing in request #157

Open aellwein opened 5 years ago

aellwein commented 5 years ago

Hi @chrysn,

it seems that the request passed to render_get() method missing the uri_path option, which is required in most use cases.

Reproducable with aiocoap==0.3 and aiocoap==0.4a1.

See the test & output below:

import asyncio
import logging

import aiocoap
import aiocoap.resource as resource
from aiocoap.numbers.codes import Code

log = logging.getLogger('uri_path_test')

class MyResource(resource.Resource):
    def __init__(self):
        super(MyResource, self).__init__()

    async def render_get(self, request):
        log.info(f'GET {request.opt.uri_path}')
        return aiocoap.Message(code=Code.CONTENT, payload=b'')

if __name__ == "__main__":
    logging.basicConfig(level=logging.DEBUG)
    root = resource.Site()
    root.add_resource(('3', '0'), MyResource())

    async def create_server():
        await aiocoap.Context.create_server_context(root)

    async def read_3_0():
        ctx = await aiocoap.Context.create_client_context()
        response = await ctx.request(aiocoap.Message(code=Code.GET, uri='coap://localhost/3/0')).response
        return response.opt.uri_path

    asyncio.get_event_loop().run_until_complete(create_server())
    uri_path = asyncio.get_event_loop().run_until_complete(read_3_0())
    assert uri_path == ('3', '0'), f'expected the Uri-Path to be ("3","0"), got "{uri_path}" !'

Output:

DEBUG:asyncio:Using selector: EpollSelector
DEBUG:coap:Sending message <aiocoap.Message at 0x7fa058ace160: Type.CON GET (ID 50715, token b'\x00\x00WV') remote <UDP6EndpointAddress [::1]:5683>, 2 option(s)>
DEBUG:coap:Exchange added, message ID: 50715.
DEBUG:coap.requester:Timeout is 93.0
DEBUG:coap.requester:Sending request - Token: 00005756, Remote: <UDP6EndpointAddress [::1]:5683>
DEBUG:coap-server:Incoming message <aiocoap.Message at 0x7fa0589e5c50: Type.CON GET (ID 50715, token b'\x00\x00WV') remote <UDP6EndpointAddress [::1]:58915 with local address>, 2 option(s)>
DEBUG:coap-server:New unique message received
DEBUG:coap-server.responder:New responder created, key (('3', '0'), <UDP6EndpointAddress [::1]:58915 with local address>)
INFO:uri_path_test:GET ()
DEBUG:coap-server.responder:Preparing response...
DEBUG:coap-server.responder:Sending token: 00005756
DEBUG:coap-server.responder:Sending response, type = Type.ACK (request type = Type.CON)
DEBUG:coap-server:Sending message <aiocoap.Message at 0x7fa058a1c860: Type.ACK 2.05 Content (ID 50715, token b'\x00\x00WV') remote <UDP6EndpointAddress [::1]:58915 with local address>>
DEBUG:coap:Incoming message <aiocoap.Message at 0x7fa0589e5a20: Type.ACK 2.05 Content (ID 50715, token b'\x00\x00WV') remote <UDP6EndpointAddress [::1]:5683 with local address>>
DEBUG:coap:New unique message received
DEBUG:coap:Exchange removed, message ID: 50715.
DEBUG:coap:Received Response: <aiocoap.Message at 0x7fa0589e5a20: Type.ACK 2.05 Content (ID 50715, token b'\x00\x00WV') remote <UDP6EndpointAddress [::1]:5683 with local address>>
Traceback (most recent call last):
  File "/home/alex/git/lwm2mclient/path_test.py", line 35, in <module>
    assert uri_path == ('3', '0'), f'expected the Uri-Path to be ("3","0"), got "{uri_path}" !'
AssertionError: expected the Uri-Path to be ("3","0"), got "()" !
aellwein commented 5 years ago

This used to work a while ago with my lwm2mclient project, but i haven't used it long ago.

chrysn commented 5 years ago

Brief response before I find the time to go into details: The Site (now) strips away all options it processes -- that's required to allow nested sites, and allows resources to err out if they encounter mandatory-to-process options. (A resource handler should not need to deal with the path (or the Uri-Host, for that matter) any more, and the site strips them away just as blockwise assembly strips away block options).

I'm not fully happy with the requests API yet, and having both an "original options" and "remaining options" list around would be a possibility, but to find a good design there: What's your need for having the path options around in a handler that was, as in your example, already constructed with the knowledge of being a "3_0" handler?

(Note to self: Add an FAQ entry about this.)

aellwein commented 5 years ago

Well, the original idea was to provide "generic" GET/PUT/... etc. handlers, which is then to be customized by the client's user. In Lightweight M2M world, CoAP resources represent parts of device functionality, so called "objects" located at defined paths, like Device (/3), Firmware Update (/5), Location (/6) etc. objects being represented by instances (/3/0 means Device object instance 0 etc.) and then their attributes mapped to the last path segment. The CoAP methods mimic the operation on the object/object's attribute, like GET /3/0 would mean "read Manufacturer attribute of the Device Object". To make the model as flexible as possible, i decided to provide the representation of attributes in a separate JSON file which is loaded at startup. The user of the client is up to implement the missing operation and for that the knowledge of "what is being accessed/changed", i.e. the Uri-path (not the host) is required. So for instance, an object Device being handled with one resource handler registered at ("3", "0") and knowing about the attributes by their numbers on the path:

image

Without the uri-path, i would not know in render_get of Device resource, which attribute of device is being accessed/changed, unless i would register a handler on per-attribute basis (which i consider to be ugly).

chrysn commented 5 years ago

On Sun, May 19, 2019 at 10:34:10AM -0700, Alex Ellwein wrote:

Well, the original idea was to provide "generic" GET/PUT/... etc. handlers, which is then to be customized by the client's user.

Currently I'd see different ways to go about this, either:

class OMASite(resource.Site):
    async def render(self, request):
        numericpath = extract_numeric_path(request)
        if numericpath is None:
            return await super().render(request)
        else:
            return await self.lwm2m_handler.render(numericpath, request)

(The last option is a bit cumbersome compared to the second, but that's iMO a consequence of the lwm2m application not being designed according to RFC7320).

Would any of those work in your use case, or would you still prefer to have the ability to access the original request path in a single handler?

aellwein commented 5 years ago

Thanks for the detailed response.

Because i cannot ensure that the link header information always contained in the resource (IIRC some older Leshan server versions don't provide it, i'm also unsure about other LwM2M implementations so i'm expecting bad behavior here), i would rather tend to implement the latter alternative, it also gives me the chance to pass additional information from the original request, if i would need to.

Best regards, Alex