Refactoring
- Added print_banner macro - Moved main logic into function - More idiomatic code
This commit is contained in:
parent
c7feeb0992
commit
39be1d684a
156
src/main.rs
156
src/main.rs
|
@ -1,31 +1,33 @@
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[clap(author, version, about, long_about = None)]
|
#[clap(author, version, about, long_about = None)]
|
||||||
struct Args {
|
struct Args {
|
||||||
#[clap(short, long)]
|
#[clap(short, long)]
|
||||||
hostname: String,
|
hostname: String,
|
||||||
|
|
||||||
//example: "IServSAT=mvuvFvCZSlpvuwhk0mmNwF1NKEQypM4d; IServSession=xEPqyJO1a5nsVd34HEpbPqQGMMQKapvw; nav-show-additional-modules=true; PHPSESSID=mchbrl11epjnnp851q0i4rt8rc"
|
//example: "IServSAT=mvuvFvCZSlpvuwhk0mmNwF1NKEQypM4d; IServSession=xEPqyJO1a5nsVd34HEpbPqQGMMQKapvw; nav-show-additional-modules=true; PHPSESSID=mchbrl11epjnnp851q0i4rt8rc"
|
||||||
#[clap(short, long)]
|
#[clap(short, long)]
|
||||||
session_cookie: String,
|
session_cookie: String,
|
||||||
|
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
target_ip: String,
|
target_ip: String,
|
||||||
|
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
target_room: u16,
|
target_room: u16,
|
||||||
|
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
target_compilation: Option<u16>,
|
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>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
loop {
|
loop { ipad(&args)?; }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ipad(args: &Args) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let params = [
|
let params = [
|
||||||
("filter[controllable]", ""),
|
("filter[controllable]", ""),
|
||||||
("filter[room][]", "__none"),
|
("filter[room][]", "__none"),
|
||||||
|
@ -34,61 +36,52 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
];
|
];
|
||||||
let header = ("Cookie", args.session_cookie.to_owned());
|
let header = ("Cookie", args.session_cookie.to_owned());
|
||||||
let client = reqwest::blocking::Client::new();
|
let client = reqwest::blocking::Client::new();
|
||||||
|
|
||||||
let resp = client
|
let resp = client
|
||||||
.get(format!("https://{}/iserv/admin/hosts", args.hostname))
|
.get(format!("https://{}/iserv/admin/hosts", args.hostname))
|
||||||
.header(header.0, header.to_owned().1)
|
.header(header.0, header.to_owned().1)
|
||||||
.query(¶ms)
|
.query(¶ms)
|
||||||
.send()?;
|
.send()?;
|
||||||
|
|
||||||
let status = &resp.status();
|
let status = &resp.status();
|
||||||
let plain = &resp.text()?;
|
let plain = &resp.text()?;
|
||||||
|
|
||||||
let content = squeeze(
|
let content = squeeze(
|
||||||
&plain,
|
&plain,
|
||||||
"<script id=\"crud-data\" type=\"application/json\">",
|
"<script id=\"crud-data\" type=\"application/json\">",
|
||||||
"</script>",
|
"</script>",
|
||||||
);
|
);
|
||||||
|
|
||||||
let json: serde_json::Value = serde_json::from_str(content).expect("Bad Jason!");
|
let json: serde_json::Value = serde_json::from_str(content).expect("Bad Jason!");
|
||||||
|
|
||||||
let json_part = &json["data"][0]["name"]["rendered"]
|
let json_part = &json["data"][0]["name"]["rendered"]
|
||||||
.to_string()
|
.to_string()
|
||||||
.replace("\\", "");
|
.replace("\\", "");
|
||||||
|
|
||||||
// skip rest of loop when no entry appeared
|
// skip rest of loop when no entry appeared
|
||||||
if json_part == "null" {
|
if json_part == "null" {
|
||||||
println!("###################################");
|
println!(
|
||||||
println!("No new host: skipping (Retry in 5s)");
|
"###################################
|
||||||
println!("###################################");
|
No new host: skipping (Retry in 5s)
|
||||||
|
###################################
|
||||||
|
");
|
||||||
//sleep for 5 seconds
|
//sleep for 5 seconds
|
||||||
std::thread::sleep(std::time::Duration::from_secs(5));
|
std::thread::sleep(std::time::Duration::from_secs(5));
|
||||||
continue;
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let host_id = squeeze(json_part, "data-host-id=\"", "\"");
|
let host_id = squeeze(json_part, "data-host-id=\"", "\"");
|
||||||
let device_name = squeeze(json_part, "</span>", "</a>");
|
let device_name = squeeze(json_part, "</span>", "</a>");
|
||||||
println!("-----------------------------------");
|
print_banner!("1/7 Found host\nHost id: {}\nDevice name: {}\nReceived response status: {:?}", host_id, device_name, status);
|
||||||
println!("1/7 Found host");
|
|
||||||
println!("Host id: {}", host_id);
|
|
||||||
println!("Device name: {}", device_name);
|
|
||||||
println!("Received response status: {:?}", status);
|
|
||||||
|
|
||||||
let dest = format!("https://{}/iserv/admin/host/edit/{}", args.hostname, &host_id);
|
let dest = format!("https://{}/iserv/admin/host/edit/{}", args.hostname, &host_id);
|
||||||
let resp = client.get(&dest).header(header.0, header.to_owned().1).send()?;
|
let resp = client.get(&dest).header(header.0, header.to_owned().1).send()?;
|
||||||
//TODO Check for non 200 response
|
//TODO Check for non 200 response
|
||||||
|
|
||||||
let status = &resp.status();
|
let status = &resp.status();
|
||||||
|
if !status.is_success() && *status != 300 {
|
||||||
|
eprintln!("WARN: Found invalid Status Code: {status}");
|
||||||
|
return Ok(())
|
||||||
|
}
|
||||||
let plain = &resp.text()?;
|
let plain = &resp.text()?;
|
||||||
|
|
||||||
let mut form_ip: &str = &args.target_ip; //default
|
let mut form_ip: &str = &args.target_ip; //default
|
||||||
|
|
||||||
let form_name = squeeze_html_form(plain, "name=\"host[name]\"", "value=\"", "\"");
|
let form_name = squeeze_html_form(plain, "name=\"host[name]\"", "value=\"", "\"");
|
||||||
let form_tags = squeeze_html_form(plain, "name=\"host[tags][]\"", "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_mac = squeeze_html_form(plain, "name=\"host[mac]\"", "value=\"", "\"");
|
||||||
let form_description = squeeze_html_form(plain, "name=\"host[description]\"", ">", "<");
|
let form_description = squeeze_html_form(plain, "name=\"host[description]\"", ">", "<");
|
||||||
let form_token = squeeze_html_form(plain, "name=\"host[_token]\"", "value=\"", "\"");
|
let form_token = squeeze_html_form(plain, "name=\"host[_token]\"", "value=\"", "\"");
|
||||||
|
|
||||||
let mut form_data = [
|
let mut form_data = [
|
||||||
("host[name]", form_name),
|
("host[name]", form_name),
|
||||||
("host[tags][]", form_tags),
|
("host[tags][]", form_tags),
|
||||||
|
@ -108,42 +101,25 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
("host[actions][submit]", ""),
|
("host[actions][submit]", ""),
|
||||||
("host[_token]", form_token),
|
("host[_token]", form_token),
|
||||||
];
|
];
|
||||||
println!("-----------------------------------");
|
print_banner!("2/7 Received current host properties\nReceived response status: {:?}", status);
|
||||||
println!("2/7 Received current host properties");
|
|
||||||
println!("Received response status: {:?}", status);
|
|
||||||
|
|
||||||
let resp = client
|
let resp = client
|
||||||
.post(&dest)
|
.post(&dest)
|
||||||
.header(header.0, header.to_owned().1)
|
.header(header.0, header.to_owned().1)
|
||||||
.form(&form_data)
|
.form(&form_data)
|
||||||
.send()?;
|
.send()?;
|
||||||
|
|
||||||
let status = &resp.status();
|
let status = &resp.status();
|
||||||
let plain = &resp.text()?;
|
let plain = &resp.text()?;
|
||||||
|
|
||||||
form_ip = squeeze_html_form(plain, "name=\"host[ip]\"", "value=\"", "\"");
|
form_ip = squeeze_html_form(plain, "name=\"host[ip]\"", "value=\"", "\"");
|
||||||
form_data[3] = ("host[ip]", form_ip);
|
form_data[3] = ("host[ip]", form_ip);
|
||||||
|
print_banner!("3/7 Received recommended ip: {}\nReceived response status: {:?}", form_ip, status);
|
||||||
println!("-----------------------------------");
|
|
||||||
println!("3/7 Received recommended ip: {}", form_ip);
|
|
||||||
println!("Received response status: {:?}", status);
|
|
||||||
|
|
||||||
let resp = client
|
let resp = client
|
||||||
.post(&dest)
|
.post(&dest)
|
||||||
.header(header.0, header.to_owned().1)
|
.header(header.0, header.to_owned().1)
|
||||||
.form(&form_data)
|
.form(&form_data)
|
||||||
.send()?;
|
.send()?;
|
||||||
|
|
||||||
let status = &resp.status();
|
let status = &resp.status();
|
||||||
|
print_banner!("4/7 Sucessfully updated host\nReceived response status: {:?}", status);
|
||||||
println!("-----------------------------------");
|
if args.target_compilation.is_none() { return Ok(()) }
|
||||||
println!("4/7 Sucessfully updated host");
|
|
||||||
println!("Received response status: {:?}", status);
|
|
||||||
println!("-----------------------------------");
|
|
||||||
|
|
||||||
if args.target_compilation.is_none() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let resp = client
|
let resp = client
|
||||||
.get(format!("https://{}/iserv/admin/mdm/ios/compilation/edit/{}", args.hostname, args.target_compilation.unwrap()))
|
.get(format!("https://{}/iserv/admin/mdm/ios/compilation/edit/{}", args.hostname, args.target_compilation.unwrap()))
|
||||||
|
@ -154,36 +130,29 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let status = &resp.status();
|
let status = &resp.status();
|
||||||
let plain = &resp.text()?;
|
let plain = &resp.text()?;
|
||||||
|
|
||||||
println!("-----------------------------------");
|
print_banner!("5/7 Received MDM compilation\nReceived response status: {:?}", status);
|
||||||
println!("5/7 Received MDM compilation");
|
|
||||||
println!("Received response status: {:?}", status);
|
|
||||||
println!("-----------------------------------");
|
|
||||||
|
|
||||||
let csrf_token = squeeze_html_form(plain, "name=\"ioscompilation[_token]\"", "value=\"", "\"");
|
|
||||||
|
|
||||||
|
//let csrf_token = squeeze_html_form(plain, "name=\"ioscompilation[_token]\"", "value=\"", "\"");
|
||||||
let mut form_data = Vec::from([
|
let mut form_data = Vec::from([
|
||||||
(
|
(
|
||||||
Cow::Borrowed("ioscompilation[name]"),
|
"ioscompilation[name]",
|
||||||
Cow::Borrowed(squeeze_html_form(plain, "name=\"ioscompilation[name]\"", "value=\"", "\"")),
|
squeeze_html_form(plain, "name=\"ioscompilation[name]\"", "value=\"", "\"")
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
Cow::Borrowed("ioscompilation[description]"),
|
"ioscompilation[description]",
|
||||||
Cow::Borrowed(squeeze_html_form(plain, "name=\"ioscompilation[description]\"", "value=\"", "\"")),
|
squeeze_html_form(plain, "name=\"ioscompilation[description]\"", "value=\"", "\"")
|
||||||
),
|
),
|
||||||
]);
|
].map(|(a, b)| (Cow::Borrowed(a), Cow::Borrowed(b))));
|
||||||
|
|
||||||
// push apps (<option value="52" selected="selected">)
|
// push apps (<option value="52" selected="selected">)
|
||||||
let plain_inner_applications = squeeze_html_form(plain, "name=\"ioscompilation[applications][]\"", ">", "</select>");
|
let plain_inner_applications = squeeze_html_form(plain, "name=\"ioscompilation[applications][]\"", ">", "</select>");
|
||||||
form_data.append(&mut squeeze_loop(plain_inner_applications, "<option value=\"", "\"", " selected=\"selected\"", 3, "ioscompilation[applications][]"));
|
squeeze_loop(&mut form_data, plain_inner_applications, "<option value=\"", "\"", " selected=\"selected\"", 3, "ioscompilation[applications][]");
|
||||||
|
|
||||||
// push profiles
|
// push profiles
|
||||||
let plain_inner_profiles = squeeze_html_form(plain, "name=\"ioscompilation[profiles][]\"", ">", "</select>");
|
let plain_inner_profiles = squeeze_html_form(plain, "name=\"ioscompilation[profiles][]\"", ">", "</select>");
|
||||||
form_data.append(&mut squeeze_loop(plain_inner_profiles, "<option value=\"", "\"", " selected=\"selected\"", 3, "ioscompilation[profiles][]"));
|
squeeze_loop(&mut form_data, plain_inner_profiles, "<option value=\"", "\"", " selected=\"selected\"", 3, "ioscompilation[profiles][]");
|
||||||
|
|
||||||
// push existing devices ([\s\n]* seems not needed)
|
// push existing devices ([\s\n]* seems not needed)
|
||||||
let plain_inner_devices = squeeze_html_form(plain, "name=\"ioscompilation[devices][]\"", ">", "</select>");
|
let plain_inner_devices = squeeze_html_form(plain, "name=\"ioscompilation[devices][]\"", ">", "</select>");
|
||||||
form_data.append(&mut squeeze_loop(plain_inner_devices, "<option value=\"", "\"", " selected=\"selected\"", 3, "ioscompilation[devices][]"));
|
squeeze_loop(&mut form_data, plain_inner_devices, "<option value=\"", "\"", " selected=\"selected\"", 3, "ioscompilation[devices][]");
|
||||||
|
|
||||||
// push new device
|
// push new device
|
||||||
let mdm_new_ipad_id = squeeze_right(plain_inner_devices, "<option value=\"", &format!("\">{}", form_name));
|
let mdm_new_ipad_id = squeeze_right(plain_inner_devices, "<option value=\"", &format!("\">{}", form_name));
|
||||||
if plain_inner_devices.contains(form_name) {
|
if plain_inner_devices.contains(form_name) {
|
||||||
|
@ -191,30 +160,20 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
} else {
|
} else {
|
||||||
eprintln!("Device not found in compilation edit.")
|
eprintln!("Device not found in compilation edit.")
|
||||||
}
|
}
|
||||||
|
|
||||||
// push submit action
|
// push submit action
|
||||||
form_data.push(("ioscompilation[actions][submit]".into(), "".into()));
|
form_data.push(("ioscompilation[actions][submit]".into(), "".into()));
|
||||||
|
|
||||||
//push token
|
//push token
|
||||||
let form_token = squeeze_html_form(plain, "name=\"ioscompilation[_token]\"", "value=\"", "\"");
|
let form_token = squeeze_html_form(plain, "name=\"ioscompilation[_token]\"", "value=\"", "\"");
|
||||||
form_data.push(("ioscompilation[_token]".into(), form_token.into()));
|
form_data.push(("ioscompilation[_token]".into(), form_token.into()));
|
||||||
|
|
||||||
let resp = client
|
let resp = client
|
||||||
.post(format!("https://{}/iserv/admin/mdm/ios/compilation/edit/{}", args.hostname, args.target_compilation.unwrap()))
|
.post(format!("https://{}/iserv/admin/mdm/ios/compilation/edit/{}", args.hostname, args.target_compilation.unwrap()))
|
||||||
.header(header.0, header.to_owned().1)
|
.header(header.0, header.to_owned().1)
|
||||||
.form(&form_data)
|
.form(&form_data)
|
||||||
.send()?;
|
.send()?;
|
||||||
|
|
||||||
let status = &resp.status();
|
let status = &resp.status();
|
||||||
let plain = &resp.text()?;
|
let plain = &resp.text()?;
|
||||||
std::fs::write("device-add.out.html", plain).unwrap();
|
std::fs::write("device-add.out.html", plain).unwrap();
|
||||||
|
print_banner!("6/7 Added device to MDM Compilation\nReceived response status: {:?}", status);
|
||||||
println!("-----------------------------------");
|
|
||||||
println!("6/7 Added device to MDM Compilation");
|
|
||||||
println!("Received response status: {:?}", status);
|
|
||||||
println!("-----------------------------------");
|
|
||||||
|
|
||||||
|
|
||||||
let form_data = [
|
let form_data = [
|
||||||
("iserv_crud_multi_select[confirm]", "apply-changes"),
|
("iserv_crud_multi_select[confirm]", "apply-changes"),
|
||||||
("iserv_crud_multi_select[multi][]", mdm_new_ipad_id),
|
("iserv_crud_multi_select[multi][]", mdm_new_ipad_id),
|
||||||
|
@ -222,28 +181,21 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
//("iserv_crud_multi_select[_token]", csrf_token),
|
//("iserv_crud_multi_select[_token]", csrf_token),
|
||||||
// seems to work with and without token :o (but without it iserv throws a csrf error) // example token: rqB8um_Y6sJ1hCFMeVlg9r3L0v__tH3GS5KcV4Lj_ug
|
// seems to work with and without token :o (but without it iserv throws a csrf error) // example token: rqB8um_Y6sJ1hCFMeVlg9r3L0v__tH3GS5KcV4Lj_ug
|
||||||
];
|
];
|
||||||
|
|
||||||
let resp = client
|
let resp = client
|
||||||
.post(format!("https://{}/iserv/admin/mdm/ios/device/batch", args.hostname))
|
.post(format!("https://{}/iserv/admin/mdm/ios/device/batch", args.hostname))
|
||||||
.header(header.0, header.to_owned().1)
|
.header(header.0, header.to_owned().1)
|
||||||
.form(&form_data)
|
.form(&form_data)
|
||||||
.send()?;
|
.send()?;
|
||||||
|
|
||||||
let status = &resp.status();
|
let status = &resp.status();
|
||||||
let plain = &resp.text()?;
|
let plain = &resp.text()?;
|
||||||
std::fs::write("confirm.out.html", plain).unwrap();
|
std::fs::write("confirm.out.html", plain).unwrap();
|
||||||
|
print_banner!("7/7 Confirmed device actions\nReceived response status: {:?}", status);
|
||||||
|
Ok(())
|
||||||
println!("-----------------------------------");
|
|
||||||
println!("7/7 Confirmed device actions");
|
|
||||||
println!("Received response status: {:?}", status);
|
|
||||||
println!("-----------------------------------");
|
|
||||||
}
|
|
||||||
//Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// helper functions
|
// helper functions
|
||||||
fn squeeze<'input>(input: &'input str, before: &str, after: &str) -> &'input str {
|
|
||||||
|
fn squeeze<'inp>(input: &'inp str, before: &str, after: &str) -> &'inp str {
|
||||||
//find before occurence (excluded from output)
|
//find before occurence (excluded from output)
|
||||||
if let Some(before_bytes) = input.find(before) {
|
if let Some(before_bytes) = input.find(before) {
|
||||||
let adjusted_before_bytes = before_bytes + before.len();
|
let adjusted_before_bytes = before_bytes + before.len();
|
||||||
|
@ -257,13 +209,11 @@ fn squeeze<'input>(input: &'input str, before: &str, after: &str) -> &'input str
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return "";
|
""
|
||||||
}
|
}
|
||||||
|
fn squeeze_right<'inp>(input: &'inp str, before: &str, after: &str) -> &'inp str {
|
||||||
fn squeeze_right<'input>(input: &'input str, before: &str, after: &str) -> &'input str {
|
|
||||||
//find before occurence (excluded from output)
|
//find before occurence (excluded from output)
|
||||||
if let Some(after_bytes) = input.find (after) {
|
if let Some(after_bytes) = input.find (after) {
|
||||||
// let adjusted_before_bytes = after_bytes + after.len();
|
|
||||||
let leftover = &input[..after_bytes];
|
let leftover = &input[..after_bytes];
|
||||||
//find after occurence (excluded from output)
|
//find after occurence (excluded from output)
|
||||||
if let Some(before_bytes) = leftover.rfind(before) {
|
if let Some(before_bytes) = leftover.rfind(before) {
|
||||||
|
@ -274,15 +224,14 @@ fn squeeze_right<'input>(input: &'input str, before: &str, after: &str) -> &'inp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return "";
|
""
|
||||||
}
|
}
|
||||||
|
fn squeeze_html_form<'inp>(
|
||||||
fn squeeze_html_form<'input>(
|
input: &'inp str,
|
||||||
input: &'input str,
|
|
||||||
first: &str,
|
first: &str,
|
||||||
second: &str,
|
second: &str,
|
||||||
last: &str,
|
last: &str,
|
||||||
) -> &'input str {
|
) -> &'inp str {
|
||||||
//find first occurence (broad search)
|
//find first occurence (broad search)
|
||||||
if let Some(from_first_bytes) = input.find(first) {
|
if let Some(from_first_bytes) = input.find(first) {
|
||||||
let after_first_bytes = from_first_bytes + first.len();
|
let after_first_bytes = from_first_bytes + first.len();
|
||||||
|
@ -298,11 +247,17 @@ fn squeeze_html_form<'input>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return "";
|
""
|
||||||
}
|
}
|
||||||
|
fn squeeze_loop<'inp>(
|
||||||
fn squeeze_loop<'input, 'vec> (input: &'input str, before: &str, after: &str, must_include_after: &str, include_after_distance: usize, index_name: &'input str) -> Vec<(Cow<'input, str>, Cow<'input, str>)> {
|
collector: &mut Vec<(Cow<'inp, str>, Cow<'inp, str>)>,
|
||||||
let mut collector: Vec<(Cow<str>, Cow<str>)> = Vec::new();
|
input: &'inp str,
|
||||||
|
before: &str,
|
||||||
|
after: &str,
|
||||||
|
must_include_after: &str,
|
||||||
|
include_after_distance: usize,
|
||||||
|
index_name: &'inp str
|
||||||
|
) {
|
||||||
let mut leftover = input;
|
let mut leftover = input;
|
||||||
//find before occurence (excluded from output)
|
//find before occurence (excluded from output)
|
||||||
while let Some(before_bytes) = leftover.find(before) {
|
while let Some(before_bytes) = leftover.find(before) {
|
||||||
|
@ -321,5 +276,4 @@ fn squeeze_loop<'input, 'vec> (input: &'input str, before: &str, after: &str, mu
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
collector
|
|
||||||
}
|
}
|
Loading…
Reference in New Issue