Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add integrity check to validate installation #218

Merged
merged 2 commits into from
Feb 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ tokio.workspace = true
tokio-stream.workspace = true
tonic.workspace = true

[build-dependencies]
walkdir = "2.3"

[features]
default = []
hot-reload = ["libloading"]
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ debug = false
-- Limit of calls per second that are executed inside of the mission scripting environment.
throughputLimit = 600

-- Whether the integrity check, meant to spot installation issues, is disabled.
integrityCheckDisabled = false

-- The default TTS provider to use if a TTS request does not explicitly specify another one.
tts.defaultProvider = "win"

Expand Down Expand Up @@ -258,6 +261,8 @@ For development:
```
- copy the hook script from `lua\Hooks\DCS-gRPC.lua` to `Scripts\Hooks\DCS-gRPC.lua`

- set `integrityCheckDisabled = true` in your `Saved Games\DCS\Config\dcs-grpc.lua` to allow changes to Lua without having to re-compile the dll

### Debugging

- Search for `[GRPC]` in the DCS logs
Expand Down
73 changes: 73 additions & 0 deletions build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
use std::collections::hash_map::DefaultHasher;
use std::fs::File;
use std::hash::Hasher;
use std::io::{BufReader, Read, Write};
use std::path::{Path, PathBuf};

use walkdir::WalkDir;

fn main() {
write_version_to_lua();
embed_lua_file_hashes();
}

/// Write the current version into `lua/DCS-gRPC/version.lua` to be picked up by the Lua side of the
/// server.
fn write_version_to_lua() {
println!("cargo:rerun-if-env-changed=CARGO_PKG_VERSION");

let path = PathBuf::from("./lua/DCS-gRPC/version.lua");
let mut out = File::create(path).unwrap();
writeln!(out, r#"-- this file is auto-generated on `cargo build`"#).unwrap();
writeln!(out, r#"GRPC.version = "{}""#, env!("CARGO_PKG_VERSION")).unwrap();
}

/// Embed the hash of each Lua file into the binary to allow a runtime integrity check.
fn embed_lua_file_hashes() {
println!("cargo:rerun-if-changed=lua/DCS-gRPC");
println!("cargo:rerun-if-changed=lua/Hooks");

let path = PathBuf::from(std::env::var("OUT_DIR").unwrap()).join("lua_files.rs");
let mut out = File::create(path).unwrap();

for (ident, base_path) in [("DCS_GRPC", "./lua/DCS-gRPC"), ("HOOKS", "./lua/Hooks")] {
writeln!(out, "const {ident}: &[(&str, u64)] = &[").unwrap();

for entry in WalkDir::new(base_path) {
let entry = entry.unwrap();
if !entry.metadata().unwrap().is_file() {
continue;
}

let path = entry
.path()
.strip_prefix(base_path)
.unwrap()
.to_str()
.expect("non-utf8 path");
let hash = file_hash(entry.path());
writeln!(out, r##" (r#"{path}"#, {hash}),"##).unwrap();
eprintln!("{}", entry.path().display());
}

writeln!(out, "];").unwrap();
}
}

fn file_hash(path: &Path) -> u64 {
// Not a cryptographic hasher, but good enough for our use-case.
let mut hasher = DefaultHasher::new();
let mut buffer = [0; 1024];
let file = File::open(path).unwrap();
let mut reader = BufReader::new(file);

loop {
let count = reader.read(&mut buffer).unwrap();
if count == 0 {
break;
}
hasher.write(&buffer[..count]);
}

hasher.finish()
}
3 changes: 3 additions & 0 deletions lua/DCS-gRPC/grpc-mission.lua
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ if GRPC.throughputLimit == nil or GRPC.throughputLimit == 0 or not type(GRPC.thr
GRPC.throughputLimit = 600
end

-- load version
dofile(GRPC.luaPath .. [[version.lua]])

-- Let DCS know where to find the DLLs
if not string.find(package.cpath, GRPC.dllPath) then
package.cpath = package.cpath .. [[;]] .. GRPC.dllPath .. [[?.dll;]]
Expand Down
8 changes: 6 additions & 2 deletions lua/DCS-gRPC/grpc.lua
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,22 @@ end
--

if isMissionEnv then
grpc.start({
assert(grpc.start({
version = GRPC.version,
writeDir = lfs.writedir(),
dllPath = GRPC.dllPath,
luaPath = GRPC.luaPath,
host = GRPC.host,
port = GRPC.port,
debug = GRPC.debug,
evalEnabled = GRPC.evalEnabled,
integrityCheckDisabled = GRPC.integrityCheckDisabled,
tts = GRPC.tts,
srs = GRPC.srs,
})
}))
end


--
-- Export methods
--
Expand Down
2 changes: 2 additions & 0 deletions lua/DCS-gRPC/version.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- this file is auto-generated on `cargo build`
GRPC.version = "0.7.1"
Empty file added lua/lua_files.rs
Empty file.
Empty file added lua_files.rs
Empty file.
4 changes: 4 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Config {
pub version: String,
pub write_dir: String,
pub dll_path: String,
pub lua_path: String,
#[serde(default = "default_host")]
pub host: String,
#[serde(default = "default_port")]
Expand All @@ -15,6 +17,8 @@ pub struct Config {
pub debug: bool,
#[serde(default)]
pub eval_enabled: bool,
#[serde(default)]
pub integrity_check_disabled: bool,
pub tts: Option<TtsConfig>,
pub srs: Option<SrsConfig>,
}
Expand Down
4 changes: 2 additions & 2 deletions src/hot_reload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use crate::Config;

static LIBRARY: Lazy<RwLock<Option<Library>>> = Lazy::new(|| RwLock::new(None));

pub fn start(lua: &Lua, config: Config) -> LuaResult<()> {
pub fn start(lua: &Lua, config: Config) -> LuaResult<(bool, Option<String>)> {
let lib_path = {
let mut lib_path = PathBuf::from(&config.dll_path);
lib_path.push("dcs_grpc.dll");
Expand All @@ -30,7 +30,7 @@ pub fn start(lua: &Lua, config: Config) -> LuaResult<()> {
let mut lib = LIBRARY.write().unwrap();
let lib = lib.get_or_insert(new_lib);

let f: Symbol<fn(lua: &Lua, config: Config) -> LuaResult<()>> = unsafe {
let f: Symbol<fn(lua: &Lua, config: Config) -> LuaResult<(bool, Option<String>)>> = unsafe {
lib.get(b"start")
.map_err(|err| mlua::Error::ExternalError(Arc::new(err)))?
};
Expand Down
85 changes: 85 additions & 0 deletions src/integrity.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
use std::collections::hash_map::DefaultHasher;
use std::fs::File;
use std::hash::Hasher;
use std::io::{self, BufReader, Read};
use std::path::{Path, PathBuf};
use std::{error, fmt};

use crate::config::Config;

include!(concat!(env!("OUT_DIR"), "/lua_files.rs"));

/// Check the integrity (compare the file hashes) of all DCS-gRPC related Lua files.
pub fn check(config: &Config) -> Result<(), IntegrityError> {
let dcs_grpc_base_path = AsRef::<Path>::as_ref(&config.lua_path);
for (path, expected_hash) in DCS_GRPC {
let path = dcs_grpc_base_path.join(path);
log::debug!("checking integrity of `{}`", path.display());
let file = File::open(&path).map_err(|err| IntegrityError::Read(path.clone(), err))?;
let hash = file_hash(&file).map_err(|err| IntegrityError::Hash(path.clone(), err))?;
if hash != *expected_hash {
return Err(IntegrityError::HashMismatch(path));
}
}

let hooks_base_path = AsRef::<Path>::as_ref(&config.write_dir).join("Scripts/Hooks");
for (path, expected_hash) in HOOKS {
let path = hooks_base_path.join(path);
log::debug!("checking integrity of `{}`", path.display());
let file = File::open(&path).map_err(|err| IntegrityError::Read(path.clone(), err))?;
let hash = file_hash(&file).map_err(|err| IntegrityError::Hash(path.clone(), err))?;
if hash != *expected_hash {
return Err(IntegrityError::HashMismatch(path));
}
}

Ok(())
}

fn file_hash(file: &File) -> io::Result<u64> {
// Not a cryptographic hasher, but good enough for our use-case.
let mut hasher = DefaultHasher::new();
let mut buffer = [0; 1024];
let mut reader = BufReader::new(file);

loop {
let count = reader.read(&mut buffer)?;
if count == 0 {
break;
}
hasher.write(&buffer[..count]);
}

Ok(hasher.finish())
}

#[derive(Debug)]
pub enum IntegrityError {
Read(PathBuf, io::Error),
Hash(PathBuf, io::Error),
HashMismatch(PathBuf),
}

impl error::Error for IntegrityError {}

impl fmt::Display for IntegrityError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "integrity check failed ")?;
match self {
IntegrityError::Read(path, err) => {
write!(f, "(could not read `{}`: {err})", path.display())
}
IntegrityError::Hash(path, err) => {
write!(f, "(could not create hash for `{}`: {err})", path.display())
}
IntegrityError::HashMismatch(path) => {
write!(f, "(hash mismatch of `{}`)", path.display())
}
}?;
write!(
f,
", DCS-gRPC is not started, please check your installation"
)?;
Ok(())
}
}
19 changes: 16 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ mod config;
mod fps;
#[cfg(feature = "hot-reload")]
mod hot_reload;
mod integrity;
pub mod rpc;
mod server;
mod shutdown;
Expand Down Expand Up @@ -71,16 +72,28 @@ pub fn init(config: &Config) {
}

#[no_mangle]
pub fn start(_: &Lua, config: Config) -> LuaResult<()> {
pub fn start(_: &Lua, config: Config) -> LuaResult<(bool, Option<String>)> {
rkusa marked this conversation as resolved.
Show resolved Hide resolved
{
if SERVER.read().unwrap().is_some() {
return Ok(());
return Ok((true, None));
}
}

init(&config);

log::debug!("Config: {:#?}", config);

if !config.integrity_check_disabled {
if env!("CARGO_PKG_VERSION") != config.version {
return Ok((false, Some("dcs_grpc.dll version does not match version of DCS-gRPC Lua files; please check your installation!".to_string())));
}

if let Err(err) = integrity::check(&config) {
return Ok((false, Some(err.to_string())));
}
log::info!("integrity check successful");
}

log::info!("Starting ...");

let mut server =
Expand All @@ -90,7 +103,7 @@ pub fn start(_: &Lua, config: Config) -> LuaResult<()> {

log::info!("Started");

Ok(())
Ok((true, None))
}

#[no_mangle]
Expand Down