Initial commit with base functionality

This commit is contained in:
Andrew Gioia 2023-12-07 13:45:54 -05:00
commit ad542cf940
No known key found for this signature in database
GPG Key ID: FC09694A000800C8
2 changed files with 617 additions and 0 deletions

362
index.php Normal file
View File

@ -0,0 +1,362 @@
<?php
class RedirectCheck
{
// constants
const USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36";
const ERROR_CURL_INIT = "Couldn't initialize a cURL handle";
const ERROR_CURL_CODE = "Could not curl_getinfo the HTTP code";
const ERROR_CURL_REDIRECT = "Could not curl_getinfo the redirect URL";
// properties
public string $url;
public string $next;
public int $code;
public int $step;
public bool $redirect;
public array $path;
public array $error;
// constructor
public function __construct(string $url)
{
$this->url = $url;
$this->next = '';
$this->code = 0;
$this->step = 1;
$this->redirect = false;
$this->error = [];
$this->path[$this->step] = [
'step' => $this->step,
'url' => $this->url,
'code' => null,
'next' => null ];
}
// create an error message
private function setError(array $error): void
{
$this->error = $error;
}
// add an entry to the URL path
private function addPath(int $step, array $path): void
{
$this->path[$step] = $path;
}
// update a path entry
public function updatePath(int $step, string $key, string $value): void
{
$this->path[$step][$key] = $value;
}
// create a curl request for a URL
public function getHttpCode()
{
// initate curl request
$ch = curl_init();
if (!$ch) {
$this->setError = ['type' => 'curl', 'message' => self::ERROR_CURL_INIT];
return false;
}
// set request headers and execute
$response = curl_setopt($ch, CURLOPT_URL, $this->url);
$response = curl_setopt($ch, CURLOPT_HEADER, true); // enable this for debugging
$response = curl_setopt($ch, CURLOPT_HTTPGET, true); // redundant but making sure it's a GET
$response = curl_setopt($ch, CURLOPT_NOBODY, false); // settings this to true was returning 405s
$response = curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
$response = curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // return output instead of going to screen
$response = curl_setopt($ch, CURLOPT_TIMEOUT, 60);
$response = curl_setopt($ch, CURLOPT_USERAGENT, self::USER_AGENT);
$response = curl_exec($ch);
// check for a response
if (empty($response))
{
$this->setError(['type' => 'curl', 'message' => curl_error($ch)]);
curl_close($ch);
}
else
{
// get the http status code
if (curl_getinfo($ch, CURLINFO_HTTP_CODE))
{
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$this->code = ($code) ? $code : 0;
if ($code == 200) {
$this->updatePath($this->step, 'code', $this->code);
return true;
}
}
else
{
$this->code = 0;
$this->setError(['type' => 'curl', 'message' => self::ERROR_CURL_CODE]);
return false;
}
// get any redirect url to follow next
if (curl_getinfo($ch, CURLINFO_REDIRECT_URL))
{
$next = curl_getinfo($ch, CURLINFO_REDIRECT_URL);
$this->redirect = ($next) ? true : false;
$this->next = ($next) ? $next : '';
}
else
{
$this->redirect = false;
$this->next = '';
}
// close the session
curl_close($ch);
// update the current path
$this->updatePath($this->step, 'code', $this->code);
$this->updatePath($this->step, 'next', $this->next);
// start the next path
$this->step++;
$this->addPath(
$this->step,
[
'step' => $this->step,
'url' => $this->next,
'code' => null,
'next' => null
]);
return true;
}
// return false if we get here
return false;
}
// get the final URL redirect
public function getFinalRedirect(): string
{
$last = end($this->path);
return $last['url'];
}
}
// handle form submissions
if (isset($_POST['url']))
{
// check that we got a valid URL
$url = (filter_var(trim($_POST['url']), FILTER_VALIDATE_URL))
? trim($_POST['url'])
: false;
// if so, start up the redirect checks
if ($url)
{
// make a request for this url and add to the path
$request = new RedirectCheck($url);
$code = '';
do {
// set the URL
$request->url = $url;
// make the curl request and update the path
$request->getHttpCode();
// end on an error
if ($request->error)
{
break;
}
// if we have a redirect to follow, update our working $url
$url = ($request->next) ? $request->next : false;
// update our code
$code = ($request->code) ? $request->code : false;
} while ($code != 200);
}
}
?>
<!DOCTYPE html>
<html>
<head>
<title>Redirect Checker</title>
<link rel="stylesheet" type="text/css" href="style.css" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<header>
<menu>
<h1>Redirect Checker</h1>
<nav>
<a href="/"><span>Check a new URL</span></a>
</nav>
</menu>
<form method="post" action="index.php" type="application/x-www-form-urlencoded">
<input type="search" value="" placeholder="URL to check..." name="url">
<button type="submit">Check</button>
</form>
</header>
<main>
<?php
if (isset($request) && count($request->path) > 0)
{
$steps = count($request->path) - 1;
$count = ($steps == 1) ? '1 redirect' : $steps.' redirects';
echo "
<article>
<h2>
Final destination
</h2>
<p>
The URL you traced had ".$count." and ended here:
</p>
<p class=\"final\">
<a href=\"".$request->getFinalRedirect()."\" target=\"blank\">
".$request->getFinalRedirect()."
</a>
</p>
<h3>
Redirect trace
</h3>
<table>
<thead>
<tr>
<th>Step</th>
<th>Request</th>
<th>Code</th>
<th>Redirect</th>
</tr>
</thead>
<tbody>";
foreach ($request->path as $step)
{
$item = $step['step'];
$url = $step['url'];
$code = $step['code'];
$next = ($step['next']) ? 'Yes' : '--';
echo "
<tr>
<td>".$item."</td>
<td>".$url."</td>
<td><code>".$code."</code></td>
<td>".$next."</td>
</tr>";
}
echo "
</tbody>
<caption>
User agent: ".$request::USER_AGENT."
</caption>
</table>
</article>";
}
else
{
echo "
<article>
<h2>
Trace a URL's redirects
</h2>
<p>
Enter a URL in the search box above to derive the final resolved URL after all redirects. The script runs recursive cURL requests until we get a <code>200</code> status code. This is helpful to get around link tracking or original URLs that Pi-hole outright blocks (like email links).
</p>
<p>
I used to use <a href=\"https://wheregoes.com\">wheregoes.com</a> which is a good, reliable service, but decided to roll my own for privacy reasons. Absolutely nothing is logged as all URL searches are via POST and that's not currently included in my nginx logs.
</p>
<h3>
Technical details
</h3>
<p>
User agent
</p>
<p>
Other cURL settings
</p>
</article>
";
}
?>
<aside>
<h3>
HTTP response status codes
</h3>
<details>
<summary>
Informational (<code>1XX</code>&ndash;<code>199</code>)
</summary>
<dl>
<dt>
<label for="100">
<code>100</code> Continue
</label>
</dt>
<input type="checkbox" id="100">
<dd>Continue the request or ignore the response if the request is already finished.</dd>
<dt>
<label for="101">
<code>101</code> Switching Protocols
</label>
</dt>
<input type="checkbox" id="101">
<dd>Sent in response to a client's <code>Upgrade</code> request header and indicates the protocol the server is switching to.</dd>
<dt>
<label for="102">
<code>102</code> Processing
</label>
</dt>
<input type="checkbox" id="102">
<dd>Server has received and is processing the request, but no response is available yet.</dd>
<dt>
<label for="103">
<code>103</code> Early Hints
</label>
</dt>
<input type="checkbox" id="103">
<dd>Intended alongside <code>Link</code> header to let the user agent preconnect or start preloading resources while the server prepares a response.</dd>
</dl>
</details>
<details>
<summary>
Successful (<code>2XX</code>&ndash;<code>299</code>)
</summary>
<p>
200
</p>
</details>
<details>
<summary>
Redirects (<code>3XX</code>&ndash;<code>399</code>)
</summary>
<p>
300
</p>
</details>
<details>
<summary>
Client error (<code>4XX</code>&ndash;<code>499</code>)
</summary>
<p>
100
</p>
</details>
<details>
<summary>
Server error (<code>5XX</code>&ndash;<code>599</code>)
</summary>
<p>
500
</p>
</details>
<p>
See <a href="https://httpwg.org/specs/rfc9110.html#overview.of.status.codes">RFC 9110</a> for official documentation on each status code.
</p>
</aside>
</main>
</body>
</html>

255
style.css Normal file
View File

@ -0,0 +1,255 @@
html {
font-size: 16px;
height: 100%;
margin: 0;
padding: 0;
}
body {
align-items: center;
background: #f5f5f5;
border-top: 5px solid #1a1a1a;
color: #1a1a1a;
display: flex;
flex-direction: column;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
font-size: 1rem;
min-height: calc(100% - 5px);
margin: 0;
}
header {
align-items: center;
background: #fff;
display: flex;
flex-direction: column;
padding-bottom: 3rem;
width: 100%;
}
menu {
align-items: center;
align-self: stretch;
display: flex;
flex-direction: row;
justify-content: space-between;
padding: 1.5rem min(5.5%, 2.5rem) 3rem;
margin: 0;
}
a {
color: #1a1a1a;
text-decoration: underline;
text-decoration-color: transparent;
transition: text-decoration-color 150ms ease-in-out;
}
a:hover {
color: #000;
text-decoration-color: #000;
}
nav a {
font-size: 1.1rem;
line-height: 1.6rem;
}
h1 {
font-size: 1.25rem;
margin: 0;
}
form {
align-items: stretch;
display: flex;
justify-content: center;
position: relative;
width: 90%;
}
input[type="search"] {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'%3E%3C/line%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: 1.25rem 50%;
border: 0.15rem solid #1a1a1a;
border-radius: 2rem;
box-shadow: inset 0 0.25rem 0 0 #f0eded;
color: #1a1a1a;
font-size: 1.25rem;
height: 3.6rem;
padding: 0 8.5rem 0 3.5rem;
transition: box-shadow 150ms ease-in-out;
width: 100%;
}
input[type="search"]:focus {
outline: none;
box-shadow: inset 0 0.25rem 0 0 #f0eded, 0 0 0 1px #fff, 0 0 0 0.25rem #39b9e4;
}
button[type="submit"] {
background: #fff;
border: 0.15rem solid #1a1a1a;
border-radius: 0 2rem 2rem 0;
box-shadow: inset 0 0.25rem 0 0 #f0eded;
color: #1a1a1a;
cursor: pointer;
height: 100%;
font-size: 1.25rem;
padding: 0.75rem 2rem 0.8rem 1.75rem;
position: absolute;
top: 0;
transition: background 150ms ease-in-out;
right: 0;
}
button[type="submit"]:hover {
background-color: #f0eded;
}
main {
align-items: center;
align-self: stretch;
display: flex;
flex-direction: column;
justify-content: flex-start;
gap: 4rem;
padding-top: 2rem;
height: 100%;
}
article {
width: 86%;
}
aside {
width: 86%;
}
details {
margin-bottom: 0.5rem;
}
summary {
cursor: pointer;
}
summary:hover {
color: #000;
}
details dl {
margin: 0.5rem 0 0;
padding-left: 1.1rem;
}
details dl dt {
margin: 0 0 0.5rem;
}
details dl dd {
display: none;
font-size: 0.875rem;
line-height: 1.2rem;
margin-left: 0;
padding: 0 0 1rem 0.1rem;
}
details dl input[type="checkbox"] {
display: none;
}
details dl input[type="checkbox"]:checked + dd {
display: block;
}
details[open] + details {
margin-top: 1rem;
}
code {
font-family: Menlo, monospace;
font-size: 0.9rem;
background: #fff;
padding: 0.1rem 0.2rem;
border-radius: 0.25rem;
}
h2 {
margin: 0 0 1.5rem;
}
p {
line-height: 1.5rem;
}
p a {
font-weight: 600;
}
p.final {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='lucide lucide-arrow-right'%3E%3Cpath d='M5 12h14'/%3E%3Cpath d='m12 5 7 7-7 7'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: 0 0.1rem;
font-size: 1.2rem;
padding: 0 0 2rem 2rem;
word-wrap: anywhere;
}
h3 {
margin: 0 0 1.5rem;
}
p + h3 {
margin-top: 2rem;
}
table {
border-spacing: 0;
padding: 0;
width: 100%;
}
th {
border-bottom: 2px solid #1a1a1a;
color: #4a4a4a;
font-size: 0.9rem;
padding: 0 1rem 0.75rem;
text-transform: uppercase;
text-align: left;
}
td {
background-color: #fafafa;
border-bottom: 1px solid #1a1a1a;
padding: 1rem;
vertical-align: top;
word-wrap: anywhere;
}
tbody tr:nth-child(odd) td {
background-color: #fff;
}
caption {
caption-side: bottom;
line-height: 1.5rem;
padding: 1rem;
text-align: left;
}
/* laptop */
@media screen and (min-width: 1024px) {
menu {
padding: 1.5rem 2.5rem 3rem;
}
h1 {
font-size: 1.75rem;
}
form {
width: 75%;
}
main {
align-items: flex-start;
flex-direction: row;
justify-content: center;
gap: 4rem;
}
article {
width: 60%;
}
aside {
width: 20%;
}
}
/* desktop */
@media screen and (min-width: 1440px) {
form {
width: 66.6666666667%;
}
article {
width: calc(50% - 5rem);
}
aside{
width: 16.6667%;
}
}