2024-04-06 02:41:10 +00:00
|
|
|
use std::net::SocketAddr;
|
|
|
|
|
|
|
|
use axum::extract::{ConnectInfo, Path};
|
|
|
|
use axum::http::HeaderMap;
|
|
|
|
use axum::http::StatusCode;
|
2024-04-06 13:19:28 +00:00
|
|
|
use axum::response::{Html, IntoResponse};
|
2024-04-06 02:41:10 +00:00
|
|
|
use axum::Extension;
|
|
|
|
|
|
|
|
use info_utils::prelude::*;
|
|
|
|
|
|
|
|
use crate::ServerState;
|
|
|
|
use crate::UrlRow;
|
|
|
|
|
2024-04-06 13:27:12 +00:00
|
|
|
pub async fn index(Extension(state): Extension<ServerState>) -> Html<String> {
|
|
|
|
Html(format!(
|
|
|
|
r#"
|
|
|
|
<!DOCTYPE html>
|
|
|
|
<html>
|
|
|
|
<head>
|
|
|
|
<title>{} URL Shortener</title>
|
|
|
|
</head>
|
|
|
|
</html>
|
|
|
|
<body>
|
|
|
|
<pre>{} URL shortener</pre>
|
|
|
|
<a href="/create">create</a>
|
|
|
|
</body>
|
|
|
|
"#,
|
|
|
|
state.host, state.host
|
|
|
|
))
|
2024-04-06 02:41:10 +00:00
|
|
|
}
|
|
|
|
|
2024-04-06 13:19:28 +00:00
|
|
|
/// # Panics
|
|
|
|
/// Will panic if `parse()` fails
|
|
|
|
pub async fn id(
|
2024-04-06 02:41:10 +00:00
|
|
|
headers: HeaderMap,
|
|
|
|
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
|
|
|
Extension(state): Extension<ServerState>,
|
|
|
|
Path(id): Path<String>,
|
|
|
|
) -> impl IntoResponse {
|
|
|
|
let mut show_request = false;
|
|
|
|
log!("Request for '{}' from {}", id.clone(), addr.ip());
|
|
|
|
let mut use_id = id;
|
|
|
|
if use_id.ends_with('+') {
|
|
|
|
show_request = true;
|
|
|
|
use_id.pop();
|
|
|
|
}
|
|
|
|
|
2024-04-06 13:19:28 +00:00
|
|
|
let item: Result<UrlRow, sqlx::Error> =
|
|
|
|
sqlx::query_as("SELECT * FROM chela.urls WHERE id = $1")
|
|
|
|
.bind(use_id)
|
|
|
|
.fetch_one(&state.db_pool)
|
|
|
|
.await;
|
2024-04-06 02:41:10 +00:00
|
|
|
if let Ok(it) = item {
|
|
|
|
if url::Url::parse(&it.url).is_ok() {
|
|
|
|
if show_request {
|
|
|
|
return Html(format!(
|
2024-04-06 13:19:28 +00:00
|
|
|
r#"<pre>http://{}/{} -> <a href="{}"">{}</a></pre>"#,
|
2024-04-06 02:41:10 +00:00
|
|
|
state.host, it.id, it.url, it.url
|
|
|
|
))
|
|
|
|
.into_response();
|
|
|
|
}
|
2024-04-06 13:19:28 +00:00
|
|
|
log!("Redirecting {} -> {}", it.id, it.url);
|
|
|
|
save_analytics(headers, it.clone(), addr, state).await;
|
|
|
|
let mut response_headers = HeaderMap::new();
|
|
|
|
response_headers.insert("Cache-Control", "private, max-age=90".parse().unwrap());
|
|
|
|
response_headers.insert("Location", it.url.parse().unwrap());
|
|
|
|
return (
|
|
|
|
StatusCode::MOVED_PERMANENTLY,
|
|
|
|
response_headers,
|
|
|
|
Html(format!(
|
|
|
|
r#"Redirecting to <a href="{}">{}</a>"#,
|
|
|
|
it.url, it.url
|
|
|
|
)),
|
|
|
|
)
|
|
|
|
.into_response();
|
2024-04-06 02:41:10 +00:00
|
|
|
}
|
2024-04-06 13:19:28 +00:00
|
|
|
} else if let Err(err) = item {
|
|
|
|
warn!("{}", err);
|
|
|
|
return (
|
|
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
|
|
Html(format!("<pre>Internal error: {err}.</pre>")),
|
|
|
|
)
|
|
|
|
.into_response();
|
2024-04-06 02:41:10 +00:00
|
|
|
}
|
|
|
|
|
2024-04-06 13:19:28 +00:00
|
|
|
(StatusCode::NOT_FOUND, Html("<pre>Not found.</pre>")).into_response()
|
2024-04-06 02:41:10 +00:00
|
|
|
}
|
|
|
|
|
2024-04-06 13:19:28 +00:00
|
|
|
async fn save_analytics(headers: HeaderMap, item: UrlRow, addr: SocketAddr, state: ServerState) {
|
2024-04-06 02:41:10 +00:00
|
|
|
let id = item.id;
|
|
|
|
let ip = addr.ip().to_string();
|
|
|
|
let referer = match headers.get("referer") {
|
|
|
|
Some(it) => {
|
|
|
|
if let Ok(i) = it.to_str() {
|
|
|
|
Some(i)
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
}
|
|
|
|
None => None,
|
|
|
|
};
|
|
|
|
let user_agent = match headers.get("user-agent") {
|
|
|
|
Some(it) => {
|
|
|
|
if let Ok(i) = it.to_str() {
|
|
|
|
Some(i)
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
}
|
|
|
|
None => None,
|
|
|
|
};
|
|
|
|
|
2024-04-06 13:19:28 +00:00
|
|
|
let res = sqlx::query(
|
2024-04-06 02:41:10 +00:00
|
|
|
"
|
|
|
|
INSERT INTO chela.tracking (id,ip,referrer,user_agent)
|
|
|
|
VALUES ($1,$2,$3,$4)
|
|
|
|
",
|
|
|
|
)
|
2024-04-06 13:19:28 +00:00
|
|
|
.bind(id.clone())
|
|
|
|
.bind(ip.clone())
|
|
|
|
.bind(referer)
|
|
|
|
.bind(user_agent)
|
2024-04-06 02:41:10 +00:00
|
|
|
.execute(&state.db_pool)
|
|
|
|
.await;
|
|
|
|
|
|
|
|
if res.is_ok() {
|
|
|
|
log!("Saved analytics for '{id}' from {ip}");
|
|
|
|
}
|
|
|
|
}
|
2024-04-06 13:19:28 +00:00
|
|
|
|
|
|
|
pub async fn create_id(Extension(state): Extension<ServerState>) -> Html<String> {
|
|
|
|
Html(format!(
|
|
|
|
r#"
|
|
|
|
<!DOCTYPE html>
|
|
|
|
<html>
|
|
|
|
<head>
|
|
|
|
<title>{} URL Shortener</title>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<form action="/" method="post">
|
|
|
|
<label for="url">
|
|
|
|
URL to shorten:
|
|
|
|
<input type="url" name="url" required>
|
|
|
|
</label>
|
2024-04-06 13:27:12 +00:00
|
|
|
<br />
|
2024-04-06 13:19:28 +00:00
|
|
|
<label for="id">
|
|
|
|
ID (optional):
|
|
|
|
<input type="text" name="id">
|
|
|
|
</label>
|
2024-04-06 13:27:12 +00:00
|
|
|
<br />
|
2024-04-06 13:19:28 +00:00
|
|
|
<input type="submit" value="create">
|
|
|
|
</form>
|
|
|
|
</body>
|
|
|
|
</html>
|
|
|
|
"#,
|
|
|
|
state.host
|
|
|
|
))
|
|
|
|
}
|