mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
212 lines
5.7 KiB
JavaScript
212 lines
5.7 KiB
JavaScript
import { writeFileSync, unlinkSync, statSync, readFileSync } from 'fs';
|
|
import { createServer } from 'net';
|
|
import { createServer as createServerHttp } from 'http';
|
|
import {
|
|
removeExistingSocketFile,
|
|
registerSocketFileCleanup,
|
|
} from './socket_file.js';
|
|
import { EventEmitter } from 'events';
|
|
import { tmpdir, userInfo, platform } from 'os';
|
|
import { main, getGroupInfo } from './webapp_server';
|
|
import express from 'express';
|
|
|
|
const testSocketFile = `${tmpdir()}/socket_file_tests`;
|
|
|
|
const getChownInfo = async (filePath) => {
|
|
try {
|
|
const stats = await statSync(filePath);
|
|
return { uid: stats.uid, gid: stats.gid };
|
|
} catch (error) {
|
|
console.error(`Error fetching ownership info for ${filePath}:`, error.message);
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const isMacOS = () => {
|
|
return platform() === 'darwin';
|
|
};
|
|
|
|
const getGroupNameForGid = (gid) => {
|
|
try {
|
|
const data = readFileSync('/etc/group', 'utf8');
|
|
const line = data
|
|
.trim()
|
|
.split('\n')
|
|
.find((groupLine) => {
|
|
const [, , groupGid] = groupLine.trim().split(':');
|
|
return Number(groupGid) === gid;
|
|
});
|
|
|
|
if (!line) return null;
|
|
const [name] = line.trim().split(':');
|
|
return name || null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const getWritableGroupName = () => {
|
|
const { gid, uid } = userInfo();
|
|
const gidsToTry = new Set();
|
|
|
|
if (typeof gid === 'number') {
|
|
gidsToTry.add(gid);
|
|
}
|
|
|
|
if (typeof process.getgroups === 'function') {
|
|
process.getgroups().forEach((groupId) => gidsToTry.add(groupId));
|
|
}
|
|
|
|
for (const groupId of gidsToTry) {
|
|
const groupName = getGroupNameForGid(groupId);
|
|
if (groupName) {
|
|
return groupName;
|
|
}
|
|
}
|
|
|
|
if (Boolean(process.env.TRAVIS)) return 'travis';
|
|
if (isMacOS()) return 'staff';
|
|
return uid === 0 ? 'root' : null;
|
|
};
|
|
|
|
const removeTestSocketFile = () => {
|
|
try {
|
|
unlinkSync(testSocketFile);
|
|
} catch (error) {
|
|
// Do nothing
|
|
}
|
|
}
|
|
|
|
Tinytest.add("socket file - don't remove a non-socket file", test => {
|
|
writeFileSync(testSocketFile, "");
|
|
test.throws(
|
|
() => { removeExistingSocketFile(testSocketFile); },
|
|
/An existing file was found/
|
|
);
|
|
removeTestSocketFile()
|
|
});
|
|
|
|
Tinytest.addAsync(
|
|
'socket file - remove a previously existing socket file',
|
|
(test, done) => {
|
|
removeTestSocketFile();
|
|
const server = createServer();
|
|
server.listen(testSocketFile);
|
|
|
|
server.on('listening', Meteor.bindEnvironment(() => {
|
|
test.isNotUndefined(statSync(testSocketFile));
|
|
removeExistingSocketFile(testSocketFile);
|
|
test.throws(
|
|
() => { statSync(testSocketFile); },
|
|
/ENOENT/
|
|
);
|
|
server.close();
|
|
done();
|
|
}));
|
|
}
|
|
);
|
|
|
|
Tinytest.add(
|
|
'socket file - no existing socket file, nothing to remove',
|
|
test => {
|
|
removeTestSocketFile();
|
|
removeExistingSocketFile(testSocketFile);
|
|
}
|
|
);
|
|
|
|
Tinytest.add('socket file - remove socket file on exit', test => {
|
|
const testEventEmitter = new EventEmitter();
|
|
registerSocketFileCleanup(testSocketFile, testEventEmitter);
|
|
['exit', 'SIGINT', 'SIGHUP', 'SIGTERM'].forEach(signal => {
|
|
writeFileSync(testSocketFile, "");
|
|
test.isNotUndefined(statSync(testSocketFile));
|
|
testEventEmitter.emit(signal);
|
|
test.throws(
|
|
() => { statSync(testSocketFile); },
|
|
/ENOENT/
|
|
);
|
|
});
|
|
});
|
|
|
|
function prepareServer() {
|
|
removeTestSocketFile();
|
|
removeExistingSocketFile(testSocketFile);
|
|
const testEventEmitter = new EventEmitter();
|
|
registerSocketFileCleanup(testSocketFile, testEventEmitter);
|
|
const server = createServer();
|
|
server.listen(testSocketFile);
|
|
const app = express();
|
|
const httpServer = createServerHttp(app);
|
|
return { httpServer, server };
|
|
}
|
|
|
|
function closeServer({ httpServer, server }) {
|
|
return new Promise((resolve) => {
|
|
httpServer.on(
|
|
"listening",
|
|
Meteor.bindEnvironment(() => {
|
|
process.env.UNIX_SOCKET_PATH = "";
|
|
process.env.UNIX_SOCKET_GROUP = "";
|
|
removeExistingSocketFile(testSocketFile);
|
|
server.close();
|
|
httpServer.close();
|
|
resolve();
|
|
})
|
|
);
|
|
});
|
|
}
|
|
|
|
testAsyncMulti(
|
|
"socket usage - use socket file for inter-process communication",
|
|
[
|
|
async (test) => {
|
|
// use UNIX_SOCKET_PATH
|
|
const { httpServer, server } = prepareServer();
|
|
|
|
process.env.UNIX_SOCKET_PATH = testSocketFile;
|
|
const result = await main({ httpServer });
|
|
|
|
const currentGid = userInfo({ encoding: "utf8" })?.gid;
|
|
test.equal((await getChownInfo(testSocketFile))?.gid, currentGid);
|
|
|
|
return closeServer({ httpServer, server });
|
|
},
|
|
async (test) => {
|
|
const isLinux = platform() === 'linux';
|
|
const isTravis = Boolean(process.env.TRAVIS);
|
|
|
|
if (isLinux && !isTravis) {
|
|
/*
|
|
* Local Linux developers usually run Meteor as an unprivileged user.
|
|
* Changing the socket file's group to "root" would require elevated
|
|
* permissions, so we skip this assertion outside CI to avoid forcing
|
|
* sudo usage. The behavior is still verified on macOS and in CI.
|
|
*/
|
|
test.ok();
|
|
return;
|
|
}
|
|
|
|
// use UNIX_SOCKET_PATH and UNIX_SOCKET_GROUP
|
|
const groupToUse = getWritableGroupName();
|
|
|
|
if (!groupToUse) {
|
|
// Skip when no writable group could be determined for the current user.
|
|
test.isTrue(true);
|
|
return;
|
|
}
|
|
|
|
const { httpServer, server } = prepareServer();
|
|
|
|
process.env.UNIX_SOCKET_PATH = testSocketFile;
|
|
process.env.UNIX_SOCKET_GROUP = groupToUse;
|
|
process.env.UNIX_SOCKET_PERMISSIONS = '777';
|
|
const result = await main({ httpServer });
|
|
|
|
test.equal(result, "DAEMON");
|
|
test.equal((await getChownInfo(testSocketFile))?.gid, getGroupInfo(process.env.UNIX_SOCKET_GROUP)?.gid);
|
|
|
|
return closeServer({ httpServer, server });
|
|
},
|
|
]
|
|
);
|