diff --git a/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml new file mode 100644 index 0000000..c50dfc3 --- /dev/null +++ b/.github/workflows/deploy-pages.yml @@ -0,0 +1,47 @@ +name: Deploy PAC Tester to GitHub Pages + +on: + push: + branches: [main] + paths: + - "web/**" + - "src/pac_utils.h" + - "src/pac_utils_dump.c" + - ".github/workflows/deploy-pages.yml" + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +# Only one Pages deployment at a time; skip queued runs but don't cancel +# an in-progress deploy. +concurrency: + group: pages + cancel-in-progress: false + +jobs: + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + # Regenerate web/pac_utils.js from src/pac_utils.h so the deployed + # site is always in sync even if someone forgot to run + # `make -C src pac_utils_js` locally before committing. + - name: Regenerate pac_utils.js + run: make -C src pac_utils_js + + - uses: actions/configure-pages@v5 + + - uses: actions/upload-pages-artifact@v3 + with: + path: web/ + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index 516ce73..a48a23c 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ tools/packages *buildstamp src/spidermonkey/js src/pactester +src/pac_utils_dump # OS specific files .DS_Store diff --git a/src/Makefile b/src/Makefile index 8840e88..5664f10 100644 --- a/src/Makefile +++ b/src/Makefile @@ -108,6 +108,12 @@ testpactester: pactester $(LIBRARY_LINK) echo "Running tests for pactester." NO_INTERNET=$(NO_INTERNET) ../tests/runtests.sh +pac_utils_dump: pac_utils_dump.c pac_utils.h + $(CC) -o pac_utils_dump pac_utils_dump.c + +pac_utils_js: pac_utils_dump + ./pac_utils_dump > ../web/pac_utils.js + docs: ../tools/generatedocs.sh @@ -150,7 +156,7 @@ install-pymod: pymod cd pymod && ARCHFLAGS="" $(PYTHON) setup.py install --root="$(DESTDIR)/" $(EXTRA_ARGS) clean: - rm -f $(LIBRARY_LINK) $(LIBRARY) pacparser.o pactester pymod/pacparser_o_buildstamp libpacparser.a + rm -f $(LIBRARY_LINK) $(LIBRARY) pacparser.o pactester pymod/pacparser_o_buildstamp libpacparser.a pac_utils_dump rm -rf dist cd pymod && $(PYTHON) setup.py clean --all cd quickjs && $(MAKE) clean diff --git a/src/pac_utils_dump.c b/src/pac_utils_dump.c new file mode 100644 index 0000000..cf748af --- /dev/null +++ b/src/pac_utils_dump.c @@ -0,0 +1,23 @@ +// pac_utils_dump.c - Dumps pac_utils.h JavaScript as a pac_utils.js file +// for use by the web-based PAC file tester. +// +// Build: cc -o pac_utils_dump pac_utils_dump.c +// Usage: ./pac_utils_dump > ../web/pac_utils.js +// (or via: make -C src pac_utils_js) + +#include +#include "pac_utils.h" + +int main() { + printf("// Auto-generated from src/pac_utils.h — do not edit manually.\n"); + printf("// Regenerate with: make -C src pac_utils_js\n"); + printf("//\n"); + printf("// Exported as a source string (PAC_UTILS_JS) rather than executed\n"); + printf("// directly, so that the eval'd PAC sandbox can define its own\n"); + printf("// dnsResolve() / myIpAddress() in the same scope as the utility\n"); + printf("// functions — keeping DNS mocking correct via lexical scoping.\n"); + printf("const PAC_UTILS_JS = `\n"); + printf("%s", pacUtils); + printf("`;\n"); + return 0; +} diff --git a/web/DEMO.md b/web/DEMO.md new file mode 100644 index 0000000..5304371 --- /dev/null +++ b/web/DEMO.md @@ -0,0 +1,189 @@ +# PAC File Tester - Demo Guide + +## Quick Start Demo + +### Step 1: Open the Application + +The web server is running! Open your browser and navigate to: +``` +http://localhost:8000 +``` + +### Step 2: Try These Test Scenarios + +#### Test Scenario 1: Plain Hostname +1. **PAC File**: Use the content from `sample.pac` or paste: + ```javascript + function FindProxyForURL(url, host) { + if (isPlainHostName(host)) { + return "DIRECT"; + } + return "PROXY proxy.example.com:8080"; + } + ``` + +2. **Test URL**: `http://localhost` + +3. **Expected Result**: `DIRECT` + +--- + +#### Test Scenario 2: Internal Network Check +1. **PAC File**: Use `sample.pac` (already created) + +2. **Configuration**: + - **Test URL**: `https://internal.company.com` + - **Client IP**: `192.168.1.100` + - **DNS Mappings**: + - `internal.company.com` → `192.168.1.50` + +3. **Expected Result**: `DIRECT` (since both client and destination are internal) + +--- + +#### Test Scenario 3: External Site via Proxy +1. **PAC File**: Use `sample.pac` + +2. **Configuration**: + - **Test URL**: `https://www.google.com` + - **Client IP**: `192.168.1.100` + - **DNS Mappings**: + - `www.google.com` → `142.250.185.46` + +3. **Expected Result**: `PROXY proxy.corporate.com:3128; DIRECT` + +--- + +#### Test Scenario 4: Domain-Based Routing +1. **PAC File**: Paste: + ```javascript + function FindProxyForURL(url, host) { + // Go direct for plain hostnames + if (isPlainHostName(host) || dnsDomainIs(host, '.local')) { + return "DIRECT"; + } + + // Check if host is resolvable (requires DNS mapping) + if (dnsDomainIs(host, '.google.com') && isResolvable(host)) { + return "PROXY google-proxy:8080"; + } + + // Default proxy + return "PROXY proxy.company.com:3128; DIRECT"; + } + ``` + +2. **Configuration**: + - **Test URL**: `https://www.google.com` + - **DNS Mappings**: + - `www.google.com` → `142.250.185.46` + +3. **Expected Result**: `PROXY google-proxy:8080` + +--- + +#### Test Scenario 5: IP Range Matching +1. **PAC File**: Paste: + ```javascript + function FindProxyForURL(url, host) { + // Check if client is in corporate network + if (isInNet(myIpAddress(), "10.10.0.0", "255.255.0.0")) { + return "PROXY internal-proxy:3128"; + } + + // Check if destination is in local network + var resolved = dnsResolve(host); + if (resolved && isInNet(resolved, "192.168.0.0", "255.255.0.0")) { + return "DIRECT"; + } + + return "PROXY external-proxy:8080"; + } + ``` + +2. **Configuration**: + - **Test URL**: `http://intranet.local` + - **Client IP**: `10.10.100.50` + - **DNS Mappings**: + - `intranet.local` → `192.168.10.5` + +3. **Expected Result**: `PROXY internal-proxy:3128` + +--- + +#### Test Scenario 6: Time-Based Routing +1. **PAC File**: Paste: + ```javascript + function FindProxyForURL(url, host) { + // Use fast proxy during business hours (9 AM - 5 PM) + if (timeRange(9, 17)) { + return "PROXY fast-proxy:8080"; + } + + // Use slower proxy outside business hours + return "PROXY slow-proxy:8080"; + } + ``` + +2. **Test URL**: `https://www.example.com` + +3. **Expected Result**: Depends on current time + - During 9 AM - 5 PM: `PROXY fast-proxy:8080` + - Outside those hours: `PROXY slow-proxy:8080` + +--- + +## Testing with Real PAC Files + +### Example: Google PAC File Pattern +```javascript +function FindProxyForURL(url, host) { + // Bypass proxy for local addresses + if (shExpMatch(host, "*.local") || + shExpMatch(host, "127.*") || + shExpMatch(host, "localhost")) { + return "DIRECT"; + } + + // Use specific proxy for certain domains + if (shExpMatch(host, "*.example.com")) { + return "PROXY proxy1.example.com:8080; PROXY proxy2.example.com:8080; DIRECT"; + } + + // Default + return "PROXY default-proxy.example.com:8080; DIRECT"; +} +``` + +**Test with**: +- URL: `http://internal.example.com` +- Expected: `PROXY proxy1.example.com:8080; PROXY proxy2.example.com:8080; DIRECT` + +--- + +## Debugging Tips + +1. **Check the Debug Information**: After testing, scroll down to see: + - Which DNS lookups were performed + - Whether `myIpAddress()` was called + - The extracted hostname from the URL + +2. **Add DNS Mappings Progressively**: Start with no DNS mappings and add them as needed based on errors + +3. **Test Multiple URLs**: Use the same PAC file with different URLs to verify routing logic + +4. **Common Issues**: + - If `dnsResolve(host)` returns null, add a DNS mapping for that host + - If `myIpAddress()` returns unexpected value, set the Client IP field + - Case sensitivity matters for domain names + +## Next Steps + +- Try uploading your own PAC file +- Test against your actual proxy configuration +- Share the web app with your team for testing PAC files +- Deploy to a static hosting service (GitHub Pages, Netlify, etc.) + +## Feedback + +If you find any issues or have suggestions, please report them on the [Pacparser GitHub Issues](https://github.com/manugarg/pacparser/issues). diff --git a/web/README.md b/web/README.md new file mode 100644 index 0000000..5861246 --- /dev/null +++ b/web/README.md @@ -0,0 +1,86 @@ +# PAC File Tester - Web Application + +A client-side web application for testing Proxy Auto-Config (PAC) files without installing any software. + +## Features + +- **100% Client-Side**: All processing happens in your browser - no data is sent to any server +- **Privacy First**: Your PAC files and configuration never leave your machine +- **Full PAC Support**: Implements all standard PAC helper functions +- **Custom DNS Mappings**: Mock DNS resolution for testing different scenarios +- **Debug Information**: See which DNS lookups were performed and what functions were called +- **Simple UI**: Clean, intuitive interface for quick testing + +## Usage + +1. **Open `index.html`** in your web browser + - You can open it directly from your file system + - Or serve it with any web server (e.g., `python -m http.server 8000`) + +2. **Provide Your PAC File** + - Paste the PAC file content directly into the text area, OR + - Upload a `.pac` file using the file picker + +3. **Configure Test Parameters** + - **Test URL**: The URL you want to test (e.g., `https://example.com`) + - **Client IP** (optional): The IP address returned by `myIpAddress()` function + - **DNS Mappings** (optional): Hostname to IP mappings for `dnsResolve()` function + +4. **Click "Test Proxy"** to see the result + +## Example + +### Sample PAC File +A sample PAC file is provided in `sample.pac` for testing. + +### Sample Test Configuration +- **URL**: `https://www.google.com` +- **Client IP**: `192.168.1.100` +- **DNS Mappings**: + - `www.google.com` → `142.250.185.46` + - `proxy.corporate.com` → `10.0.0.1` + +## Supported PAC Functions + +The tester implements all standard PAC helper functions: + +### Network Functions +- `dnsResolve(host)` - Resolves hostname to IP (uses custom mappings) +- `myIpAddress()` - Returns client IP (uses custom IP or 127.0.0.1) +- `isResolvable(host)` - Checks if hostname can be resolved + +### Matching Functions +- `isPlainHostName(host)` - True if hostname has no dots +- `dnsDomainIs(host, domain)` - True if host is in domain +- `localHostOrDomainIs(host, hostdom)` - True if host matches +- `shExpMatch(str, pattern)` - Shell expression matching +- `isInNet(host, pattern, mask)` - IP address range checking +- `dnsDomainLevels(host)` - Number of DNS domain levels + +### Time Functions +- `weekdayRange([wd1, wd2, "GMT"])` - Weekday matching +- `dateRange([day1, month1, year1, day2, month2, year2, "GMT"])` - Date range matching +- `timeRange([hour1, min1, sec1, hour2, min2, sec2, "GMT"])` - Time range matching + +## How It Works + +1. The tester loads the PAC utility functions (ported from Mozilla's implementation) +2. It creates mock implementations of `dnsResolve()` and `myIpAddress()` using your custom mappings +3. Your PAC file is evaluated in a sandboxed context +4. The `FindProxyForURL()` function is called with your test URL +5. Results and debug information are displayed + +## Privacy & Security + +- **No Server Processing**: Everything runs in your browser using JavaScript +- **No Analytics**: No tracking or data collection +- **No External Requests**: Doesn't make any network requests +- **Safe Evaluation**: PAC files are evaluated in isolated JavaScript contexts + +## License + +This web application is part of the Pacparser project and is licensed under LGPL. + +## Contributing + +Found a bug or have a feature request? Please open an issue on the [Pacparser GitHub repository](https://github.com/manugarg/pacparser). diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..ebbee59 --- /dev/null +++ b/web/index.html @@ -0,0 +1,116 @@ + + + + + + + Pacparser - PAC File Parser Library & Tester + + + + +
+
+

🌐 PAC File Tester

+

Test your Proxy Auto-Config files online - 100% client-side, no data sent to servers

+
+ +
+ +
+

PAC File

+
+ +
+ + + +
+
+
+ + +
+

Test Configuration

+ +
+ + +
+ +
+ + + Used for myIpAddress() function +
+ + +
+

DNS Mappings (optional)

+

Provide hostname to IP address mappings for dnsResolve() and isResolvable() + functions

+ +
+
+ + + + +
+
+ + +
+
+ + +
+

About Pacparser

+
+

+ Pacparser is a library to parse proxy auto-config (PAC) files. + PAC files are a widely used method for configuring web browsers to select a proxy server. + Pacparser makes it easy to use PAC files in your own programs. +

+ +
+
+ + + +
+ +
+ + + +
+ + +
+ + + + + + \ No newline at end of file diff --git a/web/pac-tester.js b/web/pac-tester.js new file mode 100644 index 0000000..5e6d749 --- /dev/null +++ b/web/pac-tester.js @@ -0,0 +1,223 @@ +// PAC File Tester - Client-side PAC file evaluator +// Copyright (C) 2025 Manu Garg (based on pacparser library) + +// Global state +let dnsMappingsMap = {}; + +// PAC_UTILS_JS is loaded from pac_utils.js, which is auto-generated from +// src/pac_utils.h via: make -C src pac_utils_js +// +// It is kept as a source string (not executed at load time) so that the +// eval'd sandbox below can define its own dnsResolve() / myIpAddress() +// in the same scope as the utility functions — ensuring DNS mocking +// works correctly via lexical scoping. + +// Add a new DNS mapping row +function addDnsMapping() { + const container = document.getElementById('dnsMappings'); + const mappingDiv = document.createElement('div'); + mappingDiv.className = 'dns-mapping'; + mappingDiv.innerHTML = ` + + + + + `; + container.appendChild(mappingDiv); +} + +// Remove a DNS mapping row +function removeDnsMapping(button) { + const mapping = button.parentElement; + mapping.remove(); +} + +// Collect DNS mappings from UI +function collectDnsMappings() { + const mappings = {}; + const mappingElements = document.querySelectorAll('.dns-mapping'); + + mappingElements.forEach(el => { + const host = el.querySelector('.dns-host').value.trim(); + const ip = el.querySelector('.dns-ip').value.trim(); + if (host && ip) { + mappings[host] = ip; + } + }); + + return mappings; +} + +// File upload handler +document.addEventListener('DOMContentLoaded', () => { + const fileInput = document.getElementById('fileInput'); + const fileName = document.getElementById('fileName'); + const pacFile = document.getElementById('pacFile'); + + fileInput.addEventListener('change', (e) => { + const file = e.target.files[0]; + if (file) { + fileName.textContent = file.name; + const reader = new FileReader(); + reader.onload = (event) => { + pacFile.value = event.target.result; + }; + reader.readAsText(file); + } + }); +}); + +// Extract hostname from URL +function extractHost(url) { + try { + const urlObj = new URL(url); + return urlObj.hostname; + } catch (e) { + // Fallback for invalid URLs + const match = url.match(/^(?:https?:\/\/)?(?:[^@\n]+@)?(?:www\.)?([^:\/\n?]+)/im); + return match ? match[1] : url; + } +} + +// Main test function +function testPac() { + // Get inputs + const pacFileContent = document.getElementById('pacFile').value.trim(); + const testUrl = document.getElementById('testUrl').value.trim(); + const clientIp = document.getElementById('clientIp').value.trim(); + + // Collect DNS mappings + dnsMappingsMap = collectDnsMappings(); + + // Validate inputs + if (!pacFileContent) { + showResult('error', 'Please provide a PAC file'); + return; + } + + if (!testUrl) { + showResult('error', 'Please provide a test URL'); + return; + } + + try { + // Extract host from URL + const host = extractHost(testUrl); + + // Build the complete self-contained script. + // PAC_UTILS_JS (from pac_utils.js) and the mock functions are all + // defined inside the IIFE so they share the same scope — this ensures + // that isInNet(), isResolvable() etc. call our local dnsResolve() mock + // rather than any global definition. + // The script returns [result, dnsLog, myIpCalled] as a tuple. + const script = ` + (function() { + ${PAC_UTILS_JS} + + // DNS resolution mock — uses user-provided mappings + var __dnsLog = []; + var __mappings = ${JSON.stringify(dnsMappingsMap)}; + function dnsResolve(host) { + __dnsLog.push(host); + return __mappings.hasOwnProperty(host) ? __mappings[host] : null; + } + + // myIpAddress mock — returns user-provided client IP + var __myIpCalled = false; + function myIpAddress() { + __myIpCalled = true; + return ${JSON.stringify(clientIp || '127.0.0.1')}; + } + + // User's PAC file + ${pacFileContent} + + // Call the entry point via the findProxyForURL dispatcher defined + // in PAC_UTILS_JS, which handles both FindProxyForURL and + // FindProxyForURLEx. Throw early if neither is defined. + if (typeof FindProxyForURL !== 'function' && typeof FindProxyForURLEx !== 'function') { + throw new Error('FindProxyForURL function not found in PAC file'); + } + var __result = findProxyForURL(${JSON.stringify(testUrl)}, ${JSON.stringify(host)}); + + return [__result, __dnsLog, __myIpCalled]; + })(); + `; + + // Evaluate the script and unpack the returned tuple + const [result, dnsLog, myIpCalled] = eval(script); + + // Show success result + showResult('success', result, { + url: testUrl, + host: host, + clientIp: clientIp || '127.0.0.1', + dnsCallLog: dnsLog, + myIpCallLog: myIpCalled + }); + + } catch (error) { + showResult('error', `Error: ${error.message}`); + console.error('PAC evaluation error:', error); + } +} + +// Display results +function showResult(type, message, debug = null) { + const resultsSection = document.getElementById('resultsSection'); + const resultsDiv = document.getElementById('results'); + + resultsSection.style.display = 'block'; + + let className = 'result-success'; + let label = 'Proxy Result'; + + if (type === 'error') { + className = 'result-error'; + label = 'Error'; + } else if (type === 'warning') { + className = 'result-warning'; + label = 'Warning'; + } + + let html = ` +
+
${label}:
+
${escapeHtml(message)}
+ `; + + if (debug) { + html += ` +
+

Debug Information:

+
    +
  • URL: ${escapeHtml(debug.url)}
  • +
  • Host: ${escapeHtml(debug.host)}
  • +
  • Client IP: ${escapeHtml(debug.clientIp)}
  • + ${debug.myIpCallLog ? '
  • myIpAddress() was called
  • ' : ''} + ${debug.dnsCallLog.length > 0 ? + `
  • DNS lookups: ${escapeHtml(debug.dnsCallLog.join(', '))}
  • ` : + '
  • No DNS lookups performed
  • '} +
+
+ `; + } + + html += '
'; + resultsDiv.innerHTML = html; + + // Scroll to results + resultsSection.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); +} + +// Escape HTML to prevent XSS +function escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; +} + +// Make functions globally accessible +window.addDnsMapping = addDnsMapping; +window.removeDnsMapping = removeDnsMapping; +window.testPac = testPac; diff --git a/web/pac_utils.js b/web/pac_utils.js new file mode 100644 index 0000000..a90864e --- /dev/null +++ b/web/pac_utils.js @@ -0,0 +1,296 @@ +// Auto-generated from src/pac_utils.h — do not edit manually. +// Regenerate with: make -C src pac_utils_js +// +// Exported as a source string (PAC_UTILS_JS) rather than executed +// directly, so that the eval'd PAC sandbox can define its own +// dnsResolve() / myIpAddress() in the same scope as the utility +// functions — keeping DNS mocking correct via lexical scoping. +const PAC_UTILS_JS = ` +function dnsDomainIs(host, domain) { + return (host.length >= domain.length && + host.substring(host.length - domain.length) == domain); +} +function dnsDomainLevels(host) { + return host.split('.').length-1; +} +function convert_addr(ipchars) { + var bytes = ipchars.split('.'); + var result = ((bytes[0] & 0xff) << 24) | + ((bytes[1] & 0xff) << 16) | + ((bytes[2] & 0xff) << 8) | + (bytes[3] & 0xff); + return result; +} +function isInNet(ipaddr, pattern, maskstr) { + var test = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(ipaddr); + if (test == null) { + ipaddr = dnsResolve(ipaddr); + if (ipaddr == null) + return false; + } else if (test[1] > 255 || test[2] > 255 || + test[3] > 255 || test[4] > 255) { + return false; // not an IP address + } + var host = convert_addr(ipaddr); + var pat = convert_addr(pattern); + var mask = convert_addr(maskstr); + return ((host & mask) == (pat & mask)); + +} +function convert_addr6(ipchars) { + ipchars = ipchars.replace(/(^:|:$)/, ''); + var fields = ipchars.split(':'); + var diff = 8 - fields.length; + for (var i = 0; i < fields.length; i++) { + if (fields[i] == '') { + fields[i] = '0'; + // inject 'diff' number of '0' elements here. + for (var j = 0; j < diff; j++) { + fields.splice(i++, 0, '0'); + } + break; + } + } + var result = []; + for (var i = 0; i < fields.length; i++) { + result.push(parseInt(fields[i], 16)); + } + return result; +} +function isInNetEx6(ipaddr, prefix, prefix_len) { + if (prefix_len > 128) { + return false; + } + prefix = convert_addr6(prefix); + ip = convert_addr6(ipaddr); + // Prefix match strategy: + // Compare only prefix length bits between 'ipaddr' and 'prefix' + // Match in the batches of 16-bit fields + prefix_rem = prefix_len % 16; + prefix_nfields = (prefix_len - prefix_rem) / 16; + + for (var i = 0; i < prefix_nfields; i++) { + if (ip[i] != prefix[i]) { + return false; + } + } + if (prefix_rem > 0) { + // Compare remaining bits + prefix_bits = prefix[prefix_nfields] >> (16 - prefix_rem); + ip_bits = ip[prefix_nfields] >> (16 - prefix_rem); + if (ip_bits != prefix_bits) { + return false; + } + } + return true; +} +function isInNetEx4(ipaddr, prefix, prefix_len) { + if (prefix_len > 32) { + return false; + } + var netmask = []; + for (var i = 1; i < 5; i++) { + var shift_len = 8 * i - prefix_len; + if (shift_len <= 0) { + netmask.push(255) + } else { + netmask.push((0xff >> shift_len) << shift_len); + } + } + return isInNet(ipaddr, prefix, netmask.join('.')); +} +function isInNetEx(ipaddr, prefix) { + prefix_a = prefix.split('/'); + if (prefix_a.length != 2) { + return false; + } + var test = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(ipaddr); + if (!test) { + return isInNetEx6(ipaddr, prefix_a[0], prefix_a[1]); + } else { + return isInNetEx4(ipaddr, prefix_a[0], prefix_a[1]); + } +} +function isPlainHostName(host) { + return (host.search('\\.') == -1); +} +function isResolvable(host) { + var ip = dnsResolve(host); + return (ip != null); +} +if (typeof(dnsResolveEx) == "function") { +function isResolvableEx(host) { + var ip = dnsResolveEx(host); + return (ip != null); +} +} +function localHostOrDomainIs(host, hostdom) { + return (host == hostdom) || + (hostdom.lastIndexOf(host + '.', 0) == 0); +} +function shExpMatch(url, pattern) { + pattern = pattern.replace(/\./g, '\\.'); + pattern = pattern.replace(/\*/g, '.*'); + pattern = pattern.replace(/\?/g, '.'); + var newRe = new RegExp('^'+pattern+'$'); + return newRe.test(url); +} +var wdays = {SUN: 0, MON: 1, TUE: 2, WED: 3, THU: 4, FRI: 5, SAT: 6}; +var months = {JAN: 0, FEB: 1, MAR: 2, APR: 3, MAY: 4, JUN: 5, JUL: 6, AUG: 7, SEP: 8, OCT: 9, NOV: 10, DEC: 11}; +function weekdayRange() { + function getDay(weekday) { + if (weekday in wdays) { + return wdays[weekday]; + } + return -1; + } + var date = new Date(); + var argc = arguments.length; + var wday; + if (argc < 1) + return false; + if (arguments[argc - 1] == 'GMT') { + argc--; + wday = date.getUTCDay(); + } else { + wday = date.getDay(); + } + var wd1 = getDay(arguments[0]); + var wd2 = (argc == 2) ? getDay(arguments[1]) : wd1; + return (wd1 == -1 || wd2 == -1) ? false + : (wd1 <= wday && wday <= wd2); +} +function dateRange() { + function getMonth(name) { + if (name in months) { + return months[name]; + } + return -1; + } + var date = new Date(); + var argc = arguments.length; + if (argc < 1) { + return false; + } + var isGMT = (arguments[argc - 1] == 'GMT'); + + if (isGMT) { + argc--; + } + // function will work even without explicit handling of this case + if (argc == 1) { + var tmp = parseInt(arguments[0]); + if (isNaN(tmp)) { + return ((isGMT ? date.getUTCMonth() : date.getMonth()) == +getMonth(arguments[0])); + } else if (tmp < 32) { + return ((isGMT ? date.getUTCDate() : date.getDate()) == tmp); + } else { + return ((isGMT ? date.getUTCFullYear() : date.getFullYear()) == +tmp); + } + } + var year = date.getFullYear(); + var date1, date2; + date1 = new Date(year, 0, 1, 0, 0, 0); + date2 = new Date(year, 11, 31, 23, 59, 59); + var adjustMonth = false; + for (var i = 0; i < (argc >> 1); i++) { + var tmp = parseInt(arguments[i]); + if (isNaN(tmp)) { + var mon = getMonth(arguments[i]); + date1.setMonth(mon); + } else if (tmp < 32) { + adjustMonth = (argc <= 2); + date1.setDate(tmp); + } else { + date1.setFullYear(tmp); + } + } + for (var i = (argc >> 1); i < argc; i++) { + var tmp = parseInt(arguments[i]); + if (isNaN(tmp)) { + var mon = getMonth(arguments[i]); + date2.setMonth(mon); + } else if (tmp < 32) { + date2.setDate(tmp); + } else { + date2.setFullYear(tmp); + } + } + if (adjustMonth) { + date1.setMonth(date.getMonth()); + date2.setMonth(date.getMonth()); + } + if (isGMT) { + var tmp = date; + tmp.setFullYear(date.getUTCFullYear()); + tmp.setMonth(date.getUTCMonth()); + tmp.setDate(date.getUTCDate()); + tmp.setHours(date.getUTCHours()); + tmp.setMinutes(date.getUTCMinutes()); + tmp.setSeconds(date.getUTCSeconds()); + date = tmp; + } + return ((date1 <= date) && (date <= date2)); +} +function timeRange() { + var argc = arguments.length; + var date = new Date(); + var isGMT= false; + + if (argc < 1) { + return false; + } + if (arguments[argc - 1] == 'GMT') { + isGMT = true; + argc--; + } + + var hour = isGMT ? date.getUTCHours() : date.getHours(); + var date1, date2; + date1 = new Date(); + date2 = new Date(); + + if (argc == 1) { + return (hour == arguments[0]); + } else if (argc == 2) { + return ((arguments[0] <= hour) && (hour <= arguments[1])); + } else { + switch (argc) { + case 6: + date1.setSeconds(arguments[2]); + date2.setSeconds(arguments[5]); + case 4: + var middle = argc >> 1; + date1.setHours(arguments[0]); + date1.setMinutes(arguments[1]); + date2.setHours(arguments[middle]); + date2.setMinutes(arguments[middle + 1]); + if (middle == 2) { + date2.setSeconds(59); + } + break; + default: + throw 'timeRange: bad number of arguments' + } + } + + if (isGMT) { + date.setFullYear(date.getUTCFullYear()); + date.setMonth(date.getUTCMonth()); + date.setDate(date.getUTCDate()); + date.setHours(date.getUTCHours()); + date.setMinutes(date.getUTCMinutes()); + date.setSeconds(date.getUTCSeconds()); + } + return ((date1 <= date) && (date <= date2)); +} +function findProxyForURL(url, host) { + if (typeof FindProxyForURLEx == 'function') { + return FindProxyForURLEx(url, host); + } else { + return FindProxyForURL(url, host); + } +} +`; diff --git a/web/sample.pac b/web/sample.pac new file mode 100644 index 0000000..c7e6b13 --- /dev/null +++ b/web/sample.pac @@ -0,0 +1,32 @@ +// Sample PAC file for testing +// This file demonstrates common PAC file patterns + +function FindProxyForURL(url, host) { + // Direct access for localhost + if (isPlainHostName(host) || + dnsDomainIs(host, ".localhost") || + isInNet(host, "127.0.0.0", "255.0.0.0")) { + return "DIRECT"; + } + + // Direct access for internal networks + if (isInNet(myIpAddress(), "10.0.0.0", "255.0.0.0") || + isInNet(myIpAddress(), "172.16.0.0", "255.240.0.0") || + isInNet(myIpAddress(), "192.168.0.0", "255.255.0.0")) { + + // Check if destination is also internal + if (isInNet(dnsResolve(host), "10.0.0.0", "255.0.0.0") || + isInNet(dnsResolve(host), "172.16.0.0", "255.240.0.0") || + isInNet(dnsResolve(host), "192.168.0.0", "255.255.0.0")) { + return "DIRECT"; + } + } + + // Use proxy for external sites + if (dnsDomainIs(host, ".example.com")) { + return "PROXY proxy.example.com:8080; DIRECT"; + } + + // Default: use main proxy + return "PROXY proxy.corporate.com:3128; DIRECT"; +} diff --git a/web/style.css b/web/style.css new file mode 100644 index 0000000..f1a0a82 --- /dev/null +++ b/web/style.css @@ -0,0 +1,409 @@ +:root { + --primary-color: #2563eb; + --primary-hover: #1d4ed8; + --success-color: #10b981; + --error-color: #ef4444; + --warning-color: #f59e0b; + --bg-color: #f8fafc; + --card-bg: #ffffff; + --border-color: #e2e8f0; + --text-primary: #1e293b; + --text-secondary: #64748b; + --text-muted: #94a3b8; + --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); + --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; + padding: 2rem 1rem; + color: var(--text-primary); + line-height: 1.6; +} + +.container { + max-width: 900px; + margin: 0 auto; + background: var(--card-bg); + border-radius: 16px; + box-shadow: var(--shadow-lg); + overflow: hidden; +} + +header { + background: linear-gradient(135deg, var(--primary-color) 0%, #6366f1 100%); + color: white; + padding: 2.5rem 2rem; + text-align: center; +} + +header h1 { + font-size: 2rem; + margin-bottom: 0.5rem; + font-weight: 700; +} + +.subtitle { + font-size: 0.95rem; + opacity: 0.95; + max-width: 600px; + margin: 0 auto; +} + +main { + padding: 2rem; +} + +.section { + margin-bottom: 2rem; + padding: 1.5rem; + background: var(--bg-color); + border-radius: 12px; + border: 1px solid var(--border-color); +} + +.section h2 { + font-size: 1.5rem; + margin-bottom: 1rem; + color: var(--text-primary); + font-weight: 600; +} + +.section h3 { + font-size: 1.1rem; + margin-bottom: 0.75rem; + color: var(--text-primary); + font-weight: 600; +} + +.input-group { + display: flex; + flex-direction: column; + gap: 1rem; +} + +textarea { + width: 100%; + padding: 1rem; + border: 2px solid var(--border-color); + border-radius: 8px; + font-family: 'Courier New', Courier, monospace; + font-size: 0.9rem; + resize: vertical; + background: white; + transition: border-color 0.2s; +} + +textarea:focus { + outline: none; + border-color: var(--primary-color); +} + +.file-upload { + display: flex; + align-items: center; + gap: 1rem; +} + +.input-row { + margin-bottom: 1.5rem; +} + +.input-row label { + display: block; + font-weight: 600; + margin-bottom: 0.5rem; + color: var(--text-primary); +} + +.input-row input[type="text"] { + width: 100%; + padding: 0.75rem 1rem; + border: 2px solid var(--border-color); + border-radius: 8px; + font-size: 1rem; + transition: border-color 0.2s; +} + +.input-row input[type="text"]:focus { + outline: none; + border-color: var(--primary-color); +} + +.input-row small { + display: block; + margin-top: 0.5rem; + color: var(--text-muted); + font-size: 0.875rem; +} + +.dns-section { + margin-top: 1.5rem; +} + +.help-text { + color: var(--text-secondary); + font-size: 0.9rem; + margin-bottom: 1rem; +} + +.dns-mapping { + display: flex; + align-items: center; + gap: 0.75rem; + margin-bottom: 0.75rem; +} + +.dns-host, .dns-ip { + flex: 1; + padding: 0.75rem; + border: 2px solid var(--border-color); + border-radius: 8px; + font-size: 0.9rem; + transition: border-color 0.2s; +} + +.dns-host:focus, .dns-ip:focus { + outline: none; + border-color: var(--primary-color); +} + +.arrow { + color: var(--text-muted); + font-size: 1.2rem; + font-weight: bold; +} + +.btn-primary { + background: linear-gradient(135deg, var(--primary-color) 0%, #6366f1 100%); + color: white; + padding: 1rem 2.5rem; + border: none; + border-radius: 8px; + font-size: 1.1rem; + font-weight: 600; + cursor: pointer; + transition: transform 0.2s, box-shadow 0.2s; + box-shadow: var(--shadow); +} + +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-lg); +} + +.btn-primary:active { + transform: translateY(0); +} + +.btn-secondary { + background: white; + color: var(--primary-color); + padding: 0.75rem 1.5rem; + border: 2px solid var(--primary-color); + border-radius: 8px; + font-size: 0.95rem; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; +} + +.btn-secondary:hover { + background: var(--primary-color); + color: white; +} + +.btn-remove { + background: var(--error-color); + color: white; + width: 32px; + height: 32px; + border: none; + border-radius: 6px; + font-size: 1.2rem; + cursor: pointer; + transition: all 0.2s; + flex-shrink: 0; +} + +.btn-remove:hover { + background: #dc2626; + transform: scale(1.1); +} + +.action-section { + text-align: center; + margin: 2rem 0; +} + +#results { + font-family: 'Courier New', Courier, monospace; +} + +.result-success { + background: #d1fae5; + border-left: 4px solid var(--success-color); + padding: 1.5rem; + border-radius: 8px; + margin-bottom: 1rem; +} + +.result-error { + background: #fee2e2; + border-left: 4px solid var(--error-color); + padding: 1.5rem; + border-radius: 8px; + margin-bottom: 1rem; +} + +.result-warning { + background: #fef3c7; + border-left: 4px solid var(--warning-color); + padding: 1.5rem; + border-radius: 8px; + margin-bottom: 1rem; +} + +.result-label { + font-weight: 700; + margin-bottom: 0.5rem; + font-size: 1.1rem; +} + +.result-value { + font-size: 1.2rem; + margin: 0.5rem 0; + word-break: break-all; +} + +.debug-info { + background: #f1f5f9; + padding: 1rem; + border-radius: 6px; + margin-top: 1rem; + font-size: 0.85rem; +} + +.debug-info h4 { + margin-bottom: 0.5rem; + color: var(--text-secondary); +} + +.debug-info ul { + list-style: none; + padding-left: 1rem; +} + +.debug-info li { + margin-bottom: 0.25rem; + color: var(--text-secondary); +} + +footer { + background: var(--bg-color); + padding: 1.5rem; + text-align: center; + color: var(--text-secondary); + font-size: 0.9rem; + border-top: 1px solid var(--border-color); +} + +footer a { + color: var(--primary-color); + text-decoration: none; + font-weight: 600; +} + +footer a:hover { + text-decoration: underline; +} + +@media (max-width: 640px) { + body { + padding: 1rem 0.5rem; + } + + header { + padding: 1.5rem 1rem; + } + + header h1 { + font-size: 1.5rem; + } + + main { + padding: 1rem; + } + + .section { + padding: 1rem; + } + + .dns-mapping { + flex-wrap: wrap; + } + + .dns-host, .dns-ip { + min-width: 100%; + } + + .btn-primary { + width: 100%; + } + + .links-grid { + grid-template-columns: 1fr; + } +} + +/* About Section Styles */ +.about-section { + background-color: var(--card-bg); + border-top: 1px solid var(--border-color); +} + +.about-content { + color: var(--text-color); + line-height: 1.6; +} + +.links-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1rem; + margin-top: 1.5rem; +} + +.btn-outline { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.75rem 1rem; + border: 1px solid var(--primary-color); + border-radius: 6px; + color: var(--primary-color); + text-decoration: none; + font-weight: 500; + transition: all 0.2s; +} + +.btn-outline:hover { + background-color: var(--primary-color); + color: white; + transform: translateY(-2px); + box-shadow: 0 4px 6px rgba(0,0,0,0.1); +} + +.icon { + margin-right: 0.5rem; + font-size: 1.2em; +}