iserv-ipad-helper/src/main.rs

296 lines
12 KiB
Rust

use std::borrow::Cow;
use clap::Parser;
#[derive(Parser, Debug)]
#[clap(author, version, about, long_about = None)]
struct Args {
#[clap(short, long)]
hostname: Option<String>,
//example: "IServSAT=mvuvFvCZSlpvuwhk0mmNwF1NKEQypM4d; IServSession=xEPqyJO1a5nsVd34HEpbPqQGMMQKapvw; nav-show-additional-modules=true; PHPSESSID=mchbrl11epjnnp851q0i4rt8rc"
#[clap(short, long)]
session_cookie: Option<String>,
#[clap(long)]
target_ip: Option<String>,
#[clap(long)]
target_room: Option<u16>,
#[clap(long)]
target_compilation: Option<u16>,
}
macro_rules! print_banner {
($f: expr, $($arg: expr),*) => {
println!(concat!("----------------------------------------\n", $f, "\n----------------------------------------"), $($arg),*)
};
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = Args::parse();
loop { ipad(&args)?; }
}
fn ipad(args: &Args) -> Result<(), Box<dyn std::error::Error>> {
let params = [
("filter[controllable]", ""),
("filter[room][]", "__none"),
("filter[group]", ""),
("filter[search]", "iPad"),
];
let header = ("Cookie", &args.session_cookie);
let client = reqwest::blocking::Client::new();
let resp = client
.get(format!("https://{}/iserv/admin/hosts", args.hostname.unwrap()))
.header(header.0, header.1)
.query(&params)
.send()?;
let status = &resp.status();
let plain = &resp.text()?;
let content = squeeze(
&plain,
"<script id=\"crud-data\" type=\"application/json\">",
"</script>",
);
let json: serde_json::Value = serde_json::from_str(content).expect("Bad Jason!");
let json_part = &json["data"][0]["name"]["rendered"]
.to_string()
.replace("\\", "");
// skip rest of loop when no entry appeared
if json_part == "null" {
println!(
"########################################
No new host: skipping (Retry in 5s)
########################################
");
//sleep for 5 seconds
std::thread::sleep(std::time::Duration::from_secs(5));
return Ok(());
}
let host_id = squeeze(json_part, "data-host-id=\"", "\"");
let device_name = squeeze(json_part, "</span>", "</a>");
print_banner!("1/8 Found host\nHost id: {}\nDevice name: {}\nReceived response status: {:?}", host_id, device_name, status);
let dest = format!("https://{}/iserv/admin/host/edit/{}", args.hostname, &host_id);
let resp = client.get(&dest).header(header.0, header.1).send()?;
//TODO Check for non 200 response
let status = &resp.status();
if !status.is_success() && *status != 300 {
eprintln!("WARN: Found invalid Status Code: {status}");
return Ok(())
}
let plain = &resp.text()?;
let mut form_ip: &str = &args.target_ip;
let form_name = squeeze_html_form(plain, "name=\"host[name]\"", "value=\"", "\"");
let form_tags = squeeze_html_form(plain, "name=\"host[tags][]\"", "value=\"", "\"");
let form_mac = squeeze_html_form(plain, "name=\"host[mac]\"", "value=\"", "\"");
let form_description = squeeze_html_form(plain, "name=\"host[description]\"", ">", "<");
let form_token = squeeze_html_form(plain, "name=\"host[_token]\"", "value=\"", "\"");
let mut form_data = [
("host[name]", form_name),
("host[tags][]", form_tags),
("host[room]", &args.target_room.to_string()),
("host[ip]", &form_ip),
("host[mac]", form_mac),
("host[internet]", "1"),
("host[proxyEnforce]", "0"),
("host[owner]", ""),
("host[controllable]", "0"),
("host[inventoryNumber]", ""),
("host[shutdown]", ""),
("host[description]", form_description),
("host[image][id]", ""),
("host[image][x]", ""),
("host[image][y]", ""),
("host[actions][submit]", ""),
("host[_token]", form_token),
];
print_banner!("2/8 Received current host properties\nReceived response status: {:?}", status);
let resp = client
.post(&dest)
.header(header.0, header.1)
.form(&form_data)
.send()?;
let status = &resp.status();
let plain = &resp.text()?;
form_ip = squeeze_html_form(plain, "name=\"host[ip]\"", "value=\"", "\"");
form_data[3] = ("host[ip]", form_ip);
print_banner!("3/8 Received recommended ip: {}\nReceived response status: {:?}", form_ip, status);
let resp = client
.post(&dest)
.header(header.0, header.1)
.form(&form_data)
.send()?;
let status = &resp.status();
print_banner!("4/8 Sucessfully updated host\nReceived response status: {:?}", status);
if args.target_compilation.is_none() { return Ok(()) }
let resp = client
.get(format!("https://{}/iserv/admin/mdm/ios/compilation/edit/{}", args.hostname, args.target_compilation.unwrap()))
.header(header.0, header.1)
.send()?;
//TODO Check for non 200 response
let status = &resp.status();
let plain = &resp.text()?;
print_banner!("5/8 Received MDM compilation\nReceived response status: {:?}", status);
let mut form_data = Vec::from([
(
"ioscompilation[name]",
squeeze_html_form(plain, "name=\"ioscompilation[name]\"", "value=\"", "\"")
),
(
"ioscompilation[description]",
squeeze_html_form(plain, "name=\"ioscompilation[description]\"", "value=\"", "\"")
),
].map(|(a, b)| (Cow::Borrowed(a), Cow::Borrowed(b))));
// push apps (<option value="52" selected="selected">)
let plain_inner_applications = squeeze_html_form(plain, "name=\"ioscompilation[applications][]\"", ">", "</select>");
squeeze_loop(&mut form_data, plain_inner_applications, "<option value=\"", "\"", " selected=\"selected\"", 3, "ioscompilation[applications][]");
// push profiles
let plain_inner_profiles = squeeze_html_form(plain, "name=\"ioscompilation[profiles][]\"", ">", "</select>");
squeeze_loop(&mut form_data, plain_inner_profiles, "<option value=\"", "\"", " selected=\"selected\"", 3, "ioscompilation[profiles][]");
// push existing devices ([\s\n]* seems not needed)
let plain_inner_devices = squeeze_html_form(plain, "name=\"ioscompilation[devices][]\"", ">", "</select>");
squeeze_loop(&mut form_data, plain_inner_devices, "<option value=\"", "\"", " selected=\"selected\"", 3, "ioscompilation[devices][]");
// push new device
let mdm_new_ipad_id = squeeze_right(plain_inner_devices, "<option value=\"", &format!("\">{}", form_name));
if plain_inner_devices.contains(form_name) {
form_data.push(("ioscompilation[devices][]".into(), mdm_new_ipad_id.into()));
} else {
eprintln!("Device not found in compilation edit.")
}
// push submit action
form_data.push(("ioscompilation[actions][submit]".into(), "".into()));
//push token
let form_token = squeeze_html_form(plain, "name=\"ioscompilation[_token]\"", "value=\"", "\"");
form_data.push(("ioscompilation[_token]".into(), form_token.into()));
let resp = client
.post(format!("https://{}/iserv/admin/mdm/ios/compilation/edit/{}", args.hostname, args.target_compilation.unwrap()))
.header(header.0, header.1)
.form(&form_data)
.send()?;
let status = &resp.status();
print_banner!("6/8 Added device to MDM Compilation\nReceived response status: {:?}", status);
let resp = client
.get(format!("https://{}/iserv/admin/mdm/ios/device", args.hostname))
.header(header.0, header.1)
.send()?;
let status = &resp.status();
let plain = &resp.text()?;
let csrf_token = squeeze_html_form(plain, "name=\"iserv_crud_multi_select[_token]\"", "value=\"", "\"");
print_banner!("7/8 Retrieved CSRF token\nReceived response status: {:?}", status);
let form_data = [
("iserv_crud_multi_select[confirm]", "apply-changes"),
("iserv_crud_multi_select[multi][]", mdm_new_ipad_id),
("iserv_crud_multi_select[topReplacementActions][apply-changes]", ""),
("iserv_crud_multi_select[_token]", csrf_token),
];
let resp = client
.post(format!("https://{}/iserv/admin/mdm/ios/device/batch", args.hostname))
.header(header.0, header.1)
.form(&form_data)
.send()?;
let status = &resp.status();
print_banner!("8/8 Confirmed device actions\nReceived response status: {:?}", status);
Ok(())
}
// helper functions (needed for naive html parsing)
fn squeeze<'inp>(input: &'inp str, before: &str, after: &str) -> &'inp str {
//find before occurence (excluded from output)
if let Some(before_bytes) = input.find(before) {
let adjusted_before_bytes = before_bytes + before.len();
let leftover = &input[adjusted_before_bytes..];
//find after occurence (excluded from output)
if let Some(after_bytes) = leftover.find(after) {
let adjusted_after_bytes = after_bytes + adjusted_before_bytes;
// ensure bytes are not equal
if adjusted_before_bytes < adjusted_after_bytes {
return &input[adjusted_before_bytes..adjusted_after_bytes];
}
}
}
""
}
fn squeeze_right<'inp>(input: &'inp str, before: &str, after: &str) -> &'inp str {
//find before occurence (excluded from output)
if let Some(after_bytes) = input.find (after) {
let leftover = &input[..after_bytes];
//find after occurence (excluded from output)
if let Some(before_bytes) = leftover.rfind(before) {
let adjusted_before_bytes = before_bytes + before.len();
// ensure bytes are not equal
if adjusted_before_bytes < after_bytes {
return &input[adjusted_before_bytes..after_bytes];
}
}
}
""
}
fn squeeze_html_form<'inp>(
input: &'inp str,
first: &str,
second: &str,
last: &str,
) -> &'inp str {
//find first occurence (broad search)
if let Some(from_first_bytes) = input.find(first) {
let after_first_bytes = from_first_bytes + first.len();
let after_first = &input[after_first_bytes..];
//find second occurence (field search)
if let Some(rel_from_second_bytes) = after_first.find(second) {
let after_second_bytes = rel_from_second_bytes + after_first_bytes + second.len();
let after_second = &input[after_second_bytes..];
//find closing last match (excluded from output)
if let Some(rel_from_last_byte) = after_second.find(last) {
let from_last_byte = rel_from_last_byte + after_second_bytes;
return &input[after_second_bytes..from_last_byte];
}
}
}
""
}
fn squeeze_loop<'inp>(
collector: &mut Vec<(Cow<'inp, str>, Cow<'inp, str>)>,
input: &'inp str,
before: &str,
after: &str,
must_include_after: &str,
include_after_distance: usize,
index_name: &'inp str
) {
let mut leftover = input;
//find before occurence (excluded from output)
while let Some(before_bytes) = leftover.find(before) {
let input = leftover;
let adjusted_before_bytes = before_bytes + before.len();
leftover = &leftover[adjusted_before_bytes..];
//find after occurence (excluded from output)
if let Some(after_bytes) = leftover.find(after) {
let absolute_after_bytes = after_bytes + adjusted_before_bytes;
leftover = &leftover[after_bytes + after.len()..];
// ensure bytes are not equal and that the item is "selected"/enabled
if let Some(must_include_distance) = leftover.find(must_include_after) {
if adjusted_before_bytes < absolute_after_bytes && must_include_distance < include_after_distance {
collector.push((index_name.into(), input[adjusted_before_bytes..absolute_after_bytes].into()));
}
}
}
}
}