让非 sudo 运行的程序用 sudo 执行

前言

在 Linux 上由普通用户所执行起来的程序进程,权限是受限的,例如不具备多数系统目录文件的可写权限,监听低于 1024 的端口号等(某些手段可以但这里不讨论)。这将导致许多操作不能完成。例如典型的用包管理工具安装软件,都需要加上 sudo 。
而 sudo 是一个让普通用户使用特殊权限运行程序的工具,有 sudo 执行能力的用户被称作 ‘sudoer’。凡是 sudoer 就无须担心多数情况下的权限问题,所以网络上很多软件产品的安装示例 Bash 命令上都会加上:sudo xxx xxx , 以避免用户出现权限问题。

那假设,有些用户并不知道 sudo 甚至看了错误提示还不知道要加上 sudo 。或者你觉得让用户有一次执行失败的操作是一种不好的体验,也有可能有些用户对 sudo 敏感,并不打算执行的你的安装命令或者前缀 sudo 运行你的程序呢?

答:这时候就要用程序自身回调 sudo 了,让程序以 sudo 再次运行自己。(所以本文其实是半个标题党)

Python

  1. 提取参数

    args = ['sudo', sys.executable] + sys.argv + [os.environ]
              

    从左至右分别是 sudo 程序、本程序、启动时接收的位置参数以及环境信息。

  2. 然后以 os 模块的 exec* 来运行子进程替换此进程

    os.execlpe('sudo', *args)
              

没错,就是简单到如此地步。但是这段代码并不能用,只会无脑的让你的程序死循环重新执行下去。因为需要判断当前进程权限是否符合需要以 sudo 重新执行自身的情况,所以要这样:

if os.geteuid() != 0:
              os.execlpe('sudo', *args)
              args = ['sudo', sys.executable] + sys.argv + [os.environ]
          

euid 默认取决于执行此进程的用户 id,其实我在以前的某篇文章有提过。进程启动会保留有“有效用户ID”(Effective UID)即 euid。如果非 root 启动程序,euid 一定是非 0。这样就达到了判断当前进程权限的目的。

再加上适当的提示,用户就会知道原来是权限不够程序在提醒输入密码呢。至于输入密码和接收密码从哪里来的,当然是 sudo 程序自己的,不然你还想获取输入的内容不成。

Lua

Lua 解释器极小,并没有做过多的系统 API 包装。想获取 euid 需要使用 luaposix 库,它对 POSIX APIs 进行了 Lua API 形式的绑定。

luarocks install luaposix
          

Lua 代码:

#!/usr/bin/env lua5.1
          
          local posix = require 'posix'
          
          function sudo()
              local euid = posix.geteuid()
              if not (euid == 0) then
                  local cmd = string.format('sudo %s %s', arg[0], table.concat(arg, ' '))
                  os.execute(cmd)
                  os.exit(0)
              end
          end
          
          sudo()
          print('Hello World!')
          

Go

Go 是一门发展良好的语言,有齐全的内置库和兼容性广(支持平台多)的编译器。是我非常喜欢使用的一门语言(最近正在用 Go 给路由器写程序)。
主要使用了 Go 的 exec 库和 os 库。使用顺手,没毛病。

package main
          
          import (
          	"os"
          	"os/exec"
          	"fmt"
          )
          
          func main() {
          	sudo()
          	fmt.Println("Hello World!")
          }
          
          func sudo() {
          	euid := os.Geteuid()
          	if euid != 0 {
          		cmd := exec.Command("sudo", os.Args...)
          		err := cmd.Start()
          		if err != nil {
          			fmt.Fprintln(os.Stderr, err)
          		}
          	}
          }
          
          

Rust

Rust 的话,相关 API 需要用到 libc 库。所以需要在 Cargo.toml 中加入:

[dependencies]
          libc = "0.2.21"
          

当然,在这之前(2014年)可能会有在 std 库中使用过 libc APIs 的情况,因为 libc 之前确实是被内置在 std 库中的,但是后来被独立了。可以看这里了解这个过程。

libc 的很多 API 都是 unsafe 的,这点要注意:

extern crate libc;
          use std::{process, env};
          use std::process::Command;
          
          fn main() {
              sudo();
              println!("Hello World!");
          }
          
          fn sudo() {
              unsafe {
                  let args: Vec<String> = env::args().collect();
                  let euid = libc::geteuid();
                  if euid != 0 {
                      Command::new("sudo")
                          .args(&args)
                          .status()
                          .expect("failed to execute sudo");
                      process::exit(1);
                  }
              }
          }
          

NodeJs

node 主要用 child_process 库和 process 库,因为回调的原因,代码可能不那么直观:

#!/usr/bin/env node
          
          const ch = require('child_process');
          
          function sudo(mainFunc) {
              let euid = process.geteuid();
              if (euid !== 0) {
                  ch.exec(`sudo ${process.argv.join(' ')}`, function (err, stdout, stderr) {
                      if(err){
                          throw new Error(stderr);
                      }
                      mainFunc();
                  });
              }
          }
          
          let main = function () {
              console.log('Hello World!');
          };
          
          sudo(main);
          

后续

后面有时间会增加其他很多语言的相同案例,顺便写短博客真轻松,一天续一点。