Skip to content

Commit

Permalink
new: web inspector
Browse files Browse the repository at this point in the history
  • Loading branch information
agrinman committed Jul 13, 2020
1 parent c9a5da1 commit b3bb057
Show file tree
Hide file tree
Showing 17 changed files with 1,345 additions and 60 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/target
tunnelto_server
!tunnelto_server/
.env
2 changes: 2 additions & 0 deletions Cargo.lock

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

5 changes: 3 additions & 2 deletions tunnelto/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ indicatif = "0.15.0"
httparse = "1.3.4"
warp = "0.2.2"
bytes = "0.5.5"
askama = "0.9.0"
askama = { version = "0.9.0", features = ["serde-json"] }
chrono = "0.4.11"
uuid = {version = "0.8.1", features = ["serde", "v4"] }
hyper = "0.13.6"
http-body = "0.3.1"
http-body = "0.3.1"
serde_urlencoded = "0.6.1"
169 changes: 155 additions & 14 deletions tunnelto/src/introspect/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,32 @@ use bytes::Buf;
use uuid::Uuid;
use http_body::Body;

type HttpClient = hyper::Client<hyper::client::HttpConnector>;

#[derive(Debug, Clone)]
pub struct Request {
id: String,
status: u16,
is_replay: bool,
path: String,
method: Method,
headers: HashMap<String, Vec<String>>,
body_data: Vec<u8>,
response_headers: HashMap<String, Vec<String>>,
response_data: Vec<u8>,
timestamp: chrono::NaiveDateTime,
started: chrono::NaiveDateTime,
completed: chrono::NaiveDateTime,
}

#[derive(Debug, Clone, askama::Template)]
#[template(path="base.html")]
struct Inspector {
requests: Vec<Request>
impl Request {
pub fn elapsed(&self) -> String {
let duration = self.completed - self.started;
if duration.num_seconds() == 0 {
format!("{}ms", duration.num_milliseconds())
} else {
format!("{}s", duration.num_seconds())
}
}
}

lazy_static::lazy_static! {
Expand All @@ -52,12 +61,20 @@ impl warp::reject::Reject for ForwardError {}
pub fn start_introspection_server(config: Config) -> IntrospectionAddrs {
let local_addr = format!("localhost:{}", &config.local_port);

let http_client = HttpClient::new();

let get_client = move || {
let client = http_client.clone();
warp::any().map(move || client.clone()).boxed()
};

let intercept = warp::any()
.and(warp::any().map(move || local_addr.clone()))
.and(warp::method())
.and(warp::path::full())
.and(warp::header::headers_cloned())
.and(warp::body::stream())
.and(get_client())
.and_then(forward);

let (forward_address, intercept_server) = warp::serve(intercept).bind_ephemeral(SocketAddr::from(([0,0,0,0], 0)));
Expand All @@ -81,8 +98,22 @@ pub fn start_introspection_server(config: Config) -> IntrospectionAddrs {
);
res
}));

let web_explorer = warp::get().and(warp::path::end()).and_then(inspector).or(css).or(logo);
let forward_clone = forward_address.clone();

let web_explorer = warp::get().and(warp::path::end()).and_then(inspector)
.or(warp::get()
.and(warp::path("detail"))
.and(warp::path::param())
.and_then(request_detail))
.or(warp::post()
.and(warp::path("replay"))
.and(warp::path::param())
.and(get_client())
.and_then(move |id, client| {
replay_request(id, client, forward_clone.clone())
}))
.or(css)
.or(logo);

let (web_explorer_address, explorer_server) = warp::serve(web_explorer).bind_ephemeral(SocketAddr::from(([0,0,0,0], 0)));
tokio::spawn(explorer_server);
Expand All @@ -94,10 +125,11 @@ async fn forward(local_addr: String,
method: Method,
path: FullPath,
mut headers: HeaderMap,
mut body: impl Stream<Item = Result<impl Buf, warp::Error>> + Send + Sync + Unpin + 'static)
-> Result<Box<dyn warp::Reply>, warp::reject::Rejection>
mut body: impl Stream<Item = Result<impl Buf, warp::Error>> + Send + Sync + Unpin + 'static,
client: HttpClient) -> Result<Box<dyn warp::Reply>, warp::reject::Rejection>
{
let now = chrono::Utc::now().naive_utc();
let started = chrono::Utc::now().naive_utc();

let mut request_headers = HashMap::new();
headers.keys().for_each(|k| {
let values = headers.get_all(k).iter().filter_map(|v| v.to_str().ok()).map(|s| s.to_owned()).collect();
Expand All @@ -115,7 +147,6 @@ async fn forward(local_addr: String,
collected.extend_from_slice(chunk.as_ref())
}

let client = hyper::client::Client::new();
let url = format!("http://{}{}", local_addr, path.as_str());

let mut request = hyper::Request::builder()
Expand All @@ -138,7 +169,7 @@ async fn forward(local_addr: String,

let mut response_headers = HashMap::new();
response.headers().keys().for_each(|k| {
let values = headers.get_all(k).iter().filter_map(|v| v.to_str().ok()).map(|s| s.to_owned()).collect();
let values = response.headers().get_all(k).iter().filter_map(|v| v.to_str().ok()).map(|s| s.to_owned()).collect();
response_headers.insert(k.as_str().to_owned(), values);
});

Expand All @@ -163,21 +194,131 @@ async fn forward(local_addr: String,
body_data: collected,
response_headers,
response_data: response_data.clone(),
timestamp: now,
started,
completed: chrono::Utc::now().naive_utc(),
is_replay: false,
};

REQUESTS.write().unwrap().insert(stored_request.id.clone(), stored_request);

Ok(Box::new(warp::http::Response::from_parts(parts, response_data)))
}

#[derive(Debug, Clone, askama::Template)]
#[template(path="index.html")]
struct Inspector {
requests: Vec<Request>
}

#[derive(Debug, Clone, askama::Template)]
#[template(path="detail.html")]
struct InspectorDetail {
request: Request,
incoming: BodyData,
response: BodyData,
}

#[derive(Debug, Clone)]
struct BodyData {
data_type: DataType,
content: Option<String>,
raw: String,
}

impl AsRef<BodyData> for BodyData {
fn as_ref(&self) -> &BodyData {
&self
}
}

#[derive(Debug, Clone)]
enum DataType {
Json,
Unknown
}

async fn inspector() -> Result<Page<Inspector>, warp::reject::Rejection> {
let mut requests:Vec<Request> = REQUESTS.read().unwrap().values().map(|r| r.clone()).collect();
requests.sort_by(|a,b| a.timestamp.cmp(&b.timestamp));
requests.sort_by(|a,b| b.completed.cmp(&a.completed));
let inspect = Inspector { requests };
Ok(Page(inspect))
}

async fn request_detail(rid: String) -> Result<Page<InspectorDetail>, warp::reject::Rejection> {
let request:Request = match REQUESTS.read().unwrap().get(&rid) {
Some(r) => r.clone(),
None => return Err(warp::reject::not_found())
};

let detail = InspectorDetail{
incoming: get_body_data(&request.body_data),
response: get_body_data(&request.response_data),
request,
};

Ok(Page(detail))
}

fn get_body_data(input: &[u8]) -> BodyData {
let mut body = BodyData {
data_type: DataType::Unknown,
content: None,
raw: std::str::from_utf8(input).map(|s| s.to_string()).unwrap_or("No UTF-8 Data".to_string())
};

match serde_json::from_slice::<serde_json::Value>(input) {
Ok(serde_json::Value::Object(map)) => {
body.data_type = DataType::Json;
body.content = serde_json::to_string_pretty(&map).ok();
},
Ok(serde_json::Value::Array(arr)) => {
body.data_type = DataType::Json;
body.content = serde_json::to_string_pretty(&arr).ok();
},
_ => {}
}

body
}

async fn replay_request(rid: String, client: HttpClient, addr: SocketAddr) -> Result<Box<dyn warp::Reply>, warp::reject::Rejection> {
let request:Request = match REQUESTS.read().unwrap().get(&rid) {
Some(r) => r.clone(),
None => return Err(warp::reject::not_found())
};

let url = format!("http://localhost:{}{}", addr.port(), &request.path);

let mut new_request = hyper::Request::builder()
.method(request.method)
.uri(url.parse::<hyper::Uri>().map_err(|e| {
log::error!("invalid incoming url: {}, error: {:?}", url, e);
warp::reject::custom(ForwardError::InvalidURL)
})?);

for (header, values) in &request.headers {
for v in values {
new_request = new_request.header(header, v)
}
}

let new_request = new_request.body(hyper::Body::from(request.body_data)).map_err(|e| {
log::error!("failed to build request: {:?}", e);
warp::reject::custom(ForwardError::InvalidRequest)
})?;

let _ = client.request(new_request).await.map_err(|e| {
log::error!("local server error: {:?}", e);
warp::reject::custom(ForwardError::LocalServerError)
})?;

let response = warp::http::Response::builder()
.status(warp::http::StatusCode::SEE_OTHER)
.header(warp::http::header::LOCATION, "/")
.body(b"".to_vec());

Ok(Box::new(response))
}

struct Page<T>(T);

Expand Down
45 changes: 1 addition & 44 deletions tunnelto/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,50 +31,7 @@ <h1 class="title has-text-white is-family-code">
</div>
</section>
<section class="section">
{% if requests.is_empty() %}
<p class="is-size-6 has-text-centered has-text-white is-family-code mb-4">No requests yet</p>
{% else %}
<div class="table-container px-2">
<table class="table with-lightgray-border is-hoverable is-fullwidth">
<thead class="has-text-left">
<th class="">
Timestamp
</th>
<th>Status</th>
<th>Method</th>
<th>Path</th>
<th>Req.</th>
<th>Resp.</th>
</thead>
<tbody>
{% for r in requests %}
<tr>
<td class="is-narrow">
<span class="">{{r.timestamp}}</span>
</td>
<td class="is-narrow has-text-weight-bold">
<span class="">{{r.status}}</span>
</td>
<td class="is-narrow is-family-code is-uppercase">
<span class="">{{r.method}}</span>
</td>
<td>
<span class="is-family-code">{{r.path}}</span>
</td>
<td class="is-narrow">
<span class="">{{r.body_data.len()/1024}} KB</span>
</td>
<td class="is-narrow">
<span class="">{{r.response_data.len() / 1024}} KB</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}


{% block content %}{% endblock %}
</section>
</body>
</html>
Loading

0 comments on commit b3bb057

Please sign in to comment.