alexryd / homebridge-shelly-ng

Homebridge plugin for the next generation of Shelly devices
GNU General Public License v3.0
124 stars 47 forks source link

Authentication #3

Closed jmaferreira closed 2 years ago

jmaferreira commented 2 years ago

Dear @alexryd,

If you ever consider developing support for authentication on the PRO Series, this bit of code might help. It comes from Shelly's own technical team, so it should work fine.

const http = require("http");
const crypto = require("crypto");

const username = "admin"; // always
const password = "secretpassword";

const postData = {
  id: 1,
  method: "Shelly.GetStatus",
};

const options = {
  hostname: "192.168.2.16",
  port: 80,
  path: "/rpc",
  method: "POST",
  headers: {
    "Content-Type": "application/json",
  },
};

const __sha256ToHex = async (str) => {
  return crypto.createHash("sha256").update(str).digest("hex");
};

const _getAuthResponse = async (authParams, pass = null) => {
  let _respAuthParams = { ...authParams };
  let _shaUserToken = "";
  if (pass !== null) {
    let _userTokenStr = username + ":" + _respAuthParams.realm + ":" + pass;
    _shaUserToken = await __sha256ToHex(_userTokenStr);
  }
  let _shaMethod = await __sha256ToHex("dummy_method:dummy_uri");

  _respAuthParams.username = username;
  _respAuthParams.nonce = parseInt(_respAuthParams.nonce, 16);
  _respAuthParams.cnonce = Math.floor(Math.random() * Math.pow(10, 8));

  let _respArray = [];
  _respArray.push(_shaUserToken);
  _respArray.push(_respAuthParams.nonce.toString());
  _respArray.push("1");
  _respArray.push(_respAuthParams.cnonce.toString());
  _respArray.push("auth");
  _respArray.push(_shaMethod);
  _respAuthParams.response = await __sha256ToHex(_respArray.join(":"));
  return _respAuthParams;
};

const shellyHttpCall = async (options, postdata) => {
  return new Promise((resolve, reject) => {
    options.headers["Content-Length"] = Buffer.byteLength(
      JSON.stringify(postdata)
    );
    const req = http.request(options, async (res) => {
      let buffer = new Buffer.alloc(0);
      if (res.statusCode == 401) {
        let _challengeAuthParams = {};
        let _auth_header_params = res.headers["www-authenticate"]
          .replace(/\"/g, "")
          .split(", ");
        for (_param of _auth_header_params) {
          let [_key, _value] = _param.split("=");
          _challengeAuthParams[_key] = _value;
        }
        let _authParams = await _getAuthResponse(
          _challengeAuthParams,
          password
        );
        return resolve(
          shellyHttpCall(options, { ...postdata, auth: _authParams })
        );
      }
      res.on("data", (chunk) => {
        buffer = Buffer.concat([buffer, chunk]);
      });
      res.on("end", () => {
        return resolve(buffer.toString("utf8"));
      });
    });
    req.write(JSON.stringify(postdata));
    req.end();
  });
};

shellyHttpCall(options, postData)
  .then((data) => {
    console.log("Device response: ", data);
  })
  .catch((err) => {
    console.log("Request failed");
  });
alexryd commented 2 years ago

Thanks @jmaferreira. I'm planning on adding authentication support soon. I've seen this example which looks very similar.

alexryd commented 2 years ago

Authentication is now supported in the latest update.