Skip to content
Draft
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
6 changes: 3 additions & 3 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ macos-11, macos-12, macos-13 ]
os: [ macos-12, macos-13, macos-14 ]
rust: [ nightly, stable ]

steps:
Expand All @@ -25,8 +25,8 @@ jobs:
toolchain: ${{ matrix.rust }}
override: true

- name: Build and run the echo server
run: cd examples/echo-server && make && make install
- name: Build, run, and test the echo agent and daemon
run: cd examples && make test

- name: Run cargo test
uses: actions-rs/cargo@v1
Expand Down
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# Generated by Cargo
# will have compiled files and executables
/target/
/examples/echo-server/com.example.echo

# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Expand Down
31 changes: 31 additions & 0 deletions examples/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
all: build

.PHONY: build
build:
cargo build

.PHONY: test
test: install
cd client && cargo run

.PHONY: install
install: build
sudo mkdir -p /Library/PrivilegedHelperTools/
sudo cp ../target/debug/echo-server /Library/PrivilegedHelperTools/echo-daemon
sudo cp echo-server/echo-daemon.plist /Library/LaunchDaemons/
sudo launchctl load /Library/LaunchDaemons/echo-daemon.plist

sudo mkdir -p /usr/local/bin
sudo cp echo-server/echo-agent.plist /Library/LaunchAgents/
sudo cp ../target/debug/echo-server /usr/local/bin/echo-agent
launchctl load /Library/LaunchAgents/echo-agent.plist

.PHONY: uninstall
uninstall:
sudo launchctl unload /Library/LaunchDaemons/echo-daemon.plist
sudo rm -f /Library/LaunchDaemons/echo-daemon.plist
sudo rm -f /Library/PrivilegedHelperTools/echo-daemon

launchctl unload /Library/LaunchAgents/echo-agent.plist
sudo rm -f /Library/LaunchAgents/echo-agent.plist
sudo rm -f /usr/local/bin/echo-agent
7 changes: 2 additions & 5 deletions examples/client/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
[package]
name = "client"
version = "0.1.0"
authors = ["Steven Joruk <[email protected]>"]
edition = "2018"
publish = false
edition = "2021"

[dependencies]
futures = { version = "0.3" }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
tokio = { version = "1", features = ["macros", "time", "rt-multi-thread"] }
xpc-connection = { path = "../../xpc-connection" }
28 changes: 18 additions & 10 deletions examples/client/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,31 @@
use futures::stream::StreamExt;
use std::{collections::HashMap, error::Error, ffi::CString};
use std::{collections::HashMap, ffi::CStr, time::Duration};
use xpc_connection::{Message, XpcClient};

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let mach_port_name = CString::new("com.example.echo")?;
async fn main() {
send_with_reply(c"echo-agent", false).await;
send_with_reply(c"echo-daemon", true).await;
}

async fn send_with_reply(mach_port_name: &CStr, privileged: bool) {
println!("Attempting to connect to {mach_port_name:?}");

println!("Attempting to connect to {:?}", mach_port_name);
let mut client = XpcClient::connect(&mach_port_name);
let mut client = if privileged {
XpcClient::connect_privileged(mach_port_name)
} else {
XpcClient::connect_unprivileged(mach_port_name)
};

let mut dictionary = HashMap::new();
dictionary.insert(CString::new("hello")?, Message::Int64(2));
dictionary.insert(c"hello".to_owned(), Message::Int64(2));

println!("Sending a message");
client.send_message(Message::Dictionary(dictionary));

if let Some(message) = client.next().await {
println!("Client received message {:?}", message);
}
let message = tokio::time::timeout(Duration::from_secs(1), client.next())
.await
.expect("No reply received");

Ok(())
println!("Client received message {:?}", message);
}
5 changes: 1 addition & 4 deletions examples/echo-server/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
[package]
name = "echo-server"
version = "0.1.0"
authors = ["Steven Joruk <[email protected]>"]
edition = "2018"
publish = false
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

Expand Down
16 changes: 0 additions & 16 deletions examples/echo-server/Makefile

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,22 @@
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.example.echo</string>
<string>echo-agent</string>
<key>MachServices</key>
<dict>
<key>com.example.echo</key>
<key>echo-agent</key>
<true/>
</dict>
<key>Program</key>
<string>/Library/PrivilegedHelperTools/com.example.echo</string>
<string>/usr/local/bin/echo-agent</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/echo-agent</string>
<string>echo-agent</string>
</array>
<key>StandardOutPath</key>
<string>/var/log/com.example.echo.log</string>
<string>/tmp/echo-agent.log</string>
<key>StandardErrorPath</key>
<string>/var/log/com.example.echo.log</string>
<string>/tmp/echo-agent.log</string>
</dict>
</plist>
24 changes: 24 additions & 0 deletions examples/echo-server/echo-daemon.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>echo-daemon</string>
<key>MachServices</key>
<dict>
<key>echo-daemon</key>
<true/>
</dict>
<key>Program</key>
<string>/Library/PrivilegedHelperTools/echo-daemon</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/echo-daemon</string>
<string>echo-daemon</string>
</array>
<key>StandardOutPath</key>
<string>/tmp/echo-daemon.log</string>
<key>StandardErrorPath</key>
<string>/tmp/echo-daemon.log</string>
</dict>
</plist>
41 changes: 10 additions & 31 deletions examples/echo-server/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use core_foundation::{base::TCFType, data::CFData};
use futures::stream::StreamExt;
use security_framework::os::macos::code_signing::{Flags, GuestAttributes, SecCode};
use std::{error::Error, ffi::CString};
use std::ffi::CString;
use xpc_connection::{Message, MessageError, XpcClient, XpcListener};

fn get_code_object_for_client(client: &XpcClient) -> SecCode {
Expand All @@ -28,31 +28,9 @@ fn validate_client_by_code_signing_requirement(client: &XpcClient) -> bool {
false
}

fn validate_client_by_path(client: &XpcClient) -> bool {
if get_code_object_for_client(client)
.path(Flags::NONE)
.unwrap()
// It'd be better to use to_path
.get_string()
.to_string()
// This is insecure, it's just so the tests can be run from anywhere
.contains("message_round_trip")
{
println!("The client was validated using its path");
return true;
}

println!("The client's path doesn't contain 'message_round_trip'");
false
}

async fn handle_client(mut client: XpcClient) {
println!("New connection");

if !validate_client_by_path(&client) {
return;
}

loop {
match client.next().await {
None => {
Expand All @@ -72,13 +50,16 @@ async fn handle_client(mut client: XpcClient) {
}

#[tokio::main(flavor = "multi_thread")]
async fn main() -> Result<(), Box<dyn Error>> {
let mach_port_name = CString::new("com.example.echo")?;
async fn main() {
let mach_port_name = CString::new(
std::env::args()
.nth(1)
.expect("Usage: echo-server <mach port name>")
.as_str(),
)
.expect("Failed to convert the mach port name to a CString");

println!(
"Waiting for new connections on {:?}",
mach_port_name.to_string_lossy()
);
println!("Waiting for new connections on {mach_port_name:?}",);

let mut listener = XpcListener::listen(&mach_port_name);

Expand All @@ -87,6 +68,4 @@ async fn main() -> Result<(), Box<dyn Error>> {
}

println!("Server is shutting down");

Ok(())
}
4 changes: 2 additions & 2 deletions xpc-connection/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
[package]
authors = ["Dylan Frankland <[email protected]>"]
name = "xpc-connection"
version = "0.2.3"
edition = "2018"
version = "0.3.0"
edition = "2021"
license = "MIT"
description = "XPC connection bindings for Rust"
homepage = "https://github.com/dfrankland/xpc-connection-rs"
Expand Down
48 changes: 35 additions & 13 deletions xpc-connection/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,9 +201,33 @@ impl XpcClient {
}

/// The connection isn't established until the first call to `send_message`.
///
/// This is an alias for [`XpcClient::connect_privileged`].
#[deprecated(
since = "0.3.0",
note = "Use connect_privileged or connect_unprivileged"
)]
pub fn connect(name: impl AsRef<CStr>) -> Self {
Self::connect_privileged(name)
}

/// The connection isn't established until the first call to `send_message`.
///
/// Connects to a privileged mach port, i.e. a launch daemon.
pub fn connect_privileged(name: impl AsRef<CStr>) -> Self {
Self::connect_with_flags(name, XPC_CONNECTION_MACH_SERVICE_PRIVILEGED as u64)
}

/// The connection isn't established until the first call to `send_message`.
///
/// Connects to an unprivileged mach port, i.e. a launch agent.
pub fn connect_unprivileged(name: impl AsRef<CStr>) -> Self {
Self::connect_with_flags(name, 0)
}

/// The connection isn't established until the first call to `send_message`.
fn connect_with_flags(name: impl AsRef<CStr>, flags: u64) -> Self {
let name = name.as_ref();
let flags = XPC_CONNECTION_MACH_SERVICE_PRIVILEGED as u64;
let connection = unsafe {
xpc_connection_create_mach_service(name.as_ptr(), std::ptr::null_mut(), flags)
};
Expand Down Expand Up @@ -247,15 +271,14 @@ impl XpcClient {
mod tests {
use super::*;
use futures::{executor::block_on, StreamExt};
use std::{collections::HashMap, ffi::CString};
use std::collections::HashMap;
use xpc_connection_sys::xpc_connection_cancel;

// This also tests that the event handler block is only freed once, as a
// double free is possible if the block isn't copied on to the heap.
#[test]
fn event_handler_receives_error_on_close() {
let mach_port_name = CString::new("com.apple.blued").unwrap();
let mut client = XpcClient::connect(&mach_port_name);
let mut client = XpcClient::connect_privileged(c"com.apple.blued");

// Cancelling the connection will cause the event handler to be called
// with an error message. This will happen under normal circumstances,
Expand All @@ -268,21 +291,20 @@ mod tests {
}

#[test]
fn stream_closed_on_drop() -> Result<(), Box<dyn std::error::Error>> {
let mach_port_name = CString::new("com.apple.blued")?;
let mut client = XpcClient::connect(&mach_port_name);
fn stream_closed_on_drop() {
let mut client = XpcClient::connect_privileged(c"com.apple.blued");

let message = Message::Dictionary({
let mut dictionary = HashMap::new();
dictionary.insert(CString::new("kCBMsgId")?, Message::Int64(1));
dictionary.insert(c"kCBMsgId".to_owned(), Message::Int64(1));
dictionary.insert(
CString::new("kCBMsgArgs")?,
c"kCBMsgArgs".to_owned(),
Message::Dictionary({
let mut temp = HashMap::new();
temp.insert(CString::new("kCBMsgArgAlert")?, Message::Int64(1));
temp.insert(c"kCBMsgArgAlert".to_owned(), Message::Int64(1));
temp.insert(
CString::new("kCBMsgArgName")?,
Message::String(CString::new("rust")?),
c"kCBMsgArgName".to_owned(),
Message::String(c"rust".to_owned()),
);
temp
}),
Expand Down Expand Up @@ -312,7 +334,7 @@ mod tests {
// from blued before the connection is cancelled, but it's
// safe to say it should be less than 5.
assert!(count < 5);
return Ok(());
return;
}
}
}
Expand Down
Loading