Initial commit with base functionality
This commit is contained in:
commit
ad542cf940
362
index.php
Normal file
362
index.php
Normal 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>–<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>–<code>299</code>)
|
||||
</summary>
|
||||
<p>
|
||||
200
|
||||
</p>
|
||||
</details>
|
||||
<details>
|
||||
<summary>
|
||||
Redirects (<code>3XX</code>–<code>399</code>)
|
||||
</summary>
|
||||
<p>
|
||||
300
|
||||
</p>
|
||||
</details>
|
||||
<details>
|
||||
<summary>
|
||||
Client error (<code>4XX</code>–<code>499</code>)
|
||||
</summary>
|
||||
<p>
|
||||
100
|
||||
</p>
|
||||
</details>
|
||||
<details>
|
||||
<summary>
|
||||
Server error (<code>5XX</code>–<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
255
style.css
Normal 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%;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user