Compare commits

..

19 Commits

Author SHA1 Message Date
218b8dc234 fix: Typo in copy instruction
All checks were successful
Build Docker / BuildImage (push) Successful in 1m26s
2025-10-30 11:58:45 +11:00
8b95ee8332 feat: Update tools to allow custom DNS server
Some checks failed
Build Docker / BuildImage (push) Failing after 32s
2025-10-30 11:55:48 +11:00
6a68432a08 fix: Add git to dockerfile to allow it to install custom request-doh
All checks were successful
Build Docker / BuildImage (push) Successful in 1m7s
2025-02-28 21:59:18 +11:00
3370d01a54 fix: Update some script replacement and use custom requests-doh
Some checks failed
Build Docker / BuildImage (push) Failing after 36s
2025-02-28 21:55:15 +11:00
003e14343b fix: Add better js parsing
All checks were successful
Build Docker / BuildImage (push) Successful in 31s
2025-02-28 13:34:01 +11:00
edbfa66249 fix: Add margin for error message
All checks were successful
Build Docker / BuildImage (push) Successful in 36s
2025-02-28 13:10:33 +11:00
7f0a13f44b feat: Add better cleaning for dprofile
All checks were successful
Build Docker / BuildImage (push) Successful in 34s
2025-02-27 23:35:23 +11:00
6d77096a15 fix: Set https in prod
All checks were successful
Build Docker / BuildImage (push) Successful in 30s
2025-02-27 22:31:21 +11:00
50275ba482 feat: Added proxy
All checks were successful
Build Docker / BuildImage (push) Successful in 1m5s
2025-02-27 22:22:40 +11:00
ec59656cad fix: Update params to allow both ssl and curl checks
All checks were successful
Build Docker / BuildImage (push) Successful in 31s
2025-02-27 15:08:09 +11:00
964779136c fix: Remove scripts from html to stop redirects or other random things
All checks were successful
Build Docker / BuildImage (push) Successful in 30s
2025-02-27 14:28:59 +11:00
3998e1cafc feat: Cleanup some style
All checks were successful
Build Docker / BuildImage (push) Successful in 1m7s
2025-02-27 14:21:46 +11:00
7bde6e7bd2 fix: Add curl into dockerfile
All checks were successful
Build Docker / BuildImage (push) Successful in 1m14s
2025-02-27 14:16:07 +11:00
0e56b2609e feat: Add curl
All checks were successful
Build Docker / BuildImage (push) Successful in 35s
2025-02-27 14:14:27 +11:00
c4b1be2149 fix: Update regex to allow xn-- domains
All checks were successful
Build Docker / BuildImage (push) Successful in 30s
2025-02-27 13:24:37 +11:00
06749d2c07 fix: Validation for negative responses
All checks were successful
Build Docker / BuildImage (push) Successful in 38s
2025-02-26 21:58:44 +11:00
07daad516f fix: Use updated HSD for DNSSEC validation
All checks were successful
Build Docker / BuildImage (push) Successful in 34s
2025-02-26 16:10:31 +11:00
1c7093307a feat: Add new UI
All checks were successful
Build Docker / BuildImage (push) Successful in 32s
2025-02-25 23:04:47 +11:00
a26eecf59e fix: remove test runner script 2025-02-25 22:43:18 +11:00
9 changed files with 642 additions and 165 deletions

View File

@@ -1,54 +0,0 @@
name: Build Docker fast
run-name: Build Docker Images fast
on:
push:
jobs:
BuildImage:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Docker Registry
uses: docker/login-action@v2
with:
registry: git.woodburn.au
username: nathanwoodburn
password: ${{ secrets.DOCKERGIT_TOKEN }}
- name: Extract metadata
id: meta
run: |
echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_OUTPUT
tag=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}
tag=${tag//\//-}
echo "tag=$tag" >> $GITHUB_OUTPUT
tag_num=${GITHUB_RUN_NUMBER}
echo "tag_num=$tag_num" >> $GITHUB_OUTPUT
if [[ "$tag" == "main" ]]; then
echo "final_tag=latest" >> $GITHUB_OUTPUT
else
echo "final_tag=${tag}-${tag_num}" >> $GITHUB_OUTPUT
fi
repo=$GITHUB_REPOSITORY
repo=${repo#*/}
repo=$(echo $repo | tr '[:upper:]' '[:lower:]')
echo "repo=$repo" >> $GITHUB_OUTPUT
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: |
git.woodburn.au/nathanwoodburn/${{ steps.meta.outputs.repo }}:${{ steps.meta.outputs.final_tag }}
git.woodburn.au/nathanwoodburn/${{ steps.meta.outputs.repo }}:${{ steps.meta.outputs.tag_num }}
git.woodburn.au/nathanwoodburn/${{ steps.meta.outputs.repo }}:${{ steps.meta.outputs.tag }}
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@@ -3,13 +3,17 @@ FROM --platform=$BUILDPLATFORM python:3.10-alpine AS builder
WORKDIR /app
# Install openssl and delv
RUN apk add --no-cache openssl bind-tools
RUN apk add --no-cache openssl bind-tools curl git
COPY requirements.txt /app
RUN --mount=type=cache,target=/root/.cache/pip \
python3 -m pip install -r requirements.txt
COPY . /app
# Copy application files
COPY *.py /app/
COPY templates/ /app/templates/
COPY hsd-ksk /app
# Optionally mount /data to store the data
# VOLUME /data

View File

@@ -4,4 +4,7 @@ requests
python-dotenv
dnspython
cryptography
datetime
datetime
beautifulsoup4
requests-doh
git+https://github.com/Nathanwoodburn/requests-doh.git

View File

@@ -17,12 +17,15 @@ from datetime import datetime
import dotenv
import re
import tools
import urllib.parse
dotenv.load_dotenv()
app = Flask(__name__)
BLOCKED_PATHS = ["https.js"]
def find(name, path):
for root, dirs, files in os.walk(path):
if name in files:
@@ -78,6 +81,39 @@ def wellknown(path):
def index():
return render_template("index.html")
@app.route("/proxy/<path:url>")
def proxy(url: str):
# Decode the URL
url = urllib.parse.unquote(url)
# Get last path segment
path = url.split("/")[-1]
if path in BLOCKED_PATHS:
return render_template("404.html"), 403
content: requests.Response = tools.proxy(url)
if not content.status_code < 500:
print(content.text)
return render_template("500.html"), 500
# Get the content type
contentType = content.headers.get("Content-Type")
if "text/html" in contentType:
response = make_response(tools.cleanProxyContent(content.text,url,request.host_url))
response.headers["Content-Type"] = contentType
return response, content.status_code
# Clean JS
if "text/javascript" in contentType or 'application/javascript' in contentType:
response = make_response(tools.proxyCleanJS(content.text,url,request.host_url))
response.headers["Content-Type"] = contentType
return response, content.status_code
response = make_response(content.content)
response.headers["Content-Type"] = contentType
return response, content.status_code
@app.route("/<path:path>")
def catch_all(path: str):
@@ -106,12 +142,19 @@ def catch_all(path: str):
#region API routes
@app.route("/api/v1/ssl/<domain>")
def api_ssl(domain: str):
regexmatch = re.match(r"^([a-z0-9]+(-[a-z0-9]+)*\.)*([a-z0-9]+(-[a-z0-9]+)*)$", domain)
regexmatch = re.match(r"^([a-z0-9]+(-[a-z0-9]+)*\.)*([a-z0-9]+([-a-z0-9])*)$", domain)
if not regexmatch:
return api_error("Please provide a valid domain to check")
result = tools.check_ssl(domain)
return jsonify(result)
@app.route("/api/v1/curl/<path:url>")
def api_curl(url: str):
# Decode the URL
url = urllib.parse.unquote(url)
result = tools.curl(url)
return jsonify(result)
# endregion

21
templates/500.html Normal file
View File

@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nathan.Woodburn/</title>
<link rel="icon" href="/assets/img/favicon.png" type="image/png">
<link rel="stylesheet" href="/assets/css/404.css">
</head>
<body>
<div class="spacer"></div>
<div class="centre">
<h1>500 | Internal Server Error</h1>
<p>Sorry, we can't seem to display this page. Maybe try again or your request might not be valid</p>
<p><a href="/">Go back to the homepage</a></p>
</div>
</body>
</html>

242
templates/assets/js/curl.js Normal file
View File

@@ -0,0 +1,242 @@
document.addEventListener('DOMContentLoaded', function () {
// Get references to elements
const curlButton = document.getElementById('curl');
const curlUrlInput = document.getElementById('curl-url');
const resultsContainer = document.getElementById('curl-results');
// Add click event listener to the button
curlButton.addEventListener('click', function () {
// Get the URL from the input
const url = curlUrlInput.value.trim();
// Validate URL
if (!url) {
showMessage('Please enter a URL', 'error');
return;
}
// Update the url
var params;
if (window.location.search){
params = new URLSearchParams(window.location.search);
params.set('url', url);
}
else {
params = new URLSearchParams();
params.append('url', url);
}
history.pushState(null, null, `?${params.toString()}`);
// Show loading state
curlButton.disabled = true;
curlButton.innerHTML = '<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>Loading...';
// Clear previous results
resultsContainer.innerHTML = '';
showMessage('Fetching content...', 'info');
// Make the request
handleCurlRequest(url);
});
// Add enter key listener to input
curlUrlInput.addEventListener('keyup', function (event) {
if (event.key === 'Enter') {
curlButton.click();
}
});
/**
* Handle the curl API request
* @param {string} url - The URL to curl
*/
function handleCurlRequest(url) {
// Encode the URL for the API endpoint
const encodedUrl = encodeURIComponent(url);
const apiEndpoint = `/api/v1/curl/${encodedUrl}`;
fetch(apiEndpoint)
.then(response => response.json())
.then(data => {
// Clear loading message
resultsContainer.innerHTML = '';
if (data.success === true) {
// Create a result card with iframe
const resultCard = document.createElement('div');
resultCard.className = 'card shadow border-0 rounded-lg mb-5';
resultCard.innerHTML = `
<div class="card-header bg-success text-white py-3 d-flex justify-content-between align-items-center">
<h3 class="h5 mb-0">Result for ${escapeHtml(url)}</h3>
<div>
<button class="btn btn-sm btn-light ms-2" id="new-tab">
<i class="bi bi-code-slash me-1"></i>View in new tab
</button>
<button class="btn btn-sm btn-light ms-2" id="view-source">
<i class="bi bi-code-slash me-1"></i>View Source
</button>
</div>
</div>
<div class="card-body p-0">
<iframe id="result-iframe" style="width:100%; height:600px; border:0;"></iframe>
</div>
`;
resultsContainer.appendChild(resultCard);
// Get reference to the iframe and buttons
const iframe = document.getElementById('result-iframe');
const viewSourceButton = document.getElementById('view-source');
const newTabButton = document.getElementById('new-tab');
const cleanedHtml = removeScripts(data.result);
// Create a blob URL from the cleaned HTML content
const blob = new Blob([cleanedHtml], { type: 'text/html' });
const blobUrl = URL.createObjectURL(blob);
// Set the iframe src to the blob URL
iframe.src = blobUrl;
// Add event listener to view source button
viewSourceButton.addEventListener('click', function () {
// Show source code in a modal or new window
const sourceWindow = window.open('', '_blank');
sourceWindow.document.write(`
<!DOCTYPE html>
<html>
<head>
<title>Source for ${escapeHtml(url)}</title>
<style>
body { font-family: monospace; white-space: pre-wrap; padding: 20px; }
</style>
</head>
<body>${escapeHtml(data.result)}</body>
</html>
`);
sourceWindow.document.close();
});
// Add event listener to new tab button
newTabButton.addEventListener('click', function () {
// Get URL from window.location
const proxyurl = `${window.location.protocol}//${window.location.host}/proxy/${url}`;
// Open host/proxy/url in new tab
window.open(proxyurl, '_blank');
});
// Set up cleanup when iframe is no longer needed
window.addEventListener('beforeunload', function () {
URL.revokeObjectURL(blobUrl);
});
} else {
// Show error message
showMessage(data.error || 'Failed to get content', 'error');
}
})
.catch(error => {
showMessage(`Request failed: ${error.message}`, 'error');
})
.finally(() => {
// Reset button state
curlButton.disabled = false;
curlButton.innerHTML = '<i class="bi bi-shield-check me-2"></i>HTTP Request';
});
}
/**
* Remove all script tags and event handlers from HTML content
* @param {string} html - The HTML content
* @returns {string} - HTML content with scripts removed
*/
function removeScripts(html) {
// Create a DOM parser to work with the HTML
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
// Remove all script elements
const scripts = doc.getElementsByTagName('script');
while (scripts.length > 0) {
scripts[0].parentNode.removeChild(scripts[0]);
}
// Remove inline event handlers from all elements
const allElements = doc.getElementsByTagName('*');
for (let i = 0; i < allElements.length; i++) {
const element = allElements[i];
const attrs = element.attributes;
const attrsToRemove = [];
// Collect all event handler attributes (on*)
for (let j = 0; j < attrs.length; j++) {
if (attrs[j].name.toLowerCase().startsWith('on')) {
attrsToRemove.push(attrs[j].name);
}
}
// Remove the collected attributes
attrsToRemove.forEach(attr => {
element.removeAttribute(attr);
});
}
// Return the cleaned HTML
return doc.documentElement.outerHTML;
}
/**
* Display a message in the results container
* @param {string} message - The message to display
* @param {string} type - Message type (error, success, info)
*/
function showMessage(message, type) {
// Clear previous content
resultsContainer.innerHTML = '';
// Create alert element
const alert = document.createElement('div');
alert.className = `alert alert-${getAlertClass(type)} shadow-sm`;
alert.innerHTML = message;
// Add the alert to the results container
resultsContainer.appendChild(alert);
}
/**
* Get Bootstrap alert class based on message type
* @param {string} type - Message type
* @returns {string} - Bootstrap alert class
*/
function getAlertClass(type) {
switch (type) {
case 'error': return 'danger';
case 'success': return 'success';
case 'info': default: return 'info';
}
}
/**
* Escape HTML special characters to prevent XSS
* @param {string} text - Text to escape
* @returns {string} - Escaped text
*/
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Check if params are present
if (window.location.search) {
const params = new URLSearchParams(window.location.search);
const url = params.get('url');
if (url) {
// Add url to input
curlUrlInput.value = url;
// Show loading state
curlButton.disabled = true;
curlButton.innerHTML = '<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>Loading...';
handleCurlRequest(url);
}
}
});

View File

@@ -12,19 +12,27 @@ document.getElementById("domain").addEventListener("keyup", function (event) {
function getSSL() {
// Get the input value
const domain = document.getElementById("domain").value;
const domain = document.getElementById("domain").value.trim().toLowerCase();
// Set the domain parameter
const params = new URLSearchParams();
params.append("domain", domain);
var params;
if (window.location.search){
params = new URLSearchParams(window.location.search);
params.set("domain", domain);
}
else {
params = new URLSearchParams();
params.append("domain", domain);
}
// Add the parameters to the URL
const url = `/?${params.toString()}`;
// Push the URL to the history
history.pushState(null, null, url);
// Add a loading spinner
document.getElementById("results").innerHTML = `<div class="spinner-border text-primary" role="status">
document.getElementById("ssl-results").innerHTML = `<div style="text-align: center; margin-bottom: 3rem;">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>`;
</div></div>`;
// Send a GET request to the API
@@ -34,8 +42,8 @@ function getSSL() {
// Check if the request was successful
if (data.success) {
// Display the results
document.getElementById("results").innerHTML = `
<div class="card shadow-sm p-4" style="max-width: 950px;margin: auto;">
document.getElementById("ssl-results").innerHTML = `
<div class="card shadow-sm p-4" style="max-width: 950px;margin: auto; margin-bottom: 3rem;">
<h2 class="mb-3">SSL Certificate Details</h2>
<ul class="list-group">
<li class="list-group-item"><strong>IP Address:</strong> ${data.ip}</li>
@@ -93,7 +101,7 @@ function getSSL() {
</button>
</div>
<div class="dnssec-details mt-2" style="display:none;">
<div class="card card-body bg-light"><pre class="mb-0" style="white-space: pre-wrap; text-align: left;">${data.dnssec.errors}${data.dnssec.output}</pre>
<div class="card card-body bg-light"><pre class="mb-0" style="white-space: pre-wrap; text-align: left;">${data.dnssec.output}</pre>
</div>
</div>
</li>
@@ -140,13 +148,13 @@ document.querySelector('.dnssec-toggle').addEventListener('click', function() {
} else {
// Display an error message
document.getElementById("results").innerHTML = `<h2>Error</h2>
<p>${data.message}</p>`;
document.getElementById("ssl-results").innerHTML = `<h2>Error</h2>
<p style="margin-bottom: 3rem;">${data.message}</p>`;
}
})
.catch(error => {
// Display an error message
document.getElementById("results").innerHTML = `<h2>Error</h2>
document.getElementById("ssl-results").innerHTML = `<h2>Error</h2>
<p>${error.message}</p>`;
});

View File

@@ -8,20 +8,92 @@
<link rel="icon" href="/assets/img/favicon.png" type="image/png">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="/assets/css/index.css">
<script src="/assets/js/index.js" defer></script>
<script src="/assets/js/ssl.js" defer></script>
<script src="/assets/js/curl.js" defer></script>
</head>
<body>
<div class="centre" style="margin-top: 20px;">
<h1>HNS Tools</h1>
<h2>Nathan.Woodburn/</h2>
<div class="container py-5">
<!-- Header Section -->
<div class="text-center mb-5">
<h1 class="display-4 fw-bold text-primary"><a href="/" class="text-decoration-none text-reset">HNS Tools</a></h1>
<h2 class="h4 fw-light text-secondary"><a href="https://nathan.woodburn.au" target="_blank" class="text-decoration-none text-reset">Nathan.Woodburn/</a></h2>
<div class="border-bottom mt-4 w-50 mx-auto"></div>
</div>
<!-- SSL check Tool Card -->
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card shadow border-0 rounded-lg mb-5">
<div class="card-header bg-primary text-white py-3">
<h2 class="h4 mb-0 text-center">SSL/DANE Checker</h2>
</div>
<div class="card-body p-4">
<p class="text-muted mb-4 text-center">Validate SSL certificates and DANE records for any domain</p>
<div class="input-group mb-3 shadow-sm">
<input
type="text"
id="domain"
placeholder="Enter domain name (e.g., nathan.woodburn firewallet)"
class="form-control form-control-lg"
aria-label="Domain to check"
>
<button class="btn btn-primary px-4" id="ssl">
<i class="bi bi-shield-check me-2"></i>Validate
</button>
</div>
<div class="text-center text-muted small">
Enter a complete domain name without any slashes or leading .
</div>
</div>
</div>
<!-- Results Container -->
<div id="ssl-results" class="animate__animated animate__fadeIn"></div>
</div>
</div>
<!-- CURL Tool Card -->
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card shadow border-0 rounded-lg mb-5">
<div class="card-header bg-primary text-white py-3">
<h2 class="h4 mb-0 text-center">Curl Website</h2>
</div>
<div class="card-body p-4">
<p class="text-muted mb-4 text-center">Do a http request for a domain</p>
<div class="input-group mb-3 shadow-sm">
<input
type="text"
id="curl-url"
placeholder="Enter url (e.g., https://nathan.woodburn http://firewallet)"
class="form-control form-control-lg"
aria-label="URL to check"
>
<button class="btn btn-primary px-4" id="curl">
<i class="bi bi-shield-check me-2"></i>HTTP Request
</button>
</div>
<div class="text-center text-muted small">
Enter a complete url including the http or https protocol
</div>
</div>
</div>
<!-- Results Container -->
<div id="curl-results" class="animate__animated animate__fadeIn"></div>
</div>
</div>
<!-- Footer -->
<footer class="text-center mt-5 pt-4 text-muted">
<small>© 2025 <a href="https://nathan.woodburn.au" target="_blank" class="text-decoration-none text-reset">Nathan.Woodburn/</a> - Handshake Name Services Tools</small>
</footer>
</div>
<div class="centre">
<h2>SSL/DANE Checker</h2>
<input type="text" id="domain" placeholder="Domain to check">
<button id="ssl">Validate SSL</button>
<div id="results" style="margin-top: 20px;"></div>
</div>
</body>
</body>
</html>

308
tools.py
View File

@@ -1,4 +1,5 @@
import random
from urllib.parse import urlparse
import dns.resolver
import subprocess
import tempfile
@@ -6,20 +7,31 @@ import binascii
from cryptography import x509
from cryptography.hazmat.backends import default_backend
import datetime
from dns import resolver, dnssec, name, exception
import time
from dns import resolver
import requests
import re
from bs4 import BeautifulSoup
import requests_doh
import urllib3
import socket
import os
resolver = dns.resolver.Resolver()
resolver.nameservers = ["194.50.5.28","194.50.5.27","194.50.5.26"]
resolver.port = 53
resolver.nameservers = os.getenv("DNS_SERVERS", "194.50.5.27").split(",")
resolver.port = int(os.getenv("DNS_PORT", "53"))
DoHsession = requests_doh.DNSOverHTTPSSession("hnsdoh")
# Disable warnings
urllib3.disable_warnings()
def check_ssl(domain: str):
def check_ssl(domain: str):
domain_check = False
returns = {"success": False,"valid":False}
try:
# Query the DNS record
response = resolver.resolve(domain, "A")
response = resolver.resolve(domain, "A",lifetime=10)
records = []
for record in response:
records.append(str(record))
@@ -55,94 +67,98 @@ def check_ssl(domain: str):
certificates = [cert[cert.find("-----BEGIN CERTIFICATE-----"):] for cert in certificates]
if not certificates:
returns["message"] = "No certificate found on remote webserver"
return returns
cert = certificates[0]
returns["cert"] = {"cert": cert}
with tempfile.NamedTemporaryFile(mode="w", delete=False) as temp_cert_file:
temp_cert_file.write(cert)
temp_cert_file.seek(0)
tlsa_command = ["openssl","x509","-in",temp_cert_file.name,"-pubkey","-noout","|","openssl","pkey","-pubin","-outform","der","|","openssl","dgst","-sha256","-binary",]
tlsa_process = subprocess.Popen(" ".join(tlsa_command), shell=True, stdout=subprocess.PIPE)
tlsa_output, _ = tlsa_process.communicate()
tlsa_server = "3 1 1 " + binascii.hexlify(tlsa_output).decode("utf-8")
returns["tlsa"] = {
"server": tlsa_server,
"nameserver": "",
"match": False
}
# Get domains
cert_obj = x509.load_pem_x509_certificate(cert.encode("utf-8"), default_backend())
domains = []
for ext in cert_obj.extensions:
if ext.oid == x509.ExtensionOID.SUBJECT_ALTERNATIVE_NAME:
san_list = ext.value.get_values_for_type(x509.DNSName)
domains.extend(san_list)
# Extract the common name (CN) from the subject
common_name = cert_obj.subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME)
if common_name:
if common_name[0].value not in domains:
domains.append(common_name[0].value)
if domains:
cert_domains = []
for cn in domains:
cert_domains.append(cn)
if cn == domain:
domain_check = True
elif cn.startswith("*."):
if domain.endswith(cn[1:]):
domain_check = True
returns["cert"]["domains"] = cert_domains
returns["cert"]["domain"] = domain_check
expiry_date = cert_obj.not_valid_after_utc
# Check if expiry date is past
if expiry_date < datetime.datetime.now(datetime.timezone.utc):
returns["cert"]["expired"] = True
returns["cert"]["valid"] = False
returns["cert"] = {
"cert":"",
"domains": [],
"domain": False,
"expired": False,
"valid": False,
"expiry_date": ""
}
returns["tlsa"] = {
"server": "",
"nameserver": "",
"match": False
}
else:
returns["cert"]["expired"] = False
returns["cert"]["valid"] = True if domain_check else False
cert = certificates[0]
returns["cert"] = {"cert": cert}
with tempfile.NamedTemporaryFile(mode="w", delete=False) as temp_cert_file:
temp_cert_file.write(cert)
temp_cert_file.seek(0)
returns["cert"]["expiry_date"] = expiry_date.strftime("%d %B %Y %H:%M:%S")
tlsa_command = ["openssl","x509","-in",temp_cert_file.name,"-pubkey","-noout","|","openssl","pkey","-pubin","-outform","der","|","openssl","dgst","-sha256","-binary",]
tlsa_process = subprocess.Popen(" ".join(tlsa_command), shell=True, stdout=subprocess.PIPE)
tlsa_output, _ = tlsa_process.communicate()
tlsa_server = "3 1 1 " + binascii.hexlify(tlsa_output).decode("utf-8")
returns["tlsa"] = {
"server": tlsa_server,
"nameserver": "",
"match": False
}
# Get domains
cert_obj = x509.load_pem_x509_certificate(cert.encode("utf-8"), default_backend())
domains = []
for ext in cert_obj.extensions:
if ext.oid == x509.ExtensionOID.SUBJECT_ALTERNATIVE_NAME:
san_list = ext.value.get_values_for_type(x509.DNSName)
domains.extend(san_list)
# Extract the common name (CN) from the subject
common_name = cert_obj.subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME)
if common_name:
if common_name[0].value not in domains:
domains.append(common_name[0].value)
if domains:
cert_domains = []
for cn in domains:
cert_domains.append(cn)
if cn == domain:
domain_check = True
elif cn.startswith("*."):
if domain.endswith(cn[1:]):
domain_check = True
returns["cert"]["domains"] = cert_domains
returns["cert"]["domain"] = domain_check
expiry_date = cert_obj.not_valid_after_utc
# Check if expiry date is past
if expiry_date < datetime.datetime.now(datetime.timezone.utc):
returns["cert"]["expired"] = True
returns["cert"]["valid"] = False
else:
returns["cert"]["expired"] = False
returns["cert"]["valid"] = True if domain_check else False
returns["cert"]["expiry_date"] = expiry_date.strftime("%d %B %Y %H:%M:%S")
try:
# Check for TLSA record
response = resolver.resolve("_443._tcp."+domain, "TLSA")
response = resolver.resolve("_443._tcp."+domain, "TLSA",lifetime=10)
tlsa_records = []
for record in response:
tlsa_records.append(str(record))
if not tlsa_records:
returns["message"] = "No TLSA record found on DNS"
return returns
returns["tlsa"]["nameserver"] = tlsa_records[0]
if tlsa_server == tlsa_records[0]:
returns["tlsa"]["match"] = True
if tlsa_records:
returns["tlsa"]["nameserver"] = tlsa_records[0]
if tlsa_server == tlsa_records[0]:
returns["tlsa"]["match"] = True
except:
returns["message"] = "No TLSA record found on DNS"
return returns
returns["tlsa"]["error"] = "No TLSA record found on DNS"
# Check if valid
if returns["cert"]["valid"] and returns["tlsa"]["match"] and returns["dnssec"]["valid"]:
@@ -152,6 +168,9 @@ def check_ssl(domain: str):
return returns
# Catch all exceptions
except dns.exception.Timeout:
returns["message"] = f"DNS resolution timed out for {domain}. Please check your internet connection or try again later."
return returns
except Exception as e:
returns["message"] = f"An error occurred: {e}"
return returns
@@ -159,11 +178,130 @@ def check_ssl(domain: str):
def validate_dnssec(domain):
# Pick a random resolver
resolverIP = random.choice(resolver.nameservers)
resolverIP = resolver.nameservers[0]
resolverPort = resolver.port
# delv @194.50.5.28 -a hsd-ksk nathan.woodburn A +rtrace +vtrace
command = f"delv @{resolverIP} -a hsd-ksk {domain} A +rtrace +vtrace"
command = f"delv @{resolverIP} -p {resolverPort} -a hsd-ksk {domain} A +rtrace +vtrace"
result = subprocess.run(command, shell=True, capture_output=True, text=True)
if "; fully validated" in result.stdout:
return {"valid": True, "message": "DNSSEC is valid", "output": result.stdout, "errors": result.stderr}
if "; fully validated" in result.stdout or "; negative response, fully validated" in result.stdout:
return {"valid": True, "message": "DNSSEC is valid", "output": result.stderr + result.stdout}
else:
return {"valid": False, "message": "DNSSEC is not valid", "output": result.stdout, "errors": result.stderr}
return {"valid": False, "message": "DNSSEC is not valid", "output": result.stderr + result.stdout}
def curl(url: str):
if not url.startswith("http"):
url = "http://" + url
try:
# curl --doh-url https://hnsdoh.com/dns-query {url} --insecure
command = f"curl --doh-url https://hnsdoh.com/dns-query {url} --insecure --silent -A 'Woodburn'"
response = subprocess.run(command, shell=True, capture_output=True, text=True, timeout=10)
if response.returncode != 0:
return {"success": False, "error": response.stderr}
else:
return {"success": True, "result": response.stdout}
except Exception as e:
return {"success": False, "error": "An error occurred", "message": str(e)}
class ProxyError(Exception):
def __init__(self, message):
self.message = message
self.text = message
self.ok = False
self.status_code = 500
super().__init__(self.message)
def proxy(url: str) -> requests.Response:
try:
r = DoHsession.get(url,verify=False,timeout=30)
return r
except Exception as e:
return ProxyError(str(e))
def cleanProxyContent(htmlContent: str,url:str, proxyHost: str):
# Set proxy host to https if not 127.0.0.1 or localhost
if ":5000" not in proxyHost:
proxyHost = proxyHost.replace("http","https")
# Find all instances of the url in the html
hostUrl = f"{urlparse(url).scheme}://{urlparse(url).netloc}"
proxyUrl = f"{proxyHost}proxy/{hostUrl}"
# htmlContent = htmlContent.replace(hostUrl,proxyUrl)
# parse html
soup = BeautifulSoup(htmlContent, 'html.parser')
# find all resources
for linkType in ['link','img','script', 'a']:
links = soup.find_all(linkType)
for link in links:
for attrib in ['src','href']:
if link.has_attr(attrib):
if str(link[attrib]).startswith('/'):
link.attrs[attrib] = proxyUrl + link[attrib]
continue
if str(link[attrib]).startswith('http'):
link.attrs[attrib] = str(link[attrib]).replace(hostUrl,proxyUrl)
continue
ignored = False
for ignore in ["data:", "mailto:", "tel:", "javascript:", "blob:"]:
if str(link[attrib]).startswith(ignore):
ignored = True
break
if not ignored:
if link[attrib].startswith("/"):
link.attrs[attrib] = f"{proxyUrl}{link[attrib]}"
else:
link.attrs[attrib] = f"{proxyUrl}/{urlparse(url).path}{link[attrib]}"
scripts = soup.find_all('script')
for script in scripts:
if script.has_attr("text"):
script.attrs["text"] = proxyCleanJS(script.text,url,proxyHost)
continue
if not script.has_attr("contents"):
continue
if len(script.contents) > 0:
newScript = soup.new_tag("script")
for content in script.contents:
newScript.append(proxyCleanJS(content,url,proxyHost))
script.replace_with(newScript)
return soup.prettify()
def proxyCleanJS(jsContent: str, url: str, proxyHost: str):
# Set proxy host to https if not 127.0.0.1 or localhost
if ":5000" not in proxyHost:
proxyHost = proxyHost.replace("http","https")
hostUrl = f"{urlparse(url).scheme}://{urlparse(url).netloc}"
proxyUrl = f"{proxyHost}proxy/{hostUrl}"
if "dprofile" in url:
jsContent = jsContent.replace("window.location.hostname", f"\"{urlparse(url).netloc}\"")
jsContent = jsContent.replace("src=\"img", f"src=\"{proxyUrl}/img")
return jsContent
# Replace all instances of the url with the proxy url
hostUrl = f"{urlparse(url).scheme}://{urlparse(url).netloc}"
proxyUrl = f"{proxyHost}proxy/{hostUrl}"
jsContent = jsContent.replace(hostUrl,proxyUrl)
# Common ways to get current url
for locator in ["window.location.href","window.location","location.href","location"]:
jsContent = jsContent.replace(locator,proxyUrl)
return jsContent
# if __name__ == "__main__":
# print(curl("https://dso.dprofile"))