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 = `
This API allows you to programmatically track and analyze URL redirect chains with detailed information.
The API is limited to 100 requests per hour per IP address.
Track a URL and get the full redirect chain using a POST request.
| 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 |
curl -X POST http://localhost:${PORT}/api/v1/track \\
-H "Content-Type: application/json" \\
-d '{
"url": "github.com",
"method": "GET"
}'
Track a URL and get the full redirect chain using a GET request with query parameters.
| 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 |
curl "http://localhost:${PORT}/api/v1/track?url=github.com&method=GET"
{
"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
]
}
}
`;
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}`);
});