初始化
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
part of '_dnsolve.dart';
|
||||
|
||||
/// Represents an answer containing a list of generic records and a list of
|
||||
/// Service (SRV) records parsed from JSON data.
|
||||
class _Answer {
|
||||
const _Answer(this.records, [this.srvs]);
|
||||
|
||||
/// List of generic records.
|
||||
final List<_Record>? records;
|
||||
|
||||
/// List of Service (SRV) records.
|
||||
final List<SRVRecord>? srvs;
|
||||
|
||||
/// Constructs an [_Answer] instance from JSON data.
|
||||
///
|
||||
/// The [json] parameter should be a list of dynamic objects representing
|
||||
/// DNS records. Returns an [_Answer] instance containing parsed records
|
||||
/// and Service (SRV) records.
|
||||
factory _Answer.fromJson(List<dynamic>? json) {
|
||||
if (json == null) {
|
||||
return const _Answer(null);
|
||||
}
|
||||
|
||||
final records = json
|
||||
.map((answer) => _Record.fromJson(answer as Map<String, dynamic>))
|
||||
.toList();
|
||||
final srvs = <SRVRecord>[];
|
||||
|
||||
{
|
||||
final RegExp regExp = RegExp(r'(\d+)\s+(\d+)\s+(\d+)\s+([\w\.\-]+)');
|
||||
for (final record in records) {
|
||||
if (record.rType == RecordType.srv) {
|
||||
final match = regExp.firstMatch(record.data);
|
||||
|
||||
if (match != null) {
|
||||
final priority = int.parse(match.group(1)!);
|
||||
final weight = int.parse(match.group(2)!);
|
||||
final port = int.parse(match.group(3)!);
|
||||
final target = match.group(4)!;
|
||||
|
||||
srvs.add(
|
||||
SRVRecord(
|
||||
priority: priority,
|
||||
weight: weight,
|
||||
port: port,
|
||||
target: target,
|
||||
fqdn: record.name,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
throw const SRVRecordFormatException(
|
||||
'Failed to parse or process the Service (SRV) record',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return _Answer(records, srvs);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => '''$records''';
|
||||
}
|
||||
|
||||
class _Record {
|
||||
const _Record({
|
||||
required this.name,
|
||||
required this.rType,
|
||||
required this.ttl,
|
||||
required this.data,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final RecordType rType;
|
||||
final int ttl;
|
||||
final String data;
|
||||
|
||||
factory _Record.fromJson(Map<String, dynamic> json) => _Record(
|
||||
name: json['name'] as String,
|
||||
rType: DNSolve.intToRecord(json['type'] as int),
|
||||
ttl: json['TTL'] as int,
|
||||
data: json['data'] as String,
|
||||
);
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'''(name: $name, type: $rType, ttl: $ttl, data: $data)''';
|
||||
|
||||
String get toBind {
|
||||
final buffer = StringBuffer();
|
||||
buffer.write(name);
|
||||
if (buffer.length < 8) {
|
||||
buffer.write('\t');
|
||||
}
|
||||
if (buffer.length > 10) {
|
||||
buffer.write('\t');
|
||||
}
|
||||
buffer.writeAll(
|
||||
[ttl, '\tIN\t', rType.name.toUpperCase(), '\t', '"', data, '"'],
|
||||
);
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a Service (SRV) record containing information about a server or
|
||||
/// service in the domain name system (DNS).
|
||||
class SRVRecord {
|
||||
/// Constructs an [SRVRecord] with the specified parameters.
|
||||
const SRVRecord({
|
||||
required this.priority,
|
||||
required this.weight,
|
||||
required this.port,
|
||||
this.target,
|
||||
required this.fqdn,
|
||||
});
|
||||
|
||||
/// The priority of this SRV record.
|
||||
final int priority;
|
||||
|
||||
/// The weight of this SRV record.
|
||||
final int weight;
|
||||
|
||||
/// The port on which the service is available.
|
||||
final int port;
|
||||
|
||||
/// The target domain name of the server.
|
||||
final String? target;
|
||||
|
||||
/// Fully Qualified Domain Name.
|
||||
final String fqdn;
|
||||
|
||||
/// Sorts a list of [SRVRecord] instances based on their priority and weight.
|
||||
static List<SRVRecord> sort(List<SRVRecord> records) {
|
||||
records.sort(_srvRecordSortComparator);
|
||||
|
||||
return records;
|
||||
}
|
||||
|
||||
/// Comparator function for sorting [SRVRecord] instances.
|
||||
static int _srvRecordSortComparator(SRVRecord a, SRVRecord b) {
|
||||
if (a.priority < b.priority) {
|
||||
return -1;
|
||||
} else {
|
||||
if (a.priority > b.priority) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (a.weight < b.weight) {
|
||||
return -1;
|
||||
} else if (a.weight > b.weight) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return other is SRVRecord &&
|
||||
other.runtimeType == runtimeType &&
|
||||
other.priority == priority &&
|
||||
other.weight == weight &&
|
||||
other.port == port &&
|
||||
other.target == other.target;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(priority, weight, port, target);
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'exception.dart';
|
||||
|
||||
part '_answer.dart';
|
||||
part '_question.dart';
|
||||
part '_response.dart';
|
||||
|
||||
/// An enumeration that represents various DNS record types.
|
||||
enum RecordType {
|
||||
A,
|
||||
aaaa,
|
||||
any,
|
||||
caa,
|
||||
cds,
|
||||
cert,
|
||||
cname,
|
||||
dname,
|
||||
dnskey,
|
||||
ds,
|
||||
hinfo,
|
||||
ipseckey,
|
||||
nsec,
|
||||
nsec3PARAM,
|
||||
naptr,
|
||||
ptr,
|
||||
rp,
|
||||
rrsig,
|
||||
soa,
|
||||
spf,
|
||||
srv,
|
||||
sshfp,
|
||||
tlsa,
|
||||
wks,
|
||||
txt,
|
||||
ns,
|
||||
mx,
|
||||
}
|
||||
|
||||
/// An enumeration that represents different DNS service providers.
|
||||
enum DNSProvider { google, cloudflare, aliyun }
|
||||
|
||||
class DNSolve {
|
||||
DNSolve() {
|
||||
_client = http.Client();
|
||||
}
|
||||
|
||||
late final http.Client _client;
|
||||
|
||||
/// A map that associates [DNSProvider] enum values with their respective DNS
|
||||
/// provider URLs.
|
||||
static const _dnsProviders = <DNSProvider, String>{
|
||||
DNSProvider.google: 'https://dns.google.com/resolve',
|
||||
DNSProvider.cloudflare: 'https://cloudflare-dns.com/dns-query',
|
||||
DNSProvider.aliyun: 'https://dns.alidns.com/resolve'
|
||||
};
|
||||
|
||||
/// Performs a DNS lookup for the given domain.
|
||||
Future<ResolveResponse> lookup(
|
||||
/// The domain to lookup.
|
||||
String domain, {
|
||||
/// Whether to enable DNSSEC (Domain Name System Security Extensions).
|
||||
bool dnsSec = false,
|
||||
|
||||
/// The DNS record type to look up (defaults to A).
|
||||
RecordType type = RecordType.A,
|
||||
|
||||
/// The DNS provider to use (defaults to Google).
|
||||
DNSProvider provider = DNSProvider.google,
|
||||
}) async {
|
||||
assert(domain.isNotEmpty, 'domain should not be empty');
|
||||
|
||||
final queryParams = <String, String>{};
|
||||
queryParams
|
||||
..putIfAbsent('name', () => domain)
|
||||
..putIfAbsent('type', () => _typeToInt(type).toString())
|
||||
..putIfAbsent('dnssec', () => dnsSec.toString());
|
||||
|
||||
final headers = <String, String>{'Accept': 'application/dns-json'};
|
||||
final url = _dnsProviders[provider] ?? 'https://dns.google.com/resolve';
|
||||
|
||||
final body =
|
||||
await _get(url, queryParameters: queryParams, headers: headers);
|
||||
|
||||
return ResolveResponse.fromJson(json.decode(body) as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
/// Performs a reverse DNS lookup for the given IP address.
|
||||
Future<List<_Record>> reverseLookup(
|
||||
/// The IP address to perform a reverse lookup for.
|
||||
String ip, {
|
||||
/// THE DNS provider to use (defaults to Google).
|
||||
DNSProvider provider = DNSProvider.google,
|
||||
}) async {
|
||||
final queryParams = <String, String>{};
|
||||
String? reverse() {
|
||||
if (ip.contains('.')) {
|
||||
return '${ip.split('.').reversed.join('.')}.in-addr.arpa';
|
||||
} else if (ip.contains(':')) {
|
||||
return '${ip.split(':').join().split('').reversed.join('.')}.ip6.arpa';
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
final reversed = reverse();
|
||||
if (reversed == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
queryParams
|
||||
..putIfAbsent('name', () => reversed)
|
||||
..putIfAbsent('type', () => _records[RecordType.ptr]!.toString());
|
||||
|
||||
final headers = <String, String>{'Accept': 'application/dns-json'};
|
||||
final url = _dnsProviders[provider] ?? 'https://dns.google.com/resolve';
|
||||
|
||||
final body =
|
||||
await _get(url, queryParameters: queryParams, headers: headers);
|
||||
final response =
|
||||
ResolveResponse.fromJson(json.decode(body) as Map<String, dynamic>);
|
||||
return response.answer!.records ?? [];
|
||||
}
|
||||
|
||||
/// Sends an HTTP GET request to the specified URL with optional query
|
||||
/// parameters and headers.
|
||||
Future<String> _get(
|
||||
String url, {
|
||||
Map<String, String>? queryParameters,
|
||||
Map<String, String>? headers,
|
||||
}) async {
|
||||
late Uri uri;
|
||||
{
|
||||
if (queryParameters == null || queryParameters.isEmpty) {
|
||||
uri = Uri.parse(url);
|
||||
} else {
|
||||
uri = Uri.parse(url).replace(queryParameters: queryParameters);
|
||||
}
|
||||
}
|
||||
|
||||
final response = await _client.get(uri, headers: headers);
|
||||
return _handleResponse(response);
|
||||
}
|
||||
|
||||
/// A map that associates RecordType enum values with their corresponding DNS
|
||||
/// record types (integer values).
|
||||
static const _records = {
|
||||
RecordType.A: 1,
|
||||
RecordType.aaaa: 28,
|
||||
RecordType.any: 255,
|
||||
RecordType.caa: 257,
|
||||
RecordType.cds: 59,
|
||||
RecordType.cert: 37,
|
||||
RecordType.cname: 5,
|
||||
RecordType.dname: 39,
|
||||
RecordType.dnskey: 48,
|
||||
RecordType.ds: 43,
|
||||
RecordType.hinfo: 13,
|
||||
RecordType.ipseckey: 45,
|
||||
RecordType.mx: 15,
|
||||
RecordType.naptr: 35,
|
||||
RecordType.ns: 2,
|
||||
RecordType.nsec: 47,
|
||||
RecordType.nsec3PARAM: 51,
|
||||
RecordType.ptr: 12,
|
||||
RecordType.rp: 17,
|
||||
RecordType.rrsig: 46,
|
||||
RecordType.soa: 6,
|
||||
RecordType.spf: 99,
|
||||
RecordType.srv: 33,
|
||||
RecordType.sshfp: 44,
|
||||
RecordType.tlsa: 52,
|
||||
RecordType.txt: 16,
|
||||
RecordType.wks: 11,
|
||||
};
|
||||
|
||||
/// Converts an integer DNS record type to a [RecordType] enum value.
|
||||
static RecordType intToRecord(int type) {
|
||||
final records = _records.map((key, value) => MapEntry(value, key));
|
||||
|
||||
return records[type] ?? RecordType.A;
|
||||
}
|
||||
|
||||
/// Converts a [RecordType] enum value to its corresponding integer DNS record
|
||||
/// type.
|
||||
static int _typeToInt(RecordType type) => _records[type] ?? 1;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
part of '_dnsolve.dart';
|
||||
|
||||
class _Question {
|
||||
const _Question({required this.name, required this.rType});
|
||||
|
||||
final String? name;
|
||||
final RecordType? rType;
|
||||
|
||||
factory _Question.fromJson(Map<String, dynamic>? json) {
|
||||
if (json == null) {
|
||||
return const _Question(name: null, rType: null);
|
||||
}
|
||||
|
||||
return _Question(
|
||||
name: json['name'] as String,
|
||||
rType: DNSolve.intToRecord(json['type'] as int),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => '''(name: $name, rType: $rType)''';
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
part of '_dnsolve.dart';
|
||||
|
||||
String _handleResponse(http.Response response) {
|
||||
if (response.statusCode >= 200 && response.statusCode <= 209) {
|
||||
return response.body;
|
||||
} else {
|
||||
throw ResponseException(
|
||||
body: response.body,
|
||||
headers: response.headers,
|
||||
statusCode: response.statusCode,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a response from a DNS resolution operation.
|
||||
///
|
||||
/// This class includes information about the resolution status, flags,
|
||||
/// comments, resolved answer, and the list of questions queried.
|
||||
class ResolveResponse {
|
||||
const ResolveResponse({
|
||||
required this.status,
|
||||
required this.tc,
|
||||
required this.rd,
|
||||
required this.ra,
|
||||
required this.ad,
|
||||
required this.cd,
|
||||
required this.comment,
|
||||
required this.answer,
|
||||
required this.questions,
|
||||
});
|
||||
|
||||
/// The status code indicating the result of the DNS resolution.
|
||||
final int? status;
|
||||
|
||||
/// Indicates if the response was truncated.
|
||||
final bool? tc;
|
||||
|
||||
/// Indicates if recursion was desired in the request.
|
||||
final bool? rd;
|
||||
|
||||
/// Indicates if recursion is available in the response.
|
||||
final bool? ra;
|
||||
|
||||
/// Indicates if the data in the response is authenticated.
|
||||
final bool? ad;
|
||||
|
||||
/// Indicates if checking is disabled in the response.
|
||||
final bool? cd;
|
||||
|
||||
/// Additional comments or information related to the resolution response.
|
||||
final String? comment;
|
||||
|
||||
/// The resolved answer containing DNS records.
|
||||
final _Answer? answer;
|
||||
|
||||
/// List of questions queried in the resolution request.
|
||||
final List<_Question>? questions;
|
||||
|
||||
/// Constructs a [ResolveResponse] instance from JSON data.
|
||||
///
|
||||
/// The [json] parameter should be a map containing the fields of a DNS
|
||||
/// resolution response. Returns a [ResolveResponse] instance with parsed
|
||||
/// data.
|
||||
factory ResolveResponse.fromJson(Map<String, dynamic> json) => ResolveResponse(
|
||||
status: json['Status'] as int?,
|
||||
tc: json['TC'] as bool?,
|
||||
rd: json['RD'] as bool?,
|
||||
ra: json['RA'] as bool?,
|
||||
ad: json['AD'] as bool?,
|
||||
cd: json['CD'] as bool?,
|
||||
comment: json['comment'] as String?,
|
||||
answer: _Answer.fromJson(json['Answer'] as List<dynamic>?),
|
||||
questions: () {
|
||||
final data = json['Question'];
|
||||
if (data == null) return null;
|
||||
if (data is List) {
|
||||
return (data as List)
|
||||
.map(
|
||||
(question) => _Question.fromJson(question as Map<String, dynamic>),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
if (data is Map) return [_Question.fromJson(Map<String, dynamic>.from(data))];
|
||||
}(),
|
||||
);
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'''status: $status, truncation: $tc, recursion desired(rd): $rd, recursion available(ra): $ra, authenticated data(ad): $ad, checking disabled(cd): $cd, comment: $comment, answer: $answer, questions: $questions''';
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
/// Provider of an easy way of performing DNS lookups.
|
||||
library;
|
||||
|
||||
export '_dnsolve.dart';
|
||||
export 'exception.dart';
|
||||
@@ -0,0 +1,42 @@
|
||||
/// An abstract class representing an exception related to DNS solving.
|
||||
///
|
||||
/// This serves as a base class for exceptions that may occur during DNS
|
||||
/// resolution or parsing operations.
|
||||
abstract class DNSolveException implements Exception {
|
||||
const DNSolveException();
|
||||
}
|
||||
|
||||
/// Represents an [Exception] that occured while processing an DNS request.
|
||||
///
|
||||
/// It contains information about the status code, headers, and body of the
|
||||
/// response.
|
||||
class ResponseException extends DNSolveException {
|
||||
const ResponseException({
|
||||
required this.statusCode,
|
||||
required this.headers,
|
||||
required this.body,
|
||||
}) : super();
|
||||
|
||||
/// The status code of the response.
|
||||
final int statusCode;
|
||||
|
||||
/// The headers of the response.
|
||||
final Map<String, String> headers;
|
||||
|
||||
/// The body of the response.
|
||||
final String body;
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'''Exception(Status Code: $statusCode, Response Headers: $headers, Response Body: $body)''';
|
||||
}
|
||||
|
||||
/// An exception indicating that an error occurred while parsing or processing a
|
||||
/// Service (SRV) record.
|
||||
///
|
||||
/// This is a specific type of [DNSolveException].
|
||||
class SRVRecordFormatException extends DNSolveException {
|
||||
const SRVRecordFormatException(this.message);
|
||||
|
||||
final String message;
|
||||
}
|
||||
Reference in New Issue
Block a user