diff options
| author | Christian Cleberg <[email protected]> | 2026-04-03 16:17:43 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-04-03 16:17:43 -0500 |
| commit | ede15c586af71b8d6f2c190ffdad0324414cac06 (patch) | |
| tree | 4d3676531b62d7bde7d18bb5030be464dc8db0ac | |
| parent | 376e7b55b4a0c4f54faa0c2828dea4fd17dff288 (diff) | |
| download | domain-dig-ede15c586af71b8d6f2c190ffdad0324414cac06.tar.gz domain-dig-ede15c586af71b8d6f2c190ffdad0324414cac06.tar.bz2 domain-dig-ede15c586af71b8d6f2c190ffdad0324414cac06.zip | |
fix: various bugs
- fix Email section to left align
- ensure container contents can overflow scroll but page cannot
- fix map width
| -rw-r--r-- | DomainDig.xcodeproj/project.pbxproj | 8 | ||||
| -rw-r--r-- | DomainDig/ContentView.swift | 77 | ||||
| -rw-r--r-- | DomainDig/HistoryView.swift | 77 | ||||
| -rw-r--r-- | claude.md | 171 |
4 files changed, 94 insertions, 239 deletions
diff --git a/DomainDig.xcodeproj/project.pbxproj b/DomainDig.xcodeproj/project.pbxproj index f37b290..12f8baf 100644 --- a/DomainDig.xcodeproj/project.pbxproj +++ b/DomainDig.xcodeproj/project.pbxproj @@ -265,7 +265,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 4; DEVELOPMENT_TEAM = ZCNAX3VL9D; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -282,7 +282,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2; + MARKETING_VERSION = 1.2.1; PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DomainDig; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = YES; @@ -301,7 +301,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 4; DEVELOPMENT_TEAM = ZCNAX3VL9D; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -318,7 +318,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2; + MARKETING_VERSION = 1.2.1; PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DomainDig; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = YES; diff --git a/DomainDig/ContentView.swift b/DomainDig/ContentView.swift index 940a25b..b6a7607 100644 --- a/DomainDig/ContentView.swift +++ b/DomainDig/ContentView.swift @@ -7,7 +7,7 @@ struct ContentView: View { var body: some View { NavigationStack { - ScrollView { + ScrollView(.vertical) { VStack(spacing: 0) { inputSection if viewModel.hasRun { @@ -225,7 +225,7 @@ struct ContentView: View { .background(Color(.systemGray6).opacity(0.5)) .cornerRadius(6) } else { - VStack(alignment: .leading, spacing: 4) { + horizontallyScrollableCard { ForEach(viewModel.redirectChain) { hop in HStack(alignment: .top, spacing: 6) { Text("\(hop.stepNumber)") @@ -248,9 +248,6 @@ struct ContentView: View { } } } - .padding(10) - .background(Color(.systemGray6).opacity(0.5)) - .cornerRadius(6) } } .padding(.top, 16) @@ -281,7 +278,7 @@ struct ContentView: View { } private var ptrRow: some View { - VStack(alignment: .leading, spacing: 4) { + horizontallyScrollableCard { Text("PTR (Reverse DNS)") .font(.system(.subheadline, design: .monospaced)) .fontWeight(.semibold) @@ -302,13 +299,10 @@ struct ContentView: View { .foregroundStyle(.secondary) } } - .padding(10) - .background(Color(.systemGray6).opacity(0.5)) - .cornerRadius(6) } private func dnsRecordSection(_ section: DNSSection) -> some View { - VStack(alignment: .leading, spacing: 4) { + horizontallyScrollableCard { Text(section.recordType.rawValue) .font(.system(.subheadline, design: .monospaced)) .fontWeight(.semibold) @@ -334,9 +328,6 @@ struct ContentView: View { dnsRecordRows(section.wildcardRecords) } } - .padding(10) - .background(Color(.systemGray6).opacity(0.5)) - .cornerRadius(6) } private func dnsRecordRows(_ records: [DNSRecord]) -> some View { @@ -369,16 +360,14 @@ struct ContentView: View { } else if let error = viewModel.emailSecurityError { errorLabel(error) } else if let email = viewModel.emailSecurity { - VStack(alignment: .leading, spacing: 6) { + horizontallyScrollableCard(spacing: 6) { emailSecurityRow("SPF", record: email.spf) emailSecurityRow("DMARC", record: email.dmarc) emailSecurityRow("DKIM", record: email.dkim) } - .padding(10) - .background(Color(.systemGray6).opacity(0.5)) - .cornerRadius(6) } } + .frame(maxWidth: .infinity, alignment: .leading) .padding(.top, 16) } @@ -434,7 +423,7 @@ struct ContentView: View { } private func sslDetail(_ info: SSLCertificateInfo) -> some View { - VStack(alignment: .leading, spacing: 8) { + horizontallyScrollableCard(spacing: 8) { certRow("Common Name", info.commonName) certRow("Issuer", info.issuer) @@ -466,9 +455,6 @@ struct ContentView: View { certRow("Chain Depth", "\(info.chainDepth)") } - .padding(10) - .background(Color(.systemGray6).opacity(0.5)) - .cornerRadius(6) } // MARK: - HTTP Headers @@ -484,7 +470,7 @@ struct ContentView: View { } else if let error = viewModel.httpHeadersError { errorLabel(error) } else { - VStack(alignment: .leading, spacing: 4) { + horizontallyScrollableCard { ForEach(viewModel.httpHeaders) { header in HStack(alignment: .top, spacing: 4) { Text(header.name + ":") @@ -497,9 +483,6 @@ struct ContentView: View { } } } - .padding(10) - .background(Color(.systemGray6).opacity(0.5)) - .cornerRadius(6) } } .padding(.top, 16) @@ -533,14 +516,17 @@ struct ContentView: View { private func ipGeolocationDetail(_ geo: IPGeolocation) -> some View { VStack(alignment: .leading, spacing: 6) { - certRow("IP", geo.ip) - if let org = geo.org { - certRow("Org / ISP", org) - } - let location = [geo.city, geo.region, geo.country_name].compactMap { $0 }.joined(separator: ", ") - if !location.isEmpty { - certRow("Location", location) + horizontallyScrollableContent(spacing: 6) { + certRow("IP", geo.ip) + if let org = geo.org { + certRow("Org / ISP", org) + } + let location = [geo.city, geo.region, geo.country_name].compactMap { $0 }.joined(separator: ", ") + if !location.isEmpty { + certRow("Location", location) + } } + if let lat = geo.latitude, let lon = geo.longitude { let coordinate = CLLocationCoordinate2D(latitude: lat, longitude: lon) Map(initialPosition: .region(MKCoordinateRegion( @@ -550,6 +536,7 @@ struct ContentView: View { Marker(geo.ip, coordinate: coordinate) } .mapStyle(.standard) + .frame(maxWidth: .infinity) .frame(height: 180) .cornerRadius(8) } @@ -620,6 +607,32 @@ struct ContentView: View { } } + private func horizontallyScrollableCard<Content: View>( + spacing: CGFloat = 4, + @ViewBuilder content: () -> Content + ) -> some View { + horizontallyScrollableContent(spacing: spacing) { + content() + } + .padding(10) + .background(Color(.systemGray6).opacity(0.5)) + .cornerRadius(6) + } + + private func horizontallyScrollableContent<Content: View>( + spacing: CGFloat = 4, + @ViewBuilder content: () -> Content + ) -> some View { + ScrollView(.horizontal) { + VStack(alignment: .leading, spacing: spacing) { + content() + } + .scrollTargetLayout() + } + .scrollBounceBehavior(.basedOnSize, axes: .horizontal) + .frame(maxWidth: .infinity, alignment: .leading) + } + private func errorLabel(_ message: String) -> some View { Label(message, systemImage: "exclamationmark.triangle.fill") .font(.system(.caption, design: .monospaced)) diff --git a/DomainDig/HistoryView.swift b/DomainDig/HistoryView.swift index b64ebf3..1963509 100644 --- a/DomainDig/HistoryView.swift +++ b/DomainDig/HistoryView.swift @@ -65,7 +65,7 @@ struct HistoryDetailView: View { }() var body: some View { - ScrollView { + ScrollView(.vertical) { VStack(alignment: .leading, spacing: 0) { cachedBanner reachabilitySection @@ -157,7 +157,7 @@ struct HistoryDetailView: View { .background(Color(.systemGray6).opacity(0.5)) .cornerRadius(6) } else { - VStack(alignment: .leading, spacing: 4) { + horizontallyScrollableCard { ForEach(entry.redirectChain) { hop in HStack(alignment: .top, spacing: 6) { Text("\(hop.stepNumber)") @@ -180,9 +180,6 @@ struct HistoryDetailView: View { } } } - .padding(10) - .background(Color(.systemGray6).opacity(0.5)) - .cornerRadius(6) } } } @@ -195,7 +192,7 @@ struct HistoryDetailView: View { VStack(alignment: .leading, spacing: 12) { sectionHeader("DNS Records") ForEach(entry.dnsSections) { section in - VStack(alignment: .leading, spacing: 4) { + horizontallyScrollableCard { Text(section.recordType.rawValue) .font(.system(.subheadline, design: .monospaced)) .fontWeight(.semibold) @@ -220,12 +217,9 @@ struct HistoryDetailView: View { recordRows(section.wildcardRecords) } } - .padding(10) - .background(Color(.systemGray6).opacity(0.5)) - .cornerRadius(6) if section.recordType == .A { - VStack(alignment: .leading, spacing: 4) { + horizontallyScrollableCard { Text("PTR (Reverse DNS)") .font(.system(.subheadline, design: .monospaced)) .fontWeight(.semibold) @@ -242,9 +236,6 @@ struct HistoryDetailView: View { .foregroundStyle(.secondary) } } - .padding(10) - .background(Color(.systemGray6).opacity(0.5)) - .cornerRadius(6) } } } @@ -259,16 +250,14 @@ struct HistoryDetailView: View { VStack(alignment: .leading, spacing: 12) { if let email = entry.emailSecurity { sectionHeader("Email Security") - VStack(alignment: .leading, spacing: 6) { + horizontallyScrollableCard(spacing: 6) { historyEmailRow("SPF", record: email.spf) historyEmailRow("DMARC", record: email.dmarc) historyEmailRow("DKIM", record: email.dkim) } - .padding(10) - .background(Color(.systemGray6).opacity(0.5)) - .cornerRadius(6) } } + .frame(maxWidth: .infinity, alignment: .leading) .padding(.top, 16) } @@ -310,7 +299,7 @@ struct HistoryDetailView: View { VStack(alignment: .leading, spacing: 12) { if let info = entry.sslInfo { sectionHeader("SSL / TLS Certificate") - VStack(alignment: .leading, spacing: 8) { + horizontallyScrollableCard(spacing: 8) { labelRow("Common Name", info.commonName) labelRow("Issuer", info.issuer) @@ -341,9 +330,6 @@ struct HistoryDetailView: View { labelRow("Chain Depth", "\(info.chainDepth)") } - .padding(10) - .background(Color(.systemGray6).opacity(0.5)) - .cornerRadius(6) } } .padding(.top, 16) @@ -355,7 +341,7 @@ struct HistoryDetailView: View { VStack(alignment: .leading, spacing: 12) { if !entry.httpHeaders.isEmpty { sectionHeader("HTTP Headers") - VStack(alignment: .leading, spacing: 4) { + horizontallyScrollableCard { ForEach(entry.httpHeaders) { header in HStack(alignment: .top, spacing: 4) { Text(header.name + ":") @@ -368,9 +354,6 @@ struct HistoryDetailView: View { } } } - .padding(10) - .background(Color(.systemGray6).opacity(0.5)) - .cornerRadius(6) } } .padding(.top, 16) @@ -383,14 +366,17 @@ struct HistoryDetailView: View { if let geo = entry.ipGeolocation { sectionHeader("IP Location") VStack(alignment: .leading, spacing: 6) { - labelRow("IP", geo.ip) - if let org = geo.org { - labelRow("Org / ISP", org) - } - let location = [geo.city, geo.region, geo.country_name].compactMap { $0 }.joined(separator: ", ") - if !location.isEmpty { - labelRow("Location", location) + horizontallyScrollableContent(spacing: 6) { + labelRow("IP", geo.ip) + if let org = geo.org { + labelRow("Org / ISP", org) + } + let location = [geo.city, geo.region, geo.country_name].compactMap { $0 }.joined(separator: ", ") + if !location.isEmpty { + labelRow("Location", location) + } } + if let lat = geo.latitude, let lon = geo.longitude { let coordinate = CLLocationCoordinate2D(latitude: lat, longitude: lon) Map(initialPosition: .region(MKCoordinateRegion( @@ -400,6 +386,7 @@ struct HistoryDetailView: View { Marker(geo.ip, coordinate: coordinate) } .mapStyle(.standard) + .frame(maxWidth: .infinity) .frame(height: 180) .cornerRadius(8) } @@ -481,6 +468,32 @@ struct HistoryDetailView: View { } } + private func horizontallyScrollableCard<Content: View>( + spacing: CGFloat = 4, + @ViewBuilder content: () -> Content + ) -> some View { + horizontallyScrollableContent(spacing: spacing) { + content() + } + .padding(10) + .background(Color(.systemGray6).opacity(0.5)) + .cornerRadius(6) + } + + private func horizontallyScrollableContent<Content: View>( + spacing: CGFloat = 4, + @ViewBuilder content: () -> Content + ) -> some View { + ScrollView(.horizontal) { + VStack(alignment: .leading, spacing: spacing) { + content() + } + .scrollTargetLayout() + } + .scrollBounceBehavior(.basedOnSize, axes: .horizontal) + .frame(maxWidth: .infinity, alignment: .leading) + } + private func errorLabel(_ message: String) -> some View { Label(message, systemImage: "exclamationmark.triangle.fill") .font(.system(.caption, design: .monospaced)) diff --git a/claude.md b/claude.md deleted file mode 100644 index bee59f1..0000000 --- a/claude.md +++ /dev/null @@ -1,171 +0,0 @@ -# DomainDig – Project Context - -## What this is -An iOS utility app for querying DNS records, inspecting SSL/TLS certificates, -checking HTTP headers, measuring TCP reachability, geolocating IP addresses, -analyzing email security records, tracing redirect chains, performing reverse -DNS lookups, and scanning common ports for any domain. Designed for developers, -sysadmins, and anyone who manages or troubleshoots domains and servers. - -## Target user -Technically literate adults — developers, IT/sysadmin, homelab enthusiasts. -Comfortable with terms like A record, CNAME, MX, TTL, and TLS. The UI should be -clean and information-dense, not hand-holding. - -## Design principles -- Minimal, utilitarian UI — this is a tool, not a consumer app -- Results should be readable at a glance — use clear labels, monospace where - appropriate for IPs and raw values -- Dark mode preferred as the primary aesthetic -- No onboarding, no tutorials, no splash screens -- Fast — results should appear as soon as they're available, not after all - lookups complete -- Each result section loads independently with its own ProgressView -- Errors shown inline per-section, never as global alerts - -## Features - -### DNS Lookup -Query the following record types for any domain via DNS-over-HTTPS (use -Cloudflare 1.1.1.1 — https://cloudflare-dns.com/dns-query): -- A -- AAAA -- MX -- NS -- TXT -- CNAME - -Display each record type in its own section. Show TTL alongside each result. -If a record type returns no results, show "No records found" for that type -rather than hiding the section entirely. - -After the apex query, also queries `*.{domain}` for A, AAAA, MX, and TXT. -Wildcard results are shown as a sub-section beneath each record type, labelled -with `*.{domain}`. Hidden if no wildcard records are returned. - -### SSL/TLS Certificate Check -Connect to the domain on port 443 via URLSession and inspect the server's -certificate chain using URLSession delegate methods. Display: -- Common name (CN) -- Subject Alternative Names (SANs) -- Issuer -- Valid from / Valid until dates -- Days until expiry — highlight in red if under 30 days, yellow if under 60 -- Certificate chain depth - -### HTTP Headers Check -Fire a HEAD request to `https://{domain}` and display all response headers. -Header names in cyan, values in primary. Security-relevant headers highlighted -in yellow: `strict-transport-security`, `x-frame-options`, -`x-content-type-options`, `content-security-policy`, `referrer-policy`. -Service: `HTTPHeadersService.swift`. - -### Reachability / TCP Latency -Use `NWConnection` (Network framework) to attempt TCP connections on ports 443 -and 80. Measure time to `.ready` state. Display green/red dot, latency in ms, -and reachable/unreachable status. Timeout after 5 seconds. -Service: `ReachabilityService.swift`. - -### IP Geolocation with Map -After DNS A record resolves, take the first IP and query -`https://ipapi.co/{ip}/json/` for country, region, city, org, lat/lon. -Display in an "IP Location" section with a SwiftUI `Map` (MapKit) centered on -the coordinates with a `Marker`. Map height 180pt, `.standard` style. -Service: `IPGeolocationService.swift`. - -### Email Security Analysis -Parse SPF from existing TXT records (no extra query). Query `_dmarc.{domain}` -for DMARC and try common DKIM selectors (`default`, `google`, `mail`) via DoH. -Display green checkmark if found, red ✗ if not. Full record values truncated -to 80 chars with tap-to-expand. Triggered after DNS completes. -Service: `EmailSecurityService.swift`. - -### Reverse DNS / PTR Lookup -After DNS A record resolves, construct reverse DNS name (reversed octets + -`.in-addr.arpa`) and query PTR record via Cloudflare DoH. Displayed inline -in the DNS Records section below the A record sub-section. -Service: `ReverseDNSService.swift`. - -### Redirect Chain -Fire HTTP request to `http://{domain}` with redirects disabled. Follow up to -10 redirects manually, recording each hop's URL and status code. Display step -number, status code in cyan, URL, and "(final)" on the last hop. Shows -"No redirects — direct connection" if the first request returns 200. -Service: `RedirectChainService.swift`. - -### Common Port Scanner -Probe 10 common ports (21/FTP, 22/SSH, 25/SMTP, 80/HTTP, 443/HTTPS, -587/SMTP-TLS, 3306/MySQL, 5432/PostgreSQL, 8080/HTTP-Alt, 8443/HTTPS-Alt) -using `NWConnection` with 3-second timeout. All probes run concurrently. -Green dot for open, grey dot for closed. Closed is expected — no error shown. -Service: `PortScanService.swift`. - -### Results layout -- Domain input at the top — large text field, keyboard shows on launch -- Run button to trigger all lookups simultaneously -- Results displayed in clearly separated sections: - Reachability → Redirect Chain → DNS Records (with PTR inline) → - Email Security → SSL/TLS Certificate → HTTP Headers → IP Location → - Open Ports -- Each section loads independently — don't block one section waiting for another -- Email security, PTR, and IP geolocation are chained after DNS; all others - run in parallel - -### Recent searches -Store the last 20 searched domains locally using UserDefaults. Display them as -a tappable list below the text field when no results are showing. Tapping a -recent domain populates the text field and runs the lookup immediately. Include -a "Clear" button to wipe history. Most recent at the top. - -### Saved domains -Bookmark button (SF Symbol: `bookmark` / `bookmark.fill`) in the results -toolbar area, next to the share button. Tapping saves/unsaves the current -domain. Filled icon when saved. Saved domains viewable from a toolbar button -that pushes to `SavedDomainsView` — a list of saved domains, tappable to run -lookups, with swipe-to-delete and an Edit button for bulk deletion. -Persisted in UserDefaults under key `savedDomains`. - -### Lookup history with cached results -After each successful lookup, a snapshot of all results (DNS, SSL, HTTP headers, -reachability, geolocation, email security, PTR, redirect chain, port scan) is -saved to history in UserDefaults as JSON. Capped at 50 entries. `HistoryView` -shows past lookups with domain and timestamp. Tapping shows full cached results -in `HistoryDetailView` using the same layout, labelled as cached with the -original timestamp. Model: `HistoryEntry` (Codable). - -### Share / export -A share button (SF Symbol: `square.and.arrow.up`) in the top-right of the -results area. Formats the full results as plain text and presents the iOS share -sheet via UIActivityViewController. The export includes: domain, timestamp, -reachability, redirect chain, all DNS records with TTLs and PTR, email security -records, SSL cert fields, HTTP headers, IP geolocation data, and port scan -results. - -## Technical constraints -- SwiftUI, iOS only -- Fully offline except for network requests (DNS-over-HTTPS, SSL, HTTP HEAD, - TCP connections, geolocation API) -- No accounts, no analytics, no ads -- No third-party dependencies — URLSession, Network, MapKit only -- Targets latest iOS - -## Architecture -- `Models.swift` — All data models (DNS, SSL, HTTP headers, reachability, - geolocation, history entry), all Codable for persistence -- `DomainViewModel.swift` — `@Observable` view model orchestrating all lookups, - managing state, history, saved domains, recent searches, and export -- `ContentView.swift` — Main screen with input, all result sections, toolbar -- `SavedDomainsView.swift` — Saved domains list with edit/delete -- `HistoryView.swift` — History list + `HistoryDetailView` for cached results -- Services: `DNSLookupService`, `SSLCheckService`, `HTTPHeadersService`, - `ReachabilityService`, `IPGeolocationService`, `EmailSecurityService`, - `ReverseDNSService`, `RedirectChainService`, `PortScanService` - -## What good looks like -A developer pastes a domain, taps run, and within a couple of seconds sees a -clean breakdown of TCP reachability, redirect chain, every DNS record type with -reverse DNS, email security posture, the full SSL cert status, HTTP response -headers with security headers highlighted, IP geolocation with a map, and a -port scan of common services. It should feel like a native, polished version of -running `dig`, `openssl s_client`, `curl -I`, `nmap`, and `whois` from the -terminal. |
