401 lines
8 KiB
D
401 lines
8 KiB
D
module app;
|
||
|
||
import dfanotify;
|
||
import fanotify;
|
||
import depoll;
|
||
|
||
import core.sys.posix.fcntl : O_RDONLY, O_LARGEFILE, O_CLOEXEC, AT_FDCWD;
|
||
import core.sys.posix.unistd : read, close;
|
||
import core.sys.posix.sys.stat : fstat, stat_t;
|
||
import core.stdc.errno : errno, EINTR;
|
||
import core.stdc.string : strerror;
|
||
import std.stdio : writeln, writefln, stderr;
|
||
import std.file : isDir, readText, exists, read;
|
||
import std.string : strip, splitLines, startsWith, toStringz, fromStringz, split;
|
||
import std.conv : to;
|
||
import std.exception : enforce;
|
||
import std.getopt : getopt, defaultGetoptPrinter;
|
||
import std.format : format;
|
||
import core.sys.linux.epoll : EPOLLIN, EPOLLERR, EPOLLHUP, EPOLLRDHUP, EPOLL_CLOEXEC;
|
||
|
||
// Переписать
|
||
import core.sys.posix.sys.types : uid_t;
|
||
import core.sys.posix.pwd : passwd, getpwuid_r;
|
||
import core.stdc.stdlib : malloc, free;
|
||
import core.stdc.string : strlen;
|
||
|
||
/// ---- syscall + pidfd_open ----
|
||
extern (C) long syscall(long number, ...);
|
||
version (X86_64) enum __NR_pidfd_open = 434;
|
||
extern (C) int pidfd_open(int pid, uint flags)
|
||
{
|
||
return cast(int) syscall(__NR_pidfd_open, pid, flags);
|
||
}
|
||
|
||
/// ---- readlink по fd ----
|
||
extern (C) long readlink(const char* path, char* buf, size_t bufsiz);
|
||
string readlinkFdPath(int fd)
|
||
{
|
||
char[4_096] buf;
|
||
auto link = format("/proc/self/fd/%d", fd);
|
||
auto n = readlink(link.toStringz, buf.ptr, buf.length.to!int - 1);
|
||
if (n <= 0)
|
||
return "";
|
||
buf[n] = 0;
|
||
return buf.ptr.fromStringz.idup.strip;
|
||
}
|
||
|
||
/// ---- имя процесса и UIDы (ruid из status, uid из loginuid) ----
|
||
struct ProcIds
|
||
{
|
||
uint ruid;
|
||
uint uid;
|
||
} // uid = loginuid
|
||
|
||
string readProcComm(int pid)
|
||
{
|
||
auto p = format("/proc/%s/comm", pid);
|
||
string s;
|
||
try
|
||
{
|
||
s = readText(p);
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
return "";
|
||
}
|
||
return s.strip;
|
||
}
|
||
|
||
uint readLoginUid(int pid)
|
||
{
|
||
auto p = format("/proc/%s/loginuid", pid);
|
||
try
|
||
{
|
||
auto s = readText(p).strip;
|
||
// loginuid может быть "-1" (unset). В этом случае вернём 4294967295 или 0 — выбери политику.
|
||
if (s.length && s[0] == '-')
|
||
return uint.max; // помечаем как "нет"
|
||
return s.to!uint;
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
return uint.max; // нет файла или отказ — считаем не установленным
|
||
}
|
||
}
|
||
|
||
ProcIds readProcIds(int pid)
|
||
{
|
||
uint ruid = 0;
|
||
auto p = format("/proc/%s/status", pid);
|
||
string s;
|
||
try
|
||
{
|
||
s = readText(p);
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
return ProcIds(0, readLoginUid(pid));
|
||
}
|
||
foreach (line; s.splitLines)
|
||
{
|
||
if (line.startsWith("Uid:"))
|
||
{
|
||
// Uid: RUID EUID SUID FSUID
|
||
auto parts = line.split;
|
||
if (parts.length >= 5)
|
||
{
|
||
ruid = parts[1].to!uint;
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
return ProcIds(ruid, readLoginUid(pid));
|
||
}
|
||
|
||
/// ---- ключ карты соответствий: (pid, dev, ino) ----
|
||
struct DevIno
|
||
{
|
||
ulong dev;
|
||
ulong ino;
|
||
int pid;
|
||
bool opEquals(const DevIno rhs) const @safe nothrow
|
||
{
|
||
return dev == rhs.dev && ino == rhs.ino && pid == rhs.pid;
|
||
}
|
||
|
||
size_t toHash() const @safe nothrow
|
||
{
|
||
return (dev * 1_315_423_911UL) ^ (ino * 2_654_435_761UL) ^ (cast(size_t) pid);
|
||
}
|
||
}
|
||
|
||
bool getDevIno(int fd, out DevIno di, int pid)
|
||
{
|
||
stat_t st;
|
||
if (fstat(fd, &st) != 0)
|
||
return false;
|
||
di = DevIno(cast(ulong) st.st_dev, cast(ulong) st.st_ino, pid);
|
||
return true;
|
||
}
|
||
|
||
/// ---- сессия файла для конкретного PID ----
|
||
struct Session
|
||
{
|
||
string procName;
|
||
uint ruid; // из /proc/PID/status (Uid:)
|
||
uint uid; // LOGINUID из /proc/PID/loginuid
|
||
string filePathOpen; // путь, снятый на PERM
|
||
bool changed; // был CLOSE_WRITE
|
||
const(ubyte)[] fileBytesSource;
|
||
const(ubyte)[] fileBytesDiff;
|
||
}
|
||
|
||
__gshared Session[DevIno] gMap; // (pid,dev,ino) -> Session
|
||
__gshared int[int] gPidfdByPid; // pid -> pidfd
|
||
|
||
private string userName(uid_t uid)
|
||
{
|
||
enum BUF = 4096;
|
||
auto buf = cast(char*) malloc(BUF);
|
||
scope (exit)
|
||
if (buf)
|
||
free(buf);
|
||
passwd pwd;
|
||
passwd* outp;
|
||
auto rc = getpwuid_r(uid, &pwd, buf, BUF, &outp);
|
||
if (rc == 0 && outp !is null)
|
||
{
|
||
return pwd.pw_name[0 .. strlen(pwd.pw_name)].idup;
|
||
}
|
||
return "";
|
||
}
|
||
|
||
/// ---- журнал: proc, file, uid(loginuid), ruid ----
|
||
void logChange(string procName, string filePath, uint uid, uint ruid)
|
||
{
|
||
writefln(`proc="%s" file="%s" user=%s(%s) realUser=%s(%s)`,
|
||
procName, filePath, userName(uid), uid, userName(ruid), ruid);
|
||
}
|
||
|
||
/// ---- маска на КАТАЛОГ ----
|
||
ulong dirMask()
|
||
{
|
||
return FAN_EVENT_ON_CHILD |
|
||
FAN_OPEN_PERM | FAN_OPEN_EXEC_PERM | FAN_ACCESS_PERM |
|
||
FAN_OPEN | FAN_CLOSE_WRITE;
|
||
}
|
||
|
||
ubyte[] readAllFromFd(int fd)
|
||
{
|
||
import std.array : appender;
|
||
|
||
ubyte[16_384] buf; // 16 KiB буфер
|
||
auto output = appender!(ubyte[]);
|
||
for (;;)
|
||
{
|
||
auto n = read(fd, buf.ptr, buf.length);
|
||
if (n <= 0)
|
||
break;
|
||
output.put(buf[0 .. n]);
|
||
}
|
||
return output.data;
|
||
}
|
||
|
||
/// ---- обработчики ----
|
||
void onOpenPerm(FanotifyEvent ev, Epoll ep)
|
||
{
|
||
const pid = ev.pid;
|
||
auto procName = readProcComm(pid);
|
||
auto ids = readProcIds(pid);
|
||
|
||
DevIno key;
|
||
if (!getDevIno(ev.eventFd, key, pid))
|
||
{
|
||
ev.respond(FAN_ALLOW);
|
||
return;
|
||
}
|
||
|
||
auto path = readlinkFdPath(ev.eventFd);
|
||
|
||
const(ubyte)[] content;
|
||
try
|
||
{
|
||
if (path.length)
|
||
{
|
||
content = readAllFromFd(ev.eventFd);
|
||
}
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
content = [];
|
||
}
|
||
|
||
if (auto ps = key in gMap)
|
||
{
|
||
// уже есть
|
||
}
|
||
else
|
||
{
|
||
gMap[key] = Session(procName, ids.ruid, ids.uid, path, false, content);
|
||
}
|
||
|
||
if (!(pid in gPidfdByPid))
|
||
{
|
||
int pfd = -1;
|
||
try
|
||
{
|
||
pfd = pidfd_open(pid, 0);
|
||
if (pfd < 0)
|
||
writeln("pidfd_open failed: ", strerror(errno).fromStringz);
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
pfd = -1;
|
||
}
|
||
if (pfd >= 0)
|
||
{
|
||
try
|
||
{
|
||
ep.add(pfd, cast(uint) pid, EPOLLIN | EPOLLHUP | EPOLLRDHUP | EPOLLERR);
|
||
gPidfdByPid[pid] = pfd;
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
}
|
||
}
|
||
}
|
||
|
||
ev.respond(FAN_ALLOW);
|
||
}
|
||
|
||
void onCloseWrite(FanotifyEvent ev)
|
||
{
|
||
DevIno key;
|
||
if (!getDevIno(ev.eventFd, key, ev.pid))
|
||
return;
|
||
if (auto ps = key in gMap)
|
||
{
|
||
ps.changed = true;
|
||
|
||
import sdiff;
|
||
|
||
const(ubyte)[] content;
|
||
try
|
||
{
|
||
if (ps.filePathOpen.length)
|
||
content = readAllFromFd(ev.eventFd);
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
content = [];
|
||
}
|
||
|
||
auto dataSource = new MMFile(ps.fileBytesSource);
|
||
auto dataChange = new MMFile(content);
|
||
|
||
ps.fileBytesDiff = dataSource.diff(dataChange).slice();
|
||
}
|
||
}
|
||
|
||
void flushAndClearForPid(int pid, Epoll ep)
|
||
{
|
||
auto keys = gMap.keys.dup;
|
||
foreach (k; keys)
|
||
{
|
||
if (k.pid != pid)
|
||
continue;
|
||
auto s = gMap[k];
|
||
if (s.changed)
|
||
{
|
||
logChange(s.procName, s.filePathOpen, s.uid, s.ruid);
|
||
writeln(cast(string)s.fileBytesDiff);
|
||
}
|
||
gMap.remove(k);
|
||
}
|
||
if (auto p = pid in gPidfdByPid)
|
||
{
|
||
try
|
||
{
|
||
ep.remove(*p);
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
}
|
||
close(*p);
|
||
gPidfdByPid.remove(pid);
|
||
}
|
||
}
|
||
|
||
/// ---- main ----
|
||
enum uint FAN_TAG = 0;
|
||
|
||
void main(string[] args)
|
||
{
|
||
string watchDir;
|
||
|
||
auto help = getopt(args, "dir|d", &watchDir);
|
||
if (help.helpWanted || watchDir.length == 0)
|
||
{
|
||
defaultGetoptPrinter("Usage: app -d <dir-to-watch>", help.options);
|
||
return;
|
||
}
|
||
enforce(isDir(watchDir), "Ожидается путь к КАТАЛОГУ (-d)");
|
||
|
||
enum initFlags = FAN_CLASS_PRE_CONTENT | FAN_CLOEXEC | FAN_NONBLOCK;
|
||
auto fan = new Fanotify(initFlags, O_RDONLY | O_LARGEFILE | O_CLOEXEC);
|
||
|
||
try
|
||
{
|
||
fan.mark(FAN_MARK_ADD | FAN_MARK_ONLYDIR, dirMask(), AT_FDCWD, watchDir);
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
stderr.writefln("fanotify_mark failed: %s", e.msg);
|
||
return;
|
||
}
|
||
|
||
auto ep = new Epoll(EPOLL_CLOEXEC);
|
||
ep.add(fan.handle, FAN_TAG, EPOLLIN | EPOLLERR | EPOLLHUP);
|
||
|
||
writeln("watching dir: ", watchDir);
|
||
|
||
for (;;)
|
||
{
|
||
auto evs = ep.wait(256, -1);
|
||
if (evs.length == 0)
|
||
continue;
|
||
|
||
foreach (e; evs)
|
||
{
|
||
if (e.tag == FAN_TAG)
|
||
{
|
||
auto list = fan.readEvents(8_192);
|
||
foreach (fev; list)
|
||
{
|
||
if (fev.isOverflow)
|
||
{
|
||
writeln(
|
||
"FAN_Q_OVERFLOW — возможна потеря корреляции");
|
||
continue;
|
||
}
|
||
if (fev.isOpenPerm || fev.isAccessPerm || fev.isOpenExecPerm)
|
||
{
|
||
onOpenPerm(fev, ep);
|
||
continue;
|
||
}
|
||
if (fev.isCloseWrite)
|
||
{
|
||
onCloseWrite(fev);
|
||
continue;
|
||
}
|
||
}
|
||
}
|
||
else
|
||
{
|
||
auto pid = cast(int) e.tag;
|
||
flushAndClearForPid(pid, ep);
|
||
}
|
||
}
|
||
}
|
||
}
|