const express = require('express'); const axios = require('axios'); const path = require('path'); const https = require('https'); const rateLimit = require('express-rate-limit'); const app = express(); const PORT = 3333; // Set up rate limiter: maximum of 100 requests per hour const apiLimiter = rateLimit({ windowMs: 60 * 60 * 1000, // 1 hour max: 100, // limit each IP to 100 requests per windowMs message: { error: 'Too many requests, please try again later.' } }); app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use(express.static(path.join(__dirname, 'public'))); // Track redirects function async function trackRedirects(url, redirects = [], options = {}) { const startTime = Date.now(); const currentRedirect = { url, timestamp: startTime, isSSL: url.toLowerCase().startsWith('https://') }; // Add the current URL to the redirects array if (redirects.length === 0) { redirects.push(currentRedirect); } try { // Prepare request config const config = { method: options.method || 'GET', url: url, maxRedirects: 0, validateStatus: status => status >= 200 && status < 600, timeout: 15000, responseType: 'text', decompress: true, headers: {} }; // Add user agent if provided if (options.userAgent) { config.headers['User-Agent'] = options.userAgent; } // Add HTTPS agent for SSL info config.httpsAgent = new https.Agent({ rejectUnauthorized: false, // Allow self-signed certs for testing checkServerIdentity: (host, cert) => { // Capture certificate info return undefined; // Allow connection } }); // Make the request const response = await axios(config); // Calculate the duration for this request const endTime = Date.now(); // Get response metadata const metadata = { status: response.status, statusText: response.statusText, headers: response.headers, contentType: response.headers['content-type'], contentLength: response.headers['content-length'], server: response.headers['server'], date: response.headers['date'], protocol: response.request.protocol, method: config.method }; // Get SSL certificate info if HTTPS let sslInfo = null; if (url.toLowerCase().startsWith('https://')) { try { const urlObj = new URL(url); const socket = response.request.socket; if (socket && socket.getPeerCertificate) { const cert = socket.getPeerCertificate(true); sslInfo = { valid: socket.authorized, issuer: cert.issuer, subject: cert.subject, validFrom: cert.valid_from, validTo: cert.valid_to, fingerprint: cert.fingerprint }; } } catch (error) { sslInfo = { error: 'Failed to retrieve SSL info' }; } } // Get response body (truncate if too large) let responseBody = response.data; if (typeof responseBody === 'string' && responseBody.length > 5000) { responseBody = responseBody.substring(0, 5000) + '... [truncated]'; } // Update the current redirect with all the detailed info if (redirects.length > 0 && redirects[redirects.length - 1].url === url) { redirects[redirects.length - 1].duration = endTime - startTime; redirects[redirects.length - 1].statusCode = response.status; redirects[redirects.length - 1].statusText = response.statusText; redirects[redirects.length - 1].metadata = metadata; redirects[redirects.length - 1].responseBody = responseBody; redirects[redirects.length - 1].sslInfo = sslInfo; } else { currentRedirect.duration = endTime - startTime; currentRedirect.statusCode = response.status; currentRedirect.statusText = response.statusText; currentRedirect.metadata = metadata; currentRedirect.responseBody = responseBody; currentRedirect.sslInfo = sslInfo; } // Check if we have a redirect if (response.status >= 300 && response.status < 400 && response.headers.location) { // Get the next URL let nextUrl = response.headers.location; // Handle relative URLs if (!nextUrl.startsWith('http')) { const baseUrl = new URL(url); nextUrl = new URL(nextUrl, baseUrl.origin).href; } // Create the next redirect object const nextRedirect = { url: nextUrl, timestamp: endTime, isSSL: nextUrl.toLowerCase().startsWith('https://') }; // Add to redirects array redirects.push(nextRedirect); // Continue following redirects (but always use GET for subsequent requests per HTTP spec) const nextOptions = { ...options, method: 'GET' }; return await trackRedirects(nextUrl, redirects, nextOptions); } else { // This is the final page if (redirects.length > 0 && redirects[redirects.length - 1].url === url) { redirects[redirects.length - 1].final = true; } else { currentRedirect.final = true; redirects.push(currentRedirect); } return redirects; } } catch (error) { // Handle errors const endTime = Date.now(); // Update the current redirect with the duration and error info if (redirects.length > 0 && redirects[redirects.length - 1].url === url) { redirects[redirects.length - 1].duration = endTime - startTime; redirects[redirects.length - 1].error = error.message; redirects[redirects.length - 1].final = true; // Try to get response info from error object if available if (error.response) { redirects[redirects.length - 1].statusCode = error.response.status; redirects[redirects.length - 1].statusText = error.response.statusText; redirects[redirects.length - 1].metadata = { status: error.response.status, statusText: error.response.statusText, headers: error.response.headers, method: options.method }; } } else { currentRedirect.duration = endTime - startTime; currentRedirect.error = error.message; currentRedirect.final = true; // Try to get response info from error object if available if (error.response) { currentRedirect.statusCode = error.response.status; currentRedirect.statusText = error.response.statusText; currentRedirect.metadata = { status: error.response.status, statusText: error.response.statusText, headers: error.response.headers, method: options.method }; } redirects.push(currentRedirect); } return redirects; } } // Original endpoint (now deprecated but maintained for backward compatibility) app.post('/api/track', async (req, res) => { const { url, method = 'GET', userAgent } = req.body; if (!url) { return res.status(400).json({ error: 'URL is required' }); } try { // Ensure URL has a protocol let inputUrl = url; if (!inputUrl.startsWith('http://') && !inputUrl.startsWith('https://')) { inputUrl = 'http://' + inputUrl; } // Set up request options const options = { method: method.toUpperCase(), userAgent }; const redirectChain = await trackRedirects(inputUrl, [], options); res.json({ redirects: redirectChain }); } catch (error) { console.error('Error:', error); res.status(500).json({ error: 'Failed to track redirects' }); } }); // New versioned API endpoints with rate limiting // API v1 track endpoint app.post('/api/v1/track', apiLimiter, async (req, res) => { const { url, method = 'GET', userAgent } = req.body; if (!url) { return res.status(400).json({ error: 'URL is required', status: 400, success: false }); } try { // Ensure URL has a protocol let inputUrl = url; if (!inputUrl.startsWith('http://') && !inputUrl.startsWith('https://')) { inputUrl = 'http://' + inputUrl; } // Set up request options const options = { method: method.toUpperCase(), userAgent }; const redirectChain = await trackRedirects(inputUrl, [], options); // Format the response in a more standardized API format res.json({ success: true, status: 200, data: { url: inputUrl, method: options.method, redirectCount: redirectChain.length - 1, finalUrl: redirectChain[redirectChain.length - 1].url, finalStatusCode: redirectChain[redirectChain.length - 1].statusCode, redirects: redirectChain } }); } catch (error) { console.error('Error:', error); res.status(500).json({ error: 'Failed to track redirects', message: error.message, status: 500, success: false }); } }); // API v1 track endpoint with GET method support (for easy browser/curl usage) app.get('/api/v1/track', apiLimiter, async (req, res) => { const { url, method = 'GET', userAgent } = req.query; if (!url) { return res.status(400).json({ error: 'URL parameter is required', status: 400, success: false }); } try { // Ensure URL has a protocol let inputUrl = url; if (!inputUrl.startsWith('http://') && !inputUrl.startsWith('https://')) { inputUrl = 'http://' + inputUrl; } // Set up request options const options = { method: (method || 'GET').toUpperCase(), userAgent }; const redirectChain = await trackRedirects(inputUrl, [], options); // Format the response in a more standardized API format res.json({ success: true, status: 200, data: { url: inputUrl, method: options.method, redirectCount: redirectChain.length - 1, finalUrl: redirectChain[redirectChain.length - 1].url, finalStatusCode: redirectChain[redirectChain.length - 1].statusCode, redirects: redirectChain } }); } catch (error) { console.error('Error:', error); res.status(500).json({ error: 'Failed to track redirects', message: error.message, status: 500, success: false }); } }); // API documentation endpoint app.get('/api/docs', (req, res) => { const apiDocs = ` URL Redirect Tracker API

URL Redirect Tracker API Documentation

This API allows you to programmatically track and analyze URL redirect chains with detailed information.

Rate Limiting

The API is limited to 100 requests per hour per IP address.

Endpoints

POST /api/v1/track

Track a URL and get the full redirect chain using a POST request.

Request Parameters (JSON Body)

Parameter Type Required Description
url string Yes The URL to track (e.g., "example.com")
method string No HTTP method (GET, HEAD, POST). Default: "GET"
userAgent string No Custom User-Agent header

Example Request

curl -X POST http://localhost:${PORT}/api/v1/track \\
  -H "Content-Type: application/json" \\
  -d '{
    "url": "github.com",
    "method": "GET"
  }'
  

GET /api/v1/track

Track a URL and get the full redirect chain using a GET request with query parameters.

Request Parameters (Query String)

Parameter Type Required Description
url string Yes The URL to track (e.g., "example.com")
method string No HTTP method (GET, HEAD, POST). Default: "GET"
userAgent string No Custom User-Agent header

Example Request

curl "http://localhost:${PORT}/api/v1/track?url=github.com&method=GET"
  

Response Format

{
  "success": true,
  "status": 200,
  "data": {
    "url": "http://github.com",
    "method": "GET",
    "redirectCount": 1,
    "finalUrl": "https://github.com/",
    "finalStatusCode": 200,
    "redirects": [
      // Array of redirect objects with detailed information
    ]
  }
}
  

Back to URL Redirect Tracker

`; res.send(apiDocs); }); app.get('/', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'index.html')); }); app.listen(PORT, () => { console.log(`Server running on http://localhost:${PORT}`); });