site infoHacknerd | Tech Blog
blog cover

🔐 提高 Linux 上 Electron 应用的权限:获取管理员权限修改目录读写权限

Node.jsElectron
问题描述: UOS 系统安装 deb 安装包后,发现写入本地缓存文件失败。

原因分析:

没有写入权限。uos 普通用户没有安装程序的权限,执行 dpkg i ${package-name}或者使用自带 package 管理器安装软件包时,需要提供管理员密码,以管理员身份安装( root:root 755)。以至于当前用户拥有读取和运行权限,而没有写入权限。

解决方案:

在运行时请求管理员密码,修改目录写入权限。尝试过调用afterinstallhook, 在安装后自动执行chmod命令,发现会损坏安装包资源,导致客户端无法正常运行。最终决定在运行时请求管理员密码,通过child_process调用 shell 更改用户目录权限。类似于 switchhost 请求密码写入host。

设计思路:

  • 1.浏览器通过 window.electron 的 ipcRenderer render-to-main-message 发送更改目录权限指令,携带要修改的目录。
  • 2.Electron 通过 ipcMain render-to-main-message 接收消息。调用更改权限接口。
  • 3.PrivilegesMain服务文件是否有写入权限。没有,则执行chmod -R 777,并请求管理员密码。
  • 4.Electron 订阅 ipcMain 获取管理员密码消息,通过 ipc 向浏览器发送请求密码指令。
  • 5.浏览器接受指令,唤起输入密码弹窗。点击确定,PrivilegesMain 向 childProcess 通过stdin 写入密码。
  • 6.PrivilegesMain进行密码重试,密码多次输入错误,更改目录权限成功等消息捕获。
  • image

    添加图片注释,不超过 140 字(可选)

    核心代码:

    core.ts。实现功能:

  • 1.唤起子进程
  • 2.执行更改目录权限
  • 3.写入管理员密码
  • 4.发送消息
  • typescriptCopy
    export type EventName = "error" | "success" | "requirePassword";
    
    export class PrivilegesMain {
      /**
       * 消息中心
       */
      public readonly message = new EventBus<EventName>();
      /**
       * 写入密码
       */
      public setPassword: (password: string) => void;
      /**
       * 更改权限
       */
      public changeDirWritePrivileges(dirName: string) {
        /* 是否可写 */
        if (!isDirctoryWritable(dirName)) {
          // 更改目录权限
          const command = `chmod -R 777 ${dirName}`;
          const options = { shell: true, sudo: true };
          const childProcess = spawn("sudo", ["-S"].concat(command.split(" ")), options);
    
          // 向子进程写入管理员密码
          this.setPassword = (password: string) => {
            childProcess.stdin.write(`${password}\n`);
          };
    
          // 监听子进程的标准输出
          childProcess.stdout.on("data", (data: any) => {
            this.message.emit("data", `stdout: ${data}`);
          });
    
          // 监听子进程的异常
          childProcess.stderr.on("data", (data: any) => {
            if (data.toString() === "Sorry, try again.\n") {
              // 密码错误
              this.message.emit("error", <PrivilegeErrorResponse>{
                type: "wrongPassword",
                message: "Sorry, try again.",
              });
            } else if (data.toString() === "sudo: 3 incorrect password attempts\n") {
              // 超出上限
              this.message.emit("error", <PrivilegeErrorResponse>{
                type: "failed",
                message: "3 incorrect password attempts",
              });
            }
          });
    
          // 监听子进程的退出事件
          childProcess.on("exit", (code: any, signal: any) => {
            if (code === 0) {
              this.message.emit("success", "Successfully changed dirctory priveleges!");
              childProcess.stdin.end();
            } else {
              this.message.emit("error", "Failed to change priveleges");
            }
          });
    
          // 没有写入权限,请求管理员密码
          this.message.emit("requirePassword");
        }
      }
    }
    

    helper.ts。实现功能:

  • 1.查看目录是否具有写入权限
  • typescriptCopy
    import { constants, accessSync } from "original-fs";
    
    /**
     * 是否是当前是否为root用户
     */
    export const isRoot = () => process.geteuid && process.geteuid() === 0;
    
    /**
     * 是否有写入权限
     */
    export const isDirctoryWritable = (dirname: string): boolean => {
      try {
        accessSync(`${dirname}`, constants.W_OK);
        return true;
      } catch {
        return false;
      }
    };
    

    event-bus.ts。实现功能:

  • 1.事件中心
  • 2.事件注册
  • 3.消息订阅
  • typescriptCopy
    /**
     * 事件中心
     */
    export class EventBus<EventName> {
      constructor() {}
      /**
       * 任务栈
       */
      public taskStack = new Map();
      /**
       * 事件注册中心
       */
      public on(name: EventName, callback: Function): void {
        this.taskStack.set(name, callback);
      }
      /**
       * 触发回调事件
       */
      public emit(name: EventName, ...args: any[]): void {
        const event = this.taskStack.get(name);
        event && event(...args);
      }
      /**
       * 取消订阅事件
       */
      public off(name: EventName): void {
        this.taskStack.delete(name);
      }
      /**
       * 销毁组件 (手动触发)
       */
      public destory(): void {
        this.taskStack.clear();
      }
    }
    

    使用:

  • 1.实例子化PrivilegesMain
  • typescriptCopy
    export class ElectronPrivileges {
      public privilegesMain: PrivilegesMain;
    
      /**
       * 更改目录权限
       */
      public changeAppDirPrivileges(dirname: string): void {
        this.privilegesMain.changeDirWritePrivileges(`${rootPath}/${dirname}`);
      }
    
      public setPassword(password: string): void {
        if (this.privilegesMain.setPassword) {
          this.privilegesMain.setPassword(password);
        }
      }
    
      // 初始化
      public init(): void {
        this.privilegesMain = new PrivilegesMain();
    
        // 订阅错误消息
        this.privilegesMain.message.on("error", (data: any) => {
          /*
           * 发送错误信息
           * requestPassowrd
           */
        });
    
        this.privilegesMain.message.on("requirePassword", (dirname: string) => {
          /*
           * 发送请求密码消息
           * requestPassowrd
           */
        });
    
        this.privilegesMain.message.on("success", () => {
          /*
           * 发送更改成功消息
           * changePrivilegesSuccessfully
           */
        });
    
        /**
         * 订阅请求修改目录权限
         * 执行 this.changeAppDirPrivileges
         */
    
        /**
         * 订阅请求修改目录权限
         * 执行 this.setPassword
         */
      }
    }
    

    Contents

    • 原因分析:
    • 解决方案:
    • 设计思路:
    • 核心代码:
    • 使用:

    2024/01/24 00:00