summaryrefslogtreecommitdiff
path: root/DomainDig/ContentView.swift
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-03-18 11:59:38 -0500
committerChristian Cleberg <[email protected]>2026-03-18 11:59:38 -0500
commit376e7b55b4a0c4f54faa0c2828dea4fd17dff288 (patch)
tree540d11ad423f14975553f2490617e05aea61ae2c /DomainDig/ContentView.swift
parente11b7d29a6aa9e52f63fcb35ee7ed9127850fffe (diff)
downloaddomain-dig-376e7b55b4a0c4f54faa0c2828dea4fd17dff288.tar.gz
domain-dig-376e7b55b4a0c4f54faa0c2828dea4fd17dff288.tar.bz2
domain-dig-376e7b55b4a0c4f54faa0c2828dea4fd17dff288.zip
realigning commits
Diffstat (limited to 'DomainDig/ContentView.swift')
-rw-r--r--DomainDig/ContentView.swift402
1 files changed, 383 insertions, 19 deletions
diff --git a/DomainDig/ContentView.swift b/DomainDig/ContentView.swift
index b6588b4..940a25b 100644
--- a/DomainDig/ContentView.swift
+++ b/DomainDig/ContentView.swift
@@ -1,4 +1,5 @@
import SwiftUI
+import MapKit
struct ContentView: View {
@State private var viewModel = DomainViewModel()
@@ -10,21 +11,15 @@ struct ContentView: View {
VStack(spacing: 0) {
inputSection
if viewModel.hasRun {
- if viewModel.resultsLoaded {
- HStack {
- Spacer()
- Button {
- shareResults()
- } label: {
- Image(systemName: "square.and.arrow.up")
- .font(.system(.body))
- .foregroundStyle(.secondary)
- }
- }
- .padding(.top, 8)
- }
+ actionButtons
+ reachabilitySection
+ redirectChainSection
dnsResultsSection
+ emailSecuritySection
sslResultsSection
+ httpHeadersSection
+ ipGeolocationSection
+ portScanSection
} else if !viewModel.recentSearches.isEmpty {
recentSearchesSection
}
@@ -36,6 +31,30 @@ struct ContentView: View {
.navigationTitle("DomainDig")
.toolbarColorScheme(.dark, for: .navigationBar)
.preferredColorScheme(.dark)
+ .toolbar {
+ ToolbarItemGroup(placement: .topBarTrailing) {
+ if viewModel.hasRun {
+ Button {
+ viewModel.reset()
+ } label: {
+ Image(systemName: "xmark.circle")
+ .foregroundStyle(.secondary)
+ }
+ }
+ NavigationLink {
+ SavedDomainsView(viewModel: viewModel)
+ } label: {
+ Image(systemName: "bookmark")
+ .foregroundStyle(.secondary)
+ }
+ NavigationLink {
+ HistoryView(viewModel: viewModel)
+ } label: {
+ Image(systemName: "clock.arrow.trianglehead.counterclockwise.rotate.90")
+ .foregroundStyle(.secondary)
+ }
+ }
+ }
}
.onAppear {
domainFieldFocused = true
@@ -72,6 +91,31 @@ struct ContentView: View {
.padding(.vertical, 16)
}
+ // MARK: - Action Buttons (Share + Bookmark)
+
+ private var actionButtons: some View {
+ HStack {
+ Spacer()
+ if viewModel.resultsLoaded {
+ Button {
+ viewModel.toggleSavedDomain()
+ } label: {
+ Image(systemName: viewModel.isCurrentDomainSaved ? "bookmark.fill" : "bookmark")
+ .font(.system(.body))
+ .foregroundStyle(viewModel.isCurrentDomainSaved ? .yellow : .secondary)
+ }
+ Button {
+ shareResults()
+ } label: {
+ Image(systemName: "square.and.arrow.up")
+ .font(.system(.body))
+ .foregroundStyle(.secondary)
+ }
+ }
+ }
+ .padding(.top, 8)
+ }
+
// MARK: - Recent Searches
private var recentSearchesSection: some View {
@@ -108,6 +152,110 @@ struct ContentView: View {
.padding(.top, 8)
}
+ // MARK: - Reachability
+
+ private var reachabilitySection: some View {
+ VStack(alignment: .leading, spacing: 12) {
+ sectionHeader("Reachability")
+
+ if viewModel.reachabilityLoading {
+ ProgressView("Checking ports…")
+ .frame(maxWidth: .infinity, alignment: .center)
+ .padding()
+ } else if let error = viewModel.reachabilityError {
+ errorLabel(error)
+ } else {
+ VStack(alignment: .leading, spacing: 4) {
+ ForEach(viewModel.reachabilityResults) { result in
+ HStack(spacing: 8) {
+ Circle()
+ .fill(result.reachable ? Color.green : Color.red)
+ .frame(width: 8, height: 8)
+ Text("Port \(result.port)")
+ .font(.system(.caption, design: .monospaced))
+ if result.reachable, let ms = result.latencyMs {
+ Text("\(ms)ms")
+ .font(.system(.caption, design: .monospaced))
+ .foregroundStyle(.secondary)
+ } else if !result.reachable {
+ Text("—")
+ .font(.system(.caption, design: .monospaced))
+ .foregroundStyle(.secondary)
+ }
+ Spacer()
+ Text(result.reachable ? "Reachable" : "Unreachable")
+ .font(.system(.caption, design: .monospaced))
+ .foregroundStyle(result.reachable ? .green : .red)
+ }
+ }
+ }
+ .padding(10)
+ .background(Color(.systemGray6).opacity(0.5))
+ .cornerRadius(6)
+ }
+ }
+ .padding(.top, 8)
+ }
+
+ // MARK: - Redirect Chain
+
+ private var redirectChainSection: some View {
+ VStack(alignment: .leading, spacing: 12) {
+ sectionHeader("Redirect Chain")
+
+ if viewModel.redirectChainLoading {
+ ProgressView("Tracing redirects…")
+ .frame(maxWidth: .infinity, alignment: .center)
+ .padding()
+ } else if let error = viewModel.redirectChainError {
+ errorLabel(error)
+ } else if viewModel.redirectChain.isEmpty {
+ Text("No redirect data")
+ .font(.system(.caption, design: .monospaced))
+ .foregroundStyle(.secondary)
+ .padding(8)
+ } else if viewModel.redirectChain.count == 1,
+ let only = viewModel.redirectChain.first,
+ only.isFinal, !(300...399).contains(only.statusCode) {
+ Text("No redirects — direct connection")
+ .font(.system(.caption, design: .monospaced))
+ .foregroundStyle(.secondary)
+ .padding(10)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .background(Color(.systemGray6).opacity(0.5))
+ .cornerRadius(6)
+ } else {
+ VStack(alignment: .leading, spacing: 4) {
+ ForEach(viewModel.redirectChain) { hop in
+ HStack(alignment: .top, spacing: 6) {
+ Text("\(hop.stepNumber)")
+ .font(.system(.caption, design: .monospaced))
+ .foregroundStyle(.secondary)
+ .frame(width: 16, alignment: .trailing)
+ Text("\(hop.statusCode)")
+ .font(.system(.caption, design: .monospaced))
+ .foregroundStyle(.cyan)
+ .frame(width: 30, alignment: .leading)
+ Text(hop.url)
+ .font(.system(.caption, design: .monospaced))
+ .foregroundStyle(.primary)
+ .textSelection(.enabled)
+ if hop.isFinal {
+ Text("(final)")
+ .font(.system(.caption2, design: .monospaced))
+ .foregroundStyle(.secondary)
+ }
+ }
+ }
+ }
+ .padding(10)
+ .background(Color(.systemGray6).opacity(0.5))
+ .cornerRadius(6)
+ }
+ }
+ .padding(.top, 16)
+ }
+
// MARK: - DNS Results
private var dnsResultsSection: some View {
@@ -123,10 +271,40 @@ struct ContentView: View {
} else {
ForEach(viewModel.dnsSections) { section in
dnsRecordSection(section)
+ if section.recordType == .A {
+ ptrRow
+ }
}
}
}
- .padding(.top, 8)
+ .padding(.top, 16)
+ }
+
+ private var ptrRow: some View {
+ VStack(alignment: .leading, spacing: 4) {
+ Text("PTR (Reverse DNS)")
+ .font(.system(.subheadline, design: .monospaced))
+ .fontWeight(.semibold)
+ .foregroundStyle(.cyan)
+
+ if viewModel.ptrLoading {
+ ProgressView()
+ .frame(maxWidth: .infinity, alignment: .center)
+ .padding(4)
+ } else if let ptr = viewModel.ptrRecord {
+ Text(ptr)
+ .font(.system(.caption, design: .monospaced))
+ .foregroundStyle(.primary)
+ .textSelection(.enabled)
+ } else {
+ Text("No PTR record found")
+ .font(.system(.caption, design: .monospaced))
+ .foregroundStyle(.secondary)
+ }
+ }
+ .padding(10)
+ .background(Color(.systemGray6).opacity(0.5))
+ .cornerRadius(6)
}
private func dnsRecordSection(_ section: DNSSection) -> some View {
@@ -146,7 +324,6 @@ struct ContentView: View {
dnsRecordRows(section.records)
}
- // Wildcard sub-section (only shown when records exist)
if !section.wildcardRecords.isEmpty {
Text("*.\(viewModel.searchedDomain)")
.font(.system(.caption, design: .monospaced))
@@ -177,6 +354,66 @@ struct ContentView: View {
}
}
+ // MARK: - Email Security
+
+ @State private var expandedEmailField: String?
+
+ private var emailSecuritySection: some View {
+ VStack(alignment: .leading, spacing: 12) {
+ sectionHeader("Email Security")
+
+ if viewModel.emailSecurityLoading {
+ ProgressView("Checking email records…")
+ .frame(maxWidth: .infinity, alignment: .center)
+ .padding()
+ } else if let error = viewModel.emailSecurityError {
+ errorLabel(error)
+ } else if let email = viewModel.emailSecurity {
+ VStack(alignment: .leading, 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)
+ }
+ }
+ .padding(.top, 16)
+ }
+
+ private func emailSecurityRow(_ label: String, record: EmailSecurityRecord) -> some View {
+ VStack(alignment: .leading, spacing: 2) {
+ HStack(spacing: 8) {
+ Text(label)
+ .font(.system(.caption, design: .monospaced))
+ .fontWeight(.semibold)
+ .frame(width: 52, alignment: .leading)
+ Text(record.found ? "✓" : "✗")
+ .font(.system(.caption, design: .monospaced))
+ .foregroundStyle(record.found ? .green : .red)
+ if let value = record.value {
+ let isExpanded = expandedEmailField == label
+ let displayValue = isExpanded ? value : String(value.prefix(80))
+ Text(displayValue)
+ .font(.system(.caption2, design: .monospaced))
+ .foregroundStyle(.primary)
+ .textSelection(.enabled)
+ .lineLimit(isExpanded ? nil : 1)
+ .onTapGesture {
+ withAnimation {
+ expandedEmailField = isExpanded ? nil : label
+ }
+ }
+ } else {
+ Text("No record found")
+ .font(.system(.caption2, design: .monospaced))
+ .foregroundStyle(.secondary)
+ }
+ }
+ }
+ }
+
// MARK: - SSL Results
private var sslResultsSection: some View {
@@ -201,7 +438,6 @@ struct ContentView: View {
certRow("Common Name", info.commonName)
certRow("Issuer", info.issuer)
- // SANs
VStack(alignment: .leading, spacing: 2) {
Text("SANs")
.font(.system(.caption2, design: .monospaced))
@@ -235,6 +471,136 @@ struct ContentView: View {
.cornerRadius(6)
}
+ // MARK: - HTTP Headers
+
+ private var httpHeadersSection: some View {
+ VStack(alignment: .leading, spacing: 12) {
+ sectionHeader("HTTP Headers")
+
+ if viewModel.httpHeadersLoading {
+ ProgressView("Fetching headers…")
+ .frame(maxWidth: .infinity, alignment: .center)
+ .padding()
+ } else if let error = viewModel.httpHeadersError {
+ errorLabel(error)
+ } else {
+ VStack(alignment: .leading, spacing: 4) {
+ ForEach(viewModel.httpHeaders) { header in
+ HStack(alignment: .top, spacing: 4) {
+ Text(header.name + ":")
+ .font(.system(.caption, design: .monospaced))
+ .foregroundStyle(header.isSecurityHeader ? .yellow : .cyan)
+ Text(header.value)
+ .font(.system(.caption, design: .monospaced))
+ .foregroundStyle(.primary)
+ .textSelection(.enabled)
+ }
+ }
+ }
+ .padding(10)
+ .background(Color(.systemGray6).opacity(0.5))
+ .cornerRadius(6)
+ }
+ }
+ .padding(.top, 16)
+ }
+
+ // MARK: - IP Geolocation
+
+ private var ipGeolocationSection: some View {
+ VStack(alignment: .leading, spacing: 12) {
+ sectionHeader("IP Location")
+
+ if viewModel.ipGeolocationLoading {
+ ProgressView("Looking up location…")
+ .frame(maxWidth: .infinity, alignment: .center)
+ .padding()
+ } else if let geo = viewModel.ipGeolocation {
+ ipGeolocationDetail(geo)
+ } else if let error = viewModel.ipGeolocationError {
+ if error == "No A record available" {
+ Text("No location data available")
+ .font(.system(.caption, design: .monospaced))
+ .foregroundStyle(.secondary)
+ .padding(8)
+ } else {
+ errorLabel(error)
+ }
+ }
+ }
+ .padding(.top, 16)
+ }
+
+ 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)
+ }
+ if let lat = geo.latitude, let lon = geo.longitude {
+ let coordinate = CLLocationCoordinate2D(latitude: lat, longitude: lon)
+ Map(initialPosition: .region(MKCoordinateRegion(
+ center: coordinate,
+ span: MKCoordinateSpan(latitudeDelta: 1, longitudeDelta: 1)
+ ))) {
+ Marker(geo.ip, coordinate: coordinate)
+ }
+ .mapStyle(.standard)
+ .frame(height: 180)
+ .cornerRadius(8)
+ }
+ }
+ .padding(10)
+ .background(Color(.systemGray6).opacity(0.5))
+ .cornerRadius(6)
+ }
+
+ // MARK: - Port Scan
+
+ private var portScanSection: some View {
+ VStack(alignment: .leading, spacing: 12) {
+ sectionHeader("Open Ports")
+
+ if viewModel.portScanLoading {
+ ProgressView("Scanning ports…")
+ .frame(maxWidth: .infinity, alignment: .center)
+ .padding()
+ } else if let error = viewModel.portScanError {
+ errorLabel(error)
+ } else {
+ VStack(alignment: .leading, spacing: 4) {
+ ForEach(viewModel.portScanResults) { result in
+ HStack(spacing: 8) {
+ Circle()
+ .fill(result.open ? Color.green : Color(.systemGray4))
+ .frame(width: 8, height: 8)
+ Text("\(result.port)")
+ .font(.system(.caption, design: .monospaced))
+ .frame(width: 44, alignment: .leading)
+ Text(result.service)
+ .font(.system(.caption, design: .monospaced))
+ .foregroundStyle(result.open ? .primary : .secondary)
+ Spacer()
+ if result.open {
+ Text("Open")
+ .font(.system(.caption2, design: .monospaced))
+ .foregroundStyle(.green)
+ }
+ }
+ }
+ }
+ .padding(10)
+ .background(Color(.systemGray6).opacity(0.5))
+ .cornerRadius(6)
+ }
+ }
+ .padding(.top, 16)
+ }
+
// MARK: - Helpers
private func sectionHeader(_ title: String) -> some View {
@@ -269,8 +635,6 @@ struct ContentView: View {
private func shareResults() {
let text = viewModel.exportText()
-
- // Write to a named temp file so "Save to Files" uses a proper filename
let dateFmt = DateFormatter()
dateFmt.dateFormat = "yyyyMMdd_HHmmss"
let timestamp = dateFmt.string(from: Date())
@@ -295,7 +659,7 @@ struct ContentView: View {
}
}
-private extension DateFormatter {
+extension DateFormatter {
static let certDate: DateFormatter = {
let f = DateFormatter()
f.dateStyle = .medium