refactor: read extra args from gp.conf

This commit is contained in:
Kevin Yue
2023-08-26 10:02:08 +08:00
parent f6ceb5ac0a
commit 11a374765c
9 changed files with 416 additions and 105 deletions

View File

@@ -17,6 +17,11 @@ ring = "0.16"
data-encoding = "2.3"
log = "0.4"
is_executable = "1.0"
configparser = "3.0"
shlex = "1.0"
anyhow = "1.0"
tempfile = "3.8"
lexopt = "0.3.0"
[build-dependencies]
cc = "1.0"

View File

@@ -1,21 +1,24 @@
use log::{debug, info, trace, warn};
use std::ffi::c_void;
use std::ffi::{c_char, c_int, c_void};
use tokio::sync::mpsc;
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub(crate) struct Options {
pub server: *const std::os::raw::c_char,
pub cookie: *const std::os::raw::c_char,
pub script: *const std::os::raw::c_char,
pub user_agent: *const std::os::raw::c_char,
pub server: *const c_char,
pub cookie: *const c_char,
pub user_agent: *const c_char,
pub user_data: *mut c_void,
pub script: *const c_char,
pub certificate: *const c_char,
pub servercert: *const c_char,
}
#[link(name = "vpn")]
extern "C" {
#[link_name = "vpn_connect"]
pub(crate) fn connect(options: *const Options) -> std::os::raw::c_int;
pub(crate) fn connect(options: *const Options) -> c_int;
#[link_name = "vpn_disconnect"]
pub(crate) fn disconnect();
@@ -33,7 +36,7 @@ extern "C" fn on_vpn_connected(value: i32, sender: *mut c_void) {
// level: 0 = error, 1 = info, 2 = debug, 3 = trace
// map the error level log in openconnect to the warning level
#[no_mangle]
extern "C" fn vpn_log(level: i32, message: *const std::os::raw::c_char) {
extern "C" fn vpn_log(level: i32, message: *const c_char) {
let message = unsafe { std::ffi::CStr::from_ptr(message) };
let message = message.to_str().unwrap_or("Invalid log message");
// Strip the trailing newline

164
gpcommon/src/vpn/gpconf.rs Normal file
View File

@@ -0,0 +1,164 @@
use anyhow::{Error, Ok, Result};
use configparser::ini::Ini;
use lexopt::Parser;
use log::warn;
const GPCONF_PATH: &str = "/etc/gpservice/gp.conf";
const DEFAULT_SECTION: &str = "*";
const PROP_OPENCONNECT_ARGS: &str = "openconnect-args";
/// A struct representing the CLI arguments for the `openconnect` command.
/// Supports most of the options from the `openconnect` command line.
#[derive(Debug, Default)]
pub(crate) struct OpenconnectArgs {
script: Option<String>,
certificate: Option<String>,
servercert: Option<String>,
}
impl OpenconnectArgs {
pub fn script(&self) -> Option<String> {
self.script.to_owned()
}
pub fn certificate(&self) -> Option<String> {
self.certificate.to_owned()
}
pub fn servercert(&self) -> Option<String> {
self.servercert.to_owned()
}
}
/// Read the `gp.conf` file and return the `openconnect` arguments for the given server.
/// If the server is not found, the default section is used.
pub(crate) fn read_conf(server: &str) -> Result<OpenconnectArgs> {
read_conf_from(GPCONF_PATH, server)
}
/// Private function to read the `openconnect` arguments for the given server from a given path.
/// Make it easy to write tests.
fn read_conf_from(path: &str, server: &str) -> Result<OpenconnectArgs> {
let mut config = Ini::new();
config.set_default_section(DEFAULT_SECTION);
config.set_multiline(true);
config.load(path).map_err(Error::msg)?;
let default_openconnect_config = config
.get(DEFAULT_SECTION, PROP_OPENCONNECT_ARGS)
.unwrap_or_default();
let server_openconnect_config = config
.get(server, PROP_OPENCONNECT_ARGS)
.unwrap_or(default_openconnect_config);
let args = shlex::split(&server_openconnect_config).unwrap_or_default();
parse_args(&args)
}
fn parse_args(args: &Vec<String>) -> Result<OpenconnectArgs> {
use lexopt::prelude::*;
let mut parser = Parser::from_args(args);
let mut script: Option<String> = None;
let mut certificate: Option<String> = None;
let mut servercert: Option<String> = None;
while let Some(arg) = parser.next()? {
match arg {
Long("script") | Short('s') => {
script = Some(parser.value()?.parse()?);
}
Long("certificate") | Short('c') => {
certificate = Some(parser.value()?.parse()?);
}
Long("servercert") => {
servercert = Some(parser.value()?.parse()?);
}
_ => {
warn!("Ignoring unknown argument: {}", arg.unexpected());
}
}
}
Ok(OpenconnectArgs {
script,
certificate,
servercert,
})
}
#[cfg(test)]
mod tests {
use std::io::Write;
use tempfile::NamedTempFile;
use super::*;
// Macro to create a temporary file with the given content.
macro_rules! tempfile {
($content:expr) => {{
let mut file = NamedTempFile::new().unwrap();
write!(file, $content).unwrap();
file
}};
}
#[test]
fn test_config_not_found() {
let args = read_conf_from("non-existent-file", "server");
assert!(args.is_err());
}
#[test]
fn test_default_config() {
let file = tempfile!(
r#"
[*]
openconnect-args=--script=/script.sh
"#
);
let path = file.path().to_str().unwrap();
let args = read_conf_from(path, "any server").unwrap();
assert_eq!(args.script, Some("/script.sh".to_string()));
assert_eq!(args.certificate, None);
assert_eq!(args.servercert, None);
}
#[test]
fn test_server_config() {
let file = tempfile!(
r#"
[*]
openconnect-args=--script=/script.sh
[server]
openconnect-args=--certificate=/cert.pem
"#
);
let path = file.path().to_str().unwrap();
let args = read_conf_from(path, "server").unwrap();
assert_eq!(args.script, None);
assert_eq!(args.certificate, Some("/cert.pem".to_string()));
assert_eq!(args.servercert, None);
}
#[test]
fn test_ignore_unknown_args() {
let file = tempfile!(
r#"
[*]
openconnect-args=--script=/script.sh --unknown-arg -c /cert.pem
"#
);
let path = file.path().to_str().unwrap();
let args = read_conf_from(path, "server").unwrap();
assert_eq!(args.script, Some("/script.sh".to_string()));
assert_eq!(args.certificate, Some("/cert.pem".to_string()));
assert_eq!(args.servercert, None);
}
}

View File

@@ -1,13 +1,15 @@
use crate::vpn::vpnc_script::find_default_vpnc_script;
use self::vpn_options::VpnOptions;
use log::{debug, info, warn};
use serde::{Deserialize, Serialize};
use std::ffi::{c_void, CString};
use std::ffi::c_void;
use std::sync::Arc;
use std::thread;
use tokio::sync::watch;
use tokio::sync::{mpsc, Mutex};
mod ffi;
mod gpconf;
mod vpn_options;
mod vpnc_script;
#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)]
@@ -51,30 +53,6 @@ impl StatusHolder {
}
}
#[derive(Debug)]
pub(crate) struct VpnOptions {
server: CString,
cookie: CString,
script: CString,
user_agent: CString,
}
impl VpnOptions {
fn as_oc_options(&self, user_data: *mut c_void) -> ffi::Options {
ffi::Options {
server: self.server.as_ptr(),
cookie: self.cookie.as_ptr(),
script: self.script.as_ptr(),
user_agent: self.user_agent.as_ptr(),
user_data,
}
}
fn to_cstr(value: &str) -> CString {
CString::new(value.to_string()).expect("Failed to convert to CString")
}
}
#[derive(Debug, Default)]
pub(crate) struct Vpn {
status_holder: Arc<Mutex<StatusHolder>>,
@@ -92,23 +70,18 @@ impl Vpn {
cookie: &str,
user_agent: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let script = match find_default_vpnc_script() {
Some(script) => {
debug!("Using default vpnc-script: {}", script);
script
}
None => {
return Err("Failed to find default vpnc-script".into());
}
};
let mut options_builder = VpnOptions::builder();
let mut options_builder = options_builder
.server(server)
.cookie(cookie)
.user_agent(user_agent);
if let Ok(openconnect_args) = gpconf::read_conf(server) {
info!("Found openconnect args in /etc/gpservice/gp.conf");
options_builder = options_builder.with_openconnect_args(openconnect_args);
}
// Save the VPN options so we can use them later, e.g. reconnect
*self.vpn_options.lock().await = Some(VpnOptions {
server: VpnOptions::to_cstr(server),
cookie: VpnOptions::to_cstr(cookie),
script: VpnOptions::to_cstr(script),
user_agent: VpnOptions::to_cstr(user_agent),
});
*self.vpn_options.lock().await = Some(options_builder.build());
let vpn_options = self.vpn_options.clone();
let status_holder = self.status_holder.clone();

View File

@@ -67,6 +67,17 @@ int vpn_connect(const vpn_options *options)
openconnect_set_hostname(vpninfo, options->server);
openconnect_set_cookie(vpninfo, options->cookie);
if (options->certificate)
{
INFO("Setting client certificate: %s", options->certificate);
openconnect_set_client_cert(vpninfo, options->certificate, NULL);
}
if (options->servercert) {
INFO("Setting server certificate: %s", options->servercert);
openconnect_set_system_trust(vpninfo, 0);
}
g_cmd_pipe_fd = openconnect_setup_cmd_pipe(vpninfo);
if (g_cmd_pipe_fd < 0)
{

View File

@@ -7,9 +7,12 @@ typedef struct vpn_options
{
const char *server;
const char *cookie;
const char *script;
const char *user_agent;
void *user_data;
const char *script;
const char *certificate;
const char *servercert;
} vpn_options;
int vpn_connect(const vpn_options *options);

View File

@@ -0,0 +1,93 @@
use super::{ffi, gpconf::OpenconnectArgs, vpnc_script::find_default_vpnc_script};
use std::ffi::{c_char, c_void, CString};
#[derive(Debug)]
pub(crate) struct VpnOptions {
server: CString,
cookie: CString,
user_agent: CString,
script: CString,
certificate: Option<CString>,
servercert: Option<CString>,
}
impl VpnOptions {
pub fn builder() -> VpnOptionsBuilder {
VpnOptionsBuilder::default()
}
pub fn as_oc_options(&self, user_data: *mut c_void) -> ffi::Options {
ffi::Options {
server: self.server.as_ptr(),
cookie: self.cookie.as_ptr(),
user_agent: self.user_agent.as_ptr(),
user_data,
script: self.script.as_ptr(),
certificate: Self::option_as_ptr(&self.certificate),
servercert: Self::option_as_ptr(&self.servercert),
}
}
fn option_as_ptr(value: &Option<CString>) -> *const c_char {
match value {
Some(value) => value.as_ptr(),
None => std::ptr::null(),
}
}
}
#[derive(Debug, Default)]
pub(crate) struct VpnOptionsBuilder {
server: String,
cookie: String,
user_agent: String,
openconnect_args: OpenconnectArgs,
}
impl VpnOptionsBuilder {
pub fn server(&mut self, server: &str) -> &mut Self {
self.server = server.to_string();
self
}
pub fn cookie(&mut self, cookie: &str) -> &mut Self {
self.cookie = cookie.to_string();
self
}
pub fn user_agent(&mut self, user_agent: &str) -> &mut Self {
self.user_agent = user_agent.to_string();
self
}
pub fn with_openconnect_args(&mut self, openconnect_args: OpenconnectArgs) -> &mut Self {
self.openconnect_args = openconnect_args;
self
}
fn to_cstr(value: &str) -> CString {
CString::new(value.to_string()).expect("Failed to convert to CString")
}
pub fn build(&self) -> VpnOptions {
let openconnect_args = &self.openconnect_args;
let script = openconnect_args
.script()
.or_else(|| find_default_vpnc_script().map(|s| s.to_string()))
.map(|s| Self::to_cstr(&s))
.unwrap_or_default();
let certificate = openconnect_args.certificate().map(|s| Self::to_cstr(&s));
let servercert = openconnect_args.servercert().map(|s| Self::to_cstr(&s));
VpnOptions {
server: Self::to_cstr(&self.server),
cookie: Self::to_cstr(&self.cookie),
user_agent: Self::to_cstr(&self.user_agent),
script,
certificate,
servercert,
}
}
}