Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement warning messages for invalid characters in names #1009

Merged
merged 6 commits into from
Jun 23, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "hap-nodejs",
"version": "0.11.1",
"version": "0.11.2",
NorthernMan54 marked this conversation as resolved.
Show resolved Hide resolved
"description": "HAP-NodeJS is a Node.js implementation of HomeKit Accessory Server.",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
197 changes: 197 additions & 0 deletions src/lib/Accessory.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,203 @@ describe("Accessory", () => {
});
});

describe("Accessory and Service naming checks", () => {
let consoleLogSpy: jest.SpyInstance;

beforeEach(() => {
consoleLogSpy = jest.spyOn(console, "warn");
});

afterEach(() => {
consoleLogSpy.mockRestore();
});

test("Accessory Name ending with !", async () => {
const accessoryBadName = new Accessory("Bad Name!",uuid.generate("Bad Name"));

const publishInfo: PublishInfo = {
username: serverUsername,
pincode: "000-00-000",
category: Categories.SWITCH,
advertiser: undefined,
};

await accessoryBadName.publish(publishInfo);
// eslint-disable-next-line max-len
expect(consoleLogSpy).toHaveBeenCalledWith("HAP-NodeJS WARNING: The accessory 'Bad Name! 7430' is getting published with the characteristic 'Name' not following HomeKit naming rules ('Bad Name! 7430'). Use only alphanumeric, space, and apostrophe characters, start and end with an alphabetic or numeric character, and don't include emojis. This might prevent the accessory from being added to the Home App or leading to the accessory being unresponsive!");
NorthernMan54 marked this conversation as resolved.
Show resolved Hide resolved

await awaitEventOnce(accessoryBadName, AccessoryEventTypes.ADVERTISED);
await accessoryBadName?.unpublish();
await accessoryBadName?.destroy();
});

test("Accessory Name containing !", async () => {
const accessoryBadName = new Accessory("Bad ! Name",uuid.generate("Bad Name"));

const publishInfo: PublishInfo = {
username: serverUsername,
pincode: "000-00-000",
category: Categories.SWITCH,
advertiser: undefined,
};

await accessoryBadName.publish(publishInfo);
// eslint-disable-next-line max-len
expect(consoleLogSpy).toHaveBeenCalledWith("HAP-NodeJS WARNING: The accessory 'Bad ! Name 7430' is getting published with the characteristic 'Name' not following HomeKit naming rules ('Bad ! Name 7430'). Use only alphanumeric, space, and apostrophe characters, start and end with an alphabetic or numeric character, and don't include emojis. This might prevent the accessory from being added to the Home App or leading to the accessory being unresponsive!");

await awaitEventOnce(accessoryBadName, AccessoryEventTypes.ADVERTISED);
await accessoryBadName?.unpublish();
await accessoryBadName?.destroy();
});

test("Accessory Name containing '", async () => {
const accessoryBadName = new Accessory("Bad ' Name",uuid.generate("Bad ' Name"));

const publishInfo: PublishInfo = {
username: serverUsername,
pincode: "000-00-000",
category: Categories.SWITCH,
advertiser: undefined,
};

await accessoryBadName.publish(publishInfo);
expect(consoleLogSpy).toBeCalledTimes(0);

await awaitEventOnce(accessoryBadName, AccessoryEventTypes.ADVERTISED);
await accessoryBadName?.unpublish();
await accessoryBadName?.destroy();
});

test("Accessory Name starting with '", async () => {
const accessoryBadName = new Accessory("'Bad Name",uuid.generate("Bad Name'"));

const publishInfo: PublishInfo = {
username: serverUsername,
pincode: "000-00-000",
category: Categories.SWITCH,
advertiser: undefined,
};

await accessoryBadName.publish(publishInfo);
expect(accessoryBadName.displayName.startsWith(TEST_DISPLAY_NAME));
expect(consoleLogSpy).toBeCalledTimes(2);
// eslint-disable-next-line max-len
expect(consoleLogSpy).toHaveBeenCalledWith("HAP-NodeJS WARNING: The accessory ''Bad Name 7430' is getting published with the characteristic 'Name' not following HomeKit naming rules (''Bad Name 7430'). Use only alphanumeric, space, and apostrophe characters, start and end with an alphabetic or numeric character, and don't include emojis. This might prevent the accessory from being added to the Home App or leading to the accessory being unresponsive!");

await awaitEventOnce(accessoryBadName, AccessoryEventTypes.ADVERTISED);
await accessoryBadName?.unpublish();
await accessoryBadName?.destroy();
});

test("Service Name containing !", async () => {
const switchService = new Service.Switch("My Bad ! Switch");
const accessoryBadName = new Accessory("Bad Name",uuid.generate("Bad Name"));
accessoryBadName.addService(switchService);

const publishInfo: PublishInfo = {
username: serverUsername,
pincode: "000-00-000",
category: Categories.SWITCH,
advertiser: undefined,
};

await accessoryBadName.publish(publishInfo);
// eslint-disable-next-line max-len
expect(consoleLogSpy).toHaveBeenCalledWith("HAP-NodeJS WARNING: The service 'My Bad ! Switch' is getting published with the characteristic 'Name' not following HomeKit naming rules ('My Bad ! Switch'). Use only alphanumeric, space, and apostrophe characters, start and end with an alphabetic or numeric character, and don't include emojis. This might prevent the accessory from being added to the Home App or leading to the accessory being unresponsive!");

await awaitEventOnce(accessoryBadName, AccessoryEventTypes.ADVERTISED);
await accessoryBadName?.unpublish();
await accessoryBadName?.destroy();
});

test("Service Name ending with !", async () => {
const switchService = new Service.Switch("My Bad Switch!");
const accessoryBadName = new Accessory("Bad Name",uuid.generate("Bad Name"));
accessoryBadName.addService(switchService);

const publishInfo: PublishInfo = {
username: serverUsername,
pincode: "000-00-000",
category: Categories.SWITCH,
advertiser: undefined,
};

await accessoryBadName.publish(publishInfo);
expect(consoleLogSpy).toBeCalledTimes(1);
// eslint-disable-next-line max-len
expect(consoleLogSpy).toHaveBeenCalledWith("HAP-NodeJS WARNING: The service 'My Bad Switch!' is getting published with the characteristic 'Name' not following HomeKit naming rules ('My Bad Switch!'). Use only alphanumeric, space, and apostrophe characters, start and end with an alphabetic or numeric character, and don't include emojis. This might prevent the accessory from being added to the Home App or leading to the accessory being unresponsive!");

await awaitEventOnce(accessoryBadName, AccessoryEventTypes.ADVERTISED);
await accessoryBadName?.unpublish();
await accessoryBadName?.destroy();
});

test("Service Name containing '", async () => {
const switchService = new Service.Switch("My Bad ' Switch");
const accessoryBadName = new Accessory("Bad Name",uuid.generate("Bad Name"));
accessoryBadName.addService(switchService);

const publishInfo: PublishInfo = {
username: serverUsername,
pincode: "000-00-000",
category: Categories.SWITCH,
advertiser: undefined,
};

await accessoryBadName.publish(publishInfo);
expect(consoleLogSpy).toBeCalledTimes(0);

await awaitEventOnce(accessoryBadName, AccessoryEventTypes.ADVERTISED);
await accessoryBadName?.unpublish();
await accessoryBadName?.destroy();
});

test("Service Name ending with '", async () => {
const switchService = new Service.Switch("My Bad Switch'");
const accessoryBadName = new Accessory("Bad Name",uuid.generate("Bad Name"));
accessoryBadName.addService(switchService);

const publishInfo: PublishInfo = {
username: serverUsername,
pincode: "000-00-000",
category: Categories.SWITCH,
advertiser: undefined,
};

await accessoryBadName.publish(publishInfo);
expect(consoleLogSpy).toBeCalledTimes(1);
// eslint-disable-next-line max-len
expect(consoleLogSpy).toHaveBeenCalledWith("HAP-NodeJS WARNING: The service 'My Bad Switch'' is getting published with the characteristic 'Name' not following HomeKit naming rules ('My Bad Switch''). Use only alphanumeric, space, and apostrophe characters, start and end with an alphabetic or numeric character, and don't include emojis. This might prevent the accessory from being added to the Home App or leading to the accessory being unresponsive!");

await awaitEventOnce(accessoryBadName, AccessoryEventTypes.ADVERTISED);
await accessoryBadName?.unpublish();
await accessoryBadName?.destroy();
});

test("Service Name beginning with '", async () => {
const switchService = new Service.Switch("'My Bad Switch");
const accessoryBadName = new Accessory("Bad Name",uuid.generate("Bad Name"));
accessoryBadName.addService(switchService);

const publishInfo: PublishInfo = {
username: serverUsername,
pincode: "000-00-000",
category: Categories.SWITCH,
advertiser: undefined,
};

await accessoryBadName.publish(publishInfo);
expect(consoleLogSpy).toBeCalledTimes(1);
// eslint-disable-next-line max-len
expect(consoleLogSpy).toHaveBeenCalledWith("HAP-NodeJS WARNING: The service ''My Bad Switch' is getting published with the characteristic 'Name' not following HomeKit naming rules (''My Bad Switch'). Use only alphanumeric, space, and apostrophe characters, start and end with an alphabetic or numeric character, and don't include emojis. This might prevent the accessory from being added to the Home App or leading to the accessory being unresponsive!");

await awaitEventOnce(accessoryBadName, AccessoryEventTypes.ADVERTISED);
await accessoryBadName?.unpublish();
await accessoryBadName?.destroy();
});

});

NorthernMan54 marked this conversation as resolved.
Show resolved Hide resolved
describe("pairing", () => {
let defaultPairingInfo: PairingInformation;

Expand Down
23 changes: 23 additions & 0 deletions src/lib/Accessory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,7 @@ export class Accessory extends EventEmitter {
"valid UUID from any arbitrary string, like a serial number.");

// create our initial "Accessory Information" Service that all Accessories are expected to have
this.checkName("name", displayName);
this.addService(Service.AccessoryInformation)
.setCharacteristic(Characteristic.Name, displayName);

Expand Down Expand Up @@ -1019,6 +1020,25 @@ export class Accessory extends EventEmitter {
return this._setupURI;
}

/**
* Checks that supplied field meets Apple HomeKit naming rules
* https://developer.apple.com/design/human-interface-guidelines/homekit#Help-people-choose-useful-names
* @private Private API
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private checkName(name: string, value?: any) {
const validHK = /^[a-zA-Z0-9\s'-.]+$/; // Ensure only letter, numbers, apostrophe, or dash
hjdhjd marked this conversation as resolved.
Show resolved Hide resolved
const startWith = /^[a-zA-Z0-9]/; // Ensure only letters or numbers are at the beginning of string
const endWith = /[a-zA-Z0-9]$/; // Ensure only letters or numbers are at the end of string

if (!validHK.test(value) || !startWith.test(value) || !endWith.test(value)) {
NorthernMan54 marked this conversation as resolved.
Show resolved Hide resolved
console.warn("HAP-NodeJS WARNING: The accessory '" + this.displayName + "' is getting published with the characteristic '" +
name + "'" + " not following HomeKit naming rules ('" + value + "'). " +
"Use only alphanumeric, space, and apostrophe characters, start and end with an alphabetic or numeric character, and don't include emojis. " +
"This might prevent the accessory from being added to the Home App or leading to the accessory being unresponsive!");
}
}

/**
* This method is called right before the accessory is published. It should be used to check for common
* mistakes in Accessory structured, which may lead to HomeKit rejecting the accessory when pairing.
Expand All @@ -1043,11 +1063,14 @@ export class Accessory extends EventEmitter {
const serialNumber = service.getCharacteristic(Characteristic.SerialNumber).value;
const firmwareRevision = service.getCharacteristic(Characteristic.FirmwareRevision).value;
const name = service.getCharacteristic(Characteristic.Name).value;
const manufacturer = service.getCharacteristic(Characteristic.Manufacturer).value;

checkValue("Model", model);
checkValue("SerialNumber", serialNumber);
checkValue("FirmwareRevision", firmwareRevision);
checkValue("Name", name);
this.checkName("Name", name);
this.checkName("Manufacturer", manufacturer);
}

if (mainAccessory) {
Expand Down
21 changes: 21 additions & 0 deletions src/lib/Service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,7 @@ export class Service extends EventEmitter {
// if you don't provide a display name, some HomeKit apps may choose to hide the device.
if (displayName) {
// create the characteristic if necessary
this.checkName("Name", displayName);
const nameCharacteristic =
this.getCharacteristic(Characteristic.Name) ||
this.addCharacteristic(Characteristic.Name);
Expand All @@ -559,6 +560,26 @@ export class Service extends EventEmitter {
}
}

/**
* Checks that supplied field meets Apple HomeKit naming rules
* https://developer.apple.com/design/human-interface-guidelines/homekit#Help-people-choose-useful-names
* @private Private API
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private checkName(name: string, value?: any) {
NorthernMan54 marked this conversation as resolved.
Show resolved Hide resolved
const validHK = /^[a-zA-Z0-9\s'-.]+$/; // Ensure only letter, numbers, apostrophe, or dash
const startWith = /^[a-zA-Z0-9]/; // Ensure only letters or numbers are at the beginning of string
const endWith = /[a-zA-Z0-9]$/; // Ensure only letters or numbers are at the end of string

if (!validHK.test(value) || !startWith.test(value) || !endWith.test(value)) {
console.warn("HAP-NodeJS WARNING: The service '" + this.displayName + "' is getting published with the characteristic '" +
name + "'" + " not following HomeKit naming rules ('" + value + "'). " +
"Use only alphanumeric, space, and apostrophe characters, start and end with an alphabetic or numeric character, and don't include emojis. " +
"This might prevent the accessory from being added to the Home App or leading to the accessory being unresponsive!");
}
}


/**
* Returns an id which uniquely identifies a service on the associated accessory.
* The serviceId is a concatenation of the UUID for the service (defined by HAP) and the subtype (could be empty)
Expand Down