Giter Site home page Giter Site logo

Comments (20)

mihai-dinculescu avatar mihai-dinculescu commented on May 27, 2024 1

That can also work, yes. It will be interesting to see what you come up with.

I'll work on the changes to help with the reusability of the RSA key pair and the session extraction these days.

from tapo.

mihai-dinculescu avatar mihai-dinculescu commented on May 27, 2024 1

Wow, that was quick!

Here are some thoughts:

  • password is used like an API key. It would make sense to call it as such. Also, the convention is to add it as an X-API-KEY header instead of a body object.
  • do you know about config-rs? It's a lovely library that helps with configs.
  • would it make sense to namespace the endpoints? e.g. /l530/on && /p110/on. The handlers can be reused.

from tapo.

mihai-dinculescu avatar mihai-dinculescu commented on May 27, 2024 1

Release v0.7.1 brings clone to the ApiClient.

from tapo.

mihai-dinculescu avatar mihai-dinculescu commented on May 27, 2024

While it's not exactly what you're asking for, this repo contains an actix actor that spins up a REST API which can serve as a decent starting point.

Your actual idea, about building a containerized REST API that fully wraps the capabilities of the library is brilliant and I will gladly support whoever decides to build it.

from tapo.

ijpatricio avatar ijpatricio commented on May 27, 2024

Hey everyone.
Someday when I get free time, I would love to learn Rust, but right now, it's hard for me...

But I'm proficient with Docker. Would that help?

from tapo.

ClementNerma avatar ClementNerma commented on May 27, 2024

Hi,

I'm going to receive a pair of L530E in the following week. I've made a lot of Rust & Docker-related projects, I can quickly build a simple REST API if you want :)

One way that could be implemented would be to have one GET route per main feature (e.g. /on, /off, /get-device-info, etc.), and one for the set() API (when available).

Is there any specific feature you'd like to see in the API?

Also @mihai-dinculescu how would you like this to be integrated? In another repo, in the same repo as your library (as a Cargo workspace for instance)?

from tapo.

mihai-dinculescu avatar mihai-dinculescu commented on May 27, 2024

It makes the most sense to be a separate repo. Let me know the URL so I can add it to the README.md.

It will be interesting to see what you can develop because there are so many ways to go about it and lots of decisions about ergonomics vs breadth of options.

It might be worth experimenting with reusable handlers under different routes, e.g. an on handler used for both POST /l530/on and POST/p110/on.

Another interesting decision will be where the credentials are stored. Are they part of the API secrets, or should they be part of the payload?

And what about the session? Should there be an endpoint that logs in on a particular device and returns the session to be used for subsequent calls?

from tapo.

ClementNerma avatar ClementNerma commented on May 27, 2024

I think secrets should be put in the environment variables, automatically loaded from a .env file.
Sessions could work with a bearer token : you log in with the credentials, the API answers with a token, and you reuse it for future calls. The session would be stored in memory.

from tapo.

mihai-dinculescu avatar mihai-dinculescu commented on May 27, 2024

The Tapo account e-mail and password should go into the service as env vars, indeed.
The device IP and type should probably be stored on the caller side and passed to the API.

There are two sessions to worry about:

  • the API session could be handled with a JWT minted serverside on login, returned to the client, and then validated by the server on each call. This will allow the API to stay stateless.
  • The Tapo session. It is by device. Here are two options:
    • The API authenticates to Tapo on each request. This should be workable but slow.

    • The API authenticates to the device and then returns the Tapo Session to the caller to be used on subsequent calls. The Tapo session it's a bit more complicated because it's made of both a token and an RSA key pair. There are two possible solutions to this:

      • the token + the RSA key-pair are encoded together and returned to the caller
      • the API uses a single RSA key pair for all devices. It can be either passed in as env vars or generated at startup. This should be good enough, security-wise. I suspect that the Tapo App does the same thing.

      Some changes will need to be made to the Tapo client to accommodate this flexibility around the Tapo session. Still, it all sounds like valuable features to have anyway, so I will go ahead and add them in the coming days.

from tapo.

ClementNerma avatar ClementNerma commented on May 27, 2024

I'm personally not fond of JWT for multiple reasons, I think a bearer token would be a better approach.

For Tapo sessions, I imagine a simple mechanism: storing the Tapo session for each devices inside the server session.

  • When calling an API route that performs an action on a given Tapo device, check if a session for this specific device is already stored in the server session (depending on the bearer token)
  • If there is one, use it ;
  • If not, authenticate against the Tapo device and store the resulting session in the server session.
  • If there is one but it expired, do the same thing as above.

English is not my primary language so I'm not sure if what I'm trying to say is clear. But basically you would have the server session (which is the one for which the user gets a bearer token to provide in the Authorization header) as well as sessions for the different Tapo devices, which would be linked to this session.

from tapo.

ClementNerma avatar ClementNerma commented on May 27, 2024

Ok so I've made a little server, which can be found in the following repository: https://github.com/ClementNerma/tapo-rest. Here is the approach I chose:

Firstly you create a JSON config file (anywhere) which has this structure:

{
    "account": {
        "username": "<your tapo account's email>",
        "password": "<your tapo account's password>"
    },
    "devices": [
        {
            "name": "living-room-bulb",
            "device_type": "L530",
            "ip_addr": "<ip address of the device>"
        },
        {
            "name": "kitchen-bulb",
            "device_type": "L530",
            "ip_addr": "<ip address of the device>"
        }
    ]
}

This allows to store credentials without them appearing anywhere: not in any environment variable (which can be inspected through processes), not on the command line itself. And it allows to register multiple devices simultaneously. This format allows adding new features later on (e.g. zones management, etc.)

You then run the server with:

cargo run -- --devices-config-path <path to your json file> --port 8000 --auth-password 'potatoes'

This will run the server on 0.0.0.0:8000 (you can chose any port you like) and will require clients to use the potatoes password to log in.

Please note though that the server is not using SSL certificates (only plain HTTP/1 and HTTP/2), so you absolutely need to use a proxy (such as Caddy) if you don't want this secret password to appear in plain text on your network.

Before exposing the REST API, the server starts by connecting to all the devices specicified in your config file, to ensure they are reachable and caching the authentication results.

Clients call the POST /login route with a body of { "password": "potatoes" }. This returns a raw string, which is the session ID.

curl -i -X POST -H 'Content-Type: application/json' --data '{ "password": "potatoes" }' http://localhost:8000/login

All subsequent calls to the API must include an Authorization header containing the session ID (Authorization: Bearer <session ID>). Note that when the server exits, all sessions are currently immediatly destroyed - I think a basic JSON file to store the sessions would be a good idea, I don't think we need a full-blown database for that. EDIT: I made it so the application stores a simple JSON file in dirs::data_local_dir().join(env!("CARGO_PKG_NAME")) with the session IDs and content. This way they can be reused after a server restart.

You can then access all other API routes which are located under /actions to use your device. Each route takes a ?device=<name> query parameter to know which device you are trying to interact with. The <name> is the same as the one you provided in your config file.

curl -i -X GET -H 'Authorization: Bearer <your session ID>' 'http://localhost:8000/on?device=living-room-bulb'

Current routes (I just started with two routes) are /actions/on and /actions/off. They work perfectly fine at my home :)
EDIT: I'm adding new ones right now, just did /actions/set-brightness with query param level=<int> and /actions/set-color with query param name=<color>.

For later routes such as changing the color of a light bulb, if you are specifying a device that's either not a light bulb or a one that does not have color control feature, it will return an HTTP error describing what happened.

This server can be very easily bootstraped into a Dockerfile (I'll do it as soon as I have a few more routes to play with). It is extremely lightweight, current version only weighs a few megabytes stripped. I also managed to vendor the openssl crate to avoid having any pre-requisite and also built an ARM64 standalone executable successfully (I didn't test it though).

What do you think of this system? Please tell me if you think some things should be added / modified / removed ;)

(By the way your library is really amazing, I've rarely seen an API interface so intuitive and easy-to-use in my career)

from tapo.

ClementNerma avatar ClementNerma commented on May 27, 2024

I don't think the header should be used as it's not a key we use on every request ; it's a password used to authenticate and create a session ID. The password itself is only used on the login route.

I didn't know about config-rs, seems nice! It'll save me some time instead of reinventing the wheel each time ^^

For endpoints I think it's a good idea, I'll see how I can implement that properly :)

from tapo.

ClementNerma avatar ClementNerma commented on May 27, 2024

I tried rewriting the whole API routing stuff, and ended up with a huge macro that allows to write a pretty elegant route system:

routes! {
    L530 {
        async fn on(state: #State, client: #Client) -> () {
            client.on().await.map_err(tapo_api_err)
        }

        async fn off(state: #State, client: #Client) -> () {
            client.off().await.map_err(tapo_api_err)
        }

        async fn set_brightness(state: #State, client: #Client, level: u8) -> () {
            client.set_brightness(level).await.map_err(tapo_api_err)
        }

        async fn set_color(state: #State, client: #Client, color: tapo::requests::Color) -> () {
            client.set_color(color).await.map_err(tapo_api_err)
        }
    }
}

Which compiles to a module exposing a make_router() -> Router<State> function populated with all routes from all devices, hierarchised.

State management, query parameters deserialization and session handling are automatically performed by the macro.

from tapo.

mihai-dinculescu avatar mihai-dinculescu commented on May 27, 2024

I don't think the header should be used as it's not a key we use on every request ; it's a password used to authenticate and create a session ID. The password itself is only used on the login route.

Yeah, when you put it like that, it does make sense :)

I just tried the API, and I love where it's going!

Would it be possible to make the routes dynamic by the configured devices instead of type? E.g.
/devices/living-room-bulb/on
instead of
/l530/off?device=living-room-bulb

The server already knows the name and the type of the device. It would be easier for the client to provide only the device name instead of both name and type.

from tapo.

ClementNerma avatar ClementNerma commented on May 27, 2024

I already thought about that but the problem is that the API would have dynamic routes, which isn't a good thing.

With the current API schema, we can generate e.g. a Swagger definition and expose all existing routes, which 1) allows to see all available routes at once and 2) see which routes are available for which device type

Even though having the device name as a query parameter is not the most elegant thing, I think it's the best thing to do, especially given action parameters wil also be in the query params.

from tapo.

mihai-dinculescu avatar mihai-dinculescu commented on May 27, 2024

I see what you mean. Swagger could be generated for the dynamic routes, but it will probably become quite involved and messy. 👍

from tapo.

ClementNerma avatar ClementNerma commented on May 27, 2024

I added support for new device types: L510, L610, L630, L900, L920 and L930.
Action routes are now nested under the /actions prefix.

from tapo.

ClementNerma avatar ClementNerma commented on May 27, 2024

I've added support for all remaining device types: P100, P105, P110, P115. All methods for all devices are now implemented.
After thinking about it, I don't think a Dockerfile would be very useful. The API can be built into a standalone executable, you just have to download the binary and run it, there's no external dependency.

from tapo.

ClementNerma avatar ClementNerma commented on May 27, 2024

I've added a README, @WeeSee could you tell me if this would fill your needs?

https://github.com/ClementNerma/tapo-rest

from tapo.

mihai-dinculescu avatar mihai-dinculescu commented on May 27, 2024

Brilliant. I'll have a look one of these days once I'm done with the super secret work of adding support for H100 and its sensors :)

I think the Dockerfile is helpful for people that want to write the config file and docker run the API on an RPI to be interacted with by HA, Node-RED, microcontrollers, etc.

from tapo.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.