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

Handle non-dev mode paths in elevate-service.ts #173

Merged
merged 2 commits into from
Mar 9, 2024
Merged
Show file tree
Hide file tree
Changes from all 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-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@
},
"dependencies": {
"bluebird": "^3.7.2",
"core-js": "^3.12.1",
"core-js": "^3.36.0",
"dompurify": "^2.2.8",
"node-fetch": "^2.6.1 <2.6.8",
"progress-stream": "^2.0.0",
Expand Down
242 changes: 154 additions & 88 deletions services/elevate-service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
#!/usr/bin/env node

/* Array.prototype.includes() only available from around Node.js v6. */
import 'core-js/es/array/includes';

import { existsSync, statSync, readFileSync, writeFileSync } from 'fs';
import { execFile } from 'child_process';
import { dirname, resolve } from 'path';
Expand All @@ -9,15 +12,15 @@ process.env['PATH'] = `/usr/sbin:${process.env['PATH']}`;
function isFile(path: string): boolean {
try {
return statSync(path).isFile();
} catch (err) {
} catch {
return false;
}
}

function parentExists(path: string): boolean {
try {
return statSync(dirname(path)).isDirectory();
} catch (err) {
} catch {
return false;
}
}
Expand All @@ -26,7 +29,7 @@ function patchServiceFile(serviceFile: string): boolean {
const serviceFileOriginal = readFileSync(serviceFile).toString();
let serviceFileNew = serviceFileOriginal;

if (serviceFileNew.indexOf('/run-js-service') !== -1) {
if (serviceFileNew.includes('/run-js-service')) {
console.info(`[ ] ${serviceFile} is a JS service`);

// run-js-service should be in the same directory as this script.
Expand All @@ -38,10 +41,10 @@ function patchServiceFile(serviceFile: string): boolean {
}

serviceFileNew = serviceFileNew.replace(/^Exec=\/usr\/bin\/run-js-service/gm, `Exec=${runJsServicePath}`);
} else if (serviceFileNew.indexOf('/jailer') !== -1) {
} else if (serviceFileNew.includes('/jailer')) {
console.info(`[ ] ${serviceFile} is a native service`);
serviceFileNew = serviceFileNew.replace(/^Exec=\/usr\/bin\/jailer .* ([^ ]*)$/gm, (_, binaryPath) => `Exec=${binaryPath}`);
} else if (serviceFileNew.indexOf('Exec=/media') === -1) {
} else if (!serviceFileNew.includes('Exec=/media')) {
// Ignore elevated native services...
console.info(`[~] ${serviceFile}: unknown service type, this may cause some troubles`);
}
Expand All @@ -56,19 +59,62 @@ function patchServiceFile(serviceFile: string): boolean {
return false;
}

function patchRolesFile(path: string, requiredNames: string[] = ['*', 'com.webos.service.capture.client*']) {
type Permission = {
service: string;
inbound?: string[];
outbound?: string[];
};

type Roles = {
appId: string;
type: string;
allowedNames: string[];
trustLevel: string;
permissions: Permission[];
};

type LegacyRoles = {
role: {
exeName: string;
type: string;
allowedNames: string[];
};
permissions: Permission[];
};

function isRecord(obj: unknown): obj is Record<string, any> {
return typeof obj === 'object' && !Array.isArray(obj);
}

function patchRolesFile(path: string, legacy: boolean = false, requiredNames: string[] = ['*', 'com.webos.service.capture.client*']) {
const rolesOriginal = readFileSync(path).toString();
const rolesNew = JSON.parse(rolesOriginal);
const rolesNew = JSON.parse(rolesOriginal) as Roles | LegacyRoles;

for (const name of requiredNames) {
// webOS <4.x /var/ls2-dev role file
if (rolesNew?.role?.allowedNames && rolesNew?.role?.allowedNames.indexOf(name) === -1) {
rolesNew.role.allowedNames.push(name);
}
let allowedNames: string[] | null = null;

if (legacy) {
// webOS <4.x /var/palm/ls2-dev role file
const legacyRoles = rolesNew as LegacyRoles;
if (isRecord(legacyRoles.role) && Array.isArray(legacyRoles.role.allowedNames)) {
allowedNames = legacyRoles.role.allowedNames;
} else {
console.warn('[!] Legacy roles is missing allowedNames');
}
} else {
// webOS 4.x+ /var/luna-service2 role file
if (rolesNew?.allowedNames && rolesNew?.allowedNames.indexOf(name) === -1) {
rolesNew.allowedNames.push(name);
const newRoles = rolesNew as Roles;
if (Array.isArray(newRoles.allowedNames)) {
allowedNames = newRoles.allowedNames;
} else {
console.warn('[!] Roles file is missing allowedNames');
}
}

if (allowedNames !== null) {
for (const name of requiredNames) {
if (!allowedNames.includes(name)) {
allowedNames.push(name);
}
}
}

Expand All @@ -81,12 +127,12 @@ function patchRolesFile(path: string, requiredNames: string[] = ['*', 'com.webos
// pieces of software verify explicit permission "service" key, thus we
// sometimes may need some extra allowedNames/permissions, even though we
// default to "*"
if (rolesNew.permissions) {
if (Array.isArray(rolesNew.permissions)) {
const missingPermissionNames = requiredNames;
rolesNew.permissions.forEach((perm: { outbound?: string[]; service?: string }) => {
if (perm.service && missingPermissionNames.indexOf(perm.service) !== -1)
if (perm.service && missingPermissionNames.includes(perm.service))
missingPermissionNames.splice(missingPermissionNames.indexOf(perm.service), 1);
if (perm.outbound && perm.outbound.indexOf('*') === -1) {
if (perm.outbound && !perm.outbound.includes('*')) {
perm.outbound.push('*');
}
});
Expand All @@ -113,6 +159,15 @@ function patchRolesFile(path: string, requiredNames: string[] = ['*', 'com.webos
return false;
}

type Manifest = {
version: string;
serviceFiles: string[];
apiPermissionFiles?: string[];
id: string;
roleFiles: string[];
clientPermissionFiles?: string[];
};

function main(argv: string[]) {
let [serviceName = 'org.webosbrew.hbchannel.service', appName = serviceName.split('.').slice(0, -1).join('.')] = argv;

Expand All @@ -123,96 +178,107 @@ function main(argv: string[]) {

let configChanged = false;

const serviceFile = `/var/luna-service2-dev/services.d/${serviceName}.service`;
const clientPermFile = `/var/luna-service2-dev/client-permissions.d/${serviceName}.root.json`;
const apiPermFile = `/var/luna-service2-dev/api-permissions.d/${serviceName}.api.public.json`;
const manifestFile = `/var/luna-service2-dev/manifests.d/${appName}.json`;
const roleFile = `/var/luna-service2-dev/roles.d/${serviceName}.service.json`;
for (const lunaRoot of ['/var/luna-service2-dev', '/var/luna-service2']) {
const serviceFile = `${lunaRoot}/services.d/${serviceName}.service`;
const clientPermFile = `${lunaRoot}/client-permissions.d/${serviceName}.root.json`;
const apiPermFile = `${lunaRoot}/api-permissions.d/${serviceName}.api.public.json`;
const manifestFile = `${lunaRoot}/manifests.d/${appName}.json`;
const roleFile = `${lunaRoot}/roles.d/${serviceName}.service.json`;

if (isFile(serviceFile)) {
console.info(`[~] Found webOS 3.x+ service file: ${serviceFile}`);
if (patchServiceFile(serviceFile)) {
configChanged = true;
}
} else {
// Skip everything else if service file is not found.
continue;
}

if (isFile(serviceFile)) {
console.info(`[~] Found webOS 3.x+ service file: ${serviceFile}`);
if (patchServiceFile(serviceFile)) {
if (parentExists(clientPermFile) && !isFile(clientPermFile)) {
console.info(`[ ] Creating client permissions file: ${clientPermFile}`);
writeFileSync(
clientPermFile,
JSON.stringify({
[`${serviceName}*`]: ['all'],
}),
);
configChanged = true;
}
}

if (parentExists(clientPermFile) && !isFile(clientPermFile)) {
console.info(`[ ] Creating client permissions file: ${clientPermFile}`);
writeFileSync(
clientPermFile,
JSON.stringify({
[`${serviceName}*`]: ['all'],
}),
);
configChanged = true;
}

if (parentExists(apiPermFile) && !isFile(apiPermFile)) {
console.info(`[ ] Creating API permissions file: ${apiPermFile}`);
writeFileSync(
apiPermFile,
JSON.stringify({
public: [`${serviceName}/*`],
}),
);
configChanged = true;
}

if (isFile(roleFile)) {
if (patchRolesFile(roleFile)) {
if (parentExists(apiPermFile) && !isFile(apiPermFile)) {
console.info(`[ ] Creating API permissions file: ${apiPermFile}`);
writeFileSync(
apiPermFile,
JSON.stringify({
public: [`${serviceName}/*`],
}),
);
configChanged = true;
}
}

if (isFile(manifestFile)) {
console.info(`[~] Found webOS 4.x+ manifest file: ${manifestFile}`);
const manifestFileOriginal = readFileSync(manifestFile).toString();
const manifestFileParsed = JSON.parse(manifestFileOriginal);
if (manifestFileParsed.clientPermissionFiles && manifestFileParsed.clientPermissionFiles.indexOf(clientPermFile) === -1) {
console.info('[ ] manifest - adding client permissions file...');
manifestFileParsed.clientPermissionFiles.push(clientPermFile);
if (isFile(roleFile)) {
if (patchRolesFile(roleFile, false)) {
configChanged = true;
}
}

if (manifestFileParsed.apiPermissionFiles && manifestFileParsed.apiPermissionFiles.indexOf(apiPermFile) === -1) {
console.info('[ ] manifest - adding API permissions file...');
manifestFileParsed.apiPermissionFiles.push(apiPermFile);
}
if (isFile(manifestFile)) {
console.info(`[~] Found webOS 4.x+ manifest file: ${manifestFile}`);
const manifestFileOriginal = readFileSync(manifestFile).toString();
const manifest = JSON.parse(manifestFileOriginal) as Manifest;
if (Array.isArray(manifest.clientPermissionFiles) && !manifest.clientPermissionFiles.includes(clientPermFile)) {
console.info('[ ] manifest - adding client permissions file...');
manifest.clientPermissionFiles.push(clientPermFile);
}

const manifestFileNew = JSON.stringify(manifestFileParsed);
if (manifestFileNew !== manifestFileOriginal) {
console.info(`[~] Updating manifest file: ${manifestFile}`);
console.info('-', manifestFileOriginal);
console.info('+', manifestFileNew);
writeFileSync(manifestFile, manifestFileNew);
configChanged = true;
if (Array.isArray(manifest.apiPermissionFiles) && !manifest.apiPermissionFiles.includes(apiPermFile)) {
console.info('[ ] manifest - adding API permissions file...');
manifest.apiPermissionFiles.push(apiPermFile);
}

const manifestFileNew = JSON.stringify(manifest);
if (manifestFileNew !== manifestFileOriginal) {
console.info(`[~] Updating manifest file: ${manifestFile}`);
console.info('-', manifestFileOriginal);
console.info('+', manifestFileNew);
writeFileSync(manifestFile, manifestFileNew);
configChanged = true;
}
}
}

const legacyPubServiceFile = `/var/palm/ls2-dev/services/pub/${serviceName}.service`;
const legacyPrvServiceFile = `/var/palm/ls2-dev/services/prv/${serviceName}.service`;
const legacyPubRolesFile = `/var/palm/ls2-dev/roles/pub/${serviceName}.json`;
const legacyPrvRolesFile = `/var/palm/ls2-dev/roles/prv/${serviceName}.json`;
for (const legacyLunaRoot of ['/var/palm/ls2-dev', '/var/palm/ls2']) {
const legacyPubServiceFile = `${legacyLunaRoot}/services/pub/${serviceName}.service`;
const legacyPrvServiceFile = `${legacyLunaRoot}/services/prv/${serviceName}.service`;
const legacyPubRolesFile = `${legacyLunaRoot}/roles/pub/${serviceName}.json`;
const legacyPrvRolesFile = `${legacyLunaRoot}/roles/prv/${serviceName}.json`;

if (isFile(legacyPubServiceFile)) {
console.info(`[~] Found legacy webOS <3.x service file: ${legacyPubServiceFile}`);
if (patchServiceFile(legacyPubServiceFile)) {
configChanged = true;
}
if (isFile(legacyPubServiceFile)) {
console.info(`[~] Found legacy webOS <3.x service file: ${legacyPubServiceFile}`);
if (patchServiceFile(legacyPubServiceFile)) {
configChanged = true;
}

if (patchServiceFile(legacyPrvServiceFile)) {
configChanged = true;
if (isFile(legacyPrvServiceFile)) {
if (patchServiceFile(legacyPrvServiceFile)) {
configChanged = true;
}
} else {
console.warn(`[!] Did not find legacy private service file: ${legacyPrvServiceFile}`);
}
}
}

if (isFile(legacyPubRolesFile)) {
if (patchRolesFile(legacyPubRolesFile)) {
configChanged = true;
if (isFile(legacyPubRolesFile)) {
if (patchRolesFile(legacyPubRolesFile)) {
configChanged = true;
}
}
}

if (isFile(legacyPrvRolesFile)) {
if (patchRolesFile(legacyPrvRolesFile)) {
configChanged = true;
if (isFile(legacyPrvRolesFile)) {
if (patchRolesFile(legacyPrvRolesFile)) {
configChanged = true;
}
}
}

Expand Down