WebThingsIO / thing-url-adapter

Proxy adapter for Web Thing API endpoints
Mozilla Public License 2.0
21 stars 18 forks source link

Unable to add subpaths as things #71

Closed AcidRobot closed 5 years ago

AcidRobot commented 5 years ago

If you have a thing at http://example.com/lamp thing-adapter parses out the "/lamp" and cannot connect to device as it tries to send commands to "example.com"

I've written around the issue for my specific url but wanted it to be known.

error:

2019-04-23 11:09:40.597 INFO   : thing-url: Failed to connect to http://example.com/properties/brightness: FetchError: invalid json response body at http://example.com/properties/brightness reason: Unexpected token < in JSON at position 0
2019-04-23 11:09:40.618 INFO   : thing-url: Failed to connect to http://example.com/properties/on: FetchError: invalid json response body at http://example.com/properties/on reason: Unexpected token < in JSON at position 0

image

work around: line 99: this.baseHref = (url.indexOf('example') !== -1) ? new URL(url) : new URL(url).origin;

line 524: const baseHref = (url.indexOf('example') !== -1) ? new URL(url) : new URL(url).origin;

mrstegeman commented 5 years ago

What library are you using to create your webthing? Is it one of ours, or is it something custom? I'm guessing you're missing the top-level href field in your thing description. Can you post your full thing description from http://example.com/lamp?

mrstegeman commented 5 years ago

Alternatively, the links inside your properties may be wrong.

AcidRobot commented 5 years ago

There's a top level href? Is it the alternative link from https://iot.mozilla.org/wot/#web-thing-description :

      "rel": "alternate",
      "href": "wss://mywebthingserver.com/things/lamp"
    },

here is my configs nodejs:

const {
  Action,
  Event,
  Property,
  SingleThing,
  Thing,
  Value,
  WebThingServer,
} = require('webthing');
const uuidv4 = require('uuid/v4');

class OverheatedEvent extends Event {
  constructor(thing, data) {
    super(thing, 'overheated', data);
  }
}

class FadeAction extends Action {
  constructor(thing, input) {
    super(uuidv4(), thing, 'fade', input);
  }

  performAction() {
    return new Promise((resolve) => {
      setTimeout(() => {
        this.thing.setProperty('brightness', this.input.brightness);
        this.thing.addEvent(new OverheatedEvent(this.thing, 102));
        resolve();
      }, this.input.duration);
    });
  }
}

function makeThing() {
  const thing = new Thing('My Lamp',
                          ['OnOffSwitch', 'Light'],
                          'A web connected lamp');

  thing.addProperty(
    new Property(thing,
                 'on',
                 new Value(true),
                 {
                   '@type': 'OnOffProperty',
                   title: 'On/Off',
                   type: 'boolean',
                   description: 'Whether the lamp is turned on',
                 }));
  thing.addProperty(
    new Property(thing,
                 'brightness',
                 new Value(50),
                 {
                   '@type': 'BrightnessProperty',
                   title: 'Brightness',
                   type: 'integer',
                   description: 'The level of light from 0-100',
                   minimum: 0,
                   maximum: 100,
                   unit: 'percent',
                 }));

  thing.addAvailableAction(
    'fade',
    {
      title: 'Fade',
      description: 'Fade the lamp to a given level',
      input: {
        type: 'object',
        required: [
          'brightness',
          'duration',
        ],
        properties: {
          brightness: {
            type: 'integer',
            minimum: 0,
            maximum: 100,
            unit: 'percent',
          },
          duration: {
            type: 'integer',
            minimum: 1,
            unit: 'milliseconds',
          },
        },
      },
    },
    FadeAction);

  thing.addAvailableEvent(
    'overheated',
    {
      description: 'The lamp has exceeded its safe operating temperature',
      type: 'number',
      unit: 'degree celsius',
    });

  return thing;
}

function runServer() {
  const thing = makeThing();

  // If adding more than one thing, use MultipleThings() with a name.
  // In the single thing case, the thing's name will be broadcast.
  const server = new WebThingServer(new SingleThing(thing), 8888);

  process.on('SIGINT', () => {
    server.stop().then(() => process.exit()).catch(() => process.exit());
  });

  server.start().catch(console.error);
}

runServer();
mrstegeman commented 5 years ago

With the code you just gave me, the thing description is located at /, not at /lamp.

AcidRobot commented 5 years ago

I'm using a proxy_pass to host it at example.com/lamp

location /lamp/ {
        proxy_pass http://localhost:8888/;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_cache_bypass $http_upgrade;
      }

Is there another way?

mrstegeman commented 5 years ago

That's not going to work. The URLs are all taken from the various links in the thing description.

{
  "name": "My Lamp",
  "href": "/",
  "@context": "https://iot.mozilla.org/schemas",
  "@type": [
    "OnOffSwitch",
    "Light"
  ],
  "properties": {
    "on": {
      "@type": "OnOffProperty",
      "title": "On/Off",
      "type": "boolean",
      "description": "Whether the lamp is turned on",
      "links": [
        {
          "rel": "property",
          "href": "/properties/on"
        }
      ]
    },
    "brightness": {
      "@type": "BrightnessProperty",
      "title": "Brightness",
      "type": "integer",
      "description": "The level of light from 0-100",
      "minimum": 0,
      "maximum": 100,
      "unit": "percent",
      "links": [
        {
          "rel": "property",
          "href": "/properties/brightness"
        }
      ]
    }
  },
  "actions": {
    "fade": {
      "title": "Fade",
      "description": "Fade the lamp to a given level",
      "input": {
        "type": "object",
        "required": [
          "brightness",
          "duration"
        ],
        "properties": {
          "brightness": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100,
            "unit": "percent"
          },
          "duration": {
            "type": "integer",
            "minimum": 1,
            "unit": "milliseconds"
          }
        }
      },
      "links": [
        {
          "rel": "action",
          "href": "/actions/fade"
        }
      ]
    }
  },
  "events": {
    "overheated": {
      "description": "The lamp has exceeded its safe operating temperature",
      "type": "number",
      "unit": "degree celsius",
      "links": [
        {
          "rel": "event",
          "href": "/events/overheated"
        }
      ]
    }
  },
  "links": [
    {
      "rel": "properties",
      "href": "/properties"
    },
    {
      "rel": "actions",
      "href": "/actions"
    },
    {
      "rel": "events",
      "href": "/events"
    },
    {
      "rel": "alternate",
      "href": "ws://localhost:8888/"
    }
  ],
  "description": "A web connected lamp"
}

See the href fields inside the various links arrays. Those are all absolute paths, which is how the gateway will interpret them.

A better option would be to host the thing description on its own virtual host, rather than as a sub-directory.

AcidRobot commented 5 years ago

yeah, unfortunately I'm running it under a tunneling service and having nginx rewrite the things to each endpoint. Could I not just add an

 "name": "My Lamp",
  "href": "/lamp", // "/" -> "/lamp"
  "@context": "https://iot.mozilla.org/schemas",
  "@type": [
    "OnOffSwitch",
    "Light"
  ],
mrstegeman commented 5 years ago

In that case, what you'll want to do is patch the webthing library. Prefix all of the paths in this code with /lamp: https://github.com/mozilla-iot/webthing-node/blob/master/lib/server.js#L734-L759

mrstegeman commented 5 years ago

Nevermind, that will only half-solve your problem. There are going to be some pretty invasive changes to make this work.

AcidRobot commented 5 years ago

then it would be available at http://localhost:8888/lamp and thing-url-adapter would query it at http://example.com/lamp?

AcidRobot commented 5 years ago

yeah, that line changing the url to allow the thing to have a subpath works pretty well

mrstegeman commented 5 years ago

Oh, great. Glad that worked.

mrstegeman commented 5 years ago

Alternatively, you could keep your proxy config as is and also proxy /properties, /actions, and /events to the web thing.

AcidRobot commented 5 years ago

The work around from my original comment.

AcidRobot commented 5 years ago

I plan on having multiple things under the same url for example:

server {
      listen 80;
      server_name example.com;
      location /light01/ {
        proxy_pass http://192.168.0.101:8888/;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_cache_bypass $http_upgrade;
      }
      location /lamp/ {
        proxy_pass http://localhost:8888/;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_cache_bypass $http_upgrade;
      }
      location /light02/ {
        proxy_pass http://192.168.0.103:8888/;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_cache_bypass $http_upgrade;
      }
AcidRobot commented 5 years ago

Could I add them as hrefs to the properties themselves? For example:

"on": {
      "@type": "OnOffProperty",
      "title": "On/Off",
      "type": "boolean",
      "description": "Whether the lamp is turned on",
      "links": [
        {
          "rel": "property",
          "href": "/lamp/properties/on"
        }
      ]
    },
mrstegeman commented 5 years ago

Would you be up for trying out a pull request?

https://github.com/mozilla-iot/webthing-node/pull/86

There's a new parameter you can pass to the WebThingServer constructor.

AcidRobot commented 5 years ago

yeah, Do I need to add something to the my lamp example posted above?

mrstegeman commented 5 years ago

Change:

const server = new WebThingServer(new SingleThing(thing), 8888);

to:

const server = new WebThingServer(new SingleThing(thing), 8888, null, null, null, '/lamp');
AcidRobot commented 5 years ago

Hey so I pulled it and everything seems to be working.

AcidRobot commented 5 years ago

I did have to change the proxy to the following:

location /lamp/ {
        proxy_pass http://localhost:8888/lamp/;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_cache_bypass $http_upgrade;
      }
mrstegeman commented 5 years ago

Excellent. I'll merge that PR and it will be available when I release v0.12.