让非 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);

后续

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