diff --git a/APIDOCS.md b/APIDOCS.md index 647eae1a..575542f2 100644 --- a/APIDOCS.md +++ b/APIDOCS.md @@ -4,6 +4,10 @@ Technitium DNS Server provides a HTTP API which is used by the web console to pe The URL in the documentation uses `localhost` and port `5380`. You should use the hostname/IP address and port that is specific to your DNS server instance. +## API Request + +Unless it is explicitly specified, all HTTP API requests can use both `GET` or `POST` methods. When using `POST` method to pass the API parameters as form data, the `Content-Type` header must be set to `application/x-www-form-urlencoded`. When the HTTP API call is used to upload files, the call must use `POST` method and the `Content-Type` header must be set to `multipart/form-data`. + ## API Response Format The HTTP API returns a JSON formatted response for all requests. The JSON object returned contains `status` property which indicate if the request was successful. @@ -63,7 +67,7 @@ None WHERE: - `user`: The username for the user account. The built-in administrator username on the DNS server is `admin`. - `pass`: The password for the user account. The default password for `admin` user is `admin`. -- `includeInfo`: Includes basic info relevant for the user in response. +- `includeInfo` (optional): Includes basic info relevant for the user in response. WARNING: It is highly recommended to change the password on first use to avoid security related issues. @@ -147,7 +151,7 @@ WHERE: Allows creating a non-expiring API token that can be used with automation scripts to make API calls. The token allows access to API calls with the same privileges as that of the user account. Thus its recommended to create a separate user account with limited permissions as required by the specific task that the token will be used for. The token cannot be used to change the user's password, or update the user profile details. URL:\ -`http://localhost:5380/api/user/createToken?user=admin&pass=admin&tokenName=MyToken1&includeInfo=true` +`http://localhost:5380/api/user/createToken?user=admin&pass=admin&tokenName=MyToken1` PERMISSIONS:\ None @@ -1540,7 +1544,7 @@ These API calls allow managing all hosted zones on the DNS server. List all authoritative zones hosted on this DNS server. The list contains only the zones that the user has View permissions for. These API calls requires permission for both the Zones section as well as the individual permission for each zone. URL:\ -`http://localhost:5380/api/zones/list?token=x` +`http://localhost:5380/api/zones/list?token=x&pageNumber=1&zonesPerPage=10` OBSOLETE PATH:\ `/api/zone/list`\ @@ -1552,11 +1556,16 @@ Zone: View WHERE: - `token`: The session token generated by the `login` or the `createToken` call. +- `pageNumber` (optional): When this parameter is specified, the API will return paginated results based on the page number and zones per pages options. When not specified, the API will return a list of all zones. +- `zonesPerPage` (optional): The number of zones per page to be returned. This option is only used when `pageNumber` options is specified. The default value is `10` when not specified. RESPONSE: ``` { "response": { + "pageNumber": 1, + "totalPages": 2, + "totalZones": 12, "zones": [ { "name": "", @@ -1609,6 +1618,30 @@ RESPONSE: "internal": true, "dnssecStatus": "Unsigned", "disabled": false + }, + { + "name": "test0.com", + "type": "Primary", + "internal": false, + "dnssecStatus": "Unsigned", + "notifyFailed": false, + "disabled": false + }, + { + "name": "test1.com", + "type": "Primary", + "internal": false, + "dnssecStatus": "Unsigned", + "notifyFailed": false, + "disabled": false + }, + { + "name": "test2.com", + "type": "Primary", + "internal": false, + "dnssecStatus": "Unsigned", + "notifyFailed": false, + "disabled": false } ] }, @@ -1635,9 +1668,9 @@ WHERE: - `zone`: The domain name for creating the new zone. The value can be valid domain name, an IP address, or an network address in CIDR format. When value is IP address or network address, a reverse zone is created. - `type`: The type of zone to be created. Valid values are [`Primary`, `Secondary`, `Stub`, `Forwarder`]. - `primaryNameServerAddresses` (optional): List of comma separated IP addresses of the primary name server. This optional parameter is used only with Secondary and Stub zones. If this parameter is not used, the DNS server will try to recursively resolve the primary name server addresses automatically. -- `zoneTransferProtocol` (optional): The zone transfer protocol to be used by secondary zones. Valid values are [`Tcp`, `Tls`]. +- `zoneTransferProtocol` (optional): The zone transfer protocol to be used by secondary zones. Valid values are [`Tcp`, `Tls`, `Quic`]. - `tsigKeyName` (optional): The TSIG key name to be used by secondary zones. -- `protocol` (optional): The DNS transport protocol to be used by the conditional forwarder zone. This optional parameter is used with Conditional Forwarder zones. Valid values are [`Udp`, `Tcp`, `Tls`, `Https`]. Default `Udp` protocol is used when this parameter is missing. +- `protocol` (optional): The DNS transport protocol to be used by the conditional forwarder zone. This optional parameter is used with Conditional Forwarder zones. Valid values are [`Udp`, `Tcp`, `Tls`, `Https`, `Quic`]. Default `Udp` protocol is used when this parameter is missing. - `forwarder` (optional): The address of the DNS server to be used as a forwarder. This optional parameter is required to be used with Conditional Forwarder zones. A special value `this-server` can be used as a forwarder which when used will forward all the requests internally to this DNS server such that you can override the zone with records and rest of the zone gets resolved via This Server. - `dnssecValidation` (optional): Set this boolean value to indicate if DNSSEC validation must be done. This optional parameter is required to be used with Conditional Forwarder zones. - `proxyType` (optional): The type of proxy that must be used for conditional forwarding. This optional parameter is required to be used with Conditional Forwarder zones. Valid values are [`None`, `Http`, `Socks5`]. Default value `None` is used when this parameter is missing. @@ -1992,7 +2025,7 @@ WHERE: - `zskKeySize` (optional): The size of the Zone Signing Key (ZSK) in bits to be used when using `RSA` algorithm. This optional parameter is required when using `RSA` algorithm. - `curve` (optional): The name of the curve to be used when using `ECDSA` algorithm. Valid values are [`P256`, `P384`]. This optional parameter is required when using `ECDSA` algorithm. - `dnsKeyTtl` (optional): The TTL value to be used for DNSKEY records. Default value is `86400` when not specified. -- `zskRolloverDays` (optional): The frequency in days that the DNS server must automatically rollover the Zone Signing Keys (ZSK) in the zone. Valid range is 0-365 days where 0 disables rollover. Default value is `90` days. +- `zskRolloverDays` (optional): The frequency in days that the DNS server must automatically rollover the Zone Signing Keys (ZSK) in the zone. Valid range is 0-365 days where 0 disables rollover. Default value is `90` when not specified. - `nxProof` (optional): The type of proof of non-existence that must be used for signing the zone. Valid values are [`NSEC`, `NSEC3`]. Default value is `NSEC` when not specified. - `iterations` (optional): The number of iterations to use for hashing in NSEC3. This optional parameter is only applicable when using `NSEC3` as the `nxProof`. Default value is `0` when not specified. - `saltLength` (optional): The length of salt in bytes to use for hashing in NSEC3. This optional parameter is only applicable when using `NSEC3` as the `nxProof`. Default value is `0` when not specified. @@ -2056,23 +2089,24 @@ RESPONSE: "internal": false, "disabled": false, "dnssecStatus": "SignedWithNSEC", - "dnsKeyTtl": 86400, + "dnsKeyTtl": 3600, "dnssecPrivateKeys": [ { - "keyTag": 19198, + "keyTag": 15048, "keyType": "KeySigningKey", "algorithm": "ECDSAP256SHA256", - "state": "Ready", - "stateChangedOn": "2022-02-19T06:53:21Z", + "state": "Published", + "stateChangedOn": "2022-12-18T14:39:50.0328321Z", + "stateReadyBy": "2022-12-18T16:14:50.0328321Z", "isRetiring": false, "rolloverDays": 0 }, { - "keyTag": 50617, + "keyTag": 46152, "keyType": "ZoneSigningKey", "algorithm": "ECDSAP256SHA256", "state": "Active", - "stateChangedOn": "2022-02-19T06:53:21Z", + "stateChangedOn": "2022-12-18T14:39:50.0661173Z", "isRetiring": false, "rolloverDays": 90 } @@ -2398,7 +2432,7 @@ WHERE: - `tag` (optional): This parameter is required for adding the `CAA` record. - `value` (optional): This parameter is required for adding the `CAA` record. - `aname` (optional): The ANAME domain name. This option is required for adding `ANAME` record. -- `protocol` (optional): This parameter is required for adding the `FWD` record. Valid values are [`Udp`, `Tcp`, `Tls`, `Https`, `HttpsJson`]. +- `protocol` (optional): This parameter is required for adding the `FWD` record. Valid values are [`Udp`, `Tcp`, `Tls`, `Https`, `Quic`]. - `forwarder` (optional): The forwarder address. A special value of `this-server` can be used to directly forward requests internally to the DNS server. This parameter is required for adding the `FWD` record. - `dnssecValidation` (optional): Set this boolean value to indicate if DNSSEC validation must be done. This optional parameter is to be used with FWD records. Default value is `false`. - `proxyType` (optional): The type of proxy that must be used for conditional forwarding. This optional parameter is to be used with FWD records. Valid values are [`None`, `Http`, `Socks5`]. Default value `None` is used when this parameter is missing. @@ -2442,7 +2476,7 @@ RESPONSE: Gets all records for a given authoritative zone. URL:\ -`http://localhost:5380/api/zones/records/get?token=x&domain=example.com&zone=example.com` +`http://localhost:5380/api/zones/records/get?token=x&domain=example.com&zone=example.com&listZone=true` OBSOLETE PATH:\ `/api/zone/getRecords`\ @@ -2456,6 +2490,7 @@ WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `domain`: The domain name of the zone to get records. - `zone` (optional): The name of the authoritative zone into which the `domain` exists. When unspecified, the closest authoritative zone will be used. +- `listZone` (optional): When set to `true` will list all records in the zone else will list records only for the given domain name. Default value is `false` when not specified. RESPONSE: ``` @@ -2626,10 +2661,6 @@ RESPONSE: "computedKeyTag": 65078, "dnsKeyState": "Ready", "computedDigests": [ - { - "digestType": "SHA1", - "digest": "3C2B05EBC20B77D8BC56EB9FFB36A6A1F07983F9" - }, { "digestType": "SHA256", "digest": "BBE017B17E5CB5FFFF1EC2C7815367DF80D8E7EAEE4832D3ED192159D79B1EEB" @@ -2661,30 +2692,28 @@ RESPONSE: "disabled": false, "name": "example.com", "type": "DNSKEY", - "ttl": 86400, + "ttl": 3600, "rData": { "flags": "SecureEntryPoint, ZoneKey", "protocol": 3, "algorithm": "ECDSAP256SHA256", - "publicKey": "KtXitZeC9ijbghCwQ5kjBfgLxCa0pBOOBGftudxGv/I= hlRGy7/Plea39T8n78xiHPaspYrTcdyidbKz6Z+ZSGw=", - "computedKeyTag": 52896, + "publicKey": "KOJWFitKm58EgjO43GDnsFbnkGoqVKeLRkP8FGPAdhqA2F758Ta1mkxieEu0YN0EoX+u5bVuc5DEBFSv+U63CA==", + "computedKeyTag": 15048, "dnsKeyState": "Published", + "dnsKeyStateReadyBy": "2022-12-18T16:14:50.0328321Z", "computedDigests": [ - { - "digestType": "SHA1", - "digest": "767EA31AD77C6AC2ACEB22B3FADB033679A6FB79" - }, { "digestType": "SHA256", - "digest": "BDBDB532C5809F890F8092DC9702D763D51A7318887B195CB52E888882FBE373" + "digest": "8EAFAE3305DB57A27CA5A261525515461CB7232A34A44AD96441B88BCA9B9849" }, { "digestType": "SHA384", - "digest": "A7EA4C7816ED5F011FDF90015D4A37BC7D1C773C22C3440B57D6717FA4ED71C5B95D09592AF48BD3ED59028D214A367E" + "digest": "4A6DA59E91872B5B835FCEE5987B17151A6F10FE409B595BEEEDB28FE64315C9C268493B59A0BF72EA84BE0F20A33F96" } ] }, - "dnssecStatus": "Unknown" + "dnssecStatus": "Unknown", + "lastUsedOn": "0001-01-01T00:00:00" }, { "disabled": false, @@ -3019,7 +3048,7 @@ WHERE: - `expire` (optional): This is the expire parameter in the SOA record. This parameter is required when updating the SOA record. - `minimum` (optional): This is the minimum parameter in the SOA record. This parameter is required when updating the SOA record. - `primaryAddresses` (optional): This is a comma separated list of IP addresses of the primary name server. This parameter is to be used with secondary and stub zones where the primary name server address is not directly resolvable. -- `zoneTransferProtocol` (optional): The zone transfer protocol to be used by the secondary zone. Valid values are [`Tcp`, `Tls`]. This parameter is used with `SOA` record. +- `zoneTransferProtocol` (optional): The zone transfer protocol to be used by the secondary zone. Valid values are [`Tcp`, `Tls`, `Quic`]. This parameter is used with `SOA` record. - `tsigKeyName` (optional): The TSIG key name to be used by the secondary zone. This parameter is used with `SOA` record. - `ptrName`(optional): The current PTR domain name. This option is required for updating `PTR` record. - `newPtrName`(optional): The new PTR domain name. This option is required for updating `PTR` record. @@ -3068,8 +3097,8 @@ WHERE: - `newValue` (optional): The new value in CAA record. This parameter is required when updating the `CAA` record. - `aname` (optional): The current ANAME domain name. This parameter is required when updating the `ANAME` record. - `newAName` (optional): The new ANAME domain name. This parameter is required when updating the `ANAME` record. -- `protocol` (optional): This is the current protocol value in the FWD record. Valid values are [`Udp`, `Tcp`, `Tls`, `Https`, `HttpsJson`]. This parameter is optional and default value `Udp` will be used when updating the `FWD` record. -- `newProtocol` (optional): This is the new protocol value in the FWD record. Valid values are [`Udp`, `Tcp`, `Tls`, `Https`, `HttpsJson`]. This parameter is optional and default value `Udp` will be used when updating the `FWD` record. +- `protocol` (optional): This is the current protocol value in the FWD record. Valid values are [`Udp`, `Tcp`, `Tls`, `Https`, `Quic`]. This parameter is optional and default value `Udp` will be used when updating the `FWD` record. +- `newProtocol` (optional): This is the new protocol value in the FWD record. Valid values are [`Udp`, `Tcp`, `Tls`, `Https`, `Quic`]. This parameter is optional and default value `Udp` will be used when updating the `FWD` record. - `forwarder` (optional): The current forwarder address. This parameter is required when updating the `FWD` record. - `newForwarder` (optional): The new forwarder address. This parameter is required when updating the `FWD` record. - `dnssecValidation` (optional): Set this boolean value to indicate if DNSSEC validation must be done. This optional parameter is to be used with FWD records. Default value is `false`. @@ -3160,7 +3189,7 @@ WHERE: - `tag` (optional): This is the tag parameter in the CAA record. This parameter is required when deleting the `CAA` record. - `value` (optional): This parameter is required when deleting the `CAA` record. - `aname` (optional): This parameter is required when deleting the `ANAME` record. -- `protocol` (optional): This is the protocol parameter in the FWD record. Valid values are [`Udp`, `Tcp`, `Tls`, `Https`, `HttpsJson`]. This parameter is optional and default value `Udp` will be used when deleting the `FWD` record. +- `protocol` (optional): This is the protocol parameter in the FWD record. Valid values are [`Udp`, `Tcp`, `Tls`, `Https`, `Quic`]. This parameter is optional and default value `Udp` will be used when deleting the `FWD` record. - `forwarder` (optional): This parameter is required when deleting the `FWD` record. RESPONSE: @@ -4003,7 +4032,7 @@ WHERE: - `server`: The name server to query using the DNS client. - `domain`: The domain name to query. - `type`: The type of the query. -- `protocol` (optional): The DNS transport protocol to be used to query. Valid values are [`Udp`, `Tcp`, `Tls`, `Https`, `HttpsJson`]. The default value of `Udp` is used when the parameter is missing. +- `protocol` (optional): The DNS transport protocol to be used to query. Valid values are [`Udp`, `Tcp`, `Tls`, `Https`, `Quic`]. The default value of `Udp` is used when the parameter is missing. - `dnssec` (optional): Set to `true` to enable DNSSEC validation. - `import` (optional): This parameter when set to `true` indicates that the response of the DNS query should be imported in the an authoritative zone on this DNS server. Default value is `false` when this parameter is missing. If a zone does not exists, a primary zone for the `domain` name is created and the records from the response are set into the zone. Import can be done only for primary and forwarder type of zones. When `type` is set to AXFR, then the import feature will work as if a zone transfer was requested and the complete zone will be updated as per the zone transfer response. Note that any existing record type for the given `type` will be overwritten when syncing the records. It is recommended to use `recursive-resolver` or the actual name server address for the `server` parameter when importing records. You must have Zones Modify permission to create a zone or Zone Modify permission to import records into an existing zone. @@ -4084,29 +4113,12 @@ RESPONSE: ``` { "response": { - "version": "10.0", + "version": "11.0", "dnsServerDomain": "server1", "dnsServerLocalEndPoints": [ "0.0.0.0:53", "[::]:53" ], - "webServiceLocalAddresses": [ - "0.0.0.0", - "[::]" - ], - "webServiceHttpPort": 5380, - "webServiceEnableTls": false, - "webServiceHttpToTlsRedirect": false, - "webServiceTlsPort": 53443, - "webServiceUseSelfSignedTlsCertificate": false, - "webServiceTlsCertificatePath": "", - "webServiceTlsCertificatePassword": "************", - "enableDnsOverHttp": false, - "enableDnsOverTls": false, - "enableDnsOverHttps": false, - "dnsTlsCertificatePath": null, - "dnsTlsCertificatePassword": "************", - "tsigKeys": [], "defaultRecordTtl": 3600, "dnsAppsEnableAutomaticUpdate": true, "preferIPv6": false, @@ -4115,32 +4127,48 @@ RESPONSE: "eDnsClientSubnet": false, "eDnsClientSubnetIPv4PrefixLength": 24, "eDnsClientSubnetIPv6PrefixLength": 56, - "resolverRetries": 2, - "resolverTimeout": 2000, - "resolverMaxStackCount": 16, - "forwarderRetries": 3, - "forwarderTimeout": 2000, - "forwarderConcurrency": 2, + "qpmLimitRequests": 0, + "qpmLimitErrors": 0, + "qpmLimitSampleMinutes": 5, + "qpmLimitIPv4PrefixLength": 24, + "qpmLimitIPv6PrefixLength": 56, "clientTimeout": 4000, "tcpSendTimeout": 10000, "tcpReceiveTimeout": 10000, - "enableLogging": true, - "logQueries": false, - "useLocalTime": false, - "logFolder": "logs", - "maxLogFileDays": 0, - "maxStatFileDays": 0, + "quicIdleTimeout": 60000, + "quicMaxInboundStreams": 100, + "listenBacklog": 100, + "webServiceLocalAddresses": [ + "[::]" + ], + "webServiceHttpPort": 5380, + "webServiceEnableTls": false, + "webServiceHttpToTlsRedirect": false, + "webServiceUseSelfSignedTlsCertificate": false, + "webServiceTlsPort": 53443, + "webServiceTlsCertificatePath": null, + "webServiceTlsCertificatePassword": "************", + "enableDnsOverHttp": true, + "enableDnsOverTls": true, + "enableDnsOverHttps": true, + "enableDnsOverQuic": false, + "dnsOverHttpPort": 8053, + "dnsOverTlsPort": 853, + "dnsOverHttpsPort": 443, + "dnsOverQuicPort": 853, + "dnsTlsCertificatePath": "z:\\ns2.technitium.com.pfx", + "dnsTlsCertificatePassword": "************", + "tsigKeys": [], "recursion": "AllowOnlyForPrivateNetworks", "recursionDeniedNetworks": [], "recursionAllowedNetworks": [], "randomizeName": true, "qnameMinimization": true, "nsRevalidation": true, - "qpmLimitRequests": 0, - "qpmLimitErrors": 0, - "qpmLimitSampleMinutes": 5, - "qpmLimitIPv4PrefixLength": 24, - "qpmLimitIPv6PrefixLength": 56, + "resolverRetries": 2, + "resolverTimeout": 2000, + "resolverMaxStackCount": 16, + "saveCache": false, "serveStale": true, "serveStaleTtl": 259200, "cacheMaximumEntries": 10000, @@ -4152,15 +4180,24 @@ RESPONSE: "cachePrefetchTrigger": 9, "cachePrefetchSampleIntervalInMinutes": 5, "cachePrefetchSampleEligibilityHitsPerHour": 30, - "proxy": null, - "forwarders": null, - "forwarderProtocol": "Udp", "enableBlocking": true, "allowTxtBlockingReport": true, "blockingType": "AnyAddress", "customBlockingAddresses": [], "blockListUrls": null, - "blockListUpdateIntervalHours": 24 + "blockListUpdateIntervalHours": 24, + "proxy": null, + "forwarders": null, + "forwarderProtocol": "Udp", + "forwarderRetries": 3, + "forwarderTimeout": 2000, + "forwarderConcurrency": 2, + "enableLogging": true, + "logQueries": false, + "useLocalTime": false, + "logFolder": "logs", + "maxLogFileDays": 0, + "maxStatFileDays": 0 }, "status": "ok" } @@ -4183,64 +4220,69 @@ WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `dnsServerDomain` (optional): The primary domain name used by this DNS Server to identify itself. - `dnsServerLocalEndPoints` (optional): Local end points are the network interface IP addresses and ports you want the DNS Server to listen for requests. +- `defaultRecordTtl` (optional): The default TTL value to use if not specified when adding or updating records in a Zone. +- `dnsAppsEnableAutomaticUpdate` (optional): Set to `true` to allow DNS server to automatically update the DNS Apps from the DNS App Store. The DNS Server will check for updates every 24 hrs when this option is enabled. +- `preferIPv6` (optional): DNS Server will use IPv6 for querying whenever possible with this option enabled. Initial value is `false`. +- `udpPayloadSize` (optional): The maximum EDNS UDP payload size that can be used to avoid IP fragmentation. Valid range is 512-4096 bytes. Initial value is `1232`. +- `dnssecValidation` (optional): Set this to `true` to enable DNSSEC validation. DNS Server will validate all responses from name servers or forwarders when this option is enabled. +- `eDnsClientSubnet` (optional): Set this to `true` to enable EDNS Client Subnet. DNS Server will use the public IP address of the request with a prefix length, or the existing Client Subnet option from the request while resolving requests. +- `eDnsClientSubnetIPv4PrefixLength` (optional): The EDNS Client Subnet IPv4 prefix length to define the client subnet. Initial value is `24`. +- `eDnsClientSubnetIPv6PrefixLength` (optional): The EDNS Client Subnet IPv6 prefix length to define the client subnet. Initial value is `56`. +- `qpmLimitRequests` (optional): Sets the Queries Per Minute (QPM) limit on total number of requests that is enforces per client subnet. Set value to `0` to disable the feature. +- `qpmLimitErrors` (optional): Sets the Queries Per Minute (QPM) limit on total number of requests which generates an error response that is enforces per client subnet. Set value to `0` to disable the feature. Response with an RCODE of FormatError, ServerFailure, or Refused is considered as an error response. +- `qpmLimitSampleMinutes` (optional): Sets the client query stats sample size in minutes for QPM limit feature. Initial value is `5`. +- `qpmLimitIPv4PrefixLength` (optional): Sets the client subnet IPv4 prefix length used to define the subnet. Initial value is `24`. +- `qpmLimitIPv6PrefixLength` (optional): Sets the client subnet IPv6 prefix length used to define the subnet. Initial value is `56`. +- `clientTimeout` (optional): The amount of time the DNS server must wait in milliseconds before responding with a ServerFailure response to a client request when no answer is available. Valid range is `1000`-`10000`. Initial value is `4000`. +- `tcpSendTimeout` (optional): The amount of time in milliseconds a TCP socket must wait for an ACK before closing the connection. This option will apply for DNS requests being received by the DNS Server over TCP, TLS, or HTTPS transports. Valid range is `1000`-`90000`. Initial value is `10000`. +- `tcpReceiveTimeout` (optional): The amount of time in milliseconds a TCP socket must wait for data before closing the connection. This option will apply for DNS requests being received by the DNS Server over TCP, TLS, or HTTPS transports. Valid range is `1000`-`90000`. Initial value is `10000`. +- `quicIdleTimeout` (optional): The time interval in milliseconds after which an idle QUIC connection will be closed. This option applies only to QUIC transport protocol. Valid range is `1000`-`90000`. Initial value is `60000`. +- `quicMaxInboundStreams` (optional): The max number of inbound bidirectional streams that can be accepted per QUIC connection. This option applies only to QUIC transport protocol. Valid range is `1`-`1000`. Initial value is `100`. +- `listenBacklog` (optional): The maximum number of pending connections. This option applies to TCP, TLS, and QUIC transport protocols. Initial value is `100`. - `webServiceLocalAddresses` (optional): Local addresses are the network interface IP addresses you want the web service to listen for requests. -- `webServiceHttpPort` (optional): Specify the TCP port number for the web console and this API web service. Default value is `5380`. -- `webServiceEnableTls` (optional): Set this to `true` to start the HTTPS service to acccess web service. +- `webServiceHttpPort` (optional): Specify the TCP port number for the web console and this API web service. Initial value is `5380`. +- `webServiceEnableTls` (optional): Set this to `true` to start the HTTPS service to access web service. - `webServiceTlsPort` (optional): Specified the TCP port number for the web console for HTTPS access. - `webServiceUseSelfSignedTlsCertificate` (optional): Set `true` for the web service to use an automatically generated self signed certificate when TLS certificate path is not specified. - `webServiceTlsCertificatePath` (optional): Specify a PKCS #12 certificate (.pfx) file path on the server. The certificate must contain private key. This certificate is used by the web console for HTTPS access. - `webServiceTlsCertificatePassword` (optional): Enter the certificate (.pfx) password, if any. -- `enableDnsOverHttp` (optional): Enable this option to accept DNS-over-HTTP requests for both wire and json response formats. It must be used with a TLS terminating reverse proxy like nginx and will work only on private networks. +- `enableDnsOverHttp` (optional): Enable this option to accept DNS-over-HTTP requests. It must be used with a TLS terminating reverse proxy like nginx and will work only on private networks. Enabling this option also allows automatic TLS certificate renewal with HTTP challenge (webroot) for DNS-over-HTTPS service. - `enableDnsOverTls` (optional): Enable this option to accept DNS-over-TLS requests. -- `enableDnsOverHttps` (optional): Enable this option to accept DNS-over-HTTPS requests for both wire and json response formats. +- `enableDnsOverHttps` (optional): Enable this option to accept DNS-over-HTTPS requests. +- `enableDnsOverQuic` (optional): Enable this option to accept DNS-over-QUIC requests. +- `dnsOverHttpPort` (optional): The TCP port number for DNS-over-HTTP protocol. Initial value is `80`. +- `dnsOverTlsPort` (optional): The TCP port number for DNS-over-TLS protocol. Initial value is `853`. +- `dnsOverHttpsPort` (optional): The TCP port number for DNS-over-HTTPS protocol. Initial value is `443`. +- `dnsOverQuicPort` (optional): The UDP port number for DNS-over-QUIC protocol. Initial value is `853`. - `dnsTlsCertificatePath` (optional): Specify a PKCS #12 certificate (.pfx) file path on the server. The certificate must contain private key. This certificate is used by the DNS-over-TLS and DNS-over-HTTPS optional protocols. - `dnsTlsCertificatePassword` (optional): Enter the certificate (.pfx) password, if any. - `tsigKeys` (optional): A pipe `|` separated multi row list of TSIG key name, shared secret, and algorithm. Set this parameter to `false` to remove all existing keys. Supported algorithms are [`hmac-md5.sig-alg.reg.int`, `hmac-sha1`, `hmac-sha256`, `hmac-sha256-128`, `hmac-sha384`, `hmac-sha384-192`, `hmac-sha512`, `hmac-sha512-256`]. -- `defaultRecordTtl` (optional): The default TTL value to use if not specified when adding or updating records in a Zone. -- `dnsAppsEnableAutomaticUpdate` (optional): Set to `true` to allow DNS server to automatically update the DNS Apps from the DNS App Store. The DNS Server will check for updates every 24 hrs when this option is enabled. -- `preferIPv6` (optional): DNS Server will use IPv6 for querying whenever possible with this option enabled. Default value is `false`. -- `udpPayloadSize` (optional): The maximum EDNS UDP payload size that can be used to avoid IP fragmentation. Valid range is 512-4096 bytes. Default value is `1232`. -- `dnssecValidation` (optional): Set this to `true` to enable DNSSEC validation. DNS Server will validate all responses from name servers or forwarders when this option is enabled. -- `eDnsClientSubnet` (optional): Set this to `true` to enable EDNS Client Subnet. DNS Server will use the public IP address of the request with a prefix length, or the existing Client Subnet option from the request while resolving requests. -- `eDnsClientSubnetIPv4PrefixLength` (optional): The EDNS Client Subnet IPv4 prefix length to define the client subnet. Default value is `24`. -- `eDnsClientSubnetIPv6PrefixLength` (optional): The EDNS Client Subnet IPv6 prefix length to define the client subnet. Default value is `56`. -- `resolverRetries` (optional): The number of retries that the recursive resolver must do. -- `resolverTimeout` (optional): The timeout value in milliseconds for the recursive resolver. -- `resolverMaxStackCount` (optional): The max stack count that the recursive resolver must use. -- `forwarderRetries` (optional): The number of retries that the forwarder DNS client must do. -- `forwarderTimeout` (optional): The timeout value in milliseconds for the forwarder DNS client. -- `forwarderConcurrency` (optional): The number of concurrent requests that the forwarder DNS client should do. -- `clientTimeout` (optional): The amount of time the DNS server must wait in milliseconds before responding with a ServerFailure response to a client request when no answer is available. -- `tcpSendTimeout` (optional): The amount of time in milliseconds a TCP socket must wait for an ACK before closing the connection. This option will apply for DNS requests being received by the DNS Server over TCP, TLS, or HTTPS transports. -- `tcpReceiveTimeout` (optional): The amount of time in milliseconds a TCP socket must wait for data before closing the connection. This option will apply for DNS requests being received by the DNS Server over TCP, TLS, or HTTPS transports. -- `enableLogging` (optional): Enable this option to log error and audit logs into the log file. Default value is `true`. -- `logQueries` (optional): Enable this option to log every query received by this DNS Server and the corresponding response answers into the log file. Default value is `false`. -- `useLocalTime` (optional): Enable this option to use local time instead of UTC for logging. Default value is `false`. -- `logFolder` (optional): The folder path on the server where the log files should be saved. The path can be relative to the DNS server config folder. Default value is `logs`. -- `maxLogFileDays` (optional): Max number of days to keep the log files. Log files older than the specified number of days will be deleted automatically. Recommended value is `365`. Set `0` to disable auto delete. -- `maxStatFileDays` (optional): Max number of days to keep the dashboard stats. Stat files older than the specified number of days will be deleted automatically. Recommended value is `365`. Set `0` to disable auto delete. - `recursion` (optional): Sets the recursion policy for the DNS server. Valid values are [`Deny`, `Allow`, `AllowOnlyForPrivateNetworks`, `UseSpecifiedNetworks`]. - `recursionDeniedNetworks` (optional): A comma separated list of network addresses in CIDR format that must be denied recursion. Set this parameter to `false` to remove existing values. These values are only used when `recursion` is set to `UseSpecifiedNetworks`. - `recursionAllowedNetworks` (optional): A comma separated list of network addresses in CIDR format that must be allowed recursion. Set this parameter to `false` to remove existing values. These values are only used when `recursion` is set to `UseSpecifiedNetworks`. -- `randomizeName` (optional): Enables QNAME randomization [draft-vixie-dnsext-dns0x20-00](https://tools.ietf.org/html/draft-vixie-dnsext-dns0x20-00) when using UDP as the transport protocol. Default value is `true`. -- `qnameMinimization` (optional): Enables QNAME minimization [draft-ietf-dnsop-rfc7816bis-04](https://tools.ietf.org/html/draft-ietf-dnsop-rfc7816bis-04) when doing recursive resolution. Default value is `true`. -- `nsRevalidation` (optional): Enables [draft-ietf-dnsop-ns-revalidation](https://datatracker.ietf.org/doc/draft-ietf-dnsop-ns-revalidation/) for recursive resolution. Default value is `true`. -- `qpmLimitRequests` (optional): Sets the Queries Per Minute (QPM) limit on total number of requests that is enforces per client subnet. Set value to `0` to disable the feature. -- `qpmLimitErrors` (optional): Sets the Queries Per Minute (QPM) limit on total number of requests which generates an error response that is enforces per client subnet. Set value to `0` to disable the feature. Response with an RCODE of FormatError, ServerFailure, or Refused is considered as an error response. -- `qpmLimitSampleMinutes` (optional): Sets the client query stats sample size in minutes for QPM limit feature. Default value is `5`. -- `qpmLimitIPv4PrefixLength` (optional): Sets the client subnet IPv4 prefix length used to define the subnet. Default value is `24`. -- `qpmLimitIPv6PrefixLength` (optional): Sets the client subnet IPv6 prefix length used to define the subnet. Default value is `56`. -- `serveStale` (optional): Enable the serve stale feature to improve resiliency by using expired or stale records in cache when the DNS server is unable to reach the upstream or authoritative name servers. Default value is `true`. -- `serveStaleTtl` (optional): The TTL value in seconds which should be used for cached records that are expired. When the serve stale TTL too expires for a stale record, it gets removed from the cache. Recommended value is between 1-3 days and maximum supported value is 7 days. Default value is `259200`. -- `temporaryDisableBlockingTill` (read only): An ISO 8601 String with the Date and Time when the Temporary Blocking will end. -- `cacheMinimumRecordTtl` (optional): The minimum TTL value that a record can have in cache. Set a value to make sure that the records with TTL value than it stays in cache for a minimum duration. Default value is `10`. -- `cacheMaximumRecordTtl` (optional): The maximum TTL value that a record can have in cache. Set a lower value to allow the records to expire early. Default value is `86400`. -- `cacheNegativeRecordTtl` (optional): The negative TTL value to use when there is no SOA MINIMUM value available. Default value is `300`. -- `cacheFailureRecordTtl` (optional): The failure TTL value to used for caching failure responses. This allows storing failure record in cache and prevent frequent recursive resolution to name servers that are responding with `ServerFailure`. Default value is `60`. +- `randomizeName` (optional): Enables QNAME randomization [draft-vixie-dnsext-dns0x20-00](https://tools.ietf.org/html/draft-vixie-dnsext-dns0x20-00) when using UDP as the transport protocol. Initial value is `true`. +- `qnameMinimization` (optional): Enables QNAME minimization [draft-ietf-dnsop-rfc7816bis-04](https://tools.ietf.org/html/draft-ietf-dnsop-rfc7816bis-04) when doing recursive resolution. Initial value is `true`. +- `nsRevalidation` (optional): Enables [draft-ietf-dnsop-ns-revalidation](https://datatracker.ietf.org/doc/draft-ietf-dnsop-ns-revalidation/) for recursive resolution. Initial value is `true`. +- `resolverRetries` (optional): The number of retries that the recursive resolver must do. +- `resolverTimeout` (optional): The timeout value in milliseconds for the recursive resolver. +- `resolverMaxStackCount` (optional): The max stack count that the recursive resolver must use. +- `saveCache` (optional): Enable this option to save DNS cache on disk when the DNS server stops. The saved cache will be loaded next time the DNS server starts. +- `serveStale` (optional): Enable the serve stale feature to improve resiliency by using expired or stale records in cache when the DNS server is unable to reach the upstream or authoritative name servers. Initial value is `true`. +- `serveStaleTtl` (optional): The TTL value in seconds which should be used for cached records that are expired. When the serve stale TTL too expires for a stale record, it gets removed from the cache. Recommended value is between 1-3 days and maximum supported value is 7 days. Initial value is `259200`. +- `cacheMinimumRecordTtl` (optional): The minimum TTL value that a record can have in cache. Set a value to make sure that the records with TTL value than it stays in cache for a minimum duration. Initial value is `10`. +- `cacheMaximumRecordTtl` (optional): The maximum TTL value that a record can have in cache. Set a lower value to allow the records to expire early. Initial value is `86400`. +- `cacheNegativeRecordTtl` (optional): The negative TTL value to use when there is no SOA MINIMUM value available. Initial value is `300`. +- `cacheFailureRecordTtl` (optional): The failure TTL value to used for caching failure responses. This allows storing failure record in cache and prevent frequent recursive resolution to name servers that are responding with `ServerFailure`. Initial value is `60`. - `cachePrefetchEligibility` (optional): The minimum initial TTL value of a record needed to be eligible for prefetching. - `cachePrefetchTrigger` (optional): A record with TTL value less than trigger value will initiate prefetch operation immediately for itself. Set `0` to disable prefetching & auto prefetching. - `cachePrefetchSampleIntervalInMinutes` (optional): The interval to sample eligible domain names from last hour stats for auto prefetch. - `cachePrefetchSampleEligibilityHitsPerHour` (optional): Minimum required hits per hour for a domain name to be eligible for auto prefetch. +- `enableBlocking` (optional): Sets the DNS server to block domain names using Blocked Zone and Block List Zone. +- `allowTxtBlockingReport` (optional): Specifies if the DNS Server should respond with TXT records containing a blocked domain report for TXT type requests. +- `blockingType` (optional): Sets how the DNS server should respond to a blocked domain request. Valid values are [`AnyAddress`, `NxDomain`, `CustomAddress`] where `AnyAddress` is default which response with `0.0.0.0` and `::` IP addresses for blocked domains. Using `NxDomain` will respond with `NX Domain` response. `CustomAddress` will return the specified custom blocking addresses. +- `customBlockingAddresses` (optional): Set the custom blocking addresses to be used for blocked domain response. These addresses are returned only when `blockingType` is set to `CustomAddress`. +- `blockListUrls` (optional): A comma separated list of block list URLs that this server must automatically download and use with the block lists zone. DNS Server will use the data returned by the block list URLs to update the block list zone automatically every 24 hours. The expected file format is standard hosts file format or plain text file containing list of domains to block. Set this parameter to `false` to remove existing values. +- `blockListUpdateIntervalHours` (optional): The interval in hours to automatically download and update the block lists. Initial value is `24`. - `proxyType` (optional): The type of proxy protocol to be used. Valid values are [`None`, `Http`, `Socks5`]. - `proxyAddress` (optional): The proxy server hostname or IP address. - `proxyPort` (optional): The proxy server port. @@ -4248,14 +4290,16 @@ WHERE: - `proxyPassword` (optional): The proxy server password. - `proxyBypass` (optional): A comma separated bypass list consisting of IP addresses, network addresses in CIDR format, or host/domain names to never use proxy for. - `forwarders` (optional): A comma separated list of forwarders to be used by this DNS server. Set this parameter to `false` string to remove existing forwarders so that the DNS server does recursive resolution by itself. -- `forwarderProtocol` (optional): The forwarder DNS transport protocol to be used. Valid values are [`Udp`, `Tcp`, `Tls`, `Https`]. -- `enableBlocking` (optional): Sets the DNS server to block domain names using Blocked Zone and Block List Zone. -- `allowTxtBlockingReport` (optional): Specifies if the DNS Server should respond with TXT records containing a blocked domain report for TXT type requests. -- `blockingType` (optional): Sets how the DNS server should respond to a blocked domain request. Valid values are [`AnyAddress`, `NxDomain`, `CustomAddress`] where `AnyAddress` is default which response with `0.0.0.0` and `::` IP addresses for blocked domains. Using `NxDomain` will respond with `NX Domain` response. `CustomAddress` will return the specified custom blocking addresses. -- `blockListNextUpdatedOn` (read only): An ISO 8601 String with the Date and Time when the blocklist will next be updated. -- `customBlockingAddresses` (optional): Set the custom blocking addresses to be used for blocked domain response. These addresses are returned only when `blockingType` is set to `CustomAddress`. -- `blockListUrls` (optional): A comma separated list of block list URLs that this server must automatically download and use with the block lists zone. DNS Server will use the data returned by the block list URLs to update the block list zone automatically every 24 hours. The expected file format is standard hosts file format or plain text file containing list of domains to block. Set this parameter to `false` to remove existing values. -- `blockListUpdateIntervalHours` (optional): The interval in hours to automatically download and update the block lists. Default value is `24`. +- `forwarderProtocol` (optional): The forwarder DNS transport protocol to be used. Valid values are [`Udp`, `Tcp`, `Tls`, `Https`, `Quic`]. +- `forwarderRetries` (optional): The number of retries that the forwarder DNS client must do. +- `forwarderTimeout` (optional): The timeout value in milliseconds for the forwarder DNS client. +- `forwarderConcurrency` (optional): The number of concurrent requests that the forwarder DNS client should do. +- `enableLogging` (optional): Enable this option to log error and audit logs into the log file. Initial value is `true`. +- `logQueries` (optional): Enable this option to log every query received by this DNS Server and the corresponding response answers into the log file. Initial value is `false`. +- `useLocalTime` (optional): Enable this option to use local time instead of UTC for logging. Initial value is `false`. +- `logFolder` (optional): The folder path on the server where the log files should be saved. The path can be relative to the DNS server config folder. Initial value is `logs`. +- `maxLogFileDays` (optional): Max number of days to keep the log files. Log files older than the specified number of days will be deleted automatically. Recommended value is `365`. Set `0` to disable auto delete. +- `maxStatFileDays` (optional): Max number of days to keep the dashboard stats. Stat files older than the specified number of days will be deleted automatically. Recommended value is `365`. Set `0` to disable auto delete. RESPONSE: This call returns the newly updated settings in the same format as that of the `getDnsSettings` call. @@ -4611,6 +4655,11 @@ RESPONSE: "pingCheckTimeout": 1000, "pingCheckRetries": 2, "domainName": "local", + "domainSearchList": [ + "home.arpa", + "lan" + ], + "dnsUpdates": true, "dnsTtl": 900, "serverAddress": "192.168.1.1", "serverHostName": "tftp-server-1", @@ -4639,6 +4688,19 @@ RESPONSE: "information": "06:01:03:0A:04:00:50:58:45:09:14:00:00:11:52:61:73:70:62:65:72:72:79:20:50:69:20:42:6F:6F:74:FF" } ], + "capwapAcIpAddresses": [ + "192.168.1.2" + ], + "tftpServerAddresses": [ + "192.168.1.5", + "192.168.1.6" + ], + "genericOptions": [ + { + "code": 150, + "value": "C0:A8:01:01" + } + ], "exclusions": [ { "startingAddress": "192.168.1.1", @@ -4653,7 +4715,8 @@ RESPONSE: "comments": "comments" } ], - "allowOnlyReservedLeases": false + "allowOnlyReservedLeases": false, + "blockLocallyAdministeredMacAddresses": true }, "status": "ok" } @@ -4682,25 +4745,32 @@ WHERE: - `leaseTimeDays` (optional): The lease time in number of days. - `leaseTimeHours` (optional): The lease time in number of hours. - `leaseTimeMinutes` (optional): The lease time in number of minutes. -- `offerDelayTime` (optional): The time duration in milli seconds that the DHCP server delays sending an DHCPOFFER message. +- `offerDelayTime` (optional): The time duration in milliseconds that the DHCP server delays sending an DHCPOFFER message. - `pingCheckEnabled` (optional): Set this option to `true` to allow the DHCP server to find out if an IP address is already in use to prevent IP address conflict when some of the devices on the network have manually configured IP addresses. - `pingCheckTimeout` (optional): The timeout interval to wait for an ping reply. - `pingCheckRetries` (optional): The maximum number of ping requests to try. -- `domainName` (optional): The domain name to be used by this network. The DHCP server automatically adds forward and reverse DNS entries for each IP address allocations when domain name is configured. +- `domainName` (optional): The domain name to be used by this network. The DHCP server automatically adds forward and reverse DNS entries for each IP address allocations when domain name is configured. (Option 15) +- `domainSearchList` (optional): A comma separated list of domain names that the clients can use as a suffix when searching a domain name. (Option 119) +- `dnsUpdates` (optional): Set this option to `true` to allow the DHCP server to automatically update forward and reverse DNS entries for clients. - `dnsTtl` (optional): The TTL value used for forward and reverse DNS records. -- `serverAddress` (optional): The bootstrap TFTP server IP address to be used by the clients. If not specified, the DHCP server's IP address is used. -- `serverHostName` (optional): The optional bootstrap TFTP server host name to be used by the clients to identify the TFTP server. -- `bootFileName` (optional): The boot file name stored on the bootstrap TFTP server to be used by the clients. -- `routerAddress` (optional): The default gateway or router IP address to be used by the clients. +- `serverAddress` (optional): The IP address of next server (TFTP) to use in bootstrap by the clients. If not specified, the DHCP server's IP address is used. (siaddr) +- `serverHostName` (optional): The optional bootstrap server host name to be used by the clients to identify the TFTP server. (sname/Option 66) +- `bootFileName` (optional): The boot file name stored on the bootstrap TFTP server to be used by the clients. (file/Option 67) +- `routerAddress` (optional): The default gateway IP address to be used by the clients. (Option 3) - `useThisDnsServer` (optional): Tells the DHCP server to use this DNS server's IP address to configure the DNS Servers DHCP option for clients. -- `dnsServers` (optional): A comma separated list of DNS server IP addresses to be used by the clients. This parameter is ignored when `useThisDnsServer` is set to `true`. -- `winsServers` (optional): A comma separated list of NBNS/WINS server IP addresses to be used by the clients. -- `ntpServers` (optional): A comma separated list of Network Time Protocol (NTP) server IP addresses to be used by the clients. -- `staticRoutes` (optional): A `|` separated list of static routes in format `{destination network address}|{subnet mask}|{router/gateway address}` to be used by the clients for accessing specified destination networks. -- `vendorInfo` (optional): A `|` separated list of vendor information in format `{vendor class identifier}|{vendor specific information}` where `{vendor specific information}` is a colon separated hex string. +- `dnsServers` (optional): A comma separated list of DNS server IP addresses to be used by the clients. This parameter is ignored when `useThisDnsServer` is set to `true`. (Option 6) +- `winsServers` (optional): A comma separated list of NBNS/WINS server IP addresses to be used by the clients. (Option 44) +- `ntpServers` (optional): A comma separated list of Network Time Protocol (NTP) server IP addresses to be used by the clients. (Option 42) +- `ntpServerDomainNames` (optional): Enter NTP server domain names (e.g. pool.ntp.org) above that the DHCP server should automatically resolve and pass the resolved IP addresses to clients as NTP server option. (Option 42) +- `staticRoutes` (optional): A `|` separated list of static routes in format `{destination network address}|{subnet mask}|{router/gateway address}` to be used by the clients for accessing specified destination networks. (Option 121) +- `vendorInfo` (optional): A `|` separated list of vendor information in format `{vendor class identifier}|{vendor specific information}` where `{vendor specific information}` is a colon separated hex string or a normal hex string. +- `capwapAcIpAddresses` (optional): A comma separated list of Control And Provisioning of Wireless Access Points (CAPWAP) Access Controller IP addresses to be used by Wireless Termination Points to discover the Access Controllers to which it is to connect. (Option 138) +- `tftpServerAddresses` (optional): A comma separated list of TFTP Server Address or the VoIP Configuration Server Address. (Option 150) +- `genericOptions` (optional): This feature allows you to define DHCP options that are not yet directly supported. Use a `|` separated list of DHCP option code defined for it and the value in either a colon (:) separated hex string or a normal hex string in format `{option-code}|{hex-string-value}`. - `exclusions` (optional): A `|` separated list of IP address range in format `{starting address}|{ending address}` that must be excluded or not assigned dynamically to any client by the DHCP server. - `reservedLeases` (optional): A `|` separated list of reserved IP addresses in format `{host name}|{MAC address}|{reserved IP address}|{comments}` to be assigned to specific clients based on their MAC address. - `allowOnlyReservedLeases` (optional): Set this parameter to `true` to stop dynamic IP address allocation and allocate only reserved IP addresses. +- `blockLocallyAdministeredMacAddresses` (optional): Set this parameter to `true` to stop dynamic IP address allocation for clients with locally administered MAC addresses. MAC address with 0x02 bit set in the first octet indicate a locally administered MAC address which usually means that the device is not using its original MAC address. RESPONSE: ``` @@ -4715,7 +4785,7 @@ RESPONSE: Adds a reserved lease entry to the specified scope. URL:\ -`http://localhost:5380/api/dhcp/scopes/addReservedLease?token=x` +`http://localhost:5380/api/dhcp/scopes/addReservedLease?token=x&name=Default&hardwareAddress=00:00:00:00:00:00` PERMISSIONS:\ DhcpServer: Modify @@ -4741,7 +4811,7 @@ RESPONSE: Removed a reserved lease entry from the specified scope. URL:\ -`http://localhost:5380/api/dhcp/scopes/removeReservedLease?token=x` +`http://localhost:5380/api/dhcp/scopes/removeReservedLease?token=x&name=Default&hardwareAddress=00:00:00:00:00:00` PERMISSIONS:\ DhcpServer: Modify @@ -5827,7 +5897,7 @@ WHERE: - `start` (optional): The start date time in ISO 8601 format to filter the logs. - `end` (optional): The end date time in ISO 8601 format to filter the logs. - `clientIpAddress` (optional): The client IP address to filter the logs. -- `protocol` (optional): The DNS transport protocol to filter the logs. Valid values are [`Udp`, `Tcp`, `Tls`, `Https`, `HttpsJson`]. +- `protocol` (optional): The DNS transport protocol to filter the logs. Valid values are [`Udp`, `Tcp`, `Tls`, `Https`, `Quic`]. - `responseType` (optional): The DNS server response type to filter the logs. Valid values are [`Authoritative`, `Recursive`, `Cached`, `Blocked`]. - `rcode` (optional): The DNS response code to filter the logs. - `qname` (optional): The query name (QNAME) in the request question section to filter the logs. diff --git a/Apps/AdvancedBlockingApp/AdvancedBlockingApp.csproj b/Apps/AdvancedBlockingApp/AdvancedBlockingApp.csproj index e8e33de3..6cd8b4ab 100644 --- a/Apps/AdvancedBlockingApp/AdvancedBlockingApp.csproj +++ b/Apps/AdvancedBlockingApp/AdvancedBlockingApp.csproj @@ -3,7 +3,7 @@ net7.0 false - 4.0.1 + 5.0 Technitium Technitium DNS Server Shreyas Zare @@ -16,10 +16,6 @@ Library - - - - false @@ -27,6 +23,10 @@ + + ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.dll + false + ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.Net.dll false diff --git a/Apps/AdvancedBlockingApp/App.cs b/Apps/AdvancedBlockingApp/App.cs index 1cd2e73d..a15781cb 100644 --- a/Apps/AdvancedBlockingApp/App.cs +++ b/Apps/AdvancedBlockingApp/App.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -18,7 +18,6 @@ along with this program. If not, see . */ using DnsServerCore.ApplicationCommon; -using Newtonsoft.Json; using System; using System.Collections.Generic; using System.IO; @@ -27,11 +26,14 @@ using System.Net.Http; using System.Net.Sockets; using System.Security.Cryptography; using System.Text; +using System.Text.Json; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; +using TechnitiumLibrary; using TechnitiumLibrary.Net; using TechnitiumLibrary.Net.Dns; +using TechnitiumLibrary.Net.Dns.EDnsOptions; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace AdvancedBlocking @@ -252,29 +254,21 @@ namespace AdvancedBlocking _soaRecord = new DnsSOARecordData(_dnsServer.ServerDomain, "hostadmin@" + _dnsServer.ServerDomain, 1, 14400, 3600, 604800, 60); _nsRecord = new DnsNSRecordData(_dnsServer.ServerDomain); - dynamic jsonConfig = JsonConvert.DeserializeObject(config); + using JsonDocument jsonDocument = JsonDocument.Parse(config); + JsonElement jsonConfig = jsonDocument.RootElement; - _enableBlocking = jsonConfig.enableBlocking.Value; - _blockListUrlUpdateIntervalHours = Convert.ToInt32(jsonConfig.blockListUrlUpdateIntervalHours.Value); + _enableBlocking = jsonConfig.GetProperty("enableBlocking").GetBoolean(); + _blockListUrlUpdateIntervalHours = jsonConfig.GetProperty("blockListUrlUpdateIntervalHours").GetInt32(); + + _networkGroupMap = jsonConfig.ReadObjectAsMap("networkGroupMap", delegate (string network, JsonElement jsonGroup) + { + if (!NetworkAddress.TryParse(network, out NetworkAddress networkAddress)) + throw new InvalidOperationException("Network group map contains an invalid network address: " + network); + + return new Tuple(networkAddress, jsonGroup.GetString()); + }); { - Dictionary networkGroupMap = new Dictionary(); - - foreach (dynamic jsonProperty in jsonConfig.networkGroupMap) - { - string network = jsonProperty.Name; - string group = jsonProperty.Value; - - if (NetworkAddress.TryParse(network, out NetworkAddress networkAddress)) - networkGroupMap.Add(networkAddress, group); - } - - _networkGroupMap = networkGroupMap; - } - - { - Dictionary groups = new Dictionary(); - Dictionary allAllowListZones = new Dictionary(0); Dictionary allBlockListZones = new Dictionary(0); @@ -283,11 +277,9 @@ namespace AdvancedBlocking Dictionary allAdBlockListZones = new Dictionary(0); - foreach (dynamic jsonGroup in jsonConfig.groups) + _groups = jsonConfig.ReadArrayAsMap("groups", delegate (JsonElement jsonGroup) { Group group = new Group(this, jsonGroup); - if (!groups.TryAdd(group.Name, group)) - continue; foreach (Uri allowListUrl in group.AllowListUrls) { @@ -343,9 +335,9 @@ namespace AdvancedBlocking allAdBlockListZones.Add(adblockListUrl, new AdBlockList(_dnsServer, adblockListUrl)); } } - } - _groups = groups; + return new Tuple(group.Name, group); + }); _allAllowListZones = allAllowListZones; _allBlockListZones = allBlockListZones; @@ -427,10 +419,10 @@ namespace AdvancedBlocking return Task.CompletedTask; } - public Task ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed) + public async Task ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed) { if (!_enableBlocking) - return Task.FromResult(null); + return null; IPAddress remoteIP = remoteEP.Address; NetworkAddress network = null; @@ -446,37 +438,66 @@ namespace AdvancedBlocking } if ((groupName is null) || !_groups.TryGetValue(groupName, out Group group) || !group.EnableBlocking) - return Task.FromResult(null); + return null; DnsQuestionRecord question = request.Question[0]; - if (!group.IsZoneBlocked(question.Name, out string blockedDomain, out string blockedRegex, out Uri blockListUrl)) - return Task.FromResult(null); - - if (group.AllowTxtBlockingReport && (question.Type == DnsResourceRecordType.TXT)) + if (!group.IsZoneBlocked(question.Name, out bool allowed, out string blockedDomain, out string blockedRegex, out Uri blockListUrl)) { - //return meta data - DnsResourceRecord[] answer; + if (allowed) + { + DnsDatagram internalResponse = await _dnsServer.DirectQueryAsync(request); + if (internalResponse.Tag is null) + internalResponse.Tag = DnsServerResponseType.Recursive; + + return internalResponse; + } + + return null; + } + + string GetBlockingReport() + { + string blockingReport = "source=advanced-blocking-app; group=" + group.Name; if (blockedRegex is null) { if (blockListUrl is not null) - answer = new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.TXT, question.Class, 60, new DnsTXTRecordData("source=advanced-blocking-app; group=" + group.Name + "; blockListUrl=" + blockListUrl.AbsoluteUri + "; domain=" + blockedDomain)) }; + blockingReport += "; blockListUrl=" + blockListUrl.AbsoluteUri + "; domain=" + blockedDomain; else - answer = new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.TXT, question.Class, 60, new DnsTXTRecordData("source=advanced-blocking-app; group=" + group.Name + "; domain=" + blockedDomain)) }; + blockingReport += "; domain=" + blockedDomain; } else { if (blockListUrl is not null) - answer = new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.TXT, question.Class, 60, new DnsTXTRecordData("source=advanced-blocking-app; group=" + group.Name + "; regexBlockListUrl=" + blockListUrl.AbsoluteUri + "; regex=" + blockedRegex)) }; + blockingReport += "; regexBlockListUrl=" + blockListUrl.AbsoluteUri + "; regex=" + blockedRegex; else - answer = new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.TXT, question.Class, 60, new DnsTXTRecordData("source=advanced-blocking-app; group=" + group.Name + "; regex=" + blockedRegex)) }; + blockingReport += "; regex=" + blockedRegex; } - return Task.FromResult(new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, answer) { Tag = DnsServerResponseType.Blocked }); + return blockingReport; + } + + if (group.AllowTxtBlockingReport && (question.Type == DnsResourceRecordType.TXT)) + { + //return meta data + string blockingReport = GetBlockingReport(); + + DnsResourceRecord[] answer = new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.TXT, question.Class, 60, new DnsTXTRecordData(blockingReport)) }; + + return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question, answer) { Tag = DnsServerResponseType.Blocked }; } else { + EDnsOption[] options = null; + + if (group.AllowTxtBlockingReport && (request.EDNS is not null)) + { + string blockingReport = GetBlockingReport(); + + options = new EDnsOption[] { new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.Blocked, blockingReport)) }; + } + DnsResponseCode rcode; IReadOnlyList answer = null; IReadOnlyList authority = null; @@ -549,7 +570,7 @@ namespace AdvancedBlocking } } - return Task.FromResult(new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, isRecursionAllowed, false, false, rcode, request.Question, answer, authority) { Tag = DnsServerResponseType.Blocked }); + return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, rcode, request.Question, answer, authority, null, request.EDNS is null ? ushort.MinValue : _dnsServer.UdpPayloadSize, EDnsHeaderFlags.None, options) { Tag = DnsServerResponseType.Blocked }; } } @@ -600,22 +621,23 @@ namespace AdvancedBlocking #region constructor - public Group(App app, dynamic jsonGroup) + public Group(App app, JsonElement jsonGroup) { _app = app; - _name = jsonGroup.name.Value; - _enableBlocking = jsonGroup.enableBlocking.Value; - _allowTxtBlockingReport = jsonGroup.allowTxtBlockingReport.Value; - _blockAsNxDomain = jsonGroup.blockAsNxDomain.Value; + _name = jsonGroup.GetProperty("name").GetString(); + _enableBlocking = jsonGroup.GetProperty("enableBlocking").GetBoolean(); + _allowTxtBlockingReport = jsonGroup.GetProperty("allowTxtBlockingReport").GetBoolean(); + _blockAsNxDomain = jsonGroup.GetProperty("blockAsNxDomain").GetBoolean(); { + JsonElement jsonBlockingAddresses = jsonGroup.GetProperty("blockingAddresses"); List aRecords = new List(); List aaaaRecords = new List(); - foreach (dynamic jsonBlockingAddress in jsonGroup.blockingAddresses) + foreach (JsonElement jsonBlockingAddress in jsonBlockingAddresses.EnumerateArray()) { - string strAddress = jsonBlockingAddress.Value; + string strAddress = jsonBlockingAddress.GetString(); if (IPAddress.TryParse(strAddress, out IPAddress address)) { @@ -636,59 +658,36 @@ namespace AdvancedBlocking _aaaaRecords = aaaaRecords; } - _allowed = ReadJsonDomainArray(jsonGroup.allowed); - _blocked = ReadJsonDomainArray(jsonGroup.blocked); - _allowListUrls = ReadJsonUrlArray(jsonGroup.allowListUrls); - _blockListUrls = ReadJsonUrlArray(jsonGroup.blockListUrls); + _allowed = jsonGroup.ReadArrayAsMap("allowed", GetMapEntry); + _blocked = jsonGroup.ReadArrayAsMap("blocked", GetMapEntry); + _allowListUrls = jsonGroup.ReadArray("allowListUrls", GetUriEntry); + _blockListUrls = jsonGroup.ReadArray("blockListUrls", GetUriEntry); - _allowedRegex = ReadJsonRegexArray(jsonGroup.allowedRegex); - _blockedRegex = ReadJsonRegexArray(jsonGroup.blockedRegex); - _regexAllowListUrls = ReadJsonUrlArray(jsonGroup.regexAllowListUrls); - _regexBlockListUrls = ReadJsonUrlArray(jsonGroup.regexBlockListUrls); + _allowedRegex = jsonGroup.ReadArray("allowedRegex", GetRegexEntry); + _blockedRegex = jsonGroup.ReadArray("blockedRegex", GetRegexEntry); + _regexAllowListUrls = jsonGroup.ReadArray("regexAllowListUrls", GetUriEntry); + _regexBlockListUrls = jsonGroup.ReadArray("regexBlockListUrls", GetUriEntry); - _adblockListUrls = ReadJsonUrlArray(jsonGroup.adblockListUrls); + _adblockListUrls = jsonGroup.ReadArray("adblockListUrls", GetUriEntry); } #endregion #region private - private static IReadOnlyDictionary ReadJsonDomainArray(dynamic jsonDomainArray) + private static Tuple GetMapEntry(JsonElement jsonElement) { - Dictionary domains = new Dictionary(jsonDomainArray.Count); - - foreach (dynamic jsonDomain in jsonDomainArray) - domains.TryAdd(jsonDomain.Value, null); - - return domains; + return new Tuple(jsonElement.GetString(), null); } - private static IReadOnlyList ReadJsonRegexArray(dynamic jsonRegexArray) + private static Uri GetUriEntry(string uriString) { - List regices = new List(jsonRegexArray.Count); - - foreach (dynamic jsonRegex in jsonRegexArray) - { - string regexPattern = jsonRegex.Value; - - regices.Add(new Regex(regexPattern, RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled)); - } - - return regices; + return new Uri(uriString); } - private static IReadOnlyList ReadJsonUrlArray(dynamic jsonUrlArray) + private static Regex GetRegexEntry(string pattern) { - List urls = new List(jsonUrlArray.Count); - - foreach (dynamic jsonUrl in jsonUrlArray) - { - string strUrl = jsonUrl.Value; - - urls.Add(new Uri(strUrl)); - } - - return urls; + return new Regex(pattern, RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled); } #endregion @@ -758,7 +757,7 @@ namespace AdvancedBlocking } } - public bool IsZoneBlocked(string domain, out string blockedDomain, out string blockedRegex, out Uri listUrl) + public bool IsZoneBlocked(string domain, out bool allowed, out string blockedDomain, out string blockedRegex, out Uri listUrl) { domain = domain.ToLower(); @@ -766,6 +765,7 @@ namespace AdvancedBlocking if (IsZoneFound(_allowed, domain, out _) || IsZoneFound(_allowListZones, domain, out _, out _) || IsMatchFound(_allowedRegex, domain, out _) || IsMatchFound(_regexAllowListZones, domain, out _, out _) || IsZoneAllowed(_adBlockListZones, domain, out _, out _)) { //found zone allowed + allowed = true; blockedDomain = null; blockedRegex = null; listUrl = null; @@ -776,6 +776,7 @@ namespace AdvancedBlocking if (IsZoneFound(_blocked, domain, out string foundZone1)) { //found zone blocked + allowed = false; blockedDomain = foundZone1; blockedRegex = null; listUrl = null; @@ -786,6 +787,7 @@ namespace AdvancedBlocking if (IsZoneFound(_blockListZones, domain, out string foundZone2, out Uri blockListUrl1)) { //found zone blocked + allowed = false; blockedDomain = foundZone2; blockedRegex = null; listUrl = blockListUrl1; @@ -796,6 +798,7 @@ namespace AdvancedBlocking if (IsMatchFound(_blockedRegex, domain, out string blockedPattern1)) { //found pattern blocked + allowed = false; blockedDomain = null; blockedRegex = blockedPattern1; listUrl = null; @@ -806,6 +809,7 @@ namespace AdvancedBlocking if (IsMatchFound(_regexBlockListZones, domain, out string blockedPattern2, out Uri blockListUrl2)) { //found pattern blocked + allowed = false; blockedDomain = null; blockedRegex = blockedPattern2; listUrl = blockListUrl2; @@ -816,12 +820,14 @@ namespace AdvancedBlocking if (App.IsZoneBlocked(_adBlockListZones, domain, out string foundZone3, out Uri blockListUrl3)) { //found zone blocked + allowed = false; blockedDomain = foundZone3; blockedRegex = null; listUrl = blockListUrl3; return true; } + allowed = false; blockedDomain = null; blockedRegex = null; listUrl = null; @@ -915,7 +921,7 @@ namespace AdvancedBlocking SocketsHttpHandler handler = new SocketsHttpHandler(); handler.Proxy = _dnsServer.Proxy; handler.UseProxy = _dnsServer.Proxy is not null; - handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; + handler.AutomaticDecompression = DecompressionMethods.All; using (HttpClient http = new HttpClient(handler)) { @@ -1035,6 +1041,8 @@ namespace AdvancedBlocking { #region variables + readonly static char[] _popWordSeperator = new char[] { ' ', '\t' }; + IReadOnlyDictionary _listZone = new Dictionary(0); #endregion @@ -1054,9 +1062,9 @@ namespace AdvancedBlocking if (line.Length == 0) return line; - line = line.TrimStart(' ', '\t'); + line = line.TrimStart(_popWordSeperator); - int i = line.IndexOfAny(new char[] { ' ', '\t' }); + int i = line.IndexOfAny(_popWordSeperator); string word; if (i < 0) @@ -1085,6 +1093,7 @@ namespace AdvancedBlocking { //parse hosts file and populate block zone StreamReader sR = new StreamReader(fS, true); + char[] trimSeperator = new char[] { ' ', '\t', '*', '.' }; string line; string firstWord; string secondWord; @@ -1096,7 +1105,7 @@ namespace AdvancedBlocking if (line == null) break; //eof - line = line.TrimStart(' ', '\t'); + line = line.TrimStart(trimSeperator); if (line.Length == 0) continue; //skip empty line @@ -1216,6 +1225,7 @@ namespace AdvancedBlocking { //parse hosts file and populate block zone StreamReader sR = new StreamReader(fS, true); + char[] trimSeperator = new char[] { ' ', '\t' }; string line; while (true) @@ -1224,7 +1234,7 @@ namespace AdvancedBlocking if (line == null) break; //eof - line = line.TrimStart(' ', '\t'); + line = line.TrimStart(trimSeperator); if (line.Length == 0) continue; //skip empty line @@ -1314,6 +1324,7 @@ namespace AdvancedBlocking { //parse hosts file and populate block zone StreamReader sR = new StreamReader(fS, true); + char[] trimSeperator = new char[] { ' ', '\t' }; string line; while (true) @@ -1322,7 +1333,7 @@ namespace AdvancedBlocking if (line == null) break; //eof - line = line.TrimStart(' ', '\t'); + line = line.TrimStart(trimSeperator); if (line.Length == 0) continue; //skip empty line @@ -1358,7 +1369,7 @@ namespace AdvancedBlocking string options = line.Substring(i + 1); if (((options.Length == 0) || (options.StartsWith("$") && (options.Contains("doc") || options.Contains("all")))) && DnsClient.IsDomainNameValid(domain)) - blockedDomains.Enqueue(domain); + allowedDomains.Enqueue(domain); } else { diff --git a/Apps/AdvancedBlockingApp/dnsApp.config b/Apps/AdvancedBlockingApp/dnsApp.config index 38d2c922..410be64e 100644 --- a/Apps/AdvancedBlockingApp/dnsApp.config +++ b/Apps/AdvancedBlockingApp/dnsApp.config @@ -10,7 +10,7 @@ "name": "everyone", "enableBlocking": true, "allowTxtBlockingReport": true, - "blockAsNxDomain": false, + "blockAsNxDomain": true, "blockingAddresses": [ "0.0.0.0", "::" @@ -35,7 +35,7 @@ "name": "kids", "enableBlocking": true, "allowTxtBlockingReport": true, - "blockAsNxDomain": false, + "blockAsNxDomain": true, "blockingAddresses": [ "0.0.0.0", "::" diff --git a/Apps/AdvancedForwardingApp/AdvancedForwardingApp.csproj b/Apps/AdvancedForwardingApp/AdvancedForwardingApp.csproj new file mode 100644 index 00000000..c923bc47 --- /dev/null +++ b/Apps/AdvancedForwardingApp/AdvancedForwardingApp.csproj @@ -0,0 +1,45 @@ + + + + net7.0 + false + 1.0 + Technitium + Technitium DNS Server + Shreyas Zare + AdvancedForwardingApp + AdvancedForwarding + https://technitium.com/dns/ + https://github.com/TechnitiumSoftware/DnsServer + Provides advanced, bulk conditional forwarding options. Supports creating groups based on client's IP address or subnet to enable different conditional forwarding configuration for each group. Supports AdGuard Upstreams config files.\n\nNote: This app works independent of the DNS server's built-in Conditional Forwarder Zones feature. + false + Library + + + + + false + + + + + + ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.dll + false + + + ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.Net.dll + false + + + + + + PreserveNewest + + + PreserveNewest + + + + diff --git a/Apps/AdvancedForwardingApp/App.cs b/Apps/AdvancedForwardingApp/App.cs new file mode 100644 index 00000000..267fc0e4 --- /dev/null +++ b/Apps/AdvancedForwardingApp/App.cs @@ -0,0 +1,808 @@ +/* +Technitium DNS Server +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +using DnsServerCore.ApplicationCommon; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using TechnitiumLibrary; +using TechnitiumLibrary.Net; +using TechnitiumLibrary.Net.Dns; +using TechnitiumLibrary.Net.Dns.ResourceRecords; +using TechnitiumLibrary.Net.Proxy; + +namespace AdvancedForwarding +{ + public class App : IDnsApplication, IDnsAuthoritativeRequestHandler + { + #region variables + + IDnsServer _dnsServer; + + bool _enableForwarding; + Dictionary _configProxyServers; + Dictionary _configForwarders; + IReadOnlyDictionary _networkGroupMap; + IReadOnlyDictionary _groups; + + #endregion + + #region IDisposable + + public void Dispose() + { + if (_groups is not null) + { + foreach (KeyValuePair group in _groups) + group.Value.Dispose(); + } + } + + #endregion + + #region private + + private static IReadOnlyList GetUpdatedForwarderRecords(IReadOnlyList forwarderRecords, bool dnssecValidation, ConfigProxyServer configProxyServer) + { + List newForwarderRecords = new List(forwarderRecords.Count); + + foreach (DnsForwarderRecordData forwarderRecord in forwarderRecords) + newForwarderRecords.Add(GetForwarderRecord(forwarderRecord.Protocol, forwarderRecord.Forwarder, dnssecValidation, configProxyServer)); + + return newForwarderRecords; + } + + private static DnsForwarderRecordData GetForwarderRecord(NameServerAddress forwarder, bool dnssecValidation, ConfigProxyServer configProxyServer) + { + return GetForwarderRecord(forwarder.Protocol, forwarder.ToString(), dnssecValidation, configProxyServer); + } + + private static DnsForwarderRecordData GetForwarderRecord(DnsTransportProtocol protocol, string forwarder, bool dnssecValidation, ConfigProxyServer configProxyServer) + { + DnsForwarderRecordData forwarderRecord; + + if (configProxyServer is null) + forwarderRecord = new DnsForwarderRecordData(protocol, forwarder, dnssecValidation, NetProxyType.None, null, 0, null, null); + else + forwarderRecord = new DnsForwarderRecordData(protocol, forwarder, dnssecValidation, configProxyServer.Type, configProxyServer.ProxyAddress, configProxyServer.ProxyPort, configProxyServer.ProxyUsername, configProxyServer.ProxyPassword); + + return forwarderRecord; + } + + private Tuple ReadGroup(JsonElement jsonGroup) + { + Group group; + string name = jsonGroup.GetProperty("name").GetString(); + + if ((_groups is not null) && _groups.TryGetValue(name, out group)) + group.ReloadConfig(_configProxyServers, _configForwarders, jsonGroup); + else + group = new Group(_dnsServer, _configProxyServers, _configForwarders, jsonGroup); + + return new Tuple(group.Name, group); + } + + #endregion + + #region public + + public Task InitializeAsync(IDnsServer dnsServer, string config) + { + _dnsServer = dnsServer; + + using JsonDocument jsonDocument = JsonDocument.Parse(config); + JsonElement jsonConfig = jsonDocument.RootElement; + + _enableForwarding = jsonConfig.GetPropertyValue("enableForwarding", true); + + if (jsonConfig.TryReadArrayAsMap("proxyServers", delegate (JsonElement jsonProxy) + { + ConfigProxyServer proxyServer = new ConfigProxyServer(jsonProxy); + return new Tuple(proxyServer.Name, proxyServer); + }, out Dictionary configProxyServers)) + _configProxyServers = configProxyServers; + else + _configProxyServers = null; + + if (jsonConfig.TryReadArrayAsMap("forwarders", delegate (JsonElement jsonForwarder) + { + ConfigForwarder forwarder = new ConfigForwarder(jsonForwarder, _configProxyServers); + return new Tuple(forwarder.Name, forwarder); + }, out Dictionary configForwarders)) + _configForwarders = configForwarders; + else + _configForwarders = null; + + _networkGroupMap = jsonConfig.ReadObjectAsMap("networkGroupMap", delegate (string network, JsonElement jsonGroup) + { + if (!NetworkAddress.TryParse(network, out NetworkAddress networkAddress)) + throw new FormatException("Network group map contains an invalid network address: " + network); + + return new Tuple(networkAddress, jsonGroup.GetString()); + }); + + if (jsonConfig.TryReadArrayAsMap("groups", ReadGroup, out Dictionary groups)) + { + if (_groups is not null) + { + foreach (KeyValuePair group in _groups) + { + if (!groups.ContainsKey(group.Key)) + group.Value.Dispose(); + } + } + + _groups = groups; + } + else + { + throw new FormatException("Groups array was not defined."); + } + + return Task.CompletedTask; + } + + public Task ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed) + { + if (!_enableForwarding) + return Task.FromResult(null); + + IPAddress remoteIP = remoteEP.Address; + NetworkAddress network = null; + string groupName = null; + + foreach (KeyValuePair entry in _networkGroupMap) + { + if (entry.Key.Contains(remoteIP) && ((network is null) || (entry.Key.PrefixLength > network.PrefixLength))) + { + network = entry.Key; + groupName = entry.Value; + } + } + + if ((groupName is null) || !_groups.TryGetValue(groupName, out Group group) || !group.EnableForwarding) + return Task.FromResult(null); + + DnsQuestionRecord question = request.Question[0]; + string qname = question.Name; + + if (!group.TryGetForwarderRecords(qname, out IReadOnlyList forwarderRecords)) + return Task.FromResult(null); + + DnsResourceRecord[] authority = new DnsResourceRecord[forwarderRecords.Count]; + + for (int i = 0; i < forwarderRecords.Count; i++) + authority[i] = new DnsResourceRecord(qname, DnsResourceRecordType.FWD, DnsClass.IN, 0, forwarderRecords[i]); + + return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, false, false, request.RecursionDesired, true, false, false, DnsResponseCode.NoError, request.Question, null, authority)); + } + + #endregion + + #region properties + + public string Description + { get { return "Performs bulk conditional forwarding for configured domain names and AdGuard Upstream config files."; } } + + #endregion + + class Group : IDisposable + { + #region variables + + readonly IDnsServer _dnsServer; + Dictionary _configProxyServers; + Dictionary _configForwarders; + + readonly string _name; + bool _enableForwarding; + IReadOnlyList _forwardings; + IReadOnlyDictionary _adguardUpstreams; + + #endregion + + #region constructor + + public Group(IDnsServer dnsServer, Dictionary configProxyServers, Dictionary configForwarders, JsonElement jsonGroup) + { + _dnsServer = dnsServer; + + _name = jsonGroup.GetProperty("name").GetString(); + + ReloadConfig(configProxyServers, configForwarders, jsonGroup); + } + + #endregion + + #region IDisposable + + public void Dispose() + { + if (_adguardUpstreams is not null) + { + foreach (KeyValuePair adguardUpstream in _adguardUpstreams) + adguardUpstream.Value.Dispose(); + + _adguardUpstreams = null; + } + } + + #endregion + + #region private + + private Tuple ReadAdGuardUpstream(JsonElement jsonAdguardUpstream) + { + AdGuardUpstream adGuardUpstream; + string name = jsonAdguardUpstream.GetProperty("configFile").GetString(); + + if ((_adguardUpstreams is not null) && _adguardUpstreams.TryGetValue(name, out adGuardUpstream)) + adGuardUpstream.ReloadConfig(_configProxyServers, jsonAdguardUpstream); + else + adGuardUpstream = new AdGuardUpstream(_dnsServer, _configProxyServers, jsonAdguardUpstream); + + return new Tuple(adGuardUpstream.Name, adGuardUpstream); + } + + #endregion + + #region public + + public void ReloadConfig(Dictionary configProxyServers, Dictionary configForwarders, JsonElement jsonGroup) + { + _configProxyServers = configProxyServers; + _configForwarders = configForwarders; + + _enableForwarding = jsonGroup.GetPropertyValue("enableForwarding", true); + + if (jsonGroup.TryReadArray("forwardings", delegate (JsonElement jsonForwarding) { return new Forwarding(jsonForwarding, _configForwarders); }, out Forwarding[] forwardings)) + _forwardings = forwardings; + else + _forwardings = null; + + if (jsonGroup.TryReadArrayAsMap("adguardUpstreams", ReadAdGuardUpstream, out Dictionary adguardUpstreams)) + { + if (_adguardUpstreams is not null) + { + foreach (KeyValuePair adguardUpstream in _adguardUpstreams) + { + if (!adguardUpstreams.ContainsKey(adguardUpstream.Key)) + adguardUpstream.Value.Dispose(); + } + } + + _adguardUpstreams = adguardUpstreams; + } + else + { + if (_adguardUpstreams is not null) + { + foreach (KeyValuePair adguardUpstream in _adguardUpstreams) + adguardUpstream.Value.Dispose(); + } + + _adguardUpstreams = null; + } + } + + public bool TryGetForwarderRecords(string domain, out IReadOnlyList forwarderRecords) + { + domain = domain.ToLower(); + + if ((_forwardings is not null) && (_forwardings.Count > 0) && Forwarding.TryGetForwarderRecords(domain, _forwardings, out forwarderRecords)) + return true; + + if (_adguardUpstreams is not null) + { + foreach (KeyValuePair adguardUpstream in _adguardUpstreams) + { + if (adguardUpstream.Value.TryGetForwarderRecords(domain, out forwarderRecords)) + return true; + } + } + + forwarderRecords = null; + return false; + } + + #endregion + + #region properties + + public string Name + { get { return _name; } } + + public bool EnableForwarding + { get { return _enableForwarding; } } + + #endregion + } + + class Forwarding + { + #region variables + + IReadOnlyList _forwarderRecords; + readonly IReadOnlyDictionary _domainMap; + + #endregion + + #region constructor + + public Forwarding(JsonElement jsonForwarding, Dictionary configForwarders) + { + JsonElement jsonForwarders = jsonForwarding.GetProperty("forwarders"); + List forwarderRecords = new List(); + + foreach (JsonElement jsonForwarder in jsonForwarders.EnumerateArray()) + { + string forwarderName = jsonForwarder.GetString(); + + if ((configForwarders is null) || !configForwarders.TryGetValue(forwarderName, out ConfigForwarder configForwarder)) + throw new FormatException("Forwarder was not defined: " + forwarderName); + + forwarderRecords.AddRange(configForwarder.ForwarderRecords); + } + + _forwarderRecords = forwarderRecords; + + _domainMap = jsonForwarding.ReadArrayAsMap("domains", delegate (JsonElement jsonDomain) + { + return new Tuple(jsonDomain.GetString().ToLower(), null); + }); + } + + public Forwarding(IReadOnlyList domains, NameServerAddress forwarder, bool dnssecValidation, ConfigProxyServer proxy) + : this(new DnsForwarderRecordData[] { GetForwarderRecord(forwarder, dnssecValidation, proxy) }, domains) + { } + + public Forwarding(IReadOnlyList forwarderRecords, IReadOnlyList domains) + { + _forwarderRecords = forwarderRecords; + + Dictionary domainMap = new Dictionary(domains.Count); + + foreach (string domain in domains) + { + if (DnsClient.IsDomainNameValid(domain)) + domainMap.TryAdd(domain.ToLower(), null); + } + + _domainMap = domainMap; + } + + #endregion + + #region static + + public static bool TryGetForwarderRecords(string domain, IReadOnlyList forwardings, out IReadOnlyList forwarderRecords) + { + if (forwardings.Count == 1) + { + if (forwardings[0].TryGetForwarderRecords(domain, out forwarderRecords, out _)) + return true; + } + else + { + Dictionary> fwdMap = new Dictionary>(forwardings.Count); + + foreach (Forwarding forwarding in forwardings) + { + if (forwarding.TryGetForwarderRecords(domain, out IReadOnlyList fwdRecords, out string matchedDomain)) + { + if (fwdMap.TryGetValue(matchedDomain, out List fwdRecordsList)) + { + fwdRecordsList.AddRange(fwdRecords); + } + else + { + fwdRecordsList = new List(fwdRecords); + fwdMap.Add(matchedDomain, fwdRecordsList); + } + } + } + + if (fwdMap.Count > 0) + { + forwarderRecords = null; + string lastMatchedDomain = null; + + foreach (KeyValuePair> fwdEntry in fwdMap) + { + if ((lastMatchedDomain is null) || (fwdEntry.Key.Length > lastMatchedDomain.Length) || ((fwdEntry.Key.Length == lastMatchedDomain.Length) && lastMatchedDomain.StartsWith("*."))) + { + lastMatchedDomain = fwdEntry.Key; + forwarderRecords = fwdEntry.Value; + } + } + + return true; + } + } + + forwarderRecords = null; + return false; + } + + #endregion + + #region private + + private static string GetParentZone(string domain) + { + int i = domain.IndexOf('.'); + if (i > -1) + return domain.Substring(i + 1); + + //dont return root zone + return null; + } + + private bool IsDomainBlocked(string domain, out string matchedDomain) + { + string parent; + + do + { + if (_domainMap.TryGetValue(domain, out _)) + { + matchedDomain = domain; + return true; + } + + parent = GetParentZone(domain); + if (parent is null) + break; + + domain = "*." + parent; + + if (_domainMap.TryGetValue(domain, out _)) + { + matchedDomain = domain; + return true; + } + + domain = parent; + } + while (true); + + matchedDomain = null; + return false; + } + + private bool TryGetForwarderRecords(string domain, out IReadOnlyList forwarderRecords, out string matchedDomain) + { + if (IsDomainBlocked(domain, out matchedDomain)) + { + forwarderRecords = _forwarderRecords; + return true; + } + + forwarderRecords = null; + return false; + } + + #endregion + + #region public + + public void UpdateForwarderRecords(bool dnssecValidation, ConfigProxyServer proxy) + { + _forwarderRecords = GetUpdatedForwarderRecords(_forwarderRecords, dnssecValidation, proxy); + } + + #endregion + } + + class AdGuardUpstream : IDisposable + { + #region variables + + readonly IDnsServer _dnsServer; + + readonly string _name; + ConfigProxyServer _configProxyServer; + bool _dnssecValidation; + + IReadOnlyList _defaultForwarderRecords; + IReadOnlyList _forwardings; + + readonly string _configFile; + DateTime _configFileLastModified; + + Timer _autoReloadTimer; + const int AUTO_RELOAD_TIMER_INTERVAL = 60000; + + #endregion + + #region constructor + + public AdGuardUpstream(IDnsServer dnsServer, Dictionary configProxyServers, JsonElement jsonAdguardUpstream) + { + _dnsServer = dnsServer; + + _name = jsonAdguardUpstream.GetProperty("configFile").GetString(); + + _configFile = _name; + + if (!Path.IsPathRooted(_configFile)) + _configFile = Path.Combine(_dnsServer.ApplicationFolder, _configFile); + + _autoReloadTimer = new Timer(delegate (object state) + { + try + { + DateTime configFileLastModified = File.GetLastWriteTimeUtc(_configFile); + if (configFileLastModified > _configFileLastModified) + ReloadUpstreamsFile(); + } + catch (Exception ex) + { + _dnsServer.WriteLog(ex); + } + finally + { + _autoReloadTimer?.Change(AUTO_RELOAD_TIMER_INTERVAL, Timeout.Infinite); + } + }); + + ReloadConfig(configProxyServers, jsonAdguardUpstream); + } + + #endregion + + #region IDisposable + + public void Dispose() + { + if (_autoReloadTimer is not null) + { + _autoReloadTimer.Dispose(); + _autoReloadTimer = null; + } + } + + #endregion + + #region private + + private void ReloadUpstreamsFile() + { + try + { + _dnsServer.WriteLog("The app is reading AdGuard Upstreams config file: " + _configFile); + + List defaultForwarderRecords = new List(); + List forwardings = new List(); + + using (FileStream fS = new FileStream(_configFile, FileMode.Open, FileAccess.Read)) + { + StreamReader sR = new StreamReader(fS, true); + string line; + + while (true) + { + line = sR.ReadLine(); + if (line is null) + break; //eof + + line = line.TrimStart(); + + if (line.Length == 0) + continue; //skip empty line + + if (line.StartsWith("#")) + continue; //skip comment line + + if (line.StartsWith("[")) + { + int i = line.LastIndexOf(']'); + if (i < 0) + throw new FormatException("Invalid AdGuard Upstreams config file format: missing ']' bracket."); + + string[] domains = line.Substring(1, i - 1).Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + string forwarder = line.Substring(i + 1); + + if (forwarder == "#") + { + if (defaultForwarderRecords.Count == 0) + throw new FormatException("Invalid AdGuard Upstreams config file format: missing default upstream servers."); + + forwardings.Add(new Forwarding(defaultForwarderRecords, domains)); + } + else + { + forwardings.Add(new Forwarding(domains, NameServerAddress.Parse(forwarder), _dnssecValidation, _configProxyServer)); + } + } + else + { + defaultForwarderRecords.Add(GetForwarderRecord(NameServerAddress.Parse(line), _dnssecValidation, _configProxyServer)); + } + } + + _configFileLastModified = File.GetLastWriteTimeUtc(fS.SafeFileHandle); + } + + _defaultForwarderRecords = defaultForwarderRecords; + _forwardings = forwardings; + + _dnsServer.WriteLog("The app has successfully loaded AdGuard Upstreams config file: " + _configFile); + } + catch (Exception ex) + { + _dnsServer.WriteLog("The app failed to read AdGuard Upstreams config file: " + _configFile + "\r\n" + ex.ToString()); + } + } + + #endregion + + #region public + + public void ReloadConfig(Dictionary configProxyServers, JsonElement jsonAdguardUpstream) + { + string proxyName = jsonAdguardUpstream.GetPropertyValue("proxy", null); + _dnssecValidation = jsonAdguardUpstream.GetPropertyValue("dnssecValidation", true); + + ConfigProxyServer configProxyServer = null; + + if (!string.IsNullOrEmpty(proxyName) && ((configProxyServers is null) || !configProxyServers.TryGetValue(proxyName, out configProxyServer))) + throw new FormatException("Proxy server was not defined: " + proxyName); + + _configProxyServer = configProxyServer; + + DateTime configFileLastModified = File.GetLastWriteTimeUtc(_configFile); + if (configFileLastModified > _configFileLastModified) + { + //reload complete config file + _autoReloadTimer.Change(0, Timeout.Infinite); + } + else + { + //update only forwarder records + _defaultForwarderRecords = GetUpdatedForwarderRecords(_defaultForwarderRecords, _dnssecValidation, _configProxyServer); + + foreach (Forwarding forwarding in _forwardings) + forwarding.UpdateForwarderRecords(_dnssecValidation, _configProxyServer); + } + } + + public bool TryGetForwarderRecords(string domain, out IReadOnlyList forwarderRecords) + { + if ((_forwardings.Count > 0) && Forwarding.TryGetForwarderRecords(domain, _forwardings, out forwarderRecords)) + return true; + + if (_defaultForwarderRecords.Count > 0) + { + forwarderRecords = _defaultForwarderRecords; + return true; + } + + forwarderRecords = null; + return false; + } + + #endregion + + #region property + + public string Name + { get { return _name; } } + + #endregion + } + + class ConfigProxyServer + { + #region variables + + readonly string _name; + readonly NetProxyType _type; + readonly string _proxyAddress; + readonly ushort _proxyPort; + readonly string _proxyUsername; + readonly string _proxyPassword; + + #endregion + + #region constructor + + public ConfigProxyServer(JsonElement jsonProxy) + { + _name = jsonProxy.GetProperty("name").GetString(); + _type = jsonProxy.GetPropertyEnumValue("type", NetProxyType.Http); + _proxyAddress = jsonProxy.GetProperty("proxyAddress").GetString(); + _proxyPort = jsonProxy.GetProperty("proxyPort").GetUInt16(); + _proxyUsername = jsonProxy.GetPropertyValue("proxyUsername", null); + _proxyPassword = jsonProxy.GetPropertyValue("proxyPassword", null); + } + + #endregion + + #region properties + + public string Name + { get { return _name; } } + + public NetProxyType Type + { get { return _type; } } + + public string ProxyAddress + { get { return _proxyAddress; } } + + public ushort ProxyPort + { get { return _proxyPort; } } + + public string ProxyUsername + { get { return _proxyUsername; } } + + public string ProxyPassword + { get { return _proxyPassword; } } + + #endregion + } + + class ConfigForwarder + { + #region variables + + readonly string _name; + readonly IReadOnlyList _forwarderRecords; + + #endregion + + #region constructor + + public ConfigForwarder(JsonElement jsonForwarder, Dictionary configProxyServers) + { + _name = jsonForwarder.GetProperty("name").GetString(); + + string proxyName = jsonForwarder.GetPropertyValue("proxy", null); + bool dnssecValidation = jsonForwarder.GetPropertyValue("dnssecValidation", true); + DnsTransportProtocol forwarderProtocol = jsonForwarder.GetPropertyEnumValue("forwarderProtocol", DnsTransportProtocol.Udp); + + ConfigProxyServer configProxyServer = null; + + if (!string.IsNullOrEmpty(proxyName) && ((configProxyServers is null) || !configProxyServers.TryGetValue(proxyName, out configProxyServer))) + throw new FormatException("Proxy server was not defined: " + proxyName); + + _forwarderRecords = jsonForwarder.ReadArray("forwarderAddresses", delegate (string address) + { + return GetForwarderRecord(forwarderProtocol, address, dnssecValidation, configProxyServer); + }); + } + + #endregion + + #region properties + + public string Name + { get { return _name; } } + + public IReadOnlyList ForwarderRecords + { get { return _forwarderRecords; } } + + #endregion + } + } +} diff --git a/Apps/AdvancedForwardingApp/adguard-upstreams.txt b/Apps/AdvancedForwardingApp/adguard-upstreams.txt new file mode 100644 index 00000000..77674a51 --- /dev/null +++ b/Apps/AdvancedForwardingApp/adguard-upstreams.txt @@ -0,0 +1,10 @@ +# AdGuard Upstreams +# File Format Reference: https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration#upstreams +# +# Example: +# 8.8.8.8 +# udp://9.9.9.9 +# [/host.com/example.com/]tls://1.1.1.1 +# [/maps.host.com/]# +# [/home/]192.168.10.2 +# [/test.com/]https://dns.quad9.net/dns-query (9.9.9.9) diff --git a/Apps/AdvancedForwardingApp/dnsApp.config b/Apps/AdvancedForwardingApp/dnsApp.config new file mode 100644 index 00000000..40479b5e --- /dev/null +++ b/Apps/AdvancedForwardingApp/dnsApp.config @@ -0,0 +1,77 @@ +{ + "enableForwarding": true, + "proxyServers": [ + { + "name": "local-proxy", + "type": "socks5", + "proxyAddress": "localhost", + "proxyPort": 1080, + "proxyUsername": null, + "proxyPassword": null + } + ], + "forwarders": [ + { + "name": "quad9-doh", + "proxy": null, + "dnssecValidation": true, + "forwarderProtocol": "Https", + "forwarderAddresses": [ + "https://dns.quad9.net/dns-query (9.9.9.9)" + ] + }, + { + "name": "cloudflare-google", + "proxy": null, + "dnssecValidation": true, + "forwarderProtocol": "Tls", + "forwarderAddresses": [ + "1.1.1.1", + "8.8.8.8" + ] + }, + { + "name": "quad9-tls-proxied", + "proxy": "local-proxy", + "dnssecValidation": true, + "forwarderProtocol": "Tls", + "forwarderAddresses": [ + "9.9.9.9" + ] + } + ], + "networkGroupMap": { + "0.0.0.0/0": "everyone" + }, + "groups": [ + { + "name": "everyone", + "enableForwarding": true, + "forwardings": [ + { + "forwarders": [ + "quad9-doh" + ], + "domains": [ + "example.com" + ] + }, + { + "forwarders": [ + "cloudflare-google" + ], + "domains": [ + "example.net" + ] + } + ], + "adguardUpstreams": [ + { + "proxy": null, + "dnssecValidation": true, + "configFile": "adguard-upstreams.txt" + } + ] + } + ] +} diff --git a/Apps/BlockPageApp/App.cs b/Apps/BlockPageApp/App.cs index cf4c8581..1b9a912d 100644 --- a/Apps/BlockPageApp/App.cs +++ b/Apps/BlockPageApp/App.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -18,7 +18,6 @@ along with this program. If not, see . */ using DnsServerCore.ApplicationCommon; -using Newtonsoft.Json; using System; using System.Collections.Generic; using System.IO; @@ -28,6 +27,7 @@ using System.Net.Sockets; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using TechnitiumLibrary; @@ -296,13 +296,17 @@ namespace BlockPage if (usingHttps) { SslStream httpsStream = new SslStream(stream); - await httpsStream.AuthenticateAsServerAsync(_webServerTlsCertificate); + await httpsStream.AuthenticateAsServerAsync(_webServerTlsCertificate).WithTimeout(TCP_RECV_TIMEOUT); stream = httpsStream; } await ProcessHttpRequestAsync(stream, remoteEP, usingHttps); } + catch (TimeoutException) + { + //ignore timeout exception on TLS auth + } catch (IOException) { //ignore IO exceptions @@ -313,8 +317,7 @@ namespace BlockPage } finally { - if (socket is not null) - socket.Dispose(); + socket.Dispose(); } } @@ -479,35 +482,29 @@ namespace BlockPage { _dnsServer = dnsServer; - dynamic jsonConfig = JsonConvert.DeserializeObject(config); + using JsonDocument jsonDocument = JsonDocument.Parse(config); + JsonElement jsonConfig = jsonDocument.RootElement; - { - List webServerLocalAddresses = new List(); + _webServerLocalAddresses = jsonConfig.ReadArray("webServerLocalAddresses", IPAddress.Parse); - foreach (dynamic jsonAddress in jsonConfig.webServerLocalAddresses) - webServerLocalAddresses.Add(IPAddress.Parse(jsonAddress.Value)); - - _webServerLocalAddresses = webServerLocalAddresses; - } - - if (jsonConfig.webServerUseSelfSignedTlsCertificate is null) - _webServerUseSelfSignedTlsCertificate = true; + if (jsonConfig.TryGetProperty("webServerUseSelfSignedTlsCertificate", out JsonElement jsonWebServerUseSelfSignedTlsCertificate)) + _webServerUseSelfSignedTlsCertificate = jsonWebServerUseSelfSignedTlsCertificate.GetBoolean(); else - _webServerUseSelfSignedTlsCertificate = jsonConfig.webServerUseSelfSignedTlsCertificate.Value; + _webServerUseSelfSignedTlsCertificate = true; - _webServerTlsCertificateFilePath = jsonConfig.webServerTlsCertificateFilePath.Value; - _webServerTlsCertificatePassword = jsonConfig.webServerTlsCertificatePassword.Value; + _webServerTlsCertificateFilePath = jsonConfig.GetProperty("webServerTlsCertificateFilePath").GetString(); + _webServerTlsCertificatePassword = jsonConfig.GetProperty("webServerTlsCertificatePassword").GetString(); - _webServerRootPath = jsonConfig.webServerRootPath.Value; + _webServerRootPath = jsonConfig.GetProperty("webServerRootPath").GetString(); if (!Path.IsPathRooted(_webServerRootPath)) _webServerRootPath = Path.Combine(_dnsServer.ApplicationFolder, _webServerRootPath); - _serveBlockPageFromWebServerRoot = jsonConfig.serveBlockPageFromWebServerRoot.Value; + _serveBlockPageFromWebServerRoot = jsonConfig.GetProperty("serveBlockPageFromWebServerRoot").GetBoolean(); - string blockPageTitle = jsonConfig.blockPageTitle.Value; - string blockPageHeading = jsonConfig.blockPageHeading.Value; - string blockPageMessage = jsonConfig.blockPageMessage.Value; + string blockPageTitle = jsonConfig.GetProperty("blockPageTitle").GetString(); + string blockPageHeading = jsonConfig.GetProperty("blockPageHeading").GetString(); + string blockPageMessage = jsonConfig.GetProperty("blockPageMessage").GetString(); string blockPageContent = @" @@ -570,7 +567,7 @@ namespace BlockPage _dnsServer.WriteLog(ex); } - if (jsonConfig.webServerUseSelfSignedTlsCertificate is null) + if (!jsonConfig.TryGetProperty("webServerUseSelfSignedTlsCertificate", out _)) { config = config.Replace("\"webServerTlsCertificateFilePath\"", "\"webServerUseSelfSignedTlsCertificate\": true,\r\n \"webServerTlsCertificateFilePath\""); diff --git a/Apps/BlockPageApp/BlockPageApp.csproj b/Apps/BlockPageApp/BlockPageApp.csproj index eb68d450..8992c987 100644 --- a/Apps/BlockPageApp/BlockPageApp.csproj +++ b/Apps/BlockPageApp/BlockPageApp.csproj @@ -3,7 +3,7 @@ net7.0 false - 3.0.1 + 4.0 Technitium Technitium DNS Server Shreyas Zare @@ -26,10 +26,6 @@ - - - - false diff --git a/Apps/Dns64App/App.cs b/Apps/Dns64App/App.cs index 39c9f682..8d2b0dae 100644 --- a/Apps/Dns64App/App.cs +++ b/Apps/Dns64App/App.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -18,12 +18,13 @@ along with this program. If not, see . */ using DnsServerCore.ApplicationCommon; -using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Net; using System.Net.Sockets; +using System.Text.Json; using System.Threading.Tasks; +using TechnitiumLibrary; using TechnitiumLibrary.Net; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; @@ -60,41 +61,27 @@ namespace Dns64 { _dnsServer = dnsServer; - dynamic jsonConfig = JsonConvert.DeserializeObject(config); + using JsonDocument jsonDocument = JsonDocument.Parse(config); + JsonElement jsonConfig = jsonDocument.RootElement; - _enableDns64 = jsonConfig.enableDns64.Value; + _enableDns64 = jsonConfig.GetProperty("enableDns64").GetBoolean(); + _networkGroupMap = jsonConfig.ReadObjectAsMap("networkGroupMap", delegate (string network, JsonElement group) { - Dictionary networkGroupMap = new Dictionary(); + if (!NetworkAddress.TryParse(network, out NetworkAddress networkAddress)) + throw new InvalidOperationException("Network group map contains an invalid network address: " + network); - foreach (dynamic jsonProperty in jsonConfig.networkGroupMap) - { - string network = jsonProperty.Name; - string group = jsonProperty.Value; + if (networkAddress.Address.AddressFamily == AddressFamily.InterNetwork) + throw new InvalidOperationException("Network group map can only have IPv6 network addresses: " + network); - if (!NetworkAddress.TryParse(network, out NetworkAddress networkAddress)) - throw new InvalidOperationException("Network group map contains an invalid network address: " + network); - - if (networkAddress.Address.AddressFamily == AddressFamily.InterNetwork) - throw new InvalidOperationException("Network group map can only have IPv6 network addresses: " + network); - - networkGroupMap.Add(networkAddress, group); - } - - _networkGroupMap = networkGroupMap; - } + return new Tuple(networkAddress, group.GetString()); + }); + _groups = jsonConfig.ReadArrayAsMap("groups", delegate (JsonElement jsonGroup) { - Dictionary groups = new Dictionary(); - - foreach (dynamic jsonGroup in jsonConfig.groups) - { - Group group = new Group(jsonGroup); - groups.Add(group.Name, group); - } - - _groups = groups; - } + Group group = new Group(jsonGroup); + return new Tuple(group.Name, group); + }); return Task.CompletedTask; } @@ -161,13 +148,13 @@ namespace Dns64 if (!synthesizeAAAA) return new DnsDatagram(response.Identifier, true, response.OPCODE, response.AuthoritativeAnswer, response.Truncation, response.RecursionDesired, response.RecursionAvailable, response.AuthenticData, response.CheckingDisabled, response.RCODE, response.Question, newAnswer, response.Authority, response.Additional) { Tag = response.Tag }; - DnsDatagram newResponse = await _dnsServer.DirectQueryAsync(new DnsQuestionRecord(question.Name, DnsResourceRecordType.A, question.Class), 2000); + DnsDatagram newResponse = await _dnsServer.DirectQueryAsync(new DnsQuestionRecord(question.Name, DnsResourceRecordType.A, DnsClass.IN), 2000); uint soaTtl; { DnsResourceRecord soa = response.FindFirstAuthorityRecord(); if ((soa is not null) && (soa.Type == DnsResourceRecordType.SOA)) - soaTtl = soa.TtlValue; + soaTtl = soa.TTL; else soaTtl = 600; } @@ -195,7 +182,7 @@ namespace Dns64 IPAddress ipv6Address = ipv4Address.MapToIPv6(dns64Prefix); - newAnswer.Add(new DnsResourceRecord(answer.Name, DnsResourceRecordType.AAAA, answer.Class, Math.Min(answer.TtlValue, soaTtl), new DnsAAAARecordData(ipv6Address))); + newAnswer.Add(new DnsResourceRecord(answer.Name, DnsResourceRecordType.AAAA, answer.Class, Math.Min(answer.TTL, soaTtl), new DnsAAAARecordData(ipv6Address))); } return new DnsDatagram(response.Identifier, true, response.OPCODE, response.AuthoritativeAnswer, response.Truncation, response.RecursionDesired, response.RecursionAvailable, response.AuthenticData, response.CheckingDisabled, newResponse.RCODE, response.Question, newAnswer, newResponse.Authority, newResponse.Additional) { Tag = response.Tag }; @@ -229,7 +216,7 @@ namespace Dns64 if ((groupName is null) || !_groups.TryGetValue(groupName, out Group group) || !group.EnableDns64) return Task.FromResult(null); - IPAddress ipv6Address = IPAddressExtension.ParseReverseDomain(question.Name); + IPAddress ipv6Address = IPAddressExtensions.ParseReverseDomain(question.Name); if (ipv6Address.AddressFamily != AddressFamily.InterNetworkV6) return Task.FromResult(null); @@ -275,61 +262,48 @@ namespace Dns64 #region constructor - public Group(dynamic jsonGroup) + public Group(JsonElement jsonGroup) { - _name = jsonGroup.name.Value; - _enableDns64 = jsonGroup.enableDns64.Value; + _name = jsonGroup.GetProperty("name").GetString(); + _enableDns64 = jsonGroup.GetProperty("enableDns64").GetBoolean(); + _dns64PrefixMap = jsonGroup.ReadObjectAsMap("dns64PrefixMap", delegate (string strNetwork, JsonElement jsonDns64Prefix) { - Dictionary dns64PrefixMap = new Dictionary(); + string strDns64Prefix = jsonDns64Prefix.GetString(); - foreach (dynamic jsonProperty in jsonGroup.dns64PrefixMap) + NetworkAddress network = NetworkAddress.Parse(strNetwork); + NetworkAddress dns64Prefix = null; + + if (strDns64Prefix is not null) { - string strNetwork = jsonProperty.Name; - string strDns64Prefix = jsonProperty.Value; + dns64Prefix = NetworkAddress.Parse(strDns64Prefix); - NetworkAddress network = NetworkAddress.Parse(strNetwork); - NetworkAddress dns64Prefix = null; - - if (strDns64Prefix is not null) + switch (dns64Prefix.PrefixLength) { - dns64Prefix = NetworkAddress.Parse(strDns64Prefix); + case 32: + case 40: + case 48: + case 56: + case 64: + case 96: + break; - switch (dns64Prefix.PrefixLength) - { - case 32: - case 40: - case 48: - case 56: - case 64: - case 96: - break; - - default: - throw new NotSupportedException("DNS64 prefix can have only the following prefixes: 32, 40, 48, 56, 64, or 96."); - } + default: + throw new NotSupportedException("DNS64 prefix can have only the following prefixes: 32, 40, 48, 56, 64, or 96."); } - - dns64PrefixMap.Add(network, dns64Prefix); } - _dns64PrefixMap = dns64PrefixMap; - } + return new Tuple(network, dns64Prefix); + }); + _excludedIpv6 = jsonGroup.ReadArray("excludedIpv6", delegate (string strNetworkAddress) { - List excludedIpv6 = new List(); + NetworkAddress networkAddress = NetworkAddress.Parse(strNetworkAddress); + if (networkAddress.Address.AddressFamily != AddressFamily.InterNetworkV6) + throw new InvalidOperationException("An IPv6 network address is expected for 'excludedIpv6' array."); - foreach (dynamic jsonItem in jsonGroup.excludedIpv6) - { - NetworkAddress networkAddress = NetworkAddress.Parse(jsonItem.Value); - if (networkAddress.Address.AddressFamily != AddressFamily.InterNetworkV6) - throw new InvalidOperationException("An IPv6 network address is expected for 'excludedIpv6' array."); - - excludedIpv6.Add(networkAddress); - } - - _excludedIpv6 = excludedIpv6; - } + return networkAddress; + }); } #endregion diff --git a/Apps/Dns64App/Dns64App.csproj b/Apps/Dns64App/Dns64App.csproj index 967db31d..4195166c 100644 --- a/Apps/Dns64App/Dns64App.csproj +++ b/Apps/Dns64App/Dns64App.csproj @@ -3,7 +3,7 @@ net7.0 false - 1.0.1 + 2.0 Technitium Technitium DNS Server Shreyas Zare @@ -16,10 +16,6 @@ Library - - - - false @@ -27,6 +23,10 @@ + + ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.dll + false + ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.Net.dll false diff --git a/Apps/DnsBlockListApp/App.cs b/Apps/DnsBlockListApp/App.cs new file mode 100644 index 00000000..d1d13e55 --- /dev/null +++ b/Apps/DnsBlockListApp/App.cs @@ -0,0 +1,813 @@ +/* +Technitium DNS Server +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +using DnsServerCore.ApplicationCommon; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using TechnitiumLibrary; +using TechnitiumLibrary.Net; +using TechnitiumLibrary.Net.Dns; +using TechnitiumLibrary.Net.Dns.ResourceRecords; + +namespace DnsBlockList +{ + //DNS Blacklists and Whitelists + //https://www.rfc-editor.org/rfc/rfc5782 + + public class App : IDnsApplication, IDnsAppRecordRequestHandler + { + #region variables + + IDnsServer _dnsServer; + + IReadOnlyDictionary _dnsBlockLists; + + #endregion + + #region IDisposable + + public void Dispose() + { + if (_dnsBlockLists is not null) + { + foreach (KeyValuePair dnsBlockList in _dnsBlockLists) + dnsBlockList.Value.Dispose(); + + _dnsBlockLists = null; + } + } + + #endregion + + #region private + + private static bool TryParseDnsblDomain(string qName, string appRecordName, out IPAddress address, out string domain) + { + qName = qName.Substring(0, qName.Length - appRecordName.Length - 1); + + string[] parts = qName.Split('.'); + string lastPart = parts[parts.Length - 1]; + + if (byte.TryParse(lastPart, out _) || byte.TryParse(lastPart, NumberStyles.HexNumber, null, out _)) + { + switch (parts.Length) + { + case 4: + { + byte[] buffer = new byte[4]; + + for (int i = 0, j = parts.Length - 1; (i < 4) && (j > -1); i++, j--) + buffer[i] = byte.Parse(parts[j]); + + address = new IPAddress(buffer); + domain = null; + return true; + } + + case 32: + { + byte[] buffer = new byte[16]; + + for (int i = 0, j = parts.Length - 1; (i < 16) && (j > 0); i++, j -= 2) + buffer[i] = (byte)(byte.Parse(parts[j], NumberStyles.HexNumber) << 4 | byte.Parse(parts[j - 1], NumberStyles.HexNumber)); + + address = new IPAddress(buffer); + domain = null; + return true; + } + + default: + address = null; + domain = null; + return false; + } + } + else + { + address = null; + domain = lastPart; + + for (int i = parts.Length - 2; i > -1; i--) + domain = parts[i] + "." + domain; + + return true; + } + } + + private Tuple ReadBlockList(JsonElement jsonBlockList) + { + BlockList blockList; + string name = jsonBlockList.GetProperty("name").GetString(); + BlockListType type = jsonBlockList.GetPropertyEnumValue("type", BlockListType.Ip); + + if ((_dnsBlockLists is not null) && _dnsBlockLists.TryGetValue(name, out BlockList existingBlockList) && (existingBlockList.Type == type)) + { + existingBlockList.ReloadConfig(jsonBlockList); + blockList = existingBlockList; + } + else + { + switch (type) + { + case BlockListType.Ip: + blockList = new IpBlockList(_dnsServer, jsonBlockList); + break; + + case BlockListType.Domain: + blockList = new DomainBlockList(_dnsServer, jsonBlockList); + break; + + default: + throw new NotSupportedException("DNSBL block list type is not supported: " + type.ToString()); + } + } + + return new Tuple(blockList.Name, blockList); + } + + #endregion + + #region public + + public Task InitializeAsync(IDnsServer dnsServer, string config) + { + _dnsServer = dnsServer; + + using JsonDocument jsonDocument = JsonDocument.Parse(config); + JsonElement jsonConfig = jsonDocument.RootElement; + + if (jsonConfig.TryReadArrayAsMap("dnsBlockLists", ReadBlockList, out Dictionary dnsBlockLists)) + { + if (_dnsBlockLists is not null) + { + foreach (KeyValuePair dnsBlockList in _dnsBlockLists) + { + if (!dnsBlockLists.ContainsKey(dnsBlockList.Key)) + dnsBlockList.Value.Dispose(); + } + } + + _dnsBlockLists = dnsBlockLists; + } + else + { + if (_dnsBlockLists is not null) + { + foreach (KeyValuePair dnsBlockList in _dnsBlockLists) + dnsBlockList.Value.Dispose(); + } + + _dnsBlockLists = null; + } + + return Task.CompletedTask; + } + + public async Task ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, string zoneName, string appRecordName, uint appRecordTtl, string appRecordData) + { + DnsQuestionRecord question = request.Question[0]; + string qname = question.Name; + + if (qname.Length == appRecordName.Length) + return null; + + if ((_dnsBlockLists is null) || !TryParseDnsblDomain(qname, appRecordName, out IPAddress address, out string domain)) + return null; + + using JsonDocument jsonDocument = JsonDocument.Parse(appRecordData); + JsonElement jsonAppRecordData = jsonDocument.RootElement; + + if (jsonAppRecordData.TryReadArray("dnsBlockLists", out string[] dnsBlockLists)) + { + bool isBlocked = false; + IPAddress responseA = null; + string responseTXT = null; + + if (address is not null) + { + foreach (string dnsBlockList in dnsBlockLists) + { + if (_dnsBlockLists.TryGetValue(dnsBlockList, out BlockList blockList) && blockList.Enabled && (blockList.Type == BlockListType.Ip) && blockList.IsBlocked(address, out responseA, out responseTXT)) + { + isBlocked = true; + + if (!string.IsNullOrEmpty(responseTXT)) + responseTXT = responseTXT.Replace("{ip}", address.ToString()); + + break; + } + } + } + else if (domain is not null) + { + foreach (string dnsBlockList in dnsBlockLists) + { + if (_dnsBlockLists.TryGetValue(dnsBlockList, out BlockList blockList) && blockList.Enabled && (blockList.Type == BlockListType.Domain) && blockList.IsBlocked(domain, out string foundDomain, out responseA, out responseTXT)) + { + isBlocked = true; + + if (!string.IsNullOrEmpty(responseTXT)) + responseTXT = responseTXT.Replace("{domain}", foundDomain); + + break; + } + } + } + + if (isBlocked) + { + switch (question.Type) + { + case DnsResourceRecordType.A: + return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, true, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question, new DnsResourceRecord[] { new DnsResourceRecord(qname, DnsResourceRecordType.A, question.Class, appRecordTtl, new DnsARecordData(responseA)) }); + + case DnsResourceRecordType.TXT: + if (!string.IsNullOrEmpty(responseTXT)) + return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, true, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question, new DnsResourceRecord[] { new DnsResourceRecord(qname, DnsResourceRecordType.TXT, question.Class, appRecordTtl, new DnsTXTRecordData(responseTXT)) }); + + break; + } + + //NODATA response + DnsDatagram soaResponse = await _dnsServer.DirectQueryAsync(new DnsQuestionRecord(zoneName, DnsResourceRecordType.SOA, DnsClass.IN)); + + return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, true, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question, null, soaResponse.Answer); + } + } + + return null; + } + + #endregion + + #region properties + + public string Description + { get { return "Returns A or TXT records based on the DNS Block Lists (DNSBL) configured in the APP record data. Returns NXDOMAIN response when an IP address or domain name is not blocked in any of the configured blocklists."; } } + + public string ApplicationRecordDataTemplate + { + get + { + return @"{ + ""dnsBlockLists"": [ + ""ipblocklist1"", + ""domainblocklist1"" + ] +}"; + } + } + + #endregion + + enum BlockListType + { + Ip = 1, + Domain = 2 + } + + abstract class BlockList : IDisposable + { + #region variables + + protected static readonly char[] _popWordSeperator = new char[] { ' ', '\t', '|' }; + + protected readonly IDnsServer _dnsServer; + readonly BlockListType _type; + + readonly string _name; + bool _enabled; + protected IPAddress _responseA; + protected string _responseTXT; + protected string _blockListFile; + + protected DateTime _blockListFileLastModified; + + Timer _autoReloadTimer; + const int AUTO_RELOAD_TIMER_INTERVAL = 60000; + + #endregion + + #region constructor + + protected BlockList(IDnsServer dnsServer, BlockListType type, JsonElement jsonBlockList) + { + _dnsServer = dnsServer; + _type = type; + + _name = jsonBlockList.GetProperty("name").GetString(); + + _autoReloadTimer = new Timer(delegate (object state) + { + try + { + DateTime blockListFileLastModified = File.GetLastWriteTimeUtc(_blockListFile); + if (blockListFileLastModified > _blockListFileLastModified) + ReloadBlockListFile(); + } + catch (Exception ex) + { + _dnsServer.WriteLog(ex); + } + finally + { + _autoReloadTimer?.Change(AUTO_RELOAD_TIMER_INTERVAL, Timeout.Infinite); + } + }); + + ReloadConfig(jsonBlockList); + } + + #endregion + + #region IDisposable + + public void Dispose() + { + if (_autoReloadTimer is not null) + { + _autoReloadTimer.Dispose(); + _autoReloadTimer = null; + } + } + + #endregion + + #region protected + + protected abstract void ReloadBlockListFile(); + + protected static string PopWord(ref string line) + { + if (line.Length == 0) + return line; + + line = line.TrimStart(_popWordSeperator); + + int i = line.IndexOfAny(_popWordSeperator); + string word; + + if (i < 0) + { + word = line; + line = ""; + } + else + { + word = line.Substring(0, i); + line = line.Substring(i + 1); + } + + return word; + } + + #endregion + + #region public + + public void ReloadConfig(JsonElement jsonBlockList) + { + _enabled = jsonBlockList.GetPropertyValue("enabled", true); + _responseA = IPAddress.Parse(jsonBlockList.GetPropertyValue("responseA", "127.0.0.2")); + + if (jsonBlockList.TryGetProperty("responseTXT", out JsonElement jsonResponseTXT)) + _responseTXT = jsonResponseTXT.GetString(); + else + _responseTXT = null; + + string blockListFile = jsonBlockList.GetProperty("blockListFile").GetString(); + + if (!Path.IsPathRooted(blockListFile)) + blockListFile = Path.Combine(_dnsServer.ApplicationFolder, blockListFile); + + if (!blockListFile.Equals(_blockListFile)) + { + _blockListFile = blockListFile; + _blockListFileLastModified = default; + } + + _autoReloadTimer.Change(0, Timeout.Infinite); + } + + public virtual bool IsBlocked(IPAddress address, out IPAddress responseA, out string responseTXT) + { + throw new InvalidOperationException(); + } + + public virtual bool IsBlocked(string domain, out string foundDomain, out IPAddress responseA, out string responseTXT) + { + throw new InvalidOperationException(); + } + + #endregion + + #region properties + + public BlockListType Type + { get { return _type; } } + + public string Name + { get { return _name; } } + + public bool Enabled + { get { return _enabled; } } + + public IPAddress ResponseA + { get { return _responseA; } } + + public string ResponseTXT + { get { return _responseTXT; } } + + public string BlockListFile + { get { return _blockListFile; } } + + #endregion + } + + class BlockEntry + { + #region variables + + readonly T _key; + readonly IPAddress _responseA; + readonly string _responseTXT; + + #endregion + + #region constructor + + public BlockEntry(T key, string responseA, string responseTXT) + { + _key = key; + + if (IPAddress.TryParse(responseA, out IPAddress addr)) + _responseA = addr; + + if (!string.IsNullOrEmpty(responseTXT)) + _responseTXT = responseTXT; + } + + #endregion + + #region properties + + public T Key + { get { return _key; } } + + public IPAddress ResponseA + { get { return _responseA; } } + + public string ResponseTXT + { get { return _responseTXT; } } + + #endregion + } + + class IpBlockList : BlockList + { + #region variables + + Dictionary> _ipv4Map; + Dictionary> _ipv6Map; + NetworkMap> _ipv4NetworkMap; + NetworkMap> _ipv6NetworkMap; + + #endregion + + #region constructor + + public IpBlockList(IDnsServer dnsServer, JsonElement jsonBlockList) + : base(dnsServer, BlockListType.Ip, jsonBlockList) + { } + + #endregion + + #region protected + + protected override void ReloadBlockListFile() + { + try + { + _dnsServer.WriteLog("The app is reading IP block list file: " + _blockListFile); + + //parse ip block list file + Queue> ipv4Addresses = new Queue>(); + Queue> ipv6Addresses = new Queue>(); + Queue> ipv4Networks = new Queue>(); + Queue> ipv6Networks = new Queue>(); + + ipv4Addresses.Enqueue(new BlockEntry(IPAddress.Parse("127.0.0.2"), "127.0.0.2", "rfc5782 test entry")); + ipv6Addresses.Enqueue(new BlockEntry(IPAddress.Parse("::FFFF:7F00:2"), "127.0.0.2", "rfc5782 test entry")); + + using (FileStream fS = new FileStream(_blockListFile, FileMode.Open, FileAccess.Read)) + { + StreamReader sR = new StreamReader(fS, true); + string line; + string network; + string responseA; + string responseTXT; + + while (true) + { + line = sR.ReadLine(); + if (line is null) + break; //eof + + line = line.TrimStart(_popWordSeperator); + + if (line.Length == 0) + continue; //skip empty line + + if (line.StartsWith("#")) + continue; //skip comment line + + network = PopWord(ref line); + responseA = PopWord(ref line); + responseTXT = line; + + if (NetworkAddress.TryParse(network, out NetworkAddress networkAddress)) + { + switch (networkAddress.AddressFamily) + { + case AddressFamily.InterNetwork: + if (networkAddress.PrefixLength == 32) + ipv4Addresses.Enqueue(new BlockEntry(networkAddress.Address, responseA, responseTXT)); + else + ipv4Networks.Enqueue(new BlockEntry(networkAddress, responseA, responseTXT)); + + break; + + case AddressFamily.InterNetworkV6: + if (networkAddress.PrefixLength == 128) + ipv6Addresses.Enqueue(new BlockEntry(networkAddress.Address, responseA, responseTXT)); + else + ipv6Networks.Enqueue(new BlockEntry(networkAddress, responseA, responseTXT)); + + break; + } + } + } + + _blockListFileLastModified = File.GetLastWriteTimeUtc(fS.SafeFileHandle); + } + + //load ip lookup list + Dictionary> ipv4AddressMap = new Dictionary>(ipv4Addresses.Count); + + while (ipv4Addresses.Count > 0) + { + BlockEntry entry = ipv4Addresses.Dequeue(); + ipv4AddressMap.TryAdd(entry.Key, entry); + } + + Dictionary> ipv6AddressMap = new Dictionary>(ipv6Addresses.Count); + + while (ipv6Addresses.Count > 0) + { + BlockEntry entry = ipv6Addresses.Dequeue(); + ipv6AddressMap.TryAdd(entry.Key, entry); + } + + NetworkMap> ipv4NetworkMap = new NetworkMap>(ipv4Networks.Count); + + while (ipv4Networks.Count > 0) + { + BlockEntry entry = ipv4Networks.Dequeue(); + ipv4NetworkMap.Add(entry.Key, entry); + } + + NetworkMap> ipv6NetworkMap = new NetworkMap>(ipv6Networks.Count); + + while (ipv6Networks.Count > 0) + { + BlockEntry entry = ipv6Networks.Dequeue(); + ipv6NetworkMap.Add(entry.Key, entry); + } + + //update + _ipv4Map = ipv4AddressMap; + _ipv6Map = ipv6AddressMap; + _ipv4NetworkMap = ipv4NetworkMap; + _ipv6NetworkMap = ipv6NetworkMap; + + _dnsServer.WriteLog("The app has successfully loaded IP block list file: " + _blockListFile); + } + catch (Exception ex) + { + _dnsServer.WriteLog("The app failed to read IP block list file: " + _blockListFile + "\r\n" + ex.ToString()); + } + } + + #endregion + + #region public + + public override bool IsBlocked(IPAddress address, out IPAddress responseA, out string responseTXT) + { + switch (address.AddressFamily) + { + case AddressFamily.InterNetwork: + { + if (_ipv4Map.TryGetValue(address, out BlockEntry ipEntry)) + { + responseA = ipEntry.ResponseA is null ? _responseA : ipEntry.ResponseA; + responseTXT = ipEntry.ResponseTXT is null ? _responseTXT : ipEntry.ResponseTXT; + return true; + } + + if (_ipv4NetworkMap.TryGetValue(address, out BlockEntry networkEntry)) + { + responseA = networkEntry.ResponseA is null ? _responseA : networkEntry.ResponseA; + responseTXT = networkEntry.ResponseTXT is null ? _responseTXT : networkEntry.ResponseTXT; + return true; + } + } + break; + + case AddressFamily.InterNetworkV6: + { + if (_ipv6Map.TryGetValue(address, out BlockEntry ipEntry)) + { + responseA = ipEntry.ResponseA is null ? _responseA : ipEntry.ResponseA; + responseTXT = ipEntry.ResponseTXT is null ? _responseTXT : ipEntry.ResponseTXT; + return true; + } + + if (_ipv6NetworkMap.TryGetValue(address, out BlockEntry networkEntry)) + { + responseA = networkEntry.ResponseA is null ? _responseA : networkEntry.ResponseA; + responseTXT = networkEntry.ResponseTXT is null ? _responseTXT : networkEntry.ResponseTXT; + return true; + } + } + break; + } + + responseA = null; + responseTXT = null; + return false; + } + + #endregion + } + + class DomainBlockList : BlockList + { + #region variables + + Dictionary> _domainMap; + + #endregion + + #region constructor + + public DomainBlockList(IDnsServer dnsServer, JsonElement jsonIpBlockList) + : base(dnsServer, BlockListType.Domain, jsonIpBlockList) + { } + + #endregion + + #region protected + + protected override void ReloadBlockListFile() + { + try + { + _dnsServer.WriteLog("The app is reading domain block list file: " + _blockListFile); + + //parse ip block list file + Queue> domains = new Queue>(); + + domains.Enqueue(new BlockEntry("test", "127.0.0.2", "rfc5782 test entry")); + + using (FileStream fS = new FileStream(_blockListFile, FileMode.Open, FileAccess.Read)) + { + StreamReader sR = new StreamReader(fS, true); + char[] trimSeperator = new char[] { ' ', '\t', ':', '|', ',' }; + string line; + string domain; + string responseA; + string responseTXT; + + while (true) + { + line = sR.ReadLine(); + if (line is null) + break; //eof + + line = line.TrimStart(trimSeperator); + + if (line.Length == 0) + continue; //skip empty line + + if (line.StartsWith("#")) + continue; //skip comment line + + domain = PopWord(ref line); + responseA = PopWord(ref line); + responseTXT = line; + + if (DnsClient.IsDomainNameValid(domain)) + domains.Enqueue(new BlockEntry(domain.ToLower(), responseA, responseTXT)); + } + + _blockListFileLastModified = File.GetLastWriteTimeUtc(fS.SafeFileHandle); + } + + //load ip lookup list + Dictionary> domainMap = new Dictionary>(domains.Count); + + while (domains.Count > 0) + { + BlockEntry entry = domains.Dequeue(); + domainMap.TryAdd(entry.Key, entry); + } + + //update + _domainMap = domainMap; + + _dnsServer.WriteLog("The app has successfully loaded domain block list file: " + _blockListFile); + } + catch (Exception ex) + { + _dnsServer.WriteLog("The app failed to read domain block list file: " + _blockListFile + "\r\n" + ex.ToString()); + } + } + + #endregion + + #region private + + private static string GetParentZone(string domain) + { + int i = domain.IndexOf('.'); + if (i > -1) + return domain.Substring(i + 1); + + //dont return root zone + return null; + } + + private bool IsDomainBlocked(string domain, out BlockEntry domainEntry) + { + do + { + if (_domainMap.TryGetValue(domain, out domainEntry)) + { + return true; + } + + domain = GetParentZone(domain); + } + while (domain is not null); + + return false; + } + + #endregion + + #region public + + public override bool IsBlocked(string domain, out string foundDomain, out IPAddress responseA, out string responseTXT) + { + if (IsDomainBlocked(domain.ToLower(), out BlockEntry domainEntry)) + { + foundDomain = domainEntry.Key; + responseA = domainEntry.ResponseA is null ? _responseA : domainEntry.ResponseA; + responseTXT = domainEntry.ResponseTXT is null ? _responseTXT : domainEntry.ResponseTXT; + return true; + } + + foundDomain = null; + responseA = null; + responseTXT = null; + return false; + } + + #endregion + } + } +} diff --git a/Apps/DnsBlockListApp/DnsBlockListApp.csproj b/Apps/DnsBlockListApp/DnsBlockListApp.csproj new file mode 100644 index 00000000..02c9578a --- /dev/null +++ b/Apps/DnsBlockListApp/DnsBlockListApp.csproj @@ -0,0 +1,48 @@ + + + + net7.0 + false + 1.0 + Technitium + Technitium DNS Server + Shreyas Zare + DnsBlockListApp + DnsBlockList + https://technitium.com/dns/ + https://github.com/TechnitiumSoftware/DnsServer + Allows creating APP records in primary and forwarder zones that can return A or TXT records based on the DNS Block Lists (DNSBL) configured. The implementation is based on RFC 5782. + false + Library + + + + + false + + + + + + ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.dll + false + + + ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.Net.dll + false + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + diff --git a/Apps/DnsBlockListApp/dnsApp.config b/Apps/DnsBlockListApp/dnsApp.config new file mode 100644 index 00000000..0a58cb61 --- /dev/null +++ b/Apps/DnsBlockListApp/dnsApp.config @@ -0,0 +1,20 @@ +{ + "dnsBlockLists": [ + { + "name": "ipblocklist1", + "type": "ip", + "enabled": true, + "responseA": "127.0.0.2", + "responseTXT": "https://example.com/dnsbl?ip={ip}", + "blockListFile": "ip-blocklist.txt" + }, + { + "name": "domainblocklist1", + "type": "domain", + "enabled": true, + "responseA": "127.0.0.2", + "responseTXT": "https://example.com/dnsbl?domain={domain}", + "blockListFile": "domain-blocklist.txt" + } + ] +} \ No newline at end of file diff --git a/Apps/DnsBlockListApp/domain-blocklist.txt b/Apps/DnsBlockListApp/domain-blocklist.txt new file mode 100644 index 00000000..f7fc3007 --- /dev/null +++ b/Apps/DnsBlockListApp/domain-blocklist.txt @@ -0,0 +1,10 @@ +# DNSBL domain block list +# Format: domain A-response TXT-response +# Seperator: , , or char +# +# A-response & TXT-response are optional but A-response must exists when TXT-response is specified +# +# Examples: +# example.com +# example.net 127.0.0.4 +# malware.com 127.0.0.4 malware see: https://example.com/dnsbl?domain={domain} diff --git a/Apps/DnsBlockListApp/ip-blocklist.txt b/Apps/DnsBlockListApp/ip-blocklist.txt new file mode 100644 index 00000000..6da722d1 --- /dev/null +++ b/Apps/DnsBlockListApp/ip-blocklist.txt @@ -0,0 +1,13 @@ +# DNSBL IP block list +# Format: ip/network A-response TXT-response +# Seperator: , , or char +# +# A-response & TXT-response are optional but A-response must exists when TXT-response is specified. +# Supports both IPv4 and IPv6 addresses. +# +# Examples: +# 192.168.1.1 +# 192.168.0.0/24 +# 192.168.2.1 127.0.0.3 +# 10.8.1.0/24 127.0.0.3 malware see: https://example.com/dnsbl?ip={ip} +# 2001:db8::/64 diff --git a/Apps/DropRequestsApp/App.cs b/Apps/DropRequestsApp/App.cs index 724bbbf6..a542f27e 100644 --- a/Apps/DropRequestsApp/App.cs +++ b/Apps/DropRequestsApp/App.cs @@ -18,12 +18,13 @@ along with this program. If not, see . */ using DnsServerCore.ApplicationCommon; -using Newtonsoft.Json; using System; using System.Collections.Generic; using System.IO; using System.Net; +using System.Text.Json; using System.Threading.Tasks; +using TechnitiumLibrary; using TechnitiumLibrary.Net; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; @@ -55,64 +56,32 @@ namespace DropRequests public async Task InitializeAsync(IDnsServer dnsServer, string config) { - dynamic jsonConfig = JsonConvert.DeserializeObject(config); + using JsonDocument jsonDocument = JsonDocument.Parse(config); + JsonElement jsonConfig = jsonDocument.RootElement; - _enableBlocking = jsonConfig.enableBlocking.Value; + _enableBlocking = jsonConfig.GetProperty("enableBlocking").GetBoolean(); - if (jsonConfig.dropMalformedRequests is null) + if (jsonConfig.TryGetProperty("dropMalformedRequests", out JsonElement jsonDropMalformedRequests)) + _dropMalformedRequests = jsonDropMalformedRequests.GetBoolean(); + else _dropMalformedRequests = false; - else - _dropMalformedRequests = jsonConfig.dropMalformedRequests.Value; - - if (jsonConfig.allowedNetworks is null) - { - _allowedNetworks = Array.Empty(); - } - else - { - List allowedNetworks = new List(); - - foreach (dynamic allowedNetwork in jsonConfig.allowedNetworks) - { - allowedNetworks.Add(NetworkAddress.Parse(allowedNetwork.Value)); - } + if (jsonConfig.TryReadArray("allowedNetworks", NetworkAddress.Parse, out NetworkAddress[] allowedNetworks)) _allowedNetworks = allowedNetworks; - } - - if (jsonConfig.blockedNetworks is null) - { - _blockedNetworks = Array.Empty(); - } else - { - List blockedNetworks = new List(); - - foreach (dynamic blockedNetwork in jsonConfig.blockedNetworks) - { - blockedNetworks.Add(NetworkAddress.Parse(blockedNetwork.Value)); - } + _allowedNetworks = Array.Empty(); + if (jsonConfig.TryReadArray("blockedNetworks", NetworkAddress.Parse, out NetworkAddress[] blockedNetworks)) _blockedNetworks = blockedNetworks; - } - - if (jsonConfig.blockedQuestions is null) - { - _blockedQuestions = Array.Empty(); - } else - { - List blockedQuestions = new List(); - - foreach (dynamic blockedQuestion in jsonConfig.blockedQuestions) - { - blockedQuestions.Add(new BlockedQuestion(blockedQuestion)); - } + _blockedNetworks = Array.Empty(); + if (jsonConfig.TryReadArray("blockedQuestions", delegate (JsonElement blockedQuestion) { return new BlockedQuestion(blockedQuestion); }, out BlockedQuestion[] blockedQuestions)) _blockedQuestions = blockedQuestions; - } + else + _blockedQuestions = Array.Empty(); - if (jsonConfig.dropMalformedRequests is null) + if (!jsonConfig.TryGetProperty("dropMalformedRequests", out _)) { config = config.Replace("\"allowedNetworks\"", "\"dropMalformedRequests\": false,\r\n \"allowedNetworks\""); @@ -177,17 +146,15 @@ namespace DropRequests #region constructor - public BlockedQuestion(dynamic jsonQuestion) + public BlockedQuestion(JsonElement jsonQuestion) { - _name = jsonQuestion.name?.Value; - if (_name is not null) - _name = _name.TrimEnd('.'); + if (jsonQuestion.TryGetProperty("name", out JsonElement jsonName)) + _name = jsonName.GetString().TrimEnd('.'); - if (jsonQuestion.blockZone is not null) - _blockZone = jsonQuestion.blockZone.Value; + if (jsonQuestion.TryGetProperty("blockZone", out JsonElement jsonBlockZone)) + _blockZone = jsonBlockZone.GetBoolean(); - string strType = jsonQuestion.type?.Value; - if (!string.IsNullOrEmpty(strType) && Enum.TryParse(strType, true, out DnsResourceRecordType type)) + if (jsonQuestion.TryGetProperty("type", out JsonElement jsonType) && Enum.TryParse(jsonType.GetString(), true, out DnsResourceRecordType type)) _type = type; else _type = DnsResourceRecordType.Unknown; diff --git a/Apps/DropRequestsApp/DropRequestsApp.csproj b/Apps/DropRequestsApp/DropRequestsApp.csproj index 859d7b39..546d56c5 100644 --- a/Apps/DropRequestsApp/DropRequestsApp.csproj +++ b/Apps/DropRequestsApp/DropRequestsApp.csproj @@ -3,7 +3,7 @@ net7.0 false - 3.0.1 + 4.0 Technitium Technitium DNS Server Shreyas Zare @@ -16,10 +16,6 @@ Library - - - - false @@ -27,6 +23,10 @@ + + ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.dll + false + ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.Net.dll false diff --git a/Apps/FailoverApp/Address.cs b/Apps/FailoverApp/Address.cs index cda6b414..38d873cb 100644 --- a/Apps/FailoverApp/Address.cs +++ b/Apps/FailoverApp/Address.cs @@ -18,11 +18,11 @@ along with this program. If not, see . */ using DnsServerCore.ApplicationCommon; -using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Net; using System.Net.Sockets; +using System.Text.Json; using System.Threading.Tasks; using TechnitiumLibrary; using TechnitiumLibrary.Net.Dns; @@ -64,17 +64,14 @@ namespace Failover #region private - private void GetAnswers(dynamic jsonAddresses, DnsQuestionRecord question, uint appRecordTtl, string healthCheck, Uri healthCheckUrl, List answers) + private void GetAnswers(JsonElement jsonAddresses, DnsQuestionRecord question, uint appRecordTtl, string healthCheck, Uri healthCheckUrl, List answers) { - if (jsonAddresses == null) - return; - switch (question.Type) { case DnsResourceRecordType.A: - foreach (dynamic jsonAddress in jsonAddresses) + foreach (JsonElement jsonAddress in jsonAddresses.EnumerateArray()) { - IPAddress address = IPAddress.Parse(jsonAddress.Value); + IPAddress address = IPAddress.Parse(jsonAddress.GetString()); if (address.AddressFamily == AddressFamily.InterNetwork) { @@ -94,9 +91,9 @@ namespace Failover break; case DnsResourceRecordType.AAAA: - foreach (dynamic jsonAddress in jsonAddresses) + foreach (JsonElement jsonAddress in jsonAddresses.EnumerateArray()) { - IPAddress address = IPAddress.Parse(jsonAddress.Value); + IPAddress address = IPAddress.Parse(jsonAddress.GetString()); if (address.AddressFamily == AddressFamily.InterNetworkV6) { @@ -117,14 +114,11 @@ namespace Failover } } - private void GetStatusAnswers(dynamic jsonAddresses, FailoverType type, DnsQuestionRecord question, uint appRecordTtl, string healthCheck, Uri healthCheckUrl, List answers) + private void GetStatusAnswers(JsonElement jsonAddresses, FailoverType type, DnsQuestionRecord question, uint appRecordTtl, string healthCheck, Uri healthCheckUrl, List answers) { - if (jsonAddresses == null) - return; - - foreach (dynamic jsonAddress in jsonAddresses) + foreach (JsonElement jsonAddress in jsonAddresses.EnumerateArray()) { - IPAddress address = IPAddress.Parse(jsonAddress.Value); + IPAddress address = IPAddress.Parse(jsonAddress.GetString()); HealthCheckResponse response = _healthService.QueryStatus(address, healthCheck, healthCheckUrl, false); string text = "app=failover; addressType=" + type.ToString() + "; address=" + address.ToString() + "; healthCheck=" + healthCheck + (healthCheckUrl is null ? "" : "; healthCheckUrl=" + healthCheckUrl.AbsoluteUri) + "; healthStatus=" + response.Status.ToString() + ";"; @@ -145,7 +139,7 @@ namespace Failover if (_healthService is null) _healthService = HealthService.Create(dnsServer); - _healthService.Initialize(JsonConvert.DeserializeObject(config)); + _healthService.Initialize(config); return Task.CompletedTask; } @@ -158,17 +152,18 @@ namespace Failover case DnsResourceRecordType.A: case DnsResourceRecordType.AAAA: { - dynamic jsonAppRecordData = JsonConvert.DeserializeObject(appRecordData); + using JsonDocument jsonDocument = JsonDocument.Parse(appRecordData); + JsonElement jsonAppRecordData = jsonDocument.RootElement; - string healthCheck = jsonAppRecordData.healthCheck?.Value; + string healthCheck = jsonAppRecordData.GetPropertyValue("healthCheck", null); Uri healthCheckUrl = null; if (_healthService.HealthChecks.TryGetValue(healthCheck, out HealthCheck hc) && ((hc.Type == HealthCheckType.Https) || (hc.Type == HealthCheckType.Http)) && (hc.Url is null)) { //read health check url only for http/https type checks and only when app config does not have an url configured - if ((jsonAppRecordData.healthCheckUrl is not null) && (jsonAppRecordData.healthCheckUrl.Value is not null)) + if (jsonAppRecordData.TryGetProperty("healthCheckUrl", out JsonElement jsonHealthCheckUrl) && (jsonHealthCheckUrl.ValueKind != JsonValueKind.Null)) { - healthCheckUrl = new Uri(jsonAppRecordData.healthCheckUrl.Value); + healthCheckUrl = new Uri(jsonHealthCheckUrl.GetString()); } else { @@ -181,19 +176,23 @@ namespace Failover List answers = new List(); - GetAnswers(jsonAppRecordData.primary, question, appRecordTtl, healthCheck, healthCheckUrl, answers); + if (jsonAppRecordData.TryGetProperty("primary", out JsonElement jsonPrimary)) + GetAnswers(jsonPrimary, question, appRecordTtl, healthCheck, healthCheckUrl, answers); + if (answers.Count == 0) { - GetAnswers(jsonAppRecordData.secondary, question, appRecordTtl, healthCheck, healthCheckUrl, answers); + if (jsonAppRecordData.TryGetProperty("secondary", out JsonElement jsonSecondary)) + GetAnswers(jsonSecondary, question, appRecordTtl, healthCheck, healthCheckUrl, answers); + if (answers.Count == 0) { - if (jsonAppRecordData.serverDown is not null) + if (jsonAppRecordData.TryGetProperty("serverDown", out JsonElement jsonServerDown)) { if (question.Type == DnsResourceRecordType.A) { - foreach (dynamic jsonAddress in jsonAppRecordData.serverDown) + foreach (JsonElement jsonAddress in jsonServerDown.EnumerateArray()) { - IPAddress address = IPAddress.Parse(jsonAddress.Value); + IPAddress address = IPAddress.Parse(jsonAddress.GetString()); if (address.AddressFamily == AddressFamily.InterNetwork) answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.A, question.Class, 30, new DnsARecordData(address))); @@ -201,9 +200,9 @@ namespace Failover } else { - foreach (dynamic jsonAddress in jsonAppRecordData.serverDown) + foreach (JsonElement jsonAddress in jsonServerDown.EnumerateArray()) { - IPAddress address = IPAddress.Parse(jsonAddress.Value); + IPAddress address = IPAddress.Parse(jsonAddress.GetString()); if (address.AddressFamily == AddressFamily.InterNetworkV6) answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.AAAA, question.Class, 30, new DnsAAAARecordData(address))); @@ -219,32 +218,27 @@ namespace Failover if (answers.Count > 1) answers.Shuffle(); - return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, answers)); + return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question, answers)); } case DnsResourceRecordType.TXT: { - dynamic jsonAppRecordData = JsonConvert.DeserializeObject(appRecordData); - - bool allowTxtStatus; - - if (jsonAppRecordData.allowTxtStatus == null) - allowTxtStatus = false; - else - allowTxtStatus = jsonAppRecordData.allowTxtStatus.Value; + using JsonDocument jsonDocument = JsonDocument.Parse(appRecordData); + JsonElement jsonAppRecordData = jsonDocument.RootElement; + bool allowTxtStatus = jsonAppRecordData.GetPropertyValue("allowTxtStatus", false); if (!allowTxtStatus) return Task.FromResult(null); - string healthCheck = jsonAppRecordData.healthCheck?.Value; + string healthCheck = jsonAppRecordData.GetPropertyValue("healthCheck", null); Uri healthCheckUrl = null; if (_healthService.HealthChecks.TryGetValue(healthCheck, out HealthCheck hc) && ((hc.Type == HealthCheckType.Https) || (hc.Type == HealthCheckType.Http)) && (hc.Url is null)) { //read health check url only for http/https type checks and only when app config does not have an url configured - if ((jsonAppRecordData.healthCheckUrl is not null) && (jsonAppRecordData.healthCheckUrl.Value is not null)) + if (jsonAppRecordData.TryGetProperty("healthCheckUrl", out JsonElement jsonHealthCheckUrl) && (jsonHealthCheckUrl.ValueKind != JsonValueKind.Null)) { - healthCheckUrl = new Uri(jsonAppRecordData.healthCheckUrl.Value); + healthCheckUrl = new Uri(jsonHealthCheckUrl.GetString()); } else { @@ -257,10 +251,13 @@ namespace Failover List answers = new List(); - GetStatusAnswers(jsonAppRecordData.primary, FailoverType.Primary, question, 30, healthCheck, healthCheckUrl, answers); - GetStatusAnswers(jsonAppRecordData.secondary, FailoverType.Secondary, question, 30, healthCheck, healthCheckUrl, answers); + if (jsonAppRecordData.TryGetProperty("primary", out JsonElement jsonPrimary)) + GetStatusAnswers(jsonPrimary, FailoverType.Primary, question, 30, healthCheck, healthCheckUrl, answers); - return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, answers)); + if (jsonAppRecordData.TryGetProperty("secondary", out JsonElement jsonSecondary)) + GetStatusAnswers(jsonSecondary, FailoverType.Secondary, question, 30, healthCheck, healthCheckUrl, answers); + + return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question, answers)); } default: diff --git a/Apps/FailoverApp/CNAME.cs b/Apps/FailoverApp/CNAME.cs index e6dcd076..c00dff30 100644 --- a/Apps/FailoverApp/CNAME.cs +++ b/Apps/FailoverApp/CNAME.cs @@ -18,11 +18,12 @@ along with this program. If not, see . */ using DnsServerCore.ApplicationCommon; -using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Net; +using System.Text.Json; using System.Threading.Tasks; +using TechnitiumLibrary; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; @@ -119,17 +120,18 @@ namespace Failover { DnsQuestionRecord question = request.Question[0]; - dynamic jsonAppRecordData = JsonConvert.DeserializeObject(appRecordData); + using JsonDocument jsonDocument = JsonDocument.Parse(appRecordData); + JsonElement jsonAppRecordData = jsonDocument.RootElement; - string healthCheck = jsonAppRecordData.healthCheck?.Value; + string healthCheck = jsonAppRecordData.GetPropertyValue("healthCheck", null); Uri healthCheckUrl = null; if (_healthService.HealthChecks.TryGetValue(healthCheck, out HealthCheck hc) && ((hc.Type == HealthCheckType.Https) || (hc.Type == HealthCheckType.Http)) && (hc.Url is null)) { //read health check url only for http/https type checks and only when app config does not have an url configured - if ((jsonAppRecordData.healthCheckUrl is not null) && (jsonAppRecordData.healthCheckUrl.Value is not null)) + if (jsonAppRecordData.TryGetProperty("healthCheckUrl", out JsonElement jsonHealthCheckUrl) && (jsonHealthCheckUrl.ValueKind != JsonValueKind.Null)) { - healthCheckUrl = new Uri(jsonAppRecordData.healthCheckUrl.Value); + healthCheckUrl = new Uri(jsonHealthCheckUrl.GetString()); } else { @@ -140,47 +142,50 @@ namespace Failover } } - IReadOnlyList answers; + IReadOnlyList answers = null; if (question.Type == DnsResourceRecordType.TXT) { - bool allowTxtStatus; - - if (jsonAppRecordData.allowTxtStatus == null) - allowTxtStatus = false; - else - allowTxtStatus = jsonAppRecordData.allowTxtStatus.Value; - + bool allowTxtStatus = jsonAppRecordData.GetPropertyValue("allowTxtStatus", false); if (!allowTxtStatus) return Task.FromResult(null); List txtAnswers = new List(); - GetStatusAnswers(jsonAppRecordData.primary.Value, FailoverType.Primary, question, 30, healthCheck, healthCheckUrl, txtAnswers); + if (jsonAppRecordData.TryGetProperty("primary", out JsonElement jsonPrimary)) + GetStatusAnswers(jsonPrimary.GetString(), FailoverType.Primary, question, 30, healthCheck, healthCheckUrl, txtAnswers); - foreach (dynamic jsonDomain in jsonAppRecordData.secondary) - GetStatusAnswers(jsonDomain.Value, FailoverType.Secondary, question, 30, healthCheck, healthCheckUrl, txtAnswers); + if (jsonAppRecordData.TryGetProperty("secondary", out JsonElement jsonSecondary)) + { + foreach (JsonElement jsonDomain in jsonSecondary.EnumerateArray()) + GetStatusAnswers(jsonDomain.GetString(), FailoverType.Secondary, question, 30, healthCheck, healthCheckUrl, txtAnswers); + } answers = txtAnswers; } else { - answers = GetAnswers(jsonAppRecordData.primary.Value, question, zoneName, appRecordTtl, healthCheck, healthCheckUrl); + if (jsonAppRecordData.TryGetProperty("primary", out JsonElement jsonPrimary)) + answers = GetAnswers(jsonPrimary.GetString(), question, zoneName, appRecordTtl, healthCheck, healthCheckUrl); + if (answers is null) { - foreach (dynamic jsonDomain in jsonAppRecordData.secondary) + if (jsonAppRecordData.TryGetProperty("secondary", out JsonElement jsonSecondary)) { - answers = GetAnswers(jsonDomain.Value, question, zoneName, appRecordTtl, healthCheck, healthCheckUrl); - if (answers is not null) - break; + foreach (JsonElement jsonDomain in jsonSecondary.EnumerateArray()) + { + answers = GetAnswers(jsonDomain.GetString(), question, zoneName, appRecordTtl, healthCheck, healthCheckUrl); + if (answers is not null) + break; + } } if (answers is null) { - if ((jsonAppRecordData.serverDown is null) || (jsonAppRecordData.serverDown.Value is null)) + if (!jsonAppRecordData.TryGetProperty("serverDown", out JsonElement jsonServerDown) || (jsonServerDown.ValueKind == JsonValueKind.Null)) return Task.FromResult(null); - string serverDown = jsonAppRecordData.serverDown.Value; + string serverDown = jsonServerDown.GetString(); if (question.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase)) //check for zone apex answers = new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.ANAME, DnsClass.IN, 30, new DnsANAMERecordData(serverDown)) }; //use ANAME @@ -190,7 +195,7 @@ namespace Failover } } - return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, answers)); + return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question, answers)); } #endregion diff --git a/Apps/FailoverApp/EmailAlert.cs b/Apps/FailoverApp/EmailAlert.cs index dea45264..d45c8d56 100644 --- a/Apps/FailoverApp/EmailAlert.cs +++ b/Apps/FailoverApp/EmailAlert.cs @@ -23,8 +23,10 @@ using System.Collections.Generic; using System.Net; using System.Net.Mail; using System.Text; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using TechnitiumLibrary; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; using TechnitiumLibrary.Net.Mail; @@ -37,7 +39,7 @@ namespace Failover readonly HealthService _service; - string _name; + readonly string _name; bool _enabled; MailAddress[] _alertTo; string _smtpServer; @@ -54,13 +56,15 @@ namespace Failover #region constructor - public EmailAlert(HealthService service, dynamic jsonEmailAlert) + public EmailAlert(HealthService service, JsonElement jsonEmailAlert) { _service = service; _smtpClient = new SmtpClientEx(); _smtpClient.DnsClient = new DnsClientInternal(_service.DnsServer); + _name = jsonEmailAlert.GetPropertyValue("name", "default"); + Reload(jsonEmailAlert); } @@ -98,7 +102,24 @@ namespace Failover { try { - await _smtpClient.SendMailAsync(message); + const int MAX_RETRIES = 3; + const int WAIT_INTERVAL = 30000; + + for (int retries = 0; retries < MAX_RETRIES; retries++) + { + try + { + await _smtpClient.SendMailAsync(message); + break; + } + catch + { + if (retries == MAX_RETRIES - 1) + throw; + + await Task.Delay(WAIT_INTERVAL); + } + } } catch (Exception ex) { @@ -110,71 +131,33 @@ namespace Failover #region public - public void Reload(dynamic jsonEmailAlert) + public void Reload(JsonElement jsonEmailAlert) { - if (jsonEmailAlert.name is null) - _name = "default"; - else - _name = jsonEmailAlert.name.Value; + _enabled = jsonEmailAlert.GetPropertyValue("enabled", false); - if (jsonEmailAlert.enabled is null) - _enabled = false; + if (jsonEmailAlert.TryReadArray("alertTo", delegate (string emailAddress) { return new MailAddress(emailAddress); }, out MailAddress[] alertTo)) + _alertTo = alertTo; else - _enabled = jsonEmailAlert.enabled.Value; - - if (jsonEmailAlert.alertTo is null) - { _alertTo = null; - } - else + + _smtpServer = jsonEmailAlert.GetPropertyValue("smtpServer", null); + _smtpPort = jsonEmailAlert.GetPropertyValue("smtpPort", 25); + _startTls = jsonEmailAlert.GetPropertyValue("startTls", false); + _smtpOverTls = jsonEmailAlert.GetPropertyValue("smtpOverTls", false); + _username = jsonEmailAlert.GetPropertyValue("username", null); + _password = jsonEmailAlert.GetPropertyValue("password", null); + + if (jsonEmailAlert.TryGetProperty("mailFrom", out JsonElement jsonMailFrom)) { - _alertTo = new MailAddress[jsonEmailAlert.alertTo.Count]; - - for (int i = 0; i < _alertTo.Length; i++) - _alertTo[i] = new MailAddress(jsonEmailAlert.alertTo[i].Value); + if (jsonEmailAlert.TryGetProperty("mailFromName", out JsonElement jsonMailFromName)) + _mailFrom = new MailAddress(jsonMailFrom.GetString(), jsonMailFromName.GetString(), Encoding.UTF8); + else + _mailFrom = new MailAddress(jsonMailFrom.GetString()); } - - if (jsonEmailAlert.smtpServer is null) - _smtpServer = null; else - _smtpServer = jsonEmailAlert.smtpServer.Value; - - if (jsonEmailAlert.smtpPort is null) - _smtpPort = 25; - else - _smtpPort = Convert.ToInt32(jsonEmailAlert.smtpPort.Value); - - if (jsonEmailAlert.startTls is null) - _startTls = false; - else - _startTls = jsonEmailAlert.startTls.Value; - - if (jsonEmailAlert.smtpOverTls is null) - _smtpOverTls = false; - else - _smtpOverTls = jsonEmailAlert.smtpOverTls.Value; - - if (jsonEmailAlert.username is null) - _username = null; - else - _username = jsonEmailAlert.username.Value; - - if (jsonEmailAlert.password is null) - _password = null; - else - _password = jsonEmailAlert.password.Value; - - if (jsonEmailAlert.mailFrom is null) { _mailFrom = null; } - else - { - if (jsonEmailAlert.mailFromName is null) - _mailFrom = new MailAddress(jsonEmailAlert.mailFrom.Value); - else - _mailFrom = new MailAddress(jsonEmailAlert.mailFrom.Value, jsonEmailAlert.mailFromName.Value, Encoding.UTF8); - } //update smtp client settings _smtpClient.Host = _smtpServer; diff --git a/Apps/FailoverApp/FailoverApp.csproj b/Apps/FailoverApp/FailoverApp.csproj index 209c7eed..63a84a75 100644 --- a/Apps/FailoverApp/FailoverApp.csproj +++ b/Apps/FailoverApp/FailoverApp.csproj @@ -3,7 +3,7 @@ net7.0 false - 5.1 + 6.0 Technitium Technitium DNS Server Shreyas Zare @@ -11,15 +11,11 @@ Failover https://technitium.com/dns/ https://github.com/TechnitiumSoftware/DnsServer - Allows creating APP records in a primary zone that can return A or AAAA records, or CNAME record based on the health status of the servers. The app supports email alerts and web hooks to relay the health status. + Allows creating APP records in a primary and forwarder zones that can return A or AAAA records, or CNAME record based on the health status of the servers. The app supports email alerts and web hooks to relay the health status. false Library - - - - false diff --git a/Apps/FailoverApp/HealthCheck.cs b/Apps/FailoverApp/HealthCheck.cs index ce202abd..85c02a2d 100644 --- a/Apps/FailoverApp/HealthCheck.cs +++ b/Apps/FailoverApp/HealthCheck.cs @@ -23,6 +23,7 @@ using System.Net; using System.Net.Http; using System.Net.NetworkInformation; using System.Net.Sockets; +using System.Text.Json; using System.Threading.Tasks; using TechnitiumLibrary; using TechnitiumLibrary.Net; @@ -49,7 +50,7 @@ namespace Failover readonly HealthService _service; - string _name; + readonly string _name; HealthCheckType _type; int _interval; int _retries; @@ -66,10 +67,12 @@ namespace Failover #region constructor - public HealthCheck(HealthService service, dynamic jsonHealthCheck) + public HealthCheck(HealthService service, JsonElement jsonHealthCheck) { _service = service; + _name = jsonHealthCheck.GetPropertyValue("name", "default"); + Reload(jsonHealthCheck); } @@ -200,63 +203,25 @@ namespace Failover #region public - public void Reload(dynamic jsonHealthCheck) + public void Reload(JsonElement jsonHealthCheck) { - if (jsonHealthCheck.name is null) - _name = "default"; - else - _name = jsonHealthCheck.name.Value; + _type = Enum.Parse(jsonHealthCheck.GetPropertyValue("type", "Tcp"), true); + _interval = jsonHealthCheck.GetPropertyValue("interval", 60) * 1000; + _retries = jsonHealthCheck.GetPropertyValue("retries", 3); + _timeout = jsonHealthCheck.GetPropertyValue("timeout", 10) * 1000; + _port = jsonHealthCheck.GetPropertyValue("port", 80); - if (jsonHealthCheck.type == null) - _type = HealthCheckType.Tcp; + if (jsonHealthCheck.TryGetProperty("url", out JsonElement jsonUrl) && (jsonUrl.ValueKind != JsonValueKind.Null)) + _url = new Uri(jsonUrl.GetString()); else - _type = Enum.Parse(jsonHealthCheck.type.Value, true); - - if (jsonHealthCheck.interval is null) - _interval = 60000; - else - _interval = Convert.ToInt32(jsonHealthCheck.interval.Value) * 1000; - - if (jsonHealthCheck.retries is null) - _retries = 3; - else - _retries = Convert.ToInt32(jsonHealthCheck.retries.Value); - - if (jsonHealthCheck.timeout is null) - _timeout = 10000; - else - _timeout = Convert.ToInt32(jsonHealthCheck.timeout.Value) * 1000; - - if (jsonHealthCheck.port is null) - _port = 80; - else - _port = Convert.ToInt32(jsonHealthCheck.port.Value); - - if ((jsonHealthCheck.url is null) || (jsonHealthCheck.url.Value is null)) _url = null; - else - _url = new Uri(jsonHealthCheck.url.Value); - string emailAlertName; - - if (jsonHealthCheck.emailAlert is null) - emailAlertName = null; - else - emailAlertName = jsonHealthCheck.emailAlert.Value; - - if ((emailAlertName is not null) && _service.EmailAlerts.TryGetValue(emailAlertName, out EmailAlert emailAlert)) + if (jsonHealthCheck.TryGetProperty("emailAlert", out JsonElement jsonEmailAlert) && _service.EmailAlerts.TryGetValue(jsonEmailAlert.GetString(), out EmailAlert emailAlert)) _emailAlert = emailAlert; else _emailAlert = null; - string webHookName; - - if (jsonHealthCheck.webHook is null) - webHookName = null; - else - webHookName = jsonHealthCheck.webHook.Value; - - if ((webHookName is not null) && _service.WebHooks.TryGetValue(webHookName, out WebHook webHook)) + if (jsonHealthCheck.TryGetProperty("webHook", out JsonElement jsonWebHook) && _service.WebHooks.TryGetValue(jsonWebHook.GetString(), out WebHook webHook)) _webHook = webHook; else _webHook = null; diff --git a/Apps/FailoverApp/HealthService.cs b/Apps/FailoverApp/HealthService.cs index adf55513..99536d0d 100644 --- a/Apps/FailoverApp/HealthService.cs +++ b/Apps/FailoverApp/HealthService.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2021 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -22,7 +22,9 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Net; +using System.Text.Json; using System.Threading; +using TechnitiumLibrary; using TechnitiumLibrary.Net; using TechnitiumLibrary.Net.Dns.ResourceRecords; @@ -178,19 +180,19 @@ namespace Failover #region public - public void Initialize(dynamic jsonConfig) + public void Initialize(string config) { + using JsonDocument jsonDocument = JsonDocument.Parse(config); + JsonElement jsonConfig = jsonDocument.RootElement; + //email alerts { - //add or update email alerts - foreach (dynamic jsonEmailAlert in jsonConfig.emailAlerts) - { - string name; + JsonElement jsonEmailAlerts = jsonConfig.GetProperty("emailAlerts"); - if (jsonEmailAlert.name is null) - name = "default"; - else - name = jsonEmailAlert.name.Value; + //add or update email alerts + foreach (JsonElement jsonEmailAlert in jsonEmailAlerts.EnumerateArray()) + { + string name = jsonEmailAlert.GetPropertyValue("name", "default"); if (_emailAlerts.TryGetValue(name, out EmailAlert existingEmailAlert)) { @@ -201,7 +203,6 @@ namespace Failover { //add EmailAlert emailAlert = new EmailAlert(this, jsonEmailAlert); - _emailAlerts.TryAdd(emailAlert.Name, emailAlert); } } @@ -211,15 +212,9 @@ namespace Failover { bool emailAlertExists = false; - foreach (dynamic jsonEmailAlert in jsonConfig.emailAlerts) + foreach (JsonElement jsonEmailAlert in jsonEmailAlerts.EnumerateArray()) { - string name; - - if (jsonEmailAlert.name is null) - name = "default"; - else - name = jsonEmailAlert.name.Value; - + string name = jsonEmailAlert.GetPropertyValue("name", "default"); if (name == emailAlert.Key) { emailAlertExists = true; @@ -237,15 +232,12 @@ namespace Failover //web hooks { - //add or update email alerts - foreach (dynamic jsonWebHook in jsonConfig.webHooks) - { - string name; + JsonElement jsonWebHooks = jsonConfig.GetProperty("webHooks"); - if (jsonWebHook.name is null) - name = "default"; - else - name = jsonWebHook.name.Value; + //add or update email alerts + foreach (JsonElement jsonWebHook in jsonWebHooks.EnumerateArray()) + { + string name = jsonWebHook.GetPropertyValue("name", "default"); if (_webHooks.TryGetValue(name, out WebHook existingWebHook)) { @@ -256,7 +248,6 @@ namespace Failover { //add WebHook webHook = new WebHook(this, jsonWebHook); - _webHooks.TryAdd(webHook.Name, webHook); } } @@ -266,15 +257,9 @@ namespace Failover { bool webHookExists = false; - foreach (dynamic jsonWebHook in jsonConfig.webHooks) + foreach (JsonElement jsonWebHook in jsonWebHooks.EnumerateArray()) { - string name; - - if (jsonWebHook.name is null) - name = "default"; - else - name = jsonWebHook.name.Value; - + string name = jsonWebHook.GetPropertyValue("name", "default"); if (name == webHook.Key) { webHookExists = true; @@ -292,15 +277,12 @@ namespace Failover //health checks { - //add or update health checks - foreach (dynamic jsonHealthCheck in jsonConfig.healthChecks) - { - string name; + JsonElement jsonHealthChecks = jsonConfig.GetProperty("healthChecks"); - if (jsonHealthCheck.name is null) - name = "default"; - else - name = jsonHealthCheck.name.Value; + //add or update health checks + foreach (JsonElement jsonHealthCheck in jsonHealthChecks.EnumerateArray()) + { + string name = jsonHealthCheck.GetPropertyValue("name", "default"); if (_healthChecks.TryGetValue(name, out HealthCheck existingHealthCheck)) { @@ -311,7 +293,6 @@ namespace Failover { //add HealthCheck healthCheck = new HealthCheck(this, jsonHealthCheck); - _healthChecks.TryAdd(healthCheck.Name, healthCheck); } } @@ -321,15 +302,9 @@ namespace Failover { bool healthCheckExists = false; - foreach (dynamic jsonHealthCheck in jsonConfig.healthChecks) + foreach (JsonElement jsonHealthCheck in jsonHealthChecks.EnumerateArray()) { - string name; - - if (jsonHealthCheck.name is null) - name = "default"; - else - name = jsonHealthCheck.name.Value; - + string name = jsonHealthCheck.GetPropertyValue("name", "default"); if (name == healthCheck.Key) { healthCheckExists = true; @@ -353,14 +328,21 @@ namespace Failover //under maintenance networks _underMaintenance.Clear(); - if (jsonConfig.underMaintenance is not null) + if (jsonConfig.TryGetProperty("underMaintenance", out JsonElement jsonUnderMaintenance)) { - foreach (dynamic jsonNetwork in jsonConfig.underMaintenance) + foreach (JsonElement jsonNetwork in jsonUnderMaintenance.EnumerateArray()) { - string network = jsonNetwork.network.Value; - bool enable = jsonNetwork.enable.Value; + string network = jsonNetwork.GetProperty("network").GetString(); + bool enabled; - _underMaintenance.TryAdd(NetworkAddress.Parse(network), enable); + if (jsonNetwork.TryGetProperty("enabled", out JsonElement jsonEnabled)) + enabled = jsonEnabled.GetBoolean(); + else if (jsonNetwork.TryGetProperty("enable", out JsonElement jsonEnable)) + enabled = jsonEnable.GetBoolean(); + else + enabled = true; + + _underMaintenance.TryAdd(NetworkAddress.Parse(network), enabled); } } } diff --git a/Apps/FailoverApp/WebHook.cs b/Apps/FailoverApp/WebHook.cs index 7aecc7f0..5356c972 100644 --- a/Apps/FailoverApp/WebHook.cs +++ b/Apps/FailoverApp/WebHook.cs @@ -17,14 +17,15 @@ along with this program. If not, see . */ -using Newtonsoft.Json; using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Http; using System.Net.Http.Headers; +using System.Text.Json; using System.Threading.Tasks; +using TechnitiumLibrary; using TechnitiumLibrary.Net.Dns.ResourceRecords; using TechnitiumLibrary.Net.Proxy; @@ -36,7 +37,7 @@ namespace Failover readonly HealthService _service; - string _name; + readonly string _name; bool _enabled; Uri[] _urls; @@ -47,10 +48,12 @@ namespace Failover #region constructor - public WebHook(HealthService service, dynamic jsonWebHook) + public WebHook(HealthService service, JsonElement jsonWebHook) { _service = service; + _name = jsonWebHook.GetPropertyValue("name", "default"); + Reload(jsonWebHook); } @@ -176,29 +179,14 @@ namespace Failover #region public - public void Reload(dynamic jsonWebHook) + public void Reload(JsonElement jsonWebHook) { - if (jsonWebHook.name is null) - _name = "default"; - else - _name = jsonWebHook.name.Value; + _enabled = jsonWebHook.GetPropertyValue("enabled", false); - if (jsonWebHook.enabled is null) - _enabled = false; + if (jsonWebHook.TryReadArray("urls", delegate (string uri) { return new Uri(uri); }, out Uri[] urls)) + _urls = urls; else - _enabled = jsonWebHook.enabled.Value; - - if (jsonWebHook.urls is null) - { _urls = null; - } - else - { - _urls = new Uri[jsonWebHook.urls.Count]; - - for (int i = 0; i < _urls.Length; i++) - _urls[i] = new Uri(jsonWebHook.urls[i].Value); - } ConditionalHttpReload(); } @@ -212,26 +200,17 @@ namespace Failover { using (MemoryStream mS = new MemoryStream()) { - JsonTextWriter jsonWriter = new JsonTextWriter(new StreamWriter(mS)); + Utf8JsonWriter jsonWriter = new Utf8JsonWriter(mS); jsonWriter.WriteStartObject(); - jsonWriter.WritePropertyName("address"); - jsonWriter.WriteValue(address.ToString()); - - jsonWriter.WritePropertyName("healthCheck"); - jsonWriter.WriteValue(healthCheck); - - jsonWriter.WritePropertyName("status"); - jsonWriter.WriteValue(healthCheckResponse.Status.ToString()); + jsonWriter.WriteString("address", address.ToString()); + jsonWriter.WriteString("healthCheck", healthCheck); + jsonWriter.WriteString("status", healthCheckResponse.Status.ToString()); if (healthCheckResponse.Status == HealthStatus.Failed) - { - jsonWriter.WritePropertyName("failureReason"); - jsonWriter.WriteValue(healthCheckResponse.FailureReason); - } + jsonWriter.WriteString("failureReason", healthCheckResponse.FailureReason); - jsonWriter.WritePropertyName("dateTime"); - jsonWriter.WriteValue(healthCheckResponse.DateTime); + jsonWriter.WriteString("dateTime", healthCheckResponse.DateTime); jsonWriter.WriteEndObject(); jsonWriter.Flush(); @@ -253,23 +232,14 @@ namespace Failover { using (MemoryStream mS = new MemoryStream()) { - JsonTextWriter jsonWriter = new JsonTextWriter(new StreamWriter(mS)); + Utf8JsonWriter jsonWriter = new Utf8JsonWriter(mS); jsonWriter.WriteStartObject(); - jsonWriter.WritePropertyName("address"); - jsonWriter.WriteValue(address.ToString()); - - jsonWriter.WritePropertyName("healthCheck"); - jsonWriter.WriteValue(healthCheck); - - jsonWriter.WritePropertyName("status"); - jsonWriter.WriteValue("Error"); - - jsonWriter.WritePropertyName("failureReason"); - jsonWriter.WriteValue(ex.ToString()); - - jsonWriter.WritePropertyName("dateTime"); - jsonWriter.WriteValue(DateTime.UtcNow); + jsonWriter.WriteString("address", address.ToString()); + jsonWriter.WriteString("healthCheck", healthCheck); + jsonWriter.WriteString("status", "Error"); + jsonWriter.WriteString("failureReason", ex.ToString()); + jsonWriter.WriteString("dateTime", DateTime.UtcNow); jsonWriter.WriteEndObject(); jsonWriter.Flush(); @@ -291,29 +261,18 @@ namespace Failover { using (MemoryStream mS = new MemoryStream()) { - JsonTextWriter jsonWriter = new JsonTextWriter(new StreamWriter(mS)); + Utf8JsonWriter jsonWriter = new Utf8JsonWriter(mS); jsonWriter.WriteStartObject(); - jsonWriter.WritePropertyName("domain"); - jsonWriter.WriteValue(domain); - - jsonWriter.WritePropertyName("recordType"); - jsonWriter.WriteValue(type.ToString()); - - jsonWriter.WritePropertyName("healthCheck"); - jsonWriter.WriteValue(healthCheck); - - jsonWriter.WritePropertyName("status"); - jsonWriter.WriteValue(healthCheckResponse.Status.ToString()); + jsonWriter.WriteString("domain", domain); + jsonWriter.WriteString("recordType", type.ToString()); + jsonWriter.WriteString("healthCheck", healthCheck); + jsonWriter.WriteString("status", healthCheckResponse.Status.ToString()); if (healthCheckResponse.Status == HealthStatus.Failed) - { - jsonWriter.WritePropertyName("failureReason"); - jsonWriter.WriteValue(healthCheckResponse.FailureReason); - } + jsonWriter.WriteString("failureReason", healthCheckResponse.FailureReason); - jsonWriter.WritePropertyName("dateTime"); - jsonWriter.WriteValue(healthCheckResponse.DateTime); + jsonWriter.WriteString("dateTime", healthCheckResponse.DateTime); jsonWriter.WriteEndObject(); jsonWriter.Flush(); @@ -335,26 +294,15 @@ namespace Failover { using (MemoryStream mS = new MemoryStream()) { - JsonTextWriter jsonWriter = new JsonTextWriter(new StreamWriter(mS)); + Utf8JsonWriter jsonWriter = new Utf8JsonWriter(mS); jsonWriter.WriteStartObject(); - jsonWriter.WritePropertyName("domain"); - jsonWriter.WriteValue(domain); - - jsonWriter.WritePropertyName("recordType"); - jsonWriter.WriteValue(type.ToString()); - - jsonWriter.WritePropertyName("healthCheck"); - jsonWriter.WriteValue(healthCheck); - - jsonWriter.WritePropertyName("status"); - jsonWriter.WriteValue("Error"); - - jsonWriter.WritePropertyName("failureReason"); - jsonWriter.WriteValue(ex.ToString()); - - jsonWriter.WritePropertyName("dateTime"); - jsonWriter.WriteValue(DateTime.UtcNow); + jsonWriter.WriteString("domain", domain); + jsonWriter.WriteString("recordType", type.ToString()); + jsonWriter.WriteString("healthCheck", healthCheck); + jsonWriter.WriteString("status", "Error"); + jsonWriter.WriteString("failureReason", ex.ToString()); + jsonWriter.WriteString("dateTime", DateTime.UtcNow); jsonWriter.WriteEndObject(); jsonWriter.Flush(); diff --git a/Apps/FailoverApp/dnsApp.config b/Apps/FailoverApp/dnsApp.config index f4954e9b..ced9d001 100644 --- a/Apps/FailoverApp/dnsApp.config +++ b/Apps/FailoverApp/dnsApp.config @@ -89,11 +89,11 @@ "underMaintenance": [ { "network": "192.168.10.2/32", - "enable": false + "enabled": false }, { "network": "10.1.1.0/24", - "enable": false + "enabled": false } ] } \ No newline at end of file diff --git a/Apps/GeoContinentApp/Address.cs b/Apps/GeoContinentApp/Address.cs index 65cd833c..38ed82d2 100644 --- a/Apps/GeoContinentApp/Address.cs +++ b/Apps/GeoContinentApp/Address.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -19,10 +19,10 @@ along with this program. If not, see . using DnsServerCore.ApplicationCommon; using MaxMind.GeoIP2.Responses; -using Newtonsoft.Json; using System.Collections.Generic; using System.Net; using System.Net.Sockets; +using System.Text.Json; using System.Threading.Tasks; using TechnitiumLibrary; using TechnitiumLibrary.Net.Dns; @@ -82,86 +82,87 @@ namespace GeoContinent { case DnsResourceRecordType.A: case DnsResourceRecordType.AAAA: - dynamic jsonAppRecordData = JsonConvert.DeserializeObject(appRecordData); - dynamic jsonContinent = null; - - bool ecsUsed = false; - EDnsClientSubnetOptionData requestECS = request.GetEDnsClientSubnetOption(); - if (requestECS is not null) + using (JsonDocument jsonDocument = JsonDocument.Parse(appRecordData)) { - if (_maxMind.DatabaseReader.TryCountry(requestECS.Address, out CountryResponse csResponse)) + JsonElement jsonAppRecordData = jsonDocument.RootElement; + JsonElement jsonContinent = default; + + bool ecsUsed = false; + EDnsClientSubnetOptionData requestECS = request.GetEDnsClientSubnetOption(); + if (requestECS is not null) { - ecsUsed = true; - jsonContinent = jsonAppRecordData[csResponse.Continent.Code]; - if (jsonContinent is null) - jsonContinent = jsonAppRecordData["default"]; + if (_maxMind.DatabaseReader.TryCountry(requestECS.Address, out CountryResponse csResponse)) + { + ecsUsed = true; + if (!jsonAppRecordData.TryGetProperty(csResponse.Continent.Code, out jsonContinent)) + jsonAppRecordData.TryGetProperty("default", out jsonContinent); + } } - } - if (jsonContinent is null) - { - if (_maxMind.DatabaseReader.TryCountry(remoteEP.Address, out CountryResponse response)) + if (jsonContinent.ValueKind == JsonValueKind.Undefined) { - jsonContinent = jsonAppRecordData[response.Continent.Code]; - if (jsonContinent is null) - jsonContinent = jsonAppRecordData["default"]; + if (_maxMind.DatabaseReader.TryCountry(remoteEP.Address, out CountryResponse response)) + { + if (!jsonAppRecordData.TryGetProperty(response.Continent.Code, out jsonContinent)) + jsonAppRecordData.TryGetProperty("default", out jsonContinent); + } + else + { + jsonAppRecordData.TryGetProperty("default", out jsonContinent); + } + + if (jsonContinent.ValueKind == JsonValueKind.Undefined) + return Task.FromResult(null); + } + + List answers = new List(); + + switch (question.Type) + { + case DnsResourceRecordType.A: + foreach (JsonElement jsonAddress in jsonContinent.EnumerateArray()) + { + IPAddress address = IPAddress.Parse(jsonAddress.GetString()); + + if (address.AddressFamily == AddressFamily.InterNetwork) + answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.A, DnsClass.IN, appRecordTtl, new DnsARecordData(address))); + } + break; + + case DnsResourceRecordType.AAAA: + foreach (JsonElement jsonAddress in jsonContinent.EnumerateArray()) + { + IPAddress address = IPAddress.Parse(jsonAddress.GetString()); + + if (address.AddressFamily == AddressFamily.InterNetworkV6) + answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.AAAA, DnsClass.IN, appRecordTtl, new DnsAAAARecordData(address))); + } + break; + } + + if (answers.Count == 0) + return Task.FromResult(null); + + if (answers.Count > 1) + answers.Shuffle(); + + EDnsOption[] options; + + if (requestECS is null) + { + options = null; } else { - jsonContinent = jsonAppRecordData["default"]; + if (ecsUsed) + options = EDnsClientSubnetOptionData.GetEDnsClientSubnetOption(requestECS.SourcePrefixLength, requestECS.SourcePrefixLength, requestECS.Address); + else + options = EDnsClientSubnetOptionData.GetEDnsClientSubnetOption(requestECS.SourcePrefixLength, 0, requestECS.Address); } + + return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question, answers, null, null, _dnsServer.UdpPayloadSize, EDnsHeaderFlags.None, options)); } - if (jsonContinent is null) - return Task.FromResult(null); - - List answers = new List(); - - switch (question.Type) - { - case DnsResourceRecordType.A: - foreach (dynamic jsonAddress in jsonContinent) - { - IPAddress address = IPAddress.Parse(jsonAddress.Value); - - if (address.AddressFamily == AddressFamily.InterNetwork) - answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.A, DnsClass.IN, appRecordTtl, new DnsARecordData(address))); - } - break; - - case DnsResourceRecordType.AAAA: - foreach (dynamic jsonAddress in jsonContinent) - { - IPAddress address = IPAddress.Parse(jsonAddress.Value); - - if (address.AddressFamily == AddressFamily.InterNetworkV6) - answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.AAAA, DnsClass.IN, appRecordTtl, new DnsAAAARecordData(address))); - } - break; - } - - if (answers.Count == 0) - return Task.FromResult(null); - - if (answers.Count > 1) - answers.Shuffle(); - - EDnsOption[] options; - - if (requestECS is null) - { - options = null; - } - else - { - if (ecsUsed) - options = EDnsClientSubnetOptionData.GetEDnsClientSubnetOption(requestECS.SourcePrefixLength, requestECS.SourcePrefixLength, requestECS.AddressValue); - else - options = EDnsClientSubnetOptionData.GetEDnsClientSubnetOption(requestECS.SourcePrefixLength, 0, requestECS.AddressValue); - } - - return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, answers, null, null, _dnsServer.UdpPayloadSize, EDnsHeaderFlags.None, options)); - default: return Task.FromResult(null); } diff --git a/Apps/GeoContinentApp/CNAME.cs b/Apps/GeoContinentApp/CNAME.cs index 1893360b..b0dbfb6a 100644 --- a/Apps/GeoContinentApp/CNAME.cs +++ b/Apps/GeoContinentApp/CNAME.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -19,10 +19,10 @@ along with this program. If not, see . using DnsServerCore.ApplicationCommon; using MaxMind.GeoIP2.Responses; -using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Net; +using System.Text.Json; using System.Threading.Tasks; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.EDnsOptions; @@ -76,8 +76,9 @@ namespace GeoContinent public Task ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, string zoneName, string appRecordName, uint appRecordTtl, string appRecordData) { - dynamic jsonAppRecordData = JsonConvert.DeserializeObject(appRecordData); - dynamic jsonContinent = null; + using JsonDocument jsonDocument = JsonDocument.Parse(appRecordData); + JsonElement jsonAppRecordData = jsonDocument.RootElement; + JsonElement jsonContinent = default; bool ecsUsed = false; EDnsClientSubnetOptionData requestECS = request.GetEDnsClientSubnetOption(); @@ -86,30 +87,28 @@ namespace GeoContinent if (_maxMind.DatabaseReader.TryCountry(requestECS.Address, out CountryResponse csResponse)) { ecsUsed = true; - jsonContinent = jsonAppRecordData[csResponse.Continent.Code]; - if (jsonContinent is null) - jsonContinent = jsonAppRecordData["default"]; + if (!jsonAppRecordData.TryGetProperty(csResponse.Continent.Code, out jsonContinent)) + jsonAppRecordData.TryGetProperty("default", out jsonContinent); } } - if (jsonContinent is null) + if (jsonContinent.ValueKind == JsonValueKind.Undefined) { if (_maxMind.DatabaseReader.TryCountry(remoteEP.Address, out CountryResponse response)) { - jsonContinent = jsonAppRecordData[response.Continent.Code]; - if (jsonContinent is null) - jsonContinent = jsonAppRecordData["default"]; + if (!jsonAppRecordData.TryGetProperty(response.Continent.Code, out jsonContinent)) + jsonAppRecordData.TryGetProperty("default", out jsonContinent); } else { - jsonContinent = jsonAppRecordData["default"]; + jsonAppRecordData.TryGetProperty("default", out jsonContinent); } + + if (jsonContinent.ValueKind == JsonValueKind.Undefined) + return Task.FromResult(null); } - if (jsonContinent is null) - return Task.FromResult(null); - - string cname = jsonContinent.Value; + string cname = jsonContinent.GetString(); if (string.IsNullOrEmpty(cname)) return Task.FromResult(null); @@ -129,12 +128,12 @@ namespace GeoContinent else { if (ecsUsed) - options = EDnsClientSubnetOptionData.GetEDnsClientSubnetOption(requestECS.SourcePrefixLength, requestECS.SourcePrefixLength, requestECS.AddressValue); + options = EDnsClientSubnetOptionData.GetEDnsClientSubnetOption(requestECS.SourcePrefixLength, requestECS.SourcePrefixLength, requestECS.Address); else - options = EDnsClientSubnetOptionData.GetEDnsClientSubnetOption(requestECS.SourcePrefixLength, 0, requestECS.AddressValue); + options = EDnsClientSubnetOptionData.GetEDnsClientSubnetOption(requestECS.SourcePrefixLength, 0, requestECS.Address); } - return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, answers, null, null, _dnsServer.UdpPayloadSize, EDnsHeaderFlags.None, options)); + return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question, answers, null, null, _dnsServer.UdpPayloadSize, EDnsHeaderFlags.None, options)); } #endregion diff --git a/Apps/GeoContinentApp/GeoContinentApp.csproj b/Apps/GeoContinentApp/GeoContinentApp.csproj index fae5dc65..6b6cf7cf 100644 --- a/Apps/GeoContinentApp/GeoContinentApp.csproj +++ b/Apps/GeoContinentApp/GeoContinentApp.csproj @@ -4,7 +4,7 @@ net7.0 false true - 5.0.1 + 6.0 Technitium Technitium DNS Server Shreyas Zare @@ -12,14 +12,13 @@ GeoContinent https://technitium.com/dns/ https://github.com/TechnitiumSoftware/DnsServer - Allows creating APP records in a primary zone that can return A or AAAA records, or CNAME record based on the continent the client queries from using MaxMind GeoIP2 Country database. Supports EDNS Client Subnet (ECS). This app requires MaxMind GeoIP2 database and includes the GeoLite2 version for trial. \n\nTo update the MaxMind GeoIP2 database for your app, download the GeoIP2-Country.mmdb file from MaxMind and zip it. Use the zip file with the manual Update option. + Allows creating APP records in a primary and forwarder zones that can return A or AAAA records, or CNAME record based on the continent the client queries from using MaxMind GeoIP2 Country database. Supports EDNS Client Subnet (ECS). This app requires MaxMind GeoIP2 database and includes the GeoLite2 version for trial. \n\nTo update the MaxMind GeoIP2 database for your app, download the GeoIP2-Country.mmdb file from MaxMind and zip it. Use the zip file with the manual Update option. false Library - diff --git a/Apps/GeoCountryApp/Address.cs b/Apps/GeoCountryApp/Address.cs index ade2a668..c1990b95 100644 --- a/Apps/GeoCountryApp/Address.cs +++ b/Apps/GeoCountryApp/Address.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -19,10 +19,10 @@ along with this program. If not, see . using DnsServerCore.ApplicationCommon; using MaxMind.GeoIP2.Responses; -using Newtonsoft.Json; using System.Collections.Generic; using System.Net; using System.Net.Sockets; +using System.Text.Json; using System.Threading.Tasks; using TechnitiumLibrary; using TechnitiumLibrary.Net.Dns; @@ -82,86 +82,87 @@ namespace GeoCountry { case DnsResourceRecordType.A: case DnsResourceRecordType.AAAA: - dynamic jsonAppRecordData = JsonConvert.DeserializeObject(appRecordData); - dynamic jsonCountry = null; - - bool ecsUsed = false; - EDnsClientSubnetOptionData requestECS = request.GetEDnsClientSubnetOption(); - if (requestECS is not null) + using (JsonDocument jsonDocument = JsonDocument.Parse(appRecordData)) { - if (_maxMind.DatabaseReader.TryCountry(requestECS.Address, out CountryResponse csResponse)) + JsonElement jsonAppRecordData = jsonDocument.RootElement; + JsonElement jsonCountry = default; + + bool ecsUsed = false; + EDnsClientSubnetOptionData requestECS = request.GetEDnsClientSubnetOption(); + if (requestECS is not null) { - ecsUsed = true; - jsonCountry = jsonAppRecordData[csResponse.Country.IsoCode]; - if (jsonCountry is null) - jsonCountry = jsonAppRecordData["default"]; + if (_maxMind.DatabaseReader.TryCountry(requestECS.Address, out CountryResponse csResponse)) + { + ecsUsed = true; + if (!jsonAppRecordData.TryGetProperty(csResponse.Country.IsoCode, out jsonCountry)) + jsonAppRecordData.TryGetProperty("default", out jsonCountry); + } } - } - if (jsonCountry is null) - { - if (_maxMind.DatabaseReader.TryCountry(remoteEP.Address, out CountryResponse response)) + if (jsonCountry.ValueKind == JsonValueKind.Undefined) { - jsonCountry = jsonAppRecordData[response.Country.IsoCode]; - if (jsonCountry is null) - jsonCountry = jsonAppRecordData["default"]; + if (_maxMind.DatabaseReader.TryCountry(remoteEP.Address, out CountryResponse response)) + { + if (!jsonAppRecordData.TryGetProperty(response.Country.IsoCode, out jsonCountry)) + jsonAppRecordData.TryGetProperty("default", out jsonCountry); + } + else + { + jsonAppRecordData.TryGetProperty("default", out jsonCountry); + } + + if (jsonCountry.ValueKind == JsonValueKind.Undefined) + return Task.FromResult(null); + } + + List answers = new List(); + + switch (question.Type) + { + case DnsResourceRecordType.A: + foreach (JsonElement jsonAddress in jsonCountry.EnumerateArray()) + { + IPAddress address = IPAddress.Parse(jsonAddress.GetString()); + + if (address.AddressFamily == AddressFamily.InterNetwork) + answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.A, DnsClass.IN, appRecordTtl, new DnsARecordData(address))); + } + break; + + case DnsResourceRecordType.AAAA: + foreach (JsonElement jsonAddress in jsonCountry.EnumerateArray()) + { + IPAddress address = IPAddress.Parse(jsonAddress.GetString()); + + if (address.AddressFamily == AddressFamily.InterNetworkV6) + answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.AAAA, DnsClass.IN, appRecordTtl, new DnsAAAARecordData(address))); + } + break; + } + + if (answers.Count == 0) + return Task.FromResult(null); + + if (answers.Count > 1) + answers.Shuffle(); + + EDnsOption[] options; + + if (requestECS is null) + { + options = null; } else { - jsonCountry = jsonAppRecordData["default"]; + if (ecsUsed) + options = EDnsClientSubnetOptionData.GetEDnsClientSubnetOption(requestECS.SourcePrefixLength, requestECS.SourcePrefixLength, requestECS.Address); + else + options = EDnsClientSubnetOptionData.GetEDnsClientSubnetOption(requestECS.SourcePrefixLength, 0, requestECS.Address); } + + return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question, answers, null, null, _dnsServer.UdpPayloadSize, EDnsHeaderFlags.None, options)); } - if (jsonCountry is null) - return Task.FromResult(null); - - List answers = new List(); - - switch (question.Type) - { - case DnsResourceRecordType.A: - foreach (dynamic jsonAddress in jsonCountry) - { - IPAddress address = IPAddress.Parse(jsonAddress.Value); - - if (address.AddressFamily == AddressFamily.InterNetwork) - answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.A, DnsClass.IN, appRecordTtl, new DnsARecordData(address))); - } - break; - - case DnsResourceRecordType.AAAA: - foreach (dynamic jsonAddress in jsonCountry) - { - IPAddress address = IPAddress.Parse(jsonAddress.Value); - - if (address.AddressFamily == AddressFamily.InterNetworkV6) - answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.AAAA, DnsClass.IN, appRecordTtl, new DnsAAAARecordData(address))); - } - break; - } - - if (answers.Count == 0) - return Task.FromResult(null); - - if (answers.Count > 1) - answers.Shuffle(); - - EDnsOption[] options; - - if (requestECS is null) - { - options = null; - } - else - { - if (ecsUsed) - options = EDnsClientSubnetOptionData.GetEDnsClientSubnetOption(requestECS.SourcePrefixLength, requestECS.SourcePrefixLength, requestECS.AddressValue); - else - options = EDnsClientSubnetOptionData.GetEDnsClientSubnetOption(requestECS.SourcePrefixLength, 0, requestECS.AddressValue); - } - - return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, answers, null, null, _dnsServer.UdpPayloadSize, EDnsHeaderFlags.None, options)); - default: return Task.FromResult(null); } diff --git a/Apps/GeoCountryApp/CNAME.cs b/Apps/GeoCountryApp/CNAME.cs index 237b06e3..4c2cc371 100644 --- a/Apps/GeoCountryApp/CNAME.cs +++ b/Apps/GeoCountryApp/CNAME.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -19,10 +19,10 @@ along with this program. If not, see . using DnsServerCore.ApplicationCommon; using MaxMind.GeoIP2.Responses; -using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Net; +using System.Text.Json; using System.Threading.Tasks; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.EDnsOptions; @@ -76,8 +76,9 @@ namespace GeoCountry public Task ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, string zoneName, string appRecordName, uint appRecordTtl, string appRecordData) { - dynamic jsonAppRecordData = JsonConvert.DeserializeObject(appRecordData); - dynamic jsonCountry = null; + using JsonDocument jsonDocument = JsonDocument.Parse(appRecordData); + JsonElement jsonAppRecordData = jsonDocument.RootElement; + JsonElement jsonCountry = default; bool ecsUsed = false; EDnsClientSubnetOptionData requestECS = request.GetEDnsClientSubnetOption(); @@ -86,30 +87,28 @@ namespace GeoCountry if (_maxMind.DatabaseReader.TryCountry(requestECS.Address, out CountryResponse csResponse)) { ecsUsed = true; - jsonCountry = jsonAppRecordData[csResponse.Country.IsoCode]; - if (jsonCountry is null) - jsonCountry = jsonAppRecordData["default"]; + if (!jsonAppRecordData.TryGetProperty(csResponse.Country.IsoCode, out jsonCountry)) + jsonAppRecordData.TryGetProperty("default", out jsonCountry); } } - if (jsonCountry is null) + if (jsonCountry.ValueKind == JsonValueKind.Undefined) { if (_maxMind.DatabaseReader.TryCountry(remoteEP.Address, out CountryResponse response)) { - jsonCountry = jsonAppRecordData[response.Country.IsoCode]; - if (jsonCountry is null) - jsonCountry = jsonAppRecordData["default"]; + if (!jsonAppRecordData.TryGetProperty(response.Country.IsoCode, out jsonCountry)) + jsonAppRecordData.TryGetProperty("default", out jsonCountry); } else { - jsonCountry = jsonAppRecordData["default"]; + jsonAppRecordData.TryGetProperty("default", out jsonCountry); } + + if (jsonCountry.ValueKind == JsonValueKind.Undefined) + return Task.FromResult(null); } - if (jsonCountry is null) - return Task.FromResult(null); - - string cname = jsonCountry.Value; + string cname = jsonCountry.GetString(); if (string.IsNullOrEmpty(cname)) return Task.FromResult(null); @@ -129,12 +128,12 @@ namespace GeoCountry else { if (ecsUsed) - options = EDnsClientSubnetOptionData.GetEDnsClientSubnetOption(requestECS.SourcePrefixLength, requestECS.SourcePrefixLength, requestECS.AddressValue); + options = EDnsClientSubnetOptionData.GetEDnsClientSubnetOption(requestECS.SourcePrefixLength, requestECS.SourcePrefixLength, requestECS.Address); else - options = EDnsClientSubnetOptionData.GetEDnsClientSubnetOption(requestECS.SourcePrefixLength, 0, requestECS.AddressValue); + options = EDnsClientSubnetOptionData.GetEDnsClientSubnetOption(requestECS.SourcePrefixLength, 0, requestECS.Address); } - return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, answers, null, null, _dnsServer.UdpPayloadSize, EDnsHeaderFlags.None, options)); + return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question, answers, null, null, _dnsServer.UdpPayloadSize, EDnsHeaderFlags.None, options)); } #endregion diff --git a/Apps/GeoCountryApp/GeoCountryApp.csproj b/Apps/GeoCountryApp/GeoCountryApp.csproj index a54fd377..1fec56aa 100644 --- a/Apps/GeoCountryApp/GeoCountryApp.csproj +++ b/Apps/GeoCountryApp/GeoCountryApp.csproj @@ -4,7 +4,7 @@ net7.0 false true - 5.0.1 + 6.0 Technitium Technitium DNS Server Shreyas Zare @@ -12,14 +12,13 @@ GeoCountry https://technitium.com/dns/ https://github.com/TechnitiumSoftware/DnsServer - Allows creating APP records in a primary zone that can return A or AAAA records, or CNAME record based on the country the client queries from using MaxMind GeoIP2 Country database. Supports EDNS Client Subnet (ECS). This app requires MaxMind GeoIP2 database and includes the GeoLite2 version for trial. \n\nTo update the MaxMind GeoIP2 database for your app, download the GeoIP2-Country.mmdb file from MaxMind and zip it. Use the zip file with the manual Update option. + Allows creating APP records in a primary and forwarder zones that can return A or AAAA records, or CNAME record based on the country the client queries from using MaxMind GeoIP2 Country database. Supports EDNS Client Subnet (ECS). This app requires MaxMind GeoIP2 database and includes the GeoLite2 version for trial. \n\nTo update the MaxMind GeoIP2 database for your app, download the GeoIP2-Country.mmdb file from MaxMind and zip it. Use the zip file with the manual Update option. false Library - diff --git a/Apps/GeoDistanceApp/Address.cs b/Apps/GeoDistanceApp/Address.cs index 3e4c5dd3..be6ebaca 100644 --- a/Apps/GeoDistanceApp/Address.cs +++ b/Apps/GeoDistanceApp/Address.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -20,11 +20,11 @@ along with this program. If not, see . using DnsServerCore.ApplicationCommon; using MaxMind.GeoIP2.Model; using MaxMind.GeoIP2.Responses; -using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Net; using System.Net.Sockets; +using System.Text.Json; using System.Threading.Tasks; using TechnitiumLibrary; using TechnitiumLibrary.Net.Dns; @@ -115,81 +115,85 @@ namespace GeoDistance if ((location is null) && _maxMind.DatabaseReader.TryCity(remoteEP.Address, out CityResponse response) && response.Location.HasCoordinates) location = response.Location; - dynamic jsonAppRecordData = JsonConvert.DeserializeObject(appRecordData); - dynamic jsonClosestServer = null; - - if (location is null) + using (JsonDocument jsonDocument = JsonDocument.Parse(appRecordData)) { - jsonClosestServer = jsonAppRecordData[0]; - } - else - { - double lastDistance = double.MaxValue; + JsonElement jsonAppRecordData = jsonDocument.RootElement; + JsonElement jsonClosestServer = default; - foreach (dynamic jsonServer in jsonAppRecordData) + if (location is null) { - double lat = Convert.ToDouble(jsonServer.lat.Value); - double @long = Convert.ToDouble(jsonServer.@long.Value); + if (jsonAppRecordData.GetArrayLength() > 0) + jsonClosestServer = jsonAppRecordData[0]; + } + else + { + double lastDistance = double.MaxValue; - double distance = GetDistance(lat, @long, location.Latitude.Value, location.Longitude.Value); - - if (distance < lastDistance) + foreach (JsonElement jsonServer in jsonAppRecordData.EnumerateArray()) { - lastDistance = distance; - jsonClosestServer = jsonServer; + double lat = Convert.ToDouble(jsonServer.GetProperty("lat").GetString()); + double @long = Convert.ToDouble(jsonServer.GetProperty("long").GetString()); + + double distance = GetDistance(lat, @long, location.Latitude.Value, location.Longitude.Value); + + if (distance < lastDistance) + { + lastDistance = distance; + jsonClosestServer = jsonServer; + } } } - } - if (jsonClosestServer is null) - return Task.FromResult(null); + if (jsonClosestServer.ValueKind == JsonValueKind.Undefined) + return Task.FromResult(null); - List answers = new List(); + List answers = new List(); - switch (question.Type) - { - case DnsResourceRecordType.A: - foreach (dynamic jsonAddress in jsonClosestServer.addresses) - { - IPAddress address = IPAddress.Parse(jsonAddress.Value); + switch (question.Type) + { + case DnsResourceRecordType.A: + foreach (JsonElement jsonAddress in jsonClosestServer.GetProperty("addresses").EnumerateArray()) + { + IPAddress address = IPAddress.Parse(jsonAddress.GetString()); - if (address.AddressFamily == AddressFamily.InterNetwork) - answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.A, DnsClass.IN, appRecordTtl, new DnsARecordData(address))); - } - break; + if (address.AddressFamily == AddressFamily.InterNetwork) + answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.A, DnsClass.IN, appRecordTtl, new DnsARecordData(address))); + } + break; - case DnsResourceRecordType.AAAA: - foreach (dynamic jsonAddress in jsonClosestServer.addresses) - { - IPAddress address = IPAddress.Parse(jsonAddress.Value); + case DnsResourceRecordType.AAAA: + foreach (JsonElement jsonAddress in jsonClosestServer.GetProperty("addresses").EnumerateArray()) + { + IPAddress address = IPAddress.Parse(jsonAddress.GetString()); - if (address.AddressFamily == AddressFamily.InterNetworkV6) - answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.AAAA, DnsClass.IN, appRecordTtl, new DnsAAAARecordData(address))); - } - break; - } + if (address.AddressFamily == AddressFamily.InterNetworkV6) + answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.AAAA, DnsClass.IN, appRecordTtl, new DnsAAAARecordData(address))); + } + break; + } - if (answers.Count == 0) - return Task.FromResult(null); + if (answers.Count == 0) + return Task.FromResult(null); - if (answers.Count > 1) - answers.Shuffle(); + if (answers.Count > 1) + answers.Shuffle(); - EDnsOption[] options; + EDnsOption[] options; - if (requestECS is null) - { - options = null; - } - else - { - if (ecsUsed) - options = EDnsClientSubnetOptionData.GetEDnsClientSubnetOption(requestECS.SourcePrefixLength, requestECS.SourcePrefixLength, requestECS.AddressValue); + if (requestECS is null) + { + options = null; + } else - options = EDnsClientSubnetOptionData.GetEDnsClientSubnetOption(requestECS.SourcePrefixLength, 0, requestECS.AddressValue); - } + { + if (ecsUsed) + options = EDnsClientSubnetOptionData.GetEDnsClientSubnetOption(requestECS.SourcePrefixLength, requestECS.SourcePrefixLength, requestECS.Address); + else + options = EDnsClientSubnetOptionData.GetEDnsClientSubnetOption(requestECS.SourcePrefixLength, 0, requestECS.Address); + } - return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, answers, null, null, _dnsServer.UdpPayloadSize, EDnsHeaderFlags.None, options)); + return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question, answers, null, null, _dnsServer.UdpPayloadSize, EDnsHeaderFlags.None, options)); + } default: return Task.FromResult(null); diff --git a/Apps/GeoDistanceApp/CNAME.cs b/Apps/GeoDistanceApp/CNAME.cs index ca3c7783..70b78013 100644 --- a/Apps/GeoDistanceApp/CNAME.cs +++ b/Apps/GeoDistanceApp/CNAME.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -20,11 +20,12 @@ along with this program. If not, see . using DnsServerCore.ApplicationCommon; using MaxMind.GeoIP2.Model; using MaxMind.GeoIP2.Responses; -using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Net; +using System.Text.Json; using System.Threading.Tasks; +using TechnitiumLibrary; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.EDnsOptions; using TechnitiumLibrary.Net.Dns.ResourceRecords; @@ -108,21 +109,23 @@ namespace GeoDistance if ((location is null) && _maxMind.DatabaseReader.TryCity(remoteEP.Address, out CityResponse response) && response.Location.HasCoordinates) location = response.Location; - dynamic jsonAppRecordData = JsonConvert.DeserializeObject(appRecordData); - dynamic jsonClosestServer = null; + using JsonDocument jsonDocument = JsonDocument.Parse(appRecordData); + JsonElement jsonAppRecordData = jsonDocument.RootElement; + JsonElement jsonClosestServer = default; if (location is null) { - jsonClosestServer = jsonAppRecordData[0]; + if (jsonAppRecordData.GetArrayLength() > 0) + jsonClosestServer = jsonAppRecordData[0]; } else { double lastDistance = double.MaxValue; - foreach (dynamic jsonServer in jsonAppRecordData) + foreach (JsonElement jsonServer in jsonAppRecordData.EnumerateArray()) { - double lat = Convert.ToDouble(jsonServer.lat.Value); - double @long = Convert.ToDouble(jsonServer.@long.Value); + double lat = Convert.ToDouble(jsonServer.GetProperty("lat").GetString()); + double @long = Convert.ToDouble(jsonServer.GetProperty("long").GetString()); double distance = GetDistance(lat, @long, location.Latitude.Value, location.Longitude.Value); @@ -134,14 +137,10 @@ namespace GeoDistance } } - if (jsonClosestServer is null) + if (jsonClosestServer.ValueKind == JsonValueKind.Undefined) return Task.FromResult(null); - dynamic jsonCname = jsonClosestServer.cname; - if (jsonCname is null) - return Task.FromResult(null); - - string cname = jsonCname.Value; + string cname = jsonClosestServer.GetPropertyValue("cname", null); if (string.IsNullOrEmpty(cname)) return Task.FromResult(null); @@ -161,12 +160,12 @@ namespace GeoDistance else { if (ecsUsed) - options = EDnsClientSubnetOptionData.GetEDnsClientSubnetOption(requestECS.SourcePrefixLength, requestECS.SourcePrefixLength, requestECS.AddressValue); + options = EDnsClientSubnetOptionData.GetEDnsClientSubnetOption(requestECS.SourcePrefixLength, requestECS.SourcePrefixLength, requestECS.Address); else - options = EDnsClientSubnetOptionData.GetEDnsClientSubnetOption(requestECS.SourcePrefixLength, 0, requestECS.AddressValue); + options = EDnsClientSubnetOptionData.GetEDnsClientSubnetOption(requestECS.SourcePrefixLength, 0, requestECS.Address); } - return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, answers, null, null, _dnsServer.UdpPayloadSize, EDnsHeaderFlags.None, options)); + return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question, answers, null, null, _dnsServer.UdpPayloadSize, EDnsHeaderFlags.None, options)); } #endregion diff --git a/Apps/GeoDistanceApp/GeoDistanceApp.csproj b/Apps/GeoDistanceApp/GeoDistanceApp.csproj index a6967219..65b478e1 100644 --- a/Apps/GeoDistanceApp/GeoDistanceApp.csproj +++ b/Apps/GeoDistanceApp/GeoDistanceApp.csproj @@ -4,7 +4,7 @@ net7.0 false true - 5.0.1 + 6.0 Technitium Technitium DNS Server Shreyas Zare @@ -12,14 +12,13 @@ GeoDistance https://technitium.com/dns/ https://github.com/TechnitiumSoftware/DnsServer - Allows creating APP records in a primary zone that can return A or AAAA records, or CNAME record of the server located geographically closest to the client using MaxMind GeoIP2 City database. Supports EDNS Client Subnet (ECS). This app requires MaxMind GeoIP2 database and includes the GeoLite2 version for trial. \n\nTo update the MaxMind GeoIP2 database for your app, download the GeoIP2-City.mmdb file from MaxMind and zip it. Use the zip file with the manual Update option. + Allows creating APP records in a primary and forwarder zones that can return A or AAAA records, or CNAME record of the server located geographically closest to the client using MaxMind GeoIP2 City database. Supports EDNS Client Subnet (ECS). This app requires MaxMind GeoIP2 database and includes the GeoLite2 version for trial. \n\nTo update the MaxMind GeoIP2 database for your app, download the GeoIP2-City.mmdb file from MaxMind and zip it. Use the zip file with the manual Update option. false Library - diff --git a/Apps/NoDataApp/App.cs b/Apps/NoDataApp/App.cs index 09f4584c..7c907473 100644 --- a/Apps/NoDataApp/App.cs +++ b/Apps/NoDataApp/App.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -18,9 +18,9 @@ along with this program. If not, see . */ using DnsServerCore.ApplicationCommon; -using Newtonsoft.Json; using System; using System.Net; +using System.Text.Json; using System.Threading.Tasks; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; @@ -52,13 +52,14 @@ namespace NoData if (question.Name.Equals(appRecordName, StringComparison.OrdinalIgnoreCase)) { - dynamic jsonAppRecordData = JsonConvert.DeserializeObject(appRecordData); + using JsonDocument jsonDocument = JsonDocument.Parse(appRecordData); + JsonElement jsonAppRecordData = jsonDocument.RootElement; - foreach (dynamic jsonBlockedType in jsonAppRecordData.blockedTypes) + foreach (JsonElement jsonBlockedType in jsonAppRecordData.GetProperty("blockedTypes").EnumerateArray()) { - DnsResourceRecordType blockedType = Enum.Parse(jsonBlockedType.Value, true); + DnsResourceRecordType blockedType = Enum.Parse(jsonBlockedType.GetString(), true); if ((blockedType == question.Type) || (blockedType == DnsResourceRecordType.ANY)) - return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, false, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question)); + return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question)); } } diff --git a/Apps/NoDataApp/NoDataApp.csproj b/Apps/NoDataApp/NoDataApp.csproj index 017e7cdc..8f867a97 100644 --- a/Apps/NoDataApp/NoDataApp.csproj +++ b/Apps/NoDataApp/NoDataApp.csproj @@ -3,7 +3,7 @@ net7.0 false - 1.0.1 + 2.0 Technitium Technitium DNS Server Shreyas Zare @@ -16,10 +16,6 @@ Library - - - - false diff --git a/Apps/NxDomainApp/App.cs b/Apps/NxDomainApp/App.cs index 51c52cb7..86d8f64b 100644 --- a/Apps/NxDomainApp/App.cs +++ b/Apps/NxDomainApp/App.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2021 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -18,10 +18,12 @@ along with this program. If not, see . */ using DnsServerCore.ApplicationCommon; -using Newtonsoft.Json; +using System; using System.Collections.Generic; using System.Net; +using System.Text.Json; using System.Threading.Tasks; +using TechnitiumLibrary; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; @@ -51,16 +53,6 @@ namespace NxDomain #region private - private static IReadOnlyDictionary ReadJsonDomainArray(dynamic jsonDomainArray) - { - Dictionary domains = new Dictionary(jsonDomainArray.Count); - - foreach (dynamic jsonDomain in jsonDomainArray) - domains.TryAdd(jsonDomain.Value, null); - - return domains; - } - private static string GetParentZone(string domain) { int i = domain.IndexOf('.'); @@ -100,18 +92,21 @@ namespace NxDomain { _soaRecord = new DnsSOARecordData(dnsServer.ServerDomain, "hostadmin@" + dnsServer.ServerDomain, 1, 14400, 3600, 604800, 60); - dynamic jsonConfig = JsonConvert.DeserializeObject(config); + using JsonDocument jsonDocument = JsonDocument.Parse(config); + JsonElement jsonConfig = jsonDocument.RootElement; - _enableBlocking = jsonConfig.enableBlocking.Value; - _allowTxtBlockingReport = jsonConfig.allowTxtBlockingReport.Value; - - _blockListZone = ReadJsonDomainArray(jsonConfig.blocked); + _enableBlocking = jsonConfig.GetProperty("enableBlocking").GetBoolean(); + _allowTxtBlockingReport = jsonConfig.GetProperty("allowTxtBlockingReport").GetBoolean(); + _blockListZone = jsonConfig.ReadArrayAsMap("blocked", delegate (JsonElement jsonDomainName) { return new Tuple(jsonDomainName.GetString(), null); }); return Task.CompletedTask; } public Task ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed) { + if (!_enableBlocking) + return Task.FromResult(null); + DnsQuestionRecord question = request.Question[0]; if (!IsZoneBlocked(question.Name, out string blockedDomain)) @@ -122,7 +117,7 @@ namespace NxDomain //return meta data DnsResourceRecord[] answer = new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.TXT, question.Class, 60, new DnsTXTRecordData("source=nx-domain-app; domain=" + blockedDomain)) }; - return Task.FromResult(new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, answer) { Tag = DnsServerResponseType.Blocked }); + return Task.FromResult(new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question, answer) { Tag = DnsServerResponseType.Blocked }); } else { @@ -132,7 +127,7 @@ namespace NxDomain IReadOnlyList authority = new DnsResourceRecord[] { new DnsResourceRecord(parentDomain, DnsResourceRecordType.SOA, question.Class, 60, _soaRecord) }; - return Task.FromResult(new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NxDomain, request.Question, null, authority) { Tag = DnsServerResponseType.Blocked }); + return Task.FromResult(new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NxDomain, request.Question, null, authority) { Tag = DnsServerResponseType.Blocked }); } } diff --git a/Apps/NxDomainApp/NxDomainApp.csproj b/Apps/NxDomainApp/NxDomainApp.csproj index 8e972fdd..d3cf276e 100644 --- a/Apps/NxDomainApp/NxDomainApp.csproj +++ b/Apps/NxDomainApp/NxDomainApp.csproj @@ -3,7 +3,7 @@ net7.0 false - 3.0.1 + 4.0 Technitium Technitium DNS Server Shreyas Zare @@ -16,10 +16,6 @@ Library - - - - false @@ -27,6 +23,10 @@ + + ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.dll + false + ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.Net.dll false diff --git a/Apps/QueryLogsSqliteApp/App.cs b/Apps/QueryLogsSqliteApp/App.cs index f59e7186..dab8ddcd 100644 --- a/Apps/QueryLogsSqliteApp/App.cs +++ b/Apps/QueryLogsSqliteApp/App.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -19,12 +19,12 @@ along with this program. If not, see . using DnsServerCore.ApplicationCommon; using Microsoft.Data.Sqlite; -using Newtonsoft.Json; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Net; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using TechnitiumLibrary.Net.Dns; @@ -192,13 +192,14 @@ namespace QueryLogsSqlite { _dnsServer = dnsServer; - dynamic jsonConfig = JsonConvert.DeserializeObject(config); + using JsonDocument jsonDocument = JsonDocument.Parse(config); + JsonElement jsonConfig = jsonDocument.RootElement; - _enableLogging = jsonConfig.enableLogging.Value; - _maxLogDays = Convert.ToInt32(jsonConfig.maxLogDays.Value); + _enableLogging = jsonConfig.GetProperty("enableLogging").GetBoolean(); + _maxLogDays = jsonConfig.GetProperty("maxLogDays").GetInt32(); - string sqliteDbPath = jsonConfig.sqliteDbPath.Value; - string connectionString = jsonConfig.connectionString.Value; + string sqliteDbPath = jsonConfig.GetProperty("sqliteDbPath").GetString(); + string connectionString = jsonConfig.GetProperty("connectionString").GetString(); if (!Path.IsPathRooted(sqliteDbPath)) sqliteDbPath = Path.Combine(_dnsServer.ApplicationFolder, sqliteDbPath); @@ -393,9 +394,7 @@ CREATE TABLE IF NOT EXISTS dns_logs public Task QueryLogsAsync(long pageNumber, int entriesPerPage, bool descendingOrder, DateTime? start, DateTime? end, IPAddress clientIpAddress, DnsTransportProtocol? protocol, DnsServerResponseType? responseType, DnsResponseCode? rcode, string qname, DnsResourceRecordType? qtype, DnsClass? qclass) { - if (pageNumber < 0) - pageNumber = long.MaxValue; - else if (pageNumber == 0) + if (pageNumber == 0) pageNumber = 1; if (qname is not null) @@ -486,7 +485,7 @@ CREATE TABLE IF NOT EXISTS dns_logs long totalPages = (totalEntries / entriesPerPage) + (totalEntries % entriesPerPage > 0 ? 1 : 0); - if (pageNumber > totalPages) + if ((pageNumber > totalPages) || (pageNumber < 0)) pageNumber = totalPages; long endRowNum; diff --git a/Apps/QueryLogsSqliteApp/QueryLogsSqliteApp.csproj b/Apps/QueryLogsSqliteApp/QueryLogsSqliteApp.csproj index 7c177cc3..61b75dd5 100644 --- a/Apps/QueryLogsSqliteApp/QueryLogsSqliteApp.csproj +++ b/Apps/QueryLogsSqliteApp/QueryLogsSqliteApp.csproj @@ -4,7 +4,7 @@ net7.0 false true - 3.1 + 4.0 Technitium Technitium DNS Server Shreyas Zare @@ -18,8 +18,7 @@ - - + diff --git a/Apps/SplitHorizonApp/AddressTranslation.cs b/Apps/SplitHorizonApp/AddressTranslation.cs index 3491e1fb..8bcc9715 100644 --- a/Apps/SplitHorizonApp/AddressTranslation.cs +++ b/Apps/SplitHorizonApp/AddressTranslation.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -18,12 +18,13 @@ along with this program. If not, see . */ using DnsServerCore.ApplicationCommon; -using Newtonsoft.Json; using System; using System.Collections.Generic; using System.IO; using System.Net; +using System.Text.Json; using System.Threading.Tasks; +using TechnitiumLibrary; using TechnitiumLibrary.Net; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; @@ -106,15 +107,18 @@ namespace SplitHorizon await File.WriteAllTextAsync(Path.Combine(dnsServer.ApplicationFolder, "dnsApp.config"), config); } - dynamic jsonConfig = JsonConvert.DeserializeObject(config); - - if (jsonConfig.enableAddressTranslation is null) + do { - //update old config with default config - config = config.TrimEnd(' ', '\t', '\r', '\n'); - config = config.Substring(0, config.Length - 1); - config = config.TrimEnd(' ', '\t', '\r', '\n'); - config += """ + using JsonDocument jsonDocument = JsonDocument.Parse(config); + JsonElement jsonConfig = jsonDocument.RootElement; + + if (!jsonConfig.TryGetProperty("enableAddressTranslation", out _)) + { + //update old config with default config + config = config.TrimEnd(' ', '\t', '\r', '\n'); + config = config.Substring(0, config.Length - 1); + config = config.TrimEnd(' ', '\t', '\r', '\n'); + config += """ , "enableAddressTranslation": false, "networkGroupMap": { @@ -153,43 +157,31 @@ namespace SplitHorizon ] } """; - await File.WriteAllTextAsync(Path.Combine(dnsServer.ApplicationFolder, "dnsApp.config"), config); + await File.WriteAllTextAsync(Path.Combine(dnsServer.ApplicationFolder, "dnsApp.config"), config); - jsonConfig = JsonConvert.DeserializeObject(config); - } - - _enableAddressTranslation = jsonConfig.enableAddressTranslation.Value; - - { - Dictionary networkGroupMap = new Dictionary(); - - foreach (dynamic jsonProperty in jsonConfig.networkGroupMap) - { - string network = jsonProperty.Name; - string group = jsonProperty.Value; - - if (!NetworkAddress.TryParse(network, out NetworkAddress networkAddress)) - throw new InvalidOperationException("Network group map contains an invalid network address: " + network); - - networkGroupMap.Add(networkAddress, group); + //reparse config + continue; } - _networkGroupMap = networkGroupMap; - } + _enableAddressTranslation = jsonConfig.GetProperty("enableAddressTranslation").GetBoolean(); - { - Dictionary groups = new Dictionary(); + _networkGroupMap = jsonConfig.ReadObjectAsMap("networkGroupMap", delegate (string strNetworkAddress, JsonElement jsonGroupName) + { + if (!NetworkAddress.TryParse(strNetworkAddress, out NetworkAddress networkAddress)) + throw new InvalidOperationException("Network group map contains an invalid network address: " + strNetworkAddress); - foreach (dynamic jsonGroup in jsonConfig.groups) + return new Tuple(networkAddress, jsonGroupName.GetString()); + }); + + _groups = jsonConfig.ReadArrayAsMap("groups", delegate (JsonElement jsonGroup) { Group group = new Group(jsonGroup); - groups.Add(group.Name, group); - } + return new Tuple(group.Name, group); + }); - _groups = groups; + break; } - - return; + while (true); } public Task PostProcessAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram response) @@ -245,7 +237,7 @@ namespace SplitHorizon IPAddress externalIp = (answer.RDATA as DnsARecordData).Address; if (group.ExternalToInternalTranslation.TryGetValue(externalIp, out IPAddress internalIp)) - newAnswer.Add(new DnsResourceRecord(answer.Name, answer.Type, answer.Class, answer.TtlValue, new DnsARecordData(internalIp))); + newAnswer.Add(new DnsResourceRecord(answer.Name, answer.Type, answer.Class, answer.TTL, new DnsARecordData(internalIp))); else newAnswer.Add(answer); } @@ -256,7 +248,7 @@ namespace SplitHorizon IPAddress externalIp = (answer.RDATA as DnsAAAARecordData).Address; if (group.ExternalToInternalTranslation.TryGetValue(externalIp, out IPAddress internalIp)) - newAnswer.Add(new DnsResourceRecord(answer.Name, answer.Type, answer.Class, answer.TtlValue, new DnsAAAARecordData(internalIp))); + newAnswer.Add(new DnsResourceRecord(answer.Name, answer.Type, answer.Class, answer.TTL, new DnsAAAARecordData(internalIp))); else newAnswer.Add(answer); } @@ -299,7 +291,7 @@ namespace SplitHorizon if ((groupName is null) || !_groups.TryGetValue(groupName, out Group group) || !group.Enabled || !group.TranslateReverseLookups) return Task.FromResult(null); - IPAddress ptrIpAddress = IPAddressExtension.ParseReverseDomain(question.Name); + IPAddress ptrIpAddress = IPAddressExtensions.ParseReverseDomain(question.Name); if (!group.InternalToExternalTranslation.TryGetValue(ptrIpAddress, out IPAddress externalIp)) return Task.FromResult(null); @@ -332,21 +324,23 @@ namespace SplitHorizon #region constructor - public Group(dynamic jsonGroup) + public Group(JsonElement jsonGroup) { - _name = jsonGroup.name.Value; - _enabled = jsonGroup.enabled.Value; - _translateReverseLookups = jsonGroup.translateReverseLookups.Value; + _name = jsonGroup.GetProperty("name").GetString(); + _enabled = jsonGroup.GetProperty("enabled").GetBoolean(); + _translateReverseLookups = jsonGroup.GetProperty("translateReverseLookups").GetBoolean(); + + JsonElement jsonExternalToInternalTranslation = jsonGroup.GetProperty("externalToInternalTranslation"); if (_translateReverseLookups) { Dictionary externalToInternalTranslation = new Dictionary(); Dictionary internalToExternalTranslation = new Dictionary(); - foreach (dynamic jsonProperty in jsonGroup.externalToInternalTranslation) + foreach (JsonProperty jsonProperty in jsonExternalToInternalTranslation.EnumerateObject()) { string strExternalIp = jsonProperty.Name; - string strInternalIp = jsonProperty.Value; + string strInternalIp = jsonProperty.Value.GetString(); IPAddress externalIp = IPAddress.Parse(strExternalIp); IPAddress internalIp = IPAddress.Parse(strInternalIp); @@ -362,10 +356,10 @@ namespace SplitHorizon { Dictionary externalToInternalTranslation = new Dictionary(); - foreach (dynamic jsonProperty in jsonGroup.externalToInternalTranslation) + foreach (JsonProperty jsonProperty in jsonExternalToInternalTranslation.EnumerateObject()) { string strExternalIp = jsonProperty.Name; - string strInternalIp = jsonProperty.Value; + string strInternalIp = jsonProperty.Value.GetString(); IPAddress externalIp = IPAddress.Parse(strExternalIp); IPAddress internalIp = IPAddress.Parse(strInternalIp); diff --git a/Apps/SplitHorizonApp/SimpleAddress.cs b/Apps/SplitHorizonApp/SimpleAddress.cs index be1e5c62..11c84f44 100644 --- a/Apps/SplitHorizonApp/SimpleAddress.cs +++ b/Apps/SplitHorizonApp/SimpleAddress.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -18,11 +18,11 @@ along with this program. If not, see . */ using DnsServerCore.ApplicationCommon; -using Newtonsoft.Json; using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Sockets; +using System.Text.Json; using System.Threading.Tasks; using TechnitiumLibrary; using TechnitiumLibrary.Net; @@ -105,28 +105,24 @@ namespace SplitHorizon await File.WriteAllTextAsync(Path.Combine(dnsServer.ApplicationFolder, "dnsApp.config"), config); } - dynamic jsonConfig = JsonConvert.DeserializeObject(config); + using JsonDocument jsonDocument = JsonDocument.Parse(config); + JsonElement jsonConfig = jsonDocument.RootElement; - dynamic jsonNetworks = jsonConfig.networks; - if (jsonNetworks is null) - { - _networks = new Dictionary>(1); - } - else + if (jsonConfig.TryGetProperty("networks", out JsonElement jsonNetworks)) { Dictionary> networks = new Dictionary>(); - foreach (dynamic jsonProperty in jsonNetworks) + foreach (JsonProperty jsonProperty in jsonNetworks.EnumerateObject()) { string networkName = jsonProperty.Name; - dynamic jsonNetworkAddresses = jsonProperty.Value; - if (jsonNetworkAddresses is not null) + JsonElement jsonNetworkAddresses = jsonProperty.Value; + if (jsonNetworkAddresses.ValueKind == JsonValueKind.Array) { - List networkAddresses = new List(); + List networkAddresses = new List(jsonNetworkAddresses.GetArrayLength()); - foreach (dynamic jsonNetworkAddress in jsonNetworkAddresses) - networkAddresses.Add(NetworkAddress.Parse(jsonNetworkAddress.Value)); + foreach (JsonElement jsonNetworkAddress in jsonNetworkAddresses.EnumerateArray()) + networkAddresses.Add(NetworkAddress.Parse(jsonNetworkAddress.GetString())); networks.TryAdd(networkName, networkAddresses); } @@ -134,6 +130,10 @@ namespace SplitHorizon _networks = networks; } + else + { + _networks = new Dictionary>(1); + } } public Task ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, string zoneName, string appRecordName, uint appRecordTtl, string appRecordData) @@ -143,82 +143,88 @@ namespace SplitHorizon { case DnsResourceRecordType.A: case DnsResourceRecordType.AAAA: - dynamic jsonAppRecordData = JsonConvert.DeserializeObject(appRecordData); - dynamic jsonAddresses = null; - - NetworkAddress selectedNetwork = null; - - foreach (dynamic jsonProperty in jsonAppRecordData) + using (JsonDocument jsonDocument = JsonDocument.Parse(appRecordData)) { - string name = jsonProperty.Name; + JsonElement jsonAppRecordData = jsonDocument.RootElement; + JsonElement jsonAddresses = default; - if ((name == "public") || (name == "private")) - continue; + NetworkAddress selectedNetwork = null; - if (_networks.TryGetValue(name, out List networkAddresses)) + foreach (JsonProperty jsonProperty in jsonAppRecordData.EnumerateObject()) { - foreach (NetworkAddress networkAddress in networkAddresses) + string name = jsonProperty.Name; + + if ((name == "public") || (name == "private")) + continue; + + if (_networks.TryGetValue(name, out List networkAddresses)) { - if (networkAddress.Contains(remoteEP.Address)) + foreach (NetworkAddress networkAddress in networkAddresses) { - jsonAddresses = jsonProperty.Value; + if (networkAddress.Contains(remoteEP.Address)) + { + jsonAddresses = jsonProperty.Value; + break; + } + } + + if (jsonAddresses.ValueKind != JsonValueKind.Undefined) break; + } + else if (NetworkAddress.TryParse(name, out NetworkAddress networkAddress)) + { + if (networkAddress.Contains(remoteEP.Address) && ((selectedNetwork is null) || (networkAddress.PrefixLength > selectedNetwork.PrefixLength))) + { + selectedNetwork = networkAddress; + jsonAddresses = jsonProperty.Value; } } + } - if (jsonAddresses is not null) + if (jsonAddresses.ValueKind == JsonValueKind.Undefined) + { + if (NetUtilities.IsPrivateIP(remoteEP.Address)) + { + if (!jsonAppRecordData.TryGetProperty("private", out jsonAddresses)) + return Task.FromResult(null); + } + else + { + if (!jsonAppRecordData.TryGetProperty("public", out jsonAddresses)) + return Task.FromResult(null); + } + } + + List answers = new List(); + + switch (question.Type) + { + case DnsResourceRecordType.A: + foreach (JsonElement jsonAddress in jsonAddresses.EnumerateArray()) + { + if (IPAddress.TryParse(jsonAddress.GetString(), out IPAddress address) && (address.AddressFamily == AddressFamily.InterNetwork)) + answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.A, DnsClass.IN, appRecordTtl, new DnsARecordData(address))); + } + break; + + case DnsResourceRecordType.AAAA: + foreach (JsonElement jsonAddress in jsonAddresses.EnumerateArray()) + { + if (IPAddress.TryParse(jsonAddress.GetString(), out IPAddress address) && (address.AddressFamily == AddressFamily.InterNetworkV6)) + answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.AAAA, DnsClass.IN, appRecordTtl, new DnsAAAARecordData(address))); + } break; } - else if (NetworkAddress.TryParse(name, out NetworkAddress networkAddress)) - { - if (networkAddress.Contains(remoteEP.Address) && ((selectedNetwork is null) || (networkAddress.PrefixLength > selectedNetwork.PrefixLength))) - { - selectedNetwork = networkAddress; - jsonAddresses = jsonProperty.Value; - } - } - } - if (jsonAddresses is null) - { - if (NetUtilities.IsPrivateIP(remoteEP.Address)) - jsonAddresses = jsonAppRecordData.@private; - else - jsonAddresses = jsonAppRecordData.@public; - - if (jsonAddresses is null) + if (answers.Count == 0) return Task.FromResult(null); + + if (answers.Count > 1) + answers.Shuffle(); + + return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question, answers)); } - List answers = new List(); - - switch (question.Type) - { - case DnsResourceRecordType.A: - foreach (dynamic jsonAddress in jsonAddresses) - { - if (IPAddress.TryParse(jsonAddress.Value, out IPAddress address) && (address.AddressFamily == AddressFamily.InterNetwork)) - answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.A, DnsClass.IN, appRecordTtl, new DnsARecordData(address))); - } - break; - - case DnsResourceRecordType.AAAA: - foreach (dynamic jsonAddress in jsonAddresses) - { - if (IPAddress.TryParse(jsonAddress.Value, out IPAddress address) && (address.AddressFamily == AddressFamily.InterNetworkV6)) - answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.AAAA, DnsClass.IN, appRecordTtl, new DnsAAAARecordData(address))); - } - break; - } - - if (answers.Count == 0) - return Task.FromResult(null); - - if (answers.Count > 1) - answers.Shuffle(); - - return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, answers)); - default: return Task.FromResult(null); } @@ -240,15 +246,15 @@ namespace SplitHorizon { return @"{ ""public"": [ - ""1.1.1.1"", + ""1.1.1.1"", ""2.2.2.2"" ], ""private"": [ - ""192.168.1.1"", + ""192.168.1.1"", ""::1"" ], ""custom-networks"": [ - ""172.16.1.1"", + ""172.16.1.1"" ], ""10.0.0.0/8"": [ ""10.1.1.1"" diff --git a/Apps/SplitHorizonApp/SimpleCNAME.cs b/Apps/SplitHorizonApp/SimpleCNAME.cs index f7e62db0..46d1d4fe 100644 --- a/Apps/SplitHorizonApp/SimpleCNAME.cs +++ b/Apps/SplitHorizonApp/SimpleCNAME.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -18,10 +18,10 @@ along with this program. If not, see . */ using DnsServerCore.ApplicationCommon; -using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Net; +using System.Text.Json; using System.Threading.Tasks; using TechnitiumLibrary.Net; using TechnitiumLibrary.Net.Dns; @@ -50,12 +50,13 @@ namespace SplitHorizon public Task ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, string zoneName, string appRecordName, uint appRecordTtl, string appRecordData) { - dynamic jsonAppRecordData = JsonConvert.DeserializeObject(appRecordData); - dynamic jsonCname = null; + using JsonDocument jsonDocument = JsonDocument.Parse(appRecordData); + JsonElement jsonAppRecordData = jsonDocument.RootElement; + JsonElement jsonCname = default; NetworkAddress selectedNetwork = null; - foreach (dynamic jsonProperty in jsonAppRecordData) + foreach (JsonProperty jsonProperty in jsonAppRecordData.EnumerateObject()) { string name = jsonProperty.Name; @@ -73,7 +74,7 @@ namespace SplitHorizon } } - if (jsonCname is not null) + if (jsonCname.ValueKind != JsonValueKind.Undefined) break; } else if (NetworkAddress.TryParse(name, out NetworkAddress networkAddress)) @@ -86,18 +87,21 @@ namespace SplitHorizon } } - if (jsonCname is null) + if (jsonCname.ValueKind == JsonValueKind.Undefined) { if (NetUtilities.IsPrivateIP(remoteEP.Address)) - jsonCname = jsonAppRecordData.@private; + { + if (!jsonAppRecordData.TryGetProperty("private", out jsonCname)) + return Task.FromResult(null); + } else - jsonCname = jsonAppRecordData.@public; - - if (jsonCname is null) - return Task.FromResult(null); + { + if (!jsonAppRecordData.TryGetProperty("public", out jsonCname)) + return Task.FromResult(null); + } } - string cname = jsonCname.Value; + string cname = jsonCname.GetString(); if (string.IsNullOrEmpty(cname)) return Task.FromResult(null); @@ -109,7 +113,7 @@ namespace SplitHorizon else answers = new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.CNAME, DnsClass.IN, appRecordTtl, new DnsCNAMERecordData(cname)) }; - return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, answers)); + return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question, answers)); } #endregion diff --git a/Apps/SplitHorizonApp/SplitHorizonApp.csproj b/Apps/SplitHorizonApp/SplitHorizonApp.csproj index 35eff512..07111f5f 100644 --- a/Apps/SplitHorizonApp/SplitHorizonApp.csproj +++ b/Apps/SplitHorizonApp/SplitHorizonApp.csproj @@ -3,7 +3,7 @@ net7.0 false - 5.0.1 + 6.0 Technitium Technitium DNS Server Shreyas Zare @@ -11,15 +11,11 @@ SplitHorizon https://technitium.com/dns/ https://github.com/TechnitiumSoftware/DnsServer - Allows creating APP records in a primary zone that can return different set of A or AAAA records, or CNAME record for clients querying over public, private, or other specified networks.\n\nEnables Address Translation of IP addresses in a DNS response for A & AAAA type request based on the client's network address and the configured 1:1 translation. + Allows creating APP records in a primary and forwarder zones that can return different set of A or AAAA records, or CNAME record for clients querying over public, private, or other specified networks.\n\nEnables Address Translation of IP addresses in a DNS response for A & AAAA type request based on the client's network address and the configured 1:1 translation. false Library - - - - false diff --git a/Apps/WhatIsMyDnsApp/App.cs b/Apps/WhatIsMyDnsApp/App.cs index ba4f9b23..3b0c02aa 100644 --- a/Apps/WhatIsMyDnsApp/App.cs +++ b/Apps/WhatIsMyDnsApp/App.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -73,7 +73,7 @@ namespace WhatIsMyDns return Task.FromResult(null); } - return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, new DnsResourceRecord[] { answer })); + return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question, new DnsResourceRecord[] { answer })); } #endregion diff --git a/Apps/WhatIsMyDnsApp/WhatIsMyDnsApp.csproj b/Apps/WhatIsMyDnsApp/WhatIsMyDnsApp.csproj index c193dc97..12682d73 100644 --- a/Apps/WhatIsMyDnsApp/WhatIsMyDnsApp.csproj +++ b/Apps/WhatIsMyDnsApp/WhatIsMyDnsApp.csproj @@ -4,7 +4,7 @@ net7.0 false true - 5.0 + 5.0.1 Technitium Technitium DNS Server Shreyas Zare @@ -12,7 +12,7 @@ WhatIsMyDns https://technitium.com/dns/ https://github.com/TechnitiumSoftware/DnsServer - Allows creating APP records in a primary zone that can return the IP address of the user's DNS Server for A, AAAA, and TXT queries. + Allows creating APP records in a primary and forwarder zones that can return the IP address of the user's DNS Server for A, AAAA, and TXT queries. false Library diff --git a/Apps/WildIpApp/App.cs b/Apps/WildIpApp/App.cs index 274a22bc..900c8c1d 100644 --- a/Apps/WildIpApp/App.cs +++ b/Apps/WildIpApp/App.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -28,6 +28,12 @@ namespace WildIp { public class App : IDnsApplication, IDnsAppRecordRequestHandler { + #region variables + + IDnsServer _dnsServer; + + #endregion + #region IDisposable public void Dispose() @@ -41,18 +47,19 @@ namespace WildIp public Task InitializeAsync(IDnsServer dnsServer, string config) { - //do nothing + _dnsServer = dnsServer; + return Task.CompletedTask; } - public Task ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, string zoneName, string appRecordName, uint appRecordTtl, string appRecordData) + public async Task ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, string zoneName, string appRecordName, uint appRecordTtl, string appRecordData) { string qname = request.Question[0].Name; if (qname.Length == appRecordName.Length) - return Task.FromResult(null); + return null; - DnsResourceRecord answer; + DnsResourceRecord answer = null; switch (request.Question[0].Type) { @@ -69,12 +76,8 @@ namespace WildIp rawIp[i++] = x; } - if (i < 4) - return Task.FromResult(null); - - IPAddress address = new IPAddress(rawIp); - - answer = new DnsResourceRecord(request.Question[0].Name, DnsResourceRecordType.A, DnsClass.IN, appRecordTtl, new DnsARecordData(address)); + if (i == 4) + answer = new DnsResourceRecord(request.Question[0].Name, DnsResourceRecordType.A, DnsClass.IN, appRecordTtl, new DnsARecordData(new IPAddress(rawIp))); } break; @@ -90,18 +93,21 @@ namespace WildIp break; } - if (address is null) - return Task.FromResult(null); - - answer = new DnsResourceRecord(request.Question[0].Name, DnsResourceRecordType.AAAA, DnsClass.IN, appRecordTtl, new DnsAAAARecordData(address)); + if (address is not null) + answer = new DnsResourceRecord(request.Question[0].Name, DnsResourceRecordType.AAAA, DnsClass.IN, appRecordTtl, new DnsAAAARecordData(address)); } break; - - default: - return Task.FromResult(null); } - return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, new DnsResourceRecord[] { answer })); + if (answer is null) + { + //NODATA reponse + DnsDatagram soaResponse = await _dnsServer.DirectQueryAsync(new DnsQuestionRecord(zoneName, DnsResourceRecordType.SOA, DnsClass.IN)); + + return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, true, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question, null, soaResponse.Answer); + } + + return new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question, new DnsResourceRecord[] { answer }); } #endregion diff --git a/Apps/WildIpApp/WildIpApp.csproj b/Apps/WildIpApp/WildIpApp.csproj index 573ef1ab..00afeca9 100644 --- a/Apps/WildIpApp/WildIpApp.csproj +++ b/Apps/WildIpApp/WildIpApp.csproj @@ -4,7 +4,7 @@ net7.0 false true - 2.0 + 2.1 Technitium Technitium DNS Server Shreyas Zare @@ -12,7 +12,7 @@ WildIp https://technitium.com/dns/ https://github.com/TechnitiumSoftware/DnsServer - Allows creating APP records in a primary zone that can return the IP address embedded in the subdomain name for A and AAAA queries. It works similar to sslip.io. + Allows creating APP records in a primary and forwarder zones that can return the IP address embedded in the subdomain name for A and AAAA queries. It works similar to sslip.io. false Library diff --git a/CHANGELOG.md b/CHANGELOG.md index a23c2faf..ff7a2177 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # Technitium DNS Server Change Log +## Version 11.0 +Release Date: 18 February 2022 + +- Added support for DNS-over-QUIC (DoQ) [RFC 9250](https://www.ietf.org/rfc/rfc9250.html). This allows you to run DoQ service as well as use it with Forwarders. DoQ implementation supports running over SOCKS5 proxy server that provides UDP transport. +- Added support for Zone Transfer over QUIC (XFR-over-QUIC) [RFC 9250](https://www.ietf.org/rfc/rfc9250.html). +- Updated DNS-over-HTTPS protocol implementation to support HTTP/2 and HTTP/3. DNS-over-HTTP/3 can be forced by using `h3` instead of `https` scheme for the URL. +- Updated DNS server's web service backend to use Kestrel web server and thus the DNS server now requires ASP.NET Core Runtime to be installed. With this change, the web service now supports both HTTP/2 and HTTP/3 protocols. If you are using HTTP API, it is recommended to test your code/script with the new release. +- Added support to save DNS cache data to disk on server shutdown and to reload it at startup. +- Updated DNS server domain name blocking feature to support Extended DNS Errors to show report on the blocked domain name. With this support added, the DNS Client tab on the web panel will show blocking report for any blocked domain name. +- Updated DNS server domain name blocking feature to support wildcard block lists file format and Adblock Plus file format. +- Updated DNS server to detect when an upstream server blocks a domain name to reflect it in dashboard stats and query logs. It will now detect blocking signal from Quad9 and show Extended DNS Error for it. +- Updated web panel Zones GUI to support pagination. +- Advanced Blocking App: Updated DNS app to support wildcard block lists file format. Updated the app to disable CNAME cloaking when a domain name is allowed in config. Implemented Extended DNS Errors support to show blocked domain report. +- Advanced Forwarding App: Added new DNS app to support bulk conditional forwarder. +- DNS Block List App: Added new DNS app to allow running your own DNSBL or RBL block lists [RFC 5782](https://www.rfc-editor.org/rfc/rfc5782). +- Added support for TFTP Server Address DHCP option (150). +- Added support for Generic DHCP option to allow configuring option currently not supported by the DHCP server. +- Removed support for non-standard DNS-over-HTTPS (JSON) protocol. +- Removed Newtonsoft.Json dependency from the DNS server and all DNS apps. +- Multiple other minor bug fixes and improvements. + ## Version 10.0.1 Release Date: 4 December 2022 @@ -18,7 +39,7 @@ Release Date: 26 November 2022 - Implemented EDNS Client Subnet (ECS) [RFC 7871](https://datatracker.ietf.org/doc/html/rfc7871) support for recursive resolution and forwarding. - Updated HTTP API to accept date time in ISO 8601 format for dashboard and query logs API calls. Any implementation that uses these API must test with new update before deploying to production. - Upgraded codebase to .NET 7 runtime. If you had manually installed the DNS Server or .NET 6 Runtime earlier then you must install .NET 7 Runtime manually before upgrading the DNS server. -- Fixed self-CNAME vulnerability reported by Xiang Li, [Network and Information Security Lab, Tsinghua University](https://netsec.ccert.edu.cn/) which caused the DNS server to follow CNAME in loop causing the answer to contain couple of hundred records before the loop limit was hit. +- Fixed self-CNAME vulnerability [CVE-2022-48256] reported by Xiang Li, [Network and Information Security Lab, Tsinghua University](https://netsec.ccert.edu.cn/) which caused the DNS server to follow CNAME in loop causing the answer to contain couple of hundred records before the loop limit was hit. - Updated DNS Apps framework with `IDnsPostProcessor` interface to allow manipulating outbound responses by DNS apps. - NO DATA App: Added new app to allow returning NO DATA response in Conditional Forwarder zones to allow overriding existing records from the forwarder for specified record types. - DNS64 App: Added new app to support DNS64 function [RFC 6147](https://www.rfc-editor.org/rfc/rfc6147) for use by IPv6 only clients. diff --git a/DnsServer.sln b/DnsServer.sln index 787db03d..57805a81 100644 --- a/DnsServer.sln +++ b/DnsServer.sln @@ -43,6 +43,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NoDataApp", "Apps\NoDataApp EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dns64App", "Apps\Dns64App\Dns64App.csproj", "{3514C4B4-78C1-46A1-82D5-4E676DD114FA}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DnsBlockListApp", "Apps\DnsBlockListApp\DnsBlockListApp.csproj", "{9F2EC41F-6A9E-47C4-B47B-75190D5B6903}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AdvancedForwardingApp", "Apps\AdvancedForwardingApp\AdvancedForwardingApp.csproj", "{42DD2C37-4082-4E33-9AB0-04A97290D5B7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -125,6 +129,14 @@ Global {3514C4B4-78C1-46A1-82D5-4E676DD114FA}.Debug|Any CPU.Build.0 = Debug|Any CPU {3514C4B4-78C1-46A1-82D5-4E676DD114FA}.Release|Any CPU.ActiveCfg = Release|Any CPU {3514C4B4-78C1-46A1-82D5-4E676DD114FA}.Release|Any CPU.Build.0 = Release|Any CPU + {9F2EC41F-6A9E-47C4-B47B-75190D5B6903}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9F2EC41F-6A9E-47C4-B47B-75190D5B6903}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9F2EC41F-6A9E-47C4-B47B-75190D5B6903}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9F2EC41F-6A9E-47C4-B47B-75190D5B6903}.Release|Any CPU.Build.0 = Release|Any CPU + {42DD2C37-4082-4E33-9AB0-04A97290D5B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {42DD2C37-4082-4E33-9AB0-04A97290D5B7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {42DD2C37-4082-4E33-9AB0-04A97290D5B7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {42DD2C37-4082-4E33-9AB0-04A97290D5B7}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -144,6 +156,8 @@ Global {8B6BEB00-0AC2-4680-A848-31AD8A0FCD82} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2} {BE08D981-DDB0-4314-A571-D68EDF0F3971} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2} {3514C4B4-78C1-46A1-82D5-4E676DD114FA} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2} + {9F2EC41F-6A9E-47C4-B47B-75190D5B6903} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2} + {42DD2C37-4082-4E33-9AB0-04A97290D5B7} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {6747BB6D-2826-4356-A213-805FBCCF9201} diff --git a/DnsServerApp/DnsServerApp.csproj b/DnsServerApp/DnsServerApp.csproj index 295865a9..827cb031 100644 --- a/DnsServerApp/DnsServerApp.csproj +++ b/DnsServerApp/DnsServerApp.csproj @@ -6,7 +6,7 @@ Exe net7.0 logo2.ico - 10.0.1 + 11.0 Technitium Technitium DNS Server Shreyas Zare diff --git a/DnsServerApp/Program.cs b/DnsServerApp/Program.cs index 0f7066a8..88359e8a 100644 --- a/DnsServerApp/Program.cs +++ b/DnsServerApp/Program.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -69,7 +69,7 @@ namespace DnsServerApp Console.WriteLine("Technitium DNS Server was started successfully."); Console.WriteLine("Using config folder: " + service.ConfigFolder); Console.WriteLine(""); - Console.WriteLine("Note: Open http://" + service.WebServiceHostname + ":" + service.WebServiceHttpPort + "/ in web browser to access web console."); + Console.WriteLine("Note: Open http://" + Environment.MachineName.ToLowerInvariant() + ":" + service.WebServiceHttpPort + "/ in web browser to access web console."); Console.WriteLine(""); Console.WriteLine("Press [CTRL + C] to stop..."); diff --git a/DnsServerApp/install.sh b/DnsServerApp/install.sh index f5c00748..5b7a1885 100644 --- a/DnsServerApp/install.sh +++ b/DnsServerApp/install.sh @@ -14,7 +14,7 @@ echo "===============================" echo "Technitium DNS Server Installer" echo "===============================" -if dotnet --list-runtimes 2> /dev/null | grep -q "Microsoft.NETCore.App 7.0."; +if dotnet --list-runtimes 2> /dev/null | grep -q "Microsoft.AspNetCore.App 7.0."; then dotnetFound="yes" else @@ -24,36 +24,36 @@ fi if [ ! -d $dotnetDir ] && [ "$dotnetFound" = "yes" ] then echo "" - echo ".NET 7 Runtime is already installed." + echo "ASP.NET Core Runtime is already installed." else echo "" if [ -d $dotnetDir ] && [ "$dotnetFound" = "yes" ] then dotnetUpdate="yes" - echo "Updating .NET 7 Runtime..." + echo "Updating ASP.NET Core Runtime..." else dotnetUpdate="no" - echo "Installing .NET 7 Runtime..." + echo "Installing ASP.NET Core Runtime..." fi - curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin -c 7.0 --runtime dotnet --no-path --install-dir $dotnetDir --verbose >> $installLog 2>&1 + curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin -c 7.0 --runtime aspnetcore --no-path --install-dir $dotnetDir --verbose >> $installLog 2>&1 if [ ! -f "/usr/bin/dotnet" ] then ln -s $dotnetDir/dotnet /usr/bin >> $installLog 2>&1 fi - if dotnet --list-runtimes 2> /dev/null | grep -q "Microsoft.NETCore.App 7.0."; + if dotnet --list-runtimes 2> /dev/null | grep -q "Microsoft.AspNetCore.App 7.0."; then if [ "$dotnetUpdate" = "yes" ] then - echo ".NET 7 Runtime was updated successfully!" + echo "ASP.NET Core Runtime was updated successfully!" else - echo ".NET 7 Runtime was installed successfully!" + echo "ASP.NET Core Runtime was installed successfully!" fi else - echo "Failed to install .NET 7 Runtime. Please try again." + echo "Failed to install ASP.NET Core Runtime. Please try again." exit 1 fi fi diff --git a/DnsServerCore.ApplicationCommon/DnsServerCore.ApplicationCommon.csproj b/DnsServerCore.ApplicationCommon/DnsServerCore.ApplicationCommon.csproj index 720dcf52..42f607a5 100644 --- a/DnsServerCore.ApplicationCommon/DnsServerCore.ApplicationCommon.csproj +++ b/DnsServerCore.ApplicationCommon/DnsServerCore.ApplicationCommon.csproj @@ -1,4 +1,4 @@ - + net7.0 @@ -11,7 +11,7 @@ DnsServerCore.ApplicationCommon - 5.0 + 5.1 diff --git a/DnsServerCore.ApplicationCommon/IDnsAppRecordRequestHandler.cs b/DnsServerCore.ApplicationCommon/IDnsAppRecordRequestHandler.cs index 94cc93d6..b8f045e4 100644 --- a/DnsServerCore.ApplicationCommon/IDnsAppRecordRequestHandler.cs +++ b/DnsServerCore.ApplicationCommon/IDnsAppRecordRequestHandler.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -39,7 +39,7 @@ namespace DnsServerCore.ApplicationCommon /// The domain name of the APP record. /// The TTL value set in the APP record. /// The record data in the APP record as required for processing the request. - /// The DNS response for the DNS request or null to send no answer response with an SOA authority. + /// The DNS response for the DNS request or null to send NODATA response when QNAME matches APP record name or else NXDOMAIN response with an SOA authority. Task ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, string zoneName, string appRecordName, uint appRecordTtl, string appRecordData); /// diff --git a/DnsServerCore.ApplicationCommon/IDnsQueryLogger.cs b/DnsServerCore.ApplicationCommon/IDnsQueryLogger.cs index 40ba6a59..dfc5145d 100644 --- a/DnsServerCore.ApplicationCommon/IDnsQueryLogger.cs +++ b/DnsServerCore.ApplicationCommon/IDnsQueryLogger.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2021 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -31,7 +31,9 @@ namespace DnsServerCore.ApplicationCommon Authoritative = 1, Recursive = 2, Cached = 3, - Blocked = 4 + Blocked = 4, + UpstreamBlocked = 5, + CacheBlocked = 6 } /// diff --git a/DnsServerCore.ApplicationCommon/IDnsServer.cs b/DnsServerCore.ApplicationCommon/IDnsServer.cs index 17b74faa..ead3ceeb 100644 --- a/DnsServerCore.ApplicationCommon/IDnsServer.cs +++ b/DnsServerCore.ApplicationCommon/IDnsServer.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -33,10 +33,20 @@ namespace DnsServerCore.ApplicationCommon /// Allows querying the DNS server core directly. This call supports recursion even if its not enabled in the DNS server configuration. The request wont be routed to any of the installed DNS Apps except for processing APP records. The request and its response are not counted in any stats or logged. /// /// The question record containing the details to query. + /// The timeout value in milliseconds to wait for response. /// The DNS response for the DNS query. /// When request times out. Task DirectQueryAsync(DnsQuestionRecord question, int timeout = 4000); + /// + /// Allows querying the DNS server core directly. This call supports recursion even if its not enabled in the DNS server configuration. The request wont be routed to any of the installed DNS Apps except for processing APP records. The request and its response are not counted in any stats or logged. + /// + /// The DNS request to query. + /// The timeout value in milliseconds to wait for response. + /// The DNS response for the DNS query. + /// When request times out. + Task DirectQueryAsync(DnsDatagram request, int timeout = 4000); + /// /// Writes a log entry to the DNS server log file. /// diff --git a/DnsServerCore/Auth/User.cs b/DnsServerCore/Auth/User.cs index e13c1f7e..1a8b37b1 100644 --- a/DnsServerCore/Auth/User.cs +++ b/DnsServerCore/Auth/User.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -66,8 +66,8 @@ namespace DnsServerCore.Auth public User(string displayName, string username, string password, int iterations = DEFAULT_ITERATIONS) { - DisplayName = displayName; Username = username; + DisplayName = displayName; ChangePassword(password, iterations); @@ -92,9 +92,9 @@ namespace DnsServerCore.Auth _sessionTimeoutSeconds = bR.ReadInt32(); _previousSessionLoggedOn = bR.ReadDateTime(); - _previousSessionRemoteAddress = IPAddressExtension.ReadFrom(bR); + _previousSessionRemoteAddress = IPAddressExtensions.ReadFrom(bR); _recentSessionLoggedOn = bR.ReadDateTime(); - _recentSessionRemoteAddress = IPAddressExtension.ReadFrom(bR); + _recentSessionRemoteAddress = IPAddressExtensions.ReadFrom(bR); { int count = bR.ReadByte(); @@ -165,6 +165,9 @@ namespace DnsServerCore.Auth public void LoggedInFrom(IPAddress remoteAddress) { + if (remoteAddress.IsIPv4MappedToIPv6) + remoteAddress = remoteAddress.MapToIPv4(); + _previousSessionLoggedOn = _recentSessionLoggedOn; _previousSessionRemoteAddress = _recentSessionRemoteAddress; @@ -220,9 +223,9 @@ namespace DnsServerCore.Auth bW.Write(_sessionTimeoutSeconds); bW.Write(_previousSessionLoggedOn); - IPAddressExtension.WriteTo(_previousSessionRemoteAddress, bW); + IPAddressExtensions.WriteTo(_previousSessionRemoteAddress, bW); bW.Write(_recentSessionLoggedOn); - IPAddressExtension.WriteTo(_recentSessionRemoteAddress, bW); + IPAddressExtensions.WriteTo(_recentSessionRemoteAddress, bW); bW.Write(Convert.ToByte(_memberOfGroups.Count)); diff --git a/DnsServerCore/Auth/UserSession.cs b/DnsServerCore/Auth/UserSession.cs index 14d5d1aa..12f19b12 100644 --- a/DnsServerCore/Auth/UserSession.cs +++ b/DnsServerCore/Auth/UserSession.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -56,6 +56,9 @@ namespace DnsServerCore.Auth if ((tokenName is not null) && (tokenName.Length > 255)) throw new ArgumentOutOfRangeException(nameof(tokenName), "Token name length cannot exceed 255 characters."); + if (remoteAddress.IsIPv4MappedToIPv6) + remoteAddress = remoteAddress.MapToIPv4(); + byte[] tokenBytes = new byte[32]; _rng.GetBytes(tokenBytes); _token = Convert.ToHexString(tokenBytes).ToLower(); @@ -85,7 +88,7 @@ namespace DnsServerCore.Auth _user = authManager.GetUser(bR.ReadShortString()); _lastSeen = bR.ReadDateTime(); - _lastSeenRemoteAddress = IPAddressExtension.ReadFrom(bR); + _lastSeenRemoteAddress = IPAddressExtensions.ReadFrom(bR); _lastSeenUserAgent = bR.ReadShortString(); if (_lastSeenUserAgent.Length == 0) @@ -104,6 +107,9 @@ namespace DnsServerCore.Auth public void UpdateLastSeen(IPAddress remoteAddress, string lastSeenUserAgent) { + if (remoteAddress.IsIPv4MappedToIPv6) + remoteAddress = remoteAddress.MapToIPv4(); + _lastSeen = DateTime.UtcNow; _lastSeenRemoteAddress = remoteAddress; _lastSeenUserAgent = lastSeenUserAgent; diff --git a/DnsServerCore/Dhcp/DhcpOption.cs b/DnsServerCore/Dhcp/DhcpOption.cs index ea77a287..8792de38 100644 --- a/DnsServerCore/Dhcp/DhcpOption.cs +++ b/DnsServerCore/Dhcp/DhcpOption.cs @@ -19,6 +19,7 @@ along with this program. If not, see . using DnsServerCore.Dhcp.Options; using System; +using System.Globalization; using System.IO; using TechnitiumLibrary.IO; @@ -105,6 +106,7 @@ namespace DnsServerCore.Dhcp DomainSearch = 119, ClasslessStaticRoute = 121, CAPWAPAccessControllerAddresses = 138, + TftpServerAddress = 150, End = 255 } @@ -119,6 +121,28 @@ namespace DnsServerCore.Dhcp #region constructor + public DhcpOption(DhcpOptionCode code, string hexValue) + { + if (hexValue is null) + throw new ArgumentNullException(nameof(hexValue)); + + _code = code; + + if (hexValue.Contains(':')) + _value = ParseColonHexString(hexValue); + else + _value = Convert.FromHexString(hexValue); + } + + public DhcpOption(DhcpOptionCode code, byte[] value) + { + if (value is null) + throw new ArgumentNullException(nameof(value)); + + _code = code; + _value = value; + } + protected DhcpOption(DhcpOptionCode code, Stream s) { _code = code; @@ -223,6 +247,9 @@ namespace DnsServerCore.Dhcp case DhcpOptionCode.CAPWAPAccessControllerAddresses: return new CAPWAPAccessControllerOption(s); + case DhcpOptionCode.TftpServerAddress: + return new TftpServerAddressOption(s); + case DhcpOptionCode.Pad: case DhcpOptionCode.End: return new DhcpOption(optionCode); @@ -233,6 +260,38 @@ namespace DnsServerCore.Dhcp } } + protected static byte[] ParseColonHexString(string value) + { + int i; + int j = -1; + string strHex; + int b; + + using (MemoryStream mS = new MemoryStream()) + { + while (true) + { + i = value.IndexOf(':', j + 1); + if (i < 0) + i = value.Length; + + strHex = value.Substring(j + 1, i - j - 1); + + if (!int.TryParse(strHex, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out b) || (b < byte.MinValue) || (b > byte.MaxValue)) + throw new InvalidDataException("VendorSpecificInformation option data must be a colon (:) separated hex string."); + + mS.WriteByte((byte)b); + + if (i == value.Length) + break; + + j = i; + } + + return mS.ToArray(); + } + } + #endregion #region internal @@ -321,6 +380,9 @@ namespace DnsServerCore.Dhcp public DhcpOptionCode Code { get { return _code; } } + public byte[] RawValue + { get { return _value; } } + #endregion } } diff --git a/DnsServerCore/Dhcp/DhcpServer.cs b/DnsServerCore/Dhcp/DhcpServer.cs index b7567527..98639e03 100644 --- a/DnsServerCore/Dhcp/DhcpServer.cs +++ b/DnsServerCore/Dhcp/DhcpServer.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -130,11 +130,9 @@ namespace DnsServerCore.Dhcp { Stop(); - if (_saveModifiedDnsAuthZonesTimer is not null) - _saveModifiedDnsAuthZonesTimer.Dispose(); + _saveModifiedDnsAuthZonesTimer?.Dispose(); - if (_maintenanceTimer is not null) - _maintenanceTimer.Dispose(); + _maintenanceTimer?.Dispose(); if (_scopes is not null) { @@ -204,9 +202,7 @@ namespace DnsServerCore.Dhcp } catch (Exception ex) { - LogManager log = _log; - if (log != null) - log.Write(result.RemoteEndPoint as IPEndPoint, ex); + _log?.Write(result.RemoteEndPoint as IPEndPoint, ex); } } } @@ -224,9 +220,7 @@ namespace DnsServerCore.Dhcp break; //server stopping default: - LogManager log = _log; - if (log != null) - log.Write(ex); + _log?.Write(ex); throw; } @@ -236,9 +230,7 @@ namespace DnsServerCore.Dhcp if ((_state == ServiceState.Stopping) || (_state == ServiceState.Stopped)) return; //server stopping - LogManager log = _log; - if (log != null) - log.Write(ex); + _log?.Write(ex); throw; } @@ -292,9 +284,7 @@ namespace DnsServerCore.Dhcp if ((_state == ServiceState.Stopping) || (_state == ServiceState.Stopped)) return; //server stopping - LogManager log = _log; - if (log != null) - log.Write(remoteEP, ex); + _log?.Write(remoteEP, ex); } } @@ -343,9 +333,7 @@ namespace DnsServerCore.Dhcp return null; //log ip offer - LogManager log = _log; - if (log != null) - log.Write(remoteEP, "DHCP Server offered IP address [" + offer.Address.ToString() + "] to " + request.GetClientFullIdentifier() + " for scope: " + scope.Name); + _log?.Write(remoteEP, "DHCP Server offered IP address [" + offer.Address.ToString() + "] to " + request.GetClientFullIdentifier() + " for scope: " + scope.Name); return DhcpMessage.CreateReply(request, offer.Address, scope.ServerAddress ?? serverIdentifierAddress, scope.ServerHostName, scope.BootFileName, options); } @@ -481,9 +469,7 @@ namespace DnsServerCore.Dhcp scope.CommitLease(leaseOffer); //log ip lease - LogManager log = _log; - if (log != null) - log.Write(remoteEP, "DHCP Server leased IP address [" + leaseOffer.Address.ToString() + "] to " + request.GetClientFullIdentifier() + " for scope: " + scope.Name); + _log?.Write(remoteEP, "DHCP Server leased IP address [" + leaseOffer.Address.ToString() + "] to " + request.GetClientFullIdentifier() + " for scope: " + scope.Name); if (string.IsNullOrWhiteSpace(scope.DomainName)) { @@ -553,9 +539,7 @@ namespace DnsServerCore.Dhcp scope.ReleaseLease(lease); //log issue - LogManager log = _log; - if (log != null) - log.Write(remoteEP, "DHCP Server received DECLINE message for scope '" + scope.Name + "': " + lease.GetClientInfo() + " detected that IP address [" + lease.Address + "] is already in use."); + _log?.Write(remoteEP, "DHCP Server received DECLINE message for scope '" + scope.Name + "': " + lease.GetClientInfo() + " detected that IP address [" + lease.Address + "] is already in use."); //update dns UpdateDnsAuthZone(false, scope, lease); @@ -591,9 +575,7 @@ namespace DnsServerCore.Dhcp scope.ReleaseLease(lease); //log ip lease release - LogManager log = _log; - if (log != null) - log.Write(remoteEP, "DHCP Server released IP address [" + lease.Address.ToString() + "] that was leased to " + lease.GetClientInfo() + " for scope: " + scope.Name); + _log?.Write(remoteEP, "DHCP Server released IP address [" + lease.Address.ToString() + "] that was leased to " + lease.GetClientInfo() + " for scope: " + scope.Name); //update dns UpdateDnsAuthZone(false, scope, lease); @@ -613,9 +595,7 @@ namespace DnsServerCore.Dhcp IPAddress serverIdentifierAddress = scope.InterfaceAddress.Equals(IPAddress.Any) ? ipPacketInformation.Address : scope.InterfaceAddress; //log inform - LogManager log = _log; - if (log != null) - log.Write(remoteEP, "DHCP Server received INFORM message from " + request.GetClientFullIdentifier() + " for scope: " + scope.Name); + _log?.Write(remoteEP, "DHCP Server received INFORM message from " + request.GetClientFullIdentifier() + " for scope: " + scope.Name); List options = await scope.GetOptionsAsync(request, serverIdentifierAddress, null, _dnsServer); if (options is null) @@ -752,7 +732,6 @@ namespace DnsServerCore.Dhcp string zoneName = null; string reverseDomain = Zone.GetReverseZone(address, 32); string reverseZoneName = null; - LogManager log = _log; if (add) { @@ -764,7 +743,7 @@ namespace DnsServerCore.Dhcp zoneInfo = _dnsServer.AuthZoneManager.CreatePrimaryZone(scope.DomainName, _dnsServer.ServerDomain, false); if (zoneInfo is null) { - log?.Write("DHCP Server failed to create DNS primary zone '" + scope.DomainName + "'."); + _log?.Write("DHCP Server failed to create DNS primary zone '" + scope.DomainName + "'."); return; } @@ -774,7 +753,7 @@ namespace DnsServerCore.Dhcp _authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _authManager.GetGroup(Group.DHCP_ADMINISTRATORS), PermissionFlag.ViewModifyDelete); _authManager.SaveConfigFile(); - log?.Write("DHCP Server create DNS primary zone '" + zoneInfo.Name + "'."); + _log?.Write("DHCP Server create DNS primary zone '" + zoneInfo.Name + "'."); _dnsServer.AuthZoneManager.SaveZoneFile(zoneInfo.Name); } else if ((zoneInfo.Type != AuthZoneType.Primary) && (zoneInfo.Type != AuthZoneType.Forwarder)) @@ -786,7 +765,7 @@ namespace DnsServerCore.Dhcp zoneInfo = _dnsServer.AuthZoneManager.CreatePrimaryZone(scope.DomainName, _dnsServer.ServerDomain, false); if (zoneInfo is null) { - log?.Write("DHCP Server failed to create DNS primary zone '" + scope.DomainName + "'."); + _log?.Write("DHCP Server failed to create DNS primary zone '" + scope.DomainName + "'."); return; } @@ -796,7 +775,7 @@ namespace DnsServerCore.Dhcp _authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _authManager.GetGroup(Group.DHCP_ADMINISTRATORS), PermissionFlag.ViewModifyDelete); _authManager.SaveConfigFile(); - log?.Write("DHCP Server create DNS primary zone '" + zoneInfo.Name + "'."); + _log?.Write("DHCP Server create DNS primary zone '" + zoneInfo.Name + "'."); _dnsServer.AuthZoneManager.SaveZoneFile(zoneInfo.Name); } @@ -821,7 +800,7 @@ namespace DnsServerCore.Dhcp } _dnsServer.AuthZoneManager.SetRecords(zoneName, domain, DnsResourceRecordType.A, scope.DnsTtl, new DnsResourceRecordData[] { new DnsARecordData(address) }); - log?.Write("DHCP Server updated DNS A record '" + domain + "' with IP address [" + address.ToString() + "]."); + _log?.Write("DHCP Server updated DNS A record '" + domain + "' with IP address [" + address.ToString() + "]."); //update reverse zone AuthZoneInfo reverseZoneInfo = _dnsServer.AuthZoneManager.FindAuthZoneInfo(reverseDomain); @@ -833,7 +812,7 @@ namespace DnsServerCore.Dhcp reverseZoneInfo = _dnsServer.AuthZoneManager.CreatePrimaryZone(reverseZone, _dnsServer.ServerDomain, false); if (reverseZoneInfo is null) { - log?.Write("DHCP Server failed to create DNS primary zone '" + reverseZone + "'."); + _log?.Write("DHCP Server failed to create DNS primary zone '" + reverseZone + "'."); return; } @@ -843,7 +822,7 @@ namespace DnsServerCore.Dhcp _authManager.SetPermission(PermissionSection.Zones, reverseZoneInfo.Name, _authManager.GetGroup(Group.DHCP_ADMINISTRATORS), PermissionFlag.ViewModifyDelete); _authManager.SaveConfigFile(); - log?.Write("DHCP Server create DNS primary zone '" + reverseZoneInfo.Name + "'."); + _log?.Write("DHCP Server create DNS primary zone '" + reverseZoneInfo.Name + "'."); _dnsServer.AuthZoneManager.SaveZoneFile(reverseZoneInfo.Name); } else if ((reverseZoneInfo.Type != AuthZoneType.Primary) && (reverseZoneInfo.Type != AuthZoneType.Forwarder)) @@ -857,7 +836,7 @@ namespace DnsServerCore.Dhcp reverseZoneInfo = _dnsServer.AuthZoneManager.CreatePrimaryZone(reverseZone, _dnsServer.ServerDomain, false); if (reverseZoneInfo is null) { - log?.Write("DHCP Server failed to create DNS primary zone '" + reverseZone + "'."); + _log?.Write("DHCP Server failed to create DNS primary zone '" + reverseZone + "'."); return; } @@ -867,13 +846,13 @@ namespace DnsServerCore.Dhcp _authManager.SetPermission(PermissionSection.Zones, reverseZoneInfo.Name, _authManager.GetGroup(Group.DHCP_ADMINISTRATORS), PermissionFlag.ViewModifyDelete); _authManager.SaveConfigFile(); - log?.Write("DHCP Server create DNS primary zone '" + reverseZoneInfo.Name + "'."); + _log?.Write("DHCP Server create DNS primary zone '" + reverseZoneInfo.Name + "'."); _dnsServer.AuthZoneManager.SaveZoneFile(reverseZoneInfo.Name); } reverseZoneName = reverseZoneInfo.Name; _dnsServer.AuthZoneManager.SetRecords(reverseZoneName, reverseDomain, DnsResourceRecordType.PTR, scope.DnsTtl, new DnsResourceRecordData[] { new DnsPTRRecordData(domain) }); - log?.Write("DHCP Server updated DNS PTR record '" + reverseDomain + "' with domain name '" + domain + "'."); + _log?.Write("DHCP Server updated DNS PTR record '" + reverseDomain + "' with domain name '" + domain + "'."); } else { @@ -884,7 +863,7 @@ namespace DnsServerCore.Dhcp //primary zone exists zoneName = zoneInfo.Name; _dnsServer.AuthZoneManager.DeleteRecord(zoneName, domain, DnsResourceRecordType.A, new DnsARecordData(address)); - log?.Write("DHCP Server deleted DNS A record '" + domain + "' with address [" + address.ToString() + "]."); + _log?.Write("DHCP Server deleted DNS A record '" + domain + "' with address [" + address.ToString() + "]."); } //remove from reverse zone @@ -894,7 +873,7 @@ namespace DnsServerCore.Dhcp //primary reverse zone exists reverseZoneName = reverseZoneInfo.Name; _dnsServer.AuthZoneManager.DeleteRecord(reverseZoneName, reverseDomain, DnsResourceRecordType.PTR, new DnsPTRRecordData(domain)); - log?.Write("DHCP Server deleted DNS PTR record '" + reverseDomain + "' with domain '" + domain + "'."); + _log?.Write("DHCP Server deleted DNS PTR record '" + reverseDomain + "' with domain '" + domain + "'."); } } @@ -908,9 +887,7 @@ namespace DnsServerCore.Dhcp } catch (Exception ex) { - LogManager log = _log; - if (log != null) - log.Write(ex); + _log?.Write(ex); } } @@ -936,9 +913,7 @@ namespace DnsServerCore.Dhcp } catch (Exception ex) { - LogManager log = _log; - if (log != null) - log.Write(ex); + _log?.Write(ex); } } } @@ -1076,17 +1051,13 @@ namespace DnsServerCore.Dhcp UpdateDnsAuthZone(utcNow < lease.Value.LeaseExpires, scope, lease.Value); //lease valid } - LogManager log = _log; - if (log != null) - log.Write(dhcpEP, "DHCP Server successfully activated scope: " + scope.Name); + _log?.Write(dhcpEP, "DHCP Server successfully activated scope: " + scope.Name); return true; } catch (Exception ex) { - LogManager log = _log; - if (log is not null) - log.Write(dhcpEP, "DHCP Server failed to activate scope: " + scope.Name + "\r\n" + ex.ToString()); + _log?.Write(dhcpEP, "DHCP Server failed to activate scope: " + scope.Name + "\r\n" + ex.ToString()); if (throwException) throw; @@ -1116,17 +1087,13 @@ namespace DnsServerCore.Dhcp UpdateDnsAuthZone(false, scope, lease.Value); } - LogManager log = _log; - if (log != null) - log.Write(dhcpEP, "DHCP Server successfully deactivated scope: " + scope.Name); + _log?.Write(dhcpEP, "DHCP Server successfully deactivated scope: " + scope.Name); return true; } catch (Exception ex) { - LogManager log = _log; - if (log is not null) - log.Write(dhcpEP, "DHCP Server failed to deactivate scope: " + scope.Name + "\r\n" + ex.ToString()); + _log?.Write(dhcpEP, "DHCP Server failed to deactivate scope: " + scope.Name + "\r\n" + ex.ToString()); if (throwException) throw; @@ -1154,9 +1121,7 @@ namespace DnsServerCore.Dhcp scope.SetEnabled(false); } - LogManager log = _log; - if (log != null) - log.Write("DHCP Server successfully loaded scope: " + scope.Name); + _log?.Write("DHCP Server successfully loaded scope: " + scope.Name); } private void UnloadScope(Scope scope) @@ -1168,9 +1133,7 @@ namespace DnsServerCore.Dhcp { removedScope.Dispose(); - LogManager log = _log; - if (log != null) - log.Write("DHCP Server successfully unloaded scope: " + scope.Name); + _log?.Write("DHCP Server successfully unloaded scope: " + scope.Name); } } @@ -1194,15 +1157,11 @@ namespace DnsServerCore.Dhcp await LoadScopeAsync(new Scope(new BinaryReader(fS), _log), true); } - LogManager log = _log; - if (log != null) - log.Write("DHCP Server successfully loaded scope file: " + scopeFile); + _log?.Write("DHCP Server successfully loaded scope file: " + scopeFile); } catch (Exception ex) { - LogManager log = _log; - if (log != null) - log.Write("DHCP Server failed to load scope file: " + scopeFile + "\r\n" + ex.ToString()); + _log?.Write("DHCP Server failed to load scope file: " + scopeFile + "\r\n" + ex.ToString()); } } @@ -1217,15 +1176,11 @@ namespace DnsServerCore.Dhcp scope.WriteTo(new BinaryWriter(fS)); } - LogManager log = _log; - if (log != null) - log.Write("DHCP Server successfully saved scope file: " + scopeFile); + _log?.Write("DHCP Server successfully saved scope file: " + scopeFile); } catch (Exception ex) { - LogManager log = _log; - if (log != null) - log.Write("DHCP Server failed to save scope file: " + scopeFile + "\r\n" + ex.ToString()); + _log?.Write("DHCP Server failed to save scope file: " + scopeFile + "\r\n" + ex.ToString()); } } @@ -1237,15 +1192,11 @@ namespace DnsServerCore.Dhcp { File.Delete(scopeFile); - LogManager log = _log; - if (log != null) - log.Write("DHCP Server successfully deleted scope file: " + scopeFile); + _log?.Write("DHCP Server successfully deleted scope file: " + scopeFile); } catch (Exception ex) { - LogManager log = _log; - if (log != null) - log.Write("DHCP Server failed to delete scope file: " + scopeFile + "\r\n" + ex.ToString()); + _log?.Write("DHCP Server failed to delete scope file: " + scopeFile + "\r\n" + ex.ToString()); } } @@ -1277,9 +1228,7 @@ namespace DnsServerCore.Dhcp List expiredLeases = scope.Value.RemoveExpiredLeases(); if (expiredLeases.Count > 0) { - LogManager log = _log; - if (log != null) - log.Write("DHCP Server removed " + expiredLeases.Count + " lease(s) from scope: " + scope.Value.Name); + _log?.Write("DHCP Server removed " + expiredLeases.Count + " lease(s) from scope: " + scope.Value.Name); foreach (Lease expiredLease in expiredLeases) UpdateDnsAuthZone(false, scope.Value, expiredLease); @@ -1290,9 +1239,7 @@ namespace DnsServerCore.Dhcp } catch (Exception ex) { - LogManager log = _log; - if (log != null) - log.Write(ex); + _log?.Write(ex); } finally { diff --git a/DnsServerCore/Dhcp/Lease.cs b/DnsServerCore/Dhcp/Lease.cs index 0635844e..89737bfc 100644 --- a/DnsServerCore/Dhcp/Lease.cs +++ b/DnsServerCore/Dhcp/Lease.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -88,7 +88,7 @@ namespace DnsServerCore.Dhcp _hostName = null; _hardwareAddress = bR.ReadBuffer(); - _address = IPAddressExtension.ReadFrom(bR); + _address = IPAddressExtensions.ReadFrom(bR); if (version >= 2) { diff --git a/DnsServerCore/Dhcp/Options/ClasslessStaticRouteOption.cs b/DnsServerCore/Dhcp/Options/ClasslessStaticRouteOption.cs index e59fe1cf..50dc25b5 100644 --- a/DnsServerCore/Dhcp/Options/ClasslessStaticRouteOption.cs +++ b/DnsServerCore/Dhcp/Options/ClasslessStaticRouteOption.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2021 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -109,7 +109,7 @@ namespace DnsServerCore.Dhcp.Options s.ReadBytes(destinationBuffer, 0, Convert.ToInt32(Math.Ceiling(Convert.ToDecimal(subnetMaskWidth) / 8))); _destination = new IPAddress(destinationBuffer); - _subnetMask = IPAddressExtension.GetSubnetMask(subnetMaskWidth); + _subnetMask = IPAddressExtensions.GetSubnetMask(subnetMaskWidth); _router = new IPAddress(s.ReadBytes(4)); } diff --git a/DnsServerCore/Dhcp/Options/TftpServerAddressOption.cs b/DnsServerCore/Dhcp/Options/TftpServerAddressOption.cs new file mode 100644 index 00000000..67588f1f --- /dev/null +++ b/DnsServerCore/Dhcp/Options/TftpServerAddressOption.cs @@ -0,0 +1,79 @@ +/* +Technitium DNS Server +Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +using System.Collections.Generic; +using System.IO; +using System.Net; +using TechnitiumLibrary.IO; + +namespace DnsServerCore.Dhcp.Options +{ + class TftpServerAddressOption : DhcpOption + { + #region variables + + IReadOnlyCollection _addresses; + + #endregion + + #region constructor + + public TftpServerAddressOption(IReadOnlyCollection addresses) + : base(DhcpOptionCode.TftpServerAddress) + { + _addresses = addresses; + } + + public TftpServerAddressOption(Stream s) + : base(DhcpOptionCode.TftpServerAddress, s) + { } + + #endregion + + #region protected + + protected override void ParseOptionValue(Stream s) + { + if ((s.Length % 4 != 0) || (s.Length < 4)) + throw new InvalidDataException(); + + IPAddress[] addresses = new IPAddress[s.Length / 4]; + + for (int i = 0; i < addresses.Length; i++) + addresses[i] = new IPAddress(s.ReadBytes(4)); + + _addresses = addresses; + } + + protected override void WriteOptionValue(Stream s) + { + foreach (IPAddress address in _addresses) + s.Write(address.GetAddressBytes()); + } + + #endregion + + #region properties + + public IReadOnlyCollection Addresses + { get { return _addresses; } } + + #endregion + } +} diff --git a/DnsServerCore/Dhcp/Options/VendorSpecificInformationOption.cs b/DnsServerCore/Dhcp/Options/VendorSpecificInformationOption.cs index a9de2681..cb7c83da 100644 --- a/DnsServerCore/Dhcp/Options/VendorSpecificInformationOption.cs +++ b/DnsServerCore/Dhcp/Options/VendorSpecificInformationOption.cs @@ -18,7 +18,6 @@ along with this program. If not, see . */ using System; -using System.Globalization; using System.IO; using TechnitiumLibrary.IO; @@ -37,7 +36,10 @@ namespace DnsServerCore.Dhcp.Options public VendorSpecificInformationOption(string hexInfo) : base(DhcpOptionCode.VendorSpecificInformation) { - _information = ParseHexString(hexInfo); + if (hexInfo.Contains(':')) + _information = ParseColonHexString(hexInfo); + else + _information = Convert.FromHexString(hexInfo); } public VendorSpecificInformationOption(byte[] information) @@ -52,42 +54,6 @@ namespace DnsServerCore.Dhcp.Options #endregion - #region private - - private static byte[] ParseHexString(string value) - { - int i; - int j = -1; - string strHex; - int b; - - using (MemoryStream mS = new MemoryStream()) - { - while (true) - { - i = value.IndexOf(':', j + 1); - if (i < 0) - i = value.Length; - - strHex = value.Substring(j + 1, i - j - 1); - - if (!int.TryParse(strHex, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out b) || (b < byte.MinValue) || (b > byte.MaxValue)) - throw new InvalidDataException("VendorSpecificInformation option data must be a colon (:) separated hex string."); - - mS.WriteByte((byte)b); - - if (i == value.Length) - break; - - j = i; - } - - return mS.ToArray(); - } - } - - #endregion - #region protected protected override void ParseOptionValue(Stream s) @@ -102,15 +68,6 @@ namespace DnsServerCore.Dhcp.Options #endregion - #region public - - public override string ToString() - { - return BitConverter.ToString(_information).Replace("-", ":"); - } - - #endregion - #region properties public byte[] Information diff --git a/DnsServerCore/Dhcp/Scope.cs b/DnsServerCore/Dhcp/Scope.cs index a2cd9afb..b33db552 100644 --- a/DnsServerCore/Dhcp/Scope.cs +++ b/DnsServerCore/Dhcp/Scope.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -76,8 +76,10 @@ namespace DnsServerCore.Dhcp IReadOnlyCollection _staticRoutes; IReadOnlyDictionary _vendorInfo; IReadOnlyCollection _capwapAcIpAddresses; + IReadOnlyCollection _tftpServerAddreses; //advanced options + IReadOnlyCollection _genericOptions; IReadOnlyCollection _exclusions; readonly ConcurrentDictionary _reservedLeases = new ConcurrentDictionary(); bool _allowOnlyReservedLeases; @@ -130,10 +132,11 @@ namespace DnsServerCore.Dhcp case 5: case 6: case 7: + case 8: _name = bR.ReadShortString(); _enabled = bR.ReadBoolean(); - ChangeNetwork(IPAddressExtension.ReadFrom(bR), IPAddressExtension.ReadFrom(bR), IPAddressExtension.ReadFrom(bR)); + ChangeNetwork(IPAddressExtensions.ReadFrom(bR), IPAddressExtensions.ReadFrom(bR), IPAddressExtensions.ReadFrom(bR)); _leaseTimeDays = bR.ReadUInt16(); _leaseTimeHours = bR.ReadByte(); @@ -172,7 +175,7 @@ namespace DnsServerCore.Dhcp if (version >= 2) { - _serverAddress = IPAddressExtension.ReadFrom(bR); + _serverAddress = IPAddressExtensions.ReadFrom(bR); if (_serverAddress.Equals(IPAddress.Any)) _serverAddress = null; } @@ -188,7 +191,7 @@ namespace DnsServerCore.Dhcp _bootFileName = null; } - _routerAddress = IPAddressExtension.ReadFrom(bR); + _routerAddress = IPAddressExtensions.ReadFrom(bR); if (_routerAddress.Equals(IPAddress.Any)) _routerAddress = null; @@ -206,7 +209,7 @@ namespace DnsServerCore.Dhcp IPAddress[] dnsServers = new IPAddress[count]; for (int i = 0; i < count; i++) - dnsServers[i] = IPAddressExtension.ReadFrom(bR); + dnsServers[i] = IPAddressExtensions.ReadFrom(bR); _dnsServers = dnsServers; } @@ -220,7 +223,7 @@ namespace DnsServerCore.Dhcp IPAddress[] winsServers = new IPAddress[count]; for (int i = 0; i < count; i++) - winsServers[i] = IPAddressExtension.ReadFrom(bR); + winsServers[i] = IPAddressExtensions.ReadFrom(bR); _winsServers = winsServers; } @@ -233,7 +236,7 @@ namespace DnsServerCore.Dhcp IPAddress[] ntpServers = new IPAddress[count]; for (int i = 0; i < count; i++) - ntpServers[i] = IPAddressExtension.ReadFrom(bR); + ntpServers[i] = IPAddressExtensions.ReadFrom(bR); _ntpServers = ntpServers; } @@ -293,12 +296,46 @@ namespace DnsServerCore.Dhcp IPAddress[] capwapAcIpAddresses = new IPAddress[count]; for (int i = 0; i < count; i++) - capwapAcIpAddresses[i] = IPAddressExtension.ReadFrom(bR); + capwapAcIpAddresses[i] = IPAddressExtensions.ReadFrom(bR); _capwapAcIpAddresses = capwapAcIpAddresses; } } + if (version >= 8) + { + int count = bR.ReadByte(); + if (count > 0) + { + IPAddress[] tftpServerAddreses = new IPAddress[count]; + + for (int i = 0; i < count; i++) + tftpServerAddreses[i] = IPAddressExtensions.ReadFrom(bR); + + _tftpServerAddreses = tftpServerAddreses; + } + } + + if (version >= 8) + { + int count = bR.ReadByte(); + if (count > 0) + { + DhcpOption[] genericOptions = new DhcpOption[count]; + + for (int i = 0; i < count; i++) + { + DhcpOptionCode code = (DhcpOptionCode)bR.ReadByte(); + short length = bR.ReadInt16(); + byte[] value = bR.ReadBytes(length); + + genericOptions[i] = new DhcpOption(code, value); + } + + _genericOptions = genericOptions; + } + } + { int count = bR.ReadByte(); if (count > 0) @@ -306,7 +343,7 @@ namespace DnsServerCore.Dhcp Exclusion[] exclusions = new Exclusion[count]; for (int i = 0; i < count; i++) - exclusions[i] = new Exclusion(IPAddressExtension.ReadFrom(bR), IPAddressExtension.ReadFrom(bR)); + exclusions[i] = new Exclusion(IPAddressExtensions.ReadFrom(bR), IPAddressExtensions.ReadFrom(bR)); _exclusions = exclusions; } @@ -955,12 +992,12 @@ namespace DnsServerCore.Dhcp return null; } - offerAddress = IPAddressExtension.ConvertNumberToIp(_startingAddress.ConvertIpToNumber() - 1u); + offerAddress = IPAddressExtensions.ConvertNumberToIp(_startingAddress.ConvertIpToNumber() - 1u); offerAddressWasResetFromEnd = true; continue; } - offerAddress = IPAddressExtension.ConvertNumberToIp(nextOfferAddressNumber); + offerAddress = IPAddressExtensions.ConvertNumberToIp(nextOfferAddressNumber); AddressStatus addressStatus = await IsAddressAvailableAsync(offerAddress); if (addressStatus.IsAddressAvailable) @@ -1121,6 +1158,27 @@ namespace DnsServerCore.Dhcp options.Add(new CAPWAPAccessControllerOption(_capwapAcIpAddresses)); break; + + case DhcpOptionCode.TftpServerAddress: + if (_tftpServerAddreses is not null) + options.Add(new TftpServerAddressOption(_tftpServerAddreses)); + + break; + + default: + if (_genericOptions is not null) + { + foreach (DhcpOption genericOption in _genericOptions) + { + if (optionCode == genericOption.Code) + { + options.Add(genericOption); + break; + } + } + } + + break; } } } @@ -1314,13 +1372,13 @@ namespace DnsServerCore.Dhcp if (broadcastAddressNumber == endingAddressNumber) throw new ArgumentException("Ending address cannot be same as the broadcast address."); - _networkAddress = IPAddressExtension.ConvertNumberToIp(networkAddressNumber); - _broadcastAddress = IPAddressExtension.ConvertNumberToIp(broadcastAddressNumber); + _networkAddress = IPAddressExtensions.ConvertNumberToIp(networkAddressNumber); + _broadcastAddress = IPAddressExtensions.ConvertNumberToIp(broadcastAddressNumber); _lastAddressOfferedLock.Wait(); try { - _lastAddressOffered = IPAddressExtension.ConvertNumberToIp(startingAddressNumber - 1u); + _lastAddressOffered = IPAddressExtensions.ConvertNumberToIp(startingAddressNumber - 1u); } finally { @@ -1429,7 +1487,7 @@ namespace DnsServerCore.Dhcp public void WriteTo(BinaryWriter bW) { bW.Write(Encoding.ASCII.GetBytes("SC")); - bW.Write((byte)7); //version + bW.Write((byte)8); //version bW.WriteShortString(_name); bW.Write(_enabled); @@ -1576,6 +1634,34 @@ namespace DnsServerCore.Dhcp capwapAcIpAddress.WriteTo(bW); } + if (_tftpServerAddreses is null) + { + bW.Write((byte)0); + } + else + { + bW.Write(Convert.ToByte(_tftpServerAddreses.Count)); + + foreach (IPAddress tftpServerAddress in _tftpServerAddreses) + tftpServerAddress.WriteTo(bW); + } + + if (_genericOptions is null) + { + bW.Write((byte)0); + } + else + { + bW.Write(Convert.ToByte(_genericOptions.Count)); + + foreach (DhcpOption genericOption in _genericOptions) + { + bW.Write((byte)genericOption.Code); + bW.Write(Convert.ToInt16(genericOption.RawValue.Length)); + bW.Write(genericOption.RawValue); + } + } + if (_exclusions is null) { bW.Write((byte)0); @@ -1898,6 +1984,22 @@ namespace DnsServerCore.Dhcp } } + public IReadOnlyCollection TftpServerAddresses + { + get { return _tftpServerAddreses; } + set + { + ValidateIpv4(value, nameof(TftpServerAddresses)); + _tftpServerAddreses = value; + } + } + + public IReadOnlyCollection GenericOptions + { + get { return _genericOptions; } + set { _genericOptions = value; } + } + public IReadOnlyCollection Exclusions { get { return _exclusions; } diff --git a/DnsServerCore/Dns/Applications/DnsServerInternal.cs b/DnsServerCore/Dns/Applications/DnsServerInternal.cs index e00783a8..836f3f66 100644 --- a/DnsServerCore/Dns/Applications/DnsServerInternal.cs +++ b/DnsServerCore/Dns/Applications/DnsServerInternal.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -53,6 +53,11 @@ namespace DnsServerCore.Dns.Applications return _dnsServer.DirectQueryAsync(question, timeout, true); } + public Task DirectQueryAsync(DnsDatagram request, int timeout = 4000) + { + return _dnsServer.DirectQueryAsync(request, timeout, true); + } + public void WriteLog(string message) { LogManager log = _dnsServer.LogManager; diff --git a/DnsServerCore/Dns/DnsServer.cs b/DnsServerCore/Dns/DnsServer.cs index ef59a9af..d71f67f9 100644 --- a/DnsServerCore/Dns/DnsServer.cs +++ b/DnsServerCore/Dns/DnsServer.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -23,29 +23,40 @@ using DnsServerCore.Dns.ResourceRecords; using DnsServerCore.Dns.Trees; using DnsServerCore.Dns.ZoneManagers; using DnsServerCore.Dns.Zones; -using Newtonsoft.Json; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.AspNetCore.Server.Kestrel.Https; +using Microsoft.AspNetCore.StaticFiles; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Logging; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Net; +using System.Net.Quic; using System.Net.Security; using System.Net.Sockets; using System.Runtime.ExceptionServices; using System.Security.Cryptography.X509Certificates; -using System.Text; using System.Threading; using System.Threading.Tasks; using TechnitiumLibrary; using TechnitiumLibrary.Net; using TechnitiumLibrary.Net.Dns; +using TechnitiumLibrary.Net.Dns.ClientConnection; using TechnitiumLibrary.Net.Dns.EDnsOptions; using TechnitiumLibrary.Net.Dns.ResourceRecords; -using TechnitiumLibrary.Net.Http; using TechnitiumLibrary.Net.Proxy; namespace DnsServerCore.Dns { +#pragma warning disable CA2252 // This API requires opting into preview features +#pragma warning disable CA1416 // Validate platform compatibility + public enum DnsServerRecursion : byte { Deny = 0, @@ -61,7 +72,7 @@ namespace DnsServerCore.Dns CustomAddress = 2 } - public sealed class DnsServer : IDisposable, IDnsClient + public sealed class DnsServer : IAsyncDisposable, IDisposable, IDnsClient { #region enum @@ -82,6 +93,8 @@ namespace DnsServerCore.Dns const int SERVE_STALE_WAIT_TIME = 1800; static readonly IPEndPoint IPENDPOINT_ANY_0 = new IPEndPoint(IPAddress.Any, 0); + static readonly IReadOnlyCollection _aRecords = new DnsARecordData[] { new DnsARecordData(IPAddress.Any) }; + static readonly IReadOnlyCollection _aaaaRecords = new DnsAAAARecordData[] { new DnsAAAARecordData(IPAddress.IPv6Any) }; string _serverDomain; readonly string _configFolder; @@ -93,16 +106,10 @@ namespace DnsServerCore.Dns readonly List _udpListeners = new List(); readonly List _tcpListeners = new List(); - readonly List _httpListeners = new List(); readonly List _tlsListeners = new List(); - readonly List _httpsListeners = new List(); + readonly List _quicListeners = new List(); - bool _enableDnsOverHttp; - bool _enableDnsOverTls; - bool _enableDnsOverHttps; - bool _isDnsOverHttpsEnabled; - X509Certificate2 _certificate; - IReadOnlyDictionary _tsigKeys; + WebApplication _dohWebService; readonly AuthZoneManager _authZoneManager; readonly AllowedZoneManager _allowedZoneManager; @@ -112,51 +119,72 @@ namespace DnsServerCore.Dns readonly DnsApplicationManager _dnsApplicationManager; readonly ResolverDnsCache _dnsCache; + readonly StatsManager _stats; - readonly IReadOnlyCollection _aRecords = new DnsARecordData[] { new DnsARecordData(IPAddress.Any) }; - readonly IReadOnlyCollection _aaaaRecords = new DnsAAAARecordData[] { new DnsAAAARecordData(IPAddress.IPv6Any) }; - - DnsServerRecursion _recursion; - IReadOnlyCollection _recursionDeniedNetworks; - IReadOnlyCollection _recursionAllowedNetworks; - NetProxy _proxy; - IReadOnlyList _forwarders; bool _preferIPv6; ushort _udpPayloadSize = DnsDatagram.EDNS_DEFAULT_UDP_PAYLOAD_SIZE; - bool _randomizeName; - bool _qnameMinimization; - bool _nsRevalidation; bool _dnssecValidation = true; + bool _eDnsClientSubnet; byte _eDnsClientSubnetIPv4PrefixLength = 24; byte _eDnsClientSubnetIPv6PrefixLength = 56; + int _qpmLimitRequests = 0; int _qpmLimitErrors = 0; int _qpmLimitSampleMinutes = 5; int _qpmLimitIPv4PrefixLength = 24; int _qpmLimitIPv6PrefixLength = 56; - int _forwarderRetries = 3; - int _resolverRetries = 2; - int _forwarderTimeout = 2000; - int _resolverTimeout = 2000; + int _clientTimeout = 4000; - int _forwarderConcurrency = 2; + int _tcpSendTimeout = 10000; + int _tcpReceiveTimeout = 10000; + int _quicIdleTimeout = 60000; + int _quicMaxInboundStreams = 100; + int _listenBacklog = 100; + + bool _enableDnsOverHttp; + bool _enableDnsOverTls; + bool _enableDnsOverHttps; + bool _enableDnsOverQuic; + int _dnsOverHttpPort = 80; + int _dnsOverTlsPort = 853; + int _dnsOverHttpsPort = 443; + int _dnsOverQuicPort = 853; + X509Certificate2 _certificate; + + IReadOnlyDictionary _tsigKeys; + + DnsServerRecursion _recursion; + IReadOnlyCollection _recursionDeniedNetworks; + IReadOnlyCollection _recursionAllowedNetworks; + + bool _randomizeName; + bool _qnameMinimization; + bool _nsRevalidation; + + int _resolverRetries = 2; + int _resolverTimeout = 2000; int _resolverMaxStackCount = 16; + bool _serveStale = true; int _cachePrefetchEligibility = 2; int _cachePrefetchTrigger = 9; int _cachePrefetchSampleIntervalInMinutes = 5; int _cachePrefetchSampleEligibilityHitsPerHour = 30; + bool _enableBlocking = true; bool _allowTxtBlockingReport = true; - DnsServerBlockingType _blockingType = DnsServerBlockingType.AnyAddress; + DnsServerBlockingType _blockingType = DnsServerBlockingType.NxDomain; IReadOnlyCollection _customBlockingARecords = Array.Empty(); IReadOnlyCollection _customBlockingAAAARecords = Array.Empty(); - LogManager _queryLog; - readonly StatsManager _stats; - int _tcpSendTimeout = 10000; - int _tcpReceiveTimeout = 10000; + NetProxy _proxy; + IReadOnlyList _forwarders; + int _forwarderRetries = 3; + int _forwarderTimeout = 2000; + int _forwarderConcurrency = 2; + + LogManager _queryLog; Timer _cachePrefetchSamplingTimer; readonly object _cachePrefetchSamplingTimerLock = new object(); @@ -179,6 +207,8 @@ namespace DnsServerCore.Dns IReadOnlyDictionary _qpmLimitClientSubnetStats; IReadOnlyDictionary _qpmLimitErrorClientSubnetStats; + readonly IndependentTaskScheduler _queryTaskScheduler = new IndependentTaskScheduler(); + readonly IndependentTaskScheduler _resolverTaskScheduler = new IndependentTaskScheduler(ThreadPriority.AboveNormal); readonly ConcurrentDictionary> _resolverTasks = new ConcurrentDictionary>(); @@ -204,9 +234,6 @@ namespace DnsServerCore.Dns ThreadPool.SetMinThreads(minWorker, minIOC); } - - if (ServicePointManager.DefaultConnectionLimit < 10) - ServicePointManager.DefaultConnectionLimit = 10; //concurrent http request limit required when using DNS-over-HTTPS forwarders } public DnsServer(string serverDomain, string configFolder, string dohwwwFolder, LogManager log = null) @@ -232,7 +259,7 @@ namespace DnsServerCore.Dns _cacheZoneManager = new CacheZoneManager(this); _dnsApplicationManager = new DnsApplicationManager(this); - _dnsCache = new ResolverDnsCache(_dnsApplicationManager, _authZoneManager, _cacheZoneManager, _log); + _dnsCache = new ResolverDnsCache(_dnsApplicationManager, _authZoneManager, _cacheZoneManager, _log, false); //init stats _stats = new StatsManager(this); @@ -244,31 +271,25 @@ namespace DnsServerCore.Dns bool _disposed; - private void Dispose(bool disposing) + public async ValueTask DisposeAsync() { if (_disposed) return; - if (disposing) - { - Stop(); + await StopAsync(); - if (_authZoneManager is not null) - _authZoneManager.Dispose(); + _authZoneManager?.Dispose(); - if (_dnsApplicationManager is not null) - _dnsApplicationManager.Dispose(); + _dnsApplicationManager?.Dispose(); - if (_stats is not null) - _stats.Dispose(); - } + _stats?.Dispose(); _disposed = true; } public void Dispose() { - Dispose(true); + DisposeAsync().Sync(); } #endregion @@ -329,7 +350,7 @@ namespace DnsServerCore.Dns if (result.RemoteEndPoint is not IPEndPoint remoteEP) continue; - if (IsQpmLimitCrossed(remoteEP)) + if (IsQpmLimitCrossed(remoteEP.Address)) continue; try @@ -347,9 +368,7 @@ namespace DnsServerCore.Dns } catch (Exception ex) { - LogManager log = _log; - if (log is not null) - log.Write(remoteEP, DnsTransportProtocol.Udp, ex); + _log?.Write(remoteEP, DnsTransportProtocol.Udp, ex); } } } @@ -370,10 +389,7 @@ namespace DnsServerCore.Dns if ((_state == ServiceState.Stopping) || (_state == ServiceState.Stopped)) return; //server stopping - LogManager log = _log; - if (log != null) - log.Write(ex); - + _log?.Write(ex); break; } } @@ -382,9 +398,7 @@ namespace DnsServerCore.Dns if ((_state == ServiceState.Stopping) || (_state == ServiceState.Stopped)) return; //server stopping - LogManager log = _log; - if (log is not null) - log.Write(ex); + _log?.Write(ex); } } @@ -392,7 +406,7 @@ namespace DnsServerCore.Dns { try { - DnsDatagram response = await PreProcessQueryAsync(request, remoteEP, DnsTransportProtocol.Udp, IsRecursionAllowed(remoteEP)); + DnsDatagram response = await PreProcessQueryAsync(request, remoteEP, DnsTransportProtocol.Udp, IsRecursionAllowed(remoteEP.Address)); if (response is null) return; //drop request @@ -435,10 +449,7 @@ namespace DnsServerCore.Dns await udpListener.SendToAsync(new ArraySegment(sendBuffer, 0, (int)sendBufferStream.Position), SocketFlags.None, remoteEP); } - LogManager queryLog = _queryLog; - if (queryLog is not null) - queryLog.Write(remoteEP, DnsTransportProtocol.Udp, request, response); - + _queryLog?.Write(remoteEP, DnsTransportProtocol.Udp, request, response); _stats.QueueUpdate(request, remoteEP, DnsTransportProtocol.Udp, response); } catch (Exception ex) @@ -446,17 +457,12 @@ namespace DnsServerCore.Dns if ((_state == ServiceState.Stopping) || (_state == ServiceState.Stopped)) return; //server stopping - LogManager queryLog = _queryLog; - if (queryLog is not null) - queryLog.Write(remoteEP, DnsTransportProtocol.Udp, request, null); - - LogManager log = _log; - if (log is not null) - log.Write(remoteEP, DnsTransportProtocol.Udp, ex); + _queryLog?.Write(remoteEP, DnsTransportProtocol.Udp, request, null); + _log?.Write(remoteEP, DnsTransportProtocol.Udp, ex); } } - private async Task AcceptConnectionAsync(Socket tcpListener, DnsTransportProtocol protocol, bool usingHttps) + private async Task AcceptConnectionAsync(Socket tcpListener, DnsTransportProtocol protocol) { IPEndPoint localEP = tcpListener.LocalEndPoint as IPEndPoint; @@ -470,7 +476,7 @@ namespace DnsServerCore.Dns { Socket socket = await tcpListener.AcceptAsync(); - _ = ProcessConnectionAsync(socket, protocol, usingHttps); + _ = ProcessConnectionAsync(socket, protocol); } } catch (SocketException ex) @@ -478,9 +484,7 @@ namespace DnsServerCore.Dns if (ex.SocketErrorCode == SocketError.OperationAborted) return; //server stopping - LogManager log = _log; - if (log is not null) - log.Write(localEP, protocol, ex); + _log?.Write(localEP, protocol, ex); } catch (ObjectDisposedException) { @@ -491,13 +495,11 @@ namespace DnsServerCore.Dns if ((_state == ServiceState.Stopping) || (_state == ServiceState.Stopped)) return; //server stopping - LogManager log = _log; - if (log is not null) - log.Write(localEP, protocol, ex); + _log?.Write(localEP, protocol, ex); } } - private async Task ProcessConnectionAsync(Socket socket, DnsTransportProtocol protocol, bool usingHttps) + private async Task ProcessConnectionAsync(Socket socket, DnsTransportProtocol protocol) { IPEndPoint remoteEP = null; @@ -508,59 +510,49 @@ namespace DnsServerCore.Dns switch (protocol) { case DnsTransportProtocol.Tcp: - await ReadStreamRequestAsync(new NetworkStream(socket), _tcpReceiveTimeout, remoteEP, protocol); + await ReadStreamRequestAsync(new NetworkStream(socket), remoteEP, protocol); break; case DnsTransportProtocol.Tls: SslStream tlsStream = new SslStream(new NetworkStream(socket)); - await tlsStream.AuthenticateAsServerAsync(_certificate); + await tlsStream.AuthenticateAsServerAsync(_certificate).WithTimeout(_tcpReceiveTimeout); - await ReadStreamRequestAsync(tlsStream, _tcpReceiveTimeout, remoteEP, protocol); + await ReadStreamRequestAsync(tlsStream, remoteEP, protocol); break; - case DnsTransportProtocol.Https: - Stream stream = new NetworkStream(socket); - - if (usingHttps) - { - SslStream httpsStream = new SslStream(stream); - await httpsStream.AuthenticateAsServerAsync(_certificate); - - stream = httpsStream; - } - - await ProcessDoHRequestAsync(stream, _tcpReceiveTimeout, remoteEP, usingHttps); - break; + default: + throw new InvalidOperationException(); } } + catch (TimeoutException) + { + //ignore timeout exception on TLS auth + } catch (IOException) { //ignore IO exceptions } catch (Exception ex) { - LogManager log = _log; - if (log is not null) - log.Write(remoteEP, protocol, ex); + _log?.Write(remoteEP, protocol, ex); } finally { - if (socket is not null) - socket.Dispose(); + socket.Dispose(); } } - private async Task ReadStreamRequestAsync(Stream stream, int receiveTimeout, IPEndPoint remoteEP, DnsTransportProtocol protocol) + private async Task ReadStreamRequestAsync(Stream stream, IPEndPoint remoteEP, DnsTransportProtocol protocol) { try { using MemoryStream readBuffer = new MemoryStream(64); - using MemoryStream writeBuffer = new MemoryStream(4096); + using MemoryStream writeBuffer = new MemoryStream(2048); using SemaphoreSlim writeSemaphore = new SemaphoreSlim(1, 1); while (true) { - if (IsQpmLimitCrossed(remoteEP)) + if (IsQpmLimitCrossed(remoteEP.Address)) break; DnsDatagram request; @@ -570,7 +562,7 @@ namespace DnsServerCore.Dns { Task task = DnsDatagram.ReadFromTcpAsync(stream, readBuffer, cancellationTokenSource.Token); - if (await Task.WhenAny(task, Task.Delay(receiveTimeout, cancellationTokenSource.Token)) != task) + if (await Task.WhenAny(task, Task.Delay(_tcpReceiveTimeout, cancellationTokenSource.Token)) != task) { //read timed out await stream.DisposeAsync(); @@ -596,9 +588,7 @@ namespace DnsServerCore.Dns } catch (Exception ex) { - LogManager log = _log; - if (log is not null) - log.Write(remoteEP, protocol, ex); + _log?.Write(remoteEP, protocol, ex); } } @@ -606,7 +596,7 @@ namespace DnsServerCore.Dns { try { - DnsDatagram response = await PreProcessQueryAsync(request, remoteEP, protocol, IsRecursionAllowed(remoteEP)); + DnsDatagram response = await PreProcessQueryAsync(request, remoteEP, protocol, IsRecursionAllowed(remoteEP.Address)); if (response is null) { await stream.DisposeAsync(); @@ -626,10 +616,7 @@ namespace DnsServerCore.Dns writeSemaphore.Release(); } - LogManager queryLog = _queryLog; - if (queryLog is not null) - queryLog.Write(remoteEP, protocol, request, response); - + _queryLog?.Write(remoteEP, protocol, request, response); _stats.QueueUpdate(request, remoteEP, protocol, response); } catch (IOException) @@ -638,343 +625,258 @@ namespace DnsServerCore.Dns } catch (Exception ex) { - LogManager queryLog = _queryLog; - if ((queryLog is not null) && (request is not null)) - queryLog.Write(remoteEP, protocol, request, null); + if (request is not null) + _queryLog.Write(remoteEP, protocol, request, null); - LogManager log = _log; - if (log is not null) - log.Write(remoteEP, protocol, ex); + _log?.Write(remoteEP, protocol, ex); } } - private async Task ProcessDoHRequestAsync(Stream stream, int receiveTimeout, IPEndPoint remoteEP, bool usingHttps) + private async Task AcceptQuicConnectionAsync(QuicListener quicListener) { - DnsDatagram dnsRequest = null; - DnsTransportProtocol dnsProtocol = DnsTransportProtocol.Https; - try { while (true) { - bool isSocketRemoteIpPrivate = NetUtilities.IsPrivateIP(remoteEP.Address); - HttpRequest httpRequest; + QuicConnection quicConnection = await quicListener.AcceptConnectionAsync(); - if (usingHttps || !isSocketRemoteIpPrivate) - { - //is HTTPS request or is over public IP - if (IsQpmLimitCrossed(remoteEP)) - break; - - httpRequest = await HttpRequest.ReadRequestAsync(stream, 512).WithTimeout(receiveTimeout); - if (httpRequest is null) - return; //connection closed gracefully by client - } - else - { - //is HTTP request (probably via reverse proxy) and is over private IP - httpRequest = await HttpRequest.ReadRequestAsync(stream, 512).WithTimeout(receiveTimeout); - if (httpRequest is null) - return; //connection closed gracefully by client - - string xRealIp = httpRequest.Headers["X-Real-IP"]; - if (IPAddress.TryParse(xRealIp, out IPAddress address)) - { - //get the real IP address of the requesting client from X-Real-IP header set in nginx proxy_pass block - remoteEP = new IPEndPoint(address, 0); - } - - if (IsQpmLimitCrossed(remoteEP)) - break; - } - - string requestConnection = httpRequest.Headers[HttpRequestHeader.Connection]; - if (string.IsNullOrEmpty(requestConnection)) - requestConnection = "close"; - - switch (httpRequest.RequestPath) - { - case "/dns-query": - if (!usingHttps && !isSocketRemoteIpPrivate) - { - //intentionally blocking public IP addresses from using DNS-over-HTTP (without TLS) - //this feature is intended to be used with an SSL terminated reverse proxy like nginx on private network - await SendErrorAsync(stream, "close", 403, "DNS-over-HTTPS (DoH) queries are supported only on HTTPS."); - return; - } - - DnsTransportProtocol protocol = DnsTransportProtocol.Udp; - - string strRequestAcceptTypes = httpRequest.Headers[HttpRequestHeader.Accept]; - if (string.IsNullOrEmpty(strRequestAcceptTypes)) - { - string strContentType = httpRequest.Headers[HttpRequestHeader.ContentType]; - if (strContentType == "application/dns-message") - protocol = DnsTransportProtocol.Https; - } - else - { - foreach (string acceptType in strRequestAcceptTypes.Split(',')) - { - if (acceptType == "application/dns-message") - { - protocol = DnsTransportProtocol.Https; - break; - } - else if (acceptType == "application/dns-json") - { - protocol = DnsTransportProtocol.HttpsJson; - dnsProtocol = DnsTransportProtocol.HttpsJson; - break; - } - } - } - - switch (protocol) - { - case DnsTransportProtocol.Https: - #region https wire format - { - switch (httpRequest.HttpMethod) - { - case "GET": - string strRequest = httpRequest.QueryString["dns"]; - if (string.IsNullOrEmpty(strRequest)) - throw new DnsServerException("Missing query string parameter: dns"); - - //convert from base64url to base64 - strRequest = strRequest.Replace('-', '+'); - strRequest = strRequest.Replace('_', '/'); - - //add padding - int x = strRequest.Length % 4; - if (x > 0) - strRequest = strRequest.PadRight(strRequest.Length - x + 4, '='); - - using (MemoryStream mS = new MemoryStream(Convert.FromBase64String(strRequest))) - { - dnsRequest = DnsDatagram.ReadFrom(mS); - } - - break; - - case "POST": - string strContentType = httpRequest.Headers[HttpRequestHeader.ContentType]; - if (string.IsNullOrEmpty(strContentType)) - throw new DnsServerException("Missing Content-Type header."); - - if (strContentType != "application/dns-message") - throw new NotSupportedException("DNS request type not supported: " + strContentType); - - using (MemoryStream mS = new MemoryStream(32)) - { - await httpRequest.InputStream.CopyToAsync(mS, 32); - - mS.Position = 0; - dnsRequest = DnsDatagram.ReadFrom(mS); - } - - break; - - default: - throw new NotSupportedException("DoH request type not supported."); - } - - DnsDatagram dnsResponse = await PreProcessQueryAsync(dnsRequest, remoteEP, protocol, IsRecursionAllowed(remoteEP)); - if (dnsResponse is null) - return; //drop request - - using (MemoryStream mS = new MemoryStream(512)) - { - dnsResponse.WriteTo(mS); - - mS.Position = 0; - await SendContentAsync(stream, requestConnection, "application/dns-message", mS); - } - - LogManager queryLog = _queryLog; - if (queryLog is not null) - queryLog.Write(remoteEP, protocol, dnsRequest, dnsResponse); - - _stats.QueueUpdate(dnsRequest, remoteEP, protocol, dnsResponse); - } - #endregion - break; - - case DnsTransportProtocol.HttpsJson: - #region https json format - { - string strName = httpRequest.QueryString["name"]; - if (string.IsNullOrEmpty(strName)) - throw new DnsServerException("Missing query string parameter: name"); - - string strType = httpRequest.QueryString["type"]; - if (string.IsNullOrEmpty(strType)) - strType = "1"; - - bool dnssecOk; - string strDO = httpRequest.QueryString["do"]; - if (string.IsNullOrEmpty(strDO)) - dnssecOk = false; - else - dnssecOk = bool.Parse(strDO); - - dnsRequest = new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, true, false, false, false, DnsResponseCode.NoError, new DnsQuestionRecord[] { new DnsQuestionRecord(strName.TrimEnd('.'), (DnsResourceRecordType)int.Parse(strType), DnsClass.IN) }, null, null, null, _udpPayloadSize, dnssecOk ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None); - - DnsDatagram dnsResponse = await PreProcessQueryAsync(dnsRequest, remoteEP, protocol, IsRecursionAllowed(remoteEP)); - if (dnsResponse is null) - return; //drop request - - using (MemoryStream mS = new MemoryStream(512)) - { - JsonTextWriter jsonWriter = new JsonTextWriter(new StreamWriter(mS)); - dnsResponse.WriteToJson(jsonWriter); - jsonWriter.Flush(); - - mS.Position = 0; - await SendContentAsync(stream, requestConnection, "application/dns-json; charset=utf-8", mS); - } - - LogManager queryLog = _queryLog; - if (queryLog is not null) - queryLog.Write(remoteEP, protocol, dnsRequest, dnsResponse); - - _stats.QueueUpdate(dnsRequest, remoteEP, protocol, dnsResponse); - } - #endregion - break; - - default: - await RedirectAsync(stream, httpRequest.Protocol, requestConnection, "https://" + httpRequest.Headers[HttpRequestHeader.Host]); - break; - } - - if (requestConnection.Equals("close", StringComparison.OrdinalIgnoreCase)) - return; - - break; - - default: - string path = httpRequest.RequestPath; - - if (!path.StartsWith("/") || path.Contains("/../") || path.Contains("/.../")) - { - await SendErrorAsync(stream, requestConnection, 404); - break; - } - - if (path == "/") - path = "/index.html"; - - path = Path.GetFullPath(_dohwwwFolder + path.Replace('/', Path.DirectorySeparatorChar)); - - if (!path.StartsWith(_dohwwwFolder) || !File.Exists(path)) - { - await SendErrorAsync(stream, requestConnection, 404); - break; - } - - await SendFileAsync(stream, requestConnection, path); - break; - } + _ = ProcessQuicConnectionAsync(quicConnection); } } - catch (TimeoutException) + catch (ObjectDisposedException) { - //ignore timeout exception - } - catch (IOException) - { - //ignore IO exceptions + //server stopped } catch (Exception ex) { - LogManager queryLog = _queryLog; - if ((queryLog is not null) && (dnsRequest is not null)) - queryLog.Write(remoteEP, dnsProtocol, dnsRequest, null); + if ((_state == ServiceState.Stopping) || (_state == ServiceState.Stopped)) + return; //server stopping - LogManager log = _log; - if (log is not null) - log.Write(remoteEP, dnsProtocol, ex); - - await SendErrorAsync(stream, "close", ex); + _log?.Write(quicListener.LocalEndPoint, DnsTransportProtocol.Quic, ex); } } - private static async Task SendContentAsync(Stream outputStream, string connection, string contentType, Stream content) - { - byte[] bufferHeader = Encoding.UTF8.GetBytes("HTTP/1.1 200 OK\r\nDate: " + DateTime.UtcNow.ToString("r") + "\r\nContent-Type: " + contentType + "\r\nContent-Length: " + content.Length + "\r\nX-Robots-Tag: noindex, nofollow\r\nConnection: " + connection + "\r\n\r\n"); - - await outputStream.WriteAsync(bufferHeader); - await content.CopyToAsync(outputStream); - await outputStream.FlushAsync(); - } - - private static Task SendErrorAsync(Stream outputStream, string connection, Exception ex) - { - return SendErrorAsync(outputStream, connection, 500, ex.ToString()); - } - - private static async Task SendErrorAsync(Stream outputStream, string connection, int statusCode, string message = null) + private async Task ProcessQuicConnectionAsync(QuicConnection quicConnection) { try { - string statusString = statusCode + " " + GetHttpStatusString((HttpStatusCode)statusCode); - byte[] bufferContent = Encoding.UTF8.GetBytes("" + statusString + "

" + statusString + "

" + (message is null ? "" : "

" + message + "

") + ""); - byte[] bufferHeader = Encoding.UTF8.GetBytes("HTTP/1.1 " + statusString + "\r\nDate: " + DateTime.UtcNow.ToString("r") + "\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: " + bufferContent.Length + "\r\nX-Robots-Tag: noindex, nofollow\r\nConnection: " + connection + "\r\n\r\n"); + while (true) + { + if (IsQpmLimitCrossed(quicConnection.RemoteEndPoint.Address)) + break; - await outputStream.WriteAsync(bufferHeader); - await outputStream.WriteAsync(bufferContent); - await outputStream.FlushAsync(); + QuicStream quicStream = await quicConnection.AcceptInboundStreamAsync(); + + _ = ProcessQuicStreamRequestAsync(quicStream, quicConnection.RemoteEndPoint); + } + } + catch (QuicException ex) + { + switch (ex.QuicError) + { + case QuicError.ConnectionIdle: + case QuicError.ConnectionAborted: + case QuicError.ConnectionTimeout: + break; + + default: + _log?.Write(quicConnection.RemoteEndPoint, DnsTransportProtocol.Quic, ex); + break; + } + } + catch (Exception ex) + { + _log?.Write(quicConnection.RemoteEndPoint, DnsTransportProtocol.Quic, ex); + } + finally + { + await quicConnection.DisposeAsync(); } - catch - { } } - private static async Task RedirectAsync(Stream outputStream, string protocol, string connection, string location) + private async Task ProcessQuicStreamRequestAsync(QuicStream quicStream, IPEndPoint remoteEP) { + MemoryStream sharedBuffer = new MemoryStream(512); + DnsDatagram request = null; + try { - string statusString = "302 Found"; - byte[] bufferContent = Encoding.UTF8.GetBytes("" + statusString + "

" + statusString + "

Location: " + location + "

"); - byte[] bufferHeader = Encoding.UTF8.GetBytes(protocol + " " + statusString + "\r\nDate: " + DateTime.UtcNow.ToString("r") + "\r\nLocation: " + location + "\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: " + bufferContent.Length + "\r\nX-Robots-Tag: noindex, nofollow\r\nConnection: " + connection + "\r\n\r\n"); + //read dns datagram with timeout + using (CancellationTokenSource cancellationTokenSource = new CancellationTokenSource()) + { + Task task = DnsDatagram.ReadFromTcpAsync(quicStream, sharedBuffer, cancellationTokenSource.Token); - await outputStream.WriteAsync(bufferHeader); - await outputStream.WriteAsync(bufferContent); - await outputStream.FlushAsync(); + if (await Task.WhenAny(task, Task.Delay(_tcpReceiveTimeout, cancellationTokenSource.Token)) != task) + { + //read timed out + quicStream.Abort(QuicAbortDirection.Both, (long)DnsOverQuicErrorCodes.DOQ_UNSPECIFIED_ERROR); + return; + } + + cancellationTokenSource.Cancel(); //cancel delay task + + request = await task; + } + + //process request async + DnsDatagram response = await PreProcessQueryAsync(request, remoteEP, DnsTransportProtocol.Quic, IsRecursionAllowed(remoteEP.Address)); + if (response is null) + return; //drop request + + //send response + await response.WriteToTcpAsync(quicStream, sharedBuffer); + + _queryLog?.Write(remoteEP, DnsTransportProtocol.Quic, request, response); + _stats.QueueUpdate(request, remoteEP, DnsTransportProtocol.Quic, response); } - catch - { } - } - - private static async Task SendFileAsync(Stream outputStream, string connection, string filePath) - { - using (FileStream fS = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) + catch (IOException) { - byte[] bufferHeader = Encoding.UTF8.GetBytes("HTTP/1.1 200 OK\r\nDate: " + DateTime.UtcNow.ToString("r") + "\r\nContent-Type: " + WebUtilities.GetContentType(filePath).MediaType + "\r\nContent-Length: " + fS.Length + "\r\nCache-Control: private, max-age=300\r\nX-Robots-Tag: noindex, nofollow\r\nConnection: " + connection + "\r\n\r\n"); - - await outputStream.WriteAsync(bufferHeader); - await fS.CopyToAsync(outputStream); - await outputStream.FlushAsync(); + //ignore QuicException / IOException } - } - - internal static string GetHttpStatusString(HttpStatusCode statusCode) - { - StringBuilder sb = new StringBuilder(); - - foreach (char c in statusCode.ToString().ToCharArray()) + catch (Exception ex) { - if (char.IsUpper(c) && sb.Length > 0) - sb.Append(' '); + if (request is not null) + _queryLog.Write(remoteEP, DnsTransportProtocol.Quic, request, null); - sb.Append(c); + _log?.Write(remoteEP, DnsTransportProtocol.Quic, ex); + } + finally + { + await sharedBuffer.DisposeAsync(); + await quicStream.DisposeAsync(); } - - return sb.ToString(); } - private bool IsRecursionAllowed(IPEndPoint remoteEP) + private async Task ProcessDoHRequestAsync(HttpContext context) + { + IPEndPoint remoteEP = context.GetRemoteEndPoint(); + DnsDatagram dnsRequest = null; + + try + { + HttpRequest request = context.Request; + HttpResponse response = context.Response; + + if (IsQpmLimitCrossed(remoteEP.Address)) + { + response.StatusCode = 429; + await response.WriteAsync("Too Many Requests"); + return; + } + + if (!request.IsHttps && !NetUtilities.IsPrivateIP(remoteEP.Address)) + { + //intentionally blocking public IP addresses from using DNS-over-HTTP (without TLS) + //this feature is intended to be used with an SSL terminated reverse proxy like nginx on private network + response.StatusCode = 403; + await response.WriteAsync("DNS-over-HTTPS (DoH) queries are supported only on HTTPS."); + return; + } + + switch (request.Method) + { + case "GET": + bool acceptsDoH = false; + string requestAccept = request.Headers["Accept"]; + if (!string.IsNullOrEmpty(requestAccept)) + { + foreach (string mediaType in requestAccept.Split(',')) + { + if (mediaType.Equals("application/dns-message", StringComparison.OrdinalIgnoreCase)) + { + acceptsDoH = true; + break; + } + } + } + + if (!acceptsDoH) + { + response.Redirect((request.IsHttps ? "https://" : "http://") + request.Headers["Host"]); + return; + } + + string dnsRequestBase64Url = request.Query["dns"]; + if (string.IsNullOrEmpty(dnsRequestBase64Url)) + { + response.StatusCode = 400; + await response.WriteAsync("Bad Request"); + return; + } + + //convert from base64url to base64 + dnsRequestBase64Url = dnsRequestBase64Url.Replace('-', '+'); + dnsRequestBase64Url = dnsRequestBase64Url.Replace('_', '/'); + + //add padding + int x = dnsRequestBase64Url.Length % 4; + if (x > 0) + dnsRequestBase64Url = dnsRequestBase64Url.PadRight(dnsRequestBase64Url.Length - x + 4, '='); + + using (MemoryStream mS = new MemoryStream(Convert.FromBase64String(dnsRequestBase64Url))) + { + dnsRequest = DnsDatagram.ReadFrom(mS); + } + + break; + + case "POST": + if (!string.Equals(request.Headers["Content-Type"], "application/dns-message", StringComparison.OrdinalIgnoreCase)) + { + response.StatusCode = 415; + await response.WriteAsync("Unsupported Media Type"); + return; + } + + using (MemoryStream mS = new MemoryStream(32)) + { + await request.Body.CopyToAsync(mS, 32); + + mS.Position = 0; + dnsRequest = DnsDatagram.ReadFrom(mS); + } + + break; + + default: + throw new InvalidOperationException(); + } + + DnsDatagram dnsResponse = await PreProcessQueryAsync(dnsRequest, remoteEP, DnsTransportProtocol.Https, IsRecursionAllowed(remoteEP.Address)); + if (dnsResponse is null) + { + //drop request + context.Connection.RequestClose(); + return; + } + + using (MemoryStream mS = new MemoryStream(512)) + { + dnsResponse.WriteTo(mS); + + mS.Position = 0; + response.ContentType = "application/dns-message"; + response.ContentLength = mS.Length; + + using (Stream s = response.Body) + { + await mS.CopyToAsync(s, 512); + } + } + + _queryLog?.Write(remoteEP, DnsTransportProtocol.Https, dnsRequest, dnsResponse); + _stats.QueueUpdate(dnsRequest, remoteEP, DnsTransportProtocol.Https, dnsResponse); + } + catch (Exception ex) + { + if (dnsRequest is not null) + _queryLog?.Write(remoteEP, DnsTransportProtocol.Https, dnsRequest, null); + + _log?.Write(remoteEP, DnsTransportProtocol.Https, ex); + } + } + + private bool IsRecursionAllowed(IPAddress remoteIP) { switch (_recursion) { @@ -982,24 +884,22 @@ namespace DnsServerCore.Dns return true; case DnsServerRecursion.AllowOnlyForPrivateNetworks: - switch (remoteEP.AddressFamily) + switch (remoteIP.AddressFamily) { case AddressFamily.InterNetwork: case AddressFamily.InterNetworkV6: - return NetUtilities.IsPrivateIP(remoteEP.Address); + return NetUtilities.IsPrivateIP(remoteIP); default: return false; } case DnsServerRecursion.UseSpecifiedNetworks: - IPAddress address = remoteEP.Address; - if (_recursionDeniedNetworks is not null) { foreach (NetworkAddress deniedNetworkAddress in _recursionDeniedNetworks) { - if (deniedNetworkAddress.Contains(address)) + if (deniedNetworkAddress.Contains(remoteIP)) return false; } } @@ -1008,12 +908,12 @@ namespace DnsServerCore.Dns { foreach (NetworkAddress allowedNetworkAddress in _recursionAllowedNetworks) { - if (allowedNetworkAddress.Contains(address)) + if (allowedNetworkAddress.Contains(remoteIP)) return true; } } - if (IPAddress.IsLoopback(address)) + if (IPAddress.IsLoopback(remoteIP)) return true; return false; @@ -1041,9 +941,7 @@ namespace DnsServerCore.Dns } catch (Exception ex) { - LogManager log = _log; - if (log is not null) - log.Write(remoteEP, protocol, ex); + _log?.Write(remoteEP, protocol, ex); } } @@ -1051,11 +949,7 @@ namespace DnsServerCore.Dns { //format error if (request.ParsingException is not IOException) - { - LogManager log = _log; - if (log is not null) - log.Write(remoteEP, protocol, request.ParsingException); - } + _log?.Write(remoteEP, protocol, request.ParsingException); //format error response return new DnsDatagram(request.Identifier, true, request.OPCODE, false, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.FormatError, request.Question, null, null, null, request.EDNS is null ? ushort.MinValue : _udpPayloadSize, request.DnssecOk ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None) { Tag = DnsServerResponseType.Authoritative }; @@ -1065,9 +959,7 @@ namespace DnsServerCore.Dns { if (!request.VerifySignedRequest(_tsigKeys, out DnsDatagram unsignedRequest, out DnsDatagram errorResponse)) { - LogManager log = _log; - if (log is not null) - log.Write(remoteEP, protocol, "DNS Server received a request that failed TSIG signature verification (RCODE: " + errorResponse.RCODE + "; TSIG Error: " + errorResponse.TsigError + ")"); + _log?.Write(remoteEP, protocol, "DNS Server received a request that failed TSIG signature verification (RCODE: " + errorResponse.RCODE + "; TSIG Error: " + errorResponse.TsigError + ")"); errorResponse.Tag = DnsServerResponseType.Authoritative; return errorResponse; @@ -1096,9 +988,7 @@ namespace DnsServerCore.Dns } catch (Exception ex) { - LogManager log = _log; - if (log is not null) - log.Write(remoteEP, protocol, ex); + _log?.Write(remoteEP, protocol, ex); } } @@ -1117,7 +1007,7 @@ namespace DnsServerCore.Dns EDnsClientSubnetOptionData requestECS = request.GetEDnsClientSubnetOption(true); if ((requestECS is not null) && (request.Question.Count == 1) && CacheZone.IsTypeSupportedForEDnsClientSubnet(request.Question[0].Type)) - options = EDnsClientSubnetOptionData.GetEDnsClientSubnetOption(requestECS.SourcePrefixLength, 0, requestECS.AddressValue); + options = EDnsClientSubnetOptionData.GetEDnsClientSubnetOption(requestECS.SourcePrefixLength, 0, requestECS.Address); if (response.Additional.Count == 0) return response.Clone(null, null, new DnsResourceRecord[] { DnsDatagramEdns.GetOPTFor(_udpPayloadSize, response.RCODE, 0, request.DnssecOk ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None, options) }); @@ -1186,7 +1076,7 @@ namespace DnsServerCore.Dns if ((question.Type == DnsResourceRecordType.ANY) && (protocol == DnsTransportProtocol.Udp)) //force TCP for ANY request return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, true, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question) { Tag = DnsServerResponseType.Authoritative }; - return await ProcessRecursiveQueryAsync(request, remoteEP, protocol, null, _dnssecValidation, false); + return await ProcessRecursiveQueryAsync(request, remoteEP, protocol, null, _dnssecValidation, false, skipDnsAppAuthoritativeRequestHandlers); } catch (InvalidDomainNameException) { @@ -1195,9 +1085,7 @@ namespace DnsServerCore.Dns } catch (Exception ex) { - LogManager log = _log; - if (log is not null) - log.Write(remoteEP, protocol, ex); + _log?.Write(remoteEP, protocol, ex); return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.ServerFailure, request.Question) { Tag = DnsServerResponseType.Authoritative }; } @@ -1234,11 +1122,13 @@ namespace DnsServerCore.Dns } if (!remoteVerified) - return new DnsDatagram(request.Identifier, true, DnsOpcode.Notify, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.Authoritative }; + { + _log?.Write(remoteEP, protocol, "DNS Server refused a NOTIFY request since the request IP address was not recognized by the secondary zone: " + (authZoneInfo.Name == "" ? "" : authZoneInfo.Name)); - LogManager log = _log; - if (log is not null) - log.Write(remoteEP, protocol, "DNS Server received NOTIFY for zone: " + (authZoneInfo.Name == "" ? "" : authZoneInfo.Name)); + return new DnsDatagram(request.Identifier, true, DnsOpcode.Notify, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.Authoritative }; + } + + _log?.Write(remoteEP, protocol, "DNS Server received a NOTIFY request for secondary zone: " + (authZoneInfo.Name == "" ? "" : authZoneInfo.Name)); if ((request.Answer.Count > 0) && (request.Answer[0].Type == DnsResourceRecordType.SOA)) { @@ -1267,9 +1157,7 @@ namespace DnsServerCore.Dns if ((authZoneInfo is null) || authZoneInfo.Disabled) return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NotAuth, request.Question) { Tag = DnsServerResponseType.Authoritative }; - LogManager log = _log; - if (log is not null) - log.Write(remoteEP, protocol, "DNS Server received UPDATE request for zone: " + (authZoneInfo.Name == "" ? "" : authZoneInfo.Name)); + _log?.Write(remoteEP, protocol, "DNS Server received a zone UPDATE request for zone: " + (authZoneInfo.Name == "" ? "" : authZoneInfo.Name)); async Task IsZoneNameServerAllowedAsync() { @@ -1331,8 +1219,7 @@ namespace DnsServerCore.Dns if (!isUpdateAllowed) { - if (log is not null) - log.Write(remoteEP, protocol, "DNS Server refused an UPDATE request since the request IP address is not allowed by the zone: " + (authZoneInfo.Name == "" ? "" : authZoneInfo.Name)); + _log?.Write(remoteEP, protocol, "DNS Server refused a zone UPDATE request since the request IP address is not allowed by the zone: " + (authZoneInfo.Name == "" ? "" : authZoneInfo.Name)); return false; } @@ -1342,8 +1229,7 @@ namespace DnsServerCore.Dns { if ((tsigAuthenticatedKeyName is null) || !authZoneInfo.UpdateSecurityPolicies.TryGetValue(tsigAuthenticatedKeyName.ToLower(), out IReadOnlyDictionary> policyMap)) { - if (log is not null) - log.Write(remoteEP, protocol, "DNS Server refused a zone UPDATE request since the request is missing TSIG auth required by the zone: " + (authZoneInfo.Name == "" ? "" : authZoneInfo.Name)); + _log?.Write(remoteEP, protocol, "DNS Server refused a zone UPDATE request since the request is missing TSIG auth required by the zone: " + (authZoneInfo.Name == "" ? "" : authZoneInfo.Name)); return false; } @@ -1393,7 +1279,7 @@ namespace DnsServerCore.Dns foreach (DnsResourceRecord prRecord in request.Answer) { - if (prRecord.TtlValue != 0) + if (prRecord.TTL != 0) return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative }; AuthZoneInfo prAuthZoneInfo = _authZoneManager.FindAuthZoneInfo(prRecord.Name); @@ -1507,7 +1393,11 @@ namespace DnsServerCore.Dns //check for permissions if (!await IsUpdatePermittedAsync()) + { + _log?.Write(remoteEP, protocol, "DNS Server refused a zone UPDATE request due to Dynamic Updates Security Policy for zone: " + (authZoneInfo.Name == "" ? "" : authZoneInfo.Name)); + return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.Authoritative }; + } //process update section { @@ -1532,7 +1422,7 @@ namespace DnsServerCore.Dns } else if (uRecord.Class == DnsClass.ANY) { - if ((uRecord.TtlValue != 0) || (uRecord.RDATA.RDLENGTH != 0)) + if ((uRecord.TTL != 0) || (uRecord.RDATA.RDLENGTH != 0)) return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative }; switch (uRecord.Type) @@ -1546,7 +1436,7 @@ namespace DnsServerCore.Dns } else if (uRecord.Class == DnsClass.NONE) { - if (uRecord.TtlValue != 0) + if (uRecord.TTL != 0) return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative }; switch (uRecord.Type) @@ -1746,8 +1636,7 @@ namespace DnsServerCore.Dns _authZoneManager.SaveZoneFile(authZoneInfo.Name); - if (log is not null) - log.Write(remoteEP, protocol, "DNS Server processes UPDATE request for zone: " + (authZoneInfo.Name == "" ? "" : authZoneInfo.Name)); + _log?.Write(remoteEP, protocol, "DNS Server successfully processed a zone UPDATE request for zone: " + (authZoneInfo.Name == "" ? "" : authZoneInfo.Name)); //NOERROR return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question) { Tag = DnsServerResponseType.Authoritative }; @@ -1759,37 +1648,45 @@ namespace DnsServerCore.Dns IReadOnlyList primaryNameServers = await authZoneInfo.GetPrimaryNameServerAddressesAsync(this); DnsResourceRecord soaRecord = authZoneInfo.GetApexRecords(DnsResourceRecordType.SOA)[0]; - DnsResourceRecordInfo recordInfo = soaRecord.GetRecordInfo(); + AuthRecordInfo recordInfo = soaRecord.GetAuthRecordInfo(); - if (recordInfo.ZoneTransferProtocol == DnsTransportProtocol.Tls) + switch (recordInfo.ZoneTransferProtocol) { - //change name server protocol to TLS - List tcpNameServers = new List(primaryNameServers.Count); + case DnsTransportProtocol.Tls: + case DnsTransportProtocol.Quic: + { + //change name server protocol to TLS/QUIC + List updatedNameServers = new List(primaryNameServers.Count); - foreach (NameServerAddress primaryNameServer in primaryNameServers) - { - if (primaryNameServer.Protocol == DnsTransportProtocol.Tls) - tcpNameServers.Add(primaryNameServer); - else - tcpNameServers.Add(primaryNameServer.ChangeProtocol(DnsTransportProtocol.Tls)); - } + foreach (NameServerAddress primaryNameServer in primaryNameServers) + { + if (primaryNameServer.Protocol == recordInfo.ZoneTransferProtocol) + updatedNameServers.Add(primaryNameServer); + else + updatedNameServers.Add(primaryNameServer.ChangeProtocol(recordInfo.ZoneTransferProtocol)); + } - primaryNameServers = tcpNameServers; - } - else if (protocol == DnsTransportProtocol.Tcp) - { - //change name server protocol to TCP - List tcpNameServers = new List(primaryNameServers.Count); + primaryNameServers = updatedNameServers; + } + break; - foreach (NameServerAddress primaryNameServer in primaryNameServers) - { - if (primaryNameServer.Protocol == DnsTransportProtocol.Tcp) - tcpNameServers.Add(primaryNameServer); - else - tcpNameServers.Add(primaryNameServer.ChangeProtocol(DnsTransportProtocol.Tcp)); - } + default: + if (protocol == DnsTransportProtocol.Tcp) + { + //change name server protocol to TCP + List updatedNameServers = new List(primaryNameServers.Count); - primaryNameServers = tcpNameServers; + foreach (NameServerAddress primaryNameServer in primaryNameServers) + { + if (primaryNameServer.Protocol == DnsTransportProtocol.Tcp) + updatedNameServers.Add(primaryNameServer); + else + updatedNameServers.Add(primaryNameServer.ChangeProtocol(DnsTransportProtocol.Tcp)); + } + + primaryNameServers = updatedNameServers; + } + break; } TsigKey key = null; @@ -1827,13 +1724,10 @@ namespace DnsServerCore.Dns private async Task ProcessZoneTransferQueryAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, string tsigAuthenticatedKeyName) { - LogManager log = _log; - AuthZoneInfo authZoneInfo = _authZoneManager.GetAuthZoneInfo(request.Question[0].Name); if ((authZoneInfo is null) || authZoneInfo.Disabled || authZoneInfo.IsExpired) { - if (log is not null) - log.Write(remoteEP, protocol, "DNS Server refused a zone transfer request due to zone not found, zone disabled, or zone expired reasons for zone: " + (authZoneInfo.Name == "" ? "" : authZoneInfo.Name)); + _log?.Write(remoteEP, protocol, "DNS Server refused a zone transfer request due to zone not found, zone disabled, or zone expired reasons for zone: " + (authZoneInfo.Name == "" ? "" : authZoneInfo.Name)); return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.Authoritative }; } @@ -1845,8 +1739,7 @@ namespace DnsServerCore.Dns break; default: - if (log is not null) - log.Write(remoteEP, protocol, "DNS Server refused a zone transfer request since the DNS server is not authoritative for zone: " + (authZoneInfo.Name == "" ? "" : authZoneInfo.Name)); + _log?.Write(remoteEP, protocol, "DNS Server refused a zone transfer request since the DNS server is not authoritative for zone: " + (authZoneInfo.Name == "" ? "" : authZoneInfo.Name)); return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.Authoritative }; } @@ -1907,8 +1800,7 @@ namespace DnsServerCore.Dns if (!isZoneTransferAllowed) { - if (log is not null) - log.Write(remoteEP, protocol, "DNS Server refused a zone transfer request since the request IP address is not allowed by the zone: " + (authZoneInfo.Name == "" ? "" : authZoneInfo.Name)); + _log?.Write(remoteEP, protocol, "DNS Server refused a zone transfer request since the request IP address is not allowed by the zone: " + (authZoneInfo.Name == "" ? "" : authZoneInfo.Name)); return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.Authoritative }; } @@ -1917,15 +1809,13 @@ namespace DnsServerCore.Dns { if ((tsigAuthenticatedKeyName is null) || !authZoneInfo.ZoneTransferTsigKeyNames.ContainsKey(tsigAuthenticatedKeyName.ToLower())) { - if (log is not null) - log.Write(remoteEP, protocol, "DNS Server refused a zone transfer request since the request is missing TSIG auth required by the zone: " + (authZoneInfo.Name == "" ? "" : authZoneInfo.Name)); + _log?.Write(remoteEP, protocol, "DNS Server refused a zone transfer request since the request is missing TSIG auth required by the zone: " + (authZoneInfo.Name == "" ? "" : authZoneInfo.Name)); return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.Authoritative }; } } - if (log is not null) - log.Write(remoteEP, protocol, "DNS Server received zone transfer request for zone: " + (authZoneInfo.Name == "" ? "" : authZoneInfo.Name)); + _log?.Write(remoteEP, protocol, "DNS Server received zone transfer request for zone: " + (authZoneInfo.Name == "" ? "" : authZoneInfo.Name)); IReadOnlyList xfrRecords; @@ -1968,16 +1858,14 @@ namespace DnsServerCore.Dns } catch (Exception ex) { - LogManager log = _log; - if (log is not null) - log.Write(remoteEP, protocol, ex); + _log?.Write(remoteEP, protocol, ex); } } } if (response is null) { - response = _authZoneManager.Query(request, isRecursionAllowed); + response = _authZoneManager.Query(request); if (response is null) return null; @@ -2001,10 +1889,10 @@ namespace DnsServerCore.Dns switch (lastRR.Type) { case DnsResourceRecordType.CNAME: - return await ProcessCNAMEAsync(request, remoteEP, response, isRecursionAllowed, protocol, false); + return await ProcessCNAMEAsync(request, remoteEP, response, isRecursionAllowed, protocol, false, skipDnsAppAuthoritativeRequestHandlers); case DnsResourceRecordType.ANAME: - return await ProcessANAMEAsync(request, remoteEP, response, isRecursionAllowed, protocol); + return await ProcessANAMEAsync(request, remoteEP, response, isRecursionAllowed, protocol, skipDnsAppAuthoritativeRequestHandlers); } } } @@ -2017,7 +1905,7 @@ namespace DnsServerCore.Dns if (request.RecursionDesired && isRecursionAllowed) { //do forced recursive resolution using empty conditional forwarders; name servers will be provided via ResolverDnsCache - return await ProcessRecursiveQueryAsync(request, remoteEP, protocol, Array.Empty(), _dnssecValidation, false); + return await ProcessRecursiveQueryAsync(request, remoteEP, protocol, Array.Empty(), _dnssecValidation, false, skipDnsAppAuthoritativeRequestHandlers); } break; @@ -2026,12 +1914,12 @@ namespace DnsServerCore.Dns if ((response.Authority.Count == 1) && (firstAuthority.RDATA is DnsForwarderRecordData fwd) && fwd.Forwarder.Equals("this-server", StringComparison.OrdinalIgnoreCase)) { //do conditional forwarding via "this-server" - return await ProcessRecursiveQueryAsync(request, remoteEP, protocol, null, fwd.DnssecValidation, false); + return await ProcessRecursiveQueryAsync(request, remoteEP, protocol, null, fwd.DnssecValidation, false, skipDnsAppAuthoritativeRequestHandlers); } else { //do conditional forwarding - return await ProcessRecursiveQueryAsync(request, remoteEP, protocol, response.Authority, _dnssecValidation, false); + return await ProcessRecursiveQueryAsync(request, remoteEP, protocol, response.Authority, _dnssecValidation, false, skipDnsAppAuthoritativeRequestHandlers); } case DnsResourceRecordType.APP: @@ -2058,14 +1946,17 @@ namespace DnsServerCore.Dns { AuthZoneInfo zoneInfo = _authZoneManager.FindAuthZoneInfo(appResourceRecord.Name); - DnsDatagram appResponse = await appRecordRequestHandler.ProcessRequestAsync(request, remoteEP, protocol, isRecursionAllowed, zoneInfo.Name, appResourceRecord.Name, appResourceRecord.TtlValue, appRecord.Data); + DnsDatagram appResponse = await appRecordRequestHandler.ProcessRequestAsync(request, remoteEP, protocol, isRecursionAllowed, zoneInfo.Name, appResourceRecord.Name, appResourceRecord.TTL, appRecord.Data); if (appResponse is null) { + DnsResponseCode rcode; IReadOnlyList authority = null; if (zoneInfo.Type == AuthZoneType.Forwarder) { //return FWD response + rcode = DnsResponseCode.NoError; + if (!zoneInfo.Name.Equals(appResourceRecord.Name, StringComparison.OrdinalIgnoreCase)) { AuthZone authZone = _authZoneManager.GetAuthZone(zoneInfo.Name, appResourceRecord.Name); @@ -2078,11 +1969,16 @@ namespace DnsServerCore.Dns } else { - //return NO DATA response + //return NODATA/NXDOMAIN response + if (request.Question[0].Name.Length > appResourceRecord.Name.Length) + rcode = DnsResponseCode.NxDomain; + else + rcode = DnsResponseCode.NoError; + authority = zoneInfo.GetApexRecords(DnsResourceRecordType.SOA); } - return new DnsDatagram(request.Identifier, true, request.OPCODE, false, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, null, authority) { Tag = DnsServerResponseType.Authoritative }; + return new DnsDatagram(request.Identifier, true, request.OPCODE, false, false, request.RecursionDesired, isRecursionAllowed, false, false, rcode, request.Question, null, authority) { Tag = DnsServerResponseType.Authoritative }; } else { @@ -2094,16 +1990,12 @@ namespace DnsServerCore.Dns } else { - LogManager log = _log; - if (log is not null) - log.Write(remoteEP, protocol, "DNS request handler '" + appRecord.ClassPath + "' was not found in the application '" + appRecord.AppName + "': " + appResourceRecord.Name); + _log?.Write(remoteEP, protocol, "DNS request handler '" + appRecord.ClassPath + "' was not found in the application '" + appRecord.AppName + "': " + appResourceRecord.Name); } } else { - LogManager log = _log; - if (log is not null) - log.Write(remoteEP, protocol, "DNS application '" + appRecord.AppName + "' was not found: " + appResourceRecord.Name); + _log?.Write(remoteEP, protocol, "DNS application '" + appRecord.AppName + "' was not found: " + appResourceRecord.Name); } //return server failure response with SOA @@ -2115,7 +2007,7 @@ namespace DnsServerCore.Dns } } - private async Task ProcessCNAMEAsync(DnsDatagram request, IPEndPoint remoteEP, DnsDatagram response, bool isRecursionAllowed, DnsTransportProtocol protocol, bool cacheRefreshOperation) + private async Task ProcessCNAMEAsync(DnsDatagram request, IPEndPoint remoteEP, DnsDatagram response, bool isRecursionAllowed, DnsTransportProtocol protocol, bool cacheRefreshOperation, bool skipDnsAppAuthoritativeRequestHandlers) { List newAnswer = new List(response.Answer.Count + 4); newAnswer.AddRange(response.Answer); @@ -2167,14 +2059,14 @@ namespace DnsServerCore.Dns DnsDatagram newRequest = new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, new DnsQuestionRecord[] { new DnsQuestionRecord(cnameDomain, request.Question[0].Type, request.Question[0].Class) }, null, null, null, _udpPayloadSize, request.DnssecOk ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None, eDnsClientSubnetOption); //query authoritative zone first - newResponse = _authZoneManager.Query(newRequest, isRecursionAllowed); + newResponse = _authZoneManager.Query(newRequest); if (newResponse is null) { //not found in auth zone if (newRequest.RecursionDesired && isRecursionAllowed) { //do recursion - newResponse = await RecursiveResolveAsync(newRequest, remoteEP, null, _dnssecValidation, false, cacheRefreshOperation); + newResponse = await RecursiveResolveAsync(newRequest, remoteEP, null, _dnssecValidation, false, cacheRefreshOperation, skipDnsAppAuthoritativeRequestHandlers); isAuthoritativeAnswer = false; } else @@ -2185,7 +2077,7 @@ namespace DnsServerCore.Dns } else if ((newResponse.Answer.Count > 0) && (newResponse.GetLastAnswerRecord().Type == DnsResourceRecordType.ANAME)) { - newResponse = await ProcessANAMEAsync(request, remoteEP, newResponse, isRecursionAllowed, protocol); + newResponse = await ProcessANAMEAsync(request, remoteEP, newResponse, isRecursionAllowed, protocol, skipDnsAppAuthoritativeRequestHandlers); } else if ((newResponse.Answer.Count == 0) && (newResponse.Authority.Count > 0)) { @@ -2197,7 +2089,7 @@ namespace DnsServerCore.Dns if (newRequest.RecursionDesired && isRecursionAllowed) { //do forced recursive resolution using empty conditional forwarders; name servers will be provided via ResolveDnsCache - newResponse = await RecursiveResolveAsync(newRequest, remoteEP, Array.Empty(), _dnssecValidation, false, false); + newResponse = await RecursiveResolveAsync(newRequest, remoteEP, Array.Empty(), _dnssecValidation, false, false, skipDnsAppAuthoritativeRequestHandlers); isAuthoritativeAnswer = false; } @@ -2207,13 +2099,13 @@ namespace DnsServerCore.Dns if ((newResponse.Authority.Count == 1) && (firstAuthority.RDATA is DnsForwarderRecordData fwd) && fwd.Forwarder.Equals("this-server", StringComparison.OrdinalIgnoreCase)) { //do conditional forwarding via "this-server" - newResponse = await RecursiveResolveAsync(newRequest, remoteEP, null, fwd.DnssecValidation, false, false); + newResponse = await RecursiveResolveAsync(newRequest, remoteEP, null, fwd.DnssecValidation, false, false, skipDnsAppAuthoritativeRequestHandlers); isAuthoritativeAnswer = false; } else { //do conditional forwarding - newResponse = await RecursiveResolveAsync(newRequest, remoteEP, newResponse.Authority, _dnssecValidation, false, false); + newResponse = await RecursiveResolveAsync(newRequest, remoteEP, newResponse.Authority, _dnssecValidation, false, false, skipDnsAppAuthoritativeRequestHandlers); isAuthoritativeAnswer = false; } @@ -2300,7 +2192,7 @@ namespace DnsServerCore.Dns return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, isAuthoritativeAnswer, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, rcode, request.Question, newAnswer, authority, additional) { Tag = response.Tag }; } - private async Task ProcessANAMEAsync(DnsDatagram request, IPEndPoint remoteEP, DnsDatagram response, bool isRecursionAllowed, DnsTransportProtocol protocol) + private async Task ProcessANAMEAsync(DnsDatagram request, IPEndPoint remoteEP, DnsDatagram response, bool isRecursionAllowed, DnsTransportProtocol protocol, bool skipDnsAppAuthoritativeRequestHandlers) { EDnsOption[] eDnsClientSubnetOption = null; @@ -2324,11 +2216,11 @@ namespace DnsServerCore.Dns DnsDatagram newRequest = new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, new DnsQuestionRecord[] { new DnsQuestionRecord(lastDomain, request.Question[0].Type, request.Question[0].Class) }, null, null, null, _udpPayloadSize, request.DnssecOk ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None, eDnsClientSubnetOption); //query authoritative zone first - DnsDatagram newResponse = _authZoneManager.Query(newRequest, isRecursionAllowed); + DnsDatagram newResponse = _authZoneManager.Query(newRequest); if (newResponse is null) { //not found in auth zone; do recursion - newResponse = await RecursiveResolveAsync(newRequest, remoteEP, null, _dnssecValidation, false, false); + newResponse = await RecursiveResolveAsync(newRequest, remoteEP, null, _dnssecValidation, false, false, skipDnsAppAuthoritativeRequestHandlers); } else if ((newResponse.Answer.Count == 0) && (newResponse.Authority.Count > 0)) { @@ -2338,19 +2230,19 @@ namespace DnsServerCore.Dns { case DnsResourceRecordType.NS: //do forced recursive resolution using empty conditional forwarders; name servers will be provided via ResolverDnsCache - newResponse = await RecursiveResolveAsync(newRequest, remoteEP, Array.Empty(), _dnssecValidation, false, false); + newResponse = await RecursiveResolveAsync(newRequest, remoteEP, Array.Empty(), _dnssecValidation, false, false, skipDnsAppAuthoritativeRequestHandlers); break; case DnsResourceRecordType.FWD: if ((newResponse.Authority.Count == 1) && (firstAuthority.RDATA is DnsForwarderRecordData fwd) && fwd.Forwarder.Equals("this-server", StringComparison.OrdinalIgnoreCase)) { //do conditional forwarding via "this-server" - newResponse = await RecursiveResolveAsync(newRequest, remoteEP, null, fwd.DnssecValidation, false, false); + newResponse = await RecursiveResolveAsync(newRequest, remoteEP, null, fwd.DnssecValidation, false, false, skipDnsAppAuthoritativeRequestHandlers); } else { //do conditional forwarding - newResponse = await RecursiveResolveAsync(newRequest, remoteEP, newResponse.Authority, _dnssecValidation, false, false); + newResponse = await RecursiveResolveAsync(newRequest, remoteEP, newResponse.Authority, _dnssecValidation, false, false, skipDnsAppAuthoritativeRequestHandlers); } break; @@ -2380,10 +2272,10 @@ namespace DnsServerCore.Dns if (answer.Type != questionType) continue; - if (anameRR.TtlValue < answer.TtlValue) - answers.Add(new DnsResourceRecord(anameRR.Name, answer.Type, answer.Class, anameRR.TtlValue, answer.RDATA)); + if (anameRR.TTL < answer.TTL) + answers.Add(new DnsResourceRecord(anameRR.Name, answer.Type, answer.Class, anameRR.TTL, answer.RDATA)); else - answers.Add(new DnsResourceRecord(anameRR.Name, answer.Type, answer.Class, answer.TtlValue, answer.RDATA)); + answers.Add(new DnsResourceRecord(anameRR.Name, answer.Type, answer.Class, answer.TTL, answer.RDATA)); } return answers; @@ -2465,7 +2357,7 @@ namespace DnsServerCore.Dns DateTime utcNow = DateTime.UtcNow; foreach (DnsResourceRecord record in authority) - record.GetRecordInfo().LastUsedOn = utcNow; + record.GetAuthRecordInfo().LastUsedOn = utcNow; } } @@ -2501,23 +2393,35 @@ namespace DnsServerCore.Dns //domain is blocked in blocked zone DnsQuestionRecord question = request.Question[0]; + string GetBlockedDomain() + { + DnsResourceRecord firstAuthority = response.FindFirstAuthorityRecord(); + if ((firstAuthority is not null) && (firstAuthority.Type == DnsResourceRecordType.SOA)) + return firstAuthority.Name; + else + return question.Name; + } + if (_allowTxtBlockingReport && (question.Type == DnsResourceRecordType.TXT)) { //return meta data - string blockedDomain; - - DnsResourceRecord firstAuthority = response.FindFirstAuthorityRecord(); - if ((firstAuthority is not null) && (firstAuthority.Type == DnsResourceRecordType.SOA)) - blockedDomain = firstAuthority.Name; - else - blockedDomain = question.Name; + string blockedDomain = GetBlockedDomain(); IReadOnlyList answer = new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.TXT, question.Class, 60, new DnsTXTRecordData("source=blocked-zone; domain=" + blockedDomain)) }; - return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, true, false, false, DnsResponseCode.NoError, request.Question, answer) { Tag = DnsServerResponseType.Blocked }; + return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question, answer) { Tag = DnsServerResponseType.Blocked }; } else { + string blockedDomain = null; + EDnsOption[] options = null; + + if (_allowTxtBlockingReport && (request.EDNS is not null)) + { + blockedDomain = GetBlockedDomain(); + options = new EDnsOption[] { new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.Blocked, "source=blocked-zone; domain=" + blockedDomain)) }; + } + IReadOnlyCollection aRecords; IReadOnlyCollection aaaaRecords; @@ -2534,19 +2438,14 @@ namespace DnsServerCore.Dns break; case DnsServerBlockingType.NxDomain: - string blockedDomain; - - DnsResourceRecord firstAuthority = response.FindFirstAuthorityRecord(); - if ((firstAuthority is not null) && (firstAuthority.Type == DnsResourceRecordType.SOA)) - blockedDomain = firstAuthority.Name; - else - blockedDomain = question.Name; + if (blockedDomain is null) + blockedDomain = GetBlockedDomain(); string parentDomain = AuthZoneManager.GetParentZone(blockedDomain); if (parentDomain is null) parentDomain = string.Empty; - return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, true, false, false, DnsResponseCode.NxDomain, request.Question, null, new DnsResourceRecord[] { new DnsResourceRecord(parentDomain, DnsResourceRecordType.SOA, question.Class, 60, _blockedZoneManager.DnsSOARecord) }) { Tag = DnsServerResponseType.Blocked }; + return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NxDomain, request.Question, null, new DnsResourceRecord[] { new DnsResourceRecord(parentDomain, DnsResourceRecordType.SOA, question.Class, 60, _blockedZoneManager.DnsSOARecord) }, null, request.EDNS is null ? ushort.MinValue : _udpPayloadSize, EDnsHeaderFlags.None, options) { Tag = DnsServerResponseType.Blocked }; default: throw new InvalidOperationException(); @@ -2585,12 +2484,12 @@ namespace DnsServerCore.Dns break; } - return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, true, false, false, DnsResponseCode.NoError, request.Question, answer, authority) { Tag = DnsServerResponseType.Blocked }; + return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question, answer, authority, null, request.EDNS is null ? ushort.MinValue : _udpPayloadSize, EDnsHeaderFlags.None, options) { Tag = DnsServerResponseType.Blocked }; } } } - private async Task ProcessRecursiveQueryAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, IReadOnlyList conditionalForwarders, bool dnssecValidation, bool cacheRefreshOperation) + private async Task ProcessRecursiveQueryAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, IReadOnlyList conditionalForwarders, bool dnssecValidation, bool cacheRefreshOperation, bool skipDnsAppAuthoritativeRequestHandlers) { bool inAllowedZone; @@ -2616,7 +2515,7 @@ namespace DnsServerCore.Dns } } - DnsDatagram response = await RecursiveResolveAsync(request, remoteEP, conditionalForwarders, dnssecValidation, false, cacheRefreshOperation); + DnsDatagram response = await RecursiveResolveAsync(request, remoteEP, conditionalForwarders, dnssecValidation, false, cacheRefreshOperation, skipDnsAppAuthoritativeRequestHandlers); if (response.Answer.Count > 0) { @@ -2624,7 +2523,7 @@ namespace DnsServerCore.Dns DnsResourceRecord lastRR = response.GetLastAnswerRecord(); if ((lastRR.Type != questionType) && (lastRR.Type == DnsResourceRecordType.CNAME) && (questionType != DnsResourceRecordType.ANY)) - response = await ProcessCNAMEAsync(request, remoteEP, response, true, protocol, cacheRefreshOperation); + response = await ProcessCNAMEAsync(request, remoteEP, response, true, protocol, cacheRefreshOperation, skipDnsAppAuthoritativeRequestHandlers); if (!inAllowedZone) { @@ -2664,10 +2563,21 @@ namespace DnsServerCore.Dns } } + if (response.Tag is null) + { + if (response.IsBlockedResponse()) + response.Tag = DnsServerResponseType.UpstreamBlocked; + } + else if ((DnsServerResponseType)response.Tag == DnsServerResponseType.Cached) + { + if (response.IsBlockedResponse()) + response.Tag = DnsServerResponseType.CacheBlocked; + } + return response; } - private async Task RecursiveResolveAsync(DnsDatagram request, IPEndPoint remoteEP, IReadOnlyList conditionalForwarders, bool dnssecValidation, bool cachePrefetchOperation, bool cacheRefreshOperation) + private async Task RecursiveResolveAsync(DnsDatagram request, IPEndPoint remoteEP, IReadOnlyList conditionalForwarders, bool dnssecValidation, bool cachePrefetchOperation, bool cacheRefreshOperation, bool skipDnsAppAuthoritativeRequestHandlers) { DnsQuestionRecord question = request.Question[0]; NetworkAddress eDnsClientSubnet = null; @@ -2702,7 +2612,7 @@ namespace DnsServerCore.Dns { return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, true, false, false, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative }; } - else if ((requestECS.SourcePrefixLength == 0) || NetUtilities.IsPrivateIP(requestECS.AddressValue)) + else if ((requestECS.SourcePrefixLength == 0) || NetUtilities.IsPrivateIP(requestECS.Address)) { //disable ECS option request.ShadowHideEDnsClientSubnetOption(); @@ -2713,12 +2623,12 @@ namespace DnsServerCore.Dns switch (requestECS.Family) { case EDnsClientSubnetAddressFamily.IPv4: - eDnsClientSubnet = new NetworkAddress(requestECS.AddressValue, Math.Min(requestECS.SourcePrefixLength, _eDnsClientSubnetIPv4PrefixLength)); + eDnsClientSubnet = new NetworkAddress(requestECS.Address, Math.Min(requestECS.SourcePrefixLength, _eDnsClientSubnetIPv4PrefixLength)); request.SetShadowEDnsClientSubnetOption(eDnsClientSubnet); break; case EDnsClientSubnetAddressFamily.IPv6: - eDnsClientSubnet = new NetworkAddress(requestECS.AddressValue, Math.Min(requestECS.SourcePrefixLength, _eDnsClientSubnetIPv6PrefixLength)); + eDnsClientSubnet = new NetworkAddress(requestECS.Address, Math.Min(requestECS.SourcePrefixLength, _eDnsClientSubnetIPv6PrefixLength)); request.SetShadowEDnsClientSubnetOption(eDnsClientSubnet); break; } @@ -2741,7 +2651,7 @@ namespace DnsServerCore.Dns //inspect response TTL values to decide if prefetch trigger is needed foreach (DnsResourceRecord answer in cacheResponse.Answer) { - if ((answer.OriginalTtlValue >= _cachePrefetchEligibility) && (answer.TtlValue <= _cachePrefetchTrigger)) + if ((answer.OriginalTtlValue >= _cachePrefetchEligibility) && (answer.TTL <= _cachePrefetchTrigger)) { //trigger prefetch async _ = PrefetchCacheAsync(request, remoteEP, conditionalForwarders); @@ -2763,7 +2673,7 @@ namespace DnsServerCore.Dns //got new resolver task added so question is not being resolved; do recursive resolution in another task on resolver thread pool _ = Task.Factory.StartNew(delegate () { - return RecursiveResolveAsync(question, eDnsClientSubnet, conditionalForwarders, dnssecValidation, cachePrefetchOperation, cacheRefreshOperation, resolverTaskCompletionSource); + return RecursiveResolveAsync(question, eDnsClientSubnet, conditionalForwarders, dnssecValidation, cachePrefetchOperation, cacheRefreshOperation, skipDnsAppAuthoritativeRequestHandlers, resolverTaskCompletionSource); }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, _resolverTaskScheduler); } @@ -2819,7 +2729,7 @@ namespace DnsServerCore.Dns return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, true, false, false, DnsResponseCode.ServerFailure, request.Question, null, null, null, _udpPayloadSize, request.DnssecOk ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None, options); } - private async Task RecursiveResolveAsync(DnsQuestionRecord question, NetworkAddress eDnsClientSubnet, IReadOnlyList conditionalForwarders, bool dnssecValidation, bool cachePrefetchOperation, bool cacheRefreshOperation, TaskCompletionSource taskCompletionSource) + private async Task RecursiveResolveAsync(DnsQuestionRecord question, NetworkAddress eDnsClientSubnet, IReadOnlyList conditionalForwarders, bool dnssecValidation, bool cachePrefetchOperation, bool cacheRefreshOperation, bool skipDnsAppAuthoritativeRequestHandlers, TaskCompletionSource taskCompletionSource) { try { @@ -2827,7 +2737,9 @@ namespace DnsServerCore.Dns IDnsCache dnsCache; if (cachePrefetchOperation || cacheRefreshOperation) - dnsCache = new ResolverPrefetchDnsCache(_dnsApplicationManager, _authZoneManager, _cacheZoneManager, _log, question); + dnsCache = new ResolverPrefetchDnsCache(_dnsApplicationManager, _authZoneManager, _cacheZoneManager, _log, skipDnsAppAuthoritativeRequestHandlers, question); + else if (skipDnsAppAuthoritativeRequestHandlers) + dnsCache = new ResolverDnsCache(_dnsApplicationManager, _authZoneManager, _cacheZoneManager, _log, true); else dnsCache = _dnsCache; @@ -2990,8 +2902,7 @@ namespace DnsServerCore.Dns } catch (Exception ex) { - LogManager log = _log; - if (log is not null) + if (_log is not null) { string strForwarders = null; @@ -3018,7 +2929,7 @@ namespace DnsServerCore.Dns } } - log.Write("DNS Server failed to resolve the request with QNAME: " + question.Name + "; QTYPE: " + question.Type.ToString() + "; QCLASS: " + question.Class.ToString() + (strForwarders is null ? "" : "; Forwarders: " + strForwarders) + ";\r\n" + ex.ToString()); + _log.Write("DNS Server failed to resolve the request with QNAME: " + question.Name + "; QTYPE: " + question.Type.ToString() + "; QCLASS: " + question.Class.ToString() + (strForwarders is null ? "" : "; Forwarders: " + strForwarders) + ";\r\n" + ex.ToString()); } if (_serveStale) @@ -3262,7 +3173,7 @@ namespace DnsServerCore.Dns //additional section checks if (additional.Count > 0) { - if ((request.EDNS is not null) && (response.EDNS is not null) && (response.EDNS.Options.Count > 0)) + if ((request.EDNS is not null) && (response.EDNS is not null) && ((response.EDNS.Options.Count > 0) || (response.DnsClientExtendedErrors.Count > 0))) { //copy options as new OPT and keep other records List newAdditional = new List(additional.Count); @@ -3321,6 +3232,19 @@ namespace DnsServerCore.Dns options = response.EDNS.Options; } + if (response.DnsClientExtendedErrors.Count > 0) + { + //add dns client extended errors + List newOptions = new List(options.Count + response.DnsClientExtendedErrors.Count); + + newOptions.AddRange(options); + + foreach (EDnsExtendedDnsErrorOptionData ee in response.DnsClientExtendedErrors) + newOptions.Add(new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, ee)); + + options = newOptions; + } + newAdditional.Add(DnsDatagramEdns.GetOPTFor(_udpPayloadSize, response.RCODE, 0, request.DnssecOk ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None, options)); additional = newAdditional; @@ -3394,13 +3318,11 @@ namespace DnsServerCore.Dns { try { - await RecursiveResolveAsync(request, remoteEP, conditionalForwarders, _dnssecValidation, true, false); + await RecursiveResolveAsync(request, remoteEP, conditionalForwarders, _dnssecValidation, true, false, false); } catch (Exception ex) { - LogManager log = _log; - if (log is not null) - log.Write(ex); + _log?.Write(ex); } } @@ -3410,14 +3332,14 @@ namespace DnsServerCore.Dns { //refresh cache DnsDatagram request = new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, true, false, false, false, DnsResponseCode.NoError, new DnsQuestionRecord[] { sample.SampleQuestion }); - DnsDatagram response = await ProcessRecursiveQueryAsync(request, IPENDPOINT_ANY_0, DnsTransportProtocol.Udp, sample.ConditionalForwarders, _dnssecValidation, true); + DnsDatagram response = await ProcessRecursiveQueryAsync(request, IPENDPOINT_ANY_0, DnsTransportProtocol.Udp, sample.ConditionalForwarders, _dnssecValidation, true, false); bool addBackToSampleList = false; DateTime utcNow = DateTime.UtcNow; foreach (DnsResourceRecord answer in response.Answer) { - if ((answer.OriginalTtlValue >= _cachePrefetchEligibility) && (utcNow.AddSeconds(answer.TtlValue) < _cachePrefetchSamplingTimerTriggersOn)) + if ((answer.OriginalTtlValue >= _cachePrefetchEligibility) && (utcNow.AddSeconds(answer.TTL) < _cachePrefetchSamplingTimerTriggersOn)) { //answer expires before next sampling so add back to the list to allow refreshing it addBackToSampleList = true; @@ -3430,9 +3352,7 @@ namespace DnsServerCore.Dns } catch (Exception ex) { - LogManager log = _log; - if (log is not null) - log.Write(ex); + _log?.Write(ex); cacheRefreshSampleList[sampleQuestionIndex] = sample; //put back into sample list to allow refreshing it again } @@ -3454,7 +3374,7 @@ namespace DnsServerCore.Dns //inspect response TTL values to decide if refresh is needed foreach (DnsResourceRecord answer in cacheResponse.Answer) { - if ((answer.OriginalTtlValue >= _cachePrefetchEligibility) && (answer.TtlValue <= trigger)) + if ((answer.OriginalTtlValue >= _cachePrefetchEligibility) && (answer.TTL <= trigger)) return question; //TTL eligible and less than trigger so refresh question } @@ -3487,7 +3407,7 @@ namespace DnsServerCore.Dns //inspect response TTL values to decide if refresh is needed foreach (DnsResourceRecord answer in cacheResponse.Answer) { - if ((answer.OriginalTtlValue >= _cachePrefetchEligibility) && (answer.TtlValue <= trigger)) + if ((answer.OriginalTtlValue >= _cachePrefetchEligibility) && (answer.TTL <= trigger)) return true; //TTL eligible less than trigger so refresh } @@ -3519,7 +3439,7 @@ namespace DnsServerCore.Dns { reQueryAuthZone = false; - DnsDatagram response = _authZoneManager.Query(new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, false, false, false, false, DnsResponseCode.NoError, new DnsQuestionRecord[] { eligibleQuerySample }), true); + DnsDatagram response = _authZoneManager.Query(new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, false, false, false, false, DnsResponseCode.NoError, new DnsQuestionRecord[] { eligibleQuerySample })); if (response is null) { //zone not hosted; do refresh @@ -3574,9 +3494,7 @@ namespace DnsServerCore.Dns } catch (Exception ex) { - LogManager log = _log; - if (log is not null) - log.Write(ex); + _log?.Write(ex); } finally { @@ -3616,16 +3534,13 @@ namespace DnsServerCore.Dns } catch (Exception ex) { - LogManager log = _log; - if (log is not null) - log.Write(ex); + _log?.Write(ex); } finally { lock (_cachePrefetchRefreshTimerLock) { - if (_cachePrefetchRefreshTimer is not null) - _cachePrefetchRefreshTimer.Change((_cachePrefetchTrigger + 1) * 1000, Timeout.Infinite); + _cachePrefetchRefreshTimer?.Change((_cachePrefetchTrigger + 1) * 1000, Timeout.Infinite); } } } @@ -3641,16 +3556,13 @@ namespace DnsServerCore.Dns } catch (Exception ex) { - LogManager log = _log; - if (log is not null) - log.Write(ex); + _log?.Write(ex); } finally { lock (_cacheMaintenanceTimerLock) { - if (_cacheMaintenanceTimer is not null) - _cacheMaintenanceTimer.Change(CACHE_MAINTENANCE_TIMER_PERIODIC_INTERVAL, Timeout.Infinite); + _cacheMaintenanceTimer?.Change(CACHE_MAINTENANCE_TIMER_PERIODIC_INTERVAL, Timeout.Infinite); } } } @@ -3661,14 +3573,12 @@ namespace DnsServerCore.Dns { lock (_cachePrefetchSamplingTimerLock) { - if (_cachePrefetchSamplingTimer is not null) - _cachePrefetchSamplingTimer.Change(Timeout.Infinite, Timeout.Infinite); + _cachePrefetchSamplingTimer?.Change(Timeout.Infinite, Timeout.Infinite); } lock (_cachePrefetchRefreshTimerLock) { - if (_cachePrefetchRefreshTimer is not null) - _cachePrefetchRefreshTimer.Change(Timeout.Infinite, Timeout.Infinite); + _cachePrefetchRefreshTimer?.Change(Timeout.Infinite, Timeout.Infinite); } } else if (_state == ServiceState.Running) @@ -3684,19 +3594,16 @@ namespace DnsServerCore.Dns lock (_cachePrefetchRefreshTimerLock) { - if (_cachePrefetchRefreshTimer is not null) - _cachePrefetchRefreshTimer.Change(CACHE_PREFETCH_REFRESH_TIMER_INITIAL_INTEVAL, Timeout.Infinite); + _cachePrefetchRefreshTimer?.Change(CACHE_PREFETCH_REFRESH_TIMER_INITIAL_INTEVAL, Timeout.Infinite); } } } - private bool IsQpmLimitCrossed(IPEndPoint remoteEP) + private bool IsQpmLimitCrossed(IPAddress remoteIP) { if ((_qpmLimitRequests < 1) && (_qpmLimitErrors < 1)) return false; - IPAddress remoteIP = remoteEP.Address; - if (IPAddress.IsLoopback(remoteIP)) return false; @@ -3741,16 +3648,13 @@ namespace DnsServerCore.Dns } catch (Exception ex) { - LogManager log = _log; - if (log is not null) - log.Write(ex); + _log?.Write(ex); } finally { lock (_qpmLimitSamplingTimerLock) { - if (_qpmLimitSamplingTimer is not null) - _qpmLimitSamplingTimer.Change(QPM_LIMIT_SAMPLING_TIMER_INTERVAL, Timeout.Infinite); + _qpmLimitSamplingTimer?.Change(QPM_LIMIT_SAMPLING_TIMER_INTERVAL, Timeout.Infinite); } } } @@ -3761,8 +3665,7 @@ namespace DnsServerCore.Dns { lock (_qpmLimitSamplingTimerLock) { - if (_qpmLimitSamplingTimer is not null) - _qpmLimitSamplingTimer.Change(Timeout.Infinite, Timeout.Infinite); + _qpmLimitSamplingTimer?.Change(Timeout.Infinite, Timeout.Infinite); _qpmLimitClientSubnetStats = null; _qpmLimitErrorClientSubnetStats = null; @@ -3772,8 +3675,7 @@ namespace DnsServerCore.Dns { lock (_qpmLimitSamplingTimerLock) { - if (_qpmLimitSamplingTimer is not null) - _qpmLimitSamplingTimer.Change(0, Timeout.Infinite); + _qpmLimitSamplingTimer?.Change(0, Timeout.Infinite); } } } @@ -3807,9 +3709,217 @@ namespace DnsServerCore.Dns #endregion + #region doh web service + + private async Task StartDoHAsync() + { + WebApplicationBuilder builder = WebApplication.CreateBuilder(); + + builder.Environment.ContentRootFileProvider = new PhysicalFileProvider(Path.GetDirectoryName(_dohwwwFolder)) + { + UseActivePolling = true, + UsePollingFileWatcher = true + }; + + builder.Environment.WebRootFileProvider = new PhysicalFileProvider(_dohwwwFolder) + { + UseActivePolling = true, + UsePollingFileWatcher = true + }; + + IReadOnlyList localAddresses = GetValidKestralLocalAddresses(_localEndPoints.Convert(delegate (IPEndPoint ep) { return ep.Address; })); + + builder.WebHost.ConfigureKestrel(delegate (WebHostBuilderContext context, KestrelServerOptions serverOptions) + { + //bind to http port + if (_enableDnsOverHttp) + { + foreach (IPAddress localAddress in localAddresses) + serverOptions.Listen(localAddress, _dnsOverHttpPort); + } + + //bind to https port + if (_enableDnsOverHttps && (_certificate is not null)) + { + serverOptions.ConfigureHttpsDefaults(delegate (HttpsConnectionAdapterOptions configureOptions) + { + configureOptions.ServerCertificateSelector = delegate (ConnectionContext context, string dnsName) + { + return _certificate; + }; + }); + + foreach (IPAddress localAddress in localAddresses) + { + serverOptions.Listen(localAddress, _dnsOverHttpsPort, delegate (ListenOptions listenOptions) + { + listenOptions.Protocols = HttpProtocols.Http1AndHttp2AndHttp3; + listenOptions.UseHttps(); + }); + } + } + + serverOptions.AddServerHeader = false; + serverOptions.Limits.RequestHeadersTimeout = TimeSpan.FromMilliseconds(_tcpReceiveTimeout); + serverOptions.Limits.KeepAliveTimeout = TimeSpan.FromMilliseconds(_tcpReceiveTimeout); + serverOptions.Limits.MaxRequestHeadersTotalSize = 4096; + serverOptions.Limits.MaxRequestLineSize = serverOptions.Limits.MaxRequestHeadersTotalSize; + serverOptions.Limits.MaxRequestBufferSize = serverOptions.Limits.MaxRequestLineSize; + serverOptions.Limits.MaxRequestBodySize = 64 * 1024; + serverOptions.Limits.MaxResponseBufferSize = 4096; + }); + + builder.Logging.ClearProviders(); + + _dohWebService = builder.Build(); + + _dohWebService.UseDefaultFiles(); + _dohWebService.UseStaticFiles(new StaticFileOptions() + { + OnPrepareResponse = delegate (StaticFileResponseContext ctx) + { + ctx.Context.Response.Headers.Add("X-Robots-Tag", "noindex, nofollow"); + ctx.Context.Response.Headers.Add("Cache-Control", "private, max-age=300"); + } + }); + + _dohWebService.UseRouting(); + _dohWebService.MapGet("/dns-query", ProcessDoHRequestAsync); + _dohWebService.MapPost("/dns-query", ProcessDoHRequestAsync); + + try + { + await _dohWebService.StartAsync(); + + if (_log is not null) + { + foreach (IPAddress localAddress in localAddresses) + { + if (_enableDnsOverHttp) + _log?.Write(new IPEndPoint(localAddress, _dnsOverHttpPort), "Http", "DNS Server was bound successfully."); + + if (_enableDnsOverHttps && (_certificate is not null)) + _log?.Write(new IPEndPoint(localAddress, _dnsOverHttpsPort), "Https", "DNS Server was bound successfully."); + } + } + } + catch (Exception ex) + { + await StopDoHAsync(); + + if (_log is not null) + { + foreach (IPAddress localAddress in localAddresses) + { + if (_enableDnsOverHttp) + _log?.Write(new IPEndPoint(localAddress, _dnsOverHttpPort), "Http", "DNS Server failed to bind."); + + if (_enableDnsOverHttps && (_certificate is not null)) + _log?.Write(new IPEndPoint(localAddress, _dnsOverHttpsPort), "Https", "DNS Server failed to bind."); + } + + _log?.Write(ex); + } + } + } + + private async Task StopDoHAsync() + { + if (_dohWebService is not null) + { + await _dohWebService.DisposeAsync(); + _dohWebService = null; + } + } + + internal static IReadOnlyList GetValidKestralLocalAddresses(IReadOnlyList localAddresses) + { + List supportedLocalAddresses = new List(localAddresses.Count); + + foreach (IPAddress localAddress in localAddresses) + { + switch (localAddress.AddressFamily) + { + case AddressFamily.InterNetwork: + if (Socket.OSSupportsIPv4) + { + if (!supportedLocalAddresses.Contains(localAddress)) + supportedLocalAddresses.Add(localAddress); + } + + break; + + case AddressFamily.InterNetworkV6: + if (Socket.OSSupportsIPv6) + { + if (!supportedLocalAddresses.Contains(localAddress)) + supportedLocalAddresses.Add(localAddress); + } + + break; + } + } + + bool containsUnicastAddress = false; + + foreach (IPAddress localAddress in supportedLocalAddresses) + { + if (!localAddress.Equals(IPAddress.Any) && !localAddress.Equals(IPAddress.IPv6Any)) + { + containsUnicastAddress = true; + break; + } + } + + List newLocalAddresses = new List(supportedLocalAddresses.Count); + + if (containsUnicastAddress) + { + //replace any with loopback address + foreach (IPAddress localAddress in supportedLocalAddresses) + { + if (localAddress.Equals(IPAddress.Any)) + { + if (!newLocalAddresses.Contains(IPAddress.Loopback)) + newLocalAddresses.Add(IPAddress.Loopback); + } + else if (localAddress.Equals(IPAddress.IPv6Any)) + { + if (!newLocalAddresses.Contains(IPAddress.IPv6Loopback)) + newLocalAddresses.Add(IPAddress.IPv6Loopback); + } + else + { + if (!newLocalAddresses.Contains(localAddress)) + newLocalAddresses.Add(localAddress); + } + } + } + else + { + //remove "0.0.0.0" if [::] exists + foreach (IPAddress localAddress in supportedLocalAddresses) + { + if (localAddress.Equals(IPAddress.Any)) + { + if (!supportedLocalAddresses.Contains(IPAddress.IPv6Any)) + newLocalAddresses.Add(localAddress); + } + else + { + newLocalAddresses.Add(localAddress); + } + } + } + + return newLocalAddresses; + } + + #endregion + #region public - public void Start() + public async Task StartAsync() { if (_disposed) throw new ObjectDisposedException("DnsServer"); @@ -3848,18 +3958,13 @@ namespace DnsServerCore.Dns _udpListeners.Add(udpListener); - LogManager log = _log; - if (log is not null) - log.Write(localEP, DnsTransportProtocol.Udp, "DNS Server was bound successfully."); + _log?.Write(localEP, DnsTransportProtocol.Udp, "DNS Server was bound successfully."); } catch (Exception ex) { - LogManager log = _log; - if (log is not null) - log.Write(localEP, DnsTransportProtocol.Udp, "DNS Server failed to bind.\r\n" + ex.ToString()); + _log?.Write(localEP, DnsTransportProtocol.Udp, "DNS Server failed to bind.\r\n" + ex.ToString()); - if (udpListener is not null) - udpListener.Dispose(); + udpListener?.Dispose(); } Socket tcpListener = null; @@ -3869,58 +3974,22 @@ namespace DnsServerCore.Dns tcpListener = new Socket(localEP.AddressFamily, SocketType.Stream, ProtocolType.Tcp); tcpListener.Bind(localEP); - tcpListener.Listen(100); + tcpListener.Listen(_listenBacklog); _tcpListeners.Add(tcpListener); - LogManager log = _log; - if (log is not null) - log.Write(localEP, DnsTransportProtocol.Tcp, "DNS Server was bound successfully."); + _log?.Write(localEP, DnsTransportProtocol.Tcp, "DNS Server was bound successfully."); } catch (Exception ex) { - LogManager log = _log; - if (log is not null) - log.Write(localEP, DnsTransportProtocol.Tcp, "DNS Server failed to bind.\r\n" + ex.ToString()); + _log?.Write(localEP, DnsTransportProtocol.Tcp, "DNS Server failed to bind.\r\n" + ex.ToString()); - if (tcpListener is not null) - tcpListener.Dispose(); - } - - if (_enableDnsOverHttp) - { - IPEndPoint httpEP = new IPEndPoint(localEP.Address, 8053); - Socket httpListener = null; - - try - { - httpListener = new Socket(httpEP.AddressFamily, SocketType.Stream, ProtocolType.Tcp); - - httpListener.Bind(httpEP); - httpListener.Listen(100); - - _httpListeners.Add(httpListener); - - _isDnsOverHttpsEnabled = true; - - LogManager log = _log; - if (log is not null) - log.Write(httpEP, "Http", "DNS Server was bound successfully."); - } - catch (Exception ex) - { - LogManager log = _log; - if (log is not null) - log.Write(httpEP, "Http", "DNS Server failed to bind.\r\n" + ex.ToString()); - - if (httpListener is not null) - httpListener.Dispose(); - } + tcpListener?.Dispose(); } if (_enableDnsOverTls && (_certificate is not null)) { - IPEndPoint tlsEP = new IPEndPoint(localEP.Address, 853); + IPEndPoint tlsEP = new IPEndPoint(localEP.Address, _dnsOverTlsPort); Socket tlsListener = null; try @@ -3928,86 +3997,64 @@ namespace DnsServerCore.Dns tlsListener = new Socket(tlsEP.AddressFamily, SocketType.Stream, ProtocolType.Tcp); tlsListener.Bind(tlsEP); - tlsListener.Listen(100); + tlsListener.Listen(_listenBacklog); _tlsListeners.Add(tlsListener); - LogManager log = _log; - if (log is not null) - log.Write(tlsEP, DnsTransportProtocol.Tls, "DNS Server was bound successfully."); + _log?.Write(tlsEP, DnsTransportProtocol.Tls, "DNS Server was bound successfully."); } catch (Exception ex) { - LogManager log = _log; - if (log is not null) - log.Write(tlsEP, DnsTransportProtocol.Tls, "DNS Server failed to bind.\r\n" + ex.ToString()); + _log?.Write(tlsEP, DnsTransportProtocol.Tls, "DNS Server failed to bind.\r\n" + ex.ToString()); - if (tlsListener is not null) - tlsListener.Dispose(); + tlsListener?.Dispose(); } } - if (_enableDnsOverHttps) + if (_enableDnsOverQuic && (_certificate is not null)) { - //bind to http port 80 for certbot webroot support + IPEndPoint quicEP = new IPEndPoint(localEP.Address, _dnsOverQuicPort); + QuicListener quicListener = null; + + try { - IPEndPoint httpEP = new IPEndPoint(localEP.Address, 80); - Socket httpListener = null; - - try + QuicListenerOptions listenerOptions = new QuicListenerOptions() { - httpListener = new Socket(httpEP.AddressFamily, SocketType.Stream, ProtocolType.Tcp); + ListenEndPoint = quicEP, + ListenBacklog = _listenBacklog, + ApplicationProtocols = new List() { new SslApplicationProtocol("doq") }, + ConnectionOptionsCallback = delegate (QuicConnection quicConnection, SslClientHelloInfo sslClientHello, CancellationToken cancellationToken) + { + QuicServerConnectionOptions serverConnectionOptions = new QuicServerConnectionOptions() + { + DefaultCloseErrorCode = (long)DnsOverQuicErrorCodes.DOQ_NO_ERROR, + DefaultStreamErrorCode = (long)DnsOverQuicErrorCodes.DOQ_UNSPECIFIED_ERROR, + MaxInboundUnidirectionalStreams = 0, + MaxInboundBidirectionalStreams = _quicMaxInboundStreams, + IdleTimeout = TimeSpan.FromMilliseconds(_quicIdleTimeout), + ServerAuthenticationOptions = new SslServerAuthenticationOptions + { + ApplicationProtocols = new List() { new SslApplicationProtocol("doq") }, + ServerCertificate = _certificate + } + }; - httpListener.Bind(httpEP); - httpListener.Listen(100); + return ValueTask.FromResult(serverConnectionOptions); + } + }; - _httpListeners.Add(httpListener); + quicListener = await QuicListener.ListenAsync(listenerOptions); - LogManager log = _log; - if (log is not null) - log.Write(httpEP, "Http", "DNS Server was bound successfully."); - } - catch (Exception ex) - { - LogManager log = _log; - if (log is not null) - log.Write(httpEP, "Http", "DNS Server failed to bind.\r\n" + ex.ToString()); + _quicListeners.Add(quicListener); - if (httpListener is not null) - httpListener.Dispose(); - } + _log?.Write(quicEP, DnsTransportProtocol.Quic, "DNS Server was bound successfully."); } - - //bind to https port 443 - if (_certificate is not null) + catch (Exception ex) { - IPEndPoint httpsEP = new IPEndPoint(localEP.Address, 443); - Socket httpsListener = null; + _log?.Write(quicEP, DnsTransportProtocol.Quic, "DNS Server failed to bind.\r\n" + ex.ToString()); - try - { - httpsListener = new Socket(httpsEP.AddressFamily, SocketType.Stream, ProtocolType.Tcp); - - httpsListener.Bind(httpsEP); - httpsListener.Listen(100); - - _httpsListeners.Add(httpsListener); - - _isDnsOverHttpsEnabled = true; - - LogManager log = _log; - if (log is not null) - log.Write(httpsEP, DnsTransportProtocol.Https, "DNS Server was bound successfully."); - } - catch (Exception ex) - { - LogManager log = _log; - if (log is not null) - log.Write(httpsEP, DnsTransportProtocol.Https, "DNS Server failed to bind.\r\n" + ex.ToString()); - - if (httpsListener is not null) - httpsListener.Dispose(); - } + if (quicListener is not null) + await quicListener.DisposeAsync(); } } } @@ -4022,7 +4069,7 @@ namespace DnsServerCore.Dns _ = Task.Factory.StartNew(delegate () { return ReadUdpRequestAsync(udpListener); - }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Current); + }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, _queryTaskScheduler); } } @@ -4032,19 +4079,8 @@ namespace DnsServerCore.Dns { _ = Task.Factory.StartNew(delegate () { - return AcceptConnectionAsync(tcpListener, DnsTransportProtocol.Tcp, false); - }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Current); - } - } - - foreach (Socket httpListener in _httpListeners) - { - for (int i = 0; i < listenerTaskCount; i++) - { - _ = Task.Factory.StartNew(delegate () - { - return AcceptConnectionAsync(httpListener, DnsTransportProtocol.Https, false); - }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Current); + return AcceptConnectionAsync(tcpListener, DnsTransportProtocol.Tcp); + }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, _queryTaskScheduler); } } @@ -4054,22 +4090,25 @@ namespace DnsServerCore.Dns { _ = Task.Factory.StartNew(delegate () { - return AcceptConnectionAsync(tlsListener, DnsTransportProtocol.Tls, false); - }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Current); + return AcceptConnectionAsync(tlsListener, DnsTransportProtocol.Tls); + }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, _queryTaskScheduler); } } - foreach (Socket httpsListener in _httpsListeners) + foreach (QuicListener quicListener in _quicListeners) { for (int i = 0; i < listenerTaskCount; i++) { _ = Task.Factory.StartNew(delegate () { - return AcceptConnectionAsync(httpsListener, DnsTransportProtocol.Https, true); - }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Current); + return AcceptQuicConnectionAsync(quicListener); + }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, _queryTaskScheduler); } } + if (_enableDnsOverHttp || (_enableDnsOverHttps && (_certificate is not null))) + await StartDoHAsync(); + _cachePrefetchSamplingTimer = new Timer(CachePrefetchSamplingTimerCallback, null, Timeout.Infinite, Timeout.Infinite); _cachePrefetchRefreshTimer = new Timer(CachePrefetchRefreshTimerCallback, null, Timeout.Infinite, Timeout.Infinite); _cacheMaintenanceTimer = new Timer(CacheMaintenanceTimerCallback, null, CACHE_MAINTENANCE_TIMER_INITIAL_INTEVAL, Timeout.Infinite); @@ -4082,7 +4121,7 @@ namespace DnsServerCore.Dns ResetQpsLimitTimer(); } - public void Stop() + public async Task StopAsync() { if (_state != ServiceState.Running) return; @@ -4131,27 +4170,30 @@ namespace DnsServerCore.Dns foreach (Socket tcpListener in _tcpListeners) tcpListener.Dispose(); - foreach (Socket httpListener in _httpListeners) - httpListener.Dispose(); - foreach (Socket tlsListener in _tlsListeners) tlsListener.Dispose(); - foreach (Socket httpsListener in _httpsListeners) - httpsListener.Dispose(); + foreach (QuicListener quicListener in _quicListeners) + await quicListener.DisposeAsync(); _udpListeners.Clear(); _tcpListeners.Clear(); - _httpListeners.Clear(); _tlsListeners.Clear(); - _httpsListeners.Clear(); + _quicListeners.Clear(); + + await StopDoHAsync(); _state = ServiceState.Stopped; } public Task DirectQueryAsync(DnsQuestionRecord question, int timeout = 4000, bool skipDnsAppAuthoritativeRequestHandlers = false) { - return ProcessQueryAsync(new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, true, false, false, false, DnsResponseCode.NoError, new DnsQuestionRecord[] { question }), IPENDPOINT_ANY_0, DnsTransportProtocol.Tcp, true, skipDnsAppAuthoritativeRequestHandlers, null).WithTimeout(timeout); + return DirectQueryAsync(new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, true, false, false, false, DnsResponseCode.NoError, new DnsQuestionRecord[] { question }), timeout, skipDnsAppAuthoritativeRequestHandlers); + } + + public Task DirectQueryAsync(DnsDatagram request, int timeout = 4000, bool skipDnsAppAuthoritativeRequestHandlers = false) + { + return ProcessQueryAsync(request, IPENDPOINT_ANY_0, DnsTransportProtocol.Tcp, true, skipDnsAppAuthoritativeRequestHandlers, null).WithTimeout(timeout); } Task IDnsClient.ResolveAsync(DnsQuestionRecord question, CancellationToken cancellationToken) @@ -4191,48 +4233,15 @@ namespace DnsServerCore.Dns set { _localEndPoints = value; } } + public LogManager LogManager + { + get { return _log; } + set { _log = value; } + } + public NameServerAddress ThisServer { get { return _thisServer; } } - public bool EnableDnsOverHttp - { - get { return _enableDnsOverHttp; } - set { _enableDnsOverHttp = value; } - } - - public bool EnableDnsOverTls - { - get { return _enableDnsOverTls; } - set { _enableDnsOverTls = value; } - } - - public bool EnableDnsOverHttps - { - get { return _enableDnsOverHttps; } - set { _enableDnsOverHttps = value; } - } - - public bool IsDnsOverHttpsEnabled - { get { return _isDnsOverHttpsEnabled; } } - - public X509Certificate2 Certificate - { - get { return _certificate; } - set - { - if (!value.HasPrivateKey) - throw new ArgumentException("Tls certificate does not contain private key."); - - _certificate = value; - } - } - - public IReadOnlyDictionary TsigKeys - { - get { return _tsigKeys; } - set { _tsigKeys = value; } - } - public AuthZoneManager AuthZoneManager { get { return _authZoneManager; } } @@ -4254,61 +4263,8 @@ namespace DnsServerCore.Dns public IDnsCache DnsCache { get { return _dnsCache; } } - public DnsServerRecursion Recursion - { - get { return _recursion; } - set - { - if (_recursion != value) - { - if ((_recursion == DnsServerRecursion.Deny) || (value == DnsServerRecursion.Deny)) - { - _recursion = value; - ResetPrefetchTimers(); - } - else - { - _recursion = value; - } - } - } - } - - public IReadOnlyCollection RecursionDeniedNetworks - { - get { return _recursionDeniedNetworks; } - set - { - if ((value is not null) && (value.Count > byte.MaxValue)) - throw new ArgumentOutOfRangeException(nameof(RecursionDeniedNetworks), "Networks cannot be more than 255."); - - _recursionDeniedNetworks = value; - } - } - - public IReadOnlyCollection RecursionAllowedNetworks - { - get { return _recursionAllowedNetworks; } - set - { - if ((value is not null) && (value.Count > byte.MaxValue)) - throw new ArgumentOutOfRangeException(nameof(RecursionAllowedNetworks), "Networks cannot be more than 255."); - - _recursionAllowedNetworks = value; - } - } - - public NetProxy Proxy - { - get { return _proxy; } - set { _proxy = value; } - } - - public IReadOnlyList Forwarders - { - get { return _forwarders; } - set { _forwarders = value; } - } + public StatsManager StatsManager + { get { return _stats; } } public bool PreferIPv6 { @@ -4328,24 +4284,6 @@ namespace DnsServerCore.Dns } } - public bool RandomizeName - { - get { return _randomizeName; } - set { _randomizeName = value; } - } - - public bool QnameMinimization - { - get { return _qnameMinimization; } - set { _qnameMinimization = value; } - } - - public bool NsRevalidation - { - get { return _nsRevalidation; } - set { _nsRevalidation = value; } - } - public bool DnssecValidation { get { return _dnssecValidation; } @@ -4484,18 +4422,200 @@ namespace DnsServerCore.Dns } } - public int ForwarderRetries + public int ClientTimeout { - get { return _forwarderRetries; } + get { return _clientTimeout; } set { - if ((value < 1) || (value > 10)) - throw new ArgumentOutOfRangeException(nameof(ForwarderRetries), "Valid range is from 1 to 10."); + if ((value < 1000) || (value > 10000)) + throw new ArgumentOutOfRangeException(nameof(ClientTimeout), "Valid range is from 1000 to 10000."); - _forwarderRetries = value; + _clientTimeout = value; } } + public int TcpSendTimeout + { + get { return _tcpSendTimeout; } + set + { + if ((value < 1000) || (value > 90000)) + throw new ArgumentOutOfRangeException(nameof(TcpSendTimeout), "Valid range is from 1000 to 90000."); + + _tcpSendTimeout = value; + } + } + + public int TcpReceiveTimeout + { + get { return _tcpReceiveTimeout; } + set + { + if ((value < 1000) || (value > 90000)) + throw new ArgumentOutOfRangeException(nameof(TcpReceiveTimeout), "Valid range is from 1000 to 90000."); + + _tcpReceiveTimeout = value; + } + } + + public int QuicIdleTimeout + { + get { return _quicIdleTimeout; } + set + { + if ((value < 1000) || (value > 90000)) + throw new ArgumentOutOfRangeException(nameof(QuicIdleTimeout), "Valid range is from 1000 to 90000."); + + _quicIdleTimeout = value; + } + } + + public int QuicMaxInboundStreams + { + get { return _quicMaxInboundStreams; } + set + { + if ((value < 0) || (value > 1000)) + throw new ArgumentOutOfRangeException(nameof(QuicMaxInboundStreams), "Valid range is from 1 to 1000."); + + _quicMaxInboundStreams = value; + } + } + + public int ListenBacklog + { + get { return _listenBacklog; } + set { _listenBacklog = value; } + } + + public bool EnableDnsOverHttp + { + get { return _enableDnsOverHttp; } + set { _enableDnsOverHttp = value; } + } + + public bool EnableDnsOverTls + { + get { return _enableDnsOverTls; } + set { _enableDnsOverTls = value; } + } + + public bool EnableDnsOverHttps + { + get { return _enableDnsOverHttps; } + set { _enableDnsOverHttps = value; } + } + + public bool EnableDnsOverQuic + { + get { return _enableDnsOverQuic; } + set { _enableDnsOverQuic = value; } + } + + public int DnsOverHttpPort + { + get { return _dnsOverHttpPort; } + set { _dnsOverHttpPort = value; } + } + + public int DnsOverTlsPort + { + get { return _dnsOverTlsPort; } + set { _dnsOverTlsPort = value; } + } + + public int DnsOverHttpsPort + { + get { return _dnsOverHttpsPort; } + set { _dnsOverHttpsPort = value; } + } + + public int DnsOverQuicPort + { + get { return _dnsOverQuicPort; } + set { _dnsOverQuicPort = value; } + } + + public X509Certificate2 Certificate + { + get { return _certificate; } + set + { + if ((value is not null) && !value.HasPrivateKey) + throw new ArgumentException("Tls certificate does not contain private key."); + + _certificate = value; + } + } + + public IReadOnlyDictionary TsigKeys + { + get { return _tsigKeys; } + set { _tsigKeys = value; } + } + + public DnsServerRecursion Recursion + { + get { return _recursion; } + set + { + if (_recursion != value) + { + if ((_recursion == DnsServerRecursion.Deny) || (value == DnsServerRecursion.Deny)) + { + _recursion = value; + ResetPrefetchTimers(); + } + else + { + _recursion = value; + } + } + } + } + + public IReadOnlyCollection RecursionDeniedNetworks + { + get { return _recursionDeniedNetworks; } + set + { + if ((value is not null) && (value.Count > byte.MaxValue)) + throw new ArgumentOutOfRangeException(nameof(RecursionDeniedNetworks), "Networks cannot be more than 255."); + + _recursionDeniedNetworks = value; + } + } + + public IReadOnlyCollection RecursionAllowedNetworks + { + get { return _recursionAllowedNetworks; } + set + { + if ((value is not null) && (value.Count > byte.MaxValue)) + throw new ArgumentOutOfRangeException(nameof(RecursionAllowedNetworks), "Networks cannot be more than 255."); + + _recursionAllowedNetworks = value; + } + } + + public bool RandomizeName + { + get { return _randomizeName; } + set { _randomizeName = value; } + } + + public bool QnameMinimization + { + get { return _qnameMinimization; } + set { _qnameMinimization = value; } + } + + public bool NsRevalidation + { + get { return _nsRevalidation; } + set { _nsRevalidation = value; } + } + public int ResolverRetries { get { return _resolverRetries; } @@ -4508,18 +4628,6 @@ namespace DnsServerCore.Dns } } - public int ForwarderTimeout - { - get { return _forwarderTimeout; } - set - { - if ((value < 1000) || (value > 10000)) - throw new ArgumentOutOfRangeException(nameof(ForwarderTimeout), "Valid range is from 1000 to 10000."); - - _forwarderTimeout = value; - } - } - public int ResolverTimeout { get { return _resolverTimeout; } @@ -4532,30 +4640,6 @@ namespace DnsServerCore.Dns } } - public int ClientTimeout - { - get { return _clientTimeout; } - set - { - if ((value < 1000) || (value > 10000)) - throw new ArgumentOutOfRangeException(nameof(ClientTimeout), "Valid range is from 1000 to 10000."); - - _clientTimeout = value; - } - } - - public int ForwarderConcurrency - { - get { return _forwarderConcurrency; } - set - { - if ((value < 1) || (value > 10)) - throw new ArgumentOutOfRangeException(nameof(ForwarderConcurrency), "Valid range is from 1 to 10."); - - _forwarderConcurrency = value; - } - } - public int ResolverMaxStackCount { get { return _resolverMaxStackCount; } @@ -4675,10 +4759,52 @@ namespace DnsServerCore.Dns } } - public LogManager LogManager + public NetProxy Proxy { - get { return _log; } - set { _log = value; } + get { return _proxy; } + set { _proxy = value; } + } + + public IReadOnlyList Forwarders + { + get { return _forwarders; } + set { _forwarders = value; } + } + + public int ForwarderRetries + { + get { return _forwarderRetries; } + set + { + if ((value < 1) || (value > 10)) + throw new ArgumentOutOfRangeException(nameof(ForwarderRetries), "Valid range is from 1 to 10."); + + _forwarderRetries = value; + } + } + + public int ForwarderTimeout + { + get { return _forwarderTimeout; } + set + { + if ((value < 1000) || (value > 10000)) + throw new ArgumentOutOfRangeException(nameof(ForwarderTimeout), "Valid range is from 1000 to 10000."); + + _forwarderTimeout = value; + } + } + + public int ForwarderConcurrency + { + get { return _forwarderConcurrency; } + set + { + if ((value < 1) || (value > 10)) + throw new ArgumentOutOfRangeException(nameof(ForwarderConcurrency), "Valid range is from 1 to 10."); + + _forwarderConcurrency = value; + } } public LogManager QueryLogManager @@ -4687,33 +4813,6 @@ namespace DnsServerCore.Dns set { _queryLog = value; } } - public StatsManager StatsManager - { get { return _stats; } } - - public int TcpSendTimeout - { - get { return _tcpSendTimeout; } - set - { - if ((value < 1000) || (value > 90000)) - throw new ArgumentOutOfRangeException(nameof(TcpSendTimeout), "Valid range is from 1000 to 60000."); - - _tcpSendTimeout = value; - } - } - - public int TcpReceiveTimeout - { - get { return _tcpReceiveTimeout; } - set - { - if ((value < 1000) || (value > 90000)) - throw new ArgumentOutOfRangeException(nameof(TcpReceiveTimeout), "Valid range is from 1000 to 60000."); - - _tcpReceiveTimeout = value; - } - } - #endregion class CacheRefreshSample @@ -4742,4 +4841,7 @@ namespace DnsServerCore.Dns public DnsDatagram CheckingDisabledResponse { get; } } } + +#pragma warning restore CA2252 // This API requires opting into preview features +#pragma warning restore CA1416 // Validate platform compatibility } diff --git a/DnsServerCore/Dns/Dnssec/DnssecPrivateKey.cs b/DnsServerCore/Dns/Dnssec/DnssecPrivateKey.cs index cd54ffcc..59041496 100644 --- a/DnsServerCore/Dns/Dnssec/DnssecPrivateKey.cs +++ b/DnsServerCore/Dns/Dnssec/DnssecPrivateKey.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -201,7 +201,7 @@ namespace DnsServerCore.Dns.Dnssec } } - public static DnssecPrivateKey Parse(BinaryReader bR) + public static DnssecPrivateKey ReadFrom(BinaryReader bR) { if (Encoding.ASCII.GetString(bR.ReadBytes(2)) != "DK") throw new InvalidDataException("DNSSEC private key format is invalid."); @@ -270,7 +270,7 @@ namespace DnsServerCore.Dns.Dnssec byte[] signature = SignHash(hash); - DnsRRSIGRecordData signedRRSigRecord = new DnsRRSIGRecordData(unsignedRRSigRecord.TypeCovered, unsignedRRSigRecord.Algorithm, unsignedRRSigRecord.Labels, unsignedRRSigRecord.OriginalTtl, unsignedRRSigRecord.SignatureExpirationValue, unsignedRRSigRecord.SignatureInceptionValue, unsignedRRSigRecord.KeyTag, unsignedRRSigRecord.SignersName, signature); + DnsRRSIGRecordData signedRRSigRecord = new DnsRRSIGRecordData(unsignedRRSigRecord.TypeCovered, unsignedRRSigRecord.Algorithm, unsignedRRSigRecord.Labels, unsignedRRSigRecord.OriginalTtl, unsignedRRSigRecord.SignatureExpiration, unsignedRRSigRecord.SignatureInception, unsignedRRSigRecord.KeyTag, unsignedRRSigRecord.SignersName, signature); return new DnsResourceRecord(firstRecord.Name, DnsResourceRecordType.RRSIG, firstRecord.Class, firstRecord.OriginalTtlValue, signedRRSigRecord); } diff --git a/DnsServerCore/Dns/ResolverDnsCache.cs b/DnsServerCore/Dns/ResolverDnsCache.cs index f57ade5e..6605e488 100644 --- a/DnsServerCore/Dns/ResolverDnsCache.cs +++ b/DnsServerCore/Dns/ResolverDnsCache.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -36,17 +36,19 @@ namespace DnsServerCore.Dns readonly AuthZoneManager _authZoneManager; readonly CacheZoneManager _cacheZoneManager; readonly LogManager _log; + readonly bool _skipDnsAppAuthoritativeRequestHandlers; #endregion #region constructor - public ResolverDnsCache(DnsApplicationManager dnsApplicationManager, AuthZoneManager authZoneManager, CacheZoneManager cacheZoneManager, LogManager log) + public ResolverDnsCache(DnsApplicationManager dnsApplicationManager, AuthZoneManager authZoneManager, CacheZoneManager cacheZoneManager, LogManager log, bool skipDnsAppAuthoritativeRequestHandlers) { _dnsApplicationManager = dnsApplicationManager; _authZoneManager = authZoneManager; _cacheZoneManager = cacheZoneManager; _log = log; + _skipDnsAppAuthoritativeRequestHandlers = skipDnsAppAuthoritativeRequestHandlers; } #endregion @@ -55,7 +57,7 @@ namespace DnsServerCore.Dns private DnsDatagram DnsApplicationQueryClosestDelegation(DnsDatagram request) { - if ((_dnsApplicationManager.DnsAuthoritativeRequestHandlers.Count < 1) || (request.Question.Count != 1)) + if (_skipDnsAppAuthoritativeRequestHandlers || (_dnsApplicationManager.DnsAuthoritativeRequestHandlers.Count < 1) || (request.Question.Count != 1)) return null; IPEndPoint localEP = new IPEndPoint(IPAddress.Any, 0); @@ -132,27 +134,30 @@ namespace DnsServerCore.Dns { DnsDatagram authResponse = null; - foreach (IDnsAuthoritativeRequestHandler requestHandler in _dnsApplicationManager.DnsAuthoritativeRequestHandlers) + if (!_skipDnsAppAuthoritativeRequestHandlers) { - try + foreach (IDnsAuthoritativeRequestHandler requestHandler in _dnsApplicationManager.DnsAuthoritativeRequestHandlers) { - authResponse = requestHandler.ProcessRequestAsync(request, new IPEndPoint(IPAddress.Any, 0), DnsTransportProtocol.Tcp, false).Sync(); - if (authResponse is not null) + try { - if ((authResponse.RCODE != DnsResponseCode.NoError) || (authResponse.Answer.Count > 0) || (authResponse.Authority.Count == 0) || authResponse.IsFirstAuthoritySOA()) - return authResponse; + authResponse = requestHandler.ProcessRequestAsync(request, new IPEndPoint(IPAddress.Any, 0), DnsTransportProtocol.Tcp, true).Sync(); + if (authResponse is not null) + { + if ((authResponse.RCODE != DnsResponseCode.NoError) || (authResponse.Answer.Count > 0) || (authResponse.Authority.Count == 0) || authResponse.IsFirstAuthoritySOA()) + return authResponse; + } + } + catch (Exception ex) + { + if (_log is not null) + _log.Write(ex); } - } - catch (Exception ex) - { - if (_log is not null) - _log.Write(ex); } } if (authResponse is null) { - authResponse = _authZoneManager.Query(request, true); + authResponse = _authZoneManager.Query(request); if (authResponse is not null) { if ((authResponse.RCODE != DnsResponseCode.NoError) || (authResponse.Answer.Count > 0) || (authResponse.Authority.Count == 0) || authResponse.IsFirstAuthoritySOA()) diff --git a/DnsServerCore/Dns/ResolverPrefetchDnsCache.cs b/DnsServerCore/Dns/ResolverPrefetchDnsCache.cs index 90c74c14..a7bb049f 100644 --- a/DnsServerCore/Dns/ResolverPrefetchDnsCache.cs +++ b/DnsServerCore/Dns/ResolverPrefetchDnsCache.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -33,8 +33,8 @@ namespace DnsServerCore.Dns #region constructor - public ResolverPrefetchDnsCache(DnsApplicationManager dnsApplicationManager, AuthZoneManager authZoneManager, CacheZoneManager cacheZoneManager, LogManager log, DnsQuestionRecord prefetchQuestion) - : base(dnsApplicationManager, authZoneManager, cacheZoneManager, log) + public ResolverPrefetchDnsCache(DnsApplicationManager dnsApplicationManager, AuthZoneManager authZoneManager, CacheZoneManager cacheZoneManager, LogManager log, bool skipDnsAppAuthoritativeRequestHandlers, DnsQuestionRecord prefetchQuestion) + : base(dnsApplicationManager, authZoneManager, cacheZoneManager, log, skipDnsAppAuthoritativeRequestHandlers) { _prefetchQuestion = prefetchQuestion; } diff --git a/DnsServerCore/Dns/ResourceRecords/DnsResourceRecordInfo.cs b/DnsServerCore/Dns/ResourceRecords/AuthRecordInfo.cs similarity index 88% rename from DnsServerCore/Dns/ResourceRecords/DnsResourceRecordInfo.cs rename to DnsServerCore/Dns/ResourceRecords/AuthRecordInfo.cs index c561518b..1648df49 100644 --- a/DnsServerCore/Dns/ResourceRecords/DnsResourceRecordInfo.cs +++ b/DnsServerCore/Dns/ResourceRecords/AuthRecordInfo.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -22,16 +22,17 @@ using System.Collections.Generic; using System.IO; using System.Net; using TechnitiumLibrary.IO; -using TechnitiumLibrary.Net; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace DnsServerCore.Dns.ResourceRecords { - class DnsResourceRecordInfo + class AuthRecordInfo { #region variables + public static readonly AuthRecordInfo Default = new AuthRecordInfo(); + bool _disabled; IReadOnlyList _glueRecords; string _comments; @@ -40,19 +41,16 @@ namespace DnsServerCore.Dns.ResourceRecords DnsTransportProtocol _zoneTransferProtocol; string _tsigKeyName = string.Empty; - IReadOnlyList _rrsigRecords; //not serialized - IReadOnlyList _nsecRecords; //not serialized - NetworkAddress _eDnsClientSubnet; //not serialized DateTime _lastUsedOn; //not serialized #endregion #region constructor - public DnsResourceRecordInfo() + public AuthRecordInfo() { } - public DnsResourceRecordInfo(BinaryReader bR, bool isSoa) + public AuthRecordInfo(BinaryReader bR, bool isSoa) { byte version = bR.ReadByte(); switch (version) @@ -155,7 +153,7 @@ namespace DnsServerCore.Dns.ResourceRecords break; default: - throw new InvalidDataException("DnsResourceRecordInfo format version not supported."); + throw new InvalidDataException("AuthRecordInfo format version not supported."); } } @@ -217,7 +215,13 @@ namespace DnsServerCore.Dns.ResourceRecords public IReadOnlyList GlueRecords { get { return _glueRecords; } - set { _glueRecords = value; } + set + { + if ((value is null) || (value.Count == 0)) + _glueRecords = null; + else + _glueRecords = value; + } } public string Comments @@ -241,7 +245,13 @@ namespace DnsServerCore.Dns.ResourceRecords public IReadOnlyList PrimaryNameServers { get { return _primaryNameServers; } - set { _primaryNameServers = value; } + set + { + if ((value is null) || (value.Count == 0)) + _primaryNameServers = null; + else + _primaryNameServers = value; + } } public DnsTransportProtocol ZoneTransferProtocol @@ -253,6 +263,7 @@ namespace DnsServerCore.Dns.ResourceRecords { case DnsTransportProtocol.Tcp: case DnsTransportProtocol.Tls: + case DnsTransportProtocol.Quic: _zoneTransferProtocol = value; break; @@ -274,24 +285,6 @@ namespace DnsServerCore.Dns.ResourceRecords } } - public IReadOnlyList RRSIGRecords - { - get { return _rrsigRecords; } - set { _rrsigRecords = value; } - } - - public IReadOnlyList NSECRecords - { - get { return _nsecRecords; } - set { _nsecRecords = value; } - } - - public NetworkAddress EDnsClientSubnet - { - get { return _eDnsClientSubnet; } - set { _eDnsClientSubnet = value; } - } - public DateTime LastUsedOn { get { return _lastUsedOn; } diff --git a/DnsServerCore/Dns/ResourceRecords/CacheRecordInfo.cs b/DnsServerCore/Dns/ResourceRecords/CacheRecordInfo.cs new file mode 100644 index 00000000..bcbaa383 --- /dev/null +++ b/DnsServerCore/Dns/ResourceRecords/CacheRecordInfo.cs @@ -0,0 +1,199 @@ +/* +Technitium DNS Server +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +using System; +using System.Collections.Generic; +using System.IO; +using TechnitiumLibrary.Net; +using TechnitiumLibrary.Net.Dns.ResourceRecords; + +namespace DnsServerCore.Dns.ResourceRecords +{ + class CacheRecordInfo + { + #region variables + + public static readonly CacheRecordInfo Default = new CacheRecordInfo(); + + IReadOnlyList _glueRecords; + IReadOnlyList _rrsigRecords; + IReadOnlyList _nsecRecords; + NetworkAddress _eDnsClientSubnet; + + DateTime _lastUsedOn; //not serialized + + #endregion + + #region constructor + + public CacheRecordInfo() + { } + + public CacheRecordInfo(BinaryReader bR) + { + byte version = bR.ReadByte(); + switch (version) + { + case 1: + _glueRecords = ReadRecordsFrom(bR, true); + _rrsigRecords = ReadRecordsFrom(bR, false); + _nsecRecords = ReadRecordsFrom(bR, true); + + if (bR.ReadBoolean()) + _eDnsClientSubnet = NetworkAddress.ReadFrom(bR); + + break; + + default: + throw new InvalidDataException("CacheRecordInfo format version not supported."); + } + } + + #endregion + + #region private + + private static IReadOnlyList ReadRecordsFrom(BinaryReader bR, bool includeInnerRRSigRecords) + { + int count = bR.ReadByte(); + if (count == 0) + return null; + + DnsResourceRecord[] records = new DnsResourceRecord[count]; + + for (int i = 0; i < count; i++) + { + records[i] = DnsResourceRecord.ReadCacheRecordFrom(bR, delegate (DnsResourceRecord record) + { + if (includeInnerRRSigRecords) + { + IReadOnlyList rrsigRecords = ReadRecordsFrom(bR, false); + if (rrsigRecords is not null) + record.GetCacheRecordInfo()._rrsigRecords = rrsigRecords; + } + }); + } + + return records; + } + + private static void WriteRecordsTo(IReadOnlyList records, BinaryWriter bW, bool includeInnerRRSigRecords) + { + if (records is null) + { + bW.Write((byte)0); + } + else + { + bW.Write(Convert.ToByte(records.Count)); + + foreach (DnsResourceRecord record in records) + { + record.WriteCacheRecordTo(bW, delegate () + { + if (includeInnerRRSigRecords) + { + if (record.Tag is CacheRecordInfo cacheRecordInfo) + WriteRecordsTo(cacheRecordInfo._rrsigRecords, bW, false); + else + bW.Write((byte)0); + } + }); + } + } + } + + #endregion + + #region public + + public void WriteTo(BinaryWriter bW) + { + bW.Write((byte)1); //version + + WriteRecordsTo(_glueRecords, bW, true); + WriteRecordsTo(_rrsigRecords, bW, false); + WriteRecordsTo(_nsecRecords, bW, true); + + if (_eDnsClientSubnet is null) + { + bW.Write(false); + } + else + { + bW.Write(true); + _eDnsClientSubnet.WriteTo(bW); + } + } + + #endregion + + #region properties + + public IReadOnlyList GlueRecords + { + get { return _glueRecords; } + set + { + if ((value is null) || (value.Count == 0)) + _glueRecords = null; + else + _glueRecords = value; + } + } + + public IReadOnlyList RRSIGRecords + { + get { return _rrsigRecords; } + set + { + if ((value is null) || (value.Count == 0)) + _rrsigRecords = null; + else + _rrsigRecords = value; + } + } + + public IReadOnlyList NSECRecords + { + get { return _nsecRecords; } + set + { + if ((value is null) || (value.Count == 0)) + _nsecRecords = null; + else + _nsecRecords = value; + } + } + + public NetworkAddress EDnsClientSubnet + { + get { return _eDnsClientSubnet; } + set { _eDnsClientSubnet = value; } + } + + public DateTime LastUsedOn + { + get { return _lastUsedOn; } + set { _lastUsedOn = value; } + } + + #endregion + } +} diff --git a/DnsServerCore/Dns/ResourceRecords/DnsResourceRecordExtension.cs b/DnsServerCore/Dns/ResourceRecords/DnsResourceRecordExtension.cs deleted file mode 100644 index aa8c87e8..00000000 --- a/DnsServerCore/Dns/ResourceRecords/DnsResourceRecordExtension.cs +++ /dev/null @@ -1,276 +0,0 @@ -/* -Technitium DNS Server -Copyright (C) 2021 Shreyas Zare (shreyas@technitium.com) - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -*/ - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.Sockets; -using TechnitiumLibrary.Net.Dns; -using TechnitiumLibrary.Net.Dns.ResourceRecords; - -namespace DnsServerCore.Dns.ResourceRecords -{ - static class DnsResourceRecordExtension - { - public static void SetGlueRecords(this DnsResourceRecord record, IReadOnlyList glueRecords) - { - if (record.Tag is not DnsResourceRecordInfo rrInfo) - { - rrInfo = new DnsResourceRecordInfo(); - record.Tag = rrInfo; - } - - rrInfo.GlueRecords = glueRecords; - } - - public static void SetGlueRecords(this DnsResourceRecord record, string glueAddresses) - { - string[] addresses = glueAddresses.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - List ipAddresses = new List(addresses.Length); - - foreach (string address in addresses) - ipAddresses.Add(IPAddress.Parse(address.Trim())); - - SetGlueRecords(record, ipAddresses); - } - - public static void SetGlueRecords(this DnsResourceRecord record, IReadOnlyList glueAddresses) - { - if (record.RDATA is not DnsNSRecordData nsRecord) - throw new InvalidOperationException(); - - string domain = nsRecord.NameServer; - - DnsResourceRecord[] glueRecords = new DnsResourceRecord[glueAddresses.Count]; - - for (int i = 0; i < glueRecords.Length; i++) - { - switch (glueAddresses[i].AddressFamily) - { - case AddressFamily.InterNetwork: - glueRecords[i] = new DnsResourceRecord(domain, DnsResourceRecordType.A, DnsClass.IN, record.TtlValue, new DnsARecordData(glueAddresses[i])); - break; - - case AddressFamily.InterNetworkV6: - glueRecords[i] = new DnsResourceRecord(domain, DnsResourceRecordType.AAAA, DnsClass.IN, record.TtlValue, new DnsAAAARecordData(glueAddresses[i])); - break; - } - } - - SetGlueRecords(record, glueRecords); - } - - public static void SyncGlueRecords(this DnsResourceRecord record, IReadOnlyList allGlueRecords) - { - if (record.RDATA is not DnsNSRecordData nsRecord) - throw new InvalidOperationException(); - - string domain = nsRecord.NameServer; - - List foundGlueRecords = new List(2); - - foreach (DnsResourceRecord glueRecord in allGlueRecords) - { - switch (glueRecord.Type) - { - case DnsResourceRecordType.A: - case DnsResourceRecordType.AAAA: - if (glueRecord.Name.Equals(domain, StringComparison.OrdinalIgnoreCase)) - foundGlueRecords.Add(glueRecord); - - break; - } - } - - if (foundGlueRecords.Count > 0) - SetGlueRecords(record, foundGlueRecords); - else - SetGlueRecords(record, Array.Empty()); - } - - public static void SyncGlueRecords(this DnsResourceRecord record, IReadOnlyCollection deletedGlueRecords, IReadOnlyCollection addedGlueRecords) - { - if (record.RDATA is not DnsNSRecordData nsRecord) - throw new InvalidOperationException(); - - bool updated = false; - - List updatedGlueRecords = new List(); - IReadOnlyList existingGlueRecords = GetGlueRecords(record); - - foreach (DnsResourceRecord existingGlueRecord in existingGlueRecords) - { - if (deletedGlueRecords.Contains(existingGlueRecord)) - updated = true; //skipped to delete existing glue record - else - updatedGlueRecords.Add(existingGlueRecord); - } - - string domain = nsRecord.NameServer; - - foreach (DnsResourceRecord addedGlueRecord in addedGlueRecords) - { - switch (addedGlueRecord.Type) - { - case DnsResourceRecordType.A: - case DnsResourceRecordType.AAAA: - if (addedGlueRecord.Name.Equals(domain, StringComparison.OrdinalIgnoreCase)) - { - updatedGlueRecords.Add(addedGlueRecord); - updated = true; - } - break; - } - } - - if (updated) - SetGlueRecords(record, updatedGlueRecords); - } - - public static IReadOnlyList GetGlueRecords(this DnsResourceRecord record) - { - if (record.Tag is DnsResourceRecordInfo rrInfo) - { - IReadOnlyList glueRecords = rrInfo.GlueRecords; - if (glueRecords is null) - return Array.Empty(); - - return glueRecords; - } - - return Array.Empty(); - } - - public static bool IsDisabled(this DnsResourceRecord record) - { - if (record.Tag is DnsResourceRecordInfo rrInfo) - return rrInfo.Disabled; - - return false; - } - - public static void Disable(this DnsResourceRecord record) - { - if (record.Tag is not DnsResourceRecordInfo rrInfo) - { - rrInfo = new DnsResourceRecordInfo(); - record.Tag = rrInfo; - } - - rrInfo.Disabled = true; - } - - public static void Enable(this DnsResourceRecord record) - { - if (record.Tag is DnsResourceRecordInfo rrInfo) - rrInfo.Disabled = false; - } - - public static string GetComments(this DnsResourceRecord record) - { - if (record.Tag is DnsResourceRecordInfo rrInfo) - return rrInfo.Comments; - - return null; - } - - public static void SetComments(this DnsResourceRecord record, string value) - { - if (record.Tag is not DnsResourceRecordInfo rrInfo) - { - rrInfo = new DnsResourceRecordInfo(); - record.Tag = rrInfo; - } - - rrInfo.Comments = value; - } - - public static DateTime GetDeletedOn(this DnsResourceRecord record) - { - if (record.Tag is DnsResourceRecordInfo rrInfo) - return rrInfo.DeletedOn; - - return DateTime.MinValue; - } - - public static void SetDeletedOn(this DnsResourceRecord record, DateTime value) - { - if (record.Tag is not DnsResourceRecordInfo rrInfo) - { - rrInfo = new DnsResourceRecordInfo(); - record.Tag = rrInfo; - } - - rrInfo.DeletedOn = value; - } - - public static void SetPrimaryNameServers(this DnsResourceRecord record, IReadOnlyList primaryNameServers) - { - if (record.Tag is not DnsResourceRecordInfo rrInfo) - { - rrInfo = new DnsResourceRecordInfo(); - record.Tag = rrInfo; - } - - rrInfo.PrimaryNameServers = primaryNameServers; - } - - public static void SetPrimaryNameServers(this DnsResourceRecord record, string primaryNameServers) - { - string[] nameServerAddresses = primaryNameServers.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - List nameServers = new List(nameServerAddresses.Length); - - foreach (string nameServerAddress in nameServerAddresses) - nameServers.Add(new NameServerAddress(nameServerAddress)); - - SetPrimaryNameServers(record, nameServers); - } - - public static IReadOnlyList GetPrimaryNameServers(this DnsResourceRecord record) - { - if (record.Tag is DnsResourceRecordInfo rrInfo) - { - IReadOnlyList primaryNameServers = rrInfo.PrimaryNameServers; - if (primaryNameServers is null) - return Array.Empty(); - - return primaryNameServers; - } - - return Array.Empty(); - } - - public static DnsResourceRecordInfo GetRecordInfo(this DnsResourceRecord record) - { - if (record.Tag is not DnsResourceRecordInfo rrInfo) - { - rrInfo = new DnsResourceRecordInfo(); - record.Tag = rrInfo; - } - - return rrInfo; - } - - public static void CopyRecordInfoFrom(this DnsResourceRecord record, DnsResourceRecord otherRecord) - { - record.Tag = otherRecord.Tag; - } - } -} diff --git a/DnsServerCore/Dns/ResourceRecords/DnsResourceRecordExtensions.cs b/DnsServerCore/Dns/ResourceRecords/DnsResourceRecordExtensions.cs new file mode 100644 index 00000000..671823d1 --- /dev/null +++ b/DnsServerCore/Dns/ResourceRecords/DnsResourceRecordExtensions.cs @@ -0,0 +1,152 @@ +/* +Technitium DNS Server +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using TechnitiumLibrary; +using TechnitiumLibrary.Net.Dns.ResourceRecords; + +namespace DnsServerCore.Dns.ResourceRecords +{ + static class DnsResourceRecordExtensions + { + public static void SetGlueRecords(this DnsResourceRecord record, string glueAddresses) + { + if (record.RDATA is not DnsNSRecordData nsRecord) + throw new InvalidOperationException(); + + string domain = nsRecord.NameServer; + + IReadOnlyList glueAddressesList = glueAddresses.Split(IPAddress.Parse, ','); + DnsResourceRecord[] glueRecords = new DnsResourceRecord[glueAddressesList.Count]; + + for (int i = 0; i < glueRecords.Length; i++) + { + switch (glueAddressesList[i].AddressFamily) + { + case AddressFamily.InterNetwork: + glueRecords[i] = new DnsResourceRecord(domain, DnsResourceRecordType.A, DnsClass.IN, record.TTL, new DnsARecordData(glueAddressesList[i])); + break; + + case AddressFamily.InterNetworkV6: + glueRecords[i] = new DnsResourceRecord(domain, DnsResourceRecordType.AAAA, DnsClass.IN, record.TTL, new DnsAAAARecordData(glueAddressesList[i])); + break; + } + } + + record.GetAuthRecordInfo().GlueRecords = glueRecords; + } + + public static void SyncGlueRecords(this DnsResourceRecord record, IReadOnlyList allGlueRecords) + { + if (record.RDATA is not DnsNSRecordData nsRecord) + throw new InvalidOperationException(); + + string domain = nsRecord.NameServer; + + List foundGlueRecords = new List(2); + + foreach (DnsResourceRecord glueRecord in allGlueRecords) + { + switch (glueRecord.Type) + { + case DnsResourceRecordType.A: + case DnsResourceRecordType.AAAA: + if (glueRecord.Name.Equals(domain, StringComparison.OrdinalIgnoreCase)) + foundGlueRecords.Add(glueRecord); + + break; + } + } + + record.GetAuthRecordInfo().GlueRecords = foundGlueRecords; + } + + public static void SyncGlueRecords(this DnsResourceRecord record, IReadOnlyCollection deletedGlueRecords, IReadOnlyCollection addedGlueRecords) + { + if (record.RDATA is not DnsNSRecordData nsRecord) + throw new InvalidOperationException(); + + bool updated = false; + + List updatedGlueRecords = new List(); + IReadOnlyList existingGlueRecords = record.GetAuthRecordInfo().GlueRecords; + if (existingGlueRecords is not null) + { + foreach (DnsResourceRecord existingGlueRecord in existingGlueRecords) + { + if (deletedGlueRecords.Contains(existingGlueRecord)) + updated = true; //skipped to delete existing glue record + else + updatedGlueRecords.Add(existingGlueRecord); + } + } + + string domain = nsRecord.NameServer; + + foreach (DnsResourceRecord addedGlueRecord in addedGlueRecords) + { + switch (addedGlueRecord.Type) + { + case DnsResourceRecordType.A: + case DnsResourceRecordType.AAAA: + if (addedGlueRecord.Name.Equals(domain, StringComparison.OrdinalIgnoreCase)) + { + updatedGlueRecords.Add(addedGlueRecord); + updated = true; + } + break; + } + } + + if (updated) + record.GetAuthRecordInfo().GlueRecords = updatedGlueRecords; + } + + public static AuthRecordInfo GetAuthRecordInfo(this DnsResourceRecord record) + { + if (record.Tag is not AuthRecordInfo rrInfo) + { + rrInfo = new AuthRecordInfo(); + record.Tag = rrInfo; + } + + return rrInfo; + } + + public static CacheRecordInfo GetCacheRecordInfo(this DnsResourceRecord record) + { + if (record.Tag is not CacheRecordInfo rrInfo) + { + rrInfo = new CacheRecordInfo(); + record.Tag = rrInfo; + } + + return rrInfo; + } + + public static void CopyRecordInfoFrom(this DnsResourceRecord record, DnsResourceRecord otherRecord) + { + record.Tag = otherRecord.Tag; + } + } +} diff --git a/DnsServerCore/Dns/StatsManager.cs b/DnsServerCore/Dns/StatsManager.cs index 148b4753..803c3521 100644 --- a/DnsServerCore/Dns/StatsManager.cs +++ b/DnsServerCore/Dns/StatsManager.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -1388,7 +1388,7 @@ namespace DnsServerCore.Dns _clientIpAddresses = new ConcurrentDictionary(1, count); for (int i = 0; i < count; i++) - _clientIpAddresses.TryAdd(IPAddressExtension.ReadFrom(bR), new Counter(bR.ReadInt32())); + _clientIpAddresses.TryAdd(IPAddressExtensions.ReadFrom(bR), new Counter(bR.ReadInt32())); if (version < 6) _totalClients = count; @@ -1413,7 +1413,7 @@ namespace DnsServerCore.Dns _errorIpAddresses = new ConcurrentDictionary(1, count); for (int i = 0; i < count; i++) - _errorIpAddresses.TryAdd(IPAddressExtension.ReadFrom(bR), new Counter(bR.ReadInt32())); + _errorIpAddresses.TryAdd(IPAddressExtensions.ReadFrom(bR), new Counter(bR.ReadInt32())); } else { @@ -1465,7 +1465,7 @@ namespace DnsServerCore.Dns _clientIpAddresses = new ConcurrentDictionary(1, count); for (int i = 0; i < count; i++) - _clientIpAddresses.TryAdd(IPAddressExtension.ReadFrom(bR), new Counter(bR.ReadInt64())); + _clientIpAddresses.TryAdd(IPAddressExtensions.ReadFrom(bR), new Counter(bR.ReadInt64())); } { @@ -1481,7 +1481,7 @@ namespace DnsServerCore.Dns _errorIpAddresses = new ConcurrentDictionary(1, count); for (int i = 0; i < count; i++) - _errorIpAddresses.TryAdd(IPAddressExtension.ReadFrom(bR), new Counter(bR.ReadInt64())); + _errorIpAddresses.TryAdd(IPAddressExtensions.ReadFrom(bR), new Counter(bR.ReadInt64())); } break; @@ -1537,10 +1537,21 @@ namespace DnsServerCore.Dns switch (responseCode) { case DnsResponseCode.NoError: - if ((query is not null) && (responseType != DnsServerResponseType.Blocked)) //skip blocked domains + if (query is not null) { - _queryDomains.GetOrAdd(query.Name.ToLower(), GetNewCounter).Increment(); - _queries.GetOrAdd(query, GetNewCounter).Increment(); + switch (responseType) + { + case DnsServerResponseType.Blocked: + case DnsServerResponseType.UpstreamBlocked: + case DnsServerResponseType.CacheBlocked: + //skip blocked domains + break; + + default: + _queryDomains.GetOrAdd(query.Name.ToLower(), GetNewCounter).Increment(); + _queries.GetOrAdd(query, GetNewCounter).Increment(); + break; + } } Interlocked.Increment(ref _totalNoError); @@ -1585,6 +1596,24 @@ namespace DnsServerCore.Dns Interlocked.Increment(ref _totalBlocked); break; + + case DnsServerResponseType.UpstreamBlocked: + Interlocked.Increment(ref _totalRecursive); + + if (query is not null) + _queryBlockedDomains.GetOrAdd(query.Name.ToLower(), GetNewCounter).Increment(); + + Interlocked.Increment(ref _totalBlocked); + break; + + case DnsServerResponseType.CacheBlocked: + Interlocked.Increment(ref _totalCached); + + if (query is not null) + _queryBlockedDomains.GetOrAdd(query.Name.ToLower(), GetNewCounter).Increment(); + + Interlocked.Increment(ref _totalBlocked); + break; } if (query is not null) diff --git a/DnsServerCore/Dns/ZoneManagers/AllowedZoneManager.cs b/DnsServerCore/Dns/ZoneManagers/AllowedZoneManager.cs index 03672e05..34eff79d 100644 --- a/DnsServerCore/Dns/ZoneManagers/AllowedZoneManager.cs +++ b/DnsServerCore/Dns/ZoneManagers/AllowedZoneManager.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -137,14 +137,14 @@ namespace DnsServerCore.Dns.ZoneManagers _zoneManager.Flush(); } - public List ListZones() + public IReadOnlyList GetAllZones() { - return _zoneManager.ListZones(); + return _zoneManager.GetAllZones(); } public void ListAllRecords(string domain, List records) { - _zoneManager.ListAllRecords(domain, records); + _zoneManager.ListAllRecords(domain, domain, records); } public void ListSubDomains(string domain, List subDomains) @@ -154,7 +154,7 @@ namespace DnsServerCore.Dns.ZoneManagers public void SaveZoneFile() { - List allowedZones = _dnsServer.AllowedZoneManager.ListZones(); + IReadOnlyList allowedZones = _dnsServer.AllowedZoneManager.GetAllZones(); string allowedZoneFile = Path.Combine(_dnsServer.ConfigFolder, "allowed.config"); @@ -178,7 +178,7 @@ namespace DnsServerCore.Dns.ZoneManagers public DnsDatagram Query(DnsDatagram request) { - return _zoneManager.Query(request, true); + return _zoneManager.Query(request); } #endregion diff --git a/DnsServerCore/Dns/ZoneManagers/AuthZoneManager.cs b/DnsServerCore/Dns/ZoneManagers/AuthZoneManager.cs index 77d92609..33c0f47b 100644 --- a/DnsServerCore/Dns/ZoneManagers/AuthZoneManager.cs +++ b/DnsServerCore/Dns/ZoneManagers/AuthZoneManager.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -45,7 +45,8 @@ namespace DnsServerCore.Dns.ZoneManagers readonly AuthZoneTree _root = new AuthZoneTree(); - int _totalZones; + readonly List _zoneIndex = new List(10); + readonly ReaderWriterLockSlim _zoneIndexLock = new ReaderWriterLockSlim(); #endregion @@ -94,7 +95,7 @@ namespace DnsServerCore.Dns.ZoneManagers //update authoritative zone SOA and NS records try { - List zones = ListZones(); + IReadOnlyList zones = GetAllZones(); foreach (AuthZoneInfo zone in zones) { @@ -110,7 +111,7 @@ namespace DnsServerCore.Dns.ZoneManagers if (responsiblePerson.EndsWith(_serverDomain)) responsiblePerson = responsiblePerson.Replace(_serverDomain, serverDomain); - SetRecords(zone.Name, record.Name, record.Type, record.TtlValue, new DnsResourceRecordData[] { new DnsSOARecordData(serverDomain, responsiblePerson, soa.Serial, soa.Refresh, soa.Retry, soa.Expire, soa.Minimum) }); + SetRecords(zone.Name, record.Name, record.Type, record.TTL, new DnsResourceRecordData[] { new DnsSOARecordData(serverDomain, responsiblePerson, soa.Serial, soa.Refresh, soa.Retry, soa.Expire, soa.Minimum) }); //update NS records IReadOnlyList nsResourceRecords = zone.GetApexRecords(DnsResourceRecordType.NS); @@ -119,7 +120,7 @@ namespace DnsServerCore.Dns.ZoneManagers { if ((nsResourceRecord.RDATA as DnsNSRecordData).NameServer.Equals(_serverDomain, StringComparison.OrdinalIgnoreCase)) { - UpdateRecord(zone.Name, nsResourceRecord, new DnsResourceRecord(nsResourceRecord.Name, nsResourceRecord.Type, nsResourceRecord.Class, nsResourceRecord.TtlValue, new DnsNSRecordData(serverDomain)) { Tag = nsResourceRecord.Tag }); + UpdateRecord(zone.Name, nsResourceRecord, new DnsResourceRecord(nsResourceRecord.Name, nsResourceRecord.Type, nsResourceRecord.Class, nsResourceRecord.TTL, new DnsNSRecordData(serverDomain)) { Tag = nsResourceRecord.Tag }); break; } } @@ -179,10 +180,7 @@ namespace DnsServerCore.Dns.ZoneManagers } if (_root.TryAdd(zone)) - { - _totalZones++; return zone; - } throw new DnsServerException("Zone already exists: " + zoneInfo.Name); } @@ -301,7 +299,7 @@ namespace DnsServerCore.Dns.ZoneManagers if (DnsClient.IsDomainNameValid(result)) { - DnsResourceRecord cnameRR = new DnsResourceRecord(question.Name, DnsResourceRecordType.CNAME, question.Class, dnameRR.TtlValue, new DnsCNAMERecordData(result)); + DnsResourceRecord cnameRR = new DnsResourceRecord(question.Name, DnsResourceRecordType.CNAME, question.Class, dnameRR.TTL, new DnsCNAMERecordData(result)); List list = new List(5); @@ -329,8 +327,8 @@ namespace DnsServerCore.Dns.ZoneManagers switch (refRecord.Type) { case DnsResourceRecordType.NS: - IReadOnlyList glueRecords = refRecord.GetGlueRecords(); - if (glueRecords.Count > 0) + IReadOnlyList glueRecords = refRecord.GetAuthRecordInfo().GlueRecords; + if (glueRecords is not null) { additionalRecords.AddRange(glueRecords); } @@ -371,7 +369,7 @@ namespace DnsServerCore.Dns.ZoneManagers } } - private DnsDatagram GetReferralResponse(DnsDatagram request, bool dnssecOk, AuthZone delegationZone, ApexZone apexZone, bool isRecursionAllowed) + private DnsDatagram GetReferralResponse(DnsDatagram request, bool dnssecOk, AuthZone delegationZone, ApexZone apexZone) { IReadOnlyList authority; @@ -383,7 +381,7 @@ namespace DnsServerCore.Dns.ZoneManagers DateTime utcNow = DateTime.UtcNow; foreach (DnsResourceRecord record in authority) - record.GetRecordInfo().LastUsedOn = utcNow; + record.GetAuthRecordInfo().LastUsedOn = utcNow; } else { @@ -426,17 +424,17 @@ namespace DnsServerCore.Dns.ZoneManagers IReadOnlyList additional = GetAdditionalRecords(authority, dnssecOk); - return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, null, authority, additional); + return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question, null, authority, additional); } - private DnsDatagram GetForwarderResponse(DnsDatagram request, AuthZone zone, AuthZone closestZone, ApexZone forwarderZone, bool isRecursionAllowed) + private DnsDatagram GetForwarderResponse(DnsDatagram request, AuthZone zone, AuthZone closestZone, ApexZone forwarderZone) { IReadOnlyList authority = null; if (zone is not null) { if (zone.ContainsNameServerRecords()) - return GetReferralResponse(request, false, zone, forwarderZone, isRecursionAllowed); + return GetReferralResponse(request, false, zone, forwarderZone); authority = zone.QueryRecords(DnsResourceRecordType.FWD, false); } @@ -444,7 +442,7 @@ namespace DnsServerCore.Dns.ZoneManagers if (((authority is null) || (authority.Count == 0)) && (closestZone is not null)) { if (closestZone.ContainsNameServerRecords()) - return GetReferralResponse(request, false, closestZone, forwarderZone, isRecursionAllowed); + return GetReferralResponse(request, false, closestZone, forwarderZone); authority = closestZone.QueryRecords(DnsResourceRecordType.FWD, false); } @@ -452,17 +450,26 @@ namespace DnsServerCore.Dns.ZoneManagers if ((authority is null) || (authority.Count == 0)) { if (forwarderZone.ContainsNameServerRecords()) - return GetReferralResponse(request, false, forwarderZone, forwarderZone, isRecursionAllowed); + return GetReferralResponse(request, false, forwarderZone, forwarderZone); authority = forwarderZone.QueryRecords(DnsResourceRecordType.FWD, false); } - return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, null, authority); + return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question, null, authority); } internal void Flush() { - _root.Clear(); + _zoneIndexLock.EnterWriteLock(); + try + { + _root.Clear(); + _zoneIndex.Clear(); + } + finally + { + _zoneIndexLock.ExitWriteLock(); + } } private static IReadOnlyList CondenseIncrementalZoneTransferRecords(string zoneName, DnsResourceRecord currentSoaRecord, IReadOnlyList xfrRecords) @@ -652,7 +659,7 @@ namespace DnsServerCore.Dns.ZoneManagers public void LoadAllZoneFiles() { - _root.Clear(); + Flush(); string zonesFolder = Path.Combine(_dnsServer.ConfigFolder, "zones"); if (!Directory.Exists(zonesFolder)) @@ -666,7 +673,7 @@ namespace DnsServerCore.Dns.ZoneManagers File.Move(oldZoneFile, Path.Combine(zonesFolder, Path.GetFileName(oldZoneFile))); } - //remove old internal zones + //remove old internal zones files { string[] oldZoneFiles = new string[] { "localhost.zone", "1.0.0.127.in-addr.arpa.zone", "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa.zone" }; @@ -722,27 +729,38 @@ namespace DnsServerCore.Dns.ZoneManagers } //load zone files - string[] zoneFiles = Directory.GetFiles(zonesFolder, "*.zone"); - - foreach (string zoneFile in zoneFiles) + _zoneIndexLock.EnterWriteLock(); + try { - try - { - using (FileStream fS = new FileStream(zoneFile, FileMode.Open, FileAccess.Read)) - { - LoadZoneFrom(fS); - } + string[] zoneFiles = Directory.GetFiles(zonesFolder, "*.zone"); - LogManager log = _dnsServer.LogManager; - if (log != null) - log.Write("DNS Server successfully loaded zone file: " + zoneFile); - } - catch (Exception ex) + foreach (string zoneFile in zoneFiles) { - LogManager log = _dnsServer.LogManager; - if (log != null) - log.Write("DNS Server failed to load zone file: " + zoneFile + "\r\n" + ex.ToString()); + try + { + using (FileStream fS = new FileStream(zoneFile, FileMode.Open, FileAccess.Read)) + { + AuthZoneInfo zoneInfo = LoadZoneFrom(fS); + _zoneIndex.Add(zoneInfo); + } + + LogManager log = _dnsServer.LogManager; + if (log != null) + log.Write("DNS Server successfully loaded zone file: " + zoneFile); + } + catch (Exception ex) + { + LogManager log = _dnsServer.LogManager; + if (log != null) + log.Write("DNS Server failed to load zone file: " + zoneFile + "\r\n" + ex.ToString()); + } } + + _zoneIndex.Sort(); + } + finally + { + _zoneIndexLock.ExitWriteLock(); } } @@ -750,10 +768,21 @@ namespace DnsServerCore.Dns.ZoneManagers { PrimaryZone apexZone = new PrimaryZone(_dnsServer, zoneName, soaRecord, ns); - if (_root.TryAdd(apexZone)) + _zoneIndexLock.EnterWriteLock(); + try { - _totalZones++; - return new AuthZoneInfo(apexZone); + if (_root.TryAdd(apexZone)) + { + AuthZoneInfo zoneInfo = new AuthZoneInfo(apexZone); + _zoneIndex.Add(zoneInfo); + _zoneIndex.Sort(); + + return zoneInfo; + } + } + finally + { + _zoneIndexLock.ExitWriteLock(); } return null; @@ -763,10 +792,21 @@ namespace DnsServerCore.Dns.ZoneManagers { PrimaryZone apexZone = new PrimaryZone(_dnsServer, zoneName, primaryNameServer, @internal); - if (_root.TryAdd(apexZone)) + _zoneIndexLock.EnterWriteLock(); + try { - _totalZones++; - return new AuthZoneInfo(apexZone); + if (_root.TryAdd(apexZone)) + { + AuthZoneInfo zoneInfo = new AuthZoneInfo(apexZone); + _zoneIndex.Add(zoneInfo); + _zoneIndex.Sort(); + + return zoneInfo; + } + } + finally + { + _zoneIndexLock.ExitWriteLock(); } return null; @@ -776,11 +816,23 @@ namespace DnsServerCore.Dns.ZoneManagers { SecondaryZone apexZone = await SecondaryZone.CreateAsync(_dnsServer, zoneName, primaryNameServerAddresses, zoneTransferProtocol, tsigKeyName); - if (_root.TryAdd(apexZone)) + _zoneIndexLock.EnterWriteLock(); + try { - apexZone.TriggerRefresh(0); - _totalZones++; - return new AuthZoneInfo(apexZone); + if (_root.TryAdd(apexZone)) + { + apexZone.TriggerRefresh(0); + + AuthZoneInfo zoneInfo = new AuthZoneInfo(apexZone); + _zoneIndex.Add(zoneInfo); + _zoneIndex.Sort(); + + return zoneInfo; + } + } + finally + { + _zoneIndexLock.ExitWriteLock(); } return null; @@ -790,11 +842,23 @@ namespace DnsServerCore.Dns.ZoneManagers { StubZone apexZone = await StubZone.CreateAsync(_dnsServer, zoneName, primaryNameServerAddresses); - if (_root.TryAdd(apexZone)) + _zoneIndexLock.EnterWriteLock(); + try { - apexZone.TriggerRefresh(0); - _totalZones++; - return new AuthZoneInfo(apexZone); + if (_root.TryAdd(apexZone)) + { + apexZone.TriggerRefresh(0); + + AuthZoneInfo zoneInfo = new AuthZoneInfo(apexZone); + _zoneIndex.Add(zoneInfo); + _zoneIndex.Sort(); + + return zoneInfo; + } + } + finally + { + _zoneIndexLock.ExitWriteLock(); } return null; @@ -804,10 +868,21 @@ namespace DnsServerCore.Dns.ZoneManagers { ForwarderZone apexZone = new ForwarderZone(zoneName, forwarderProtocol, forwarder, dnssecValidation, proxyType, proxyAddress, proxyPort, proxyUsername, proxyPassword, fwdRecordComments); - if (_root.TryAdd(apexZone)) + _zoneIndexLock.EnterWriteLock(); + try { - _totalZones++; - return new AuthZoneInfo(apexZone); + if (_root.TryAdd(apexZone)) + { + AuthZoneInfo zoneInfo = new AuthZoneInfo(apexZone); + _zoneIndex.Add(zoneInfo); + _zoneIndex.Sort(); + + return zoneInfo; + } + } + finally + { + _zoneIndexLock.ExitWriteLock(); } return null; @@ -943,12 +1018,23 @@ namespace DnsServerCore.Dns.ZoneManagers public bool DeleteZone(string zoneName) { - if (_root.TryRemove(zoneName, out ApexZone apexZone)) + _zoneIndexLock.EnterWriteLock(); + try { - apexZone.Dispose(); + if (_root.TryRemove(zoneName, out ApexZone apexZone)) + { + apexZone.Dispose(); - _totalZones--; - return true; + AuthZoneInfo zoneInfo = new AuthZoneInfo(apexZone); + if (!_zoneIndex.Remove(zoneInfo)) + throw new InvalidOperationException("Zone deleted from tree but failed to remove from zone index."); + + return true; + } + } + finally + { + _zoneIndexLock.ExitWriteLock(); } return false; @@ -978,12 +1064,20 @@ namespace DnsServerCore.Dns.ZoneManagers return _root.TryGet(zoneName, domain, out _); } - public void ListAllRecords(string zoneName, List records) + public void ListAllZoneRecords(string zoneName, List records) { foreach (AuthZone zone in _root.GetZoneWithSubDomainZones(zoneName)) zone.ListAllRecords(records); } + public void ListAllRecords(string zoneName, string domain, List records) + { + ValidateZoneNameFor(zoneName, domain); + + if (_root.TryGet(zoneName, domain, out AuthZone authZone)) + authZone.ListAllRecords(records); + } + public IReadOnlyList GetRecords(string zoneName, string domain, DnsResourceRecordType type) { ValidateZoneNameFor(zoneName, domain); @@ -1018,7 +1112,7 @@ namespace DnsServerCore.Dns.ZoneManagers DnsResourceRecord soaRecord = soaRecords[0]; List records = new List(); - ListAllRecords(zoneName, records); + ListAllZoneRecords(zoneName, records); List xfrRecords = new List(records.Count + 1); @@ -1027,7 +1121,8 @@ namespace DnsServerCore.Dns.ZoneManagers foreach (DnsResourceRecord record in records) { - if (record.IsDisabled()) + AuthRecordInfo authRecordInfo = record.GetAuthRecordInfo(); + if (authRecordInfo.Disabled) continue; switch (record.Type) @@ -1038,9 +1133,12 @@ namespace DnsServerCore.Dns.ZoneManagers case DnsResourceRecordType.NS: xfrRecords.Add(record); - foreach (DnsResourceRecord glueRecord in record.GetGlueRecords()) - xfrRecords.Add(glueRecord); - + IReadOnlyList glueRecords = authRecordInfo.GlueRecords; + if (glueRecords is not null) + { + foreach (DnsResourceRecord glueRecord in glueRecords) + xfrRecords.Add(glueRecord); + } break; default: @@ -1187,7 +1285,7 @@ namespace DnsServerCore.Dns.ZoneManagers //sync records List currentRecords = new List(); - ListAllRecords(zoneName, currentRecords); + ListAllZoneRecords(zoneName, currentRecords); Dictionary>> currentRecordsGroupedByDomain = DnsResourceRecord.GroupRecords(currentRecords); Dictionary>> latestRecordsGroupedByDomain = DnsResourceRecord.GroupRecords(latestRecords); @@ -1686,31 +1784,47 @@ namespace DnsServerCore.Dns.ZoneManagers } } - public List ListZones() + public IReadOnlyList GetAllZones() { - List zones = new List(); - - foreach (AuthZoneNode zoneNode in _root) + _zoneIndexLock.EnterReadLock(); + try { - ApexZone apexZone = zoneNode.ApexZone; - if (apexZone is null) - continue; - - AuthZoneInfo zoneInfo = new AuthZoneInfo(apexZone); - switch (zoneInfo.Type) - { - case AuthZoneType.Primary: - case AuthZoneType.Secondary: - case AuthZoneType.Stub: - case AuthZoneType.Forwarder: - zones.Add(zoneInfo); - break; - } + return new List(_zoneIndex); } + finally + { + _zoneIndexLock.ExitReadLock(); + } + } - _totalZones = zones.Count; + public ZonesPage GetZonesPage(int pageNumber, int zonesPerPage) + { + _zoneIndexLock.EnterReadLock(); + try + { + if (pageNumber == 0) + pageNumber = 1; - return zones; + int totalZones = _zoneIndex.Count; + int totalPages = (totalZones / zonesPerPage) + (totalZones % zonesPerPage > 0 ? 1 : 0); + + if ((pageNumber > totalPages) || (pageNumber < 0)) + pageNumber = totalPages; + + int start = (pageNumber - 1) * zonesPerPage; + int end = Math.Min(start + zonesPerPage, totalZones); + + List zones = new List(end - start); + + for (int i = start; i < end; i++) + zones.Add(_zoneIndex[i]); + + return new ZonesPage(pageNumber, totalPages, totalZones, zones); + } + finally + { + _zoneIndexLock.ExitReadLock(); + } } public void ListSubDomains(string domain, List subDomains) @@ -1725,14 +1839,14 @@ namespace DnsServerCore.Dns.ZoneManagers { bool dnssecOk = request.DnssecOk && (apexZone.DnssecStatus != AuthZoneDnssecStatus.Unsigned); - return GetReferralResponse(request, dnssecOk, delegation, apexZone, true); + return GetReferralResponse(request, dnssecOk, delegation, apexZone); } //no delegation found return null; } - public DnsDatagram Query(DnsDatagram request, bool isRecursionAllowed) + public DnsDatagram Query(DnsDatagram request) { DnsQuestionRecord question = request.Question[0]; @@ -1747,10 +1861,10 @@ namespace DnsServerCore.Dns.ZoneManagers { //zone not found if ((delegation is not null) && delegation.IsActive && (delegation.Name.Length > apexZone.Name.Length)) - return GetReferralResponse(request, dnssecOk, delegation, apexZone, isRecursionAllowed); + return GetReferralResponse(request, dnssecOk, delegation, apexZone); if (apexZone is StubZone) - return GetReferralResponse(request, false, apexZone, apexZone, isRecursionAllowed); + return GetReferralResponse(request, false, apexZone, apexZone); DnsResponseCode rCode = DnsResponseCode.NoError; IReadOnlyList answer = null; @@ -1786,7 +1900,7 @@ namespace DnsServerCore.Dns.ZoneManagers if (authority.Count == 0) { if (apexZone is ForwarderZone) - return GetForwarderResponse(request, null, closest, apexZone, isRecursionAllowed); //no DNAME or APP record available so process FWD response + return GetForwarderResponse(request, null, closest, apexZone); //no DNAME or APP record available so process FWD response if (!hasSubDomains) rCode = DnsResponseCode.NxDomain; @@ -1817,7 +1931,7 @@ namespace DnsServerCore.Dns.ZoneManagers } } - return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, true, false, request.RecursionDesired, isRecursionAllowed, false, false, rCode, request.Question, answer, authority); + return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, true, false, request.RecursionDesired, false, false, false, rCode, request.Question, answer, authority); } else { @@ -1835,7 +1949,7 @@ namespace DnsServerCore.Dns.ZoneManagers else if (zone.Equals(delegation)) { //zone is delegation - return GetReferralResponse(request, dnssecOk, delegation, apexZone, isRecursionAllowed); + return GetReferralResponse(request, dnssecOk, delegation, apexZone); } IReadOnlyList authority = null; @@ -1865,10 +1979,10 @@ namespace DnsServerCore.Dns.ZoneManagers { //check for delegation, stub & forwarder if ((delegation is not null) && delegation.IsActive && (delegation.Name.Length > apexZone.Name.Length)) - return GetReferralResponse(request, dnssecOk, delegation, apexZone, isRecursionAllowed); + return GetReferralResponse(request, dnssecOk, delegation, apexZone); if (apexZone is StubZone) - return GetReferralResponse(request, false, apexZone, apexZone, isRecursionAllowed); + return GetReferralResponse(request, false, apexZone, apexZone); } authority = zone.QueryRecords(DnsResourceRecordType.APP, false); @@ -1883,7 +1997,7 @@ namespace DnsServerCore.Dns.ZoneManagers if (authority.Count == 0) { if (apexZone is ForwarderZone) - return GetForwarderResponse(request, zone, closest, apexZone, isRecursionAllowed); //no APP record available so process FWD response + return GetForwarderResponse(request, zone, closest, apexZone); //no APP record available so process FWD response authority = apexZone.QueryRecords(DnsResourceRecordType.SOA, dnssecOk); @@ -1922,7 +2036,7 @@ namespace DnsServerCore.Dns.ZoneManagers DnsResourceRecord[] wildcardAnswers = new DnsResourceRecord[answers.Count]; for (int i = 0; i < answers.Count; i++) - wildcardAnswers[i] = new DnsResourceRecord(question.Name, answers[i].Type, answers[i].Class, answers[i].TtlValue, answers[i].RDATA) { Tag = answers[i].Tag }; + wildcardAnswers[i] = new DnsResourceRecord(question.Name, answers[i].Type, answers[i].Class, answers[i].TTL, answers[i].RDATA) { Tag = answers[i].Tag }; answers = wildcardAnswers; @@ -1975,7 +2089,7 @@ namespace DnsServerCore.Dns.ZoneManagers } } - return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, answers, authority, additional); + return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, true, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question, answers, authority, additional); } } @@ -2006,7 +2120,7 @@ namespace DnsServerCore.Dns.ZoneManagers } } - public void LoadZoneFrom(Stream s) + public AuthZoneInfo LoadZoneFrom(Stream s) { BinaryReader bR = new BinaryReader(s); @@ -2018,108 +2132,110 @@ namespace DnsServerCore.Dns.ZoneManagers case 2: { DnsResourceRecord[] records = new DnsResourceRecord[bR.ReadInt32()]; - if (records.Length > 0) + if (records.Length == 0) + throw new InvalidDataException("Zone does not contain SOA record."); + + DnsResourceRecord soaRecord = null; + + for (int i = 0; i < records.Length; i++) { - DnsResourceRecord soaRecord = null; + records[i] = new DnsResourceRecord(s); - for (int i = 0; i < records.Length; i++) - { - records[i] = new DnsResourceRecord(s); - - if (records[i].Type == DnsResourceRecordType.SOA) - soaRecord = records[i]; - } - - if (soaRecord == null) - throw new InvalidDataException("Zone does not contain SOA record."); - - //make zone info - AuthZoneType zoneType; - if (_dnsServer.ServerDomain.Equals((soaRecord.RDATA as DnsSOARecordData).PrimaryNameServer, StringComparison.OrdinalIgnoreCase)) - zoneType = AuthZoneType.Primary; - else - zoneType = AuthZoneType.Stub; - - AuthZoneInfo zoneInfo = new AuthZoneInfo(records[0].Name, zoneType, false); - - //create zone - ApexZone apexZone = CreateEmptyZone(zoneInfo); - - try - { - //load records - LoadRecords(apexZone, records); - } - catch - { - DeleteZone(zoneInfo.Name); - throw; - } - - //init zone - switch (zoneInfo.Type) - { - case AuthZoneType.Primary: - (apexZone as PrimaryZone).TriggerNotify(); - break; - } + if (records[i].Type == DnsResourceRecordType.SOA) + soaRecord = records[i]; } + + if (soaRecord == null) + throw new InvalidDataException("Zone does not contain SOA record."); + + //make zone info + AuthZoneType zoneType; + if (_dnsServer.ServerDomain.Equals((soaRecord.RDATA as DnsSOARecordData).PrimaryNameServer, StringComparison.OrdinalIgnoreCase)) + zoneType = AuthZoneType.Primary; + else + zoneType = AuthZoneType.Stub; + + AuthZoneInfo zoneInfo = new AuthZoneInfo(records[0].Name, zoneType, false); + + //create zone + ApexZone apexZone = CreateEmptyZone(zoneInfo); + + try + { + //load records + LoadRecords(apexZone, records); + } + catch + { + DeleteZone(zoneInfo.Name); + throw; + } + + //init zone + switch (zoneInfo.Type) + { + case AuthZoneType.Primary: + (apexZone as PrimaryZone).TriggerNotify(); + break; + } + + return new AuthZoneInfo(apexZone); } - break; case 3: { bool zoneDisabled = bR.ReadBoolean(); DnsResourceRecord[] records = new DnsResourceRecord[bR.ReadInt32()]; - if (records.Length > 0) + if (records.Length == 0) + throw new InvalidDataException("Zone does not contain SOA record."); + + DnsResourceRecord soaRecord = null; + + for (int i = 0; i < records.Length; i++) { - DnsResourceRecord soaRecord = null; + records[i] = new DnsResourceRecord(s); + records[i].Tag = new AuthRecordInfo(bR, records[i].Type == DnsResourceRecordType.SOA); - for (int i = 0; i < records.Length; i++) - { - records[i] = new DnsResourceRecord(s); - records[i].Tag = new DnsResourceRecordInfo(bR, records[i].Type == DnsResourceRecordType.SOA); - - if (records[i].Type == DnsResourceRecordType.SOA) - soaRecord = records[i]; - } - - if (soaRecord == null) - throw new InvalidDataException("Zone does not contain SOA record."); - - //make zone info - AuthZoneType zoneType; - if (_dnsServer.ServerDomain.Equals((soaRecord.RDATA as DnsSOARecordData).PrimaryNameServer, StringComparison.OrdinalIgnoreCase)) - zoneType = AuthZoneType.Primary; - else - zoneType = AuthZoneType.Stub; - - AuthZoneInfo zoneInfo = new AuthZoneInfo(records[0].Name, zoneType, zoneDisabled); - - //create zone - ApexZone apexZone = CreateEmptyZone(zoneInfo); - - try - { - //load records - LoadRecords(apexZone, records); - } - catch - { - DeleteZone(zoneInfo.Name); - throw; - } - - //init zone - switch (zoneInfo.Type) - { - case AuthZoneType.Primary: - (apexZone as PrimaryZone).TriggerNotify(); - break; - } + if (records[i].Type == DnsResourceRecordType.SOA) + soaRecord = records[i]; } + + if (soaRecord == null) + throw new InvalidDataException("Zone does not contain SOA record."); + + //make zone info + AuthZoneType zoneType; + if (_dnsServer.ServerDomain.Equals((soaRecord.RDATA as DnsSOARecordData).PrimaryNameServer, StringComparison.OrdinalIgnoreCase)) + zoneType = AuthZoneType.Primary; + else + zoneType = AuthZoneType.Stub; + + AuthZoneInfo zoneInfo = new AuthZoneInfo(records[0].Name, zoneType, zoneDisabled); + + //create zone + ApexZone apexZone = CreateEmptyZone(zoneInfo); + + try + { + //load records + LoadRecords(apexZone, records); + } + catch + { + DeleteZone(zoneInfo.Name); + throw; + } + + //init zone + switch (zoneInfo.Type) + { + case AuthZoneType.Primary: + (apexZone as PrimaryZone).TriggerNotify(); + break; + } + + return new AuthZoneInfo(apexZone); } - break; case 4: { @@ -2136,7 +2252,7 @@ namespace DnsServerCore.Dns.ZoneManagers for (int i = 0; i < records.Length; i++) { records[i] = new DnsResourceRecord(s); - records[i].Tag = new DnsResourceRecordInfo(bR, records[i].Type == DnsResourceRecordType.SOA); + records[i].Tag = new AuthRecordInfo(bR, records[i].Type == DnsResourceRecordType.SOA); } try @@ -2169,8 +2285,9 @@ namespace DnsServerCore.Dns.ZoneManagers break; } } + + return new AuthZoneInfo(apexZone); } - break; default: throw new InvalidDataException("DNS Zone file version not supported."); @@ -2197,7 +2314,7 @@ namespace DnsServerCore.Dns.ZoneManagers //write all zone records List records = new List(); - ListAllRecords(zoneName, records); + ListAllZoneRecords(zoneName, records); bW.Write(records.Count); @@ -2205,8 +2322,8 @@ namespace DnsServerCore.Dns.ZoneManagers { record.WriteTo(s); - if (record.Tag is not DnsResourceRecordInfo rrInfo) - rrInfo = new DnsResourceRecordInfo(); //default info + if (record.Tag is not AuthRecordInfo rrInfo) + rrInfo = AuthRecordInfo.Default; //default info rrInfo.WriteTo(bW); } @@ -2257,8 +2374,48 @@ namespace DnsServerCore.Dns.ZoneManagers } public int TotalZones - { get { return _totalZones; } } + { get { return _zoneIndex.Count; } } #endregion + + public class ZonesPage + { + #region variables + + readonly long _pageNumber; + readonly long _totalPages; + readonly long _totalZones; + readonly IReadOnlyList _zones; + + #endregion + + #region constructor + + public ZonesPage(long pageNumber, long totalPages, long totalZones, IReadOnlyList zones) + { + _pageNumber = pageNumber; + _totalPages = totalPages; + _totalZones = totalZones; + _zones = zones; + } + + #endregion + + #region properties + + public long PageNumber + { get { return _pageNumber; } } + + public long TotalPages + { get { return _totalPages; } } + + public long TotalZones + { get { return _totalZones; } } + + public IReadOnlyList Zones + { get { return _zones; } } + + #endregion + } } } diff --git a/DnsServerCore/Dns/ZoneManagers/BlockListZoneManager.cs b/DnsServerCore/Dns/ZoneManagers/BlockListZoneManager.cs index a33a1421..0dc0bcfb 100644 --- a/DnsServerCore/Dns/ZoneManagers/BlockListZoneManager.cs +++ b/DnsServerCore/Dns/ZoneManagers/BlockListZoneManager.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -27,6 +27,7 @@ using System.Text; using System.Threading.Tasks; using TechnitiumLibrary.Net; using TechnitiumLibrary.Net.Dns; +using TechnitiumLibrary.Net.Dns.EDnsOptions; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace DnsServerCore.Dns.ZoneManagers @@ -35,6 +36,8 @@ namespace DnsServerCore.Dns.ZoneManagers { #region variables + readonly static char[] _popWordSeperator = new char[] { ' ', '\t' }; + readonly DnsServer _dnsServer; readonly string _localCacheFolder; @@ -87,9 +90,9 @@ namespace DnsServerCore.Dns.ZoneManagers if (line.Length == 0) return line; - line = line.TrimStart(' ', '\t'); + line = line.TrimStart(_popWordSeperator); - int i = line.IndexOfAny(new char[] { ' ', '\t' }); + int i = line.IndexOfAny(_popWordSeperator); string word; if (i < 0) @@ -106,92 +109,138 @@ namespace DnsServerCore.Dns.ZoneManagers return word; } - private Queue ReadListFile(Uri listUrl, bool isAllowList) + private Queue ReadListFile(Uri listUrl, bool isAllowList, Dictionary allowedDomains) { Queue domains = new Queue(); try { - LogManager log = _dnsServer.LogManager; - if (log != null) - log.Write("DNS Server is reading " + (isAllowList ? "allow" : "block") + " list from: " + listUrl.AbsoluteUri); + _dnsServer.LogManager?.Write("DNS Server is reading " + (isAllowList ? "allow" : "block") + " list from: " + listUrl.AbsoluteUri); using (FileStream fS = new FileStream(GetBlockListFilePath(listUrl), FileMode.Open, FileAccess.Read)) { //parse hosts file and populate block zone StreamReader sR = new StreamReader(fS, true); + char[] trimSeperator = new char[] { ' ', '\t', '*', '.' }; string line; string firstWord; string secondWord; string hostname; + string domain; + string options; + int i; while (true) { line = sR.ReadLine(); - if (line == null) + if (line is null) break; //eof - line = line.TrimStart(' ', '\t'); + line = line.TrimStart(trimSeperator); if (line.Length == 0) continue; //skip empty line - if (line.StartsWith("#")) + if (line.StartsWith("#") || line.StartsWith("!")) continue; //skip comment line - firstWord = PopWord(ref line); - - if (line.Length == 0) + if (line.StartsWith("||")) { - hostname = firstWord; + //adblock format + i = line.IndexOf('^'); + if (i > -1) + { + domain = line.Substring(2, i - 2); + options = line.Substring(i + 1); + + if (((options.Length == 0) || (options.StartsWith("$") && (options.Contains("doc") || options.Contains("all")))) && DnsClient.IsDomainNameValid(domain)) + domains.Enqueue(domain); + } + else + { + domain = line.Substring(2); + + if (DnsClient.IsDomainNameValid(domain)) + domains.Enqueue(domain); + } + } + else if (line.StartsWith("@@||")) + { + //adblock format + if (!isAllowList) + { + i = line.IndexOf('^'); + if (i > -1) + { + domain = line.Substring(4, i - 4); + options = line.Substring(i + 1); + + if (((options.Length == 0) || (options.StartsWith("$") && (options.Contains("doc") || options.Contains("all")))) && DnsClient.IsDomainNameValid(domain)) + allowedDomains.TryAdd(domain, null); + } + else + { + domain = line.Substring(4); + + if (DnsClient.IsDomainNameValid(domain)) + allowedDomains.TryAdd(domain, null); + } + } } else { - secondWord = PopWord(ref line); + //hosts file format + firstWord = PopWord(ref line); - if (secondWord.Length == 0) + if (line.Length == 0) + { hostname = firstWord; + } else - hostname = secondWord; + { + secondWord = PopWord(ref line); + + if (secondWord.Length == 0) + hostname = firstWord; + else + hostname = secondWord; + } + + hostname = hostname.Trim('.').ToLower(); + + switch (hostname) + { + case "": + case "localhost": + case "localhost.localdomain": + case "local": + case "broadcasthost": + case "ip6-localhost": + case "ip6-loopback": + case "ip6-localnet": + case "ip6-mcastprefix": + case "ip6-allnodes": + case "ip6-allrouters": + case "ip6-allhosts": + continue; //skip these hostnames + } + + if (!DnsClient.IsDomainNameValid(hostname)) + continue; + + if (IPAddress.TryParse(hostname, out _)) + continue; //skip line when hostname is IP address + + domains.Enqueue(hostname); } - - hostname = hostname.Trim('.').ToLower(); - - switch (hostname) - { - case "": - case "localhost": - case "localhost.localdomain": - case "local": - case "broadcasthost": - case "ip6-localhost": - case "ip6-loopback": - case "ip6-localnet": - case "ip6-mcastprefix": - case "ip6-allnodes": - case "ip6-allrouters": - case "ip6-allhosts": - continue; //skip these hostnames - } - - if (!DnsClient.IsDomainNameValid(hostname)) - continue; - - if (IPAddress.TryParse(hostname, out _)) - continue; //skip line when hostname is IP address - - domains.Enqueue(hostname); } } - if (log != null) - log.Write("DNS Server read " + (isAllowList ? "allow" : "block") + " list file (" + domains.Count + " domains) from: " + listUrl.AbsoluteUri); + _dnsServer.LogManager?.Write("DNS Server read " + (isAllowList ? "allow" : "block") + " list file (" + domains.Count + " domains) from: " + listUrl.AbsoluteUri); } catch (Exception ex) { - LogManager log = _dnsServer.LogManager; - if (log != null) - log.Write("DNS Server failed to read " + (isAllowList ? "allow" : "block") + " list from: " + listUrl.AbsoluteUri + "\r\n" + ex.ToString()); + _dnsServer.LogManager?.Write("DNS Server failed to read " + (isAllowList ? "allow" : "block") + " list from: " + listUrl.AbsoluteUri + "\r\n" + ex.ToString()); } return domains; @@ -243,7 +292,7 @@ namespace DnsServerCore.Dns.ZoneManagers foreach (Uri allowListUrl in _allowListUrls) { - Queue queue = ReadListFile(allowListUrl, true); + Queue queue = ReadListFile(allowListUrl, true, null); while (queue.Count > 0) { @@ -261,7 +310,7 @@ namespace DnsServerCore.Dns.ZoneManagers { if (!blockListQueues.ContainsKey(blockListUrl)) { - Queue blockListQueue = ReadListFile(blockListUrl, false); + Queue blockListQueue = ReadListFile(blockListUrl, false, allowedDomains); totalDomains += blockListQueue.Count; blockListQueues.Add(blockListUrl, blockListQueue); } @@ -322,7 +371,7 @@ namespace DnsServerCore.Dns.ZoneManagers SocketsHttpHandler handler = new SocketsHttpHandler(); handler.Proxy = _dnsServer.Proxy; handler.UseProxy = _dnsServer.Proxy is not null; - handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; + handler.AutomaticDecompression = DecompressionMethods.All; using (HttpClient http = new HttpClient(handler)) { @@ -419,10 +468,20 @@ namespace DnsServerCore.Dns.ZoneManagers for (int i = 0; i < answer.Length; i++) answer[i] = new DnsResourceRecord(question.Name, DnsResourceRecordType.TXT, question.Class, 60, new DnsTXTRecordData("source=block-list-zone; blockListUrl=" + blockLists[i].AbsoluteUri + "; domain=" + blockedDomain)); - return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, true, false, false, DnsResponseCode.NoError, request.Question, answer); + return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question, answer); } else { + EDnsOption[] options = null; + + if (_dnsServer.AllowTxtBlockingReport && (request.EDNS is not null)) + { + options = new EDnsOption[blockLists.Count]; + + for (int i = 0; i < options.Length; i++) + options[i] = new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.Blocked, "source=block-list-zone; blockListUrl=" + blockLists[i].AbsoluteUri + "; domain=" + blockedDomain)); + } + IReadOnlyCollection aRecords; IReadOnlyCollection aaaaRecords; @@ -443,7 +502,7 @@ namespace DnsServerCore.Dns.ZoneManagers if (parentDomain is null) parentDomain = string.Empty; - return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, true, false, false, DnsResponseCode.NxDomain, request.Question, null, new DnsResourceRecord[] { new DnsResourceRecord(parentDomain, DnsResourceRecordType.SOA, question.Class, 60, _soaRecord) }); + return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NxDomain, request.Question, null, new DnsResourceRecord[] { new DnsResourceRecord(parentDomain, DnsResourceRecordType.SOA, question.Class, 60, _soaRecord) }, null, request.EDNS is null ? ushort.MinValue : _dnsServer.UdpPayloadSize, EDnsHeaderFlags.None, options); default: throw new InvalidOperationException(); @@ -493,7 +552,7 @@ namespace DnsServerCore.Dns.ZoneManagers break; } - return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, true, false, false, DnsResponseCode.NoError, request.Question, answer, authority); + return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question, answer, authority, null, request.EDNS is null ? ushort.MinValue : _dnsServer.UdpPayloadSize, EDnsHeaderFlags.None, options); } } diff --git a/DnsServerCore/Dns/ZoneManagers/BlockedZoneManager.cs b/DnsServerCore/Dns/ZoneManagers/BlockedZoneManager.cs index 14a39b01..3f6f4695 100644 --- a/DnsServerCore/Dns/ZoneManagers/BlockedZoneManager.cs +++ b/DnsServerCore/Dns/ZoneManagers/BlockedZoneManager.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -155,14 +155,14 @@ namespace DnsServerCore.Dns.ZoneManagers _zoneManager.Flush(); } - public List ListZones() + public IReadOnlyList GetAllZones() { - return _zoneManager.ListZones(); + return _zoneManager.GetAllZones(); } public void ListAllRecords(string domain, List records) { - _zoneManager.ListAllRecords(domain, records); + _zoneManager.ListAllRecords(domain, domain, records); } public void ListSubDomains(string domain, List subDomains) @@ -172,7 +172,7 @@ namespace DnsServerCore.Dns.ZoneManagers public void SaveZoneFile() { - List blockedZones = _dnsServer.BlockedZoneManager.ListZones(); + IReadOnlyList blockedZones = _dnsServer.BlockedZoneManager.GetAllZones(); string blockedZoneFile = Path.Combine(_dnsServer.ConfigFolder, "blocked.config"); @@ -196,7 +196,7 @@ namespace DnsServerCore.Dns.ZoneManagers public DnsDatagram Query(DnsDatagram request) { - return _zoneManager.Query(request, true); + return _zoneManager.Query(request); } #endregion diff --git a/DnsServerCore/Dns/ZoneManagers/CacheZoneManager.cs b/DnsServerCore/Dns/ZoneManagers/CacheZoneManager.cs index 1d28c6e6..5d4391e5 100644 --- a/DnsServerCore/Dns/ZoneManagers/CacheZoneManager.cs +++ b/DnsServerCore/Dns/ZoneManagers/CacheZoneManager.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -22,6 +22,8 @@ using DnsServerCore.Dns.Trees; using DnsServerCore.Dns.Zones; using System; using System.Collections.Generic; +using System.IO; +using System.Text; using System.Threading; using TechnitiumLibrary.Net; using TechnitiumLibrary.Net.Dns; @@ -80,7 +82,7 @@ namespace DnsServerCore.Dns.ZoneManagers if ((glueRecords is not null) || (rrsigRecords is not null) || (nsecRecords is not null) || (eDnsClientSubnet is not null)) { - DnsResourceRecordInfo rrInfo = resourceRecord.GetRecordInfo(); + CacheRecordInfo rrInfo = resourceRecord.GetCacheRecordInfo(); rrInfo.GlueRecords = glueRecords; rrInfo.RRSIGRecords = rrsigRecords; @@ -93,7 +95,7 @@ namespace DnsServerCore.Dns.ZoneManagers { IReadOnlyList glueRRSIGRecords = GetRRSIGRecordsFrom(glueRecord); if (glueRRSIGRecords is not null) - glueRecord.GetRecordInfo().RRSIGRecords = glueRRSIGRecords; + glueRecord.GetCacheRecordInfo().RRSIGRecords = glueRRSIGRecords; } } @@ -103,7 +105,7 @@ namespace DnsServerCore.Dns.ZoneManagers { IReadOnlyList nsecRRSIGRecords = GetRRSIGRecordsFrom(nsecRecord); if (nsecRRSIGRecords is not null) - nsecRecord.GetRecordInfo().RRSIGRecords = nsecRRSIGRecords; + nsecRecord.GetCacheRecordInfo().RRSIGRecords = nsecRRSIGRecords; } } } @@ -191,7 +193,7 @@ namespace DnsServerCore.Dns.ZoneManagers } //no DS records found check for NSEC records - IReadOnlyList nsecRecords = nsRecords[0].GetRecordInfo().NSECRecords; + IReadOnlyList nsecRecords = nsRecords[0].GetCacheRecordInfo().NSECRecords; if (nsecRecords is not null) { List newNSRecords = new List(nsRecords.Count + nsecRecords.Count); @@ -215,7 +217,7 @@ namespace DnsServerCore.Dns.ZoneManagers { newAnswerList.Add(record); - DnsResourceRecordInfo rrInfo = record.GetRecordInfo(); + CacheRecordInfo rrInfo = record.GetCacheRecordInfo(); IReadOnlyList rrsigRecords = rrInfo.RRSIGRecords; if (rrsigRecords is not null) @@ -238,7 +240,7 @@ namespace DnsServerCore.Dns.ZoneManagers { newAuthorityList.Add(nsecRecord); - IReadOnlyList nsecRRSIGRecords = nsecRecord.GetRecordInfo().RRSIGRecords; + IReadOnlyList nsecRRSIGRecords = nsecRecord.GetCacheRecordInfo().RRSIGRecords; if (nsecRRSIGRecords is not null) newAuthorityList.AddRange(nsecRRSIGRecords); } @@ -299,7 +301,7 @@ namespace DnsServerCore.Dns.ZoneManagers if (DnsClient.IsDomainNameValid(result)) { - DnsResourceRecord cnameRR = new DnsResourceRecord(question.Name, DnsResourceRecordType.CNAME, question.Class, dnameRR.TtlValue, new DnsCNAMERecordData(result)); + DnsResourceRecord cnameRR = new DnsResourceRecord(question.Name, DnsResourceRecordType.CNAME, question.Class, dnameRR.TTL, new DnsCNAMERecordData(result)); List list = new List(5) { @@ -355,8 +357,8 @@ namespace DnsServerCore.Dns.ZoneManagers private void ResolveAdditionalRecords(DnsResourceRecord refRecord, string domain, bool serveStale, bool dnssecOk, List additionalRecords) { - IReadOnlyList glueRecords = refRecord.GetGlueRecords(); - if (glueRecords.Count > 0) + IReadOnlyList glueRecords = refRecord.GetCacheRecordInfo().GlueRecords; + if (glueRecords is not null) { bool added = false; @@ -369,7 +371,7 @@ namespace DnsServerCore.Dns.ZoneManagers if (dnssecOk) { - IReadOnlyList rrsigRecords = glueRecord.GetRecordInfo().RRSIGRecords; + IReadOnlyList rrsigRecords = glueRecord.GetCacheRecordInfo().RRSIGRecords; if (rrsigRecords is not null) additionalRecords.AddRange(rrsigRecords); } @@ -591,7 +593,7 @@ namespace DnsServerCore.Dns.ZoneManagers { EDnsClientSubnetOptionData requestECS = request.GetEDnsClientSubnetOption(); if (requestECS is not null) - eDnsClientSubnet = new NetworkAddress(requestECS.AddressValue, requestECS.SourcePrefixLength); + eDnsClientSubnet = new NetworkAddress(requestECS.Address, requestECS.SourcePrefixLength); } CacheZone zone; @@ -668,10 +670,10 @@ namespace DnsServerCore.Dns.ZoneManagers EDnsClientSubnetOptionData requestECS = request.GetEDnsClientSubnetOption(true); if (requestECS is not null) { - NetworkAddress recordECS = firstRR.GetRecordInfo().EDnsClientSubnet; + NetworkAddress recordECS = firstRR.GetCacheRecordInfo().EDnsClientSubnet; if (recordECS is not null) { - EDnsOption[] ecsOption = EDnsClientSubnetOptionData.GetEDnsClientSubnetOption(requestECS.SourcePrefixLength, recordECS.PrefixLength, requestECS.AddressValue); + EDnsOption[] ecsOption = EDnsClientSubnetOptionData.GetEDnsClientSubnetOption(requestECS.SourcePrefixLength, recordECS.PrefixLength, requestECS.Address); if ((specialOptions is null) || (specialOptions.Count == 0)) { @@ -798,7 +800,7 @@ namespace DnsServerCore.Dns.ZoneManagers foreach (DnsResourceRecord record in answer) { - NetworkAddress recordECS = record.GetRecordInfo().EDnsClientSubnet; + NetworkAddress recordECS = record.GetCacheRecordInfo().EDnsClientSubnet; if (recordECS is not null) { if ((suitableECS is null) || (recordECS.PrefixLength > suitableECS.PrefixLength)) @@ -808,7 +810,7 @@ namespace DnsServerCore.Dns.ZoneManagers if (suitableECS is not null) { - EDnsOption[] ecsOption = EDnsClientSubnetOptionData.GetEDnsClientSubnetOption(requestECS.SourcePrefixLength, suitableECS.PrefixLength, requestECS.AddressValue); + EDnsOption[] ecsOption = EDnsClientSubnetOptionData.GetEDnsClientSubnetOption(requestECS.SourcePrefixLength, suitableECS.PrefixLength, requestECS.Address); if (options is null) { @@ -950,6 +952,85 @@ namespace DnsServerCore.Dns.ZoneManagers return null; } + public void LoadCacheZoneFile() + { + string cacheZoneFile = Path.Combine(_dnsServer.ConfigFolder, "cache.bin"); + + if (!File.Exists(cacheZoneFile)) + return; + + _dnsServer.LogManager?.Write("Loading DNS Cache from disk..."); + + using (FileStream fS = new FileStream(cacheZoneFile, FileMode.Open, FileAccess.Read)) + { + BinaryReader bR = new BinaryReader(fS); + + if (Encoding.ASCII.GetString(bR.ReadBytes(2)) != "CZ") + throw new InvalidDataException("CacheZoneManager format is invalid."); + + int version = bR.ReadByte(); + switch (version) + { + case 1: + int addedEntries = 0; + + try + { + bool serveStale = _dnsServer.ServeStale; + + while (bR.BaseStream.Position < bR.BaseStream.Length) + { + CacheZone zone = CacheZone.ReadFrom(bR, serveStale); + if (!zone.IsEmpty) + { + if (_root.TryAdd(zone.Name, zone)) + addedEntries += zone.TotalEntries; + } + } + } + finally + { + if (addedEntries > 0) + Interlocked.Add(ref _totalEntries, addedEntries); + } + break; + + default: + throw new InvalidDataException("CacheZoneManager format version not supported: " + version); + } + } + + _dnsServer.LogManager?.Write("DNS Cache was loaded from disk successfully."); + } + + public void SaveCacheZoneFile() + { + _dnsServer.LogManager?.Write("Saving DNS Cache to disk..."); + + string cacheZoneFile = Path.Combine(_dnsServer.ConfigFolder, "cache.bin"); + + using (FileStream fS = new FileStream(cacheZoneFile, FileMode.Create, FileAccess.Write)) + { + BinaryWriter bW = new BinaryWriter(fS); + + bW.Write(Encoding.ASCII.GetBytes("CZ")); //format + bW.Write((byte)1); //version + + foreach (CacheZone zone in _root) + zone.WriteTo(bW); + } + + _dnsServer.LogManager?.Write("DNS Cache was saved to disk successfully."); + } + + public void DeleteCacheZoneFile() + { + string cacheZoneFile = Path.Combine(_dnsServer.ConfigFolder, "cache.bin"); + + if (File.Exists(cacheZoneFile)) + File.Delete(cacheZoneFile); + } + #endregion #region properties diff --git a/DnsServerCore/Dns/Zones/ApexZone.cs b/DnsServerCore/Dns/Zones/ApexZone.cs index 14911e68..3017cfc9 100644 --- a/DnsServerCore/Dns/Zones/ApexZone.cs +++ b/DnsServerCore/Dns/Zones/ApexZone.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -146,7 +146,7 @@ namespace DnsServerCore.Dns.Zones while (index < history.Count) { //check difference sequence - if (history[index].GetDeletedOn() > expiry) + if (history[index].GetAuthRecordInfo().DeletedOn > expiry) break; //found record to keep //skip to next difference sequence @@ -207,7 +207,7 @@ namespace DnsServerCore.Dns.Zones //notify all secondary name servers foreach (DnsResourceRecord nsRecord in nsRecords) { - if (nsRecord.IsDisabled()) + if (nsRecord.GetAuthRecordInfo().Disabled) continue; string nameServerHost = (nsRecord.RDATA as DnsNSRecordData).NameServer; @@ -430,8 +430,8 @@ namespace DnsServerCore.Dns.Zones { string nsDomain = (nsRecord.RDATA as DnsNSRecordData).NameServer; - IReadOnlyList glueRecords = nsRecord.GetGlueRecords(); - if (glueRecords.Count > 0) + IReadOnlyList glueRecords = nsRecord.GetAuthRecordInfo().GlueRecords; + if (glueRecords is not null) { foreach (DnsResourceRecord glueRecord in glueRecords) { @@ -514,8 +514,8 @@ namespace DnsServerCore.Dns.Zones { DnsResourceRecord soaRecord = _entries[DnsResourceRecordType.SOA][0]; - IReadOnlyList primaryNameServers = soaRecord.GetPrimaryNameServers(); - if (primaryNameServers.Count > 0) + IReadOnlyList primaryNameServers = soaRecord.GetAuthRecordInfo().PrimaryNameServers; + if (primaryNameServers is not null) { List resolvedNameServers = new List(primaryNameServers.Count * 2); @@ -537,7 +537,7 @@ namespace DnsServerCore.Dns.Zones foreach (DnsResourceRecord nsRecord in nsRecords) { - if (nsRecord.IsDisabled()) + if (nsRecord.GetAuthRecordInfo().Disabled) continue; if (primaryNameServer.Equals((nsRecord.RDATA as DnsNSRecordData).NameServer, StringComparison.OrdinalIgnoreCase)) @@ -563,7 +563,7 @@ namespace DnsServerCore.Dns.Zones foreach (DnsResourceRecord nsRecord in nsRecords) { - if (nsRecord.IsDisabled()) + if (nsRecord.GetAuthRecordInfo().Disabled) continue; if (primaryNameServer.Equals((nsRecord.RDATA as DnsNSRecordData).NameServer, StringComparison.OrdinalIgnoreCase)) diff --git a/DnsServerCore/Dns/Zones/AuthZone.cs b/DnsServerCore/Dns/Zones/AuthZone.cs index 22bc39bf..faf0ed70 100644 --- a/DnsServerCore/Dns/Zones/AuthZone.cs +++ b/DnsServerCore/Dns/Zones/AuthZone.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -56,22 +56,30 @@ namespace DnsServerCore.Dns.Zones if (records.Count == 1) { - if (records[0].IsDisabled()) + AuthRecordInfo authRecordInfo = records[0].GetAuthRecordInfo(); + + if (authRecordInfo.Disabled) return Array.Empty(); //record disabled //update last used on - records[0].GetRecordInfo().LastUsedOn = DateTime.UtcNow; + authRecordInfo.LastUsedOn = DateTime.UtcNow; return records; } List newRecords = new List(records.Count); + DateTime utcNow = DateTime.UtcNow; foreach (DnsResourceRecord record in records) { - if (record.IsDisabled()) + AuthRecordInfo authRecordInfo = record.GetAuthRecordInfo(); + + if (authRecordInfo.Disabled) continue; //record disabled + //update last used on + authRecordInfo.LastUsedOn = utcNow; + newRecords.Add(record); } @@ -87,12 +95,6 @@ namespace DnsServerCore.Dns.Zones } } - //update last used on - DateTime utcNow = DateTime.UtcNow; - - foreach (DnsResourceRecord record in newRecords) - record.GetRecordInfo().LastUsedOn = utcNow; - return newRecords; } @@ -112,7 +114,7 @@ namespace DnsServerCore.Dns.Zones { if ((rrsigRecord.RDATA as DnsRRSIGRecordData).TypeCovered == type) { - rrsigRecord.GetRecordInfo().LastUsedOn = utcNow; + rrsigRecord.GetAuthRecordInfo().LastUsedOn = utcNow; newRecords.Add(rrsigRecord); } } @@ -491,10 +493,10 @@ namespace DnsServerCore.Dns.Zones { DnsRRSIGRecordData rrsig = rrsigRecord.RDATA as DnsRRSIGRecordData; - uint signatureValidityPeriod = rrsig.SignatureExpirationValue - rrsig.SignatureInceptionValue; + uint signatureValidityPeriod = rrsig.SignatureExpiration - rrsig.SignatureInception; uint refreshPeriod = signatureValidityPeriod / 3; - if (utcNow > DateTime.UnixEpoch.AddSeconds(rrsig.SignatureExpirationValue - refreshPeriod)) + if (utcNow > DateTime.UnixEpoch.AddSeconds(rrsig.SignatureExpiration - refreshPeriod)) typesToRefresh.Add(rrsig.TypeCovered); } @@ -531,7 +533,7 @@ namespace DnsServerCore.Dns.Zones DnsNSECRecordData newNSecRecord = new DnsNSECRecordData(nextDomainName, types); - if (!_entries.TryGetValue(DnsResourceRecordType.NSEC, out IReadOnlyList existingRecords) || (existingRecords[0].TtlValue != ttl) || !existingRecords[0].RDATA.Equals(newNSecRecord)) + if (!_entries.TryGetValue(DnsResourceRecordType.NSEC, out IReadOnlyList existingRecords) || (existingRecords[0].TTL != ttl) || !existingRecords[0].RDATA.Equals(newNSecRecord)) return new DnsResourceRecord[] { new DnsResourceRecord(_name, DnsResourceRecordType.NSEC, DnsClass.IN, ttl, newNSecRecord) }; return Array.Empty(); @@ -539,7 +541,7 @@ namespace DnsServerCore.Dns.Zones internal IReadOnlyList GetUpdatedNSec3RRSet(IReadOnlyList newNSec3Records) { - if (!_entries.TryGetValue(DnsResourceRecordType.NSEC3, out IReadOnlyList existingRecords) || (existingRecords[0].TtlValue != newNSec3Records[0].TtlValue) || !existingRecords[0].RDATA.Equals(newNSec3Records[0].RDATA)) + if (!_entries.TryGetValue(DnsResourceRecordType.NSEC3, out IReadOnlyList existingRecords) || (existingRecords[0].TTL != newNSec3Records[0].TTL) || !existingRecords[0].RDATA.Equals(newNSec3Records[0].RDATA)) return newNSec3Records; return Array.Empty(); @@ -901,7 +903,7 @@ namespace DnsServerCore.Dns.Zones foreach (DnsResourceRecord record in records) { - if (record.IsDisabled()) + if (record.GetAuthRecordInfo().Disabled) continue; return true; diff --git a/DnsServerCore/Dns/Zones/AuthZoneInfo.cs b/DnsServerCore/Dns/Zones/AuthZoneInfo.cs index 3df2fe0d..05de3920 100644 --- a/DnsServerCore/Dns/Zones/AuthZoneInfo.cs +++ b/DnsServerCore/Dns/Zones/AuthZoneInfo.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -61,9 +61,6 @@ namespace DnsServerCore.Dns.Zones readonly IReadOnlyDictionary>> _updateSecurityPolicies; readonly IReadOnlyCollection _dnssecPrivateKeys; - readonly bool _notifyFailed; //not serialized - readonly bool _syncFailed; //not serialized - #endregion #region constructor @@ -117,7 +114,7 @@ namespace DnsServerCore.Dns.Zones IPAddress[] nameServers = new IPAddress[count]; for (int i = 0; i < count; i++) - nameServers[i] = IPAddressExtension.ReadFrom(bR); + nameServers[i] = IPAddressExtensions.ReadFrom(bR); _zoneTransferNameServers = nameServers; } @@ -132,7 +129,7 @@ namespace DnsServerCore.Dns.Zones IPAddress[] nameServers = new IPAddress[count]; for (int i = 0; i < count; i++) - nameServers[i] = IPAddressExtension.ReadFrom(bR); + nameServers[i] = IPAddressExtensions.ReadFrom(bR); _notifyNameServers = nameServers; } @@ -148,7 +145,7 @@ namespace DnsServerCore.Dns.Zones IPAddress[] ipAddresses = new IPAddress[count]; for (int i = 0; i < count; i++) - ipAddresses[i] = IPAddressExtension.ReadFrom(bR); + ipAddresses[i] = IPAddressExtensions.ReadFrom(bR); _updateIpAddresses = ipAddresses; } @@ -183,7 +180,7 @@ namespace DnsServerCore.Dns.Zones for (int i = 0; i < count; i++) { zoneHistory[i] = new DnsResourceRecord(bR.BaseStream); - zoneHistory[i].Tag = new DnsResourceRecordInfo(bR, zoneHistory[i].Type == DnsResourceRecordType.SOA); + zoneHistory[i].Tag = new AuthRecordInfo(bR, zoneHistory[i].Type == DnsResourceRecordType.SOA); } _zoneHistory = zoneHistory; @@ -259,7 +256,7 @@ namespace DnsServerCore.Dns.Zones List dnssecPrivateKeys = new List(count); for (int i = 0; i < count; i++) - dnssecPrivateKeys.Add(DnssecPrivateKey.Parse(bR)); + dnssecPrivateKeys.Add(DnssecPrivateKey.ReadFrom(bR)); _dnssecPrivateKeys = dnssecPrivateKeys; } @@ -278,7 +275,7 @@ namespace DnsServerCore.Dns.Zones for (int i = 0; i < count; i++) { zoneHistory[i] = new DnsResourceRecord(bR.BaseStream); - zoneHistory[i].Tag = new DnsResourceRecordInfo(bR, zoneHistory[i].Type == DnsResourceRecordType.SOA); + zoneHistory[i].Tag = new AuthRecordInfo(bR, zoneHistory[i].Type == DnsResourceRecordType.SOA); } _zoneHistory = zoneHistory; @@ -334,8 +331,6 @@ namespace DnsServerCore.Dns.Zones _zoneTransferTsigKeyNames = primaryZone.ZoneTransferTsigKeyNames; _updateSecurityPolicies = primaryZone.UpdateSecurityPolicies; _dnssecPrivateKeys = primaryZone.DnssecPrivateKeys; - - _notifyFailed = primaryZone.NotifyFailed; } else if (_apexZone is SecondaryZone secondaryZone) { @@ -346,16 +341,11 @@ namespace DnsServerCore.Dns.Zones _expiry = secondaryZone.Expiry; _zoneTransferTsigKeyNames = secondaryZone.ZoneTransferTsigKeyNames; - - _notifyFailed = secondaryZone.NotifyFailed; - _syncFailed = secondaryZone.SyncFailed; } else if (_apexZone is StubZone stubZone) { _type = AuthZoneType.Stub; _expiry = stubZone.Expiry; - - _syncFailed = stubZone.SyncFailed; } else if (_apexZone is ForwarderZone) { @@ -527,8 +517,8 @@ namespace DnsServerCore.Dns.Zones { record.WriteTo(bW.BaseStream); - if (record.Tag is not DnsResourceRecordInfo rrInfo) - rrInfo = new DnsResourceRecordInfo(); //default info + if (record.Tag is not AuthRecordInfo rrInfo) + rrInfo = AuthRecordInfo.Default; //default info rrInfo.WriteTo(bW); } @@ -598,8 +588,8 @@ namespace DnsServerCore.Dns.Zones { record.WriteTo(bW.BaseStream); - if (record.Tag is not DnsResourceRecordInfo rrInfo) - rrInfo = new DnsResourceRecordInfo(); //default info + if (record.Tag is not AuthRecordInfo rrInfo) + rrInfo = AuthRecordInfo.Default; //default info rrInfo.WriteTo(bW); } @@ -630,6 +620,22 @@ namespace DnsServerCore.Dns.Zones return _name.CompareTo(other._name); } + public override bool Equals(object obj) + { + if (ReferenceEquals(this, obj)) + return true; + + if (obj is not AuthZoneInfo other) + return false; + + return _name.Equals(other._name, StringComparison.OrdinalIgnoreCase); + } + + public override int GetHashCode() + { + return _name.GetHashCode(); + } + public override string ToString() { return _name; @@ -650,7 +656,13 @@ namespace DnsServerCore.Dns.Zones public bool Disabled { - get { return _disabled; } + get + { + if (_apexZone is null) + return _disabled; + + return _apexZone.Disabled; + } set { if (_apexZone is null) @@ -662,7 +674,13 @@ namespace DnsServerCore.Dns.Zones public AuthZoneTransfer ZoneTransfer { - get { return _zoneTransfer; } + get + { + if (_apexZone is null) + return _zoneTransfer; + + return _apexZone.ZoneTransfer; + } set { if (_apexZone is null) @@ -674,7 +692,13 @@ namespace DnsServerCore.Dns.Zones public IReadOnlyCollection ZoneTransferNameServers { - get { return _zoneTransferNameServers; } + get + { + if (_apexZone is null) + return _zoneTransferNameServers; + + return _apexZone.ZoneTransferNameServers; + } set { if (_apexZone is null) @@ -686,7 +710,13 @@ namespace DnsServerCore.Dns.Zones public AuthZoneNotify Notify { - get { return _notify; } + get + { + if (_apexZone is null) + return _notify; + + return _apexZone.Notify; + } set { if (_apexZone is null) @@ -698,7 +728,13 @@ namespace DnsServerCore.Dns.Zones public IReadOnlyCollection NotifyNameServers { - get { return _notifyNameServers; } + get + { + if (_apexZone is null) + return _notifyNameServers; + + return _apexZone.NotifyNameServers; + } set { if (_apexZone is null) @@ -710,7 +746,13 @@ namespace DnsServerCore.Dns.Zones public AuthZoneUpdate Update { - get { return _update; } + get + { + if (_apexZone is null) + return _update; + + return _apexZone.Update; + } set { if (_apexZone is null) @@ -722,7 +764,13 @@ namespace DnsServerCore.Dns.Zones public IReadOnlyCollection UpdateIpAddresses { - get { return _updateIpAddresses; } + get + { + if (_apexZone is null) + return _updateIpAddresses; + + return _apexZone.UpdateIpAddresses; + } set { if (_apexZone is null) @@ -733,53 +781,46 @@ namespace DnsServerCore.Dns.Zones } public DateTime Expiry - { get { return _expiry; } } - - public bool IsExpired { get { if (_apexZone is null) - throw new InvalidOperationException(); + return _expiry; switch (_type) { case AuthZoneType.Secondary: - return (_apexZone as SecondaryZone).IsExpired; + return (_apexZone as SecondaryZone).Expiry; case AuthZoneType.Stub: - return (_apexZone as StubZone).IsExpired; + return (_apexZone as StubZone).Expiry; default: - return false; - } - } - } - - public bool Internal - { - get - { - if (_apexZone is null) - throw new InvalidOperationException(); - - switch (_type) - { - case AuthZoneType.Primary: - return (_apexZone as PrimaryZone).Internal; - - default: - return false; + throw new InvalidOperationException(); } } } public IReadOnlyList ZoneHistory - { get { return _zoneHistory; } } + { + get + { + if (_apexZone is null) + return _zoneHistory; + + return _apexZone.GetZoneHistory(); + } + } public IReadOnlyDictionary ZoneTransferTsigKeyNames { - get { return _zoneTransferTsigKeyNames; } + get + { + if (_apexZone is null) + return _zoneTransferTsigKeyNames; + + return _apexZone.ZoneTransferTsigKeyNames; + } set { if (_apexZone is null) @@ -800,7 +841,13 @@ namespace DnsServerCore.Dns.Zones public IReadOnlyDictionary>> UpdateSecurityPolicies { - get { return _updateSecurityPolicies; } + get + { + if (_apexZone is null) + return _updateSecurityPolicies; + + return _apexZone.UpdateSecurityPolicies; + } set { if (_apexZone is null) @@ -818,6 +865,24 @@ namespace DnsServerCore.Dns.Zones } } + public IReadOnlyCollection DnssecPrivateKeys + { + get + { + if (_apexZone is null) + return _dnssecPrivateKeys; + + switch (_type) + { + case AuthZoneType.Primary: + return (_apexZone as PrimaryZone).DnssecPrivateKeys; + + default: + throw new InvalidOperationException(); + } + } + } + public AuthZoneDnssecStatus DnssecStatus { get @@ -842,19 +907,91 @@ namespace DnsServerCore.Dns.Zones return (_apexZone as PrimaryZone).GetDnsKeyTtl(); default: - throw new NotSupportedException(); + throw new InvalidOperationException(); } } } - public IReadOnlyCollection DnssecPrivateKeys - { get { return _dnssecPrivateKeys; } } + public bool Internal + { + get + { + if (_apexZone is null) + throw new InvalidOperationException(); + + switch (_type) + { + case AuthZoneType.Primary: + return (_apexZone as PrimaryZone).Internal; + + default: + return false; + } + } + } + + public bool IsExpired + { + get + { + if (_apexZone is null) + throw new InvalidOperationException(); + + switch (_type) + { + case AuthZoneType.Secondary: + return (_apexZone as SecondaryZone).IsExpired; + + case AuthZoneType.Stub: + return (_apexZone as StubZone).IsExpired; + + default: + return false; + } + } + } public bool NotifyFailed - { get { return _notifyFailed; } } + { + get + { + if (_apexZone is null) + throw new InvalidOperationException(); + + switch (_type) + { + case AuthZoneType.Primary: + return (_apexZone as PrimaryZone).NotifyFailed; + + case AuthZoneType.Secondary: + return (_apexZone as SecondaryZone).NotifyFailed; + + default: + throw new InvalidOperationException(); + } + } + } public bool SyncFailed - { get { return _syncFailed; } } + { + get + { + if (_apexZone is null) + throw new InvalidOperationException(); + + switch (_type) + { + case AuthZoneType.Secondary: + return (_apexZone as SecondaryZone).SyncFailed; + + case AuthZoneType.Stub: + return (_apexZone as StubZone).SyncFailed; + + default: + throw new InvalidOperationException(); + } + } + } #endregion } diff --git a/DnsServerCore/Dns/Zones/CacheZone.cs b/DnsServerCore/Dns/Zones/CacheZone.cs index 866476b1..417836f0 100644 --- a/DnsServerCore/Dns/Zones/CacheZone.cs +++ b/DnsServerCore/Dns/Zones/CacheZone.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -21,6 +21,7 @@ using DnsServerCore.Dns.ResourceRecords; using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.IO; using TechnitiumLibrary; using TechnitiumLibrary.Net; using TechnitiumLibrary.Net.Dns; @@ -42,6 +43,67 @@ namespace DnsServerCore.Dns.Zones : base(name, capacity) { } + private CacheZone(string name, ConcurrentDictionary> entries) + : base(name, entries) + { } + + #endregion + + #region static + + public static CacheZone ReadFrom(BinaryReader bR, bool serveStale) + { + byte version = bR.ReadByte(); + switch (version) + { + case 1: + string name = bR.ReadString(); + ConcurrentDictionary> entries = ReadEntriesFrom(bR, serveStale); + + CacheZone cacheZone = new CacheZone(name, entries); + + //write all ECS cache records + { + int ecsCount = bR.ReadInt32(); + if (ecsCount > 0) + { + ConcurrentDictionary>> ecsEntries = new ConcurrentDictionary>>(1, ecsCount); + + for (int i = 0; i < ecsCount; i++) + { + NetworkAddress key = NetworkAddress.ReadFrom(bR); + ConcurrentDictionary> ecsEntry = ReadEntriesFrom(bR, serveStale); + + if (!ecsEntry.IsEmpty) + ecsEntries.TryAdd(key, ecsEntry); + } + + if (!ecsEntries.IsEmpty) + cacheZone._ecsEntries = ecsEntries; + } + } + + return cacheZone; + + default: + throw new InvalidDataException("CacheZone format version not supported."); + } + } + + public static bool IsTypeSupportedForEDnsClientSubnet(DnsResourceRecordType type) + { + switch (type) + { + case DnsResourceRecordType.A: + case DnsResourceRecordType.AAAA: + case DnsResourceRecordType.CNAME: + return true; + + default: + return false; + } + } + #endregion #region private @@ -73,22 +135,56 @@ namespace DnsServerCore.Dns.Zones DateTime utcNow = DateTime.UtcNow; foreach (DnsResourceRecord record in records) - record.GetRecordInfo().LastUsedOn = utcNow; + record.GetCacheRecordInfo().LastUsedOn = utcNow; return records; } - public static bool IsTypeSupportedForEDnsClientSubnet(DnsResourceRecordType type) + private static ConcurrentDictionary> ReadEntriesFrom(BinaryReader bR, bool serveStale) { - switch (type) - { - case DnsResourceRecordType.A: - case DnsResourceRecordType.AAAA: - case DnsResourceRecordType.CNAME: - return true; + int count = bR.ReadInt32(); + ConcurrentDictionary> entries = new ConcurrentDictionary>(1, count); - default: - return false; + for (int i = 0; i < count; i++) + { + DnsResourceRecordType key = (DnsResourceRecordType)bR.ReadUInt16(); + int rrCount = bR.ReadInt32(); + DnsResourceRecord[] records = new DnsResourceRecord[rrCount]; + + for (int j = 0; j < rrCount; j++) + { + records[j] = DnsResourceRecord.ReadCacheRecordFrom(bR, delegate (DnsResourceRecord record) + { + record.Tag = new CacheRecordInfo(bR); + }); + } + + if (!DnsResourceRecord.IsRRSetExpired(records, serveStale)) + entries.TryAdd(key, records); + } + + return entries; + } + + private static void WriteEntriesTo(ConcurrentDictionary> entries, BinaryWriter bW) + { + bW.Write(entries.Count); + + foreach (KeyValuePair> entry in entries) + { + bW.Write((ushort)entry.Key); + bW.Write(entry.Value.Count); + + foreach (DnsResourceRecord record in entry.Value) + { + record.WriteCacheRecordTo(bW, delegate () + { + if (record.Tag is not CacheRecordInfo rrInfo) + rrInfo = CacheRecordInfo.Default; //default info + + rrInfo.WriteTo(bW); + }); + } } } @@ -103,7 +199,7 @@ namespace DnsServerCore.Dns.Zones ConcurrentDictionary> entries; - NetworkAddress eDnsClientSubnet = records[0].GetRecordInfo().EDnsClientSubnet; + NetworkAddress eDnsClientSubnet = records[0].GetCacheRecordInfo().EDnsClientSubnet; if ((eDnsClientSubnet is null) || !IsTypeSupportedForEDnsClientSubnet(type)) { entries = _entries; @@ -163,7 +259,7 @@ namespace DnsServerCore.Dns.Zones DateTime utcNow = DateTime.UtcNow; foreach (DnsResourceRecord record in records) - record.GetRecordInfo().LastUsedOn = utcNow; + record.GetCacheRecordInfo().LastUsedOn = utcNow; //set records bool added = true; @@ -223,7 +319,7 @@ namespace DnsServerCore.Dns.Zones } } - if (ecsEntry.Value.Count == 0) + if (ecsEntry.Value.IsEmpty) _ecsEntries.TryRemove(ecsEntry.Key, out _); } } @@ -250,21 +346,21 @@ namespace DnsServerCore.Dns.Zones { foreach (KeyValuePair> entry in ecsEntry.Value) { - if ((entry.Value.Count == 0) || (entry.Value[0].GetRecordInfo().LastUsedOn < cutoff)) + if ((entry.Value.Count == 0) || (entry.Value[0].GetCacheRecordInfo().LastUsedOn < cutoff)) { if (ecsEntry.Value.TryRemove(entry.Key, out _)) //RR Set was last used before cutoff; remove entry removedEntries++; } } - if (ecsEntry.Value.Count == 0) + if (ecsEntry.Value.IsEmpty) _ecsEntries.TryRemove(ecsEntry.Key, out _); } } foreach (KeyValuePair> entry in _entries) { - if ((entry.Value.Count == 0) || (entry.Value[0].GetRecordInfo().LastUsedOn < cutoff)) + if ((entry.Value.Count == 0) || (entry.Value[0].GetCacheRecordInfo().LastUsedOn < cutoff)) { if (_entries.TryRemove(entry.Key, out _)) //RR Set was last used before cutoff; remove entry removedEntries++; @@ -420,6 +516,33 @@ namespace DnsServerCore.Dns.Zones return false; } + public void WriteTo(BinaryWriter bW) + { + bW.Write((byte)1); //version + + //cache zone info + bW.Write(_name); + + //write all cache records + WriteEntriesTo(_entries, bW); + + //write all ECS cache records + if (_ecsEntries is null) + { + bW.Write(0); + } + else + { + bW.Write(_ecsEntries.Count); + + foreach (KeyValuePair>> ecsEntry in _ecsEntries) + { + ecsEntry.Key.WriteTo(bW); + WriteEntriesTo(ecsEntry.Value, bW); + } + } + } + #endregion #region properties diff --git a/DnsServerCore/Dns/Zones/ForwarderZone.cs b/DnsServerCore/Dns/Zones/ForwarderZone.cs index d99cedf0..df84d621 100644 --- a/DnsServerCore/Dns/Zones/ForwarderZone.cs +++ b/DnsServerCore/Dns/Zones/ForwarderZone.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -44,7 +44,7 @@ namespace DnsServerCore.Dns.Zones DnsResourceRecord fwdRecord = new DnsResourceRecord(name, DnsResourceRecordType.FWD, DnsClass.IN, 0, new DnsForwarderRecordData(forwarderProtocol, forwarder, dnssecValidation, proxyType, proxyAddress, proxyPort, proxyUsername, proxyPassword)); if (!string.IsNullOrEmpty(fwdRecordComments)) - fwdRecord.SetComments(fwdRecordComments); + fwdRecord.GetAuthRecordInfo().Comments = fwdRecordComments; _entries[DnsResourceRecordType.FWD] = new DnsResourceRecord[] { fwdRecord }; } diff --git a/DnsServerCore/Dns/Zones/PrimarySubDomainZone.cs b/DnsServerCore/Dns/Zones/PrimarySubDomainZone.cs index f1318aaa..394f76a2 100644 --- a/DnsServerCore/Dns/Zones/PrimarySubDomainZone.cs +++ b/DnsServerCore/Dns/Zones/PrimarySubDomainZone.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -66,7 +66,7 @@ namespace DnsServerCore.Dns.Zones default: foreach (DnsResourceRecord record in records) { - if (record.IsDisabled()) + if (record.GetAuthRecordInfo().Disabled) throw new DnsServerException("Cannot set records: disabling records in a signed zones is not supported."); } @@ -117,7 +117,7 @@ namespace DnsServerCore.Dns.Zones throw new DnsServerException("The record type is not supported by DNSSEC signed primary zones."); default: - if (record.IsDisabled()) + if (record.GetAuthRecordInfo().Disabled) throw new DnsServerException("Cannot add record: disabling records in a signed zones is not supported."); break; @@ -226,7 +226,7 @@ namespace DnsServerCore.Dns.Zones if (oldRecord.Type != newRecord.Type) throw new InvalidOperationException("Old and new record types do not match."); - if ((_primaryZone.DnssecStatus != AuthZoneDnssecStatus.Unsigned) && newRecord.IsDisabled()) + if ((_primaryZone.DnssecStatus != AuthZoneDnssecStatus.Unsigned) && newRecord.GetAuthRecordInfo().Disabled) throw new DnsServerException("Cannot update record: disabling records in a signed zones is not supported."); if (newRecord.OriginalTtlValue > _primaryZone.GetZoneSoaExpire()) diff --git a/DnsServerCore/Dns/Zones/PrimaryZone.cs b/DnsServerCore/Dns/Zones/PrimaryZone.cs index 4cb3b0be..90856f0b 100644 --- a/DnsServerCore/Dns/Zones/PrimaryZone.cs +++ b/DnsServerCore/Dns/Zones/PrimaryZone.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -826,7 +826,7 @@ namespace DnsServerCore.Dns.Zones IReadOnlyList nsec3ParamRecords = GetRecords(DnsResourceRecordType.NSEC3PARAM); DnsNSEC3PARAMRecordData nsec3Param = nsec3ParamRecords[0].RDATA as DnsNSEC3PARAMRecordData; - EnableNSec3(nonNSec3Zones, nsec3Param.Iterations, nsec3Param.SaltValue); + EnableNSec3(nonNSec3Zones, nsec3Param.Iterations, nsec3Param.Salt); } } @@ -2195,7 +2195,7 @@ namespace DnsServerCore.Dns.Zones if (nextHashedOwnerName is null) nextHashedOwnerName = DnsNSEC3RecordData.GetHashedOwnerNameFrom(hashedOwnerName); //only 1 NSEC3 record in zone - IReadOnlyList newNSec3Records = zone.CreateNSec3RRSet(hashedOwnerName, nextHashedOwnerName, ttl, nsec3Param.Iterations, nsec3Param.SaltValue); + IReadOnlyList newNSec3Records = zone.CreateNSec3RRSet(hashedOwnerName, nextHashedOwnerName, ttl, nsec3Param.Iterations, nsec3Param.Salt); if (forceGetNewRRSet) return newNSec3Records; @@ -2310,9 +2310,9 @@ namespace DnsServerCore.Dns.Zones DnsNSEC3RecordData newPreviousNSec3; if (wasRemoved) - newPreviousNSec3 = new DnsNSEC3RecordData(DnssecNSEC3HashAlgorithm.SHA1, DnssecNSEC3Flags.None, previousNSec3.Iterations, previousNSec3.SaltValue, currentNSec3.NextHashedOwnerNameValue, previousNSec3.Types); + newPreviousNSec3 = new DnsNSEC3RecordData(DnssecNSEC3HashAlgorithm.SHA1, DnssecNSEC3Flags.None, previousNSec3.Iterations, previousNSec3.Salt, currentNSec3.NextHashedOwnerNameValue, previousNSec3.Types); else - newPreviousNSec3 = new DnsNSEC3RecordData(DnssecNSEC3HashAlgorithm.SHA1, DnssecNSEC3Flags.None, previousNSec3.Iterations, previousNSec3.SaltValue, DnsNSEC3RecordData.GetHashedOwnerNameFrom(currentNSec3Record.Name), previousNSec3.Types); + newPreviousNSec3 = new DnsNSEC3RecordData(DnssecNSEC3HashAlgorithm.SHA1, DnssecNSEC3Flags.None, previousNSec3.Iterations, previousNSec3.Salt, DnsNSEC3RecordData.GetHashedOwnerNameFrom(currentNSec3Record.Name), previousNSec3.Types); DnsResourceRecord[] newPreviousNSec3Records = new DnsResourceRecord[] { new DnsResourceRecord(previousNSec3Record.Name, DnsResourceRecordType.NSEC3, DnsClass.IN, ttl, newPreviousNSec3) }; @@ -2611,7 +2611,7 @@ namespace DnsServerCore.Dns.Zones else serial = 1; - newSoaRecord = new DnsResourceRecord(_name, DnsResourceRecordType.SOA, DnsClass.IN, addSoaRecord.TtlValue, new DnsSOARecordData(addSoa.PrimaryNameServer, addSoa.ResponsiblePerson, serial, addSoa.Refresh, addSoa.Retry, addSoa.Expire, addSoa.Minimum)) { Tag = addSoaRecord.Tag }; + newSoaRecord = new DnsResourceRecord(_name, DnsResourceRecordType.SOA, DnsClass.IN, addSoaRecord.TTL, new DnsSOARecordData(addSoa.PrimaryNameServer, addSoa.ResponsiblePerson, serial, addSoa.Refresh, addSoa.Retry, addSoa.Expire, addSoa.Minimum)) { Tag = addSoaRecord.Tag }; addedRecords = null; } else @@ -2623,7 +2623,7 @@ namespace DnsServerCore.Dns.Zones else serial = 1; - newSoaRecord = new DnsResourceRecord(_name, DnsResourceRecordType.SOA, DnsClass.IN, oldSoaRecord.TtlValue, new DnsSOARecordData(oldSoa.PrimaryNameServer, oldSoa.ResponsiblePerson, serial, oldSoa.Refresh, oldSoa.Retry, oldSoa.Expire, oldSoa.Minimum)) { Tag = oldSoaRecord.Tag }; + newSoaRecord = new DnsResourceRecord(_name, DnsResourceRecordType.SOA, DnsClass.IN, oldSoaRecord.TTL, new DnsSOARecordData(oldSoa.PrimaryNameServer, oldSoa.ResponsiblePerson, serial, oldSoa.Refresh, oldSoa.Retry, oldSoa.Expire, oldSoa.Minimum)) { Tag = oldSoaRecord.Tag }; } } @@ -2646,7 +2646,7 @@ namespace DnsServerCore.Dns.Zones oldSoaRecord.Tag = null; //start commit - oldSoaRecord.SetDeletedOn(DateTime.UtcNow); + oldSoaRecord.GetAuthRecordInfo().DeletedOn = DateTime.UtcNow; //write removed _zoneHistory.Add(oldSoaRecord); @@ -2655,13 +2655,17 @@ namespace DnsServerCore.Dns.Zones { foreach (DnsResourceRecord deletedRecord in deletedRecords) { - if (deletedRecord.IsDisabled()) + if (deletedRecord.GetAuthRecordInfo().Disabled) continue; _zoneHistory.Add(deletedRecord); if (deletedRecord.Type == DnsResourceRecordType.NS) - _zoneHistory.AddRange(deletedRecord.GetGlueRecords()); + { + IReadOnlyList glueRecords = deletedRecord.GetAuthRecordInfo().GlueRecords; + if (glueRecords is not null) + _zoneHistory.AddRange(glueRecords); + } } } @@ -2675,13 +2679,17 @@ namespace DnsServerCore.Dns.Zones { foreach (DnsResourceRecord addedRecord in addedRecords) { - if (addedRecord.IsDisabled()) + if (addedRecord.GetAuthRecordInfo().Disabled) continue; _zoneHistory.Add(addedRecord); if (addedRecord.Type == DnsResourceRecordType.NS) - _zoneHistory.AddRange(addedRecord.GetGlueRecords()); + { + IReadOnlyList glueRecords = addedRecord.GetAuthRecordInfo().GlueRecords; + if (glueRecords is not null) + _zoneHistory.AddRange(glueRecords); + } } } @@ -2711,7 +2719,7 @@ namespace DnsServerCore.Dns.Zones default: foreach (DnsResourceRecord record in records) { - if (record.IsDisabled()) + if (record.GetAuthRecordInfo().Disabled) throw new DnsServerException("Cannot set records: disabling records in a signed zones is not supported."); } @@ -2741,10 +2749,10 @@ namespace DnsServerCore.Dns.Zones if (newSoa.Refresh > newSoa.Expire) throw new DnsServerException("Failed to set records: SOA REFRESH cannot be greater than SOA EXPIRE."); - //remove any resource record info except comments - string comments = newSoaRecord.GetComments(); - newSoaRecord.Tag = null; - newSoaRecord.SetComments(comments); + //remove any record info except comments + string comments = newSoaRecord.GetAuthRecordInfo().Comments; + newSoaRecord.Tag = null; //remove old record info + newSoaRecord.GetAuthRecordInfo().Comments = comments; uint oldSoaMinimum = GetZoneSoaMinimum(); @@ -2806,7 +2814,7 @@ namespace DnsServerCore.Dns.Zones throw new DnsServerException("The record type is not supported by DNSSEC signed primary zones."); default: - if (record.IsDisabled()) + if (record.GetAuthRecordInfo().Disabled) throw new DnsServerException("Cannot add record: disabling records in a signed zones is not supported."); break; @@ -2930,7 +2938,7 @@ namespace DnsServerCore.Dns.Zones if (oldRecord.Type != newRecord.Type) throw new InvalidOperationException("Old and new record types do not match."); - if ((_dnssecStatus != AuthZoneDnssecStatus.Unsigned) && newRecord.IsDisabled()) + if ((_dnssecStatus != AuthZoneDnssecStatus.Unsigned) && newRecord.GetAuthRecordInfo().Disabled) throw new DnsServerException("Cannot update record: disabling records in a signed zones is not supported."); if (newRecord.OriginalTtlValue > GetZoneSoaExpire()) diff --git a/DnsServerCore/Dns/Zones/SecondaryZone.cs b/DnsServerCore/Dns/Zones/SecondaryZone.cs index e8b73644..f015a6cc 100644 --- a/DnsServerCore/Dns/Zones/SecondaryZone.cs +++ b/DnsServerCore/Dns/Zones/SecondaryZone.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -22,6 +22,7 @@ using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using TechnitiumLibrary; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; @@ -88,6 +89,7 @@ namespace DnsServerCore.Dns.Zones { case DnsTransportProtocol.Tcp: case DnsTransportProtocol.Tls: + case DnsTransportProtocol.Quic: break; default: @@ -98,14 +100,25 @@ namespace DnsServerCore.Dns.Zones DnsQuestionRecord soaQuestion = new DnsQuestionRecord(name, DnsResourceRecordType.SOA, DnsClass.IN); DnsDatagram soaResponse; + NameServerAddress[] primaryNameServers = null; - if (primaryNameServerAddresses == null) + if (string.IsNullOrEmpty(primaryNameServerAddresses)) { soaResponse = await secondaryZone._dnsServer.DirectQueryAsync(soaQuestion); } else { - DnsClient dnsClient = new DnsClient(primaryNameServerAddresses); + primaryNameServers = primaryNameServerAddresses.Split(delegate (string address) + { + NameServerAddress nameServer = NameServerAddress.Parse(address); + + if (nameServer.Protocol != zoneTransferProtocol) + nameServer = nameServer.ChangeProtocol(zoneTransferProtocol); + + return nameServer; + }, ','); + + DnsClient dnsClient = new DnsClient(primaryNameServers); foreach (NameServerAddress nameServerAddress in dnsClient.Servers) { @@ -134,13 +147,11 @@ namespace DnsServerCore.Dns.Zones DnsSOARecordData soa = new DnsSOARecordData(receivedSoa.PrimaryNameServer, receivedSoa.ResponsiblePerson, 0u, receivedSoa.Refresh, receivedSoa.Retry, receivedSoa.Expire, receivedSoa.Minimum); DnsResourceRecord[] soaRR = new DnsResourceRecord[] { new DnsResourceRecord(secondaryZone._name, DnsResourceRecordType.SOA, DnsClass.IN, soa.Refresh, soa) }; - if (!string.IsNullOrEmpty(primaryNameServerAddresses)) - soaRR[0].SetPrimaryNameServers(primaryNameServerAddresses); + AuthRecordInfo authRecordInfo = soaRR[0].GetAuthRecordInfo(); - DnsResourceRecordInfo recordInfo = soaRR[0].GetRecordInfo(); - - recordInfo.ZoneTransferProtocol = zoneTransferProtocol; - recordInfo.TsigKeyName = tsigKeyName; + authRecordInfo.PrimaryNameServers = primaryNameServers; + authRecordInfo.ZoneTransferProtocol = zoneTransferProtocol; + authRecordInfo.TsigKeyName = tsigKeyName; secondaryZone._entries[DnsResourceRecordType.SOA] = soaRR; @@ -214,7 +225,7 @@ namespace DnsServerCore.Dns.Zones return; } - DnsResourceRecordInfo recordInfo = currentSoaRecord.GetRecordInfo(); + AuthRecordInfo recordInfo = currentSoaRecord.GetAuthRecordInfo(); TsigKey key = null; if (!string.IsNullOrEmpty(recordInfo.TsigKeyName) && ((_dnsServer.TsigKeys is null) || !_dnsServer.TsigKeys.TryGetValue(recordInfo.TsigKeyName, out key))) @@ -289,7 +300,18 @@ namespace DnsServerCore.Dns.Zones if (!_resync) { - DnsClient client = new DnsClient(primaryNameServers); + //check for update; use UDP transport + List udpNameServers = new List(primaryNameServers.Count); + + foreach (NameServerAddress primaryNameServer in primaryNameServers) + { + if (primaryNameServer.Protocol == DnsTransportProtocol.Udp) + udpNameServers.Add(primaryNameServer); + else + udpNameServers.Add(primaryNameServer.ChangeProtocol(DnsTransportProtocol.Udp)); + } + + DnsClient client = new DnsClient(udpNameServers); client.Proxy = _dnsServer.Proxy; client.PreferIPv6 = _dnsServer.PreferIPv6; @@ -309,7 +331,7 @@ namespace DnsServerCore.Dns.Zones { LogManager log = _dnsServer.LogManager; if (log != null) - log.Write("DNS Server received RCODE=" + soaResponse.RCODE.ToString() + " for '" + (_name == "" ? "" : _name) + "' secondary zone refresh from: " + soaResponse.Metadata.NameServerAddress.ToString()); + log.Write("DNS Server received RCODE=" + soaResponse.RCODE.ToString() + " for '" + (_name == "" ? "" : _name) + "' secondary zone refresh from: " + soaResponse.Metadata.NameServer.ToString()); return false; } @@ -318,7 +340,7 @@ namespace DnsServerCore.Dns.Zones { LogManager log = _dnsServer.LogManager; if (log != null) - log.Write("DNS Server received an empty response for SOA query for '" + (_name == "" ? "" : _name) + "' secondary zone refresh from: " + soaResponse.Metadata.NameServerAddress.ToString()); + log.Write("DNS Server received an empty response for SOA query for '" + (_name == "" ? "" : _name) + "' secondary zone refresh from: " + soaResponse.Metadata.NameServer.ToString()); return false; } @@ -331,46 +353,44 @@ namespace DnsServerCore.Dns.Zones { LogManager log = _dnsServer.LogManager; if (log != null) - log.Write("DNS Server successfully checked for '" + (_name == "" ? "" : _name) + "' secondary zone update from: " + soaResponse.Metadata.NameServerAddress.ToString()); + log.Write("DNS Server successfully checked for '" + (_name == "" ? "" : _name) + "' secondary zone update from: " + soaResponse.Metadata.NameServer.ToString()); return true; } } - //update available; do zone transfer with TLS or TCP transport + //update available; do zone transfer with TLS, QUIC, or TCP transport + List updatedNameServers = new List(primaryNameServers.Count); - if (zoneTransferProtocol == DnsTransportProtocol.Tls) + switch (zoneTransferProtocol) { - //change name server protocol to TLS - List tlsNameServers = new List(primaryNameServers.Count); + case DnsTransportProtocol.Tls: + case DnsTransportProtocol.Quic: + //change name server protocol to TLS/QUIC + foreach (NameServerAddress primaryNameServer in primaryNameServers) + { + if (primaryNameServer.Protocol == zoneTransferProtocol) + updatedNameServers.Add(primaryNameServer); + else + updatedNameServers.Add(primaryNameServer.ChangeProtocol(zoneTransferProtocol)); + } - foreach (NameServerAddress primaryNameServer in primaryNameServers) - { - if (primaryNameServer.Protocol == DnsTransportProtocol.Tls) - tlsNameServers.Add(primaryNameServer); - else - tlsNameServers.Add(primaryNameServer.ChangeProtocol(DnsTransportProtocol.Tls)); - } + break; - primaryNameServers = tlsNameServers; - } - else - { - //change name server protocol to TCP - List tcpNameServers = new List(primaryNameServers.Count); + default: + //change name server protocol to TCP + foreach (NameServerAddress primaryNameServer in primaryNameServers) + { + if (primaryNameServer.Protocol == DnsTransportProtocol.Tcp) + updatedNameServers.Add(primaryNameServer); + else + updatedNameServers.Add(primaryNameServer.ChangeProtocol(DnsTransportProtocol.Tcp)); + } - foreach (NameServerAddress primaryNameServer in primaryNameServers) - { - if (primaryNameServer.Protocol == DnsTransportProtocol.Tcp) - tcpNameServers.Add(primaryNameServer); - else - tcpNameServers.Add(primaryNameServer.ChangeProtocol(DnsTransportProtocol.Tcp)); - } - - primaryNameServers = tcpNameServers; + break; } - DnsClient xfrClient = new DnsClient(primaryNameServers); + DnsClient xfrClient = new DnsClient(updatedNameServers); xfrClient.Proxy = _dnsServer.Proxy; xfrClient.PreferIPv6 = _dnsServer.PreferIPv6; @@ -414,7 +434,7 @@ namespace DnsServerCore.Dns.Zones { LogManager log = _dnsServer.LogManager; if (log != null) - log.Write("DNS Server received a zone transfer response (RCODE=" + xfrResponse.RCODE.ToString() + ") for '" + (_name == "" ? "" : _name) + "' secondary zone from: " + xfrResponse.Metadata.NameServerAddress.ToString()); + log.Write("DNS Server received a zone transfer response (RCODE=" + xfrResponse.RCODE.ToString() + ") for '" + (_name == "" ? "" : _name) + "' secondary zone from: " + xfrResponse.Metadata.NameServer.ToString()); return false; } @@ -423,7 +443,7 @@ namespace DnsServerCore.Dns.Zones { LogManager log = _dnsServer.LogManager; if (log != null) - log.Write("DNS Server received an empty response for zone transfer query for '" + (_name == "" ? "" : _name) + "' secondary zone from: " + xfrResponse.Metadata.NameServerAddress.ToString()); + log.Write("DNS Server received an empty response for zone transfer query for '" + (_name == "" ? "" : _name) + "' secondary zone from: " + xfrResponse.Metadata.NameServer.ToString()); return false; } @@ -432,7 +452,7 @@ namespace DnsServerCore.Dns.Zones { LogManager log = _dnsServer.LogManager; if (log != null) - log.Write("DNS Server received invalid response for zone transfer query for '" + (_name == "" ? "" : _name) + "' secondary zone from: " + xfrResponse.Metadata.NameServerAddress.ToString()); + log.Write("DNS Server received invalid response for zone transfer query for '" + (_name == "" ? "" : _name) + "' secondary zone from: " + xfrResponse.Metadata.NameServer.ToString()); return false; } @@ -460,13 +480,13 @@ namespace DnsServerCore.Dns.Zones LogManager log = _dnsServer.LogManager; if (log != null) - log.Write("DNS Server successfully refreshed '" + (_name == "" ? "" : _name) + "' secondary zone from: " + xfrResponse.Metadata.NameServerAddress.ToString()); + log.Write("DNS Server successfully refreshed '" + (_name == "" ? "" : _name) + "' secondary zone from: " + xfrResponse.Metadata.NameServer.ToString()); } else { LogManager log = _dnsServer.LogManager; if (log != null) - log.Write("DNS Server successfully checked for '" + (_name == "" ? "" : _name) + "' secondary zone update from: " + xfrResponse.Metadata.NameServerAddress.ToString()); + log.Write("DNS Server successfully checked for '" + (_name == "" ? "" : _name) + "' secondary zone update from: " + xfrResponse.Metadata.NameServer.ToString()); } return true; @@ -499,7 +519,7 @@ namespace DnsServerCore.Dns.Zones { lock (_zoneHistory) { - historyRecords[0].SetDeletedOn(DateTime.UtcNow); + historyRecords[0].GetAuthRecordInfo().DeletedOn = DateTime.UtcNow; //write history _zoneHistory.AddRange(historyRecords); diff --git a/DnsServerCore/Dns/Zones/StubZone.cs b/DnsServerCore/Dns/Zones/StubZone.cs index 25168511..5e21f0e1 100644 --- a/DnsServerCore/Dns/Zones/StubZone.cs +++ b/DnsServerCore/Dns/Zones/StubZone.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -22,6 +22,7 @@ using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using TechnitiumLibrary; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; @@ -81,14 +82,25 @@ namespace DnsServerCore.Dns.Zones DnsQuestionRecord soaQuestion = new DnsQuestionRecord(name, DnsResourceRecordType.SOA, DnsClass.IN); DnsDatagram soaResponse; + NameServerAddress[] primaryNameServers = null; - if (primaryNameServerAddresses == null) + if (string.IsNullOrEmpty(primaryNameServerAddresses)) { soaResponse = await stubZone._dnsServer.DirectQueryAsync(soaQuestion); } else { - DnsClient dnsClient = new DnsClient(primaryNameServerAddresses); + primaryNameServers = primaryNameServerAddresses.Split(delegate (string address) + { + NameServerAddress nameServer = NameServerAddress.Parse(address); + + if (nameServer.Protocol != DnsTransportProtocol.Udp) + nameServer = nameServer.ChangeProtocol(DnsTransportProtocol.Udp); + + return nameServer; + }, ','); + + DnsClient dnsClient = new DnsClient(primaryNameServers); foreach (NameServerAddress nameServerAddress in dnsClient.Servers) { @@ -112,8 +124,8 @@ namespace DnsServerCore.Dns.Zones DnsSOARecordData soa = new DnsSOARecordData(receivedSoa.PrimaryNameServer, receivedSoa.ResponsiblePerson, 0u, receivedSoa.Refresh, receivedSoa.Retry, receivedSoa.Expire, receivedSoa.Minimum); DnsResourceRecord[] soaRR = new DnsResourceRecord[] { new DnsResourceRecord(stubZone._name, DnsResourceRecordType.SOA, DnsClass.IN, soa.Refresh, soa) }; - if (!string.IsNullOrEmpty(primaryNameServerAddresses)) - soaRR[0].SetPrimaryNameServers(primaryNameServerAddresses); + if (primaryNameServers is not null) + soaRR[0].GetAuthRecordInfo().PrimaryNameServers = primaryNameServers; stubZone._entries[DnsResourceRecordType.SOA] = soaRR; @@ -255,7 +267,7 @@ namespace DnsServerCore.Dns.Zones { LogManager log = _dnsServer.LogManager; if (log != null) - log.Write("DNS Server received RCODE=" + soaResponse.RCODE.ToString() + " for '" + (_name == "" ? "" : _name) + "' stub zone refresh from: " + soaResponse.Metadata.NameServerAddress.ToString()); + log.Write("DNS Server received RCODE=" + soaResponse.RCODE.ToString() + " for '" + (_name == "" ? "" : _name) + "' stub zone refresh from: " + soaResponse.Metadata.NameServer.ToString()); return false; } @@ -264,7 +276,7 @@ namespace DnsServerCore.Dns.Zones { LogManager log = _dnsServer.LogManager; if (log != null) - log.Write("DNS Server received an empty response for SOA query for '" + (_name == "" ? "" : _name) + "' stub zone refresh from: " + soaResponse.Metadata.NameServerAddress.ToString()); + log.Write("DNS Server received an empty response for SOA query for '" + (_name == "" ? "" : _name) + "' stub zone refresh from: " + soaResponse.Metadata.NameServer.ToString()); return false; } @@ -280,7 +292,7 @@ namespace DnsServerCore.Dns.Zones { LogManager log = _dnsServer.LogManager; if (log != null) - log.Write("DNS Server successfully checked for '" + (_name == "" ? "" : _name) + "' stub zone update from: " + soaResponse.Metadata.NameServerAddress.ToString()); + log.Write("DNS Server successfully checked for '" + (_name == "" ? "" : _name) + "' stub zone update from: " + soaResponse.Metadata.NameServer.ToString()); return true; } @@ -291,8 +303,7 @@ namespace DnsServerCore.Dns.Zones foreach (NameServerAddress nameServer in nameServers) tcpNameServers.Add(nameServer.ChangeProtocol(DnsTransportProtocol.Tcp)); - nameServers = tcpNameServers; - client = new DnsClient(nameServers); + client = new DnsClient(tcpNameServers); client.Proxy = _dnsServer.Proxy; client.PreferIPv6 = _dnsServer.PreferIPv6; @@ -307,7 +318,7 @@ namespace DnsServerCore.Dns.Zones { LogManager log = _dnsServer.LogManager; if (log != null) - log.Write("DNS Server received RCODE=" + nsResponse.RCODE.ToString() + " for '" + (_name == "" ? "" : _name) + "' stub zone refresh from: " + nsResponse.Metadata.NameServerAddress.ToString()); + log.Write("DNS Server received RCODE=" + nsResponse.RCODE.ToString() + " for '" + (_name == "" ? "" : _name) + "' stub zone refresh from: " + nsResponse.Metadata.NameServer.ToString()); return false; } @@ -316,7 +327,7 @@ namespace DnsServerCore.Dns.Zones { LogManager log = _dnsServer.LogManager; if (log != null) - log.Write("DNS Server received an empty response for NS query for '" + (_name == "" ? "" : _name) + "' stub zone from: " + nsResponse.Metadata.NameServerAddress.ToString()); + log.Write("DNS Server received an empty response for NS query for '" + (_name == "" ? "" : _name) + "' stub zone from: " + nsResponse.Metadata.NameServer.ToString()); return false; } @@ -342,7 +353,7 @@ namespace DnsServerCore.Dns.Zones { LogManager log = _dnsServer.LogManager; if (log != null) - log.Write("DNS Server successfully refreshed '" + (_name == "" ? "" : _name) + "' stub zone from: " + nsResponse.Metadata.NameServerAddress.ToString()); + log.Write("DNS Server successfully refreshed '" + (_name == "" ? "" : _name) + "' stub zone from: " + nsResponse.Metadata.NameServer.ToString()); } return true; diff --git a/DnsServerCore/Dns/Zones/SubDomainZone.cs b/DnsServerCore/Dns/Zones/SubDomainZone.cs index 737802b2..19011441 100644 --- a/DnsServerCore/Dns/Zones/SubDomainZone.cs +++ b/DnsServerCore/Dns/Zones/SubDomainZone.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2021 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -49,7 +49,7 @@ namespace DnsServerCore.Dns.Zones { foreach (DnsResourceRecord record in entry.Value) { - if (!record.IsDisabled()) + if (!record.GetAuthRecordInfo().Disabled) { _disabled = false; return; diff --git a/DnsServerCore/Dns/Zones/Zone.cs b/DnsServerCore/Dns/Zones/Zone.cs index d8c1a5c1..40bf1064 100644 --- a/DnsServerCore/Dns/Zones/Zone.cs +++ b/DnsServerCore/Dns/Zones/Zone.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -50,6 +50,12 @@ namespace DnsServerCore.Dns.Zones _entries = new ConcurrentDictionary>(1, capacity); } + protected Zone(string name, ConcurrentDictionary> entries) + { + _name = name.ToLower(); + _entries = entries; + } + #endregion #region static diff --git a/DnsServerCore/DnsServerCore.csproj b/DnsServerCore/DnsServerCore.csproj index 7561cc4b..6e8ebc08 100644 --- a/DnsServerCore/DnsServerCore.csproj +++ b/DnsServerCore/DnsServerCore.csproj @@ -12,9 +12,32 @@ DnsServer - 10.0.1 + 11.0 + + + + + + + + + + + ..\..\TechnitiumLibrary\bin\TechnitiumLibrary.dll + + + ..\..\TechnitiumLibrary\bin\TechnitiumLibrary.ByteTree.dll + + + ..\..\TechnitiumLibrary\bin\TechnitiumLibrary.IO.dll + + + ..\..\TechnitiumLibrary\bin\TechnitiumLibrary.Net.dll + + + @@ -28,19 +51,11 @@ - - - - - - - - @@ -61,12 +76,10 @@ - - @@ -113,30 +126,6 @@ PreserveNewest - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - PreserveNewest @@ -206,9 +195,6 @@ PreserveNewest - - PreserveNewest - PreserveNewest @@ -218,9 +204,6 @@ PreserveNewest - - PreserveNewest - PreserveNewest @@ -247,27 +230,4 @@ - - - - - - - ..\..\TechnitiumLibrary\bin\TechnitiumLibrary.dll - - - ..\..\TechnitiumLibrary\bin\TechnitiumLibrary.ByteTree.dll - - - ..\..\TechnitiumLibrary\bin\TechnitiumLibrary.IO.dll - - - ..\..\TechnitiumLibrary\bin\TechnitiumLibrary.Net.dll - - - - - - - diff --git a/DnsServerCore/DnsWebService.cs b/DnsServerCore/DnsWebService.cs index 46dd560f..98079fef 100644 --- a/DnsServerCore/DnsWebService.cs +++ b/DnsServerCore/DnsWebService.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -20,23 +20,29 @@ along with this program. If not, see . using DnsServerCore.Auth; using DnsServerCore.Dhcp; using DnsServerCore.Dns; -using DnsServerCore.Dns.ResourceRecords; using DnsServerCore.Dns.ZoneManagers; using DnsServerCore.Dns.Zones; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.AspNetCore.Server.Kestrel.Https; +using Microsoft.AspNetCore.StaticFiles; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.IO; -using System.IO.Compression; using System.Net; -using System.Net.Http; -using System.Net.Security; +using System.Net.Quic; using System.Net.Sockets; using System.Reflection; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using TechnitiumLibrary; @@ -44,84 +50,60 @@ using TechnitiumLibrary.IO; using TechnitiumLibrary.Net; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; -using TechnitiumLibrary.Net.Http; using TechnitiumLibrary.Net.Proxy; namespace DnsServerCore { - public sealed class DnsWebService : IDisposable + public sealed class DnsWebService : IAsyncDisposable, IDisposable { - #region enum - - enum ServiceState - { - Stopped = 0, - Starting = 1, - Running = 2, - Stopping = 3 - } - - #endregion - #region variables - readonly static RandomNumberGenerator _rng = RandomNumberGenerator.Create(); + internal readonly Version _currentVersion; + readonly string _appFolder; + internal readonly string _configFolder; - readonly LogManager _log; - readonly AuthManager _authManager; + internal readonly LogManager _log; + internal readonly AuthManager _authManager; - readonly WebServiceAuthApi _authApi; + readonly WebServiceApi _api; readonly WebServiceDashboardApi _dashboardApi; - readonly WebServiceZonesApi _zonesApi; + internal readonly WebServiceZonesApi _zonesApi; readonly WebServiceOtherZonesApi _otherZonesApi; - readonly WebServiceAppsApi _appsApi; + internal readonly WebServiceAppsApi _appsApi; + readonly WebServiceSettingsApi _settingsApi; readonly WebServiceDhcpApi _dhcpApi; + readonly WebServiceAuthApi _authApi; readonly WebServiceLogsApi _logsApi; - readonly Version _currentVersion; - readonly string _appFolder; - readonly string _configFolder; - readonly Uri _updateCheckUri; + WebApplication _webService; + X509Certificate2 _webServiceTlsCertificate; DnsServer _dnsServer; DhcpServer _dhcpServer; - IReadOnlyList _webServiceLocalAddresses = new IPAddress[] { IPAddress.Any, IPAddress.IPv6Any }; - int _webServiceHttpPort = 5380; - int _webServiceTlsPort = 53443; - bool _webServiceEnableTls; - bool _webServiceHttpToTlsRedirect; - bool _webServiceUseSelfSignedTlsCertificate; - string _webServiceTlsCertificatePath; - string _webServiceTlsCertificatePassword; + //web service + internal IReadOnlyList _webServiceLocalAddresses = new IPAddress[] { IPAddress.Any, IPAddress.IPv6Any }; + internal int _webServiceHttpPort = 5380; + internal int _webServiceTlsPort = 53443; + internal bool _webServiceEnableTls; + internal bool _webServiceHttpToTlsRedirect; + internal bool _webServiceUseSelfSignedTlsCertificate; + internal string _webServiceTlsCertificatePath; + internal string _webServiceTlsCertificatePassword; DateTime _webServiceTlsCertificateLastModifiedOn; - HttpListener _webService; - IReadOnlyList _webServiceTlsListeners; - X509Certificate2 _webServiceTlsCertificate; - readonly IndependentTaskScheduler _webServiceTaskScheduler = new IndependentTaskScheduler(ThreadPriority.AboveNormal); - string _webServiceHostname; - IPEndPoint _webServiceHttpEP; - - string _dnsTlsCertificatePath; - string _dnsTlsCertificatePassword; + //optional protocols + internal string _dnsTlsCertificatePath; + internal string _dnsTlsCertificatePassword; DateTime _dnsTlsCertificateLastModifiedOn; + //cache + internal bool _saveCache; + Timer _tlsCertificateUpdateTimer; const int TLS_CERTIFICATE_UPDATE_TIMER_INITIAL_INTERVAL = 60000; const int TLS_CERTIFICATE_UPDATE_TIMER_INTERVAL = 60000; - volatile ServiceState _state = ServiceState.Stopped; - - Timer _blockListUpdateTimer; - DateTime _blockListLastUpdatedOn; - int _blockListUpdateIntervalHours = 24; - const int BLOCK_LIST_UPDATE_TIMER_INITIAL_INTERVAL = 5000; - const int BLOCK_LIST_UPDATE_TIMER_PERIODIC_INTERVAL = 900000; - - Timer _temporaryDisableBlockingTimer; - DateTime _temporaryDisableBlockingTill; - List _configDisabledZones; #endregion @@ -140,26 +122,21 @@ namespace DnsServerCore else _configFolder = configFolder; - if (!Directory.Exists(_configFolder)) - Directory.CreateDirectory(_configFolder); - - _updateCheckUri = updateCheckUri; + Directory.CreateDirectory(_configFolder); + Directory.CreateDirectory(Path.Combine(_configFolder, "blocklists")); _log = new LogManager(_configFolder); _authManager = new AuthManager(_configFolder, _log); - _authApi = new WebServiceAuthApi(this); + _api = new WebServiceApi(this, updateCheckUri); _dashboardApi = new WebServiceDashboardApi(this); _zonesApi = new WebServiceZonesApi(this); _otherZonesApi = new WebServiceOtherZonesApi(this); _appsApi = new WebServiceAppsApi(this, appStoreUri); + _settingsApi = new WebServiceSettingsApi(this); _dhcpApi = new WebServiceDhcpApi(this); + _authApi = new WebServiceAuthApi(this); _logsApi = new WebServiceLogsApi(this); - - string blockListsFolder = Path.Combine(_configFolder, "blocklists"); - - if (!Directory.Exists(blockListsFolder)) - Directory.CreateDirectory(blockListsFolder); } #endregion @@ -168,24 +145,18 @@ namespace DnsServerCore bool _disposed; - public void Dispose() + public async ValueTask DisposeAsync() { if (_disposed) return; - Stop(); + await StopAsync(); if (_appsApi is not null) _appsApi.Dispose(); - if (_webService is not null) - _webService.Close(); - - if (_dnsServer is not null) - _dnsServer.Dispose(); - - if (_dhcpServer is not null) - _dhcpServer.Dispose(); + if (_settingsApi is not null) + _settingsApi.Dispose(); if (_authManager is not null) _authManager.Dispose(); @@ -196,1341 +167,18 @@ namespace DnsServerCore _disposed = true; } - #endregion - - #region private - - #region web service - - private async Task AcceptWebRequestAsync() + public void Dispose() { - try - { - while (true) - { - HttpListenerContext context = await _webService.GetContextAsync(); - - if ((_webServiceTlsListeners != null) && (_webServiceTlsListeners.Count > 0) && _webServiceHttpToTlsRedirect) - { - IPEndPoint remoteEP = context.Request.RemoteEndPoint; - - if ((remoteEP != null) && !IPAddress.IsLoopback(remoteEP.Address)) - { - string domain = _webServiceTlsCertificate.GetNameInfo(X509NameType.DnsName, false); - string redirectUri = "https://" + domain + ":" + _webServiceTlsPort + context.Request.Url.PathAndQuery; - - context.Response.Redirect(redirectUri); - context.Response.Close(); - - continue; - } - } - - _ = ProcessRequestAsync(context.Request, context.Response); - } - } - catch (HttpListenerException ex) - { - if (ex.ErrorCode == 995) - return; //web service stopping - - _log.Write(ex); - } - catch (ObjectDisposedException) - { - //web service stopped - } - catch (Exception ex) - { - if ((_state == ServiceState.Stopping) || (_state == ServiceState.Stopped)) - return; //web service stopping - - _log.Write(ex); - } - } - - private async Task AcceptTlsWebRequestAsync(Socket tlsListener) - { - try - { - while (true) - { - Socket socket = await tlsListener.AcceptAsync(); - - _ = TlsToHttpTunnelAsync(socket); - } - } - catch (SocketException ex) - { - if (ex.SocketErrorCode == SocketError.OperationAborted) - return; //web service stopping - - _log.Write(ex); - } - catch (ObjectDisposedException) - { - //web service stopped - } - catch (Exception ex) - { - if ((_state == ServiceState.Stopping) || (_state == ServiceState.Stopped)) - return; //web service stopping - - _log.Write(ex); - } - } - - private async Task TlsToHttpTunnelAsync(Socket socket) - { - Socket tunnel = null; - - try - { - if (_webServiceLocalAddresses.Count < 1) - return; - - string remoteIP = (socket.RemoteEndPoint as IPEndPoint).Address.ToString(); - - SslStream sslStream = new SslStream(new NetworkStream(socket, true)); - - await sslStream.AuthenticateAsServerAsync(_webServiceTlsCertificate); - - tunnel = new Socket(_webServiceHttpEP.AddressFamily, SocketType.Stream, ProtocolType.Tcp); - tunnel.Connect(_webServiceHttpEP); - - NetworkStream tunnelStream = new NetworkStream(tunnel, true); - - //copy tunnel to ssl - _ = tunnelStream.CopyToAsync(sslStream).ContinueWith(delegate (Task prevTask) { sslStream.Dispose(); tunnelStream.Dispose(); }); - - //copy ssl to tunnel - try - { - while (true) - { - HttpRequest httpRequest = await HttpRequest.ReadRequestAsync(sslStream); - if (httpRequest == null) - return; //connection closed gracefully by client - - //inject X-Real-IP & host header - httpRequest.Headers.Add("X-Real-IP", remoteIP); - httpRequest.Headers[HttpRequestHeader.Host] = "localhost:" + _webServiceHttpPort.ToString(); - - //relay request - await tunnelStream.WriteAsync(Encoding.ASCII.GetBytes(httpRequest.HttpMethod + " " + httpRequest.RequestPathAndQuery + " " + httpRequest.Protocol + "\r\n")); - await tunnelStream.WriteAsync(httpRequest.Headers.ToByteArray()); - - if (httpRequest.InputStream != null) - await httpRequest.InputStream.CopyToAsync(tunnelStream); - - await tunnelStream.FlushAsync(); - } - } - finally - { - sslStream.Dispose(); - tunnelStream.Dispose(); - } - } - catch (IOException) - { - //ignore - } - catch (Exception ex) - { - _log.Write(ex); - } - finally - { - socket.Dispose(); - - if (tunnel != null) - tunnel.Dispose(); - } - } - - private async Task ProcessRequestAsync(HttpListenerRequest request, HttpListenerResponse response) - { - response.AddHeader("Server", ""); - response.AddHeader("X-Robots-Tag", "noindex, nofollow"); - - try - { - Uri url = request.Url; - string path = url.AbsolutePath; - - if (!path.StartsWith("/") || path.Contains("/../") || path.Contains("/.../")) - { - await SendErrorAsync(response, 404); - return; - } - - if (path.StartsWith("/api/")) - { - using (MemoryStream mS = new MemoryStream()) - { - try - { - JsonTextWriter jsonWriter = new JsonTextWriter(new StreamWriter(mS)); - jsonWriter.WriteStartObject(); - - switch (path) - { - case "/api/user/login": - case "/api/login": - await _authApi.LoginAsync(request, jsonWriter, UserSessionType.Standard); - break; - - case "/api/user/createToken": - await _authApi.LoginAsync(request, jsonWriter, UserSessionType.ApiToken); - break; - - case "/api/user/logout": - case "/api/logout": - _authApi.Logout(request); - break; - - case "/api/user/session/get": - _authApi.GetCurrentSessionDetails(request, jsonWriter); - break; - - default: - if (!TryGetSession(request, out UserSession session)) - throw new InvalidTokenWebServiceException("Invalid token or session expired."); - - jsonWriter.WritePropertyName("response"); - jsonWriter.WriteStartObject(); - - try - { - switch (path) - { - case "/api/user/session/delete": - _authApi.DeleteSession(request, false); - break; - - case "/api/user/changePassword": - case "/api/changePassword": - _authApi.ChangePassword(request); - break; - - case "/api/user/profile/get": - _authApi.GetProfile(request, jsonWriter); - break; - - case "/api/user/profile/set": - _authApi.SetProfile(request, jsonWriter); - break; - - case "/api/user/checkForUpdate": - case "/api/checkForUpdate": - await CheckForUpdateAsync(request, jsonWriter); - break; - - case "/api/dashboard/stats/get": - case "/api/getStats": - if (!_authManager.IsPermitted(PermissionSection.Dashboard, session.User, PermissionFlag.View)) - throw new DnsWebServiceException("Access was denied."); - - await _dashboardApi.GetStats(request, jsonWriter); - break; - - case "/api/dashboard/stats/getTop": - case "/api/getTopStats": - if (!_authManager.IsPermitted(PermissionSection.Dashboard, session.User, PermissionFlag.View)) - throw new DnsWebServiceException("Access was denied."); - - await _dashboardApi.GetTopStats(request, jsonWriter); - break; - - case "/api/dashboard/stats/deleteAll": - case "/api/deleteAllStats": - if (!_authManager.IsPermitted(PermissionSection.Dashboard, session.User, PermissionFlag.Delete)) - throw new DnsWebServiceException("Access was denied."); - - _logsApi.DeleteAllStats(request); - break; - - case "/api/zones/list": - case "/api/zone/list": - case "/api/listZones": - if (!_authManager.IsPermitted(PermissionSection.Zones, session.User, PermissionFlag.View)) - throw new DnsWebServiceException("Access was denied."); - - _zonesApi.ListZones(request, jsonWriter); - break; - - case "/api/zones/create": - case "/api/zone/create": - case "/api/createZone": - if (!_authManager.IsPermitted(PermissionSection.Zones, session.User, PermissionFlag.Modify)) - throw new DnsWebServiceException("Access was denied."); - - await _zonesApi.CreateZoneAsync(request, jsonWriter); - break; - - case "/api/zones/enable": - case "/api/zone/enable": - case "/api/enableZone": - if (!_authManager.IsPermitted(PermissionSection.Zones, session.User, PermissionFlag.Modify)) - throw new DnsWebServiceException("Access was denied."); - - _zonesApi.EnableZone(request); - break; - - case "/api/zones/disable": - case "/api/zone/disable": - case "/api/disableZone": - if (!_authManager.IsPermitted(PermissionSection.Zones, session.User, PermissionFlag.Modify)) - throw new DnsWebServiceException("Access was denied."); - - _zonesApi.DisableZone(request); - break; - - case "/api/zones/delete": - case "/api/zone/delete": - case "/api/deleteZone": - if (!_authManager.IsPermitted(PermissionSection.Zones, session.User, PermissionFlag.Delete)) - throw new DnsWebServiceException("Access was denied."); - - _zonesApi.DeleteZone(request); - break; - - case "/api/zones/resync": - case "/api/zone/resync": - if (!_authManager.IsPermitted(PermissionSection.Zones, session.User, PermissionFlag.Modify)) - throw new DnsWebServiceException("Access was denied."); - - _zonesApi.ResyncZone(request); - break; - - case "/api/zones/options/get": - case "/api/zone/options/get": - if (!_authManager.IsPermitted(PermissionSection.Zones, session.User, PermissionFlag.Modify)) - throw new DnsWebServiceException("Access was denied."); - - _zonesApi.GetZoneOptions(request, jsonWriter); - break; - - case "/api/zones/options/set": - case "/api/zone/options/set": - if (!_authManager.IsPermitted(PermissionSection.Zones, session.User, PermissionFlag.Modify)) - throw new DnsWebServiceException("Access was denied."); - - _zonesApi.SetZoneOptions(request); - break; - - case "/api/zones/permissions/get": - if (!_authManager.IsPermitted(PermissionSection.Zones, session.User, PermissionFlag.Modify)) - throw new DnsWebServiceException("Access was denied."); - - _authApi.GetPermissionDetails(request, jsonWriter, PermissionSection.Zones); - break; - - case "/api/zones/permissions/set": - if (!_authManager.IsPermitted(PermissionSection.Zones, session.User, PermissionFlag.Modify)) - throw new DnsWebServiceException("Access was denied."); - - _authApi.SetPermissionsDetails(request, jsonWriter, PermissionSection.Zones); - break; - - case "/api/zones/dnssec/sign": - case "/api/zone/dnssec/sign": - if (!_authManager.IsPermitted(PermissionSection.Zones, session.User, PermissionFlag.Modify)) - throw new DnsWebServiceException("Access was denied."); - - _zonesApi.SignPrimaryZone(request); - break; - - case "/api/zones/dnssec/unsign": - case "/api/zone/dnssec/unsign": - if (!_authManager.IsPermitted(PermissionSection.Zones, session.User, PermissionFlag.Modify)) - throw new DnsWebServiceException("Access was denied."); - - _zonesApi.UnsignPrimaryZone(request); - break; - - case "/api/zones/dnssec/properties/get": - case "/api/zone/dnssec/getProperties": - if (!_authManager.IsPermitted(PermissionSection.Zones, session.User, PermissionFlag.Modify)) - throw new DnsWebServiceException("Access was denied."); - - _zonesApi.GetPrimaryZoneDnssecProperties(request, jsonWriter); - break; - - case "/api/zones/dnssec/properties/convertToNSEC": - case "/api/zone/dnssec/convertToNSEC": - if (!_authManager.IsPermitted(PermissionSection.Zones, session.User, PermissionFlag.Modify)) - throw new DnsWebServiceException("Access was denied."); - - _zonesApi.ConvertPrimaryZoneToNSEC(request); - break; - - case "/api/zones/dnssec/properties/convertToNSEC3": - case "/api/zone/dnssec/convertToNSEC3": - if (!_authManager.IsPermitted(PermissionSection.Zones, session.User, PermissionFlag.Modify)) - throw new DnsWebServiceException("Access was denied."); - - _zonesApi.ConvertPrimaryZoneToNSEC3(request); - break; - - case "/api/zones/dnssec/properties/updateNSEC3Params": - case "/api/zone/dnssec/updateNSEC3Params": - if (!_authManager.IsPermitted(PermissionSection.Zones, session.User, PermissionFlag.Modify)) - throw new DnsWebServiceException("Access was denied."); - - _zonesApi.UpdatePrimaryZoneNSEC3Parameters(request); - break; - - case "/api/zones/dnssec/properties/updateDnsKeyTtl": - case "/api/zone/dnssec/updateDnsKeyTtl": - if (!_authManager.IsPermitted(PermissionSection.Zones, session.User, PermissionFlag.Modify)) - throw new DnsWebServiceException("Access was denied."); - - _zonesApi.UpdatePrimaryZoneDnssecDnsKeyTtl(request); - break; - - case "/api/zones/dnssec/properties/generatePrivateKey": - case "/api/zone/dnssec/generatePrivateKey": - if (!_authManager.IsPermitted(PermissionSection.Zones, session.User, PermissionFlag.Modify)) - throw new DnsWebServiceException("Access was denied."); - - _zonesApi.GenerateAndAddPrimaryZoneDnssecPrivateKey(request); - break; - - case "/api/zones/dnssec/properties/updatePrivateKey": - case "/api/zone/dnssec/updatePrivateKey": - if (!_authManager.IsPermitted(PermissionSection.Zones, session.User, PermissionFlag.Modify)) - throw new DnsWebServiceException("Access was denied."); - - _zonesApi.UpdatePrimaryZoneDnssecPrivateKey(request); - break; - - case "/api/zones/dnssec/properties/deletePrivateKey": - case "/api/zone/dnssec/deletePrivateKey": - if (!_authManager.IsPermitted(PermissionSection.Zones, session.User, PermissionFlag.Modify)) - throw new DnsWebServiceException("Access was denied."); - - _zonesApi.DeletePrimaryZoneDnssecPrivateKey(request); - break; - - case "/api/zones/dnssec/properties/publishAllPrivateKeys": - case "/api/zone/dnssec/publishAllPrivateKeys": - if (!_authManager.IsPermitted(PermissionSection.Zones, session.User, PermissionFlag.Modify)) - throw new DnsWebServiceException("Access was denied."); - - _zonesApi.PublishAllGeneratedPrimaryZoneDnssecPrivateKeys(request); - break; - - case "/api/zones/dnssec/properties/rolloverDnsKey": - case "/api/zone/dnssec/rolloverDnsKey": - if (!_authManager.IsPermitted(PermissionSection.Zones, session.User, PermissionFlag.Modify)) - throw new DnsWebServiceException("Access was denied."); - - _zonesApi.RolloverPrimaryZoneDnsKey(request); - break; - - case "/api/zones/dnssec/properties/retireDnsKey": - case "/api/zone/dnssec/retireDnsKey": - if (!_authManager.IsPermitted(PermissionSection.Zones, session.User, PermissionFlag.Modify)) - throw new DnsWebServiceException("Access was denied."); - - _zonesApi.RetirePrimaryZoneDnsKey(request); - break; - - case "/api/zones/records/add": - case "/api/zone/addRecord": - case "/api/addRecord": - _zonesApi.AddRecord(request, jsonWriter); - break; - - case "/api/zones/records/get": - case "/api/zone/getRecords": - case "/api/getRecords": - _zonesApi.GetRecords(request, jsonWriter); - break; - - case "/api/zones/records/update": - case "/api/zone/updateRecord": - case "/api/updateRecord": - _zonesApi.UpdateRecord(request, jsonWriter); - break; - - case "/api/zones/records/delete": - case "/api/zone/deleteRecord": - case "/api/deleteRecord": - _zonesApi.DeleteRecord(request); - break; - - case "/api/cache/list": - case "/api/listCachedZones": - if (!_authManager.IsPermitted(PermissionSection.Cache, session.User, PermissionFlag.View)) - throw new DnsWebServiceException("Access was denied."); - - _otherZonesApi.ListCachedZones(request, jsonWriter); - break; - - case "/api/cache/delete": - case "/api/deleteCachedZone": - if (!_authManager.IsPermitted(PermissionSection.Cache, session.User, PermissionFlag.Delete)) - throw new DnsWebServiceException("Access was denied."); - - _otherZonesApi.DeleteCachedZone(request); - break; - - case "/api/cache/flush": - case "/api/flushDnsCache": - if (!_authManager.IsPermitted(PermissionSection.Cache, session.User, PermissionFlag.Delete)) - throw new DnsWebServiceException("Access was denied."); - - _otherZonesApi.FlushCache(request); - break; - - case "/api/allowed/list": - case "/api/listAllowedZones": - if (!_authManager.IsPermitted(PermissionSection.Allowed, session.User, PermissionFlag.View)) - throw new DnsWebServiceException("Access was denied."); - - _otherZonesApi.ListAllowedZones(request, jsonWriter); - break; - - case "/api/allowed/add": - case "/api/allowZone": - if (!_authManager.IsPermitted(PermissionSection.Allowed, session.User, PermissionFlag.Modify)) - throw new DnsWebServiceException("Access was denied."); - - _otherZonesApi.AllowZone(request); - break; - - case "/api/allowed/delete": - case "/api/deleteAllowedZone": - if (!_authManager.IsPermitted(PermissionSection.Allowed, session.User, PermissionFlag.Delete)) - throw new DnsWebServiceException("Access was denied."); - - _otherZonesApi.DeleteAllowedZone(request); - break; - - case "/api/allowed/flush": - case "/api/flushAllowedZone": - if (!_authManager.IsPermitted(PermissionSection.Allowed, session.User, PermissionFlag.Delete)) - throw new DnsWebServiceException("Access was denied."); - - _otherZonesApi.FlushAllowedZone(request); - break; - - case "/api/allowed/import": - case "/api/importAllowedZones": - if (!_authManager.IsPermitted(PermissionSection.Allowed, session.User, PermissionFlag.Modify)) - throw new DnsWebServiceException("Access was denied."); - - await _otherZonesApi.ImportAllowedZonesAsync(request); - break; - - case "/api/allowed/export": - case "/api/exportAllowedZones": - if (!_authManager.IsPermitted(PermissionSection.Allowed, session.User, PermissionFlag.View)) - throw new DnsWebServiceException("Access was denied."); - - _otherZonesApi.ExportAllowedZones(response); - return; - - case "/api/blocked/list": - case "/api/listBlockedZones": - if (!_authManager.IsPermitted(PermissionSection.Blocked, session.User, PermissionFlag.View)) - throw new DnsWebServiceException("Access was denied."); - - _otherZonesApi.ListBlockedZones(request, jsonWriter); - break; - - case "/api/blocked/add": - case "/api/blockZone": - if (!_authManager.IsPermitted(PermissionSection.Blocked, session.User, PermissionFlag.Modify)) - throw new DnsWebServiceException("Access was denied."); - - _otherZonesApi.BlockZone(request); - break; - - case "/api/blocked/delete": - case "/api/deleteBlockedZone": - if (!_authManager.IsPermitted(PermissionSection.Blocked, session.User, PermissionFlag.Delete)) - throw new DnsWebServiceException("Access was denied."); - - _otherZonesApi.DeleteBlockedZone(request); - break; - - case "/api/blocked/flush": - case "/api/flushBlockedZone": - if (!_authManager.IsPermitted(PermissionSection.Blocked, session.User, PermissionFlag.Delete)) - throw new DnsWebServiceException("Access was denied."); - - _otherZonesApi.FlushBlockedZone(request); - break; - - case "/api/blocked/import": - case "/api/importBlockedZones": - if (!_authManager.IsPermitted(PermissionSection.Blocked, session.User, PermissionFlag.Modify)) - throw new DnsWebServiceException("Access was denied."); - - await _otherZonesApi.ImportBlockedZonesAsync(request); - break; - - case "/api/blocked/export": - case "/api/exportBlockedZones": - if (!_authManager.IsPermitted(PermissionSection.Blocked, session.User, PermissionFlag.View)) - throw new DnsWebServiceException("Access was denied."); - - _otherZonesApi.ExportBlockedZones(response); - return; - - case "/api/apps/list": - if ( - _authManager.IsPermitted(PermissionSection.Apps, session.User, PermissionFlag.View) || - _authManager.IsPermitted(PermissionSection.Zones, session.User, PermissionFlag.View) || - _authManager.IsPermitted(PermissionSection.Logs, session.User, PermissionFlag.View) - ) - { - await _appsApi.ListInstalledAppsAsync(jsonWriter); - } - else - { - throw new DnsWebServiceException("Access was denied."); - } - - break; - - case "/api/apps/listStoreApps": - if (!_authManager.IsPermitted(PermissionSection.Apps, session.User, PermissionFlag.View)) - throw new DnsWebServiceException("Access was denied."); - - await _appsApi.ListStoreApps(jsonWriter); - break; - - case "/api/apps/downloadAndInstall": - if (!_authManager.IsPermitted(PermissionSection.Apps, session.User, PermissionFlag.Delete)) - throw new DnsWebServiceException("Access was denied."); - - await _appsApi.DownloadAndInstallAppAsync(request, jsonWriter); - break; - - case "/api/apps/downloadAndUpdate": - if (!_authManager.IsPermitted(PermissionSection.Apps, session.User, PermissionFlag.Delete)) - throw new DnsWebServiceException("Access was denied."); - - await _appsApi.DownloadAndUpdateAppAsync(request, jsonWriter); - break; - - case "/api/apps/install": - if (!_authManager.IsPermitted(PermissionSection.Apps, session.User, PermissionFlag.Delete)) - throw new DnsWebServiceException("Access was denied."); - - await _appsApi.InstallAppAsync(request, jsonWriter); - break; - - case "/api/apps/update": - if (!_authManager.IsPermitted(PermissionSection.Apps, session.User, PermissionFlag.Delete)) - throw new DnsWebServiceException("Access was denied."); - - await _appsApi.UpdateAppAsync(request, jsonWriter); - break; - - case "/api/apps/uninstall": - if (!_authManager.IsPermitted(PermissionSection.Apps, session.User, PermissionFlag.Delete)) - throw new DnsWebServiceException("Access was denied."); - - _appsApi.UninstallApp(request); - break; - - case "/api/apps/config/get": - case "/api/apps/getConfig": - if (!_authManager.IsPermitted(PermissionSection.Apps, session.User, PermissionFlag.View)) - throw new DnsWebServiceException("Access was denied."); - - await _appsApi.GetAppConfigAsync(request, jsonWriter); - break; - - case "/api/apps/config/set": - case "/api/apps/setConfig": - if (!_authManager.IsPermitted(PermissionSection.Apps, session.User, PermissionFlag.Modify)) - throw new DnsWebServiceException("Access was denied."); - - await _appsApi.SetAppConfigAsync(request); - break; - - case "/api/dnsClient/resolve": - case "/api/resolveQuery": - if (!_authManager.IsPermitted(PermissionSection.DnsClient, session.User, PermissionFlag.View)) - throw new DnsWebServiceException("Access was denied."); - - await ResolveQueryAsync(request, jsonWriter); - break; - - case "/api/settings/get": - case "/api/getDnsSettings": - if (!_authManager.IsPermitted(PermissionSection.Settings, session.User, PermissionFlag.View)) - throw new DnsWebServiceException("Access was denied."); - - GetDnsSettings(jsonWriter); - break; - - case "/api/settings/set": - case "/api/setDnsSettings": - if (!_authManager.IsPermitted(PermissionSection.Settings, session.User, PermissionFlag.Modify)) - throw new DnsWebServiceException("Access was denied."); - - SetDnsSettings(request, jsonWriter); - break; - - case "/api/settings/getTsigKeyNames": - if ( - _authManager.IsPermitted(PermissionSection.Settings, session.User, PermissionFlag.View) || - _authManager.IsPermitted(PermissionSection.Zones, session.User, PermissionFlag.Modify) - ) - { - GetTsigKeyNames(jsonWriter); - } - else - { - throw new DnsWebServiceException("Access was denied."); - } - - break; - - case "/api/settings/forceUpdateBlockLists": - case "/api/forceUpdateBlockLists": - if (!_authManager.IsPermitted(PermissionSection.Settings, session.User, PermissionFlag.Modify)) - throw new DnsWebServiceException("Access was denied."); - - ForceUpdateBlockLists(request); - break; - - case "/api/settings/temporaryDisableBlocking": - case "/api/temporaryDisableBlocking": - if (!_authManager.IsPermitted(PermissionSection.Settings, session.User, PermissionFlag.Modify)) - throw new DnsWebServiceException("Access was denied."); - - TemporaryDisableBlocking(request, jsonWriter); - break; - - case "/api/settings/backup": - case "/api/backupSettings": - if (!_authManager.IsPermitted(PermissionSection.Settings, session.User, PermissionFlag.Delete)) - throw new DnsWebServiceException("Access was denied."); - - await BackupSettingsAsync(request, response); - return; - - case "/api/settings/restore": - case "/api/restoreSettings": - if (!_authManager.IsPermitted(PermissionSection.Settings, session.User, PermissionFlag.Delete)) - throw new DnsWebServiceException("Access was denied."); - - await RestoreSettingsAsync(request, jsonWriter); - break; - - case "/api/dhcp/leases/list": - case "/api/listDhcpLeases": - if (!_authManager.IsPermitted(PermissionSection.DhcpServer, session.User, PermissionFlag.View)) - throw new DnsWebServiceException("Access was denied."); - - _dhcpApi.ListDhcpLeases(jsonWriter); - break; - - case "/api/dhcp/leases/remove": - case "/api/removeDhcpLease": - if (!_authManager.IsPermitted(PermissionSection.DhcpServer, session.User, PermissionFlag.Delete)) - throw new DnsWebServiceException("Access was denied."); - - _dhcpApi.RemoveDhcpLease(request); - break; - - case "/api/dhcp/leases/convertToReserved": - case "/api/convertToReservedLease": - if (!_authManager.IsPermitted(PermissionSection.DhcpServer, session.User, PermissionFlag.Modify)) - throw new DnsWebServiceException("Access was denied."); - - _dhcpApi.ConvertToReservedLease(request); - break; - - case "/api/dhcp/leases/convertToDynamic": - case "/api/convertToDynamicLease": - if (!_authManager.IsPermitted(PermissionSection.DhcpServer, session.User, PermissionFlag.Modify)) - throw new DnsWebServiceException("Access was denied."); - - _dhcpApi.ConvertToDynamicLease(request); - break; - - case "/api/dhcp/scopes/list": - case "/api/listDhcpScopes": - if (!_authManager.IsPermitted(PermissionSection.DhcpServer, session.User, PermissionFlag.View)) - throw new DnsWebServiceException("Access was denied."); - - _dhcpApi.ListDhcpScopes(jsonWriter); - break; - - case "/api/dhcp/scopes/get": - case "/api/getDhcpScope": - if (!_authManager.IsPermitted(PermissionSection.DhcpServer, session.User, PermissionFlag.View)) - throw new DnsWebServiceException("Access was denied."); - - _dhcpApi.GetDhcpScope(request, jsonWriter); - break; - - case "/api/dhcp/scopes/set": - case "/api/setDhcpScope": - if (!_authManager.IsPermitted(PermissionSection.DhcpServer, session.User, PermissionFlag.Modify)) - throw new DnsWebServiceException("Access was denied."); - - await _dhcpApi.SetDhcpScopeAsync(request); - break; - - case "/api/dhcp/scopes/addReservedLease": - if (!_authManager.IsPermitted(PermissionSection.DhcpServer, session.User, PermissionFlag.Modify)) - throw new DnsWebServiceException("Access was denied."); - - _dhcpApi.AddReservedLease(request); - break; - - case "/api/dhcp/scopes/removeReservedLease": - if (!_authManager.IsPermitted(PermissionSection.DhcpServer, session.User, PermissionFlag.Modify)) - throw new DnsWebServiceException("Access was denied."); - - _dhcpApi.RemoveReservedLease(request); - break; - - case "/api/dhcp/scopes/enable": - case "/api/enableDhcpScope": - if (!_authManager.IsPermitted(PermissionSection.DhcpServer, session.User, PermissionFlag.Modify)) - throw new DnsWebServiceException("Access was denied."); - - await _dhcpApi.EnableDhcpScopeAsync(request); - break; - - case "/api/dhcp/scopes/disable": - case "/api/disableDhcpScope": - if (!_authManager.IsPermitted(PermissionSection.DhcpServer, session.User, PermissionFlag.Modify)) - throw new DnsWebServiceException("Access was denied."); - - _dhcpApi.DisableDhcpScope(request); - break; - - case "/api/dhcp/scopes/delete": - case "/api/deleteDhcpScope": - if (!_authManager.IsPermitted(PermissionSection.DhcpServer, session.User, PermissionFlag.Delete)) - throw new DnsWebServiceException("Access was denied."); - - _dhcpApi.DeleteDhcpScope(request); - break; - - case "/api/admin/sessions/list": - if (!_authManager.IsPermitted(PermissionSection.Administration, session.User, PermissionFlag.View)) - throw new DnsWebServiceException("Access was denied."); - - _authApi.ListSessions(request, jsonWriter); - break; - - case "/api/admin/sessions/createToken": - if (!_authManager.IsPermitted(PermissionSection.Administration, session.User, PermissionFlag.Modify)) - throw new DnsWebServiceException("Access was denied."); - - _authApi.CreateApiToken(request, jsonWriter); - break; - - case "/api/admin/sessions/delete": - if (!_authManager.IsPermitted(PermissionSection.Administration, session.User, PermissionFlag.Delete)) - throw new DnsWebServiceException("Access was denied."); - - _authApi.DeleteSession(request, true); - break; - - case "/api/admin/users/list": - if (!_authManager.IsPermitted(PermissionSection.Administration, session.User, PermissionFlag.View)) - throw new DnsWebServiceException("Access was denied."); - - _authApi.ListUsers(jsonWriter); - break; - - case "/api/admin/users/create": - if (!_authManager.IsPermitted(PermissionSection.Administration, session.User, PermissionFlag.Modify)) - throw new DnsWebServiceException("Access was denied."); - - _authApi.CreateUser(request, jsonWriter); - break; - - case "/api/admin/users/get": - if (!_authManager.IsPermitted(PermissionSection.Administration, session.User, PermissionFlag.View)) - throw new DnsWebServiceException("Access was denied."); - - _authApi.GetUserDetails(request, jsonWriter); - break; - - case "/api/admin/users/set": - if (!_authManager.IsPermitted(PermissionSection.Administration, session.User, PermissionFlag.Modify)) - throw new DnsWebServiceException("Access was denied."); - - _authApi.SetUserDetails(request, jsonWriter); - break; - - case "/api/admin/users/delete": - if (!_authManager.IsPermitted(PermissionSection.Administration, session.User, PermissionFlag.Delete)) - throw new DnsWebServiceException("Access was denied."); - - _authApi.DeleteUser(request); - break; - - case "/api/admin/groups/list": - if (!_authManager.IsPermitted(PermissionSection.Administration, session.User, PermissionFlag.View)) - throw new DnsWebServiceException("Access was denied."); - - _authApi.ListGroups(jsonWriter); - break; - - case "/api/admin/groups/create": - if (!_authManager.IsPermitted(PermissionSection.Administration, session.User, PermissionFlag.Modify)) - throw new DnsWebServiceException("Access was denied."); - - _authApi.CreateGroup(request, jsonWriter); - break; - - case "/api/admin/groups/get": - if (!_authManager.IsPermitted(PermissionSection.Administration, session.User, PermissionFlag.View)) - throw new DnsWebServiceException("Access was denied."); - - _authApi.GetGroupDetails(request, jsonWriter); - break; - - case "/api/admin/groups/set": - if (!_authManager.IsPermitted(PermissionSection.Administration, session.User, PermissionFlag.Modify)) - throw new DnsWebServiceException("Access was denied."); - - _authApi.SetGroupDetails(request, jsonWriter); - break; - - case "/api/admin/groups/delete": - if (!_authManager.IsPermitted(PermissionSection.Administration, session.User, PermissionFlag.Delete)) - throw new DnsWebServiceException("Access was denied."); - - _authApi.DeleteGroup(request); - break; - - case "/api/admin/permissions/list": - if (!_authManager.IsPermitted(PermissionSection.Administration, session.User, PermissionFlag.View)) - throw new DnsWebServiceException("Access was denied."); - - _authApi.ListPermissions(jsonWriter); - break; - - case "/api/admin/permissions/get": - if (!_authManager.IsPermitted(PermissionSection.Administration, session.User, PermissionFlag.View)) - throw new DnsWebServiceException("Access was denied."); - - _authApi.GetPermissionDetails(request, jsonWriter, PermissionSection.Unknown); - break; - - case "/api/admin/permissions/set": - if (!_authManager.IsPermitted(PermissionSection.Administration, session.User, PermissionFlag.Delete)) - throw new DnsWebServiceException("Access was denied."); - - _authApi.SetPermissionsDetails(request, jsonWriter, PermissionSection.Unknown); - break; - - case "/api/logs/list": - case "/api/listLogs": - if (!_authManager.IsPermitted(PermissionSection.Logs, session.User, PermissionFlag.View)) - throw new DnsWebServiceException("Access was denied."); - - _logsApi.ListLogs(jsonWriter); - break; - - case "/api/logs/download": - if (!_authManager.IsPermitted(PermissionSection.Logs, session.User, PermissionFlag.View)) - throw new DnsWebServiceException("Access was denied."); - - await _logsApi.DownloadLogAsync(request, response); - return; - - case "/api/logs/delete": - case "/api/deleteLog": - if (!_authManager.IsPermitted(PermissionSection.Logs, session.User, PermissionFlag.Delete)) - throw new DnsWebServiceException("Access was denied."); - - _logsApi.DeleteLog(request); - break; - - case "/api/logs/deleteAll": - case "/api/deleteAllLogs": - if (!_authManager.IsPermitted(PermissionSection.Logs, session.User, PermissionFlag.Delete)) - throw new DnsWebServiceException("Access was denied."); - - _logsApi.DeleteAllLogs(request); - break; - - case "/api/logs/query": - case "/api/queryLogs": - if (!_authManager.IsPermitted(PermissionSection.Logs, session.User, PermissionFlag.View)) - throw new DnsWebServiceException("Access was denied."); - - await _logsApi.QueryLogsAsync(request, jsonWriter); - break; - - default: - await SendErrorAsync(response, 404); - return; - } - } - finally - { - jsonWriter.WriteEndObject(); - } - break; - } - - jsonWriter.WritePropertyName("status"); - jsonWriter.WriteValue("ok"); - - jsonWriter.WriteEndObject(); - jsonWriter.Flush(); - } - catch (InvalidTokenWebServiceException ex) - { - mS.SetLength(0); - JsonTextWriter jsonWriter = new JsonTextWriter(new StreamWriter(mS)); - jsonWriter.WriteStartObject(); - - jsonWriter.WritePropertyName("status"); - jsonWriter.WriteValue("invalid-token"); - - jsonWriter.WritePropertyName("errorMessage"); - jsonWriter.WriteValue(ex.Message); - - jsonWriter.WriteEndObject(); - jsonWriter.Flush(); - } - catch (Exception ex) - { - UserSession session = null; - - string strToken = request.QueryString["token"]; - if (!string.IsNullOrEmpty(strToken)) - session = _authManager.GetSession(strToken); - - if (session is null) - _log.Write(GetRequestRemoteEndPoint(request), ex); - else - _log.Write(GetRequestRemoteEndPoint(request), "[" + session.User.Username + "] " + ex.ToString()); - - mS.SetLength(0); - JsonTextWriter jsonWriter = new JsonTextWriter(new StreamWriter(mS)); - jsonWriter.WriteStartObject(); - - jsonWriter.WritePropertyName("status"); - jsonWriter.WriteValue("error"); - - jsonWriter.WritePropertyName("errorMessage"); - jsonWriter.WriteValue(ex.Message); - - jsonWriter.WritePropertyName("stackTrace"); - jsonWriter.WriteValue(ex.StackTrace); - - if (ex.InnerException != null) - { - jsonWriter.WritePropertyName("innerErrorMessage"); - jsonWriter.WriteValue(ex.InnerException.Message); - } - - jsonWriter.WriteEndObject(); - jsonWriter.Flush(); - } - - response.ContentType = "application/json; charset=utf-8"; - response.ContentEncoding = Encoding.UTF8; - response.ContentLength64 = mS.Length; - - mS.Position = 0; - using (Stream stream = response.OutputStream) - { - await mS.CopyToAsync(stream); - } - } - } - else if (path.StartsWith("/log/")) - { - if (!TryGetSession(request, out UserSession session)) - { - await SendErrorAsync(response, 403, "Invalid token or session expired."); - return; - } - - if (!_authManager.IsPermitted(PermissionSection.Logs, session.User, PermissionFlag.View)) - throw new DnsWebServiceException("Access was denied."); - - string[] pathParts = path.Split('/'); - string logFileName = pathParts[2]; - - int limit = 0; - string strLimit = request.QueryString["limit"]; - if (!string.IsNullOrEmpty(strLimit)) - limit = int.Parse(strLimit); - - await _log.DownloadLogAsync(request, response, logFileName, limit * 1024 * 1024); - } - else - { - if (path == "/") - { - path = "/index.html"; - } - else if ((path == "/blocklist.txt") && !IPAddress.IsLoopback(GetRequestRemoteEndPoint(request).Address)) - { - await SendErrorAsync(response, 403); - return; - } - - string wwwroot = Path.Combine(_appFolder, "www"); - path = Path.GetFullPath(wwwroot + path.Replace('/', Path.DirectorySeparatorChar)); - - if (!path.StartsWith(wwwroot) || !File.Exists(path)) - { - await SendErrorAsync(response, 404); - return; - } - - await SendFileAsync(request, response, path); - } - } - catch (Exception ex) - { - if ((_state == ServiceState.Stopping) || (_state == ServiceState.Stopped)) - return; //web service stopping - - UserSession session = null; - - string strToken = request.QueryString["token"]; - if (!string.IsNullOrEmpty(strToken)) - session = _authManager.GetSession(strToken); - - if (session is null) - _log.Write(GetRequestRemoteEndPoint(request), ex); - else - _log.Write(GetRequestRemoteEndPoint(request), "[" + session.User.Username + "] " + ex.ToString()); - - await SendError(response, ex); - } - } - - internal static IPEndPoint GetRequestRemoteEndPoint(HttpListenerRequest request) - { - try - { - if (request.RemoteEndPoint == null) - return new IPEndPoint(IPAddress.Any, 0); - - if (NetUtilities.IsPrivateIP(request.RemoteEndPoint.Address)) - { - string xRealIp = request.Headers["X-Real-IP"]; - if (IPAddress.TryParse(xRealIp, out IPAddress address)) - { - //get the real IP address of the requesting client from X-Real-IP header set in nginx proxy_pass block - return new IPEndPoint(address, 0); - } - } - - return request.RemoteEndPoint; - } - catch - { - return new IPEndPoint(IPAddress.Any, 0); - } - } - - public static Stream GetOutputStream(HttpListenerRequest request, HttpListenerResponse response) - { - string strAcceptEncoding = request.Headers["Accept-Encoding"]; - if (string.IsNullOrEmpty(strAcceptEncoding)) - { - return response.OutputStream; - } - else - { - if (strAcceptEncoding.Contains("gzip")) - { - response.AddHeader("Content-Encoding", "gzip"); - return new GZipStream(response.OutputStream, CompressionMode.Compress); - } - else if (strAcceptEncoding.Contains("deflate")) - { - response.AddHeader("Content-Encoding", "deflate"); - return new DeflateStream(response.OutputStream, CompressionMode.Compress); - } - else - { - return response.OutputStream; - } - } - } - - private static Task SendError(HttpListenerResponse response, Exception ex) - { - return SendErrorAsync(response, 500, ex.ToString()); - } - - private static async Task SendErrorAsync(HttpListenerResponse response, int statusCode, string message = null) - { - try - { - string statusString = statusCode + " " + DnsServer.GetHttpStatusString((HttpStatusCode)statusCode); - byte[] buffer = Encoding.UTF8.GetBytes("" + statusString + "

" + statusString + "

" + (message == null ? "" : "

" + message + "

") + ""); - - response.StatusCode = statusCode; - response.ContentType = "text/html"; - response.ContentLength64 = buffer.Length; - - using (Stream stream = response.OutputStream) - { - await stream.WriteAsync(buffer); - } - } - catch - { } - } - - private static async Task SendFileAsync(HttpListenerRequest request, HttpListenerResponse response, string filePath) - { - using (FileStream fS = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) - { - response.ContentType = WebUtilities.GetContentType(filePath).MediaType; - response.AddHeader("Cache-Control", "private, max-age=300"); - - using (Stream stream = GetOutputStream(request, response)) - { - try - { - await fS.CopyToAsync(stream); - } - catch (HttpListenerException) - { - //ignore this error - } - } - } - } - - internal UserSession GetSession(HttpListenerRequest request) - { - string strToken = request.QueryString["token"]; - if (string.IsNullOrEmpty(strToken)) - throw new DnsWebServiceException("Parameter 'token' missing."); - - return _authManager.GetSession(strToken); - } - - internal bool TryGetSession(HttpListenerRequest request, out UserSession session) - { - session = GetSession(request); - if ((session is null) || session.User.Disabled) - return false; - - if (session.HasExpired()) - { - _authManager.DeleteSession(session.Token); - _authManager.SaveConfigFile(); - return false; - } - - IPEndPoint remoteEP = GetRequestRemoteEndPoint(request); - - session.UpdateLastSeen(remoteEP.Address, request.UserAgent); - return true; + DisposeAsync().Sync(); } #endregion - #region update api + #region server version - private async Task CheckForUpdateAsync(HttpListenerRequest request, JsonTextWriter jsonWriter) + internal string GetServerVersion() { - if (_updateCheckUri is null) - { - jsonWriter.WritePropertyName("updateAvailable"); - jsonWriter.WriteValue(false); - return; - } - - try - { - SocketsHttpHandler handler = new SocketsHttpHandler(); - handler.Proxy = _dnsServer.Proxy; - handler.UseProxy = _dnsServer.Proxy is not null; - - using (HttpClient http = new HttpClient(handler)) - { - string response = await http.GetStringAsync(_updateCheckUri); - dynamic jsonResponse = JsonConvert.DeserializeObject(response); - - string updateVersion = jsonResponse.updateVersion.Value; - string updateTitle = jsonResponse.updateTitle?.Value; - string updateMessage = jsonResponse.updateMessage?.Value; - string downloadLink = jsonResponse.downloadLink?.Value; - string instructionsLink = jsonResponse.instructionsLink?.Value; - string changeLogLink = jsonResponse.changeLogLink?.Value; - - bool updateAvailable = new Version(updateVersion) > _currentVersion; - - jsonWriter.WritePropertyName("updateAvailable"); - jsonWriter.WriteValue(updateAvailable); - - jsonWriter.WritePropertyName("updateVersion"); - jsonWriter.WriteValue(updateVersion); - - jsonWriter.WritePropertyName("currentVersion"); - jsonWriter.WriteValue(GetCleanVersion(_currentVersion)); - - if (updateAvailable) - { - jsonWriter.WritePropertyName("updateTitle"); - jsonWriter.WriteValue(updateTitle); - - jsonWriter.WritePropertyName("updateMessage"); - jsonWriter.WriteValue(updateMessage); - - jsonWriter.WritePropertyName("downloadLink"); - jsonWriter.WriteValue(downloadLink); - - jsonWriter.WritePropertyName("instructionsLink"); - jsonWriter.WriteValue(instructionsLink); - - jsonWriter.WritePropertyName("changeLogLink"); - jsonWriter.WriteValue(changeLogLink); - } - - string strLog = "Check for update was done {updateAvailable: " + updateAvailable + "; updateVersion: " + updateVersion + ";"; - - if (!string.IsNullOrEmpty(updateTitle)) - strLog += " updateTitle: " + updateTitle + ";"; - - if (!string.IsNullOrEmpty(updateMessage)) - strLog += " updateMessage: " + updateMessage + ";"; - - if (!string.IsNullOrEmpty(downloadLink)) - strLog += " downloadLink: " + downloadLink + ";"; - - if (!string.IsNullOrEmpty(instructionsLink)) - strLog += " instructionsLink: " + instructionsLink + ";"; - - if (!string.IsNullOrEmpty(changeLogLink)) - strLog += " changeLogLink: " + changeLogLink + ";"; - - strLog += "}"; - - _log.Write(GetRequestRemoteEndPoint(request), strLog); - } - } - catch (Exception ex) - { - _log.Write(GetRequestRemoteEndPoint(request), "Check for update was done {updateAvailable: False;}\r\n" + ex.ToString()); - - jsonWriter.WritePropertyName("updateAvailable"); - jsonWriter.WriteValue(false); - } + return GetCleanVersion(_currentVersion); } internal static string GetCleanVersion(Version version) @@ -1546,2163 +194,430 @@ namespace DnsServerCore return strVersion; } - internal string GetServerVersion() - { - return GetCleanVersion(_currentVersion); - } - #endregion - #region settings api + #region web service - private void GetDnsSettings(JsonTextWriter jsonWriter) + internal async Task TryStartWebServiceAsync() { - jsonWriter.WritePropertyName("version"); - jsonWriter.WriteValue(GetServerVersion()); - - jsonWriter.WritePropertyName("dnsServerDomain"); - jsonWriter.WriteValue(_dnsServer.ServerDomain); - - jsonWriter.WritePropertyName("dnsServerLocalEndPoints"); - jsonWriter.WriteStartArray(); - - foreach (IPEndPoint localEP in _dnsServer.LocalEndPoints) - jsonWriter.WriteValue(localEP.ToString()); - - jsonWriter.WriteEndArray(); - - jsonWriter.WritePropertyName("webServiceLocalAddresses"); - jsonWriter.WriteStartArray(); - - foreach (IPAddress localAddress in _webServiceLocalAddresses) + try { - if (localAddress.AddressFamily == AddressFamily.InterNetworkV6) - jsonWriter.WriteValue("[" + localAddress.ToString() + "]"); - else - jsonWriter.WriteValue(localAddress.ToString()); + _webServiceLocalAddresses = DnsServer.GetValidKestralLocalAddresses(_webServiceLocalAddresses); + await StartWebServiceAsync(); + } + catch (Exception ex) + { + _log.Write("Web Service failed to start: " + ex.ToString()); + _log.Write("Attempting to start Web Service on ANY (0.0.0.0) fallback address..."); + + try + { + _webServiceLocalAddresses = new IPAddress[] { IPAddress.Any }; + await StartWebServiceAsync(); + } + catch (Exception ex2) + { + _log.Write("Web Service failed to start: " + ex2.ToString()); + _log.Write("Attempting to start Web Service on loopback (127.0.0.1) fallback address..."); + + _webServiceLocalAddresses = new IPAddress[] { IPAddress.Loopback }; + await StartWebServiceAsync(); + } + } + } + + private async Task StartWebServiceAsync() + { + WebApplicationBuilder builder = WebApplication.CreateBuilder(); + + builder.Environment.ContentRootFileProvider = new PhysicalFileProvider(_appFolder) + { + UseActivePolling = true, + UsePollingFileWatcher = true + }; + + builder.Environment.WebRootFileProvider = new PhysicalFileProvider(Path.Combine(_appFolder, "www")) + { + UseActivePolling = true, + UsePollingFileWatcher = true + }; + + builder.WebHost.ConfigureKestrel(delegate (WebHostBuilderContext context, KestrelServerOptions serverOptions) + { + //http + foreach (IPAddress webServiceLocalAddress in _webServiceLocalAddresses) + serverOptions.Listen(webServiceLocalAddress, _webServiceHttpPort); + + //https + if (_webServiceEnableTls && (_webServiceTlsCertificate is not null)) + { + serverOptions.ConfigureHttpsDefaults(delegate (HttpsConnectionAdapterOptions configureOptions) + { + configureOptions.ServerCertificateSelector = delegate (ConnectionContext context, string dnsName) + { + return _webServiceTlsCertificate; + }; + }); + + foreach (IPAddress webServiceLocalAddress in _webServiceLocalAddresses) + { + serverOptions.Listen(webServiceLocalAddress, _webServiceTlsPort, delegate (ListenOptions listenOptions) + { + listenOptions.Protocols = HttpProtocols.Http1AndHttp2AndHttp3; + listenOptions.UseHttps(); + }); + } + } + + serverOptions.AddServerHeader = false; + serverOptions.Limits.MaxRequestBodySize = null; + }); + + builder.Logging.ClearProviders(); + + _webService = builder.Build(); + + if (_webServiceHttpToTlsRedirect) + _webService.UseHttpsRedirection(); + + _webService.UseDefaultFiles(); + _webService.UseStaticFiles(new StaticFileOptions() + { + OnPrepareResponse = delegate (StaticFileResponseContext ctx) + { + ctx.Context.Response.Headers.Add("X-Robots-Tag", "noindex, nofollow"); + ctx.Context.Response.Headers.Add("Cache-Control", "private, max-age=300"); + } + }); + + ConfigureWebServiceRoutes(); + + try + { + await _webService.StartAsync(); + + foreach (IPAddress webServiceLocalAddress in _webServiceLocalAddresses) + { + _log?.Write(new IPEndPoint(webServiceLocalAddress, _webServiceHttpPort), "Http", "Web Service was bound successfully."); + + if (_webServiceEnableTls && (_webServiceTlsCertificate is not null)) + _log?.Write(new IPEndPoint(webServiceLocalAddress, _webServiceHttpPort), "Https", "Web Service was bound successfully."); + } + } + catch + { + await StopWebServiceAsync(); + + foreach (IPAddress webServiceLocalAddress in _webServiceLocalAddresses) + { + _log?.Write(new IPEndPoint(webServiceLocalAddress, _webServiceHttpPort), "Http", "Web Service failed to bind."); + + if (_webServiceEnableTls && (_webServiceTlsCertificate is not null)) + _log?.Write(new IPEndPoint(webServiceLocalAddress, _webServiceHttpPort), "Https", "Web Service failed to bind."); + } + + throw; + } + } + + internal async Task StopWebServiceAsync() + { + if (_webService is not null) + { + await _webService.DisposeAsync(); + _webService = null; + } + } + + private void ConfigureWebServiceRoutes() + { + _webService.UseExceptionHandler(WebServiceExceptionHandler); + + _webService.Use(WebServiceApiMiddleware); + + _webService.UseRouting(); + + //user auth + _webService.MapGetAndPost("/api/user/login", delegate (HttpContext context) { return _authApi.LoginAsync(context, UserSessionType.Standard); }); + _webService.MapGetAndPost("/api/user/createToken", delegate (HttpContext context) { return _authApi.LoginAsync(context, UserSessionType.ApiToken); }); + _webService.MapGetAndPost("/api/user/logout", _authApi.Logout); + + //user + _webService.MapGetAndPost("/api/user/session/get", _authApi.GetCurrentSessionDetails); + _webService.MapGetAndPost("/api/user/session/delete", delegate (HttpContext context) { _authApi.DeleteSession(context, false); }); + _webService.MapGetAndPost("/api/user/changePassword", _authApi.ChangePassword); + _webService.MapGetAndPost("/api/user/profile/get", _authApi.GetProfile); + _webService.MapGetAndPost("/api/user/profile/set", _authApi.SetProfile); + _webService.MapGetAndPost("/api/user/checkForUpdate", _api.CheckForUpdateAsync); + + //dashboard + _webService.MapGetAndPost("/api/dashboard/stats/get", _dashboardApi.GetStats); + _webService.MapGetAndPost("/api/dashboard/stats/getTop", _dashboardApi.GetTopStats); + _webService.MapGetAndPost("/api/dashboard/stats/deleteAll", _logsApi.DeleteAllStats); + + //zones + _webService.MapGetAndPost("/api/zones/list", _zonesApi.ListZones); + _webService.MapGetAndPost("/api/zones/create", _zonesApi.CreateZoneAsync); + _webService.MapGetAndPost("/api/zones/enable", _zonesApi.EnableZone); + _webService.MapGetAndPost("/api/zones/disable", _zonesApi.DisableZone); + _webService.MapGetAndPost("/api/zones/delete", _zonesApi.DeleteZone); + _webService.MapGetAndPost("/api/zones/resync", _zonesApi.ResyncZone); + _webService.MapGetAndPost("/api/zones/options/get", _zonesApi.GetZoneOptions); + _webService.MapGetAndPost("/api/zones/options/set", _zonesApi.SetZoneOptions); + _webService.MapGetAndPost("/api/zones/permissions/get", delegate (HttpContext context) { _authApi.GetPermissionDetails(context, PermissionSection.Zones); }); + _webService.MapGetAndPost("/api/zones/permissions/set", delegate (HttpContext context) { _authApi.SetPermissionsDetails(context, PermissionSection.Zones); }); + _webService.MapGetAndPost("/api/zones/dnssec/sign", _zonesApi.SignPrimaryZone); + _webService.MapGetAndPost("/api/zones/dnssec/unsign", _zonesApi.UnsignPrimaryZone); + _webService.MapGetAndPost("/api/zones/dnssec/properties/get", _zonesApi.GetPrimaryZoneDnssecProperties); + _webService.MapGetAndPost("/api/zones/dnssec/properties/convertToNSEC", _zonesApi.ConvertPrimaryZoneToNSEC); + _webService.MapGetAndPost("/api/zones/dnssec/properties/convertToNSEC3", _zonesApi.ConvertPrimaryZoneToNSEC3); + _webService.MapGetAndPost("/api/zones/dnssec/properties/updateNSEC3Params", _zonesApi.UpdatePrimaryZoneNSEC3Parameters); + _webService.MapGetAndPost("/api/zones/dnssec/properties/updateDnsKeyTtl", _zonesApi.UpdatePrimaryZoneDnssecDnsKeyTtl); + _webService.MapGetAndPost("/api/zones/dnssec/properties/generatePrivateKey", _zonesApi.GenerateAndAddPrimaryZoneDnssecPrivateKey); + _webService.MapGetAndPost("/api/zones/dnssec/properties/updatePrivateKey", _zonesApi.UpdatePrimaryZoneDnssecPrivateKey); + _webService.MapGetAndPost("/api/zones/dnssec/properties/deletePrivateKey", _zonesApi.DeletePrimaryZoneDnssecPrivateKey); + _webService.MapGetAndPost("/api/zones/dnssec/properties/publishAllPrivateKeys", _zonesApi.PublishAllGeneratedPrimaryZoneDnssecPrivateKeys); + _webService.MapGetAndPost("/api/zones/dnssec/properties/rolloverDnsKey", _zonesApi.RolloverPrimaryZoneDnsKey); + _webService.MapGetAndPost("/api/zones/dnssec/properties/retireDnsKey", _zonesApi.RetirePrimaryZoneDnsKey); + _webService.MapGetAndPost("/api/zones/records/add", _zonesApi.AddRecord); + _webService.MapGetAndPost("/api/zones/records/get", _zonesApi.GetRecords); + _webService.MapGetAndPost("/api/zones/records/update", _zonesApi.UpdateRecord); + _webService.MapGetAndPost("/api/zones/records/delete", _zonesApi.DeleteRecord); + + //cache + _webService.MapGetAndPost("/api/cache/list", _otherZonesApi.ListCachedZones); + _webService.MapGetAndPost("/api/cache/delete", _otherZonesApi.DeleteCachedZone); + _webService.MapGetAndPost("/api/cache/flush", _otherZonesApi.FlushCache); + + //allowed + _webService.MapGetAndPost("/api/allowed/list", _otherZonesApi.ListAllowedZones); + _webService.MapGetAndPost("/api/allowed/add", _otherZonesApi.AllowZone); + _webService.MapGetAndPost("/api/allowed/delete", _otherZonesApi.DeleteAllowedZone); + _webService.MapGetAndPost("/api/allowed/flush", _otherZonesApi.FlushAllowedZone); + _webService.MapGetAndPost("/api/allowed/import", _otherZonesApi.ImportAllowedZones); + _webService.MapGetAndPost("/api/allowed/export", _otherZonesApi.ExportAllowedZonesAsync); + + //blocked + _webService.MapGetAndPost("/api/blocked/list", _otherZonesApi.ListBlockedZones); + _webService.MapGetAndPost("/api/blocked/add", _otherZonesApi.BlockZone); + _webService.MapGetAndPost("/api/blocked/delete", _otherZonesApi.DeleteBlockedZone); + _webService.MapGetAndPost("/api/blocked/flush", _otherZonesApi.FlushBlockedZone); + _webService.MapGetAndPost("/api/blocked/import", _otherZonesApi.ImportBlockedZones); + _webService.MapGetAndPost("/api/blocked/export", _otherZonesApi.ExportBlockedZonesAsync); + + //apps + _webService.MapGetAndPost("/api/apps/list", _appsApi.ListInstalledAppsAsync); + _webService.MapGetAndPost("/api/apps/listStoreApps", _appsApi.ListStoreApps); + _webService.MapGetAndPost("/api/apps/downloadAndInstall", _appsApi.DownloadAndInstallAppAsync); + _webService.MapGetAndPost("/api/apps/downloadAndUpdate", _appsApi.DownloadAndUpdateAppAsync); + _webService.MapPost("/api/apps/install", _appsApi.InstallAppAsync); + _webService.MapPost("/api/apps/update", _appsApi.UpdateAppAsync); + _webService.MapGetAndPost("/api/apps/uninstall", _appsApi.UninstallApp); + _webService.MapGetAndPost("/api/apps/config/get", _appsApi.GetAppConfigAsync); + _webService.MapGetAndPost("/api/apps/config/set", _appsApi.SetAppConfigAsync); + + //dns client + _webService.MapGetAndPost("/api/dnsClient/resolve", _api.ResolveQueryAsync); + + //settings + _webService.MapGetAndPost("/api/settings/get", _settingsApi.GetDnsSettings); + _webService.MapGetAndPost("/api/settings/set", _settingsApi.SetDnsSettings); + _webService.MapGetAndPost("/api/settings/getTsigKeyNames", _settingsApi.GetTsigKeyNames); + _webService.MapGetAndPost("/api/settings/forceUpdateBlockLists", _settingsApi.ForceUpdateBlockLists); + _webService.MapGetAndPost("/api/settings/temporaryDisableBlocking", _settingsApi.TemporaryDisableBlocking); + _webService.MapGetAndPost("/api/settings/backup", _settingsApi.BackupSettingsAsync); + _webService.MapPost("/api/settings/restore", _settingsApi.RestoreSettingsAsync); + + //dhcp + _webService.MapGetAndPost("/api/dhcp/leases/list", _dhcpApi.ListDhcpLeases); + _webService.MapGetAndPost("/api/dhcp/leases/remove", _dhcpApi.RemoveDhcpLease); + _webService.MapGetAndPost("/api/dhcp/leases/convertToReserved", _dhcpApi.ConvertToReservedLease); + _webService.MapGetAndPost("/api/dhcp/leases/convertToDynamic", _dhcpApi.ConvertToDynamicLease); + _webService.MapGetAndPost("/api/dhcp/scopes/list", _dhcpApi.ListDhcpScopes); + _webService.MapGetAndPost("/api/dhcp/scopes/get", _dhcpApi.GetDhcpScope); + _webService.MapGetAndPost("/api/dhcp/scopes/set", _dhcpApi.SetDhcpScopeAsync); + _webService.MapGetAndPost("/api/dhcp/scopes/addReservedLease", _dhcpApi.AddReservedLease); + _webService.MapGetAndPost("/api/dhcp/scopes/removeReservedLease", _dhcpApi.RemoveReservedLease); + _webService.MapGetAndPost("/api/dhcp/scopes/enable", _dhcpApi.EnableDhcpScopeAsync); + _webService.MapGetAndPost("/api/dhcp/scopes/disable", _dhcpApi.DisableDhcpScope); + _webService.MapGetAndPost("/api/dhcp/scopes/delete", _dhcpApi.DeleteDhcpScope); + + //administration + _webService.MapGetAndPost("/api/admin/sessions/list", _authApi.ListSessions); + _webService.MapGetAndPost("/api/admin/sessions/createToken", _authApi.CreateApiToken); + _webService.MapGetAndPost("/api/admin/sessions/delete", delegate (HttpContext context) { _authApi.DeleteSession(context, true); }); + _webService.MapGetAndPost("/api/admin/users/list", _authApi.ListUsers); + _webService.MapGetAndPost("/api/admin/users/create", _authApi.CreateUser); + _webService.MapGetAndPost("/api/admin/users/get", _authApi.GetUserDetails); + _webService.MapGetAndPost("/api/admin/users/set", _authApi.SetUserDetails); + _webService.MapGetAndPost("/api/admin/users/delete", _authApi.DeleteUser); + _webService.MapGetAndPost("/api/admin/groups/list", _authApi.ListGroups); + _webService.MapGetAndPost("/api/admin/groups/create", _authApi.CreateGroup); + _webService.MapGetAndPost("/api/admin/groups/get", _authApi.GetGroupDetails); + _webService.MapGetAndPost("/api/admin/groups/set", _authApi.SetGroupDetails); + _webService.MapGetAndPost("/api/admin/groups/delete", _authApi.DeleteGroup); + _webService.MapGetAndPost("/api/admin/permissions/list", _authApi.ListPermissions); + _webService.MapGetAndPost("/api/admin/permissions/get", delegate (HttpContext context) { _authApi.GetPermissionDetails(context, PermissionSection.Unknown); }); + _webService.MapGetAndPost("/api/admin/permissions/set", delegate (HttpContext context) { _authApi.SetPermissionsDetails(context, PermissionSection.Unknown); }); + + //logs + _webService.MapGetAndPost("/api/logs/list", _logsApi.ListLogs); + _webService.MapGetAndPost("/api/logs/download", _logsApi.DownloadLogAsync); + _webService.MapGetAndPost("/api/logs/delete", _logsApi.DeleteLog); + _webService.MapGetAndPost("/api/logs/deleteAll", _logsApi.DeleteAllLogs); + _webService.MapGetAndPost("/api/logs/query", _logsApi.QueryLogsAsync); + } + + private async Task WebServiceApiMiddleware(HttpContext context, RequestDelegate next) + { + bool needsJsonResponseObject; + + switch (context.Request.Path) + { + case "/api/user/login": + case "/api/user/createToken": + case "/api/user/logout": + needsJsonResponseObject = false; + break; + + case "/api/user/session/get": + { + if (!TryGetSession(context, out UserSession session)) + throw new InvalidTokenWebServiceException("Invalid token or session expired."); + + context.Items["session"] = session; + + needsJsonResponseObject = false; + } + break; + + case "/api/allowed/export": + case "/api/blocked/export": + case "/api/settings/backup": + case "/api/logs/download": + { + if (!TryGetSession(context, out UserSession session)) + throw new InvalidTokenWebServiceException("Invalid token or session expired."); + + context.Items["session"] = session; + + await next(context); + } + return; + + default: + { + if (!TryGetSession(context, out UserSession session)) + throw new InvalidTokenWebServiceException("Invalid token or session expired."); + + context.Items["session"] = session; + needsJsonResponseObject = true; + } + break; } - jsonWriter.WriteEndArray(); - - jsonWriter.WritePropertyName("webServiceHttpPort"); - jsonWriter.WriteValue(_webServiceHttpPort); - - jsonWriter.WritePropertyName("webServiceEnableTls"); - jsonWriter.WriteValue(_webServiceEnableTls); - - jsonWriter.WritePropertyName("webServiceHttpToTlsRedirect"); - jsonWriter.WriteValue(_webServiceHttpToTlsRedirect); - - jsonWriter.WritePropertyName("webServiceTlsPort"); - jsonWriter.WriteValue(_webServiceTlsPort); - - jsonWriter.WritePropertyName("webServiceUseSelfSignedTlsCertificate"); - jsonWriter.WriteValue(_webServiceUseSelfSignedTlsCertificate); - - jsonWriter.WritePropertyName("webServiceTlsCertificatePath"); - jsonWriter.WriteValue(_webServiceTlsCertificatePath); - - jsonWriter.WritePropertyName("webServiceTlsCertificatePassword"); - jsonWriter.WriteValue("************"); - - jsonWriter.WritePropertyName("enableDnsOverHttp"); - jsonWriter.WriteValue(_dnsServer.EnableDnsOverHttp); - - jsonWriter.WritePropertyName("enableDnsOverTls"); - jsonWriter.WriteValue(_dnsServer.EnableDnsOverTls); - - jsonWriter.WritePropertyName("enableDnsOverHttps"); - jsonWriter.WriteValue(_dnsServer.EnableDnsOverHttps); - - jsonWriter.WritePropertyName("dnsTlsCertificatePath"); - jsonWriter.WriteValue(_dnsTlsCertificatePath); - - jsonWriter.WritePropertyName("dnsTlsCertificatePassword"); - jsonWriter.WriteValue("************"); - - jsonWriter.WritePropertyName("tsigKeys"); + using (MemoryStream mS = new MemoryStream()) { - jsonWriter.WriteStartArray(); + Utf8JsonWriter jsonWriter = new Utf8JsonWriter(mS); + context.Items["jsonWriter"] = jsonWriter; - if (_dnsServer.TsigKeys is not null) + jsonWriter.WriteStartObject(); + + if (needsJsonResponseObject) { - foreach (KeyValuePair tsigKey in _dnsServer.TsigKeys) + jsonWriter.WritePropertyName("response"); + jsonWriter.WriteStartObject(); + + await next(context); + + jsonWriter.WriteEndObject(); + } + else + { + await next(context); + } + + jsonWriter.WriteString("status", "ok"); + + jsonWriter.WriteEndObject(); + jsonWriter.Flush(); + + mS.Position = 0; + + HttpResponse response = context.Response; + + response.StatusCode = StatusCodes.Status200OK; + response.ContentType = "application/json; charset=utf-8"; + response.ContentLength = mS.Length; + + await mS.CopyToAsync(response.Body); + } + } + + private static void WebServiceExceptionHandler(IApplicationBuilder exceptionHandlerApp) + { + exceptionHandlerApp.Run(async delegate (HttpContext context) + { + IExceptionHandlerPathFeature exceptionHandlerPathFeature = context.Features.Get(); + if (exceptionHandlerPathFeature.Path.StartsWith("/api/")) + { + Exception ex = exceptionHandlerPathFeature.Error; + + context.Response.StatusCode = StatusCodes.Status200OK; + context.Response.ContentType = "application/json; charset=utf-8"; + + await using (Utf8JsonWriter jsonWriter = new Utf8JsonWriter(context.Response.Body)) { jsonWriter.WriteStartObject(); - jsonWriter.WritePropertyName("keyName"); - jsonWriter.WriteValue(tsigKey.Key); + if (ex is InvalidTokenWebServiceException) + { + jsonWriter.WriteString("status", "invalid-token"); + jsonWriter.WriteString("errorMessage", ex.Message); + } + else + { + jsonWriter.WriteString("status", "error"); + jsonWriter.WriteString("errorMessage", ex.Message); + jsonWriter.WriteString("stackTrace", ex.StackTrace); - jsonWriter.WritePropertyName("sharedSecret"); - jsonWriter.WriteValue(tsigKey.Value.SharedSecret); - - jsonWriter.WritePropertyName("algorithmName"); - jsonWriter.WriteValue(tsigKey.Value.AlgorithmName); + if (ex.InnerException is not null) + jsonWriter.WriteString("innerErrorMessage", ex.InnerException.Message); + } jsonWriter.WriteEndObject(); } } - - jsonWriter.WriteEndArray(); - } - - jsonWriter.WritePropertyName("defaultRecordTtl"); - jsonWriter.WriteValue(_zonesApi.DefaultRecordTtl); - - jsonWriter.WritePropertyName("dnsAppsEnableAutomaticUpdate"); - jsonWriter.WriteValue(_appsApi.EnableAutomaticUpdate); - - jsonWriter.WritePropertyName("preferIPv6"); - jsonWriter.WriteValue(_dnsServer.PreferIPv6); - - jsonWriter.WritePropertyName("udpPayloadSize"); - jsonWriter.WriteValue(_dnsServer.UdpPayloadSize); - - jsonWriter.WritePropertyName("dnssecValidation"); - jsonWriter.WriteValue(_dnsServer.DnssecValidation); - - jsonWriter.WritePropertyName("eDnsClientSubnet"); - jsonWriter.WriteValue(_dnsServer.EDnsClientSubnet); - - jsonWriter.WritePropertyName("eDnsClientSubnetIPv4PrefixLength"); - jsonWriter.WriteValue(_dnsServer.EDnsClientSubnetIPv4PrefixLength); - - jsonWriter.WritePropertyName("eDnsClientSubnetIPv6PrefixLength"); - jsonWriter.WriteValue(_dnsServer.EDnsClientSubnetIPv6PrefixLength); - - jsonWriter.WritePropertyName("resolverRetries"); - jsonWriter.WriteValue(_dnsServer.ResolverRetries); - - jsonWriter.WritePropertyName("resolverTimeout"); - jsonWriter.WriteValue(_dnsServer.ResolverTimeout); - - jsonWriter.WritePropertyName("resolverMaxStackCount"); - jsonWriter.WriteValue(_dnsServer.ResolverMaxStackCount); - - jsonWriter.WritePropertyName("forwarderRetries"); - jsonWriter.WriteValue(_dnsServer.ForwarderRetries); - - jsonWriter.WritePropertyName("forwarderTimeout"); - jsonWriter.WriteValue(_dnsServer.ForwarderTimeout); - - jsonWriter.WritePropertyName("forwarderConcurrency"); - jsonWriter.WriteValue(_dnsServer.ForwarderConcurrency); - - jsonWriter.WritePropertyName("clientTimeout"); - jsonWriter.WriteValue(_dnsServer.ClientTimeout); - - jsonWriter.WritePropertyName("tcpSendTimeout"); - jsonWriter.WriteValue(_dnsServer.TcpSendTimeout); - - jsonWriter.WritePropertyName("tcpReceiveTimeout"); - jsonWriter.WriteValue(_dnsServer.TcpReceiveTimeout); - - jsonWriter.WritePropertyName("enableLogging"); - jsonWriter.WriteValue(_log.EnableLogging); - - jsonWriter.WritePropertyName("logQueries"); - jsonWriter.WriteValue(_dnsServer.QueryLogManager != null); - - jsonWriter.WritePropertyName("useLocalTime"); - jsonWriter.WriteValue(_log.UseLocalTime); - - jsonWriter.WritePropertyName("logFolder"); - jsonWriter.WriteValue(_log.LogFolder); - - jsonWriter.WritePropertyName("maxLogFileDays"); - jsonWriter.WriteValue(_log.MaxLogFileDays); - - jsonWriter.WritePropertyName("maxStatFileDays"); - jsonWriter.WriteValue(_dnsServer.StatsManager.MaxStatFileDays); - - jsonWriter.WritePropertyName("recursion"); - jsonWriter.WriteValue(_dnsServer.Recursion.ToString()); - - jsonWriter.WritePropertyName("recursionDeniedNetworks"); - { - jsonWriter.WriteStartArray(); - - if (_dnsServer.RecursionDeniedNetworks is not null) - { - foreach (NetworkAddress networkAddress in _dnsServer.RecursionDeniedNetworks) - jsonWriter.WriteValue(networkAddress.ToString()); - } - - jsonWriter.WriteEndArray(); - } - - jsonWriter.WritePropertyName("recursionAllowedNetworks"); - { - jsonWriter.WriteStartArray(); - - if (_dnsServer.RecursionAllowedNetworks is not null) - { - foreach (NetworkAddress networkAddress in _dnsServer.RecursionAllowedNetworks) - jsonWriter.WriteValue(networkAddress.ToString()); - } - - jsonWriter.WriteEndArray(); - } - - jsonWriter.WritePropertyName("randomizeName"); - jsonWriter.WriteValue(_dnsServer.RandomizeName); - - jsonWriter.WritePropertyName("qnameMinimization"); - jsonWriter.WriteValue(_dnsServer.QnameMinimization); - - jsonWriter.WritePropertyName("nsRevalidation"); - jsonWriter.WriteValue(_dnsServer.NsRevalidation); - - jsonWriter.WritePropertyName("qpmLimitRequests"); - jsonWriter.WriteValue(_dnsServer.QpmLimitRequests); - - jsonWriter.WritePropertyName("qpmLimitErrors"); - jsonWriter.WriteValue(_dnsServer.QpmLimitErrors); - - jsonWriter.WritePropertyName("qpmLimitSampleMinutes"); - jsonWriter.WriteValue(_dnsServer.QpmLimitSampleMinutes); - - jsonWriter.WritePropertyName("qpmLimitIPv4PrefixLength"); - jsonWriter.WriteValue(_dnsServer.QpmLimitIPv4PrefixLength); - - jsonWriter.WritePropertyName("qpmLimitIPv6PrefixLength"); - jsonWriter.WriteValue(_dnsServer.QpmLimitIPv6PrefixLength); - - jsonWriter.WritePropertyName("serveStale"); - jsonWriter.WriteValue(_dnsServer.ServeStale); - - jsonWriter.WritePropertyName("serveStaleTtl"); - jsonWriter.WriteValue(_dnsServer.CacheZoneManager.ServeStaleTtl); - - jsonWriter.WritePropertyName("cacheMaximumEntries"); - jsonWriter.WriteValue(_dnsServer.CacheZoneManager.MaximumEntries); - - jsonWriter.WritePropertyName("cacheMinimumRecordTtl"); - jsonWriter.WriteValue(_dnsServer.CacheZoneManager.MinimumRecordTtl); - - jsonWriter.WritePropertyName("cacheMaximumRecordTtl"); - jsonWriter.WriteValue(_dnsServer.CacheZoneManager.MaximumRecordTtl); - - jsonWriter.WritePropertyName("cacheNegativeRecordTtl"); - jsonWriter.WriteValue(_dnsServer.CacheZoneManager.NegativeRecordTtl); - - jsonWriter.WritePropertyName("cacheFailureRecordTtl"); - jsonWriter.WriteValue(_dnsServer.CacheZoneManager.FailureRecordTtl); - - jsonWriter.WritePropertyName("cachePrefetchEligibility"); - jsonWriter.WriteValue(_dnsServer.CachePrefetchEligibility); - - jsonWriter.WritePropertyName("cachePrefetchTrigger"); - jsonWriter.WriteValue(_dnsServer.CachePrefetchTrigger); - - jsonWriter.WritePropertyName("cachePrefetchSampleIntervalInMinutes"); - jsonWriter.WriteValue(_dnsServer.CachePrefetchSampleIntervalInMinutes); - - jsonWriter.WritePropertyName("cachePrefetchSampleEligibilityHitsPerHour"); - jsonWriter.WriteValue(_dnsServer.CachePrefetchSampleEligibilityHitsPerHour); - - jsonWriter.WritePropertyName("proxy"); - if (_dnsServer.Proxy == null) - { - jsonWriter.WriteNull(); - } - else - { - jsonWriter.WriteStartObject(); - - NetProxy proxy = _dnsServer.Proxy; - - jsonWriter.WritePropertyName("type"); - jsonWriter.WriteValue(proxy.Type.ToString()); - - jsonWriter.WritePropertyName("address"); - jsonWriter.WriteValue(proxy.Address); - - jsonWriter.WritePropertyName("port"); - jsonWriter.WriteValue(proxy.Port); - - NetworkCredential credential = proxy.Credential; - - if (credential != null) - { - jsonWriter.WritePropertyName("username"); - jsonWriter.WriteValue(credential.UserName); - - jsonWriter.WritePropertyName("password"); - jsonWriter.WriteValue(credential.Password); - } - - jsonWriter.WritePropertyName("bypass"); - jsonWriter.WriteStartArray(); - - foreach (NetProxyBypassItem item in proxy.BypassList) - jsonWriter.WriteValue(item.Value); - - jsonWriter.WriteEndArray(); - - jsonWriter.WriteEndObject(); - } - - jsonWriter.WritePropertyName("forwarders"); - - DnsTransportProtocol forwarderProtocol = DnsTransportProtocol.Udp; - - if (_dnsServer.Forwarders == null) - { - jsonWriter.WriteNull(); - } - else - { - forwarderProtocol = _dnsServer.Forwarders[0].Protocol; - - jsonWriter.WriteStartArray(); - - foreach (NameServerAddress forwarder in _dnsServer.Forwarders) - jsonWriter.WriteValue(forwarder.OriginalAddress); - - jsonWriter.WriteEndArray(); - } - - jsonWriter.WritePropertyName("forwarderProtocol"); - jsonWriter.WriteValue(forwarderProtocol.ToString()); - - jsonWriter.WritePropertyName("enableBlocking"); - jsonWriter.WriteValue(_dnsServer.EnableBlocking); - - jsonWriter.WritePropertyName("allowTxtBlockingReport"); - jsonWriter.WriteValue(_dnsServer.AllowTxtBlockingReport); - - if (!_dnsServer.EnableBlocking && (DateTime.UtcNow < _temporaryDisableBlockingTill)) - { - jsonWriter.WritePropertyName("temporaryDisableBlockingTill"); - jsonWriter.WriteValue(_temporaryDisableBlockingTill); - } - - jsonWriter.WritePropertyName("blockingType"); - jsonWriter.WriteValue(_dnsServer.BlockingType.ToString()); - - jsonWriter.WritePropertyName("customBlockingAddresses"); - jsonWriter.WriteStartArray(); - - foreach (DnsARecordData record in _dnsServer.CustomBlockingARecords) - jsonWriter.WriteValue(record.Address.ToString()); - - foreach (DnsAAAARecordData record in _dnsServer.CustomBlockingAAAARecords) - jsonWriter.WriteValue(record.Address.ToString()); - - jsonWriter.WriteEndArray(); - - jsonWriter.WritePropertyName("blockListUrls"); - - if ((_dnsServer.BlockListZoneManager.AllowListUrls.Count == 0) && (_dnsServer.BlockListZoneManager.BlockListUrls.Count == 0)) - { - jsonWriter.WriteNull(); - } - else - { - jsonWriter.WriteStartArray(); - - foreach (Uri allowListUrl in _dnsServer.BlockListZoneManager.AllowListUrls) - jsonWriter.WriteValue("!" + allowListUrl.AbsoluteUri); - - foreach (Uri blockListUrl in _dnsServer.BlockListZoneManager.BlockListUrls) - jsonWriter.WriteValue(blockListUrl.AbsoluteUri); - - jsonWriter.WriteEndArray(); - } - - jsonWriter.WritePropertyName("blockListUpdateIntervalHours"); - jsonWriter.WriteValue(_blockListUpdateIntervalHours); - - if (_blockListUpdateTimer is not null) - { - DateTime blockListNextUpdatedOn = _blockListLastUpdatedOn.AddHours(_blockListUpdateIntervalHours); - - jsonWriter.WritePropertyName("blockListNextUpdatedOn"); - jsonWriter.WriteValue(blockListNextUpdatedOn); - } - } - - private void SetDnsSettings(HttpListenerRequest request, JsonTextWriter jsonWriter) - { - bool serverDomainChanged = false; - bool restartDnsService = false; - bool restartWebService = false; - - string strDnsServerDomain = request.QueryString["dnsServerDomain"]; - if (!string.IsNullOrEmpty(strDnsServerDomain)) - { - serverDomainChanged = !_dnsServer.ServerDomain.Equals(strDnsServerDomain, StringComparison.OrdinalIgnoreCase); - _dnsServer.ServerDomain = strDnsServerDomain; - } - - string strDnsServerLocalEndPoints = request.QueryString["dnsServerLocalEndPoints"]; - if (strDnsServerLocalEndPoints != null) - { - if (string.IsNullOrEmpty(strDnsServerLocalEndPoints)) - strDnsServerLocalEndPoints = "0.0.0.0:53,[::]:53"; - - string[] strLocalEndPoints = strDnsServerLocalEndPoints.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - List localEndPoints = new List(strLocalEndPoints.Length); - - for (int i = 0; i < strLocalEndPoints.Length; i++) - { - NameServerAddress nameServer = new NameServerAddress(strLocalEndPoints[i]); - if (nameServer.IPEndPoint != null) - localEndPoints.Add(nameServer.IPEndPoint); - } - - if (localEndPoints.Count > 0) - { - if (_dnsServer.LocalEndPoints.Count != localEndPoints.Count) - { - restartDnsService = true; - } - else - { - foreach (IPEndPoint currentLocalEP in _dnsServer.LocalEndPoints) - { - if (!localEndPoints.Contains(currentLocalEP)) - { - restartDnsService = true; - break; - } - } - } - - _dnsServer.LocalEndPoints = localEndPoints; - } - } - - string strWebServiceLocalAddresses = request.QueryString["webServiceLocalAddresses"]; - if (strWebServiceLocalAddresses != null) - { - if (string.IsNullOrEmpty(strWebServiceLocalAddresses)) - strWebServiceLocalAddresses = "0.0.0.0,[::]"; - - string[] strLocalAddresses = strWebServiceLocalAddresses.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - List localAddresses = new List(strLocalAddresses.Length); - - for (int i = 0; i < strLocalAddresses.Length; i++) - { - if (IPAddress.TryParse(strLocalAddresses[i], out IPAddress localAddress)) - localAddresses.Add(localAddress); - } - - if (localAddresses.Count > 0) - { - if (_webServiceLocalAddresses.Count != localAddresses.Count) - { - restartWebService = true; - } - else - { - foreach (IPAddress currentlocalAddress in _webServiceLocalAddresses) - { - if (!localAddresses.Contains(currentlocalAddress)) - { - restartWebService = true; - break; - } - } - } - - _webServiceLocalAddresses = localAddresses; - } - } - - int oldWebServiceHttpPort = _webServiceHttpPort; - - string strWebServiceHttpPort = request.QueryString["webServiceHttpPort"]; - if (!string.IsNullOrEmpty(strWebServiceHttpPort)) - { - _webServiceHttpPort = int.Parse(strWebServiceHttpPort); - - if (oldWebServiceHttpPort != _webServiceHttpPort) - restartWebService = true; - } - - string strWebServiceEnableTls = request.QueryString["webServiceEnableTls"]; - if (!string.IsNullOrEmpty(strWebServiceEnableTls)) - { - bool oldWebServiceEnableTls = _webServiceEnableTls; - - _webServiceEnableTls = bool.Parse(strWebServiceEnableTls); - - if (oldWebServiceEnableTls != _webServiceEnableTls) - restartWebService = true; - } - - string strWebServiceHttpToTlsRedirect = request.QueryString["webServiceHttpToTlsRedirect"]; - if (!string.IsNullOrEmpty(strWebServiceHttpToTlsRedirect)) - _webServiceHttpToTlsRedirect = bool.Parse(strWebServiceHttpToTlsRedirect); - - string strWebServiceTlsPort = request.QueryString["webServiceTlsPort"]; - if (!string.IsNullOrEmpty(strWebServiceTlsPort)) - { - int oldWebServiceTlsPort = _webServiceTlsPort; - - _webServiceTlsPort = int.Parse(strWebServiceTlsPort); - - if (oldWebServiceTlsPort != _webServiceTlsPort) - restartWebService = true; - } - - string strWebServiceUseSelfSignedTlsCertificate = request.QueryString["webServiceUseSelfSignedTlsCertificate"]; - if (!string.IsNullOrEmpty(strWebServiceUseSelfSignedTlsCertificate)) - _webServiceUseSelfSignedTlsCertificate = bool.Parse(strWebServiceUseSelfSignedTlsCertificate); - - string strWebServiceTlsCertificatePath = request.QueryString["webServiceTlsCertificatePath"]; - string strWebServiceTlsCertificatePassword = request.QueryString["webServiceTlsCertificatePassword"]; - if (string.IsNullOrEmpty(strWebServiceTlsCertificatePath)) - { - _webServiceTlsCertificatePath = null; - _webServiceTlsCertificatePassword = ""; - } - else - { - if (strWebServiceTlsCertificatePassword == "************") - strWebServiceTlsCertificatePassword = _webServiceTlsCertificatePassword; - - if ((strWebServiceTlsCertificatePath != _webServiceTlsCertificatePath) || (strWebServiceTlsCertificatePassword != _webServiceTlsCertificatePassword)) - { - LoadWebServiceTlsCertificate(strWebServiceTlsCertificatePath, strWebServiceTlsCertificatePassword); - - _webServiceTlsCertificatePath = strWebServiceTlsCertificatePath; - _webServiceTlsCertificatePassword = strWebServiceTlsCertificatePassword; - - StartTlsCertificateUpdateTimer(); - } - } - - string enableDnsOverHttp = request.QueryString["enableDnsOverHttp"]; - if (!string.IsNullOrEmpty(enableDnsOverHttp)) - { - bool oldEnableDnsOverHttp = _dnsServer.EnableDnsOverHttp; - - _dnsServer.EnableDnsOverHttp = bool.Parse(enableDnsOverHttp); - - if (oldEnableDnsOverHttp != _dnsServer.EnableDnsOverHttp) - restartDnsService = true; - } - - string strEnableDnsOverTls = request.QueryString["enableDnsOverTls"]; - if (!string.IsNullOrEmpty(strEnableDnsOverTls)) - { - bool oldEnableDnsOverTls = _dnsServer.EnableDnsOverTls; - - _dnsServer.EnableDnsOverTls = bool.Parse(strEnableDnsOverTls); - - if (oldEnableDnsOverTls != _dnsServer.EnableDnsOverTls) - restartDnsService = true; - } - - string strEnableDnsOverHttps = request.QueryString["enableDnsOverHttps"]; - if (!string.IsNullOrEmpty(strEnableDnsOverHttps)) - { - bool oldEnableDnsOverHttps = _dnsServer.EnableDnsOverHttps; - - _dnsServer.EnableDnsOverHttps = bool.Parse(strEnableDnsOverHttps); - - if (oldEnableDnsOverHttps != _dnsServer.EnableDnsOverHttps) - restartDnsService = true; - } - - string strDnsTlsCertificatePath = request.QueryString["dnsTlsCertificatePath"]; - string strDnsTlsCertificatePassword = request.QueryString["dnsTlsCertificatePassword"]; - if (string.IsNullOrEmpty(strDnsTlsCertificatePath)) - { - _dnsTlsCertificatePath = null; - _dnsTlsCertificatePassword = ""; - } - else - { - if (strDnsTlsCertificatePassword == "************") - strDnsTlsCertificatePassword = _dnsTlsCertificatePassword; - - if ((strDnsTlsCertificatePath != _dnsTlsCertificatePath) || (strDnsTlsCertificatePassword != _dnsTlsCertificatePassword)) - { - LoadDnsTlsCertificate(strDnsTlsCertificatePath, strDnsTlsCertificatePassword); - - _dnsTlsCertificatePath = strDnsTlsCertificatePath; - _dnsTlsCertificatePassword = strDnsTlsCertificatePassword; - - StartTlsCertificateUpdateTimer(); - } - } - - string strTsigKeys = request.QueryString["tsigKeys"]; - if (!string.IsNullOrEmpty(strTsigKeys)) - { - if (strTsigKeys == "false") - { - _dnsServer.TsigKeys = null; - } - else - { - string[] strTsigKeyParts = strTsigKeys.Split('|'); - Dictionary tsigKeys = new Dictionary(strTsigKeyParts.Length); - - for (int i = 0; i < strTsigKeyParts.Length; i += 3) - { - string keyName = strTsigKeyParts[i + 0].ToLower(); - string sharedSecret = strTsigKeyParts[i + 1]; - string algorithmName = strTsigKeyParts[i + 2]; - - if (sharedSecret.Length == 0) - { - byte[] key = new byte[32]; - _rng.GetBytes(key); - - tsigKeys.Add(keyName, new TsigKey(keyName, Convert.ToBase64String(key), algorithmName)); - } - else - { - tsigKeys.Add(keyName, new TsigKey(keyName, sharedSecret, algorithmName)); - } - } - - _dnsServer.TsigKeys = tsigKeys; - } - } - - string strDefaultRecordTtl = request.QueryString["defaultRecordTtl"]; - if (!string.IsNullOrEmpty(strDefaultRecordTtl)) - _zonesApi.DefaultRecordTtl = uint.Parse(strDefaultRecordTtl); - - string strDnsAppsEnableAutomaticUpdate = request.QueryString["dnsAppsEnableAutomaticUpdate"]; - if (!string.IsNullOrEmpty(strDnsAppsEnableAutomaticUpdate)) - _appsApi.EnableAutomaticUpdate = bool.Parse(strDnsAppsEnableAutomaticUpdate); - - string strPreferIPv6 = request.QueryString["preferIPv6"]; - if (!string.IsNullOrEmpty(strPreferIPv6)) - _dnsServer.PreferIPv6 = bool.Parse(strPreferIPv6); - - string strUdpPayloadSize = request.QueryString["udpPayloadSize"]; - if (!string.IsNullOrEmpty(strUdpPayloadSize)) - _dnsServer.UdpPayloadSize = ushort.Parse(strUdpPayloadSize); - - string strDnssecValidation = request.QueryString["dnssecValidation"]; - if (!string.IsNullOrEmpty(strDnssecValidation)) - _dnsServer.DnssecValidation = bool.Parse(strDnssecValidation); - - string strEDnsClientSubnet = request.QueryString["eDnsClientSubnet"]; - if (!string.IsNullOrEmpty(strEDnsClientSubnet)) - _dnsServer.EDnsClientSubnet = bool.Parse(strEDnsClientSubnet); - - string strEDnsClientSubnetIPv4PrefixLength = request.QueryString["eDnsClientSubnetIPv4PrefixLength"]; - if (!string.IsNullOrEmpty(strEDnsClientSubnetIPv4PrefixLength)) - _dnsServer.EDnsClientSubnetIPv4PrefixLength = byte.Parse(strEDnsClientSubnetIPv4PrefixLength); - - string strEDnsClientSubnetIPv6PrefixLength = request.QueryString["eDnsClientSubnetIPv6PrefixLength"]; - if (!string.IsNullOrEmpty(strEDnsClientSubnetIPv6PrefixLength)) - _dnsServer.EDnsClientSubnetIPv6PrefixLength = byte.Parse(strEDnsClientSubnetIPv6PrefixLength); - - string strResolverRetries = request.QueryString["resolverRetries"]; - if (!string.IsNullOrEmpty(strResolverRetries)) - _dnsServer.ResolverRetries = int.Parse(strResolverRetries); - - string strResolverTimeout = request.QueryString["resolverTimeout"]; - if (!string.IsNullOrEmpty(strResolverTimeout)) - _dnsServer.ResolverTimeout = int.Parse(strResolverTimeout); - - string strResolverMaxStackCount = request.QueryString["resolverMaxStackCount"]; - if (!string.IsNullOrEmpty(strResolverMaxStackCount)) - _dnsServer.ResolverMaxStackCount = int.Parse(strResolverMaxStackCount); - - string strForwarderRetries = request.QueryString["forwarderRetries"]; - if (!string.IsNullOrEmpty(strForwarderRetries)) - _dnsServer.ForwarderRetries = int.Parse(strForwarderRetries); - - string strForwarderTimeout = request.QueryString["forwarderTimeout"]; - if (!string.IsNullOrEmpty(strForwarderTimeout)) - _dnsServer.ForwarderTimeout = int.Parse(strForwarderTimeout); - - string strForwarderConcurrency = request.QueryString["forwarderConcurrency"]; - if (!string.IsNullOrEmpty(strForwarderConcurrency)) - _dnsServer.ForwarderConcurrency = int.Parse(strForwarderConcurrency); - - string strClientTimeout = request.QueryString["clientTimeout"]; - if (!string.IsNullOrEmpty(strClientTimeout)) - _dnsServer.ClientTimeout = int.Parse(strClientTimeout); - - string strTcpSendTimeout = request.QueryString["tcpSendTimeout"]; - if (!string.IsNullOrEmpty(strTcpSendTimeout)) - _dnsServer.TcpSendTimeout = int.Parse(strTcpSendTimeout); - - string strTcpReceiveTimeout = request.QueryString["tcpReceiveTimeout"]; - if (!string.IsNullOrEmpty(strTcpReceiveTimeout)) - _dnsServer.TcpReceiveTimeout = int.Parse(strTcpReceiveTimeout); - - string strEnableLogging = request.QueryString["enableLogging"]; - if (!string.IsNullOrEmpty(strEnableLogging)) - _log.EnableLogging = bool.Parse(strEnableLogging); - - string strLogQueries = request.QueryString["logQueries"]; - if (!string.IsNullOrEmpty(strLogQueries)) - { - if (bool.Parse(strLogQueries)) - _dnsServer.QueryLogManager = _log; - else - _dnsServer.QueryLogManager = null; - } - - string strUseLocalTime = request.QueryString["useLocalTime"]; - if (!string.IsNullOrEmpty(strUseLocalTime)) - _log.UseLocalTime = bool.Parse(strUseLocalTime); - - string strLogFolder = request.QueryString["logFolder"]; - if (!string.IsNullOrEmpty(strLogFolder)) - _log.LogFolder = strLogFolder; - - string strMaxLogFileDays = request.QueryString["maxLogFileDays"]; - if (!string.IsNullOrEmpty(strMaxLogFileDays)) - _log.MaxLogFileDays = int.Parse(strMaxLogFileDays); - - string strMaxStatFileDays = request.QueryString["maxStatFileDays"]; - if (!string.IsNullOrEmpty(strMaxStatFileDays)) - _dnsServer.StatsManager.MaxStatFileDays = int.Parse(strMaxStatFileDays); - - string strRecursion = request.QueryString["recursion"]; - if (!string.IsNullOrEmpty(strRecursion)) - _dnsServer.Recursion = Enum.Parse(strRecursion, true); - - string strRecursionDeniedNetworks = request.QueryString["recursionDeniedNetworks"]; - if (!string.IsNullOrEmpty(strRecursionDeniedNetworks)) - { - if (strRecursionDeniedNetworks == "false") - { - _dnsServer.RecursionDeniedNetworks = null; - } - else - { - string[] strNetworks = strRecursionDeniedNetworks.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - - NetworkAddress[] networks = new NetworkAddress[strNetworks.Length]; - - for (int i = 0; i < networks.Length; i++) - networks[i] = NetworkAddress.Parse(strNetworks[i]); - - _dnsServer.RecursionDeniedNetworks = networks; - } - } - - string strRecursionAllowedNetworks = request.QueryString["recursionAllowedNetworks"]; - if (!string.IsNullOrEmpty(strRecursionAllowedNetworks)) - { - if (strRecursionAllowedNetworks == "false") - { - _dnsServer.RecursionAllowedNetworks = null; - } - else - { - string[] strNetworks = strRecursionAllowedNetworks.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - - NetworkAddress[] networks = new NetworkAddress[strNetworks.Length]; - - for (int i = 0; i < networks.Length; i++) - networks[i] = NetworkAddress.Parse(strNetworks[i]); - - _dnsServer.RecursionAllowedNetworks = networks; - } - } - - string strRandomizeName = request.QueryString["randomizeName"]; - if (!string.IsNullOrEmpty(strRandomizeName)) - _dnsServer.RandomizeName = bool.Parse(strRandomizeName); - - string strQnameMinimization = request.QueryString["qnameMinimization"]; - if (!string.IsNullOrEmpty(strQnameMinimization)) - _dnsServer.QnameMinimization = bool.Parse(strQnameMinimization); - - string strNsRevalidation = request.QueryString["nsRevalidation"]; - if (!string.IsNullOrEmpty(strNsRevalidation)) - _dnsServer.NsRevalidation = bool.Parse(strNsRevalidation); - - string strQpmLimitRequests = request.QueryString["qpmLimitRequests"]; - if (!string.IsNullOrEmpty(strQpmLimitRequests)) - _dnsServer.QpmLimitRequests = int.Parse(strQpmLimitRequests); - - string strQpmLimitErrors = request.QueryString["qpmLimitErrors"]; - if (!string.IsNullOrEmpty(strQpmLimitErrors)) - _dnsServer.QpmLimitErrors = int.Parse(strQpmLimitErrors); - - string strQpmLimitSampleMinutes = request.QueryString["qpmLimitSampleMinutes"]; - if (!string.IsNullOrEmpty(strQpmLimitSampleMinutes)) - _dnsServer.QpmLimitSampleMinutes = int.Parse(strQpmLimitSampleMinutes); - - string strQpmLimitIPv4PrefixLength = request.QueryString["qpmLimitIPv4PrefixLength"]; - if (!string.IsNullOrEmpty(strQpmLimitIPv4PrefixLength)) - _dnsServer.QpmLimitIPv4PrefixLength = int.Parse(strQpmLimitIPv4PrefixLength); - - string strQpmLimitIPv6PrefixLength = request.QueryString["qpmLimitIPv6PrefixLength"]; - if (!string.IsNullOrEmpty(strQpmLimitIPv6PrefixLength)) - _dnsServer.QpmLimitIPv6PrefixLength = int.Parse(strQpmLimitIPv6PrefixLength); - - string strServeStale = request.QueryString["serveStale"]; - if (!string.IsNullOrEmpty(strServeStale)) - _dnsServer.ServeStale = bool.Parse(strServeStale); - - string strServeStaleTtl = request.QueryString["serveStaleTtl"]; - if (!string.IsNullOrEmpty(strServeStaleTtl)) - _dnsServer.CacheZoneManager.ServeStaleTtl = uint.Parse(strServeStaleTtl); - - string strCacheMaximumEntries = request.QueryString["cacheMaximumEntries"]; - if (!string.IsNullOrEmpty(strCacheMaximumEntries)) - _dnsServer.CacheZoneManager.MaximumEntries = long.Parse(strCacheMaximumEntries); - - string strCacheMinimumRecordTtl = request.QueryString["cacheMinimumRecordTtl"]; - if (!string.IsNullOrEmpty(strCacheMinimumRecordTtl)) - _dnsServer.CacheZoneManager.MinimumRecordTtl = uint.Parse(strCacheMinimumRecordTtl); - - string strCacheMaximumRecordTtl = request.QueryString["cacheMaximumRecordTtl"]; - if (!string.IsNullOrEmpty(strCacheMaximumRecordTtl)) - _dnsServer.CacheZoneManager.MaximumRecordTtl = uint.Parse(strCacheMaximumRecordTtl); - - string strCacheNegativeRecordTtl = request.QueryString["cacheNegativeRecordTtl"]; - if (!string.IsNullOrEmpty(strCacheNegativeRecordTtl)) - _dnsServer.CacheZoneManager.NegativeRecordTtl = uint.Parse(strCacheNegativeRecordTtl); - - string strCacheFailureRecordTtl = request.QueryString["cacheFailureRecordTtl"]; - if (!string.IsNullOrEmpty(strCacheFailureRecordTtl)) - _dnsServer.CacheZoneManager.FailureRecordTtl = uint.Parse(strCacheFailureRecordTtl); - - string strCachePrefetchEligibility = request.QueryString["cachePrefetchEligibility"]; - if (!string.IsNullOrEmpty(strCachePrefetchEligibility)) - _dnsServer.CachePrefetchEligibility = int.Parse(strCachePrefetchEligibility); - - string strCachePrefetchTrigger = request.QueryString["cachePrefetchTrigger"]; - if (!string.IsNullOrEmpty(strCachePrefetchTrigger)) - _dnsServer.CachePrefetchTrigger = int.Parse(strCachePrefetchTrigger); - - string strCachePrefetchSampleIntervalInMinutes = request.QueryString["cachePrefetchSampleIntervalInMinutes"]; - if (!string.IsNullOrEmpty(strCachePrefetchSampleIntervalInMinutes)) - _dnsServer.CachePrefetchSampleIntervalInMinutes = int.Parse(strCachePrefetchSampleIntervalInMinutes); - - string strCachePrefetchSampleEligibilityHitsPerHour = request.QueryString["cachePrefetchSampleEligibilityHitsPerHour"]; - if (!string.IsNullOrEmpty(strCachePrefetchSampleEligibilityHitsPerHour)) - _dnsServer.CachePrefetchSampleEligibilityHitsPerHour = int.Parse(strCachePrefetchSampleEligibilityHitsPerHour); - - string strProxyType = request.QueryString["proxyType"]; - if (!string.IsNullOrEmpty(strProxyType)) - { - NetProxyType proxyType = Enum.Parse(strProxyType, true); - if (proxyType == NetProxyType.None) - { - _dnsServer.Proxy = null; - } - else - { - NetworkCredential credential = null; - - string strUsername = request.QueryString["proxyUsername"]; - if (!string.IsNullOrEmpty(strUsername)) - credential = new NetworkCredential(strUsername, request.QueryString["proxyPassword"]); - - _dnsServer.Proxy = NetProxy.CreateProxy(proxyType, request.QueryString["proxyAddress"], int.Parse(request.QueryString["proxyPort"]), credential); - - string strProxyBypass = request.QueryString["proxyBypass"]; - if (!string.IsNullOrEmpty(strProxyBypass)) - { - string[] strBypassList = strProxyBypass.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - List bypassList = new List(strBypassList.Length); - - for (int i = 0; i < strBypassList.Length; i++) - bypassList.Add(new NetProxyBypassItem(strBypassList[i])); - - _dnsServer.Proxy.BypassList = bypassList; - } - } - } - - DnsTransportProtocol forwarderProtocol = DnsTransportProtocol.Udp; - string strForwarderProtocol = request.QueryString["forwarderProtocol"]; - if (!string.IsNullOrEmpty(strForwarderProtocol)) - forwarderProtocol = Enum.Parse(strForwarderProtocol, true); - - string strForwarders = request.QueryString["forwarders"]; - if (!string.IsNullOrEmpty(strForwarders)) - { - if (strForwarders == "false") - { - _dnsServer.Forwarders = null; - } - else - { - string[] strForwardersList = strForwarders.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - NameServerAddress[] forwarders = new NameServerAddress[strForwardersList.Length]; - - for (int i = 0; i < strForwardersList.Length; i++) - { - NameServerAddress forwarder = new NameServerAddress(strForwardersList[i]); - - if (forwarder.Protocol != forwarderProtocol) - forwarder = forwarder.ChangeProtocol(forwarderProtocol); - - forwarders[i] = forwarder; - } - - _dnsServer.Forwarders = forwarders; - } - } - - string strEnableBlocking = request.QueryString["enableBlocking"]; - if (!string.IsNullOrEmpty(strEnableBlocking)) - { - _dnsServer.EnableBlocking = bool.Parse(strEnableBlocking); - if (_dnsServer.EnableBlocking) - { - if (_temporaryDisableBlockingTimer is not null) - _temporaryDisableBlockingTimer.Dispose(); - } - } - - string strAllowTxtBlockingReport = request.QueryString["allowTxtBlockingReport"]; - if (!string.IsNullOrEmpty(strAllowTxtBlockingReport)) - _dnsServer.AllowTxtBlockingReport = bool.Parse(strAllowTxtBlockingReport); - - string strBlockingType = request.QueryString["blockingType"]; - if (!string.IsNullOrEmpty(strBlockingType)) - _dnsServer.BlockingType = Enum.Parse(strBlockingType, true); - - string strCustomBlockingAddresses = request.QueryString["customBlockingAddresses"]; - if (!string.IsNullOrEmpty(strCustomBlockingAddresses)) - { - if (strCustomBlockingAddresses == "false") - { - _dnsServer.CustomBlockingARecords = null; - _dnsServer.CustomBlockingAAAARecords = null; - } - else - { - string[] strAddresses = strCustomBlockingAddresses.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - - List dnsARecords = new List(); - List dnsAAAARecords = new List(); - - foreach (string strAddress in strAddresses) - { - if (IPAddress.TryParse(strAddress, out IPAddress customAddress)) - { - switch (customAddress.AddressFamily) - { - case AddressFamily.InterNetwork: - dnsARecords.Add(new DnsARecordData(customAddress)); - break; - - case AddressFamily.InterNetworkV6: - dnsAAAARecords.Add(new DnsAAAARecordData(customAddress)); - break; - } - } - } - - _dnsServer.CustomBlockingARecords = dnsARecords; - _dnsServer.CustomBlockingAAAARecords = dnsAAAARecords; - } - } - - bool blockListUrlsUpdated = false; - string strBlockListUrls = request.QueryString["blockListUrls"]; - if (!string.IsNullOrEmpty(strBlockListUrls)) - { - if (strBlockListUrls == "false") - { - _dnsServer.BlockListZoneManager.AllowListUrls.Clear(); - _dnsServer.BlockListZoneManager.BlockListUrls.Clear(); - _dnsServer.BlockListZoneManager.Flush(); - } - else - { - string[] strBlockListUrlList = strBlockListUrls.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - - if (oldWebServiceHttpPort != _webServiceHttpPort) - { - for (int i = 0; i < strBlockListUrlList.Length; i++) - { - if (strBlockListUrlList[i].Contains("http://localhost:" + oldWebServiceHttpPort + "/blocklist.txt")) - { - strBlockListUrlList[i] = "http://localhost:" + _webServiceHttpPort + "/blocklist.txt"; - blockListUrlsUpdated = true; - break; - } - } - } - - if (!blockListUrlsUpdated) - { - if (strBlockListUrlList.Length != (_dnsServer.BlockListZoneManager.AllowListUrls.Count + _dnsServer.BlockListZoneManager.BlockListUrls.Count)) - { - blockListUrlsUpdated = true; - } - else - { - foreach (string strBlockListUrl in strBlockListUrlList) - { - if (strBlockListUrl.StartsWith("!")) - { - string strAllowListUrl = strBlockListUrl.Substring(1); - - if (!_dnsServer.BlockListZoneManager.AllowListUrls.Contains(new Uri(strAllowListUrl))) - { - blockListUrlsUpdated = true; - break; - } - } - else - { - if (!_dnsServer.BlockListZoneManager.BlockListUrls.Contains(new Uri(strBlockListUrl))) - { - blockListUrlsUpdated = true; - break; - } - } - } - } - } - - if (blockListUrlsUpdated) - { - _dnsServer.BlockListZoneManager.AllowListUrls.Clear(); - _dnsServer.BlockListZoneManager.BlockListUrls.Clear(); - - foreach (string strBlockListUrl in strBlockListUrlList) - { - if (strBlockListUrl.StartsWith("!")) - { - Uri allowListUrl = new Uri(strBlockListUrl.Substring(1)); - - if (!_dnsServer.BlockListZoneManager.AllowListUrls.Contains(allowListUrl)) - _dnsServer.BlockListZoneManager.AllowListUrls.Add(allowListUrl); - } - else - { - Uri blockListUrl = new Uri(strBlockListUrl); - - if (!_dnsServer.BlockListZoneManager.BlockListUrls.Contains(blockListUrl)) - _dnsServer.BlockListZoneManager.BlockListUrls.Add(blockListUrl); - } - } - } - } - } - - string strBlockListUpdateIntervalHours = request.QueryString["blockListUpdateIntervalHours"]; - if (!string.IsNullOrEmpty(strBlockListUpdateIntervalHours)) - { - int blockListUpdateIntervalHours = int.Parse(strBlockListUpdateIntervalHours); - - if ((blockListUpdateIntervalHours < 0) || (blockListUpdateIntervalHours > 168)) - throw new DnsWebServiceException("Parameter `blockListUpdateIntervalHours` must be between 1 hour and 168 hours (7 days) or 0 to disable automatic update."); - - _blockListUpdateIntervalHours = blockListUpdateIntervalHours; - } - - if ((_blockListUpdateIntervalHours > 0) && ((_dnsServer.BlockListZoneManager.AllowListUrls.Count + _dnsServer.BlockListZoneManager.BlockListUrls.Count) > 0)) - { - if (blockListUrlsUpdated || (_blockListUpdateTimer is null)) - ForceUpdateBlockLists(); - - StartBlockListUpdateTimer(); - } - else - { - StopBlockListUpdateTimer(); - } - - if ((_webServiceTlsCertificatePath == null) && (_dnsTlsCertificatePath == null)) - StopTlsCertificateUpdateTimer(); - - SelfSignedCertCheck(serverDomainChanged, true); - - if (_webServiceEnableTls && string.IsNullOrEmpty(_webServiceTlsCertificatePath) && !_webServiceUseSelfSignedTlsCertificate) - { - //disable TLS - _webServiceEnableTls = false; - restartWebService = true; - } - - SaveConfigFile(); - _log.Save(); - - _log.Write(GetRequestRemoteEndPoint(request), "[" + GetSession(request).User.Username + "] DNS Settings were updated {dnsServerDomain: " + _dnsServer.ServerDomain + "; dnsServerLocalEndPoints: " + strDnsServerLocalEndPoints + "; webServiceLocalAddresses: " + strWebServiceLocalAddresses + "; webServiceHttpPort: " + _webServiceHttpPort + "; webServiceEnableTls: " + strWebServiceEnableTls + "; webServiceHttpToTlsRedirect: " + strWebServiceHttpToTlsRedirect + "; webServiceTlsPort: " + strWebServiceTlsPort + "; webServiceUseSelfSignedTlsCertificate: " + _webServiceUseSelfSignedTlsCertificate + "; webServiceTlsCertificatePath: " + strWebServiceTlsCertificatePath + "; enableDnsOverHttp: " + _dnsServer.EnableDnsOverHttp + "; enableDnsOverTls: " + _dnsServer.EnableDnsOverTls + "; enableDnsOverHttps: " + _dnsServer.EnableDnsOverHttps + "; dnsTlsCertificatePath: " + _dnsTlsCertificatePath + "; defaultRecordTtl: " + _zonesApi.DefaultRecordTtl + "; preferIPv6: " + _dnsServer.PreferIPv6 + "; enableLogging: " + strEnableLogging + "; logQueries: " + (_dnsServer.QueryLogManager != null) + "; useLocalTime: " + strUseLocalTime + "; logFolder: " + strLogFolder + "; maxLogFileDays: " + strMaxLogFileDays + "; recursion: " + _dnsServer.Recursion.ToString() + "; randomizeName: " + strRandomizeName + "; qnameMinimization: " + strQnameMinimization + "; serveStale: " + strServeStale + "; serveStaleTtl: " + strServeStaleTtl + "; cachePrefetchEligibility: " + strCachePrefetchEligibility + "; cachePrefetchTrigger: " + strCachePrefetchTrigger + "; cachePrefetchSampleIntervalInMinutes: " + strCachePrefetchSampleIntervalInMinutes + "; cachePrefetchSampleEligibilityHitsPerHour: " + strCachePrefetchSampleEligibilityHitsPerHour + "; proxyType: " + strProxyType + "; forwarders: " + strForwarders + "; forwarderProtocol: " + strForwarderProtocol + "; enableBlocking: " + _dnsServer.EnableBlocking + "; allowTxtBlockingReport: " + _dnsServer.AllowTxtBlockingReport + "; blockingType: " + _dnsServer.BlockingType.ToString() + "; blockListUrl: " + strBlockListUrls + "; blockListUpdateIntervalHours: " + strBlockListUpdateIntervalHours + ";}"); - - GetDnsSettings(jsonWriter); - - RestartService(restartDnsService, restartWebService); - } - - private void GetTsigKeyNames(JsonTextWriter jsonWriter) - { - jsonWriter.WritePropertyName("tsigKeyNames"); - { - jsonWriter.WriteStartArray(); - - if (_dnsServer.TsigKeys is not null) - { - foreach (KeyValuePair tsigKey in _dnsServer.TsigKeys) - jsonWriter.WriteValue(tsigKey.Key); - } - - jsonWriter.WriteEndArray(); - } - } - - private void SelfSignedCertCheck(bool generateNew, bool throwException) - { - string selfSignedCertificateFilePath = Path.Combine(_configFolder, "cert.pfx"); - - if (_webServiceUseSelfSignedTlsCertificate) - { - if (generateNew || !File.Exists(selfSignedCertificateFilePath)) - { - RSA rsa = RSA.Create(2048); - CertificateRequest req = new CertificateRequest("cn=" + _dnsServer.ServerDomain, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); - X509Certificate2 cert = req.CreateSelfSigned(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddYears(5)); - - File.WriteAllBytes(selfSignedCertificateFilePath, cert.Export(X509ContentType.Pkcs12, null as string)); - } - - if (_webServiceEnableTls && string.IsNullOrEmpty(_webServiceTlsCertificatePath)) - { - try - { - LoadWebServiceTlsCertificate(selfSignedCertificateFilePath, null); - } - catch (Exception ex) - { - _log.Write("DNS Server encountered an error while loading self signed Web Service TLS certificate: " + selfSignedCertificateFilePath + "\r\n" + ex.ToString()); - - if (throwException) - throw; - } - } - } - else - { - File.Delete(selfSignedCertificateFilePath); - } - } - - private void RestartService(bool restartDnsService, bool restartWebService) - { - if (restartDnsService) - { - _ = Task.Run(delegate () - { - _log.Write("Attempting to restart DNS service."); - - try - { - _dnsServer.Stop(); - _dnsServer.Start(); - - _log.Write("DNS service was restarted successfully."); - } - catch (Exception ex) - { - _log.Write("Failed to restart DNS service."); - _log.Write(ex); - } - }); - } - - if (restartWebService) - { - _ = Task.Run(async delegate () - { - await Task.Delay(2000); //wait for this HTTP response to be delivered before stopping web server - - _log.Write("Attempting to restart web service."); - - try - { - StopDnsWebService(); - StartDnsWebService(); - - _log.Write("Web service was restarted successfully."); - } - catch (Exception ex) - { - _log.Write("Failed to restart web service."); - _log.Write(ex); - } - }); - } - } - - private async Task BackupSettingsAsync(HttpListenerRequest request, HttpListenerResponse response) - { - bool blockLists = false; - bool logs = false; - bool scopes = false; - bool apps = false; - bool stats = false; - bool zones = false; - bool allowedZones = false; - bool blockedZones = false; - bool dnsSettings = false; - bool authConfig = false; - bool logSettings = false; - - string strBlockLists = request.QueryString["blockLists"]; - if (!string.IsNullOrEmpty(strBlockLists)) - blockLists = bool.Parse(strBlockLists); - - string strLogs = request.QueryString["logs"]; - if (!string.IsNullOrEmpty(strLogs)) - logs = bool.Parse(strLogs); - - string strScopes = request.QueryString["scopes"]; - if (!string.IsNullOrEmpty(strScopes)) - scopes = bool.Parse(strScopes); - - string strApps = request.QueryString["apps"]; - if (!string.IsNullOrEmpty(strApps)) - apps = bool.Parse(strApps); - - string strStats = request.QueryString["stats"]; - if (!string.IsNullOrEmpty(strStats)) - stats = bool.Parse(strStats); - - string strZones = request.QueryString["zones"]; - if (!string.IsNullOrEmpty(strZones)) - zones = bool.Parse(strZones); - - string strAllowedZones = request.QueryString["allowedZones"]; - if (!string.IsNullOrEmpty(strAllowedZones)) - allowedZones = bool.Parse(strAllowedZones); - - string strBlockedZones = request.QueryString["blockedZones"]; - if (!string.IsNullOrEmpty(strBlockedZones)) - blockedZones = bool.Parse(strBlockedZones); - - string strDnsSettings = request.QueryString["dnsSettings"]; - if (!string.IsNullOrEmpty(strDnsSettings)) - dnsSettings = bool.Parse(strDnsSettings); - - string strAuthConfig = request.QueryString["authConfig"]; - if (!string.IsNullOrEmpty(strAuthConfig)) - authConfig = bool.Parse(strAuthConfig); - - string strLogSettings = request.QueryString["logSettings"]; - if (!string.IsNullOrEmpty(strLogSettings)) - logSettings = bool.Parse(strLogSettings); - - string tmpFile = Path.GetTempFileName(); - try - { - using (FileStream backupZipStream = new FileStream(tmpFile, FileMode.Create, FileAccess.ReadWrite)) - { - //create backup zip - using (ZipArchive backupZip = new ZipArchive(backupZipStream, ZipArchiveMode.Create, true, Encoding.UTF8)) - { - if (blockLists) - { - string[] blockListFiles = Directory.GetFiles(Path.Combine(_configFolder, "blocklists"), "*", SearchOption.TopDirectoryOnly); - foreach (string blockListFile in blockListFiles) - { - string entryName = "blocklists/" + Path.GetFileName(blockListFile); - backupZip.CreateEntryFromFile(blockListFile, entryName); - } - } - - if (logs) - { - string[] logFiles = Directory.GetFiles(_log.LogFolderAbsolutePath, "*.log", SearchOption.TopDirectoryOnly); - foreach (string logFile in logFiles) - { - string entryName = "logs/" + Path.GetFileName(logFile); - - if (logFile.Equals(_log.CurrentLogFile, StringComparison.OrdinalIgnoreCase)) - { - await CreateBackupEntryFromFileAsync(backupZip, logFile, entryName); - } - else - { - backupZip.CreateEntryFromFile(logFile, entryName); - } - } - } - - if (scopes) - { - string[] scopeFiles = Directory.GetFiles(Path.Combine(_configFolder, "scopes"), "*.scope", SearchOption.TopDirectoryOnly); - foreach (string scopeFile in scopeFiles) - { - string entryName = "scopes/" + Path.GetFileName(scopeFile); - backupZip.CreateEntryFromFile(scopeFile, entryName); - } - } - - if (apps) - { - string[] appFiles = Directory.GetFiles(Path.Combine(_configFolder, "apps"), "*", SearchOption.AllDirectories); - foreach (string appFile in appFiles) - { - string entryName = appFile.Substring(_configFolder.Length); - - if (Path.DirectorySeparatorChar != '/') - entryName = entryName.Replace(Path.DirectorySeparatorChar, '/'); - - entryName = entryName.TrimStart('/'); - - await CreateBackupEntryFromFileAsync(backupZip, appFile, entryName); - } - } - - if (stats) - { - string[] hourlyStatsFiles = Directory.GetFiles(Path.Combine(_configFolder, "stats"), "*.stat", SearchOption.TopDirectoryOnly); - foreach (string hourlyStatsFile in hourlyStatsFiles) - { - string entryName = "stats/" + Path.GetFileName(hourlyStatsFile); - backupZip.CreateEntryFromFile(hourlyStatsFile, entryName); - } - - string[] dailyStatsFiles = Directory.GetFiles(Path.Combine(_configFolder, "stats"), "*.dstat", SearchOption.TopDirectoryOnly); - foreach (string dailyStatsFile in dailyStatsFiles) - { - string entryName = "stats/" + Path.GetFileName(dailyStatsFile); - backupZip.CreateEntryFromFile(dailyStatsFile, entryName); - } - } - - if (zones) - { - string[] zoneFiles = Directory.GetFiles(Path.Combine(_configFolder, "zones"), "*.zone", SearchOption.TopDirectoryOnly); - foreach (string zoneFile in zoneFiles) - { - string entryName = "zones/" + Path.GetFileName(zoneFile); - backupZip.CreateEntryFromFile(zoneFile, entryName); - } - } - - if (allowedZones) - { - string allowedZonesFile = Path.Combine(_configFolder, "allowed.config"); - - if (File.Exists(allowedZonesFile)) - backupZip.CreateEntryFromFile(allowedZonesFile, "allowed.config"); - } - - if (blockedZones) - { - string blockedZonesFile = Path.Combine(_configFolder, "blocked.config"); - - if (File.Exists(blockedZonesFile)) - backupZip.CreateEntryFromFile(blockedZonesFile, "blocked.config"); - } - - if (dnsSettings) - { - string dnsSettingsFile = Path.Combine(_configFolder, "dns.config"); - - if (File.Exists(dnsSettingsFile)) - backupZip.CreateEntryFromFile(dnsSettingsFile, "dns.config"); - } - - if (authConfig) - { - string authSettingsFile = Path.Combine(_configFolder, "auth.config"); - - if (File.Exists(authSettingsFile)) - backupZip.CreateEntryFromFile(authSettingsFile, "auth.config"); - } - - if (logSettings) - { - string logSettingsFile = Path.Combine(_configFolder, "log.config"); - - if (File.Exists(logSettingsFile)) - backupZip.CreateEntryFromFile(logSettingsFile, "log.config"); - } - } - - //send zip file - backupZipStream.Position = 0; - - response.ContentType = "application/zip"; - response.ContentLength64 = backupZipStream.Length; - response.AddHeader("Content-Disposition", "attachment;filename=DnsServerBackup.zip"); - - using (Stream output = response.OutputStream) - { - await backupZipStream.CopyToAsync(output); - } - } - } - finally - { - try - { - File.Delete(tmpFile); - } - catch (Exception ex) - { - _log.Write(ex); - } - } - - _log.Write(GetRequestRemoteEndPoint(request), "[" + GetSession(request).User.Username + "] Settings backup zip file was exported."); - } - - private static async Task CreateBackupEntryFromFileAsync(ZipArchive backupZip, string sourceFileName, string entryName) - { - using (FileStream fS = new FileStream(sourceFileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) - { - ZipArchiveEntry entry = backupZip.CreateEntry(entryName); - - DateTime lastWrite = File.GetLastWriteTime(sourceFileName); - - // If file to be archived has an invalid last modified time, use the first datetime representable in the Zip timestamp format - // (midnight on January 1, 1980): - if (lastWrite.Year < 1980 || lastWrite.Year > 2107) - lastWrite = new DateTime(1980, 1, 1, 0, 0, 0); - - entry.LastWriteTime = lastWrite; - - using (Stream sE = entry.Open()) - { - await fS.CopyToAsync(sE); - } - } - } - - private async Task RestoreSettingsAsync(HttpListenerRequest request, JsonTextWriter jsonWriter) - { - bool blockLists = false; - bool logs = false; - bool scopes = false; - bool apps = false; - bool stats = false; - bool zones = false; - bool allowedZones = false; - bool blockedZones = false; - bool dnsSettings = false; - bool authConfig = false; - bool logSettings = false; - - bool deleteExistingFiles = false; - - string strBlockLists = request.QueryString["blockLists"]; - if (!string.IsNullOrEmpty(strBlockLists)) - blockLists = bool.Parse(strBlockLists); - - string strLogs = request.QueryString["logs"]; - if (!string.IsNullOrEmpty(strLogs)) - logs = bool.Parse(strLogs); - - string strScopes = request.QueryString["scopes"]; - if (!string.IsNullOrEmpty(strScopes)) - scopes = bool.Parse(strScopes); - - string strApps = request.QueryString["apps"]; - if (!string.IsNullOrEmpty(strApps)) - apps = bool.Parse(strApps); - - string strStats = request.QueryString["stats"]; - if (!string.IsNullOrEmpty(strStats)) - stats = bool.Parse(strStats); - - string strZones = request.QueryString["zones"]; - if (!string.IsNullOrEmpty(strZones)) - zones = bool.Parse(strZones); - - string strAllowedZones = request.QueryString["allowedZones"]; - if (!string.IsNullOrEmpty(strAllowedZones)) - allowedZones = bool.Parse(strAllowedZones); - - string strBlockedZones = request.QueryString["blockedZones"]; - if (!string.IsNullOrEmpty(strBlockedZones)) - blockedZones = bool.Parse(strBlockedZones); - - string strDnsSettings = request.QueryString["dnsSettings"]; - if (!string.IsNullOrEmpty(strDnsSettings)) - dnsSettings = bool.Parse(strDnsSettings); - - string strAuthConfig = request.QueryString["authConfig"]; - if (!string.IsNullOrEmpty(strAuthConfig)) - authConfig = bool.Parse(strAuthConfig); - - string strLogSettings = request.QueryString["logSettings"]; - if (!string.IsNullOrEmpty(strLogSettings)) - logSettings = bool.Parse(strLogSettings); - - string strDeleteExistingFiles = request.QueryString["deleteExistingFiles"]; - if (!string.IsNullOrEmpty(strDeleteExistingFiles)) - deleteExistingFiles = bool.Parse(strDeleteExistingFiles); - - #region skip to content - - int crlfCount = 0; - int byteRead; - - while (crlfCount != 4) - { - byteRead = await request.InputStream.ReadByteValueAsync(); - switch (byteRead) - { - case 13: //CR - case 10: //LF - crlfCount++; - break; - - default: - crlfCount = 0; - break; - } - } - - #endregion - - //write to temp file - string tmpFile = Path.GetTempFileName(); - try - { - using (FileStream fS = new FileStream(tmpFile, FileMode.Create, FileAccess.ReadWrite)) - { - await request.InputStream.CopyToAsync(fS); - - fS.Position = 0; - using (ZipArchive backupZip = new ZipArchive(fS, ZipArchiveMode.Read, false, Encoding.UTF8)) - { - UserSession session = GetSession(request); - - if (logSettings || logs) - { - //stop logging - _log.StopLogging(); - } - - try - { - if (logSettings) - { - ZipArchiveEntry entry = backupZip.GetEntry("log.config"); - if (entry != null) - entry.ExtractToFile(Path.Combine(_configFolder, entry.Name), true); - - //reload config - _log.LoadConfig(); - } - - if (logs) - { - if (deleteExistingFiles) - { - //delete existing log files - string[] logFiles = Directory.GetFiles(_log.LogFolderAbsolutePath, "*.log", SearchOption.TopDirectoryOnly); - foreach (string logFile in logFiles) - { - File.Delete(logFile); - } - } - - //extract log files from backup - foreach (ZipArchiveEntry entry in backupZip.Entries) - { - if (entry.FullName.StartsWith("logs/")) - entry.ExtractToFile(Path.Combine(_log.LogFolderAbsolutePath, entry.Name), true); - } - } - } - finally - { - if (logSettings || logs) - { - //start logging - if (_log.EnableLogging) - _log.StartLogging(); - } - } - - if (authConfig) - { - ZipArchiveEntry entry = backupZip.GetEntry("auth.config"); - if (entry != null) - entry.ExtractToFile(Path.Combine(_configFolder, entry.Name), true); - - //reload auth config - _authManager.LoadConfigFile(session); - } - - if (dnsSettings) - { - ZipArchiveEntry entry = backupZip.GetEntry("dns.config"); - if (entry != null) - entry.ExtractToFile(Path.Combine(_configFolder, entry.Name), true); - - //reload settings and block list zone - LoadConfigFile(); - _dnsServer.BlockListZoneManager.LoadBlockLists(); - } - - if (blockLists) - { - if (deleteExistingFiles) - { - //delete existing block list files - string[] blockListFiles = Directory.GetFiles(Path.Combine(_configFolder, "blocklists"), "*", SearchOption.TopDirectoryOnly); - foreach (string blockListFile in blockListFiles) - { - File.Delete(blockListFile); - } - } - - //extract block list files from backup - foreach (ZipArchiveEntry entry in backupZip.Entries) - { - if (entry.FullName.StartsWith("blocklists/")) - entry.ExtractToFile(Path.Combine(_configFolder, "blocklists", entry.Name), true); - } - } - - if (apps) - { - //unload apps - _dnsServer.DnsApplicationManager.UnloadAllApplications(); - - if (deleteExistingFiles) - { - //delete existing apps - string appFolder = Path.Combine(_configFolder, "apps"); - if (Directory.Exists(appFolder)) - Directory.Delete(appFolder, true); - - //create apps folder - Directory.CreateDirectory(appFolder); - } - - //extract apps files from backup - foreach (ZipArchiveEntry entry in backupZip.Entries) - { - if (entry.FullName.StartsWith("apps/")) - { - string entryPath = entry.FullName; - - if (Path.DirectorySeparatorChar != '/') - entryPath = entryPath.Replace('/', '\\'); - - string filePath = Path.Combine(_configFolder, entryPath); - - Directory.CreateDirectory(Path.GetDirectoryName(filePath)); - - entry.ExtractToFile(filePath, true); - } - } - - //reload apps - _dnsServer.DnsApplicationManager.LoadAllApplications(); - } - - if (zones) - { - if (deleteExistingFiles) - { - //delete existing zone files - string[] zoneFiles = Directory.GetFiles(Path.Combine(_configFolder, "zones"), "*.zone", SearchOption.TopDirectoryOnly); - foreach (string zoneFile in zoneFiles) - { - File.Delete(zoneFile); - } - } - - //extract zone files from backup - foreach (ZipArchiveEntry entry in backupZip.Entries) - { - if (entry.FullName.StartsWith("zones/")) - entry.ExtractToFile(Path.Combine(_configFolder, "zones", entry.Name), true); - } - - //reload zones - _dnsServer.AuthZoneManager.LoadAllZoneFiles(); - InspectAndFixZonePermissions(); - } - - if (allowedZones) - { - ZipArchiveEntry entry = backupZip.GetEntry("allowed.config"); - if (entry == null) - { - string fileName = Path.Combine(_configFolder, "allowed.config"); - if (File.Exists(fileName)) - File.Delete(fileName); - } - else - { - entry.ExtractToFile(Path.Combine(_configFolder, entry.Name), true); - } - - //reload - _dnsServer.AllowedZoneManager.LoadAllowedZoneFile(); - } - - if (blockedZones) - { - ZipArchiveEntry entry = backupZip.GetEntry("blocked.config"); - if (entry == null) - { - string fileName = Path.Combine(_configFolder, "allowed.config"); - if (File.Exists(fileName)) - File.Delete(fileName); - } - else - { - entry.ExtractToFile(Path.Combine(_configFolder, entry.Name), true); - } - - //reload - _dnsServer.BlockedZoneManager.LoadBlockedZoneFile(); - } - - if (scopes) - { - //stop dhcp server - _dhcpServer.Stop(); - - try - { - if (deleteExistingFiles) - { - //delete existing scope files - string[] scopeFiles = Directory.GetFiles(Path.Combine(_configFolder, "scopes"), "*.scope", SearchOption.TopDirectoryOnly); - foreach (string scopeFile in scopeFiles) - { - File.Delete(scopeFile); - } - } - - //extract scope files from backup - foreach (ZipArchiveEntry entry in backupZip.Entries) - { - if (entry.FullName.StartsWith("scopes/")) - entry.ExtractToFile(Path.Combine(_configFolder, "scopes", entry.Name), true); - } - } - finally - { - //start dhcp server - _dhcpServer.Start(); - } - } - - if (stats) - { - if (deleteExistingFiles) - { - //delete existing stats files - string[] hourlyStatsFiles = Directory.GetFiles(Path.Combine(_configFolder, "stats"), "*.stat", SearchOption.TopDirectoryOnly); - foreach (string hourlyStatsFile in hourlyStatsFiles) - { - File.Delete(hourlyStatsFile); - } - - string[] dailyStatsFiles = Directory.GetFiles(Path.Combine(_configFolder, "stats"), "*.dstat", SearchOption.TopDirectoryOnly); - foreach (string dailyStatsFile in dailyStatsFiles) - { - File.Delete(dailyStatsFile); - } - } - - //extract stats files from backup - foreach (ZipArchiveEntry entry in backupZip.Entries) - { - if (entry.FullName.StartsWith("stats/")) - entry.ExtractToFile(Path.Combine(_configFolder, "stats", entry.Name), true); - } - - //reload stats - _dnsServer.StatsManager.ReloadStats(); - } - - _log.Write(GetRequestRemoteEndPoint(request), "[" + session.User.Username + "] Settings backup zip file was restored."); - } - } - } - finally - { - try - { - File.Delete(tmpFile); - } - catch (Exception ex) - { - _log.Write(ex); - } - } - - if (dnsSettings) - RestartService(true, true); - - GetDnsSettings(jsonWriter); - } - - private void ForceUpdateBlockLists(HttpListenerRequest request) - { - ForceUpdateBlockLists(); - _log.Write(GetRequestRemoteEndPoint(request), "[" + GetSession(request).User.Username + "] Block list update was triggered."); - } - - private void TemporaryDisableBlocking(HttpListenerRequest request, JsonTextWriter jsonWriter) - { - string strMinutes = request.QueryString["minutes"]; - if (string.IsNullOrEmpty(strMinutes)) - throw new DnsWebServiceException("Parameter 'minutes' missing."); - - int minutes = int.Parse(strMinutes); - - Timer temporaryDisableBlockingTimer = _temporaryDisableBlockingTimer; - if (temporaryDisableBlockingTimer is not null) - temporaryDisableBlockingTimer.Dispose(); - - Timer newTemporaryDisableBlockingTimer = new Timer(delegate (object state) - { - try - { - _dnsServer.EnableBlocking = true; - _log.Write(GetRequestRemoteEndPoint(request), "[" + GetSession(request).User.Username + "] Blocking was enabled after " + minutes + " minute(s) being temporarily disabled."); - } - catch (Exception ex) - { - _log.Write(ex); - } - }); - - Timer originalTimer = Interlocked.CompareExchange(ref _temporaryDisableBlockingTimer, newTemporaryDisableBlockingTimer, temporaryDisableBlockingTimer); - if (ReferenceEquals(originalTimer, temporaryDisableBlockingTimer)) - { - newTemporaryDisableBlockingTimer.Change(minutes * 60 * 1000, Timeout.Infinite); - _dnsServer.EnableBlocking = false; - _temporaryDisableBlockingTill = DateTime.UtcNow.AddMinutes(minutes); - - _log.Write(GetRequestRemoteEndPoint(request), "[" + GetSession(request).User.Username + "] Blocking was temporarily disabled for " + minutes + " minute(s)."); - } - else - { - newTemporaryDisableBlockingTimer.Dispose(); - } - - jsonWriter.WritePropertyName("temporaryDisableBlockingTill"); - jsonWriter.WriteValue(_temporaryDisableBlockingTill); - } - - #endregion - - #region dns client api - - private async Task ResolveQueryAsync(HttpListenerRequest request, JsonTextWriter jsonWriter) - { - string server = request.QueryString["server"]; - if (string.IsNullOrEmpty(server)) - throw new DnsWebServiceException("Parameter 'server' missing."); - - string domain = request.QueryString["domain"]; - if (string.IsNullOrEmpty(domain)) - throw new DnsWebServiceException("Parameter 'domain' missing."); - - domain = domain.Trim(new char[] { '\t', ' ', '.' }); - - string strType = request.QueryString["type"]; - if (string.IsNullOrEmpty(strType)) - throw new DnsWebServiceException("Parameter 'type' missing."); - - DnsResourceRecordType type = Enum.Parse(strType, true); - - string strProtocol = request.QueryString["protocol"]; - if (string.IsNullOrEmpty(strProtocol)) - strProtocol = "Udp"; - - bool dnssecValidation = false; - string strDnssecValidation = request.QueryString["dnssec"]; - if (!string.IsNullOrEmpty(strDnssecValidation)) - dnssecValidation = bool.Parse(strDnssecValidation); - - bool importResponse = false; - string strImport = request.QueryString["import"]; - if (!string.IsNullOrEmpty(strImport)) - importResponse = bool.Parse(strImport); - - NetProxy proxy = _dnsServer.Proxy; - bool preferIPv6 = _dnsServer.PreferIPv6; - ushort udpPayloadSize = _dnsServer.UdpPayloadSize; - bool randomizeName = false; - bool qnameMinimization = _dnsServer.QnameMinimization; - DnsTransportProtocol protocol = Enum.Parse(strProtocol, true); - const int RETRIES = 1; - const int TIMEOUT = 10000; - - DnsDatagram dnsResponse; - string dnssecErrorMessage = null; - - if (server.Equals("recursive-resolver", StringComparison.OrdinalIgnoreCase)) - { - if (type == DnsResourceRecordType.AXFR) - throw new DnsServerException("Cannot do zone transfer (AXFR) for 'recursive-resolver'."); - - DnsQuestionRecord question; - - if ((type == DnsResourceRecordType.PTR) && IPAddress.TryParse(domain, out IPAddress address)) - question = new DnsQuestionRecord(address, DnsClass.IN); - else - question = new DnsQuestionRecord(domain, type, DnsClass.IN); - - DnsCache dnsCache = new DnsCache(); - dnsCache.MinimumRecordTtl = 0; - dnsCache.MaximumRecordTtl = 7 * 24 * 60 * 60; - - try - { - dnsResponse = await DnsClient.RecursiveResolveAsync(question, dnsCache, proxy, preferIPv6, udpPayloadSize, randomizeName, qnameMinimization, false, dnssecValidation, null, RETRIES, TIMEOUT); - } - catch (DnsClientResponseDnssecValidationException ex) - { - dnsResponse = ex.Response; - dnssecErrorMessage = ex.Message; - importResponse = false; - } - } - else - { - if ((type == DnsResourceRecordType.AXFR) && (protocol == DnsTransportProtocol.Udp)) - protocol = DnsTransportProtocol.Tcp; - - NameServerAddress nameServer; - - if (server.Equals("this-server", StringComparison.OrdinalIgnoreCase)) - { - switch (protocol) - { - case DnsTransportProtocol.Udp: - nameServer = _dnsServer.ThisServer; - break; - - case DnsTransportProtocol.Tcp: - nameServer = _dnsServer.ThisServer.ChangeProtocol(DnsTransportProtocol.Tcp); - break; - - case DnsTransportProtocol.Tls: - throw new DnsServerException("Cannot use DNS-over-TLS protocol for 'this-server'. Please use the TLS certificate domain name as the server."); - - case DnsTransportProtocol.Https: - throw new DnsServerException("Cannot use DNS-over-HTTPS protocol for 'this-server'. Please use the TLS certificate domain name with a url as the server."); - - case DnsTransportProtocol.HttpsJson: - throw new DnsServerException("Cannot use DNS-over-HTTPS (JSON) protocol for 'this-server'. Please use the TLS certificate domain name with a url as the server."); - - default: - throw new InvalidOperationException(); - } - - proxy = null; //no proxy required for this server - } - else - { - nameServer = new NameServerAddress(server); - - if (nameServer.Protocol != protocol) - nameServer = nameServer.ChangeProtocol(protocol); - - if (nameServer.IsIPEndPointStale) - { - if (proxy is null) - await nameServer.ResolveIPAddressAsync(_dnsServer, _dnsServer.PreferIPv6); - } - else if ((nameServer.DomainEndPoint is null) && ((protocol == DnsTransportProtocol.Udp) || (protocol == DnsTransportProtocol.Tcp))) - { - try - { - await nameServer.ResolveDomainNameAsync(_dnsServer); - } - catch - { } - } - } - - DnsClient dnsClient = new DnsClient(nameServer); - - dnsClient.Proxy = proxy; - dnsClient.PreferIPv6 = preferIPv6; - dnsClient.RandomizeName = randomizeName; - dnsClient.Retries = RETRIES; - dnsClient.Timeout = TIMEOUT; - dnsClient.UdpPayloadSize = udpPayloadSize; - dnsClient.DnssecValidation = dnssecValidation; - - if (dnssecValidation) - { - //load trust anchors into dns client if domain is locally hosted - _dnsServer.AuthZoneManager.LoadTrustAnchorsTo(dnsClient, domain, type); - } - - try - { - dnsResponse = await dnsClient.ResolveAsync(domain, type); - } - catch (DnsClientResponseDnssecValidationException ex) - { - dnsResponse = ex.Response; - dnssecErrorMessage = ex.Message; - importResponse = false; - } - - if (type == DnsResourceRecordType.AXFR) - dnsResponse = dnsResponse.Join(); - } - - if (importResponse) - { - UserSession session = GetSession(request); - - AuthZoneInfo zoneInfo = _dnsServer.AuthZoneManager.FindAuthZoneInfo(domain); - if ((zoneInfo is null) || ((zoneInfo.Type == AuthZoneType.Secondary) && !zoneInfo.Name.Equals(domain, StringComparison.OrdinalIgnoreCase))) - { - if (!_authManager.IsPermitted(PermissionSection.Zones, session.User, PermissionFlag.Modify)) - throw new DnsWebServiceException("Access was denied."); - - zoneInfo = _dnsServer.AuthZoneManager.CreatePrimaryZone(domain, _dnsServer.ServerDomain, false); - if (zoneInfo is null) - throw new DnsServerException("Cannot import records: failed to create primary zone."); - - //set permissions - _authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, session.User, PermissionFlag.ViewModifyDelete); - _authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _authManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete); - _authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _authManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.ViewModifyDelete); - _authManager.SaveConfigFile(); - } - else - { - if (!_authManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, session.User, PermissionFlag.Modify)) - throw new DnsWebServiceException("Access was denied."); - - switch (zoneInfo.Type) - { - case AuthZoneType.Primary: - break; - - case AuthZoneType.Forwarder: - if (type == DnsResourceRecordType.AXFR) - throw new DnsServerException("Cannot import records via zone transfer: import zone must be of primary type."); - - break; - - default: - throw new DnsServerException("Cannot import records: import zone must be of primary or forwarder type."); - } - } - - if (type == DnsResourceRecordType.AXFR) - { - _dnsServer.AuthZoneManager.SyncZoneTransferRecords(zoneInfo.Name, dnsResponse.Answer); - } - else - { - List importRecords = new List(dnsResponse.Answer.Count + dnsResponse.Authority.Count); - - foreach (DnsResourceRecord record in dnsResponse.Answer) - { - if (record.Name.Equals(zoneInfo.Name, StringComparison.OrdinalIgnoreCase) || record.Name.EndsWith("." + zoneInfo.Name, StringComparison.OrdinalIgnoreCase) || (zoneInfo.Name.Length == 0)) - { - record.RemoveExpiry(); - importRecords.Add(record); - - if (record.Type == DnsResourceRecordType.NS) - record.SyncGlueRecords(dnsResponse.Additional); - } - } - - foreach (DnsResourceRecord record in dnsResponse.Authority) - { - if (record.Name.Equals(zoneInfo.Name, StringComparison.OrdinalIgnoreCase) || record.Name.EndsWith("." + zoneInfo.Name, StringComparison.OrdinalIgnoreCase) || (zoneInfo.Name.Length == 0)) - { - record.RemoveExpiry(); - importRecords.Add(record); - - if (record.Type == DnsResourceRecordType.NS) - record.SyncGlueRecords(dnsResponse.Additional); - } - } - - _dnsServer.AuthZoneManager.ImportRecords(zoneInfo.Name, importRecords); - } - - _log.Write(GetRequestRemoteEndPoint(request), "[" + session.User.Username + "] DNS Client imported record(s) for authoritative zone {server: " + server + "; zone: " + zoneInfo.Name + "; type: " + type + ";}"); - - _dnsServer.AuthZoneManager.SaveZoneFile(zoneInfo.Name); - } - - if (dnssecErrorMessage is not null) - { - jsonWriter.WritePropertyName("warningMessage"); - jsonWriter.WriteValue(dnssecErrorMessage); - } - - jsonWriter.WritePropertyName("result"); - jsonWriter.WriteRawValue(JsonConvert.SerializeObject(dnsResponse, new StringEnumConverter())); - } - - #endregion - - #region block list - - private void ForceUpdateBlockLists() - { - Task.Run(async delegate () - { - if (await _dnsServer.BlockListZoneManager.UpdateBlockListsAsync()) - { - //block lists were updated - //save last updated on time - _blockListLastUpdatedOn = DateTime.UtcNow; - SaveConfigFile(); - } }); } - private void StartBlockListUpdateTimer() + private bool TryGetSession(HttpContext context, out UserSession session) { - if (_blockListUpdateTimer is null) - { - _blockListUpdateTimer = new Timer(async delegate (object state) - { - try - { - if (DateTime.UtcNow > _blockListLastUpdatedOn.AddHours(_blockListUpdateIntervalHours)) - { - if (await _dnsServer.BlockListZoneManager.UpdateBlockListsAsync()) - { - //block lists were updated - //save last updated on time - _blockListLastUpdatedOn = DateTime.UtcNow; - SaveConfigFile(); - } - } - } - catch (Exception ex) - { - _log.Write("DNS Server encountered an error while updating block lists.\r\n" + ex.ToString()); - } + string token = context.Request.GetQueryOrForm("token"); + session = _authManager.GetSession(token); + if ((session is null) || session.User.Disabled) + return false; - }, null, BLOCK_LIST_UPDATE_TIMER_INITIAL_INTERVAL, BLOCK_LIST_UPDATE_TIMER_PERIODIC_INTERVAL); - } - } - - private void StopBlockListUpdateTimer() - { - if (_blockListUpdateTimer is not null) + if (session.HasExpired()) { - _blockListUpdateTimer.Dispose(); - _blockListUpdateTimer = null; + _authManager.DeleteSession(session.Token); + _authManager.SaveConfigFile(); + return false; } + + IPEndPoint remoteEP = context.GetRemoteEndPoint(); + + session.UpdateLastSeen(remoteEP.Address, context.Request.Headers.UserAgent); + return true; } #endregion #region tls - private void StartTlsCertificateUpdateTimer() + internal void StartTlsCertificateUpdateTimer() { - if (_tlsCertificateUpdateTimer == null) + if (_tlsCertificateUpdateTimer is null) { _tlsCertificateUpdateTimer = new Timer(delegate (object state) { @@ -3740,16 +655,16 @@ namespace DnsServerCore } } - private void StopTlsCertificateUpdateTimer() + internal void StopTlsCertificateUpdateTimer() { - if (_tlsCertificateUpdateTimer != null) + if (_tlsCertificateUpdateTimer is not null) { _tlsCertificateUpdateTimer.Dispose(); _tlsCertificateUpdateTimer = null; } } - private void LoadWebServiceTlsCertificate(string tlsCertificatePath, string tlsCertificatePassword) + internal void LoadWebServiceTlsCertificate(string tlsCertificatePath, string tlsCertificatePassword) { FileInfo fileInfo = new FileInfo(tlsCertificatePath); @@ -3759,15 +674,13 @@ namespace DnsServerCore if (Path.GetExtension(tlsCertificatePath) != ".pfx") throw new ArgumentException("Web Service TLS certificate file must be PKCS #12 formatted with .pfx extension: " + tlsCertificatePath); - X509Certificate2 certificate = new X509Certificate2(tlsCertificatePath, tlsCertificatePassword); - - _webServiceTlsCertificate = certificate; + _webServiceTlsCertificate = new X509Certificate2(tlsCertificatePath, tlsCertificatePassword); _webServiceTlsCertificateLastModifiedOn = fileInfo.LastWriteTimeUtc; _log.Write("Web Service TLS certificate was loaded: " + tlsCertificatePath); } - private void LoadDnsTlsCertificate(string tlsCertificatePath, string tlsCertificatePassword) + internal void LoadDnsTlsCertificate(string tlsCertificatePath, string tlsCertificatePassword) { FileInfo fileInfo = new FileInfo(tlsCertificatePath); @@ -3777,19 +690,69 @@ namespace DnsServerCore if (Path.GetExtension(tlsCertificatePath) != ".pfx") throw new ArgumentException("DNS Server TLS certificate file must be PKCS #12 formatted with .pfx extension: " + tlsCertificatePath); - X509Certificate2 certificate = new X509Certificate2(tlsCertificatePath, tlsCertificatePassword); - - _dnsServer.Certificate = certificate; + _dnsServer.Certificate = new X509Certificate2(tlsCertificatePath, tlsCertificatePassword); _dnsTlsCertificateLastModifiedOn = fileInfo.LastWriteTimeUtc; _log.Write("DNS Server TLS certificate was loaded: " + tlsCertificatePath); } + internal void SelfSignedCertCheck(bool generateNew, bool throwException) + { + string selfSignedCertificateFilePath = Path.Combine(_configFolder, "cert.pfx"); + + if (_webServiceUseSelfSignedTlsCertificate) + { + if (generateNew || !File.Exists(selfSignedCertificateFilePath)) + { + RSA rsa = RSA.Create(2048); + CertificateRequest req = new CertificateRequest("cn=" + _dnsServer.ServerDomain, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + X509Certificate2 cert = req.CreateSelfSigned(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddYears(5)); + + File.WriteAllBytes(selfSignedCertificateFilePath, cert.Export(X509ContentType.Pkcs12, null as string)); + } + + if (_webServiceEnableTls && string.IsNullOrEmpty(_webServiceTlsCertificatePath)) + { + try + { + LoadWebServiceTlsCertificate(selfSignedCertificateFilePath, null); + } + catch (Exception ex) + { + _log.Write("DNS Server encountered an error while loading self signed Web Service TLS certificate: " + selfSignedCertificateFilePath + "\r\n" + ex.ToString()); + + if (throwException) + throw; + } + } + } + else + { + File.Delete(selfSignedCertificateFilePath); + } + } + + #endregion + + #region quic + + internal static void ValidateQuicSupport() + { +#pragma warning disable CA2252 // This API requires opting into preview features +#pragma warning disable CA1416 // Validate platform compatibility + + if (!QuicConnection.IsSupported) + throw new DnsWebServiceException("DNS-over-QUIC is supported only on Windows 11, Windows Server 2022, and Linux. On Linux, you must install 'libmsquic' and OpenSSL v1.1.1 manually."); + +#pragma warning restore CA1416 // Validate platform compatibility +#pragma warning restore CA2252 // This API requires opting into preview features + } + #endregion #region config - private void LoadConfigFile() + internal void LoadConfigFile() { string configFile = Path.Combine(_configFolder, "dns.config"); @@ -3840,27 +803,11 @@ namespace DnsServerCore string strRecursionDeniedNetworks = Environment.GetEnvironmentVariable("DNS_SERVER_RECURSION_DENIED_NETWORKS"); if (!string.IsNullOrEmpty(strRecursionDeniedNetworks)) - { - string[] strRecursionDeniedNetworkAddresses = strRecursionDeniedNetworks.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - NetworkAddress[] networks = new NetworkAddress[strRecursionDeniedNetworkAddresses.Length]; - - for (int i = 0; i < networks.Length; i++) - networks[i] = NetworkAddress.Parse(strRecursionDeniedNetworkAddresses[i].Trim()); - - _dnsServer.RecursionDeniedNetworks = networks; - } + _dnsServer.RecursionDeniedNetworks = strRecursionDeniedNetworks.Split(NetworkAddress.Parse, ','); string strRecursionAllowedNetworks = Environment.GetEnvironmentVariable("DNS_SERVER_RECURSION_ALLOWED_NETWORKS"); if (!string.IsNullOrEmpty(strRecursionAllowedNetworks)) - { - string[] strRecursionAllowedNetworkAddresses = strRecursionAllowedNetworks.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - NetworkAddress[] networks = new NetworkAddress[strRecursionAllowedNetworkAddresses.Length]; - - for (int i = 0; i < networks.Length; i++) - networks[i] = NetworkAddress.Parse(strRecursionAllowedNetworkAddresses[i].Trim()); - - _dnsServer.RecursionAllowedNetworks = networks; - } + _dnsServer.RecursionAllowedNetworks = strRecursionAllowedNetworks.Split(NetworkAddress.Parse, ','); _dnsServer.RandomizeName = true; //default true to enable security feature _dnsServer.QnameMinimization = true; //default true to enable privacy feature @@ -3910,24 +857,25 @@ namespace DnsServerCore string strForwarderProtocol = Environment.GetEnvironmentVariable("DNS_SERVER_FORWARDER_PROTOCOL"); if (string.IsNullOrEmpty(strForwarderProtocol)) - forwarderProtocol = DnsTransportProtocol.Udp; - else - forwarderProtocol = Enum.Parse(strForwarderProtocol, true); - - List forwarders = new List(); - string[] strForwardersAddresses = strForwarders.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - - foreach (string strForwarderAddress in strForwardersAddresses) { - NameServerAddress forwarder = new NameServerAddress(strForwarderAddress.Trim()); + forwarderProtocol = DnsTransportProtocol.Udp; + } + else + { + forwarderProtocol = Enum.Parse(strForwarderProtocol, true); + if (forwarderProtocol == DnsTransportProtocol.HttpsJson) + forwarderProtocol = DnsTransportProtocol.Https; + } + + _dnsServer.Forwarders = strForwarders.Split(delegate (string value) + { + NameServerAddress forwarder = NameServerAddress.Parse(value); if (forwarder.Protocol != forwarderProtocol) forwarder = forwarder.ChangeProtocol(forwarderProtocol); - forwarders.Add(forwarder); - } - - _dnsServer.Forwarders = forwarders; + return forwarder; + }, ','); } //logging @@ -3965,7 +913,7 @@ namespace DnsServerCore } } - private void SaveConfigFile() + internal void SaveConfigFile() { string configFile = Path.Combine(_configFolder, "dns.config"); @@ -3986,7 +934,7 @@ namespace DnsServerCore _log.Write("DNS Server config file was saved: " + configFile); } - private void InspectAndFixZonePermissions() + internal void InspectAndFixZonePermissions() { Permission permission = _authManager.GetPermission(PermissionSection.Zones); IReadOnlyDictionary subItemPermissions = permission.SubItemPermissions; @@ -4001,7 +949,7 @@ namespace DnsServerCore } //add missing admin permissions - List zones = _dnsServer.AuthZoneManager.ListZones(); + IReadOnlyList zones = _dnsServer.AuthZoneManager.GetAllZones(); Group admins = _authManager.GetGroup(Group.ADMINISTRATORS); Group dnsAdmins = _authManager.GetGroup(Group.DNS_ADMINISTRATORS); @@ -4029,7 +977,7 @@ namespace DnsServerCore int version = bR.ReadByte(); - if ((version >= 28) && (version <= 29)) + if ((version >= 28) && (version <= 31)) { ReadConfigFrom(bR, version); } @@ -4062,7 +1010,7 @@ namespace DnsServerCore IPAddress[] localAddresses = new IPAddress[count]; for (int i = 0; i < count; i++) - localAddresses[i] = IPAddressExtension.ReadFrom(bR); + localAddresses[i] = IPAddressExtensions.ReadFrom(bR); _webServiceLocalAddresses = localAddresses; } @@ -4107,7 +1055,7 @@ namespace DnsServerCore IPEndPoint[] localEndPoints = new IPEndPoint[count]; for (int i = 0; i < count; i++) - localEndPoints[i] = (IPEndPoint)EndPointExtension.ReadFrom(bR); + localEndPoints[i] = (IPEndPoint)EndPointExtensions.ReadFrom(bR); _dnsServer.LocalEndPoints = localEndPoints; } @@ -4143,11 +1091,66 @@ namespace DnsServerCore _dnsServer.TcpSendTimeout = bR.ReadInt32(); _dnsServer.TcpReceiveTimeout = bR.ReadInt32(); + if (version >= 30) + { + _dnsServer.QuicIdleTimeout = bR.ReadInt32(); + _dnsServer.QuicMaxInboundStreams = bR.ReadInt32(); + _dnsServer.ListenBacklog = bR.ReadInt32(); + } + else + { + _dnsServer.QuicIdleTimeout = 60000; + _dnsServer.QuicMaxInboundStreams = 100; + _dnsServer.ListenBacklog = 100; + } + //optional protocols _dnsServer.EnableDnsOverHttp = bR.ReadBoolean(); _dnsServer.EnableDnsOverTls = bR.ReadBoolean(); _dnsServer.EnableDnsOverHttps = bR.ReadBoolean(); + if (version >= 31) + { + _dnsServer.EnableDnsOverQuic = bR.ReadBoolean(); + + _dnsServer.DnsOverHttpPort = bR.ReadInt32(); + _dnsServer.DnsOverTlsPort = bR.ReadInt32(); + _dnsServer.DnsOverHttpsPort = bR.ReadInt32(); + _dnsServer.DnsOverQuicPort = bR.ReadInt32(); + } + else if (version >= 30) + { + _ = bR.ReadBoolean(); //removed EnableDnsOverHttpPort80 value + _dnsServer.EnableDnsOverQuic = bR.ReadBoolean(); + + _dnsServer.DnsOverHttpPort = bR.ReadInt32(); + _dnsServer.DnsOverTlsPort = bR.ReadInt32(); + _dnsServer.DnsOverHttpsPort = bR.ReadInt32(); + _dnsServer.DnsOverQuicPort = bR.ReadInt32(); + } + else + { + _dnsServer.EnableDnsOverQuic = false; + + if (_dnsServer.EnableDnsOverHttps) + { + _dnsServer.EnableDnsOverHttp = true; + _dnsServer.DnsOverHttpPort = 80; + } + else if (_dnsServer.EnableDnsOverHttp) + { + _dnsServer.DnsOverHttpPort = 8053; + } + else + { + _dnsServer.DnsOverHttpPort = 80; + } + + _dnsServer.DnsOverTlsPort = 853; + _dnsServer.DnsOverHttpsPort = 443; + _dnsServer.DnsOverQuicPort = 853; + } + _dnsTlsCertificatePath = bR.ReadShortString(); _dnsTlsCertificatePassword = bR.ReadShortString(); @@ -4223,6 +1226,11 @@ namespace DnsServerCore _dnsServer.ResolverMaxStackCount = bR.ReadInt32(); //cache + if (version >= 30) + _saveCache = bR.ReadBoolean(); + else + _saveCache = false; + _dnsServer.ServeStale = bR.ReadBoolean(); _dnsServer.CacheZoneManager.ServeStaleTtl = bR.ReadUInt32(); @@ -4253,7 +1261,7 @@ namespace DnsServerCore for (int i = 0; i < count; i++) { - IPAddress customAddress = IPAddressExtension.ReadFrom(bR); + IPAddress customAddress = IPAddressExtensions.ReadFrom(bR); switch (customAddress.AddressFamily) { @@ -4286,8 +1294,8 @@ namespace DnsServerCore _dnsServer.BlockListZoneManager.BlockListUrls.Add(new Uri(listUrl)); } - _blockListUpdateIntervalHours = bR.ReadInt32(); - _blockListLastUpdatedOn = bR.ReadDateTime(); + _settingsApi.BlockListUpdateIntervalHours = bR.ReadInt32(); + _settingsApi.BlockListLastUpdatedOn = bR.ReadDateTime(); } //proxy & forwarders @@ -4323,8 +1331,13 @@ namespace DnsServerCore NameServerAddress[] forwarders = new NameServerAddress[count]; for (int i = 0; i < count; i++) + { forwarders[i] = new NameServerAddress(bR); + if (forwarders[i].Protocol == DnsTransportProtocol.HttpsJson) + forwarders[i] = forwarders[i].ChangeProtocol(DnsTransportProtocol.Https); + } + _dnsServer.Forwarders = forwarders; } } @@ -4360,7 +1373,7 @@ namespace DnsServerCore IPAddress[] localAddresses = new IPAddress[count]; for (int i = 0; i < count; i++) - localAddresses[i] = IPAddressExtension.ReadFrom(bR); + localAddresses[i] = IPAddressExtensions.ReadFrom(bR); _webServiceLocalAddresses = localAddresses; } @@ -4562,7 +1575,11 @@ namespace DnsServerCore NameServerAddress[] forwarders = new NameServerAddress[count]; for (int i = 0; i < count; i++) + { forwarders[i] = new NameServerAddress(bR); + if (forwarders[i].Protocol == DnsTransportProtocol.HttpsJson) + forwarders[i] = forwarders[i].ChangeProtocol(DnsTransportProtocol.Https); + } _dnsServer.Forwarders = forwarders; } @@ -4571,6 +1588,8 @@ namespace DnsServerCore if (version <= 10) { DnsTransportProtocol forwarderProtocol = (DnsTransportProtocol)bR.ReadByte(); + if (forwarderProtocol == DnsTransportProtocol.HttpsJson) + forwarderProtocol = DnsTransportProtocol.Https; if (_dnsServer.Forwarders != null) { @@ -4658,7 +1677,7 @@ namespace DnsServerCore for (int i = 0; i < count; i++) { - IPAddress customAddress = IPAddressExtension.ReadFrom(bR); + IPAddress customAddress = IPAddressExtensions.ReadFrom(bR); switch (customAddress.AddressFamily) { @@ -4697,17 +1716,17 @@ namespace DnsServerCore _dnsServer.BlockListZoneManager.BlockListUrls.Add(new Uri(listUrl)); } - _blockListLastUpdatedOn = bR.ReadDateTime(); + _settingsApi.BlockListLastUpdatedOn = bR.ReadDateTime(); if (version >= 13) - _blockListUpdateIntervalHours = bR.ReadInt32(); + _settingsApi.BlockListUpdateIntervalHours = bR.ReadInt32(); } else { _dnsServer.BlockListZoneManager.AllowListUrls.Clear(); _dnsServer.BlockListZoneManager.BlockListUrls.Clear(); - _blockListLastUpdatedOn = DateTime.MinValue; - _blockListUpdateIntervalHours = 24; + _settingsApi.BlockListLastUpdatedOn = DateTime.MinValue; + _settingsApi.BlockListUpdateIntervalHours = 24; } if (version >= 11) @@ -4718,7 +1737,7 @@ namespace DnsServerCore IPEndPoint[] localEndPoints = new IPEndPoint[count]; for (int i = 0; i < count; i++) - localEndPoints[i] = (IPEndPoint)EndPointExtension.ReadFrom(bR); + localEndPoints[i] = (IPEndPoint)EndPointExtensions.ReadFrom(bR); _dnsServer.LocalEndPoints = localEndPoints; } @@ -4731,7 +1750,7 @@ namespace DnsServerCore IPEndPoint[] localEndPoints = new IPEndPoint[count]; for (int i = 0; i < count; i++) - localEndPoints[i] = new IPEndPoint(IPAddressExtension.ReadFrom(bR), 53); + localEndPoints[i] = new IPEndPoint(IPAddressExtensions.ReadFrom(bR), 53); _dnsServer.LocalEndPoints = localEndPoints; } @@ -4901,7 +1920,7 @@ namespace DnsServerCore private void WriteConfigTo(BinaryWriter bW) { bW.Write(Encoding.ASCII.GetBytes("DS")); //format - bW.Write((byte)29); //version + bW.Write((byte)31); //version //web service { @@ -4962,11 +1981,20 @@ namespace DnsServerCore bW.Write(_dnsServer.ClientTimeout); bW.Write(_dnsServer.TcpSendTimeout); bW.Write(_dnsServer.TcpReceiveTimeout); + bW.Write(_dnsServer.QuicIdleTimeout); + bW.Write(_dnsServer.QuicMaxInboundStreams); + bW.Write(_dnsServer.ListenBacklog); //optional protocols bW.Write(_dnsServer.EnableDnsOverHttp); bW.Write(_dnsServer.EnableDnsOverTls); bW.Write(_dnsServer.EnableDnsOverHttps); + bW.Write(_dnsServer.EnableDnsOverQuic); + + bW.Write(_dnsServer.DnsOverHttpPort); + bW.Write(_dnsServer.DnsOverTlsPort); + bW.Write(_dnsServer.DnsOverHttpsPort); + bW.Write(_dnsServer.DnsOverQuicPort); if (_dnsTlsCertificatePath == null) bW.WriteShortString(string.Empty); @@ -5029,6 +2057,7 @@ namespace DnsServerCore bW.Write(_dnsServer.ResolverMaxStackCount); //cache + bW.Write(_saveCache); bW.Write(_dnsServer.ServeStale); bW.Write(_dnsServer.CacheZoneManager.ServeStaleTtl); @@ -5068,8 +2097,8 @@ namespace DnsServerCore foreach (Uri blockListUrl in _dnsServer.BlockListZoneManager.BlockListUrls) bW.WriteShortString(blockListUrl.AbsoluteUri); - bW.Write(_blockListUpdateIntervalHours); - bW.Write(_blockListLastUpdatedOn); + bW.Write(_settingsApi.BlockListUpdateIntervalHours); + bW.Write(_settingsApi.BlockListLastUpdatedOn); } //proxy & forwarders @@ -5129,168 +2158,12 @@ namespace DnsServerCore #endregion - #region web service start stop - - private void StartDnsWebService() - { - int acceptTasks = Math.Max(1, Environment.ProcessorCount); - - //HTTP service - try - { - string webServiceHostname = null; - - _webService = new HttpListener(); - IPAddress httpAddress = null; - - foreach (IPAddress webServiceLocalAddress in _webServiceLocalAddresses) - { - string host; - - if (webServiceLocalAddress.Equals(IPAddress.Any)) - { - host = "+"; - - httpAddress = IPAddress.Loopback; - } - else if (webServiceLocalAddress.Equals(IPAddress.IPv6Any)) - { - host = "+"; - - if ((httpAddress == null) || !IPAddress.IsLoopback(httpAddress)) - httpAddress = IPAddress.IPv6Loopback; - } - else - { - if (webServiceLocalAddress.AddressFamily == AddressFamily.InterNetworkV6) - host = "[" + webServiceLocalAddress.ToString() + "]"; - else - host = webServiceLocalAddress.ToString(); - - if (httpAddress == null) - httpAddress = webServiceLocalAddress; - - if (webServiceHostname == null) - webServiceHostname = host; - } - - _webService.Prefixes.Add("http://" + host + ":" + _webServiceHttpPort + "/"); - } - - _webService.Start(); - - if (httpAddress == null) - httpAddress = IPAddress.Loopback; - - _webServiceHttpEP = new IPEndPoint(httpAddress, _webServiceHttpPort); - - _webServiceHostname = webServiceHostname ?? Environment.MachineName.ToLower(); - } - catch (Exception ex) - { - _log.Write("Web Service failed to bind using default hostname. Attempting to bind again using 'localhost' hostname.\r\n" + ex.ToString()); - - try - { - _webService = new HttpListener(); - _webService.Prefixes.Add("http://localhost:" + _webServiceHttpPort + "/"); - _webService.Prefixes.Add("http://127.0.0.1:" + _webServiceHttpPort + "/"); - _webService.Start(); - } - catch - { - _webService = new HttpListener(); - _webService.Prefixes.Add("http://localhost:" + _webServiceHttpPort + "/"); - _webService.Start(); - } - - _webServiceHttpEP = new IPEndPoint(IPAddress.Loopback, _webServiceHttpPort); - - _webServiceHostname = "localhost"; - } - - _webService.IgnoreWriteExceptions = true; - - for (int i = 0; i < acceptTasks; i++) - { - _ = Task.Factory.StartNew(delegate () - { - return AcceptWebRequestAsync(); - }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, _webServiceTaskScheduler); - } - - _log.Write(new IPEndPoint(IPAddress.Any, _webServiceHttpPort), "HTTP Web Service was started successfully."); - - //TLS service - if (_webServiceEnableTls && (_webServiceTlsCertificate != null)) - { - List webServiceTlsListeners = new List(); - - try - { - foreach (IPAddress webServiceLocalAddress in _webServiceLocalAddresses) - { - Socket tlsListener = new Socket(webServiceLocalAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp); - - tlsListener.Bind(new IPEndPoint(webServiceLocalAddress, _webServiceTlsPort)); - tlsListener.Listen(10); - - webServiceTlsListeners.Add(tlsListener); - } - - foreach (Socket tlsListener in webServiceTlsListeners) - { - for (int i = 0; i < acceptTasks; i++) - { - _ = Task.Factory.StartNew(delegate () - { - return AcceptTlsWebRequestAsync(tlsListener); - }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, _webServiceTaskScheduler); - } - } - - _webServiceTlsListeners = webServiceTlsListeners; - - _log.Write(new IPEndPoint(IPAddress.Any, _webServiceHttpPort), "TLS Web Service was started successfully."); - } - catch (Exception ex) - { - _log.Write("TLS Web Service failed to start.\r\n" + ex.ToString()); - - foreach (Socket tlsListener in webServiceTlsListeners) - tlsListener.Dispose(); - } - } - } - - private void StopDnsWebService() - { - _webService.Stop(); - - if (_webServiceTlsListeners != null) - { - foreach (Socket tlsListener in _webServiceTlsListeners) - tlsListener.Dispose(); - - _webServiceTlsListeners = null; - } - } - - #endregion - - #endregion - #region public - public void Start() + public async Task StartAsync() { if (_disposed) - throw new ObjectDisposedException("WebService"); - - if (_state != ServiceState.Stopped) - throw new InvalidOperationException("Web Service is already running."); - - _state = ServiceState.Starting; + throw new ObjectDisposedException(nameof(DnsWebService)); try { @@ -5339,14 +2212,14 @@ namespace DnsServerCore _dnsServer.BlockedZoneManager.LoadBlockedZoneFile(); //load block list zone async - if ((_blockListUpdateIntervalHours > 0) && (_dnsServer.BlockListZoneManager.BlockListUrls.Count > 0)) + if ((_settingsApi.BlockListUpdateIntervalHours > 0) && (_dnsServer.BlockListZoneManager.BlockListUrls.Count > 0)) { ThreadPool.QueueUserWorkItem(delegate (object state) { try { _dnsServer.BlockListZoneManager.LoadBlockLists(); - StartBlockListUpdateTimer(); + _settingsApi.StartBlockListUpdateTimer(); } catch (Exception ex) { @@ -5355,14 +2228,28 @@ namespace DnsServerCore }); } - //start dns and dhcp - _dnsServer.Start(); - _dhcpServer.Start(); + //load dns cache async + if (_saveCache) + { + ThreadPool.QueueUserWorkItem(delegate (object state) + { + try + { + _dnsServer.CacheZoneManager.LoadCacheZoneFile(); + } + catch (Exception ex) + { + _log.Write(ex); + } + }); + } //start web service - StartDnsWebService(); + await TryStartWebServiceAsync(); - _state = ServiceState.Running; + //start dns and dhcp + await _dnsServer.StartAsync(); + _dhcpServer.Start(); _log.Write("DNS Server (v" + _currentVersion.ToString() + ") was started successfully."); } @@ -5373,58 +2260,73 @@ namespace DnsServerCore } } - public void Stop() + public async Task StopAsync() { - if (_state != ServiceState.Running) + if (_disposed) return; - _state = ServiceState.Stopping; - try { - StopDnsWebService(); - _dnsServer.Dispose(); - _dhcpServer.Dispose(); + //stop dns + if (_dnsServer is not null) + await _dnsServer.DisposeAsync(); + + //stop dhcp + if (_dhcpServer is not null) + _dhcpServer.Dispose(); + + //stop web service + if (_settingsApi is not null) + { + _settingsApi.StopBlockListUpdateTimer(); + _settingsApi.StopTemporaryDisableBlockingTimer(); + } - StopBlockListUpdateTimer(); StopTlsCertificateUpdateTimer(); - if (_temporaryDisableBlockingTimer is not null) - _temporaryDisableBlockingTimer.Dispose(); + await StopWebServiceAsync(); - _state = ServiceState.Stopped; + if (_saveCache) + { + try + { + _dnsServer.CacheZoneManager.SaveCacheZoneFile(); + } + catch (Exception ex) + { + _log.Write(ex); + } + } - _log.Write("DNS Server (v" + _currentVersion.ToString() + ") was stopped successfully."); + _log?.Write("DNS Server (v" + _currentVersion.ToString() + ") was stopped successfully."); } catch (Exception ex) { - _log.Write("Failed to stop DNS Server (v" + _currentVersion.ToString() + ")\r\n" + ex.ToString()); + _log?.Write("Failed to stop DNS Server (v" + _currentVersion.ToString() + ")\r\n" + ex.ToString()); throw; } } + public void Start() + { + StartAsync().Sync(); + } + + public void Stop() + { + StopAsync().Sync(); + } + #endregion #region properties - internal LogManager Log - { get { return _log; } } - - internal AuthManager AuthManager - { get { return _authManager; } } - - internal WebServiceZonesApi ZonesApi - { get { return _zonesApi; } } - internal DnsServer DnsServer { get { return _dnsServer; } } internal DhcpServer DhcpServer { get { return _dhcpServer; } } - internal Version ServerVersion - { get { return _currentVersion; } } - public string ConfigFolder { get { return _configFolder; } } @@ -5434,9 +2336,6 @@ namespace DnsServerCore public int WebServiceTlsPort { get { return _webServiceTlsPort; } } - public string WebServiceHostname - { get { return _webServiceHostname; } } - #endregion } } diff --git a/DnsServerCore/Extensions.cs b/DnsServerCore/Extensions.cs new file mode 100644 index 00000000..630bbfef --- /dev/null +++ b/DnsServerCore/Extensions.cs @@ -0,0 +1,236 @@ +/* +Technitium DNS Server +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +using DnsServerCore.Auth; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using System; +using System.Net; +using System.Text.Json; +using TechnitiumLibrary.Net; + +namespace DnsServerCore +{ + static class Extensions + { + readonly static string[] HTTP_METHODS = new string[] { "GET", "POST" }; + + public static IPEndPoint GetRemoteEndPoint(this HttpContext context) + { + try + { + IPAddress remoteIP = context.Connection.RemoteIpAddress; + if (remoteIP is null) + return new IPEndPoint(IPAddress.Any, 0); + + if (NetUtilities.IsPrivateIP(remoteIP)) + { + string xRealIp = context.Request.Headers["X-Real-IP"]; + if (IPAddress.TryParse(xRealIp, out IPAddress address)) + { + //get the real IP address of the requesting client from X-Real-IP header set in nginx proxy_pass block + return new IPEndPoint(address, 0); + } + } + + return new IPEndPoint(remoteIP, context.Connection.RemotePort); + } + catch + { + return new IPEndPoint(IPAddress.Any, 0); + } + } + + public static UserSession GetCurrentSession(this HttpContext context) + { + if (context.Items["session"] is UserSession userSession) + return userSession; + + throw new InvalidOperationException(); + } + + public static Utf8JsonWriter GetCurrentJsonWriter(this HttpContext context) + { + if (context.Items["jsonWriter"] is Utf8JsonWriter jsonWriter) + return jsonWriter; + + throw new InvalidOperationException(); + } + + public static IEndpointConventionBuilder MapGetAndPost(this IEndpointRouteBuilder endpoints, string pattern, RequestDelegate requestDelegate) + { + return endpoints.MapMethods(pattern, HTTP_METHODS, requestDelegate); + } + + public static IEndpointConventionBuilder MapGetAndPost(this IEndpointRouteBuilder endpoints, string pattern, Delegate handler) + { + return endpoints.MapMethods(pattern, HTTP_METHODS, handler); + } + + public static string QueryOrForm(this HttpRequest request, string parameter) + { + string value = request.Query[parameter]; + if ((value is null) && request.HasFormContentType) + value = request.Form[parameter]; + + return value; + } + + public static string GetQueryOrForm(this HttpRequest request, string parameter) + { + string value = request.QueryOrForm(parameter); + if (string.IsNullOrEmpty(value)) + throw new DnsWebServiceException("Parameter '" + parameter + "' missing."); + + return value; + } + + public static string GetQueryOrForm(this HttpRequest request, string parameter, string defaultValue) + { + string value = request.QueryOrForm(parameter); + if (string.IsNullOrEmpty(value)) + return defaultValue; + + return value; + } + + public static T GetQueryOrForm(this HttpRequest request, string parameter, Func parse) + { + string value = request.QueryOrForm(parameter); + if (string.IsNullOrEmpty(value)) + throw new DnsWebServiceException("Parameter '" + parameter + "' missing."); + + return parse(value); + } + + public static T GetQueryOrFormEnum(this HttpRequest request, string parameter) where T : struct + { + string value = request.QueryOrForm(parameter); + if (string.IsNullOrEmpty(value)) + throw new DnsWebServiceException("Parameter '" + parameter + "' missing."); + + return Enum.Parse(value, true); + } + + public static T GetQueryOrForm(this HttpRequest request, string parameter, Func parse, T defaultValue) + { + string value = request.QueryOrForm(parameter); + if (string.IsNullOrEmpty(value)) + return defaultValue; + + return parse(value); + } + + public static T GetQueryOrFormEnum(this HttpRequest request, string parameter, T defaultValue) where T : struct + { + string value = request.QueryOrForm(parameter); + if (string.IsNullOrEmpty(value)) + return defaultValue; + + return Enum.Parse(value, true); + } + + public static bool TryGetQueryOrForm(this HttpRequest request, string parameter, out string value) + { + value = request.QueryOrForm(parameter); + if (string.IsNullOrEmpty(value)) + return false; + + return true; + } + + public static bool TryGetQueryOrForm(this HttpRequest request, string parameter, Func parse, out T value) + { + string strValue = request.QueryOrForm(parameter); + if (string.IsNullOrEmpty(strValue)) + { + value = default; + return false; + } + + value = parse(strValue); + return true; + } + + public static bool TryGetQueryOrFormEnum(this HttpRequest request, string parameter, out T value) where T : struct + { + string strValue = request.QueryOrForm(parameter); + if (string.IsNullOrEmpty(strValue)) + { + value = default; + return false; + } + + return Enum.TryParse(strValue, true, out value); + } + + public static string GetQueryOrFormAlt(this HttpRequest request, string parameter, string alternateParameter) + { + string value = request.QueryOrForm(parameter); + if (string.IsNullOrEmpty(value)) + { + value = request.QueryOrForm(alternateParameter); + if (string.IsNullOrEmpty(value)) + throw new DnsWebServiceException("Parameter '" + parameter + "' missing."); + } + + return value; + } + + public static string GetQueryOrFormAlt(this HttpRequest request, string parameter, string alternateParameter, string defaultValue) + { + string value = request.QueryOrForm(parameter); + if (string.IsNullOrEmpty(value)) + { + value = request.QueryOrForm(alternateParameter); + if (string.IsNullOrEmpty(value)) + return defaultValue; + } + + return value; + } + + public static T GetQueryOrFormAlt(this HttpRequest request, string parameter, string alternateParameter, Func parse) + { + string value = request.QueryOrForm(parameter); + if (string.IsNullOrEmpty(value)) + { + value = request.QueryOrForm(alternateParameter); + if (string.IsNullOrEmpty(value)) + throw new DnsWebServiceException("Parameter '" + parameter + "' missing."); + } + + return parse(value); + } + + public static T GetQueryOrFormAlt(this HttpRequest request, string parameter, string alternateParameter, Func parse, T defaultValue) + { + string value = request.QueryOrForm(parameter); + if (string.IsNullOrEmpty(value)) + { + value = request.QueryOrForm(alternateParameter); + if (string.IsNullOrEmpty(value)) + return defaultValue; + } + + return parse(value); + } + } +} diff --git a/DnsServerCore/LogManager.cs b/DnsServerCore/LogManager.cs index 4a7f224d..4ba1994a 100644 --- a/DnsServerCore/LogManager.cs +++ b/DnsServerCore/LogManager.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -17,10 +17,13 @@ along with this program. If not, see . */ +using Microsoft.AspNetCore.Http; using System; using System.Collections.Concurrent; using System.Globalization; using System.IO; +using System.IO.Compression; +using System.Linq; using System.Net; using System.Text; using System.Threading; @@ -434,29 +437,60 @@ namespace DnsServerCore return Directory.GetFiles(ConvertToAbsolutePath(_logFolder), "*.log", SearchOption.TopDirectoryOnly); } - public async Task DownloadLogAsync(HttpListenerRequest request, HttpListenerResponse response, string logName, long limit) + public async Task DownloadLogAsync(HttpContext context, string logName, long limit) { string logFileName = logName + ".log"; using (FileStream fS = new FileStream(Path.Combine(ConvertToAbsolutePath(_logFolder), logFileName), FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 64 * 1024, true)) { + HttpResponse response = context.Response; + response.ContentType = "text/plain"; - response.AddHeader("Content-Disposition", "attachment;filename=" + logFileName); + response.Headers.ContentDisposition = "attachment;filename=" + logFileName; if ((limit > fS.Length) || (limit < 1)) limit = fS.Length; OffsetStream oFS = new OffsetStream(fS, 0, limit); + HttpRequest request = context.Request; + Stream s; - using (Stream s = DnsWebService.GetOutputStream(request, response)) + string acceptEncoding = request.Headers["Accept-Encoding"]; + if (string.IsNullOrEmpty(acceptEncoding)) + { + s = response.Body; + } + else + { + string[] acceptEncodingParts = acceptEncoding.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + + if (acceptEncodingParts.Contains("br")) + { + response.Headers.ContentEncoding = "br"; + s = new BrotliStream(response.Body, CompressionMode.Compress); + } + else if (acceptEncodingParts.Contains("gzip")) + { + response.Headers.ContentEncoding = "gzip"; + s = new GZipStream(response.Body, CompressionMode.Compress); + } + else if (acceptEncodingParts.Contains("deflate")) + { + response.Headers.ContentEncoding = "deflate"; + s = new DeflateStream(response.Body, CompressionMode.Compress); + } + else + { + s = response.Body; + } + } + + await using (s) { await oFS.CopyToAsync(s); if (fS.Length > limit) - { - byte[] buffer = Encoding.UTF8.GetBytes("\r\n####___TRUNCATED___####"); - s.Write(buffer, 0, buffer.Length); - } + await s.WriteAsync(Encoding.UTF8.GetBytes("\r\n####___TRUNCATED___####")); } } } @@ -592,7 +626,7 @@ namespace DnsServerCore EDnsClientSubnetOptionData responseECS = response.GetEDnsClientSubnetOption(); if (responseECS is not null) - answer += "; ECS: " + responseECS.Address.ToString() + "/" + responseECS.SourcePrefixLength; + answer += "; ECS: " + responseECS.Address.ToString() + "/" + responseECS.ScopePrefixLength; responseInfo = " RCODE: " + response.RCODE.ToString() + "; ANSWER: " + answer; } diff --git a/DnsServerCore/WebServiceApi.cs b/DnsServerCore/WebServiceApi.cs new file mode 100644 index 00000000..7c940cc1 --- /dev/null +++ b/DnsServerCore/WebServiceApi.cs @@ -0,0 +1,384 @@ +/* +Technitium DNS Server +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +using DnsServerCore.Auth; +using DnsServerCore.Dns; +using DnsServerCore.Dns.ResourceRecords; +using DnsServerCore.Dns.Zones; +using Microsoft.AspNetCore.Http; +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Text.Json; +using System.Threading.Tasks; +using TechnitiumLibrary; +using TechnitiumLibrary.Net.Dns; +using TechnitiumLibrary.Net.Dns.ResourceRecords; +using TechnitiumLibrary.Net.Proxy; + +namespace DnsServerCore +{ + class WebServiceApi + { + #region variables + + readonly DnsWebService _dnsWebService; + readonly Uri _updateCheckUri; + + string _checkForUpdateJsonData; + DateTime _checkForUpdateJsonDataUpdatedOn; + const int CHECK_FOR_UPDATE_JSON_DATA_CACHE_TIME_SECONDS = 3600; + + #endregion + + #region constructor + + public WebServiceApi(DnsWebService dnsWebService, Uri updateCheckUri) + { + _dnsWebService = dnsWebService; + _updateCheckUri = updateCheckUri; + } + + #endregion + + #region private + + private async Task GetCheckForUpdateJsonData() + { + if ((_checkForUpdateJsonData is null) || (DateTime.UtcNow > _checkForUpdateJsonDataUpdatedOn.AddSeconds(CHECK_FOR_UPDATE_JSON_DATA_CACHE_TIME_SECONDS))) + { + SocketsHttpHandler handler = new SocketsHttpHandler(); + handler.Proxy = _dnsWebService.DnsServer.Proxy; + handler.UseProxy = _dnsWebService.DnsServer.Proxy is not null; + handler.AutomaticDecompression = DecompressionMethods.All; + + using (HttpClient http = new HttpClient(handler)) + { + _checkForUpdateJsonData = await http.GetStringAsync(_updateCheckUri); + _checkForUpdateJsonDataUpdatedOn = DateTime.UtcNow; + } + } + + return _checkForUpdateJsonData; + } + + #endregion + + #region public + + public async Task CheckForUpdateAsync(HttpContext context) + { + Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); + + if (_updateCheckUri is null) + { + jsonWriter.WriteBoolean("updateAvailable", false); + return; + } + + try + { + string jsonData = await GetCheckForUpdateJsonData(); + using JsonDocument jsonDocument = JsonDocument.Parse(jsonData); + JsonElement jsonResponse = jsonDocument.RootElement; + + string updateVersion = jsonResponse.GetProperty("updateVersion").GetString(); + string updateTitle = jsonResponse.GetPropertyValue("updateTitle", null); + string updateMessage = jsonResponse.GetPropertyValue("updateMessage", null); + string downloadLink = jsonResponse.GetPropertyValue("downloadLink", null); + string instructionsLink = jsonResponse.GetPropertyValue("instructionsLink", null); + string changeLogLink = jsonResponse.GetPropertyValue("changeLogLink", null); + + bool updateAvailable = new Version(updateVersion) > _dnsWebService._currentVersion; + + jsonWriter.WriteBoolean("updateAvailable", updateAvailable); + jsonWriter.WriteString("updateVersion", updateVersion); + jsonWriter.WriteString("currentVersion", _dnsWebService.GetServerVersion()); + + if (updateAvailable) + { + jsonWriter.WriteString("updateTitle", updateTitle); + jsonWriter.WriteString("updateMessage", updateMessage); + jsonWriter.WriteString("downloadLink", downloadLink); + jsonWriter.WriteString("instructionsLink", instructionsLink); + jsonWriter.WriteString("changeLogLink", changeLogLink); + } + + string strLog = "Check for update was done {updateAvailable: " + updateAvailable + "; updateVersion: " + updateVersion + ";"; + + if (!string.IsNullOrEmpty(updateTitle)) + strLog += " updateTitle: " + updateTitle + ";"; + + if (!string.IsNullOrEmpty(updateMessage)) + strLog += " updateMessage: " + updateMessage + ";"; + + if (!string.IsNullOrEmpty(downloadLink)) + strLog += " downloadLink: " + downloadLink + ";"; + + if (!string.IsNullOrEmpty(instructionsLink)) + strLog += " instructionsLink: " + instructionsLink + ";"; + + if (!string.IsNullOrEmpty(changeLogLink)) + strLog += " changeLogLink: " + changeLogLink + ";"; + + strLog += "}"; + + _dnsWebService._log.Write(context.GetRemoteEndPoint(), strLog); + } + catch (Exception ex) + { + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "Check for update was done {updateAvailable: False;}\r\n" + ex.ToString()); + + jsonWriter.WriteBoolean("updateAvailable", false); + } + } + + public async Task ResolveQueryAsync(HttpContext context) + { + UserSession session = context.GetCurrentSession(); + + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.DnsClient, session.User, PermissionFlag.View)) + throw new DnsWebServiceException("Access was denied."); + + HttpRequest request = context.Request; + + string server = request.GetQueryOrForm("server"); + string domain = request.GetQueryOrForm("domain").Trim(new char[] { '\t', ' ', '.' }); + DnsResourceRecordType type = request.GetQueryOrFormEnum("type"); + DnsTransportProtocol protocol = request.GetQueryOrFormEnum("protocol", DnsTransportProtocol.Udp); + bool dnssecValidation = request.GetQueryOrForm("dnssec", bool.Parse, false); + bool importResponse = request.GetQueryOrForm("import", bool.Parse, false); + NetProxy proxy = _dnsWebService.DnsServer.Proxy; + bool preferIPv6 = _dnsWebService.DnsServer.PreferIPv6; + ushort udpPayloadSize = _dnsWebService.DnsServer.UdpPayloadSize; + bool randomizeName = false; + bool qnameMinimization = _dnsWebService.DnsServer.QnameMinimization; + const int RETRIES = 1; + const int TIMEOUT = 10000; + + DnsDatagram dnsResponse; + string dnssecErrorMessage = null; + + if (server.Equals("recursive-resolver", StringComparison.OrdinalIgnoreCase)) + { + if (type == DnsResourceRecordType.AXFR) + throw new DnsServerException("Cannot do zone transfer (AXFR) for 'recursive-resolver'."); + + DnsQuestionRecord question; + + if ((type == DnsResourceRecordType.PTR) && IPAddress.TryParse(domain, out IPAddress address)) + question = new DnsQuestionRecord(address, DnsClass.IN); + else + question = new DnsQuestionRecord(domain, type, DnsClass.IN); + + DnsCache dnsCache = new DnsCache(); + dnsCache.MinimumRecordTtl = 0; + dnsCache.MaximumRecordTtl = 7 * 24 * 60 * 60; + + try + { + dnsResponse = await DnsClient.RecursiveResolveAsync(question, dnsCache, proxy, preferIPv6, udpPayloadSize, randomizeName, qnameMinimization, false, dnssecValidation, null, RETRIES, TIMEOUT); + } + catch (DnsClientResponseDnssecValidationException ex) + { + dnsResponse = ex.Response; + dnssecErrorMessage = ex.Message; + importResponse = false; + } + } + else + { + if ((type == DnsResourceRecordType.AXFR) && (protocol == DnsTransportProtocol.Udp)) + protocol = DnsTransportProtocol.Tcp; + + NameServerAddress nameServer; + + if (server.Equals("this-server", StringComparison.OrdinalIgnoreCase)) + { + switch (protocol) + { + case DnsTransportProtocol.Udp: + nameServer = _dnsWebService.DnsServer.ThisServer; + break; + + case DnsTransportProtocol.Tcp: + nameServer = _dnsWebService.DnsServer.ThisServer.ChangeProtocol(DnsTransportProtocol.Tcp); + break; + + case DnsTransportProtocol.Tls: + throw new DnsServerException("Cannot use DNS-over-TLS protocol for 'this-server'. Please use the TLS certificate domain name as the server."); + + case DnsTransportProtocol.Https: + throw new DnsServerException("Cannot use DNS-over-HTTPS protocol for 'this-server'. Please use the TLS certificate domain name with a url as the server."); + + case DnsTransportProtocol.Quic: + throw new DnsServerException("Cannot use DNS-over-QUIC protocol for 'this-server'. Please use the TLS certificate domain name as the server."); + + default: + throw new NotSupportedException("DNS transport protocol is not supported: " + protocol.ToString()); + } + + proxy = null; //no proxy required for this server + } + else + { + nameServer = NameServerAddress.Parse(server); + + if (nameServer.Protocol != protocol) + nameServer = nameServer.ChangeProtocol(protocol); + + if (nameServer.IsIPEndPointStale) + { + if (proxy is null) + await nameServer.ResolveIPAddressAsync(_dnsWebService.DnsServer, _dnsWebService.DnsServer.PreferIPv6); + } + else if ((nameServer.DomainEndPoint is null) && ((protocol == DnsTransportProtocol.Udp) || (protocol == DnsTransportProtocol.Tcp))) + { + try + { + await nameServer.ResolveDomainNameAsync(_dnsWebService.DnsServer); + } + catch + { } + } + } + + DnsClient dnsClient = new DnsClient(nameServer); + + dnsClient.Proxy = proxy; + dnsClient.PreferIPv6 = preferIPv6; + dnsClient.RandomizeName = randomizeName; + dnsClient.Retries = RETRIES; + dnsClient.Timeout = TIMEOUT; + dnsClient.UdpPayloadSize = udpPayloadSize; + dnsClient.DnssecValidation = dnssecValidation; + + if (dnssecValidation) + { + //load trust anchors into dns client if domain is locally hosted + _dnsWebService.DnsServer.AuthZoneManager.LoadTrustAnchorsTo(dnsClient, domain, type); + } + + try + { + dnsResponse = await dnsClient.ResolveAsync(domain, type); + } + catch (DnsClientResponseDnssecValidationException ex) + { + dnsResponse = ex.Response; + dnssecErrorMessage = ex.Message; + importResponse = false; + } + + if (type == DnsResourceRecordType.AXFR) + dnsResponse = dnsResponse.Join(); + } + + if (importResponse) + { + AuthZoneInfo zoneInfo = _dnsWebService.DnsServer.AuthZoneManager.FindAuthZoneInfo(domain); + if ((zoneInfo is null) || ((zoneInfo.Type == AuthZoneType.Secondary) && !zoneInfo.Name.Equals(domain, StringComparison.OrdinalIgnoreCase))) + { + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, session.User, PermissionFlag.Modify)) + throw new DnsWebServiceException("Access was denied."); + + zoneInfo = _dnsWebService.DnsServer.AuthZoneManager.CreatePrimaryZone(domain, _dnsWebService.DnsServer.ServerDomain, false); + if (zoneInfo is null) + throw new DnsServerException("Cannot import records: failed to create primary zone."); + + //set permissions + _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, session.User, PermissionFlag.ViewModifyDelete); + _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete); + _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.ViewModifyDelete); + _dnsWebService._authManager.SaveConfigFile(); + } + else + { + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, session.User, PermissionFlag.Modify)) + throw new DnsWebServiceException("Access was denied."); + + switch (zoneInfo.Type) + { + case AuthZoneType.Primary: + break; + + case AuthZoneType.Forwarder: + if (type == DnsResourceRecordType.AXFR) + throw new DnsServerException("Cannot import records via zone transfer: import zone must be of primary type."); + + break; + + default: + throw new DnsServerException("Cannot import records: import zone must be of primary or forwarder type."); + } + } + + if (type == DnsResourceRecordType.AXFR) + { + _dnsWebService.DnsServer.AuthZoneManager.SyncZoneTransferRecords(zoneInfo.Name, dnsResponse.Answer); + } + else + { + List importRecords = new List(dnsResponse.Answer.Count + dnsResponse.Authority.Count); + + foreach (DnsResourceRecord record in dnsResponse.Answer) + { + if (record.Name.Equals(zoneInfo.Name, StringComparison.OrdinalIgnoreCase) || record.Name.EndsWith("." + zoneInfo.Name, StringComparison.OrdinalIgnoreCase) || (zoneInfo.Name.Length == 0)) + { + record.RemoveExpiry(); + importRecords.Add(record); + + if (record.Type == DnsResourceRecordType.NS) + record.SyncGlueRecords(dnsResponse.Additional); + } + } + + foreach (DnsResourceRecord record in dnsResponse.Authority) + { + if (record.Name.Equals(zoneInfo.Name, StringComparison.OrdinalIgnoreCase) || record.Name.EndsWith("." + zoneInfo.Name, StringComparison.OrdinalIgnoreCase) || (zoneInfo.Name.Length == 0)) + { + record.RemoveExpiry(); + importRecords.Add(record); + + if (record.Type == DnsResourceRecordType.NS) + record.SyncGlueRecords(dnsResponse.Additional); + } + } + + _dnsWebService.DnsServer.AuthZoneManager.ImportRecords(zoneInfo.Name, importRecords); + } + + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] DNS Client imported record(s) for authoritative zone {server: " + server + "; zone: " + zoneInfo.Name + "; type: " + type + ";}"); + + _dnsWebService.DnsServer.AuthZoneManager.SaveZoneFile(zoneInfo.Name); + } + + Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); + + if (dnssecErrorMessage is not null) + jsonWriter.WriteString("warningMessage", dnssecErrorMessage); + + jsonWriter.WritePropertyName("result"); + dnsResponse.SerializeTo(jsonWriter); + } + + #endregion + } +} diff --git a/DnsServerCore/WebServiceAppsApi.cs b/DnsServerCore/WebServiceAppsApi.cs index d72b5801..ed87c093 100644 --- a/DnsServerCore/WebServiceAppsApi.cs +++ b/DnsServerCore/WebServiceAppsApi.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -18,17 +18,18 @@ along with this program. If not, see . */ using DnsServerCore.ApplicationCommon; +using DnsServerCore.Auth; using DnsServerCore.Dns.Applications; -using Newtonsoft.Json; +using Microsoft.AspNetCore.Http; using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Http; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using TechnitiumLibrary; -using TechnitiumLibrary.IO; namespace DnsServerCore { @@ -41,7 +42,7 @@ namespace DnsServerCore string _storeAppsJsonData; DateTime _storeAppsJsonDataUpdatedOn; - const int STORE_APPS_JSON_DATA_CACHE_TIME_SECONDS = 300; + const int STORE_APPS_JSON_DATA_CACHE_TIME_SECONDS = 900; Timer _appUpdateTimer; const int APP_UPDATE_TIMER_INITIAL_INTERVAL = 10000; @@ -89,35 +90,36 @@ namespace DnsServerCore if (_dnsWebService.DnsServer.DnsApplicationManager.Applications.Count < 1) return; - _dnsWebService.Log.Write("DNS Server has started automatic update check for DNS Apps."); + _dnsWebService._log.Write("DNS Server has started automatic update check for DNS Apps."); string storeAppsJsonData = await GetStoreAppsJsonData().WithTimeout(5000); - dynamic jsonStoreAppsArray = JsonConvert.DeserializeObject(storeAppsJsonData); + using JsonDocument jsonDocument = JsonDocument.Parse(storeAppsJsonData); + JsonElement jsonStoreAppsArray = jsonDocument.RootElement; foreach (DnsApplication application in _dnsWebService.DnsServer.DnsApplicationManager.Applications.Values) { - foreach (dynamic jsonStoreApp in jsonStoreAppsArray) + foreach (JsonElement jsonStoreApp in jsonStoreAppsArray.EnumerateArray()) { - string name = jsonStoreApp.name.Value; + string name = jsonStoreApp.GetProperty("name").GetString(); if (name.Equals(application.Name)) { string url = null; Version storeAppVersion = null; Version lastServerVersion = null; - foreach (dynamic jsonVersion in jsonStoreApp.versions) + foreach (JsonElement jsonVersion in jsonStoreApp.GetProperty("versions").EnumerateArray()) { - string strServerVersion = jsonVersion.serverVersion.Value; + string strServerVersion = jsonVersion.GetProperty("serverVersion").GetString(); Version requiredServerVersion = new Version(strServerVersion); - if (_dnsWebService.ServerVersion < requiredServerVersion) + if (_dnsWebService._currentVersion < requiredServerVersion) continue; if ((lastServerVersion is not null) && (lastServerVersion > requiredServerVersion)) continue; - string version = jsonVersion.version.Value; - url = jsonVersion.url.Value; + string version = jsonVersion.GetProperty("version").GetString(); + url = jsonVersion.GetProperty("url").GetString(); storeAppVersion = new Version(version); lastServerVersion = requiredServerVersion; @@ -129,11 +131,11 @@ namespace DnsServerCore { await DownloadAndUpdateAppAsync(application.Name, url); - _dnsWebService.Log.Write("DNS application '" + application.Name + "' was automatically updated successfully from: " + url); + _dnsWebService._log.Write("DNS application '" + application.Name + "' was automatically updated successfully from: " + url); } catch (Exception ex) { - _dnsWebService.Log.Write("Failed to automatically download and update DNS application '" + application.Name + "': " + ex.ToString()); + _dnsWebService._log.Write("Failed to automatically download and update DNS application '" + application.Name + "': " + ex.ToString()); } } @@ -144,7 +146,7 @@ namespace DnsServerCore } catch (Exception ex) { - _dnsWebService.Log.Write(ex); + _dnsWebService._log.Write(ex); } }); @@ -163,12 +165,12 @@ namespace DnsServerCore private async Task GetStoreAppsJsonData() { - if ((_storeAppsJsonData == null) || (DateTime.UtcNow > _storeAppsJsonDataUpdatedOn.AddSeconds(STORE_APPS_JSON_DATA_CACHE_TIME_SECONDS))) + if ((_storeAppsJsonData is null) || (DateTime.UtcNow > _storeAppsJsonDataUpdatedOn.AddSeconds(STORE_APPS_JSON_DATA_CACHE_TIME_SECONDS))) { SocketsHttpHandler handler = new SocketsHttpHandler(); handler.Proxy = _dnsWebService.DnsServer.Proxy; handler.UseProxy = _dnsWebService.DnsServer.Proxy is not null; - handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; + handler.AutomaticDecompression = DecompressionMethods.All; using (HttpClient http = new HttpClient(handler)) { @@ -191,7 +193,7 @@ namespace DnsServerCore SocketsHttpHandler handler = new SocketsHttpHandler(); handler.Proxy = _dnsWebService.DnsServer.Proxy; handler.UseProxy = _dnsWebService.DnsServer.Proxy is not null; - handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; + handler.AutomaticDecompression = DecompressionMethods.All; using (HttpClient http = new HttpClient(handler)) { @@ -214,29 +216,24 @@ namespace DnsServerCore } catch (Exception ex) { - _dnsWebService.Log.Write(ex); + _dnsWebService._log.Write(ex); } } } - private void WriteAppAsJson(JsonTextWriter jsonWriter, DnsApplication application, dynamic jsonStoreAppsArray) + private void WriteAppAsJson(Utf8JsonWriter jsonWriter, DnsApplication application, JsonElement jsonStoreAppsArray = default) { jsonWriter.WriteStartObject(); - jsonWriter.WritePropertyName("name"); - jsonWriter.WriteValue(application.Name); - - jsonWriter.WritePropertyName("description"); - jsonWriter.WriteValue(application.Description); + jsonWriter.WriteString("name", application.Name); + jsonWriter.WriteString("description", application.Description); + jsonWriter.WriteString("version", DnsWebService.GetCleanVersion(application.Version)); - jsonWriter.WritePropertyName("version"); - jsonWriter.WriteValue(DnsWebService.GetCleanVersion(application.Version)); - - if (jsonStoreAppsArray != null) + if (jsonStoreAppsArray.ValueKind != JsonValueKind.Undefined) { - foreach (dynamic jsonStoreApp in jsonStoreAppsArray) + foreach (JsonElement jsonStoreApp in jsonStoreAppsArray.EnumerateArray()) { - string name = jsonStoreApp.name.Value; + string name = jsonStoreApp.GetProperty("name").GetString(); if (name.Equals(application.Name)) { string version = null; @@ -244,19 +241,19 @@ namespace DnsServerCore Version storeAppVersion = null; Version lastServerVersion = null; - foreach (dynamic jsonVersion in jsonStoreApp.versions) + foreach (JsonElement jsonVersion in jsonStoreApp.GetProperty("versions").EnumerateArray()) { - string strServerVersion = jsonVersion.serverVersion.Value; + string strServerVersion = jsonVersion.GetProperty("serverVersion").GetString(); Version requiredServerVersion = new Version(strServerVersion); - if (_dnsWebService.ServerVersion < requiredServerVersion) + if (_dnsWebService._currentVersion < requiredServerVersion) continue; if ((lastServerVersion is not null) && (lastServerVersion > requiredServerVersion)) continue; - version = jsonVersion.version.Value; - url = jsonVersion.url.Value; + version = jsonVersion.GetProperty("version").GetString(); + url = jsonVersion.GetProperty("url").GetString(); storeAppVersion = new Version(version); lastServerVersion = requiredServerVersion; @@ -265,14 +262,9 @@ namespace DnsServerCore if (storeAppVersion is null) break; //no compatible update available - jsonWriter.WritePropertyName("updateVersion"); - jsonWriter.WriteValue(version); - - jsonWriter.WritePropertyName("updateUrl"); - jsonWriter.WriteValue(url); - - jsonWriter.WritePropertyName("updateAvailable"); - jsonWriter.WriteValue(storeAppVersion > application.Version); + jsonWriter.WriteString("updateVersion", version); + jsonWriter.WriteString("updateUrl", url); + jsonWriter.WriteBoolean("updateAvailable", storeAppVersion > application.Version); break; } } @@ -286,37 +278,23 @@ namespace DnsServerCore { jsonWriter.WriteStartObject(); - jsonWriter.WritePropertyName("classPath"); - jsonWriter.WriteValue(dnsApp.Key); - - jsonWriter.WritePropertyName("description"); - jsonWriter.WriteValue(dnsApp.Value.Description); + jsonWriter.WriteString("classPath", dnsApp.Key); + jsonWriter.WriteString("description", dnsApp.Value.Description); if (dnsApp.Value is IDnsAppRecordRequestHandler appRecordHandler) { - jsonWriter.WritePropertyName("isAppRecordRequestHandler"); - jsonWriter.WriteValue(true); - - jsonWriter.WritePropertyName("recordDataTemplate"); - jsonWriter.WriteValue(appRecordHandler.ApplicationRecordDataTemplate); + jsonWriter.WriteBoolean("isAppRecordRequestHandler", true); + jsonWriter.WriteString("recordDataTemplate", appRecordHandler.ApplicationRecordDataTemplate); } else { - jsonWriter.WritePropertyName("isAppRecordRequestHandler"); - jsonWriter.WriteValue(false); + jsonWriter.WriteBoolean("isAppRecordRequestHandler", false); } - jsonWriter.WritePropertyName("isRequestController"); - jsonWriter.WriteValue(dnsApp.Value is IDnsRequestController); - - jsonWriter.WritePropertyName("isAuthoritativeRequestHandler"); - jsonWriter.WriteValue(dnsApp.Value is IDnsAuthoritativeRequestHandler); - - jsonWriter.WritePropertyName("isQueryLogger"); - jsonWriter.WriteValue(dnsApp.Value is IDnsQueryLogger); - - jsonWriter.WritePropertyName("isPostProcessor"); - jsonWriter.WriteValue(dnsApp.Value is IDnsPostProcessor); + jsonWriter.WriteBoolean("isRequestController", dnsApp.Value is IDnsRequestController); + jsonWriter.WriteBoolean("isAuthoritativeRequestHandler", dnsApp.Value is IDnsAuthoritativeRequestHandler); + jsonWriter.WriteBoolean("isQueryLogger", dnsApp.Value is IDnsQueryLogger); + jsonWriter.WriteBoolean("isPostProcessor", dnsApp.Value is IDnsPostProcessor); jsonWriter.WriteEndObject(); } @@ -331,68 +309,99 @@ namespace DnsServerCore #region public - public async Task ListInstalledAppsAsync(JsonTextWriter jsonWriter) + public async Task ListInstalledAppsAsync(HttpContext context) { + UserSession session = context.GetCurrentSession(); + + if ( + !_dnsWebService._authManager.IsPermitted(PermissionSection.Apps, session.User, PermissionFlag.View) && + !_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, session.User, PermissionFlag.View) && + !_dnsWebService._authManager.IsPermitted(PermissionSection.Logs, session.User, PermissionFlag.View) + ) + { + throw new DnsWebServiceException("Access was denied."); + } + List apps = new List(_dnsWebService.DnsServer.DnsApplicationManager.Applications.Keys); apps.Sort(); - dynamic jsonStoreAppsArray = null; - - if (apps.Count > 0) + JsonDocument jsonDocument = null; + try { - try + JsonElement jsonStoreAppsArray = default; + + if (apps.Count > 0) { - string storeAppsJsonData = await GetStoreAppsJsonData().WithTimeout(5000); - jsonStoreAppsArray = JsonConvert.DeserializeObject(storeAppsJsonData); + try + { + string storeAppsJsonData = await GetStoreAppsJsonData().WithTimeout(5000); + jsonDocument = JsonDocument.Parse(storeAppsJsonData); + jsonStoreAppsArray = jsonDocument.RootElement; + } + catch + { } } - catch - { } + + Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); + + jsonWriter.WritePropertyName("apps"); + jsonWriter.WriteStartArray(); + + foreach (string app in apps) + { + if (_dnsWebService.DnsServer.DnsApplicationManager.Applications.TryGetValue(app, out DnsApplication application)) + WriteAppAsJson(jsonWriter, application, jsonStoreAppsArray); + } + + jsonWriter.WriteEndArray(); } - - jsonWriter.WritePropertyName("apps"); - jsonWriter.WriteStartArray(); - - foreach (string app in apps) + finally { - if (_dnsWebService.DnsServer.DnsApplicationManager.Applications.TryGetValue(app, out DnsApplication application)) - WriteAppAsJson(jsonWriter, application, jsonStoreAppsArray); + if (jsonDocument is not null) + jsonDocument.Dispose(); } - - jsonWriter.WriteEndArray(); } - public async Task ListStoreApps(JsonTextWriter jsonWriter) + public async Task ListStoreApps(HttpContext context) { + UserSession session = context.GetCurrentSession(); + + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Apps, session.User, PermissionFlag.View)) + throw new DnsWebServiceException("Access was denied."); + string storeAppsJsonData = await GetStoreAppsJsonData(); - dynamic jsonStoreAppsArray = JsonConvert.DeserializeObject(storeAppsJsonData); + using JsonDocument jsonDocument = JsonDocument.Parse(storeAppsJsonData); + JsonElement jsonStoreAppsArray = jsonDocument.RootElement; + + Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); jsonWriter.WritePropertyName("storeApps"); jsonWriter.WriteStartArray(); - foreach (dynamic jsonStoreApp in jsonStoreAppsArray) + foreach (JsonElement jsonStoreApp in jsonStoreAppsArray.EnumerateArray()) { - string name = jsonStoreApp.name.Value; - string description = jsonStoreApp.description.Value; + string name = jsonStoreApp.GetProperty("name").GetString(); + string description = jsonStoreApp.GetProperty("description").GetString(); string version = null; string url = null; string size = null; Version storeAppVersion = null; Version lastServerVersion = null; - foreach (dynamic jsonVersion in jsonStoreApp.versions) + foreach (JsonElement jsonVersion in jsonStoreApp.GetProperty("versions").EnumerateArray()) { - string strServerVersion = jsonVersion.serverVersion.Value; + string strServerVersion = jsonVersion.GetProperty("serverVersion").GetString(); Version requiredServerVersion = new Version(strServerVersion); - if (_dnsWebService.ServerVersion < requiredServerVersion) + if (_dnsWebService._currentVersion < requiredServerVersion) continue; if ((lastServerVersion is not null) && (lastServerVersion > requiredServerVersion)) continue; - version = jsonVersion.version.Value; - url = jsonVersion.url.Value; - size = jsonVersion.size.Value; + version = jsonVersion.GetProperty("version").GetString(); + url = jsonVersion.GetProperty("url").GetString(); + size = jsonVersion.GetProperty("size").GetString(); storeAppVersion = new Version(version); lastServerVersion = requiredServerVersion; @@ -403,33 +412,20 @@ namespace DnsServerCore jsonWriter.WriteStartObject(); - jsonWriter.WritePropertyName("name"); - jsonWriter.WriteValue(name); - - jsonWriter.WritePropertyName("description"); - jsonWriter.WriteValue(description); - - jsonWriter.WritePropertyName("version"); - jsonWriter.WriteValue(version); - - jsonWriter.WritePropertyName("url"); - jsonWriter.WriteValue(url); - - jsonWriter.WritePropertyName("size"); - jsonWriter.WriteValue(size); + jsonWriter.WriteString("name", name); + jsonWriter.WriteString("description", description); + jsonWriter.WriteString("version", version); + jsonWriter.WriteString("url", url); + jsonWriter.WriteString("size", size); bool installed = _dnsWebService.DnsServer.DnsApplicationManager.Applications.TryGetValue(name, out DnsApplication installedApp); - jsonWriter.WritePropertyName("installed"); - jsonWriter.WriteValue(installed); + jsonWriter.WriteBoolean("installed", installed); if (installed) { - jsonWriter.WritePropertyName("installedVersion"); - jsonWriter.WriteValue(DnsWebService.GetCleanVersion(installedApp.Version)); - - jsonWriter.WritePropertyName("updateAvailable"); - jsonWriter.WriteValue(storeAppVersion > installedApp.Version); + jsonWriter.WriteString("installedVersion", DnsWebService.GetCleanVersion(installedApp.Version)); + jsonWriter.WriteBoolean("updateAvailable", storeAppVersion > installedApp.Version); } jsonWriter.WriteEndObject(); @@ -438,17 +434,17 @@ namespace DnsServerCore jsonWriter.WriteEndArray(); } - public async Task DownloadAndInstallAppAsync(HttpListenerRequest request, JsonTextWriter jsonWriter) + public async Task DownloadAndInstallAppAsync(HttpContext context) { - string name = request.QueryString["name"]; - if (string.IsNullOrEmpty(name)) - throw new DnsWebServiceException("Parameter 'name' missing."); + UserSession session = context.GetCurrentSession(); - name = name.Trim(); + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Apps, session.User, PermissionFlag.Delete)) + throw new DnsWebServiceException("Access was denied."); - string url = request.QueryString["url"]; - if (string.IsNullOrEmpty(url)) - throw new DnsWebServiceException("Parameter 'url' missing."); + HttpRequest request = context.Request; + + string name = request.GetQueryOrForm("name").Trim(); + string url = request.GetQueryOrForm("url"); if (!url.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) throw new DnsWebServiceException("Parameter 'url' value must start with 'https://'."); @@ -462,7 +458,7 @@ namespace DnsServerCore SocketsHttpHandler handler = new SocketsHttpHandler(); handler.Proxy = _dnsWebService.DnsServer.Proxy; handler.UseProxy = _dnsWebService.DnsServer.Proxy is not null; - handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; + handler.AutomaticDecompression = DecompressionMethods.All; using (HttpClient http = new HttpClient(handler)) { @@ -476,10 +472,12 @@ namespace DnsServerCore fS.Position = 0; DnsApplication application = await _dnsWebService.DnsServer.DnsApplicationManager.InstallApplicationAsync(name, fS); - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + _dnsWebService.GetSession(request).User.Username + "] DNS application '" + name + "' was installed successfully from: " + url); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] DNS application '" + name + "' was installed successfully from: " + url); + + Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); jsonWriter.WritePropertyName("installedApp"); - WriteAppAsJson(jsonWriter, application, null); + WriteAppAsJson(jsonWriter, application); } } finally @@ -490,64 +488,49 @@ namespace DnsServerCore } catch (Exception ex) { - _dnsWebService.Log.Write(ex); + _dnsWebService._log.Write(ex); } } } - public async Task DownloadAndUpdateAppAsync(HttpListenerRequest request, JsonTextWriter jsonWriter) + public async Task DownloadAndUpdateAppAsync(HttpContext context) { - string name = request.QueryString["name"]; - if (string.IsNullOrEmpty(name)) - throw new DnsWebServiceException("Parameter 'name' missing."); + UserSession session = context.GetCurrentSession(); - name = name.Trim(); + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Apps, session.User, PermissionFlag.Delete)) + throw new DnsWebServiceException("Access was denied."); - string url = request.QueryString["url"]; - if (string.IsNullOrEmpty(url)) - throw new DnsWebServiceException("Parameter 'url' missing."); + HttpRequest request = context.Request; + + string name = request.GetQueryOrForm("name").Trim(); + string url = request.GetQueryOrForm("url"); if (!url.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) throw new DnsWebServiceException("Parameter 'url' value must start with 'https://'."); DnsApplication application = await DownloadAndUpdateAppAsync(name, url); - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + _dnsWebService.GetSession(request).User.Username + "] DNS application '" + name + "' was updated successfully from: " + url); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] DNS application '" + name + "' was updated successfully from: " + url); + + Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); jsonWriter.WritePropertyName("updatedApp"); - WriteAppAsJson(jsonWriter, application, null); + WriteAppAsJson(jsonWriter, application); } - public async Task InstallAppAsync(HttpListenerRequest request, JsonTextWriter jsonWriter) + public async Task InstallAppAsync(HttpContext context) { - string name = request.QueryString["name"]; - if (string.IsNullOrEmpty(name)) - throw new DnsWebServiceException("Parameter 'name' missing."); + UserSession session = context.GetCurrentSession(); - name = name.Trim(); + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Apps, session.User, PermissionFlag.Delete)) + throw new DnsWebServiceException("Access was denied."); - #region skip to content + HttpRequest request = context.Request; - int crlfCount = 0; - int byteRead; + string name = request.GetQueryOrForm("name").Trim(); - while (crlfCount != 4) - { - byteRead = await request.InputStream.ReadByteValueAsync(); - switch (byteRead) - { - case 13: //CR - case 10: //LF - crlfCount++; - break; - - default: - crlfCount = 0; - break; - } - } - - #endregion + if (!request.HasFormContentType || (request.Form.Files.Count == 0)) + throw new DnsWebServiceException("DNS application zip file is missing."); string tmpFile = Path.GetTempFileName(); try @@ -555,16 +538,18 @@ namespace DnsServerCore using (FileStream fS = new FileStream(tmpFile, FileMode.Create, FileAccess.ReadWrite)) { //write to temp file - await request.InputStream.CopyToAsync(fS); + await request.Form.Files[0].CopyToAsync(fS); //install app fS.Position = 0; DnsApplication application = await _dnsWebService.DnsServer.DnsApplicationManager.InstallApplicationAsync(name, fS); - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + _dnsWebService.GetSession(request).User.Username + "] DNS application '" + name + "' was installed successfully."); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] DNS application '" + name + "' was installed successfully."); + + Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); jsonWriter.WritePropertyName("installedApp"); - WriteAppAsJson(jsonWriter, application, null); + WriteAppAsJson(jsonWriter, application); } } finally @@ -575,41 +560,24 @@ namespace DnsServerCore } catch (Exception ex) { - _dnsWebService.Log.Write(ex); + _dnsWebService._log.Write(ex); } } } - public async Task UpdateAppAsync(HttpListenerRequest request, JsonTextWriter jsonWriter) + public async Task UpdateAppAsync(HttpContext context) { - string name = request.QueryString["name"]; - if (string.IsNullOrEmpty(name)) - throw new DnsWebServiceException("Parameter 'name' missing."); + UserSession session = context.GetCurrentSession(); - name = name.Trim(); + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Apps, session.User, PermissionFlag.Delete)) + throw new DnsWebServiceException("Access was denied."); - #region skip to content + HttpRequest request = context.Request; - int crlfCount = 0; - int byteRead; + string name = request.GetQueryOrForm("name").Trim(); - while (crlfCount != 4) - { - byteRead = await request.InputStream.ReadByteValueAsync(); - switch (byteRead) - { - case 13: //CR - case 10: //LF - crlfCount++; - break; - - default: - crlfCount = 0; - break; - } - } - - #endregion + if (!request.HasFormContentType || (request.Form.Files.Count == 0)) + throw new DnsWebServiceException("DNS application zip file is missing."); string tmpFile = Path.GetTempFileName(); try @@ -617,16 +585,18 @@ namespace DnsServerCore using (FileStream fS = new FileStream(tmpFile, FileMode.Create, FileAccess.ReadWrite)) { //write to temp file - await request.InputStream.CopyToAsync(fS); + await request.Form.Files[0].CopyToAsync(fS); //update app fS.Position = 0; DnsApplication application = await _dnsWebService.DnsServer.DnsApplicationManager.UpdateApplicationAsync(name, fS); - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + _dnsWebService.GetSession(request).User.Username + "] DNS application '" + name + "' was updated successfully."); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] DNS application '" + name + "' was updated successfully."); + + Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); jsonWriter.WritePropertyName("updatedApp"); - WriteAppAsJson(jsonWriter, application, null); + WriteAppAsJson(jsonWriter, application); } } finally @@ -637,76 +607,70 @@ namespace DnsServerCore } catch (Exception ex) { - _dnsWebService.Log.Write(ex); + _dnsWebService._log.Write(ex); } } } - public void UninstallApp(HttpListenerRequest request) + public void UninstallApp(HttpContext context) { - string name = request.QueryString["name"]; - if (string.IsNullOrEmpty(name)) - throw new DnsWebServiceException("Parameter 'name' missing."); + UserSession session = context.GetCurrentSession(); - name = name.Trim(); + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Apps, session.User, PermissionFlag.Delete)) + throw new DnsWebServiceException("Access was denied."); + + HttpRequest request = context.Request; + + string name = request.GetQueryOrForm("name").Trim(); _dnsWebService.DnsServer.DnsApplicationManager.UninstallApplication(name); - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + _dnsWebService.GetSession(request).User.Username + "] DNS application '" + name + "' was uninstalled successfully."); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] DNS application '" + name + "' was uninstalled successfully."); } - public async Task GetAppConfigAsync(HttpListenerRequest request, JsonTextWriter jsonWriter) + public async Task GetAppConfigAsync(HttpContext context) { - string name = request.QueryString["name"]; - if (string.IsNullOrEmpty(name)) - throw new DnsWebServiceException("Parameter 'name' missing."); + UserSession session = context.GetCurrentSession(); - name = name.Trim(); + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Apps, session.User, PermissionFlag.View)) + throw new DnsWebServiceException("Access was denied."); + + HttpRequest request = context.Request; + + string name = request.GetQueryOrForm("name").Trim(); if (!_dnsWebService.DnsServer.DnsApplicationManager.Applications.TryGetValue(name, out DnsApplication application)) throw new DnsWebServiceException("DNS application was not found: " + name); string config = await application.GetConfigAsync(); - jsonWriter.WritePropertyName("config"); - jsonWriter.WriteValue(config); + Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); + jsonWriter.WriteString("config", config); } - public async Task SetAppConfigAsync(HttpListenerRequest request) + public async Task SetAppConfigAsync(HttpContext context) { - string name = request.QueryString["name"]; - if (string.IsNullOrEmpty(name)) - throw new DnsWebServiceException("Parameter 'name' missing."); + UserSession session = context.GetCurrentSession(); - name = name.Trim(); + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Apps, session.User, PermissionFlag.Modify)) + throw new DnsWebServiceException("Access was denied."); + + HttpRequest request = context.Request; + + string name = request.GetQueryOrForm("name").Trim(); if (!_dnsWebService.DnsServer.DnsApplicationManager.Applications.TryGetValue(name, out DnsApplication application)) throw new DnsWebServiceException("DNS application was not found: " + name); - string formRequest; - using (StreamReader sR = new StreamReader(request.InputStream, request.ContentEncoding)) - { - formRequest = await sR.ReadToEndAsync(); - } + string config = request.QueryOrForm("config"); + if (config is null) + throw new DnsWebServiceException("Parameter 'config' missing."); - string[] formParts = formRequest.Split('&'); + if (config.Length == 0) + config = null; - foreach (string formPart in formParts) - { - if (formPart.StartsWith("config=")) - { - string config = Uri.UnescapeDataString(formPart.Substring(7)); + await application.SetConfigAsync(config); - if (config.Length == 0) - config = null; - - await application.SetConfigAsync(config); - - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + _dnsWebService.GetSession(request).User.Username + "] DNS application '" + name + "' app config was saved successfully."); - return; - } - } - - throw new DnsWebServiceException("Missing POST parameter: config"); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] DNS application '" + name + "' app config was saved successfully."); } #endregion diff --git a/DnsServerCore/WebServiceAuthApi.cs b/DnsServerCore/WebServiceAuthApi.cs index 869e4548..acc0088f 100644 --- a/DnsServerCore/WebServiceAuthApi.cs +++ b/DnsServerCore/WebServiceAuthApi.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -18,10 +18,11 @@ along with this program. If not, see . */ using DnsServerCore.Auth; -using Newtonsoft.Json; +using Microsoft.AspNetCore.Http; using System; using System.Collections.Generic; using System.Net; +using System.Text.Json; using System.Threading.Tasks; namespace DnsServerCore @@ -45,29 +46,19 @@ namespace DnsServerCore #region private - private void WriteCurrentSessionDetails(JsonTextWriter jsonWriter, UserSession currentSession, bool includeInfo) + private void WriteCurrentSessionDetails(Utf8JsonWriter jsonWriter, UserSession currentSession, bool includeInfo) { if (currentSession.Type == UserSessionType.ApiToken) { - jsonWriter.WritePropertyName("username"); - jsonWriter.WriteValue(currentSession.User.Username); - - jsonWriter.WritePropertyName("tokenName"); - jsonWriter.WriteValue(currentSession.TokenName); - - jsonWriter.WritePropertyName("token"); - jsonWriter.WriteValue(currentSession.Token); + jsonWriter.WriteString("username", currentSession.User.Username); + jsonWriter.WriteString("tokenName", currentSession.TokenName); + jsonWriter.WriteString("token", currentSession.Token); } else { - jsonWriter.WritePropertyName("displayName"); - jsonWriter.WriteValue(currentSession.User.DisplayName); - - jsonWriter.WritePropertyName("username"); - jsonWriter.WriteValue(currentSession.User.Username); - - jsonWriter.WritePropertyName("token"); - jsonWriter.WriteValue(currentSession.Token); + jsonWriter.WriteString("displayName", currentSession.User.DisplayName); + jsonWriter.WriteString("username", currentSession.User.Username); + jsonWriter.WriteString("token", currentSession.Token); } if (includeInfo) @@ -75,14 +66,9 @@ namespace DnsServerCore jsonWriter.WritePropertyName("info"); jsonWriter.WriteStartObject(); - jsonWriter.WritePropertyName("version"); - jsonWriter.WriteValue(_dnsWebService.GetServerVersion()); - - jsonWriter.WritePropertyName("dnsServerDomain"); - jsonWriter.WriteValue(_dnsWebService.DnsServer.ServerDomain); - - jsonWriter.WritePropertyName("defaultRecordTtl"); - jsonWriter.WriteValue(_dnsWebService.ZonesApi.DefaultRecordTtl); + jsonWriter.WriteString("version", _dnsWebService.GetServerVersion()); + jsonWriter.WriteString("dnsServerDomain", _dnsWebService.DnsServer.ServerDomain); + jsonWriter.WriteNumber("defaultRecordTtl", _dnsWebService._zonesApi.DefaultRecordTtl); jsonWriter.WritePropertyName("permissions"); jsonWriter.WriteStartObject(); @@ -94,14 +80,9 @@ namespace DnsServerCore jsonWriter.WritePropertyName(section.ToString()); jsonWriter.WriteStartObject(); - jsonWriter.WritePropertyName("canView"); - jsonWriter.WriteValue(_dnsWebService.AuthManager.IsPermitted(section, currentSession.User, PermissionFlag.View)); - - jsonWriter.WritePropertyName("canModify"); - jsonWriter.WriteValue(_dnsWebService.AuthManager.IsPermitted(section, currentSession.User, PermissionFlag.Modify)); - - jsonWriter.WritePropertyName("canDelete"); - jsonWriter.WriteValue(_dnsWebService.AuthManager.IsPermitted(section, currentSession.User, PermissionFlag.Delete)); + jsonWriter.WriteBoolean("canView", _dnsWebService._authManager.IsPermitted(section, currentSession.User, PermissionFlag.View)); + jsonWriter.WriteBoolean("canModify", _dnsWebService._authManager.IsPermitted(section, currentSession.User, PermissionFlag.Modify)); + jsonWriter.WriteBoolean("canDelete", _dnsWebService._authManager.IsPermitted(section, currentSession.User, PermissionFlag.Delete)); jsonWriter.WriteEndObject(); } @@ -112,33 +93,19 @@ namespace DnsServerCore } } - private void WriteUserDetails(JsonTextWriter jsonWriter, User user, UserSession currentSession, bool includeMoreDetails, bool includeGroups) + private void WriteUserDetails(Utf8JsonWriter jsonWriter, User user, UserSession currentSession, bool includeMoreDetails, bool includeGroups) { - jsonWriter.WritePropertyName("displayName"); - jsonWriter.WriteValue(user.DisplayName); - - jsonWriter.WritePropertyName("username"); - jsonWriter.WriteValue(user.Username); - - jsonWriter.WritePropertyName("disabled"); - jsonWriter.WriteValue(user.Disabled); - - jsonWriter.WritePropertyName("previousSessionLoggedOn"); - jsonWriter.WriteValue(user.PreviousSessionLoggedOn); - - jsonWriter.WritePropertyName("previousSessionRemoteAddress"); - jsonWriter.WriteValue(user.PreviousSessionRemoteAddress.ToString()); - - jsonWriter.WritePropertyName("recentSessionLoggedOn"); - jsonWriter.WriteValue(user.RecentSessionLoggedOn); - - jsonWriter.WritePropertyName("recentSessionRemoteAddress"); - jsonWriter.WriteValue(user.RecentSessionRemoteAddress.ToString()); + jsonWriter.WriteString("displayName", user.DisplayName); + jsonWriter.WriteString("username", user.Username); + jsonWriter.WriteBoolean("disabled", user.Disabled); + jsonWriter.WriteString("previousSessionLoggedOn", user.PreviousSessionLoggedOn); + jsonWriter.WriteString("previousSessionRemoteAddress", user.PreviousSessionRemoteAddress.ToString()); + jsonWriter.WriteString("recentSessionLoggedOn", user.RecentSessionLoggedOn); + jsonWriter.WriteString("recentSessionRemoteAddress", user.RecentSessionRemoteAddress.ToString()); if (includeMoreDetails) { - jsonWriter.WritePropertyName("sessionTimeoutSeconds"); - jsonWriter.WriteValue(user.SessionTimeoutSeconds); + jsonWriter.WriteNumber("sessionTimeoutSeconds", user.SessionTimeoutSeconds); jsonWriter.WritePropertyName("memberOfGroups"); jsonWriter.WriteStartArray(); @@ -151,7 +118,7 @@ namespace DnsServerCore if (group.Name.Equals("Everyone", StringComparison.OrdinalIgnoreCase)) continue; - jsonWriter.WriteValue(group.Name); + jsonWriter.WriteStringValue(group.Name); } jsonWriter.WriteEndArray(); @@ -159,7 +126,7 @@ namespace DnsServerCore jsonWriter.WritePropertyName("sessions"); jsonWriter.WriteStartArray(); - List sessions = _dnsWebService.AuthManager.GetSessions(user); + List sessions = _dnsWebService._authManager.GetSessions(user); sessions.Sort(); foreach (UserSession session in sessions) @@ -170,7 +137,7 @@ namespace DnsServerCore if (includeGroups) { - List groups = new List(_dnsWebService.AuthManager.Groups); + List groups = new List(_dnsWebService._authManager.Groups); groups.Sort(); jsonWriter.WritePropertyName("groups"); @@ -181,91 +148,69 @@ namespace DnsServerCore if (group.Name.Equals("Everyone", StringComparison.OrdinalIgnoreCase)) continue; - jsonWriter.WriteValue(group.Name); + jsonWriter.WriteStringValue(group.Name); } jsonWriter.WriteEndArray(); } } - private static void WriteUserSessionDetails(JsonTextWriter jsonWriter, UserSession session, UserSession currentSession) + private static void WriteUserSessionDetails(Utf8JsonWriter jsonWriter, UserSession session, UserSession currentSession) { jsonWriter.WriteStartObject(); - jsonWriter.WritePropertyName("username"); - jsonWriter.WriteValue(session.User.Username); - - jsonWriter.WritePropertyName("isCurrentSession"); - jsonWriter.WriteValue(session.Equals(currentSession)); - - jsonWriter.WritePropertyName("partialToken"); - jsonWriter.WriteValue(session.Token.Substring(0, 16)); - - jsonWriter.WritePropertyName("type"); - jsonWriter.WriteValue(session.Type.ToString()); - - jsonWriter.WritePropertyName("tokenName"); - jsonWriter.WriteValue(session.TokenName); - - jsonWriter.WritePropertyName("lastSeen"); - jsonWriter.WriteValue(session.LastSeen); - - jsonWriter.WritePropertyName("lastSeenRemoteAddress"); - jsonWriter.WriteValue(session.LastSeenRemoteAddress.ToString()); - - jsonWriter.WritePropertyName("lastSeenUserAgent"); - jsonWriter.WriteValue(session.LastSeenUserAgent); + jsonWriter.WriteString("username", session.User.Username); + jsonWriter.WriteBoolean("isCurrentSession", session.Equals(currentSession)); + jsonWriter.WriteString("partialToken", session.Token.Substring(0, 16)); + jsonWriter.WriteString("type", session.Type.ToString()); + jsonWriter.WriteString("tokenName", session.TokenName); + jsonWriter.WriteString("lastSeen", session.LastSeen); + jsonWriter.WriteString("lastSeenRemoteAddress", session.LastSeenRemoteAddress.ToString()); + jsonWriter.WriteString("lastSeenUserAgent", session.LastSeenUserAgent); jsonWriter.WriteEndObject(); } - private void WriteGroupDetails(JsonTextWriter jsonWriter, Group group, bool includeMembers, bool includeUsers) + private void WriteGroupDetails(Utf8JsonWriter jsonWriter, Group group, bool includeMembers, bool includeUsers) { - jsonWriter.WritePropertyName("name"); - jsonWriter.WriteValue(group.Name); - - jsonWriter.WritePropertyName("description"); - jsonWriter.WriteValue(group.Description); + jsonWriter.WriteString("name", group.Name); + jsonWriter.WriteString("description", group.Description); if (includeMembers) { jsonWriter.WritePropertyName("members"); jsonWriter.WriteStartArray(); - List members = _dnsWebService.AuthManager.GetGroupMembers(group); + List members = _dnsWebService._authManager.GetGroupMembers(group); members.Sort(); foreach (User user in members) - jsonWriter.WriteValue(user.Username); + jsonWriter.WriteStringValue(user.Username); jsonWriter.WriteEndArray(); } if (includeUsers) { - List users = new List(_dnsWebService.AuthManager.Users); + List users = new List(_dnsWebService._authManager.Users); users.Sort(); jsonWriter.WritePropertyName("users"); jsonWriter.WriteStartArray(); foreach (User user in users) - jsonWriter.WriteValue(user.Username); + jsonWriter.WriteStringValue(user.Username); jsonWriter.WriteEndArray(); } } - private void WritePermissionDetails(JsonTextWriter jsonWriter, Permission permission, string subItem, bool includeUsersAndGroups) + private void WritePermissionDetails(Utf8JsonWriter jsonWriter, Permission permission, string subItem, bool includeUsersAndGroups) { - jsonWriter.WritePropertyName("section"); - jsonWriter.WriteValue(permission.Section.ToString()); + jsonWriter.WriteString("section", permission.Section.ToString()); if (subItem is not null) - { - jsonWriter.WritePropertyName("subItem"); - jsonWriter.WriteValue(subItem.Length == 0 ? "." : subItem); - } + jsonWriter.WriteString("subItem", subItem.Length == 0 ? "." : subItem); jsonWriter.WritePropertyName("userPermissions"); jsonWriter.WriteStartArray(); @@ -281,17 +226,10 @@ namespace DnsServerCore { jsonWriter.WriteStartObject(); - jsonWriter.WritePropertyName("username"); - jsonWriter.WriteValue(userPermission.Key.Username); - - jsonWriter.WritePropertyName("canView"); - jsonWriter.WriteValue(userPermission.Value.HasFlag(PermissionFlag.View)); - - jsonWriter.WritePropertyName("canModify"); - jsonWriter.WriteValue(userPermission.Value.HasFlag(PermissionFlag.Modify)); - - jsonWriter.WritePropertyName("canDelete"); - jsonWriter.WriteValue(userPermission.Value.HasFlag(PermissionFlag.Delete)); + jsonWriter.WriteString("username", userPermission.Key.Username); + jsonWriter.WriteBoolean("canView", userPermission.Value.HasFlag(PermissionFlag.View)); + jsonWriter.WriteBoolean("canModify", userPermission.Value.HasFlag(PermissionFlag.Modify)); + jsonWriter.WriteBoolean("canDelete", userPermission.Value.HasFlag(PermissionFlag.Delete)); jsonWriter.WriteEndObject(); } @@ -312,17 +250,10 @@ namespace DnsServerCore { jsonWriter.WriteStartObject(); - jsonWriter.WritePropertyName("name"); - jsonWriter.WriteValue(groupPermission.Key.Name); - - jsonWriter.WritePropertyName("canView"); - jsonWriter.WriteValue(groupPermission.Value.HasFlag(PermissionFlag.View)); - - jsonWriter.WritePropertyName("canModify"); - jsonWriter.WriteValue(groupPermission.Value.HasFlag(PermissionFlag.Modify)); - - jsonWriter.WritePropertyName("canDelete"); - jsonWriter.WriteValue(groupPermission.Value.HasFlag(PermissionFlag.Delete)); + jsonWriter.WriteString("name", groupPermission.Key.Name); + jsonWriter.WriteBoolean("canView", groupPermission.Value.HasFlag(PermissionFlag.View)); + jsonWriter.WriteBoolean("canModify", groupPermission.Value.HasFlag(PermissionFlag.Modify)); + jsonWriter.WriteBoolean("canDelete", groupPermission.Value.HasFlag(PermissionFlag.Delete)); jsonWriter.WriteEndObject(); } @@ -331,17 +262,17 @@ namespace DnsServerCore if (includeUsersAndGroups) { - List users = new List(_dnsWebService.AuthManager.Users); + List users = new List(_dnsWebService._authManager.Users); users.Sort(); - List groups = new List(_dnsWebService.AuthManager.Groups); + List groups = new List(_dnsWebService._authManager.Groups); groups.Sort(); jsonWriter.WritePropertyName("users"); jsonWriter.WriteStartArray(); foreach (User user in users) - jsonWriter.WriteValue(user.Username); + jsonWriter.WriteStringValue(user.Username); jsonWriter.WriteEndArray(); @@ -349,7 +280,7 @@ namespace DnsServerCore jsonWriter.WriteStartArray(); foreach (Group group in groups) - jsonWriter.WriteValue(group.Name); + jsonWriter.WriteStringValue(group.Name); jsonWriter.WriteEndArray(); } @@ -359,121 +290,105 @@ namespace DnsServerCore #region public - public async Task LoginAsync(HttpListenerRequest request, JsonTextWriter jsonWriter, UserSessionType sessionType) + public async Task LoginAsync(HttpContext context, UserSessionType sessionType) { - string strUsername = request.QueryString["user"]; - if (string.IsNullOrEmpty(strUsername)) - throw new DnsWebServiceException("Parameter 'user' missing."); + HttpRequest request = context.Request; - string strPassword = request.QueryString["pass"]; - if (string.IsNullOrEmpty(strPassword)) - throw new DnsWebServiceException("Parameter 'pass' missing."); + string username = request.GetQueryOrForm("user"); + string password = request.GetQueryOrForm("pass"); + string tokenName = (sessionType == UserSessionType.ApiToken) ? request.GetQueryOrForm("tokenName") : null; + bool includeInfo = request.GetQueryOrForm("includeInfo", bool.Parse, false); + IPEndPoint remoteEP = context.GetRemoteEndPoint(); - string strTokenName = null; + UserSession session = await _dnsWebService._authManager.CreateSessionAsync(sessionType, tokenName, username, password, remoteEP.Address, request.Headers.UserAgent); - if (sessionType == UserSessionType.ApiToken) - { - strTokenName = request.QueryString["tokenName"]; - if (string.IsNullOrEmpty(strTokenName)) - throw new DnsWebServiceException("Parameter 'tokenName' missing."); - } + _dnsWebService._log.Write(remoteEP, "[" + session.User.Username + "] User logged in."); - bool includeInfo; - string strIncludeInfo = request.QueryString["includeInfo"]; - if (string.IsNullOrEmpty(strIncludeInfo)) - includeInfo = false; - else - includeInfo = bool.Parse(strIncludeInfo); - - IPEndPoint remoteEP = DnsWebService.GetRequestRemoteEndPoint(request); - - UserSession session = await _dnsWebService.AuthManager.CreateSessionAsync(sessionType, strTokenName, strUsername, strPassword, remoteEP.Address, request.UserAgent); - - _dnsWebService.Log.Write(remoteEP, "[" + session.User.Username + "] User logged in."); - - _dnsWebService.AuthManager.SaveConfigFile(); + _dnsWebService._authManager.SaveConfigFile(); + Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); WriteCurrentSessionDetails(jsonWriter, session, includeInfo); } - public void Logout(HttpListenerRequest request) + public void Logout(HttpContext context) { - string strToken = request.QueryString["token"]; - if (string.IsNullOrEmpty(strToken)) - throw new DnsWebServiceException("Parameter 'token' missing."); + string token = context.Request.GetQueryOrForm("token"); - UserSession session = _dnsWebService.AuthManager.DeleteSession(strToken); + UserSession session = _dnsWebService._authManager.DeleteSession(token); if (session is not null) { - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + session.User.Username + "] User logged out."); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] User logged out."); - _dnsWebService.AuthManager.SaveConfigFile(); + _dnsWebService._authManager.SaveConfigFile(); } } - public void GetCurrentSessionDetails(HttpListenerRequest request, JsonTextWriter jsonWriter) + public void GetCurrentSessionDetails(HttpContext context) { - if (!_dnsWebService.TryGetSession(request, out UserSession session)) - throw new InvalidTokenWebServiceException("Invalid token or session expired."); - + UserSession session = context.GetCurrentSession(); + Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); WriteCurrentSessionDetails(jsonWriter, session, true); } - public void ChangePassword(HttpListenerRequest request) + public void ChangePassword(HttpContext context) { - UserSession session = _dnsWebService.GetSession(request); + UserSession session = context.GetCurrentSession(); if (session.Type != UserSessionType.Standard) throw new DnsWebServiceException("Access was denied."); - string strPassword = request.QueryString["pass"]; - if (string.IsNullOrEmpty(strPassword)) - throw new DnsWebServiceException("Parameter 'pass' missing."); + string password = context.Request.GetQueryOrForm("pass"); - session.User.ChangePassword(strPassword); + session.User.ChangePassword(password); - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + session.User.Username + "] Password was changed successfully."); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] Password was changed successfully."); - _dnsWebService.AuthManager.SaveConfigFile(); + _dnsWebService._authManager.SaveConfigFile(); } - public void GetProfile(HttpListenerRequest request, JsonTextWriter jsonWriter) + public void GetProfile(HttpContext context) { - UserSession session = _dnsWebService.GetSession(request); - + UserSession session = context.GetCurrentSession(); + Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); WriteUserDetails(jsonWriter, session.User, session, true, false); } - public void SetProfile(HttpListenerRequest request, JsonTextWriter jsonWriter) + public void SetProfile(HttpContext context) { - UserSession session = _dnsWebService.GetSession(request); + UserSession session = context.GetCurrentSession(); if (session.Type != UserSessionType.Standard) throw new DnsWebServiceException("Access was denied."); - string strDisplayName = request.QueryString["displayName"]; - if (!string.IsNullOrEmpty(strDisplayName)) - session.User.DisplayName = strDisplayName; + HttpRequest request = context.Request; - string strSessionTimeoutSeconds = request.QueryString["sessionTimeoutSeconds"]; - if (!string.IsNullOrEmpty(strSessionTimeoutSeconds)) - session.User.SessionTimeoutSeconds = int.Parse(strSessionTimeoutSeconds); + if (request.TryGetQueryOrForm("displayName", out string displayName)) + session.User.DisplayName = displayName; - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + session.User.Username + "] User profile was updated successfully."); + if (request.TryGetQueryOrForm("sessionTimeoutSeconds", int.Parse, out int sessionTimeoutSeconds)) + session.User.SessionTimeoutSeconds = sessionTimeoutSeconds; - _dnsWebService.AuthManager.SaveConfigFile(); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] User profile was updated successfully."); + _dnsWebService._authManager.SaveConfigFile(); + + Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); WriteUserDetails(jsonWriter, session.User, session, true, false); } - public void ListSessions(HttpListenerRequest request, JsonTextWriter jsonWriter) + public void ListSessions(HttpContext context) { - UserSession session = _dnsWebService.GetSession(request); + UserSession session = context.GetCurrentSession(); + + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, session.User, PermissionFlag.View)) + throw new DnsWebServiceException("Access was denied."); + + Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); jsonWriter.WritePropertyName("sessions"); jsonWriter.WriteStartArray(); - List sessions = new List(_dnsWebService.AuthManager.Sessions); + List sessions = new List(_dnsWebService._authManager.Sessions); sessions.Sort(); foreach (UserSession activeSession in sessions) @@ -485,48 +400,50 @@ namespace DnsServerCore jsonWriter.WriteEndArray(); } - public void CreateApiToken(HttpListenerRequest request, JsonTextWriter jsonWriter) + public void CreateApiToken(HttpContext context) { - string strUsername = request.QueryString["user"]; - if (string.IsNullOrEmpty(strUsername)) - throw new DnsWebServiceException("Parameter 'user' missing."); + UserSession session = context.GetCurrentSession(); - string strTokenName = request.QueryString["tokenName"]; - if (string.IsNullOrEmpty(strTokenName)) - throw new DnsWebServiceException("Parameter 'tokenName' missing."); + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, session.User, PermissionFlag.Modify)) + throw new DnsWebServiceException("Access was denied."); - IPEndPoint remoteEP = DnsWebService.GetRequestRemoteEndPoint(request); + HttpRequest request = context.Request; - UserSession session = _dnsWebService.AuthManager.CreateApiToken(strTokenName, strUsername, remoteEP.Address, request.UserAgent); + string username = request.GetQueryOrForm("user"); + string tokenName = request.GetQueryOrForm("tokenName"); - _dnsWebService.Log.Write(remoteEP, "[" + session.User.Username + "] API token [" + strTokenName + "] was created successfully for user: " + strUsername); + IPEndPoint remoteEP = context.GetRemoteEndPoint(); - _dnsWebService.AuthManager.SaveConfigFile(); + UserSession createdSession = _dnsWebService._authManager.CreateApiToken(tokenName, username, remoteEP.Address, request.Headers.UserAgent); - jsonWriter.WritePropertyName("username"); - jsonWriter.WriteValue(session.User.Username); + _dnsWebService._log.Write(remoteEP, "[" + session.User.Username + "] API token [" + tokenName + "] was created successfully for user: " + username); - jsonWriter.WritePropertyName("tokenName"); - jsonWriter.WriteValue(session.TokenName); + _dnsWebService._authManager.SaveConfigFile(); - jsonWriter.WritePropertyName("token"); - jsonWriter.WriteValue(session.Token); + Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); + + jsonWriter.WriteString("username", createdSession.User.Username); + jsonWriter.WriteString("tokenName", createdSession.TokenName); + jsonWriter.WriteString("token", createdSession.Token); } - public void DeleteSession(HttpListenerRequest request, bool isAdminContext) + public void DeleteSession(HttpContext context, bool isAdminContext) { - string strPartialToken = request.QueryString["partialToken"]; - if (string.IsNullOrEmpty(strPartialToken)) - throw new DnsWebServiceException("Parameter 'partialToken' missing."); + UserSession session = context.GetCurrentSession(); - UserSession session = _dnsWebService.GetSession(request); + if (isAdminContext) + { + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, session.User, PermissionFlag.Delete)) + throw new DnsWebServiceException("Access was denied."); + } + string strPartialToken = context.Request.GetQueryOrForm("partialToken"); if (session.Token.StartsWith(strPartialToken)) throw new InvalidOperationException("Invalid operation: cannot delete current session."); string token = null; - foreach (UserSession activeSession in _dnsWebService.AuthManager.Sessions) + foreach (UserSession activeSession in _dnsWebService._authManager.Sessions) { if (activeSession.Token.StartsWith(strPartialToken)) { @@ -540,23 +457,30 @@ namespace DnsServerCore if (!isAdminContext) { - UserSession sessionToDelete = _dnsWebService.AuthManager.GetSession(token); + UserSession sessionToDelete = _dnsWebService._authManager.GetSession(token); if (sessionToDelete.User != session.User) throw new DnsWebServiceException("Access was denied."); } - UserSession deletedSession = _dnsWebService.AuthManager.DeleteSession(token); + UserSession deletedSession = _dnsWebService._authManager.DeleteSession(token); - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + session.User.Username + "] User session [" + strPartialToken + "] was deleted successfully for user: " + deletedSession.User.Username); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] User session [" + strPartialToken + "] was deleted successfully for user: " + deletedSession.User.Username); - _dnsWebService.AuthManager.SaveConfigFile(); + _dnsWebService._authManager.SaveConfigFile(); } - public void ListUsers(JsonTextWriter jsonWriter) + public void ListUsers(HttpContext context) { - List users = new List(_dnsWebService.AuthManager.Users); + UserSession session = context.GetCurrentSession(); + + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, session.User, PermissionFlag.View)) + throw new DnsWebServiceException("Access was denied."); + + List users = new List(_dnsWebService._authManager.Users); users.Sort(); + Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); + jsonWriter.WritePropertyName("users"); jsonWriter.WriteStartArray(); @@ -572,108 +496,102 @@ namespace DnsServerCore jsonWriter.WriteEndArray(); } - public void CreateUser(HttpListenerRequest request, JsonTextWriter jsonWriter) + public void CreateUser(HttpContext context) { - string strDisplayName = request.QueryString["displayName"]; + UserSession session = context.GetCurrentSession(); - string strUsername = request.QueryString["user"]; - if (string.IsNullOrEmpty(strUsername)) - throw new DnsWebServiceException("Parameter 'user' missing."); + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, session.User, PermissionFlag.Modify)) + throw new DnsWebServiceException("Access was denied."); - string strPassword = request.QueryString["pass"]; - if (string.IsNullOrEmpty(strPassword)) - throw new DnsWebServiceException("Parameter 'pass' missing."); + HttpRequest request = context.Request; - User user = _dnsWebService.AuthManager.CreateUser(strDisplayName, strUsername, strPassword); + string displayName = request.QueryOrForm("displayName"); + string username = request.GetQueryOrForm("user"); + string password = request.GetQueryOrForm("pass"); - UserSession session = _dnsWebService.GetSession(request); + User user = _dnsWebService._authManager.CreateUser(displayName, username, password); - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + session.User.Username + "] User account was created successfully with username: " + user.Username); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] User account was created successfully with username: " + user.Username); - _dnsWebService.AuthManager.SaveConfigFile(); + _dnsWebService._authManager.SaveConfigFile(); + Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); WriteUserDetails(jsonWriter, user, null, false, false); } - public void GetUserDetails(HttpListenerRequest request, JsonTextWriter jsonWriter) + public void GetUserDetails(HttpContext context) { - string strUsername = request.QueryString["user"]; - if (string.IsNullOrEmpty(strUsername)) - throw new DnsWebServiceException("Parameter 'user' missing."); + UserSession session = context.GetCurrentSession(); - bool includeGroups; - string strIncludeGroups = request.QueryString["includeGroups"]; - if (string.IsNullOrEmpty(strIncludeGroups)) - includeGroups = false; - else - includeGroups = bool.Parse(strIncludeGroups); + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, session.User, PermissionFlag.View)) + throw new DnsWebServiceException("Access was denied."); - User user = _dnsWebService.AuthManager.GetUser(strUsername); + HttpRequest request = context.Request; + + string username = request.GetQueryOrForm("user"); + bool includeGroups = request.GetQueryOrForm("includeGroups", bool.Parse, false); + + User user = _dnsWebService._authManager.GetUser(username); if (user is null) - throw new DnsWebServiceException("No such user exists: " + strUsername); + throw new DnsWebServiceException("No such user exists: " + username); + Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); WriteUserDetails(jsonWriter, user, null, true, includeGroups); } - public void SetUserDetails(HttpListenerRequest request, JsonTextWriter jsonWriter) + public void SetUserDetails(HttpContext context) { - string strUsername = request.QueryString["user"]; - if (string.IsNullOrEmpty(strUsername)) - throw new DnsWebServiceException("Parameter 'user' missing."); + UserSession session = context.GetCurrentSession(); - User user = _dnsWebService.AuthManager.GetUser(strUsername); + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, session.User, PermissionFlag.Modify)) + throw new DnsWebServiceException("Access was denied."); + + HttpRequest request = context.Request; + + string username = request.GetQueryOrForm("user"); + + User user = _dnsWebService._authManager.GetUser(username); if (user is null) - throw new DnsWebServiceException("No such user exists: " + strUsername); + throw new DnsWebServiceException("No such user exists: " + username); - string strDisplayName = request.QueryString["displayName"]; - if (!string.IsNullOrEmpty(strDisplayName)) - user.DisplayName = strDisplayName; + if (request.TryGetQueryOrForm("displayName", out string displayName)) + user.DisplayName = displayName; - string strNewUsername = request.QueryString["newUser"]; - if (!string.IsNullOrEmpty(strNewUsername)) - _dnsWebService.AuthManager.ChangeUsername(user, strNewUsername); + if (request.TryGetQueryOrForm("newUser", out string newUsername)) + _dnsWebService._authManager.ChangeUsername(user, newUsername); - UserSession session = _dnsWebService.GetSession(request); - - string strDisabled = request.QueryString["disabled"]; - if (!string.IsNullOrEmpty(strDisabled) && (session.User != user)) //to avoid self lockout + if (request.TryGetQueryOrForm("disabled", bool.Parse, out bool disabled) && (session.User != user)) //to avoid self lockout { - user.Disabled = bool.Parse(strDisabled); + user.Disabled = disabled; if (user.Disabled) { - foreach (UserSession userSession in _dnsWebService.AuthManager.Sessions) + foreach (UserSession userSession in _dnsWebService._authManager.Sessions) { if (userSession.Type == UserSessionType.ApiToken) continue; if (userSession.User == user) - _dnsWebService.AuthManager.DeleteSession(userSession.Token); + _dnsWebService._authManager.DeleteSession(userSession.Token); } } } - string strSessionTimeoutSeconds = request.QueryString["sessionTimeoutSeconds"]; - if (!string.IsNullOrEmpty(strSessionTimeoutSeconds)) - user.SessionTimeoutSeconds = int.Parse(strSessionTimeoutSeconds); + if (request.TryGetQueryOrForm("sessionTimeoutSeconds", int.Parse, out int sessionTimeoutSeconds)) + user.SessionTimeoutSeconds = sessionTimeoutSeconds; - string strNewPassword = request.QueryString["newPass"]; - if (!string.IsNullOrWhiteSpace(strNewPassword)) + string newPassword = request.QueryOrForm("newPass"); + if (!string.IsNullOrWhiteSpace(newPassword)) { - int iterations; - string strIterations = request.QueryString["iterations"]; - if (string.IsNullOrEmpty(strIterations)) - iterations = User.DEFAULT_ITERATIONS; - else - iterations = int.Parse(strIterations); + int iterations = request.GetQueryOrForm("iterations", int.Parse, User.DEFAULT_ITERATIONS); - user.ChangePassword(strNewPassword, iterations); + user.ChangePassword(newPassword, iterations); } - string strMemberOfGroups = request.QueryString["memberOfGroups"]; - if (strMemberOfGroups is not null) + string memberOfGroups = request.QueryOrForm("memberOfGroups"); + if (memberOfGroups is not null) { - string[] parts = strMemberOfGroups.Split(','); + string[] parts = memberOfGroups.Split(','); Dictionary groups = new Dictionary(parts.Length); foreach (string part in parts) @@ -681,7 +599,7 @@ namespace DnsServerCore if (part.Length == 0) continue; - Group group = _dnsWebService.AuthManager.GetGroup(part); + Group group = _dnsWebService._authManager.GetGroup(part); if (group is null) throw new DnsWebServiceException("No such group exists: " + part); @@ -689,50 +607,59 @@ namespace DnsServerCore } //ensure user is member of everyone group - Group everyone = _dnsWebService.AuthManager.GetGroup(Group.EVERYONE); + Group everyone = _dnsWebService._authManager.GetGroup(Group.EVERYONE); groups[everyone.Name.ToLower()] = everyone; if (session.User == user) { //ensure current admin user is member of administrators group to avoid self lockout - Group admins = _dnsWebService.AuthManager.GetGroup(Group.ADMINISTRATORS); + Group admins = _dnsWebService._authManager.GetGroup(Group.ADMINISTRATORS); groups[admins.Name.ToLower()] = admins; } user.SyncGroups(groups); } - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + session.User.Username + "] User account details were updated successfully for user: " + strUsername); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] User account details were updated successfully for user: " + username); - _dnsWebService.AuthManager.SaveConfigFile(); + _dnsWebService._authManager.SaveConfigFile(); + Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); WriteUserDetails(jsonWriter, user, null, true, false); } - public void DeleteUser(HttpListenerRequest request) + public void DeleteUser(HttpContext context) { - string strUsername = request.QueryString["user"]; - if (string.IsNullOrEmpty(strUsername)) - throw new DnsWebServiceException("Parameter 'user' missing."); + UserSession session = context.GetCurrentSession(); - UserSession session = _dnsWebService.GetSession(request); + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, session.User, PermissionFlag.Delete)) + throw new DnsWebServiceException("Access was denied."); - if (session.User.Username.Equals(strUsername, StringComparison.OrdinalIgnoreCase)) + string username = context.Request.GetQueryOrForm("user"); + + if (session.User.Username.Equals(username, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("Invalid operation: cannot delete current user."); - if (!_dnsWebService.AuthManager.DeleteUser(strUsername)) - throw new DnsWebServiceException("Failed to delete user: " + strUsername); + if (!_dnsWebService._authManager.DeleteUser(username)) + throw new DnsWebServiceException("Failed to delete user: " + username); - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + session.User.Username + "] User account was deleted successfully with username: " + strUsername); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] User account was deleted successfully with username: " + username); - _dnsWebService.AuthManager.SaveConfigFile(); + _dnsWebService._authManager.SaveConfigFile(); } - public void ListGroups(JsonTextWriter jsonWriter) + public void ListGroups(HttpContext context) { - List groups = new List(_dnsWebService.AuthManager.Groups); + UserSession session = context.GetCurrentSession(); + + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, session.User, PermissionFlag.View)) + throw new DnsWebServiceException("Access was denied."); + + List groups = new List(_dnsWebService._authManager.Groups); groups.Sort(); + Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); + jsonWriter.WritePropertyName("groups"); jsonWriter.WriteStartArray(); @@ -751,71 +678,73 @@ namespace DnsServerCore jsonWriter.WriteEndArray(); } - public void CreateGroup(HttpListenerRequest request, JsonTextWriter jsonWriter) + public void CreateGroup(HttpContext context) { - string strGroup = request.QueryString["group"]; - if (string.IsNullOrEmpty(strGroup)) - throw new DnsWebServiceException("Parameter 'group' missing."); + UserSession session = context.GetCurrentSession(); - string strDescription = request.QueryString["description"]; - if (string.IsNullOrEmpty(strDescription)) - strDescription = ""; + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, session.User, PermissionFlag.Modify)) + throw new DnsWebServiceException("Access was denied."); - Group group = _dnsWebService.AuthManager.CreateGroup(strGroup, strDescription); + HttpRequest request = context.Request; - UserSession session = _dnsWebService.GetSession(request); + string groupName = request.GetQueryOrForm("group"); + string description = request.GetQueryOrForm("description", ""); - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + session.User.Username + "] Group was created successfully with name: " + group.Name); + Group group = _dnsWebService._authManager.CreateGroup(groupName, description); - _dnsWebService.AuthManager.SaveConfigFile(); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] Group was created successfully with name: " + group.Name); + _dnsWebService._authManager.SaveConfigFile(); + + Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); WriteGroupDetails(jsonWriter, group, false, false); } - public void GetGroupDetails(HttpListenerRequest request, JsonTextWriter jsonWriter) + public void GetGroupDetails(HttpContext context) { - string strGroup = request.QueryString["group"]; - if (string.IsNullOrEmpty(strGroup)) - throw new DnsWebServiceException("Parameter 'group' missing."); + UserSession session = context.GetCurrentSession(); - bool includeUsers; - string strIncludeGroups = request.QueryString["includeUsers"]; - if (string.IsNullOrEmpty(strIncludeGroups)) - includeUsers = false; - else - includeUsers = bool.Parse(strIncludeGroups); + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, session.User, PermissionFlag.View)) + throw new DnsWebServiceException("Access was denied."); - Group group = _dnsWebService.AuthManager.GetGroup(strGroup); + HttpRequest request = context.Request; + + string groupName = request.GetQueryOrForm("group"); + bool includeUsers = request.GetQueryOrForm("includeUsers", bool.Parse, false); + + Group group = _dnsWebService._authManager.GetGroup(groupName); if (group is null) - throw new DnsWebServiceException("No such group exists: " + strGroup); + throw new DnsWebServiceException("No such group exists: " + groupName); + Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); WriteGroupDetails(jsonWriter, group, true, includeUsers); } - public void SetGroupDetails(HttpListenerRequest request, JsonTextWriter jsonWriter) + public void SetGroupDetails(HttpContext context) { - string strGroup = request.QueryString["group"]; - if (string.IsNullOrEmpty(strGroup)) - throw new DnsWebServiceException("Parameter 'group' missing."); + UserSession session = context.GetCurrentSession(); - Group group = _dnsWebService.AuthManager.GetGroup(strGroup); + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, session.User, PermissionFlag.Modify)) + throw new DnsWebServiceException("Access was denied."); + + HttpRequest request = context.Request; + + string groupName = request.GetQueryOrForm("group"); + + Group group = _dnsWebService._authManager.GetGroup(groupName); if (group is null) - throw new DnsWebServiceException("No such group exists: " + strGroup); + throw new DnsWebServiceException("No such group exists: " + groupName); - string strNewGroup = request.QueryString["newGroup"]; - if (!string.IsNullOrEmpty(strNewGroup)) - _dnsWebService.AuthManager.RenameGroup(group, strNewGroup); + if (request.TryGetQueryOrForm("newGroup", out string newGroup)) + _dnsWebService._authManager.RenameGroup(group, newGroup); - string strDescription = request.QueryString["description"]; - if (!string.IsNullOrEmpty(strDescription)) - group.Description = strDescription; + if (request.TryGetQueryOrForm("description", out string description)) + group.Description = description; - UserSession session = _dnsWebService.GetSession(request); - - string strMembers = request.QueryString["members"]; - if (strMembers is not null) + string members = request.QueryOrForm("members"); + if (members is not null) { - string[] parts = strMembers.Split(','); + string[] parts = members.Split(','); Dictionary users = new Dictionary(); foreach (string part in parts) @@ -823,7 +752,7 @@ namespace DnsServerCore if (part.Length == 0) continue; - User user = _dnsWebService.AuthManager.GetUser(part); + User user = _dnsWebService._authManager.GetUser(part); if (user is null) throw new DnsWebServiceException("No such user exists: " + part); @@ -833,37 +762,46 @@ namespace DnsServerCore if (group.Name.Equals("administrators", StringComparison.OrdinalIgnoreCase)) users[session.User.Username] = session.User; //ensure current admin user is member of administrators group to avoid self lockout - _dnsWebService.AuthManager.SyncGroupMembers(group, users); + _dnsWebService._authManager.SyncGroupMembers(group, users); } - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + session.User.Username + "] Group details were updated successfully for group: " + strGroup); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] Group details were updated successfully for group: " + groupName); - _dnsWebService.AuthManager.SaveConfigFile(); + _dnsWebService._authManager.SaveConfigFile(); + Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); WriteGroupDetails(jsonWriter, group, true, false); } - public void DeleteGroup(HttpListenerRequest request) + public void DeleteGroup(HttpContext context) { - string strGroup = request.QueryString["group"]; - if (string.IsNullOrEmpty(strGroup)) - throw new DnsWebServiceException("Parameter 'group' missing."); + UserSession session = context.GetCurrentSession(); - if (!_dnsWebService.AuthManager.DeleteGroup(strGroup)) - throw new DnsWebServiceException("Failed to delete group: " + strGroup); + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, session.User, PermissionFlag.Delete)) + throw new DnsWebServiceException("Access was denied."); - UserSession session = _dnsWebService.GetSession(request); + string groupName = context.Request.GetQueryOrForm("group"); - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + session.User.Username + "] Group was deleted successfully with name: " + strGroup); + if (!_dnsWebService._authManager.DeleteGroup(groupName)) + throw new DnsWebServiceException("Failed to delete group: " + groupName); - _dnsWebService.AuthManager.SaveConfigFile(); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] Group was deleted successfully with name: " + groupName); + + _dnsWebService._authManager.SaveConfigFile(); } - public void ListPermissions(JsonTextWriter jsonWriter) + public void ListPermissions(HttpContext context) { - List permissions = new List(_dnsWebService.AuthManager.Permissions); + UserSession session = context.GetCurrentSession(); + + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, session.User, PermissionFlag.View)) + throw new DnsWebServiceException("Access was denied."); + + List permissions = new List(_dnsWebService._authManager.Permissions); permissions.Sort(); + Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); + jsonWriter.WritePropertyName("permissions"); jsonWriter.WriteStartArray(); @@ -879,111 +817,97 @@ namespace DnsServerCore jsonWriter.WriteEndArray(); } - public void GetPermissionDetails(HttpListenerRequest request, JsonTextWriter jsonWriter, PermissionSection section) + public void GetPermissionDetails(HttpContext context, PermissionSection section) { - if (section == PermissionSection.Unknown) - { - string strSection = request.QueryString["section"]; - if (string.IsNullOrEmpty(strSection)) - throw new DnsWebServiceException("Parameter 'section' missing."); - - if (!Enum.TryParse(strSection, true, out section)) - throw new DnsWebServiceException("No such permission section exists: " + strSection); - } - - string strSubItem; + UserSession session = context.GetCurrentSession(); + HttpRequest request = context.Request; + string strSubItem = null; switch (section) { + case PermissionSection.Unknown: + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, session.User, PermissionFlag.View)) + throw new DnsWebServiceException("Access was denied."); + + section = request.GetQueryOrFormEnum("section"); + break; + case PermissionSection.Zones: - strSubItem = request.QueryString["zone"]; - - if (strSubItem is not null) - strSubItem = strSubItem.TrimEnd('.'); + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, session.User, PermissionFlag.Modify)) + throw new DnsWebServiceException("Access was denied."); + strSubItem = request.GetQueryOrForm("zone").TrimEnd('.'); break; default: - strSubItem = null; - break; + throw new InvalidOperationException(); } - bool includeUsersAndGroups; - string strIncludeUsersAndGroups = request.QueryString["includeUsersAndGroups"]; - if (string.IsNullOrEmpty(strIncludeUsersAndGroups)) - includeUsersAndGroups = false; - else - includeUsersAndGroups = bool.Parse(strIncludeUsersAndGroups); + bool includeUsersAndGroups = request.GetQueryOrForm("includeUsersAndGroups", bool.Parse, false); if (strSubItem is not null) { - UserSession session = _dnsWebService.GetSession(request); - - if (!_dnsWebService.AuthManager.IsPermitted(section, strSubItem, session.User, PermissionFlag.View)) + if (!_dnsWebService._authManager.IsPermitted(section, strSubItem, session.User, PermissionFlag.View)) throw new DnsWebServiceException("Access was denied."); } Permission permission; if (strSubItem is null) - permission = _dnsWebService.AuthManager.GetPermission(section); + permission = _dnsWebService._authManager.GetPermission(section); else - permission = _dnsWebService.AuthManager.GetPermission(section, strSubItem); + permission = _dnsWebService._authManager.GetPermission(section, strSubItem); if (permission is null) throw new DnsWebServiceException("No permissions exists for section: " + section.ToString() + (strSubItem is null ? "" : "/" + strSubItem)); + Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); WritePermissionDetails(jsonWriter, permission, strSubItem, includeUsersAndGroups); } - public void SetPermissionsDetails(HttpListenerRequest request, JsonTextWriter jsonWriter, PermissionSection section) + public void SetPermissionsDetails(HttpContext context, PermissionSection section) { - if (section == PermissionSection.Unknown) - { - string strSection = request.QueryString["section"]; - if (string.IsNullOrEmpty(strSection)) - throw new DnsWebServiceException("Parameter 'section' missing."); - - if (!Enum.TryParse(strSection, true, out section)) - throw new DnsWebServiceException("No such permission section exists: " + strSection); - } - - string strSubItem; + UserSession session = context.GetCurrentSession(); + HttpRequest request = context.Request; + string strSubItem = null; switch (section) { + case PermissionSection.Unknown: + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, session.User, PermissionFlag.Delete)) + throw new DnsWebServiceException("Access was denied."); + + section = request.GetQueryOrFormEnum("section"); + break; + case PermissionSection.Zones: - strSubItem = request.QueryString["zone"]; - - if (strSubItem is not null) - strSubItem = strSubItem.TrimEnd('.'); + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, session.User, PermissionFlag.Modify)) + throw new DnsWebServiceException("Access was denied."); + strSubItem = request.GetQueryOrForm("zone").TrimEnd('.'); break; default: - strSubItem = null; - break; + throw new InvalidOperationException(); } - UserSession session = _dnsWebService.GetSession(request); - if (strSubItem is not null) { - if (!_dnsWebService.AuthManager.IsPermitted(section, strSubItem, session.User, PermissionFlag.Delete)) + if (!_dnsWebService._authManager.IsPermitted(section, strSubItem, session.User, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); } Permission permission; if (strSubItem is null) - permission = _dnsWebService.AuthManager.GetPermission(section); + permission = _dnsWebService._authManager.GetPermission(section); else - permission = _dnsWebService.AuthManager.GetPermission(section, strSubItem); + permission = _dnsWebService._authManager.GetPermission(section, strSubItem); if (permission is null) throw new DnsWebServiceException("No permissions exists for section: " + section.ToString() + (strSubItem is null ? "" : "/" + strSubItem)); - string strUserPermissions = request.QueryString["userPermissions"]; + string strUserPermissions = request.QueryOrForm("userPermissions"); if (strUserPermissions is not null) { string[] parts = strUserPermissions.Split('|'); @@ -994,7 +918,7 @@ namespace DnsServerCore if (parts[i].Length == 0) continue; - User user = _dnsWebService.AuthManager.GetUser(parts[i]); + User user = _dnsWebService._authManager.GetUser(parts[i]); bool canView = bool.Parse(parts[i + 1]); bool canModify = bool.Parse(parts[i + 2]); bool canDelete = bool.Parse(parts[i + 3]); @@ -1019,7 +943,7 @@ namespace DnsServerCore permission.SyncPermissions(userPermissions); } - string strGroupPermissions = request.QueryString["groupPermissions"]; + string strGroupPermissions = request.QueryOrForm("groupPermissions"); if (strGroupPermissions is not null) { string[] parts = strGroupPermissions.Split('|'); @@ -1030,7 +954,7 @@ namespace DnsServerCore if (parts[i].Length == 0) continue; - Group group = _dnsWebService.AuthManager.GetGroup(parts[i]); + Group group = _dnsWebService._authManager.GetGroup(parts[i]); bool canView = bool.Parse(parts[i + 1]); bool canModify = bool.Parse(parts[i + 2]); bool canDelete = bool.Parse(parts[i + 3]); @@ -1053,20 +977,20 @@ namespace DnsServerCore } //ensure administrators group always has all permissions - Group admins = _dnsWebService.AuthManager.GetGroup(Group.ADMINISTRATORS); + Group admins = _dnsWebService._authManager.GetGroup(Group.ADMINISTRATORS); groupPermissions[admins] = PermissionFlag.ViewModifyDelete; switch (section) { case PermissionSection.Zones: //ensure DNS administrators group always has all permissions - Group dnsAdmins = _dnsWebService.AuthManager.GetGroup(Group.DNS_ADMINISTRATORS); + Group dnsAdmins = _dnsWebService._authManager.GetGroup(Group.DNS_ADMINISTRATORS); groupPermissions[dnsAdmins] = PermissionFlag.ViewModifyDelete; break; case PermissionSection.DhcpServer: //ensure DHCP administrators group always has all permissions - Group dhcpAdmins = _dnsWebService.AuthManager.GetGroup(Group.DHCP_ADMINISTRATORS); + Group dhcpAdmins = _dnsWebService._authManager.GetGroup(Group.DHCP_ADMINISTRATORS); groupPermissions[dhcpAdmins] = PermissionFlag.ViewModifyDelete; break; } @@ -1074,10 +998,11 @@ namespace DnsServerCore permission.SyncPermissions(groupPermissions); } - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + session.User.Username + "] Permissions were updated successfully for section: " + section.ToString() + (string.IsNullOrEmpty(strSubItem) ? "" : "/" + strSubItem)); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] Permissions were updated successfully for section: " + section.ToString() + (string.IsNullOrEmpty(strSubItem) ? "" : "/" + strSubItem)); - _dnsWebService.AuthManager.SaveConfigFile(); + _dnsWebService._authManager.SaveConfigFile(); + Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); WritePermissionDetails(jsonWriter, permission, strSubItem, false); } diff --git a/DnsServerCore/WebServiceDashboardApi.cs b/DnsServerCore/WebServiceDashboardApi.cs index f30d6d0a..f15ab21d 100644 --- a/DnsServerCore/WebServiceDashboardApi.cs +++ b/DnsServerCore/WebServiceDashboardApi.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -17,12 +17,14 @@ along with this program. If not, see . */ +using DnsServerCore.Auth; using DnsServerCore.Dns; -using Newtonsoft.Json; +using Microsoft.AspNetCore.Http; using System; using System.Collections.Generic; using System.Globalization; using System.Net; +using System.Text.Json; using System.Threading.Tasks; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; @@ -48,29 +50,22 @@ namespace DnsServerCore #region private - private static void WriteChartDataSet(JsonTextWriter jsonWriter, string label, string backgroundColor, string borderColor, List> statsPerInterval) + private static void WriteChartDataSet(Utf8JsonWriter jsonWriter, string label, string backgroundColor, string borderColor, List> statsPerInterval) { jsonWriter.WriteStartObject(); - jsonWriter.WritePropertyName("label"); - jsonWriter.WriteValue(label); - - jsonWriter.WritePropertyName("backgroundColor"); - jsonWriter.WriteValue(backgroundColor); - - jsonWriter.WritePropertyName("borderColor"); - jsonWriter.WriteValue(borderColor); - - jsonWriter.WritePropertyName("borderWidth"); - jsonWriter.WriteValue(2); - - jsonWriter.WritePropertyName("fill"); - jsonWriter.WriteValue(true); + jsonWriter.WriteString("label", label); + jsonWriter.WriteString("backgroundColor", backgroundColor); + jsonWriter.WriteString("borderColor", borderColor); + jsonWriter.WriteNumber("borderWidth", 2); + jsonWriter.WriteBoolean("fill", true); jsonWriter.WritePropertyName("data"); jsonWriter.WriteStartArray(); + foreach (KeyValuePair item in statsPerInterval) - jsonWriter.WriteValue(item.Value); + jsonWriter.WriteNumberValue(item.Value); + jsonWriter.WriteEndArray(); jsonWriter.WriteEndObject(); @@ -128,28 +123,23 @@ namespace DnsServerCore #region public - public async Task GetStats(HttpListenerRequest request, JsonTextWriter jsonWriter) + public async Task GetStats(HttpContext context) { - string strType = request.QueryString["type"]; - if (string.IsNullOrEmpty(strType)) - strType = "lastHour"; + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Dashboard, context.GetCurrentSession().User, PermissionFlag.View)) + throw new DnsWebServiceException("Access was denied."); - bool utcFormat; - string strUtcFormat = request.QueryString["utc"]; - if (string.IsNullOrEmpty(strUtcFormat)) - utcFormat = false; - else - utcFormat = bool.Parse(strUtcFormat); + HttpRequest request = context.Request; + + string strType = request.GetQueryOrForm("type", "lastHour"); + bool utcFormat = request.GetQueryOrForm("utc", bool.Parse, false); + + bool isLanguageEnUs = true; + string acceptLanguage = request.Headers["Accept-Language"]; + if (!string.IsNullOrEmpty(acceptLanguage)) + isLanguageEnUs = acceptLanguage.StartsWith("en-us", StringComparison.OrdinalIgnoreCase); Dictionary>> data; string labelFormat; - bool isLanguageEnUs; - - string acceptLanguage = request.Headers["Accept-Language"]; - if (string.IsNullOrEmpty(acceptLanguage)) - isLanguageEnUs = true; - else - isLanguageEnUs = acceptLanguage.StartsWith("en-us", StringComparison.OrdinalIgnoreCase); switch (strType.ToLower()) { @@ -194,13 +184,8 @@ namespace DnsServerCore break; case "custom": - string strStartDate = request.QueryString["start"]; - if (string.IsNullOrEmpty(strStartDate)) - throw new DnsWebServiceException("Parameter 'start' missing."); - - string strEndDate = request.QueryString["end"]; - if (string.IsNullOrEmpty(strEndDate)) - throw new DnsWebServiceException("Parameter 'end' missing."); + string strStartDate = request.GetQueryOrForm("start"); + string strEndDate = request.GetQueryOrForm("end"); if (!DateTime.TryParse(strStartDate, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out DateTime startDate)) throw new DnsWebServiceException("Invalid start date format."); @@ -236,6 +221,8 @@ namespace DnsServerCore throw new DnsWebServiceException("Unknown stats type requested: " + strType); } + Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); + //stats { List> stats = data["stats"]; @@ -244,25 +231,13 @@ namespace DnsServerCore jsonWriter.WriteStartObject(); foreach (KeyValuePair item in stats) - { - jsonWriter.WritePropertyName(item.Key); - jsonWriter.WriteValue(item.Value); - } + jsonWriter.WriteNumber(item.Key, item.Value); - jsonWriter.WritePropertyName("zones"); - jsonWriter.WriteValue(_dnsWebService.DnsServer.AuthZoneManager.TotalZones); - - jsonWriter.WritePropertyName("cachedEntries"); - jsonWriter.WriteValue(_dnsWebService.DnsServer.CacheZoneManager.TotalEntries); - - jsonWriter.WritePropertyName("allowedZones"); - jsonWriter.WriteValue(_dnsWebService.DnsServer.AllowedZoneManager.TotalZonesAllowed); - - jsonWriter.WritePropertyName("blockedZones"); - jsonWriter.WriteValue(_dnsWebService.DnsServer.BlockedZoneManager.TotalZonesBlocked); - - jsonWriter.WritePropertyName("blockListZones"); - jsonWriter.WriteValue(_dnsWebService.DnsServer.BlockListZoneManager.TotalZonesBlocked); + jsonWriter.WriteNumber("zones", _dnsWebService.DnsServer.AuthZoneManager.TotalZones); + jsonWriter.WriteNumber("cachedEntries", _dnsWebService.DnsServer.CacheZoneManager.TotalEntries); + jsonWriter.WriteNumber("allowedZones", _dnsWebService.DnsServer.AllowedZoneManager.TotalZonesAllowed); + jsonWriter.WriteNumber("blockedZones", _dnsWebService.DnsServer.BlockedZoneManager.TotalZonesBlocked); + jsonWriter.WriteNumber("blockListZones", _dnsWebService.DnsServer.BlockListZoneManager.TotalZonesBlocked); jsonWriter.WriteEndObject(); } @@ -274,8 +249,7 @@ namespace DnsServerCore //label format { - jsonWriter.WritePropertyName("labelFormat"); - jsonWriter.WriteValue(labelFormat); + jsonWriter.WriteString("labelFormat", labelFormat); } //label @@ -286,7 +260,7 @@ namespace DnsServerCore jsonWriter.WriteStartArray(); foreach (KeyValuePair item in statsPerInterval) - jsonWriter.WriteValue(item.Key); + jsonWriter.WriteStringValue(item.Key); jsonWriter.WriteEndArray(); } @@ -332,19 +306,19 @@ namespace DnsServerCore switch (item.Key) { case "totalAuthoritative": - jsonWriter.WriteValue("Authoritative"); + jsonWriter.WriteStringValue("Authoritative"); break; case "totalRecursive": - jsonWriter.WriteValue("Recursive"); + jsonWriter.WriteStringValue("Recursive"); break; case "totalCached": - jsonWriter.WriteValue("Cached"); + jsonWriter.WriteStringValue("Cached"); break; case "totalBlocked": - jsonWriter.WriteValue("Blocked"); + jsonWriter.WriteStringValue("Blocked"); break; } } @@ -370,7 +344,7 @@ namespace DnsServerCore case "totalRecursive": case "totalCached": case "totalBlocked": - jsonWriter.WriteValue(item.Value); + jsonWriter.WriteNumberValue(item.Value); break; } } @@ -379,10 +353,10 @@ namespace DnsServerCore jsonWriter.WritePropertyName("backgroundColor"); jsonWriter.WriteStartArray(); - jsonWriter.WriteValue("rgba(150, 150, 0, 0.5)"); - jsonWriter.WriteValue("rgba(23, 162, 184, 0.5)"); - jsonWriter.WriteValue("rgba(111, 84, 153, 0.5)"); - jsonWriter.WriteValue("rgba(255, 165, 0, 0.5)"); + jsonWriter.WriteStringValue("rgba(150, 150, 0, 0.5)"); + jsonWriter.WriteStringValue("rgba(23, 162, 184, 0.5)"); + jsonWriter.WriteStringValue("rgba(111, 84, 153, 0.5)"); + jsonWriter.WriteStringValue("rgba(255, 165, 0, 0.5)"); jsonWriter.WriteEndArray(); jsonWriter.WriteEndObject(); @@ -406,7 +380,7 @@ namespace DnsServerCore jsonWriter.WriteStartArray(); foreach (KeyValuePair item in queryTypes) - jsonWriter.WriteValue(item.Key); + jsonWriter.WriteStringValue(item.Key); jsonWriter.WriteEndArray(); } @@ -420,22 +394,24 @@ namespace DnsServerCore jsonWriter.WritePropertyName("data"); jsonWriter.WriteStartArray(); + foreach (KeyValuePair item in queryTypes) - jsonWriter.WriteValue(item.Value); + jsonWriter.WriteNumberValue(item.Value); + jsonWriter.WriteEndArray(); jsonWriter.WritePropertyName("backgroundColor"); jsonWriter.WriteStartArray(); - jsonWriter.WriteValue("rgba(102, 153, 255, 0.5)"); - jsonWriter.WriteValue("rgba(92, 184, 92, 0.5)"); - jsonWriter.WriteValue("rgba(7, 7, 7, 0.5)"); - jsonWriter.WriteValue("rgba(91, 192, 222, 0.5)"); - jsonWriter.WriteValue("rgba(150, 150, 0, 0.5)"); - jsonWriter.WriteValue("rgba(23, 162, 184, 0.5)"); - jsonWriter.WriteValue("rgba(111, 84, 153, 0.5)"); - jsonWriter.WriteValue("rgba(255, 165, 0, 0.5)"); - jsonWriter.WriteValue("rgba(51, 122, 183, 0.5)"); - jsonWriter.WriteValue("rgba(150, 150, 150, 0.5)"); + jsonWriter.WriteStringValue("rgba(102, 153, 255, 0.5)"); + jsonWriter.WriteStringValue("rgba(92, 184, 92, 0.5)"); + jsonWriter.WriteStringValue("rgba(7, 7, 7, 0.5)"); + jsonWriter.WriteStringValue("rgba(91, 192, 222, 0.5)"); + jsonWriter.WriteStringValue("rgba(150, 150, 0, 0.5)"); + jsonWriter.WriteStringValue("rgba(23, 162, 184, 0.5)"); + jsonWriter.WriteStringValue("rgba(111, 84, 153, 0.5)"); + jsonWriter.WriteStringValue("rgba(255, 165, 0, 0.5)"); + jsonWriter.WriteStringValue("rgba(51, 122, 183, 0.5)"); + jsonWriter.WriteStringValue("rgba(150, 150, 150, 0.5)"); jsonWriter.WriteEndArray(); jsonWriter.WriteEndObject(); @@ -459,17 +435,12 @@ namespace DnsServerCore { jsonWriter.WriteStartObject(); - jsonWriter.WritePropertyName("name"); - jsonWriter.WriteValue(item.Key); + jsonWriter.WriteString("name", item.Key); if (clientIpMap.TryGetValue(item.Key, out string clientDomain) && !string.IsNullOrEmpty(clientDomain)) - { - jsonWriter.WritePropertyName("domain"); - jsonWriter.WriteValue(clientDomain); - } + jsonWriter.WriteString("domain", clientDomain); - jsonWriter.WritePropertyName("hits"); - jsonWriter.WriteValue(item.Value); + jsonWriter.WriteNumber("hits", item.Value); jsonWriter.WriteEndObject(); } @@ -488,11 +459,8 @@ namespace DnsServerCore { jsonWriter.WriteStartObject(); - jsonWriter.WritePropertyName("name"); - jsonWriter.WriteValue(item.Key); - - jsonWriter.WritePropertyName("hits"); - jsonWriter.WriteValue(item.Value); + jsonWriter.WriteString("name", item.Key); + jsonWriter.WriteNumber("hits", item.Value); jsonWriter.WriteEndObject(); } @@ -511,11 +479,8 @@ namespace DnsServerCore { jsonWriter.WriteStartObject(); - jsonWriter.WritePropertyName("name"); - jsonWriter.WriteValue(item.Key); - - jsonWriter.WritePropertyName("hits"); - jsonWriter.WriteValue(item.Value); + jsonWriter.WriteString("name", item.Key); + jsonWriter.WriteNumber("hits", item.Value); jsonWriter.WriteEndObject(); } @@ -524,22 +489,16 @@ namespace DnsServerCore } } - public async Task GetTopStats(HttpListenerRequest request, JsonTextWriter jsonWriter) + public async Task GetTopStats(HttpContext context) { - string strType = request.QueryString["type"]; - if (string.IsNullOrEmpty(strType)) - strType = "lastHour"; + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Dashboard, context.GetCurrentSession().User, PermissionFlag.View)) + throw new DnsWebServiceException("Access was denied."); - string strStatsType = request.QueryString["statsType"]; - if (string.IsNullOrEmpty(strStatsType)) - throw new DnsWebServiceException("Parameter 'statsType' missing."); + HttpRequest request = context.Request; - string strLimit = request.QueryString["limit"]; - if (string.IsNullOrEmpty(strLimit)) - strLimit = "1000"; - - TopStatsType statsType = Enum.Parse(strStatsType, true); - int limit = int.Parse(strLimit); + string strType = request.GetQueryOrForm("type", "lastHour"); + TopStatsType statsType = request.GetQueryOrFormEnum("statsType"); + int limit = request.GetQueryOrForm("limit", int.Parse, 1000); List> topStatsData; @@ -566,13 +525,8 @@ namespace DnsServerCore break; case "custom": - string strStartDate = request.QueryString["start"]; - if (string.IsNullOrEmpty(strStartDate)) - throw new DnsWebServiceException("Parameter 'start' missing."); - - string strEndDate = request.QueryString["end"]; - if (string.IsNullOrEmpty(strEndDate)) - throw new DnsWebServiceException("Parameter 'end' missing."); + string strStartDate = request.GetQueryOrForm("start"); + string strEndDate = request.GetQueryOrForm("end"); if (!DateTime.TryParseExact(strStartDate, "yyyy-M-d", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out DateTime startDate)) throw new DnsWebServiceException("Invalid start date format."); @@ -594,6 +548,8 @@ namespace DnsServerCore throw new DnsWebServiceException("Unknown stats type requested: " + strType); } + Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); + switch (statsType) { case TopStatsType.TopClients: @@ -607,17 +563,12 @@ namespace DnsServerCore { jsonWriter.WriteStartObject(); - jsonWriter.WritePropertyName("name"); - jsonWriter.WriteValue(item.Key); + jsonWriter.WriteString("name", item.Key); if (clientIpMap.TryGetValue(item.Key, out string clientDomain) && !string.IsNullOrEmpty(clientDomain)) - { - jsonWriter.WritePropertyName("domain"); - jsonWriter.WriteValue(clientDomain); - } + jsonWriter.WriteString("domain", clientDomain); - jsonWriter.WritePropertyName("hits"); - jsonWriter.WriteValue(item.Value); + jsonWriter.WriteNumber("hits", item.Value); jsonWriter.WriteEndObject(); } @@ -635,11 +586,8 @@ namespace DnsServerCore { jsonWriter.WriteStartObject(); - jsonWriter.WritePropertyName("name"); - jsonWriter.WriteValue(item.Key); - - jsonWriter.WritePropertyName("hits"); - jsonWriter.WriteValue(item.Value); + jsonWriter.WriteString("name", item.Key); + jsonWriter.WriteNumber("hits", item.Value); jsonWriter.WriteEndObject(); } @@ -657,11 +605,8 @@ namespace DnsServerCore { jsonWriter.WriteStartObject(); - jsonWriter.WritePropertyName("name"); - jsonWriter.WriteValue(item.Key); - - jsonWriter.WritePropertyName("hits"); - jsonWriter.WriteValue(item.Value); + jsonWriter.WriteString("name", item.Key); + jsonWriter.WriteNumber("hits", item.Value); jsonWriter.WriteEndObject(); } diff --git a/DnsServerCore/WebServiceDhcpApi.cs b/DnsServerCore/WebServiceDhcpApi.cs index 5159775b..0759fc10 100644 --- a/DnsServerCore/WebServiceDhcpApi.cs +++ b/DnsServerCore/WebServiceDhcpApi.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -17,13 +17,16 @@ along with this program. If not, see . */ +using DnsServerCore.Auth; using DnsServerCore.Dhcp; using DnsServerCore.Dhcp.Options; -using Newtonsoft.Json; +using Microsoft.AspNetCore.Http; using System; using System.Collections.Generic; using System.Net; +using System.Text.Json; using System.Threading.Tasks; +using TechnitiumLibrary; namespace DnsServerCore { @@ -46,8 +49,13 @@ namespace DnsServerCore #region public - public void ListDhcpLeases(JsonTextWriter jsonWriter) + public void ListDhcpLeases(HttpContext context) { + UserSession session = context.GetCurrentSession(); + + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.DhcpServer, session.User, PermissionFlag.View)) + throw new DnsWebServiceException("Access was denied."); + IReadOnlyDictionary scopes = _dnsWebService.DhcpServer.Scopes; //sort by name @@ -58,6 +66,8 @@ namespace DnsServerCore sortedScopes.Sort(); + Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); + jsonWriter.WritePropertyName("leases"); jsonWriter.WriteStartArray(); @@ -77,29 +87,14 @@ namespace DnsServerCore { jsonWriter.WriteStartObject(); - jsonWriter.WritePropertyName("scope"); - jsonWriter.WriteValue(scope.Name); - - jsonWriter.WritePropertyName("type"); - jsonWriter.WriteValue(lease.Type.ToString()); - - jsonWriter.WritePropertyName("hardwareAddress"); - jsonWriter.WriteValue(BitConverter.ToString(lease.HardwareAddress)); - - jsonWriter.WritePropertyName("clientIdentifier"); - jsonWriter.WriteValue(lease.ClientIdentifier.ToString()); - - jsonWriter.WritePropertyName("address"); - jsonWriter.WriteValue(lease.Address.ToString()); - - jsonWriter.WritePropertyName("hostName"); - jsonWriter.WriteValue(lease.HostName); - - jsonWriter.WritePropertyName("leaseObtained"); - jsonWriter.WriteValue(lease.LeaseObtained); - - jsonWriter.WritePropertyName("leaseExpires"); - jsonWriter.WriteValue(lease.LeaseExpires); + jsonWriter.WriteString("scope", scope.Name); + jsonWriter.WriteString("type", lease.Type.ToString()); + jsonWriter.WriteString("hardwareAddress", BitConverter.ToString(lease.HardwareAddress)); + jsonWriter.WriteString("clientIdentifier", lease.ClientIdentifier.ToString()); + jsonWriter.WriteString("address", lease.Address.ToString()); + jsonWriter.WriteString("hostName", lease.HostName); + jsonWriter.WriteString("leaseObtained", lease.LeaseObtained); + jsonWriter.WriteString("leaseExpires", lease.LeaseExpires); jsonWriter.WriteEndObject(); } @@ -108,8 +103,13 @@ namespace DnsServerCore jsonWriter.WriteEndArray(); } - public void ListDhcpScopes(JsonTextWriter jsonWriter) + public void ListDhcpScopes(HttpContext context) { + UserSession session = context.GetCurrentSession(); + + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.DhcpServer, session.User, PermissionFlag.View)) + throw new DnsWebServiceException("Access was denied."); + IReadOnlyDictionary scopes = _dnsWebService.DhcpServer.Scopes; //sort by name @@ -120,6 +120,8 @@ namespace DnsServerCore sortedScopes.Sort(); + Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); + jsonWriter.WritePropertyName("scopes"); jsonWriter.WriteStartArray(); @@ -127,32 +129,16 @@ namespace DnsServerCore { jsonWriter.WriteStartObject(); - jsonWriter.WritePropertyName("name"); - jsonWriter.WriteValue(scope.Name); + jsonWriter.WriteString("name", scope.Name); + jsonWriter.WriteBoolean("enabled", scope.Enabled); + jsonWriter.WriteString("startingAddress", scope.StartingAddress.ToString()); + jsonWriter.WriteString("endingAddress", scope.EndingAddress.ToString()); + jsonWriter.WriteString("subnetMask", scope.SubnetMask.ToString()); + jsonWriter.WriteString("networkAddress", scope.NetworkAddress.ToString()); + jsonWriter.WriteString("broadcastAddress", scope.BroadcastAddress.ToString()); - jsonWriter.WritePropertyName("enabled"); - jsonWriter.WriteValue(scope.Enabled); - - jsonWriter.WritePropertyName("startingAddress"); - jsonWriter.WriteValue(scope.StartingAddress.ToString()); - - jsonWriter.WritePropertyName("endingAddress"); - jsonWriter.WriteValue(scope.EndingAddress.ToString()); - - jsonWriter.WritePropertyName("subnetMask"); - jsonWriter.WriteValue(scope.SubnetMask.ToString()); - - jsonWriter.WritePropertyName("networkAddress"); - jsonWriter.WriteValue(scope.NetworkAddress.ToString()); - - jsonWriter.WritePropertyName("broadcastAddress"); - jsonWriter.WriteValue(scope.BroadcastAddress.ToString()); - - if (scope.InterfaceAddress != null) - { - jsonWriter.WritePropertyName("interfaceAddress"); - jsonWriter.WriteValue(scope.InterfaceAddress.ToString()); - } + if (scope.InterfaceAddress is not null) + jsonWriter.WriteString("interfaceAddress", scope.InterfaceAddress.ToString()); jsonWriter.WriteEndObject(); } @@ -160,54 +146,36 @@ namespace DnsServerCore jsonWriter.WriteEndArray(); } - public void GetDhcpScope(HttpListenerRequest request, JsonTextWriter jsonWriter) + public void GetDhcpScope(HttpContext context) { - string scopeName = request.QueryString["name"]; - if (string.IsNullOrEmpty(scopeName)) - throw new DnsWebServiceException("Parameter 'name' missing."); + UserSession session = context.GetCurrentSession(); + + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.DhcpServer, session.User, PermissionFlag.View)) + throw new DnsWebServiceException("Access was denied."); + + string scopeName = context.Request.GetQueryOrForm("name"); Scope scope = _dnsWebService.DhcpServer.GetScope(scopeName); - if (scope == null) + if (scope is null) throw new DnsWebServiceException("DHCP scope was not found: " + scopeName); - jsonWriter.WritePropertyName("name"); - jsonWriter.WriteValue(scope.Name); + Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); - jsonWriter.WritePropertyName("startingAddress"); - jsonWriter.WriteValue(scope.StartingAddress.ToString()); + jsonWriter.WriteString("name", scope.Name); + jsonWriter.WriteString("startingAddress", scope.StartingAddress.ToString()); + jsonWriter.WriteString("endingAddress", scope.EndingAddress.ToString()); + jsonWriter.WriteString("subnetMask", scope.SubnetMask.ToString()); + jsonWriter.WriteNumber("leaseTimeDays", scope.LeaseTimeDays); + jsonWriter.WriteNumber("leaseTimeHours", scope.LeaseTimeHours); + jsonWriter.WriteNumber("leaseTimeMinutes", scope.LeaseTimeMinutes); + jsonWriter.WriteNumber("offerDelayTime", scope.OfferDelayTime); - jsonWriter.WritePropertyName("endingAddress"); - jsonWriter.WriteValue(scope.EndingAddress.ToString()); - - jsonWriter.WritePropertyName("subnetMask"); - jsonWriter.WriteValue(scope.SubnetMask.ToString()); - - jsonWriter.WritePropertyName("leaseTimeDays"); - jsonWriter.WriteValue(scope.LeaseTimeDays); - - jsonWriter.WritePropertyName("leaseTimeHours"); - jsonWriter.WriteValue(scope.LeaseTimeHours); - - jsonWriter.WritePropertyName("leaseTimeMinutes"); - jsonWriter.WriteValue(scope.LeaseTimeMinutes); - - jsonWriter.WritePropertyName("offerDelayTime"); - jsonWriter.WriteValue(scope.OfferDelayTime); - - jsonWriter.WritePropertyName("pingCheckEnabled"); - jsonWriter.WriteValue(scope.PingCheckEnabled); - - jsonWriter.WritePropertyName("pingCheckTimeout"); - jsonWriter.WriteValue(scope.PingCheckTimeout); - - jsonWriter.WritePropertyName("pingCheckRetries"); - jsonWriter.WriteValue(scope.PingCheckRetries); + jsonWriter.WriteBoolean("pingCheckEnabled", scope.PingCheckEnabled); + jsonWriter.WriteNumber("pingCheckTimeout", scope.PingCheckTimeout); + jsonWriter.WriteNumber("pingCheckRetries", scope.PingCheckRetries); if (!string.IsNullOrEmpty(scope.DomainName)) - { - jsonWriter.WritePropertyName("domainName"); - jsonWriter.WriteValue(scope.DomainName); - } + jsonWriter.WriteString("domainName", scope.DomainName); if (scope.DomainSearchList is not null) { @@ -215,73 +183,57 @@ namespace DnsServerCore jsonWriter.WriteStartArray(); foreach (string domainSearchString in scope.DomainSearchList) - jsonWriter.WriteValue(domainSearchString); + jsonWriter.WriteStringValue(domainSearchString); jsonWriter.WriteEndArray(); } - jsonWriter.WritePropertyName("dnsUpdates"); - jsonWriter.WriteValue(scope.DnsUpdates); + jsonWriter.WriteBoolean("dnsUpdates", scope.DnsUpdates); + jsonWriter.WriteNumber("dnsTtl", scope.DnsTtl); - jsonWriter.WritePropertyName("dnsTtl"); - jsonWriter.WriteValue(scope.DnsTtl); + if (scope.ServerAddress is not null) + jsonWriter.WriteString("serverAddress", scope.ServerAddress.ToString()); - if (scope.ServerAddress != null) - { - jsonWriter.WritePropertyName("serverAddress"); - jsonWriter.WriteValue(scope.ServerAddress.ToString()); - } + if (scope.ServerHostName is not null) + jsonWriter.WriteString("serverHostName", scope.ServerHostName); - if (scope.ServerHostName != null) - { - jsonWriter.WritePropertyName("serverHostName"); - jsonWriter.WriteValue(scope.ServerHostName); - } + if (scope.BootFileName is not null) + jsonWriter.WriteString("bootFileName", scope.BootFileName); - if (scope.BootFileName != null) - { - jsonWriter.WritePropertyName("bootFileName"); - jsonWriter.WriteValue(scope.BootFileName); - } + if (scope.RouterAddress is not null) + jsonWriter.WriteString("routerAddress", scope.RouterAddress.ToString()); - if (scope.RouterAddress != null) - { - jsonWriter.WritePropertyName("routerAddress"); - jsonWriter.WriteValue(scope.RouterAddress.ToString()); - } + jsonWriter.WriteBoolean("useThisDnsServer", scope.UseThisDnsServer); - jsonWriter.WritePropertyName("useThisDnsServer"); - jsonWriter.WriteValue(scope.UseThisDnsServer); - - if (scope.DnsServers != null) + if (scope.DnsServers is not null) { jsonWriter.WritePropertyName("dnsServers"); jsonWriter.WriteStartArray(); foreach (IPAddress dnsServer in scope.DnsServers) - jsonWriter.WriteValue(dnsServer.ToString()); + jsonWriter.WriteStringValue(dnsServer.ToString()); jsonWriter.WriteEndArray(); } - if (scope.WinsServers != null) + if (scope.WinsServers is not null) { jsonWriter.WritePropertyName("winsServers"); jsonWriter.WriteStartArray(); foreach (IPAddress winsServer in scope.WinsServers) - jsonWriter.WriteValue(winsServer.ToString()); + jsonWriter.WriteStringValue(winsServer.ToString()); jsonWriter.WriteEndArray(); } - if (scope.NtpServers != null) + if (scope.NtpServers is not null) { jsonWriter.WritePropertyName("ntpServers"); jsonWriter.WriteStartArray(); foreach (IPAddress ntpServer in scope.NtpServers) - jsonWriter.WriteValue(ntpServer.ToString()); + jsonWriter.WriteStringValue(ntpServer.ToString()); jsonWriter.WriteEndArray(); } @@ -292,12 +244,12 @@ namespace DnsServerCore jsonWriter.WriteStartArray(); foreach (string ntpServerDomainName in scope.NtpServerDomainNames) - jsonWriter.WriteValue(ntpServerDomainName); + jsonWriter.WriteStringValue(ntpServerDomainName); jsonWriter.WriteEndArray(); } - if (scope.StaticRoutes != null) + if (scope.StaticRoutes is not null) { jsonWriter.WritePropertyName("staticRoutes"); jsonWriter.WriteStartArray(); @@ -306,14 +258,9 @@ namespace DnsServerCore { jsonWriter.WriteStartObject(); - jsonWriter.WritePropertyName("destination"); - jsonWriter.WriteValue(route.Destination.ToString()); - - jsonWriter.WritePropertyName("subnetMask"); - jsonWriter.WriteValue(route.SubnetMask.ToString()); - - jsonWriter.WritePropertyName("router"); - jsonWriter.WriteValue(route.Router.ToString()); + jsonWriter.WriteString("destination", route.Destination.ToString()); + jsonWriter.WriteString("subnetMask", route.SubnetMask.ToString()); + jsonWriter.WriteString("router", route.Router.ToString()); jsonWriter.WriteEndObject(); } @@ -321,7 +268,7 @@ namespace DnsServerCore jsonWriter.WriteEndArray(); } - if (scope.VendorInfo != null) + if (scope.VendorInfo is not null) { jsonWriter.WritePropertyName("vendorInfo"); jsonWriter.WriteStartArray(); @@ -330,11 +277,8 @@ namespace DnsServerCore { jsonWriter.WriteStartObject(); - jsonWriter.WritePropertyName("identifier"); - jsonWriter.WriteValue(entry.Key); - - jsonWriter.WritePropertyName("information"); - jsonWriter.WriteValue(entry.Value.ToString()); + jsonWriter.WriteString("identifier", entry.Key); + jsonWriter.WriteString("information", BitConverter.ToString(entry.Value.Information).Replace('-', ':')); jsonWriter.WriteEndObject(); } @@ -348,12 +292,41 @@ namespace DnsServerCore jsonWriter.WriteStartArray(); foreach (IPAddress acIpAddress in scope.CAPWAPAcIpAddresses) - jsonWriter.WriteValue(acIpAddress.ToString()); + jsonWriter.WriteStringValue(acIpAddress.ToString()); jsonWriter.WriteEndArray(); } - if (scope.Exclusions != null) + if (scope.TftpServerAddresses is not null) + { + jsonWriter.WritePropertyName("tftpServerAddresses"); + jsonWriter.WriteStartArray(); + + foreach (IPAddress address in scope.TftpServerAddresses) + jsonWriter.WriteStringValue(address.ToString()); + + jsonWriter.WriteEndArray(); + } + + if (scope.GenericOptions is not null) + { + jsonWriter.WritePropertyName("genericOptions"); + jsonWriter.WriteStartArray(); + + foreach (DhcpOption genericOption in scope.GenericOptions) + { + jsonWriter.WriteStartObject(); + + jsonWriter.WriteNumber("code", (byte)genericOption.Code); + jsonWriter.WriteString("value", BitConverter.ToString(genericOption.RawValue).Replace('-', ':')); + + jsonWriter.WriteEndObject(); + } + + jsonWriter.WriteEndArray(); + } + + if (scope.Exclusions is not null) { jsonWriter.WritePropertyName("exclusions"); jsonWriter.WriteStartArray(); @@ -362,11 +335,8 @@ namespace DnsServerCore { jsonWriter.WriteStartObject(); - jsonWriter.WritePropertyName("startingAddress"); - jsonWriter.WriteValue(exclusion.StartingAddress.ToString()); - - jsonWriter.WritePropertyName("endingAddress"); - jsonWriter.WriteValue(exclusion.EndingAddress.ToString()); + jsonWriter.WriteString("startingAddress", exclusion.StartingAddress.ToString()); + jsonWriter.WriteString("endingAddress", exclusion.EndingAddress.ToString()); jsonWriter.WriteEndObject(); } @@ -381,39 +351,33 @@ namespace DnsServerCore { jsonWriter.WriteStartObject(); - jsonWriter.WritePropertyName("hostName"); - jsonWriter.WriteValue(reservedLease.HostName); - - jsonWriter.WritePropertyName("hardwareAddress"); - jsonWriter.WriteValue(BitConverter.ToString(reservedLease.HardwareAddress)); - - jsonWriter.WritePropertyName("address"); - jsonWriter.WriteValue(reservedLease.Address.ToString()); - - jsonWriter.WritePropertyName("comments"); - jsonWriter.WriteValue(reservedLease.Comments); + jsonWriter.WriteString("hostName", reservedLease.HostName); + jsonWriter.WriteString("hardwareAddress", BitConverter.ToString(reservedLease.HardwareAddress)); + jsonWriter.WriteString("address", reservedLease.Address.ToString()); + jsonWriter.WriteString("comments", reservedLease.Comments); jsonWriter.WriteEndObject(); } jsonWriter.WriteEndArray(); - jsonWriter.WritePropertyName("allowOnlyReservedLeases"); - jsonWriter.WriteValue(scope.AllowOnlyReservedLeases); - - jsonWriter.WritePropertyName("blockLocallyAdministeredMacAddresses"); - jsonWriter.WriteValue(scope.BlockLocallyAdministeredMacAddresses); + jsonWriter.WriteBoolean("allowOnlyReservedLeases", scope.AllowOnlyReservedLeases); + jsonWriter.WriteBoolean("blockLocallyAdministeredMacAddresses", scope.BlockLocallyAdministeredMacAddresses); } - public async Task SetDhcpScopeAsync(HttpListenerRequest request) + public async Task SetDhcpScopeAsync(HttpContext context) { - string scopeName = request.QueryString["name"]; - if (string.IsNullOrEmpty(scopeName)) - throw new DnsWebServiceException("Parameter 'name' missing."); + UserSession session = context.GetCurrentSession(); - string strStartingAddress = request.QueryString["startingAddress"]; - string strEndingAddress = request.QueryString["endingAddress"]; - string strSubnetMask = request.QueryString["subnetMask"]; + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.DhcpServer, session.User, PermissionFlag.Modify)) + throw new DnsWebServiceException("Access was denied."); + + HttpRequest request = context.Request; + + string scopeName = request.GetQueryOrForm("name"); + string strStartingAddress = request.QueryOrForm("startingAddress"); + string strEndingAddress = request.QueryOrForm("endingAddress"); + string strSubnetMask = request.QueryOrForm("subnetMask"); bool scopeExists; Scope scope = _dnsWebService.DhcpServer.GetScope(scopeName); @@ -430,29 +394,15 @@ namespace DnsServerCore throw new DnsWebServiceException("Parameter 'subnetMask' missing."); scopeExists = false; - scope = new Scope(scopeName, true, IPAddress.Parse(strStartingAddress), IPAddress.Parse(strEndingAddress), IPAddress.Parse(strSubnetMask), _dnsWebService.Log); + scope = new Scope(scopeName, true, IPAddress.Parse(strStartingAddress), IPAddress.Parse(strEndingAddress), IPAddress.Parse(strSubnetMask), _dnsWebService._log); } else { scopeExists = true; - IPAddress startingAddress; - if (string.IsNullOrEmpty(strStartingAddress)) - startingAddress = scope.StartingAddress; - else - startingAddress = IPAddress.Parse(strStartingAddress); - - IPAddress endingAddress; - if (string.IsNullOrEmpty(strEndingAddress)) - endingAddress = scope.EndingAddress; - else - endingAddress = IPAddress.Parse(strEndingAddress); - - IPAddress subnetMask; - if (string.IsNullOrEmpty(strSubnetMask)) - subnetMask = scope.SubnetMask; - else - subnetMask = IPAddress.Parse(strSubnetMask); + IPAddress startingAddress = string.IsNullOrEmpty(strStartingAddress) ? scope.StartingAddress : IPAddress.Parse(strStartingAddress); + IPAddress endingAddress = string.IsNullOrEmpty(strEndingAddress) ? scope.EndingAddress : IPAddress.Parse(strEndingAddress); + IPAddress subnetMask = string.IsNullOrEmpty(strSubnetMask) ? scope.SubnetMask : IPAddress.Parse(strSubnetMask); //validate scope address foreach (KeyValuePair entry in _dnsWebService.DhcpServer.Scopes) @@ -469,146 +419,106 @@ namespace DnsServerCore scope.ChangeNetwork(startingAddress, endingAddress, subnetMask); } - string strLeaseTimeDays = request.QueryString["leaseTimeDays"]; - if (!string.IsNullOrEmpty(strLeaseTimeDays)) - scope.LeaseTimeDays = ushort.Parse(strLeaseTimeDays); + if (request.TryGetQueryOrForm("leaseTimeDays", ushort.Parse, out ushort leaseTimeDays)) + scope.LeaseTimeDays = leaseTimeDays; - string strLeaseTimeHours = request.QueryString["leaseTimeHours"]; - if (!string.IsNullOrEmpty(strLeaseTimeHours)) - scope.LeaseTimeHours = byte.Parse(strLeaseTimeHours); + if (request.TryGetQueryOrForm("leaseTimeHours", byte.Parse, out byte leaseTimeHours)) + scope.LeaseTimeHours = leaseTimeHours; - string strLeaseTimeMinutes = request.QueryString["leaseTimeMinutes"]; - if (!string.IsNullOrEmpty(strLeaseTimeMinutes)) - scope.LeaseTimeMinutes = byte.Parse(strLeaseTimeMinutes); + if (request.TryGetQueryOrForm("leaseTimeMinutes", byte.Parse, out byte leaseTimeMinutes)) + scope.LeaseTimeMinutes = leaseTimeMinutes; - string strOfferDelayTime = request.QueryString["offerDelayTime"]; - if (!string.IsNullOrEmpty(strOfferDelayTime)) - scope.OfferDelayTime = ushort.Parse(strOfferDelayTime); + if (request.TryGetQueryOrForm("offerDelayTime", ushort.Parse, out ushort offerDelayTime)) + scope.OfferDelayTime = offerDelayTime; - string strPingCheckEnabled = request.QueryString["pingCheckEnabled"]; - if (!string.IsNullOrEmpty(strPingCheckEnabled)) - scope.PingCheckEnabled = bool.Parse(strPingCheckEnabled); + if (request.TryGetQueryOrForm("pingCheckEnabled", bool.Parse, out bool pingCheckEnabled)) + scope.PingCheckEnabled = pingCheckEnabled; - string strPingCheckTimeout = request.QueryString["pingCheckTimeout"]; - if (!string.IsNullOrEmpty(strPingCheckTimeout)) - scope.PingCheckTimeout = ushort.Parse(strPingCheckTimeout); + if (request.TryGetQueryOrForm("pingCheckTimeout", ushort.Parse, out ushort pingCheckTimeout)) + scope.PingCheckTimeout = pingCheckTimeout; - string strPingCheckRetries = request.QueryString["pingCheckRetries"]; - if (!string.IsNullOrEmpty(strPingCheckRetries)) - scope.PingCheckRetries = byte.Parse(strPingCheckRetries); + if (request.TryGetQueryOrForm("pingCheckRetries", byte.Parse, out byte pingCheckRetries)) + scope.PingCheckRetries = pingCheckRetries; - string strDomainName = request.QueryString["domainName"]; - if (strDomainName != null) - scope.DomainName = strDomainName.Length == 0 ? null : strDomainName; + string domainName = request.QueryOrForm("domainName"); + if (domainName is not null) + scope.DomainName = domainName.Length == 0 ? null : domainName; - string strDomainSearchList = request.QueryString["domainSearchList"]; - if (strDomainSearchList is not null) + string domainSearchList = request.QueryOrForm("domainSearchList"); + if (domainSearchList is not null) { - if (strDomainSearchList.Length == 0) + if (domainSearchList.Length == 0) scope.DomainSearchList = null; else - scope.DomainSearchList = strDomainSearchList.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + scope.DomainSearchList = domainSearchList.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); } - string strDnsUpdates = request.QueryString["dnsUpdates"]; - if (!string.IsNullOrEmpty(strDnsUpdates)) - scope.DnsUpdates = bool.Parse(strDnsUpdates); + if (request.TryGetQueryOrForm("dnsUpdates", bool.Parse, out bool dnsUpdates)) + scope.DnsUpdates = dnsUpdates; - string strDnsTtl = request.QueryString["dnsTtl"]; - if (!string.IsNullOrEmpty(strDnsTtl)) - scope.DnsTtl = uint.Parse(strDnsTtl); + if (request.TryGetQueryOrForm("dnsTtl", uint.Parse, out uint dnsTtl)) + scope.DnsTtl = dnsTtl; - string strServerAddress = request.QueryString["serverAddress"]; - if (strServerAddress != null) - scope.ServerAddress = strServerAddress.Length == 0 ? null : IPAddress.Parse(strServerAddress); + string serverAddress = request.QueryOrForm("serverAddress"); + if (serverAddress is not null) + scope.ServerAddress = serverAddress.Length == 0 ? null : IPAddress.Parse(serverAddress); - string strServerHostName = request.QueryString["serverHostName"]; - if (strServerHostName != null) - scope.ServerHostName = strServerHostName.Length == 0 ? null : strServerHostName; + string serverHostName = request.QueryOrForm("serverHostName"); + if (serverHostName is not null) + scope.ServerHostName = serverHostName.Length == 0 ? null : serverHostName; - string strBootFileName = request.QueryString["bootFileName"]; - if (strBootFileName != null) - scope.BootFileName = strBootFileName.Length == 0 ? null : strBootFileName; + string bootFileName = request.QueryOrForm("bootFileName"); + if (bootFileName is not null) + scope.BootFileName = bootFileName.Length == 0 ? null : bootFileName; - string strRouterAddress = request.QueryString["routerAddress"]; - if (strRouterAddress != null) - scope.RouterAddress = strRouterAddress.Length == 0 ? null : IPAddress.Parse(strRouterAddress); + string routerAddress = request.QueryOrForm("routerAddress"); + if (routerAddress is not null) + scope.RouterAddress = routerAddress.Length == 0 ? null : IPAddress.Parse(routerAddress); - string strUseThisDnsServer = request.QueryString["useThisDnsServer"]; - if (!string.IsNullOrEmpty(strUseThisDnsServer)) - scope.UseThisDnsServer = bool.Parse(strUseThisDnsServer); + if (request.TryGetQueryOrForm("useThisDnsServer", bool.Parse, out bool useThisDnsServer)) + scope.UseThisDnsServer = useThisDnsServer; if (!scope.UseThisDnsServer) { - string strDnsServers = request.QueryString["dnsServers"]; - if (strDnsServers != null) + string dnsServers = request.QueryOrForm("dnsServers"); + if (dnsServers is not null) { - if (strDnsServers.Length == 0) - { + if (dnsServers.Length == 0) scope.DnsServers = null; - } else - { - string[] strDnsServerParts = strDnsServers.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - IPAddress[] dnsServers = new IPAddress[strDnsServerParts.Length]; - - for (int i = 0; i < strDnsServerParts.Length; i++) - dnsServers[i] = IPAddress.Parse(strDnsServerParts[i]); - - scope.DnsServers = dnsServers; - } + scope.DnsServers = dnsServers.Split(IPAddress.Parse, ','); } } - string strWinsServers = request.QueryString["winsServers"]; - if (strWinsServers != null) + string winsServers = request.QueryOrForm("winsServers"); + if (winsServers is not null) { - if (strWinsServers.Length == 0) - { + if (winsServers.Length == 0) scope.WinsServers = null; - } else - { - string[] strWinsServerParts = strWinsServers.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - IPAddress[] winsServers = new IPAddress[strWinsServerParts.Length]; - - for (int i = 0; i < strWinsServerParts.Length; i++) - winsServers[i] = IPAddress.Parse(strWinsServerParts[i]); - - scope.WinsServers = winsServers; - } + scope.WinsServers = winsServers.Split(IPAddress.Parse, ','); } - string strNtpServers = request.QueryString["ntpServers"]; - if (strNtpServers != null) + string ntpServers = request.QueryOrForm("ntpServers"); + if (ntpServers is not null) { - if (strNtpServers.Length == 0) - { + if (ntpServers.Length == 0) scope.NtpServers = null; - } else - { - string[] strNtpServerParts = strNtpServers.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - IPAddress[] ntpServers = new IPAddress[strNtpServerParts.Length]; - - for (int i = 0; i < strNtpServerParts.Length; i++) - ntpServers[i] = IPAddress.Parse(strNtpServerParts[i]); - - scope.NtpServers = ntpServers; - } + scope.NtpServers = ntpServers.Split(IPAddress.Parse, ','); } - string strNtpServerDomainNames = request.QueryString["ntpServerDomainNames"]; - if (strNtpServerDomainNames is not null) + string ntpServerDomainNames = request.QueryOrForm("ntpServerDomainNames"); + if (ntpServerDomainNames is not null) { - if (strNtpServerDomainNames.Length == 0) + if (ntpServerDomainNames.Length == 0) scope.NtpServerDomainNames = null; else - scope.NtpServerDomainNames = strNtpServerDomainNames.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + scope.NtpServerDomainNames = ntpServerDomainNames.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); } - string strStaticRoutes = request.QueryString["staticRoutes"]; - if (strStaticRoutes != null) + string strStaticRoutes = request.QueryOrForm("staticRoutes"); + if (strStaticRoutes is not null) { if (strStaticRoutes.Length == 0) { @@ -620,16 +530,14 @@ namespace DnsServerCore List staticRoutes = new List(); for (int i = 0; i < strStaticRoutesParts.Length; i += 3) - { staticRoutes.Add(new ClasslessStaticRouteOption.Route(IPAddress.Parse(strStaticRoutesParts[i + 0]), IPAddress.Parse(strStaticRoutesParts[i + 1]), IPAddress.Parse(strStaticRoutesParts[i + 2]))); - } scope.StaticRoutes = staticRoutes; } } - string strVendorInfo = request.QueryString["vendorInfo"]; - if (strVendorInfo != null) + string strVendorInfo = request.QueryOrForm("vendorInfo"); + if (strVendorInfo is not null) { if (strVendorInfo.Length == 0) { @@ -641,35 +549,51 @@ namespace DnsServerCore Dictionary vendorInfo = new Dictionary(); for (int i = 0; i < strVendorInfoParts.Length; i += 2) - { vendorInfo.Add(strVendorInfoParts[i + 0], new VendorSpecificInformationOption(strVendorInfoParts[i + 1])); - } scope.VendorInfo = vendorInfo; } } - string strCAPWAPAcIpAddresses = request.QueryString["capwapAcIpAddresses"]; - if (strCAPWAPAcIpAddresses is not null) + string capwapAcIpAddresses = request.QueryOrForm("capwapAcIpAddresses"); + if (capwapAcIpAddresses is not null) { - if (strCAPWAPAcIpAddresses.Length == 0) - { + if (capwapAcIpAddresses.Length == 0) scope.CAPWAPAcIpAddresses = null; + else + scope.CAPWAPAcIpAddresses = capwapAcIpAddresses.Split(IPAddress.Parse, ','); + } + + string tftpServerAddresses = request.QueryOrForm("tftpServerAddresses"); + if (tftpServerAddresses is not null) + { + if (tftpServerAddresses.Length == 0) + scope.TftpServerAddresses = null; + else + scope.TftpServerAddresses = tftpServerAddresses.Split(IPAddress.Parse, ','); + } + + string strGenericOptions = request.QueryOrForm("genericOptions"); + if (strGenericOptions is not null) + { + if (strGenericOptions.Length == 0) + { + scope.GenericOptions = null; } else { - string[] strCAPWAPAcIpAddressesParts = strCAPWAPAcIpAddresses.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - IPAddress[] capwapAcIpAddresses = new IPAddress[strCAPWAPAcIpAddressesParts.Length]; + string[] strGenericOptionsParts = strGenericOptions.Split('|'); + List genericOptions = new List(); - for (int i = 0; i < strCAPWAPAcIpAddressesParts.Length; i++) - capwapAcIpAddresses[i] = IPAddress.Parse(strCAPWAPAcIpAddressesParts[i]); + for (int i = 0; i < strGenericOptionsParts.Length; i += 2) + genericOptions.Add(new DhcpOption((DhcpOptionCode)byte.Parse(strGenericOptionsParts[i + 0]), strGenericOptionsParts[i + 1])); - scope.CAPWAPAcIpAddresses = capwapAcIpAddresses; + scope.GenericOptions = genericOptions; } } - string strExclusions = request.QueryString["exclusions"]; - if (strExclusions != null) + string strExclusions = request.QueryOrForm("exclusions"); + if (strExclusions is not null) { if (strExclusions.Length == 0) { @@ -681,16 +605,14 @@ namespace DnsServerCore List exclusions = new List(); for (int i = 0; i < strExclusionsParts.Length; i += 2) - { exclusions.Add(new Exclusion(IPAddress.Parse(strExclusionsParts[i + 0]), IPAddress.Parse(strExclusionsParts[i + 1]))); - } scope.Exclusions = exclusions; } } - string strReservedLeases = request.QueryString["reservedLeases"]; - if (strReservedLeases != null) + string strReservedLeases = request.QueryOrForm("reservedLeases"); + if (strReservedLeases is not null) { if (strReservedLeases.Length == 0) { @@ -702,65 +624,58 @@ namespace DnsServerCore List reservedLeases = new List(); for (int i = 0; i < strReservedLeaseParts.Length; i += 4) - { reservedLeases.Add(new Lease(LeaseType.Reserved, strReservedLeaseParts[i + 0], DhcpMessageHardwareAddressType.Ethernet, strReservedLeaseParts[i + 1], IPAddress.Parse(strReservedLeaseParts[i + 2]), strReservedLeaseParts[i + 3])); - } scope.ReservedLeases = reservedLeases; } } - string strAllowOnlyReservedLeases = request.QueryString["allowOnlyReservedLeases"]; - if (!string.IsNullOrEmpty(strAllowOnlyReservedLeases)) - scope.AllowOnlyReservedLeases = bool.Parse(strAllowOnlyReservedLeases); + if (request.TryGetQueryOrForm("allowOnlyReservedLeases", bool.Parse, out bool allowOnlyReservedLeases)) + scope.AllowOnlyReservedLeases = allowOnlyReservedLeases; - string strBlockLocallyAdministeredMacAddresses = request.QueryString["blockLocallyAdministeredMacAddresses"]; - if (!string.IsNullOrEmpty(strBlockLocallyAdministeredMacAddresses)) - scope.BlockLocallyAdministeredMacAddresses = bool.Parse(strBlockLocallyAdministeredMacAddresses); + if (request.TryGetQueryOrForm("blockLocallyAdministeredMacAddresses", bool.Parse, out bool blockLocallyAdministeredMacAddresses)) + scope.BlockLocallyAdministeredMacAddresses = blockLocallyAdministeredMacAddresses; if (scopeExists) { _dnsWebService.DhcpServer.SaveScope(scopeName); - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + _dnsWebService.GetSession(request).User.Username + "] DHCP scope was updated successfully: " + scopeName); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] DHCP scope was updated successfully: " + scopeName); } else { await _dnsWebService.DhcpServer.AddScopeAsync(scope); - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + _dnsWebService.GetSession(request).User.Username + "] DHCP scope was added successfully: " + scopeName); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] DHCP scope was added successfully: " + scopeName); } - string newName = request.QueryString["newName"]; - if (!string.IsNullOrEmpty(newName) && !newName.Equals(scopeName)) + if (request.TryGetQueryOrForm("newName", out string newName) && !newName.Equals(scopeName)) { _dnsWebService.DhcpServer.RenameScope(scopeName, newName); - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + _dnsWebService.GetSession(request).User.Username + "] DHCP scope was renamed successfully: '" + scopeName + "' to '" + newName + "'"); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] DHCP scope was renamed successfully: '" + scopeName + "' to '" + newName + "'"); } } - public void AddReservedLease(HttpListenerRequest request) + public void AddReservedLease(HttpContext context) { - string scopeName = request.QueryString["name"]; - if (string.IsNullOrEmpty(scopeName)) - throw new DnsWebServiceException("Parameter 'name' missing."); + UserSession session = context.GetCurrentSession(); + + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.DhcpServer, session.User, PermissionFlag.Modify)) + throw new DnsWebServiceException("Access was denied."); + + HttpRequest request = context.Request; + + string scopeName = request.GetQueryOrForm("name"); Scope scope = _dnsWebService.DhcpServer.GetScope(scopeName); if (scope is null) throw new DnsWebServiceException("No such scope exists: " + scopeName); - string hostName = request.QueryString["hostName"]; - - string hardwareAddress = request.QueryString["hardwareAddress"]; - if (string.IsNullOrEmpty(hardwareAddress)) - throw new DnsWebServiceException("Parameter 'hardwareAddress' missing."); - - string strIpAddress = request.QueryString["ipAddress"]; - if (string.IsNullOrEmpty(strIpAddress)) - throw new DnsWebServiceException("Parameter 'ipAddress' missing."); - - string comments = request.QueryString["comments"]; + string hostName = request.QueryOrForm("hostName"); + string hardwareAddress = request.GetQueryOrForm("hardwareAddress"); + string strIpAddress = request.GetQueryOrForm("ipAddress"); + string comments = request.QueryOrForm("comments"); Lease reservedLease = new Lease(LeaseType.Reserved, hostName, DhcpMessageHardwareAddressType.Ethernet, hardwareAddress, IPAddress.Parse(strIpAddress), comments); @@ -769,137 +684,164 @@ namespace DnsServerCore _dnsWebService.DhcpServer.SaveScope(scopeName); - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + _dnsWebService.GetSession(request).User.Username + "] DHCP scope reserved lease was added successfully: " + scopeName); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] DHCP scope reserved lease was added successfully: " + scopeName); } - public void RemoveReservedLease(HttpListenerRequest request) + public void RemoveReservedLease(HttpContext context) { - string scopeName = request.QueryString["name"]; - if (string.IsNullOrEmpty(scopeName)) - throw new DnsWebServiceException("Parameter 'name' missing."); + UserSession session = context.GetCurrentSession(); + + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.DhcpServer, session.User, PermissionFlag.Modify)) + throw new DnsWebServiceException("Access was denied."); + + HttpRequest request = context.Request; + + string scopeName = request.GetQueryOrForm("name"); Scope scope = _dnsWebService.DhcpServer.GetScope(scopeName); if (scope is null) throw new DnsWebServiceException("No such scope exists: " + scopeName); - string hardwareAddress = request.QueryString["hardwareAddress"]; - if (string.IsNullOrEmpty(hardwareAddress)) - throw new DnsWebServiceException("Parameter 'hardwareAddress' missing."); + string hardwareAddress = request.GetQueryOrForm("hardwareAddress"); if (!scope.RemoveReservedLease(hardwareAddress)) throw new DnsWebServiceException("Failed to remove reserved lease for scope: " + scopeName); _dnsWebService.DhcpServer.SaveScope(scopeName); - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + _dnsWebService.GetSession(request).User.Username + "] DHCP scope reserved lease was removed successfully: " + scopeName); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] DHCP scope reserved lease was removed successfully: " + scopeName); } - public async Task EnableDhcpScopeAsync(HttpListenerRequest request) + public async Task EnableDhcpScopeAsync(HttpContext context) { - string scopeName = request.QueryString["name"]; - if (string.IsNullOrEmpty(scopeName)) - throw new DnsWebServiceException("Parameter 'name' missing."); + UserSession session = context.GetCurrentSession(); + + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.DhcpServer, session.User, PermissionFlag.Modify)) + throw new DnsWebServiceException("Access was denied."); + + string scopeName = context.Request.GetQueryOrForm("name"); await _dnsWebService.DhcpServer.EnableScopeAsync(scopeName, true); - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + _dnsWebService.GetSession(request).User.Username + "] DHCP scope was enabled successfully: " + scopeName); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] DHCP scope was enabled successfully: " + scopeName); } - public void DisableDhcpScope(HttpListenerRequest request) + public void DisableDhcpScope(HttpContext context) { - string scopeName = request.QueryString["name"]; - if (string.IsNullOrEmpty(scopeName)) - throw new DnsWebServiceException("Parameter 'name' missing."); + UserSession session = context.GetCurrentSession(); + + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.DhcpServer, session.User, PermissionFlag.Modify)) + throw new DnsWebServiceException("Access was denied."); + + string scopeName = context.Request.GetQueryOrForm("name"); _dnsWebService.DhcpServer.DisableScope(scopeName, true); - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + _dnsWebService.GetSession(request).User.Username + "] DHCP scope was disabled successfully: " + scopeName); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] DHCP scope was disabled successfully: " + scopeName); } - public void DeleteDhcpScope(HttpListenerRequest request) + public void DeleteDhcpScope(HttpContext context) { - string scopeName = request.QueryString["name"]; - if (string.IsNullOrEmpty(scopeName)) - throw new DnsWebServiceException("Parameter 'name' missing."); + UserSession session = context.GetCurrentSession(); + + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.DhcpServer, session.User, PermissionFlag.Delete)) + throw new DnsWebServiceException("Access was denied."); + + string scopeName = context.Request.GetQueryOrForm("name"); _dnsWebService.DhcpServer.DeleteScope(scopeName); - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + _dnsWebService.GetSession(request).User.Username + "] DHCP scope was deleted successfully: " + scopeName); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] DHCP scope was deleted successfully: " + scopeName); } - public void RemoveDhcpLease(HttpListenerRequest request) + public void RemoveDhcpLease(HttpContext context) { - string scopeName = request.QueryString["name"]; - if (string.IsNullOrEmpty(scopeName)) - throw new DnsWebServiceException("Parameter 'name' missing."); + UserSession session = context.GetCurrentSession(); + + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.DhcpServer, session.User, PermissionFlag.Delete)) + throw new DnsWebServiceException("Access was denied."); + + HttpRequest request = context.Request; + + string scopeName = request.GetQueryOrForm("name"); Scope scope = _dnsWebService.DhcpServer.GetScope(scopeName); if (scope is null) throw new DnsWebServiceException("DHCP scope does not exists: " + scopeName); - string strClientIdentifier = request.QueryString["clientIdentifier"]; - string strHardwareAddress = request.QueryString["hardwareAddress"]; + string clientIdentifier = request.QueryOrForm("clientIdentifier"); + string hardwareAddress = request.QueryOrForm("hardwareAddress"); - if (!string.IsNullOrEmpty(strClientIdentifier)) - scope.RemoveLease(ClientIdentifierOption.Parse(strClientIdentifier)); - else if (!string.IsNullOrEmpty(strHardwareAddress)) - scope.RemoveLease(strHardwareAddress); + if (!string.IsNullOrEmpty(clientIdentifier)) + scope.RemoveLease(ClientIdentifierOption.Parse(clientIdentifier)); + else if (!string.IsNullOrEmpty(hardwareAddress)) + scope.RemoveLease(hardwareAddress); else throw new DnsWebServiceException("Parameter 'hardwareAddress' or 'clientIdentifier' missing. At least one of them must be specified."); _dnsWebService.DhcpServer.SaveScope(scopeName); - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + _dnsWebService.GetSession(request).User.Username + "] DHCP scope's lease was removed successfully: " + scopeName); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] DHCP scope's lease was removed successfully: " + scopeName); } - public void ConvertToReservedLease(HttpListenerRequest request) + public void ConvertToReservedLease(HttpContext context) { - string scopeName = request.QueryString["name"]; - if (string.IsNullOrEmpty(scopeName)) - throw new DnsWebServiceException("Parameter 'name' missing."); + UserSession session = context.GetCurrentSession(); + + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.DhcpServer, session.User, PermissionFlag.Modify)) + throw new DnsWebServiceException("Access was denied."); + + HttpRequest request = context.Request; + + string scopeName = request.GetQueryOrForm("name"); Scope scope = _dnsWebService.DhcpServer.GetScope(scopeName); if (scope == null) throw new DnsWebServiceException("DHCP scope does not exists: " + scopeName); - string strClientIdentifier = request.QueryString["clientIdentifier"]; - string strHardwareAddress = request.QueryString["hardwareAddress"]; + string clientIdentifier = request.QueryOrForm("clientIdentifier"); + string hardwareAddress = request.QueryOrForm("hardwareAddress"); - if (!string.IsNullOrEmpty(strClientIdentifier)) - scope.ConvertToReservedLease(ClientIdentifierOption.Parse(strClientIdentifier)); - else if (!string.IsNullOrEmpty(strHardwareAddress)) - scope.ConvertToReservedLease(strHardwareAddress); + if (!string.IsNullOrEmpty(clientIdentifier)) + scope.ConvertToReservedLease(ClientIdentifierOption.Parse(clientIdentifier)); + else if (!string.IsNullOrEmpty(hardwareAddress)) + scope.ConvertToReservedLease(hardwareAddress); else throw new DnsWebServiceException("Parameter 'hardwareAddress' or 'clientIdentifier' missing. At least one of them must be specified."); _dnsWebService.DhcpServer.SaveScope(scopeName); - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + _dnsWebService.GetSession(request).User.Username + "] DHCP scope's lease was reserved successfully: " + scopeName); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] DHCP scope's lease was reserved successfully: " + scopeName); } - public void ConvertToDynamicLease(HttpListenerRequest request) + public void ConvertToDynamicLease(HttpContext context) { - string scopeName = request.QueryString["name"]; - if (string.IsNullOrEmpty(scopeName)) - throw new DnsWebServiceException("Parameter 'name' missing."); + UserSession session = context.GetCurrentSession(); + + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.DhcpServer, session.User, PermissionFlag.Modify)) + throw new DnsWebServiceException("Access was denied."); + + HttpRequest request = context.Request; + + string scopeName = request.GetQueryOrForm("name"); Scope scope = _dnsWebService.DhcpServer.GetScope(scopeName); if (scope == null) throw new DnsWebServiceException("DHCP scope does not exists: " + scopeName); - string strClientIdentifier = request.QueryString["clientIdentifier"]; - string strHardwareAddress = request.QueryString["hardwareAddress"]; + string clientIdentifier = request.QueryOrForm("clientIdentifier"); + string hardwareAddress = request.QueryOrForm("hardwareAddress"); - if (!string.IsNullOrEmpty(strClientIdentifier)) - scope.ConvertToDynamicLease(ClientIdentifierOption.Parse(strClientIdentifier)); - else if (!string.IsNullOrEmpty(strHardwareAddress)) - scope.ConvertToDynamicLease(strHardwareAddress); + if (!string.IsNullOrEmpty(clientIdentifier)) + scope.ConvertToDynamicLease(ClientIdentifierOption.Parse(clientIdentifier)); + else if (!string.IsNullOrEmpty(hardwareAddress)) + scope.ConvertToDynamicLease(hardwareAddress); else throw new DnsWebServiceException("Parameter 'hardwareAddress' or 'clientIdentifier' missing. At least one of them must be specified."); _dnsWebService.DhcpServer.SaveScope(scopeName); - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + _dnsWebService.GetSession(request).User.Username + "] DHCP scope's lease was unreserved successfully: " + scopeName); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] DHCP scope's lease was unreserved successfully: " + scopeName); } #endregion diff --git a/DnsServerCore/WebServiceLogsApi.cs b/DnsServerCore/WebServiceLogsApi.cs index d1b7995c..9fb8d902 100644 --- a/DnsServerCore/WebServiceLogsApi.cs +++ b/DnsServerCore/WebServiceLogsApi.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -18,12 +18,14 @@ along with this program. If not, see . */ using DnsServerCore.ApplicationCommon; +using DnsServerCore.Auth; using DnsServerCore.Dns.Applications; -using Newtonsoft.Json; +using Microsoft.AspNetCore.Http; using System; using System.Globalization; using System.IO; using System.Net; +using System.Text.Json; using System.Threading.Tasks; using TechnitiumLibrary.Net; using TechnitiumLibrary.Net.Dns; @@ -50,13 +52,20 @@ namespace DnsServerCore #region public - public void ListLogs(JsonTextWriter jsonWriter) + public void ListLogs(HttpContext context) { - string[] logFiles = _dnsWebService.Log.ListLogFiles(); + UserSession session = context.GetCurrentSession(); + + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Logs, session.User, PermissionFlag.View)) + throw new DnsWebServiceException("Access was denied."); + + string[] logFiles = _dnsWebService._log.ListLogFiles(); Array.Sort(logFiles); Array.Reverse(logFiles); + Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); + jsonWriter.WritePropertyName("logFiles"); jsonWriter.WriteStartArray(); @@ -64,11 +73,8 @@ namespace DnsServerCore { jsonWriter.WriteStartObject(); - jsonWriter.WritePropertyName("fileName"); - jsonWriter.WriteValue(Path.GetFileNameWithoutExtension(logFile)); - - jsonWriter.WritePropertyName("size"); - jsonWriter.WriteValue(WebUtilities.GetFormattedSize(new FileInfo(logFile).Length)); + jsonWriter.WriteString("fileName", Path.GetFileNameWithoutExtension(logFile)); + jsonWriter.WriteString("size", WebUtilities.GetFormattedSize(new FileInfo(logFile).Length)); jsonWriter.WriteEndObject(); } @@ -76,56 +82,72 @@ namespace DnsServerCore jsonWriter.WriteEndArray(); } - public Task DownloadLogAsync(HttpListenerRequest request, HttpListenerResponse response) + public Task DownloadLogAsync(HttpContext context) { - string strFileName = request.QueryString["fileName"]; - if (string.IsNullOrEmpty(strFileName)) - throw new DnsWebServiceException("Parameter 'fileName' missing."); + UserSession session = context.GetCurrentSession(); - int limit; - string strLimit = request.QueryString["limit"]; - if (string.IsNullOrEmpty(strLimit)) - limit = 0; - else - limit = int.Parse(strLimit); + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Logs, session.User, PermissionFlag.View)) + throw new DnsWebServiceException("Access was denied."); - return _dnsWebService.Log.DownloadLogAsync(request, response, strFileName, limit * 1024 * 1024); + HttpRequest request = context.Request; + + string fileName = request.GetQueryOrForm("fileName"); + int limit = request.GetQueryOrForm("limit", int.Parse, 0); + + return _dnsWebService._log.DownloadLogAsync(context, fileName, limit * 1024 * 1024); } - public void DeleteLog(HttpListenerRequest request) + public void DeleteLog(HttpContext context) { - string log = request.QueryString["log"]; - if (string.IsNullOrEmpty(log)) - throw new DnsWebServiceException("Parameter 'log' missing."); + UserSession session = context.GetCurrentSession(); - _dnsWebService.Log.DeleteLog(log); + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Logs, session.User, PermissionFlag.Delete)) + throw new DnsWebServiceException("Access was denied."); - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + _dnsWebService.GetSession(request).User.Username + "] Log file was deleted: " + log); + HttpRequest request = context.Request; + + string log = request.GetQueryOrForm("log"); + + _dnsWebService._log.DeleteLog(log); + + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] Log file was deleted: " + log); } - public void DeleteAllLogs(HttpListenerRequest request) + public void DeleteAllLogs(HttpContext context) { - _dnsWebService.Log.DeleteAllLogs(); + UserSession session = context.GetCurrentSession(); - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + _dnsWebService.GetSession(request).User.Username + "] All log files were deleted."); + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Logs, session.User, PermissionFlag.Delete)) + throw new DnsWebServiceException("Access was denied."); + + _dnsWebService._log.DeleteAllLogs(); + + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] All log files were deleted."); } - public void DeleteAllStats(HttpListenerRequest request) + public void DeleteAllStats(HttpContext context) { + UserSession session = context.GetCurrentSession(); + + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Dashboard, session.User, PermissionFlag.Delete)) + throw new DnsWebServiceException("Access was denied."); + _dnsWebService.DnsServer.StatsManager.DeleteAllStats(); - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + _dnsWebService.GetSession(request).User.Username + "] All stats files were deleted."); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] All stats files were deleted."); } - public async Task QueryLogsAsync(HttpListenerRequest request, JsonTextWriter jsonWriter) + public async Task QueryLogsAsync(HttpContext context) { - string name = request.QueryString["name"]; - if (string.IsNullOrEmpty(name)) - throw new DnsWebServiceException("Parameter 'name' missing."); + UserSession session = context.GetCurrentSession(); - string classPath = request.QueryString["classPath"]; - if (string.IsNullOrEmpty(classPath)) - throw new DnsWebServiceException("Parameter 'classPath' missing."); + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Logs, session.User, PermissionFlag.View)) + throw new DnsWebServiceException("Access was denied."); + + HttpRequest request = context.Request; + + string name = request.GetQueryOrForm("name"); + string classPath = request.GetQueryOrForm("classPath"); if (!_dnsWebService.DnsServer.DnsApplicationManager.Applications.TryGetValue(name, out DnsApplication application)) throw new DnsWebServiceException("DNS application was not found: " + name); @@ -133,97 +155,56 @@ namespace DnsServerCore if (!application.DnsQueryLoggers.TryGetValue(classPath, out IDnsQueryLogger logger)) throw new DnsWebServiceException("DNS application '" + classPath + "' class path was not found: " + name); - long pageNumber; - string strPageNumber = request.QueryString["pageNumber"]; - if (string.IsNullOrEmpty(strPageNumber)) - pageNumber = 1; - else - pageNumber = long.Parse(strPageNumber); + long pageNumber = request.GetQueryOrForm("pageNumber", long.Parse, 1); + int entriesPerPage = request.GetQueryOrForm("entriesPerPage", int.Parse, 25); + bool descendingOrder = request.GetQueryOrForm("descendingOrder", bool.Parse, true); - int entriesPerPage; - string strEntriesPerPage = request.QueryString["entriesPerPage"]; - if (string.IsNullOrEmpty(strEntriesPerPage)) - entriesPerPage = 25; - else - entriesPerPage = int.Parse(strEntriesPerPage); - - bool descendingOrder; - string strDescendingOrder = request.QueryString["descendingOrder"]; - if (string.IsNullOrEmpty(strDescendingOrder)) - descendingOrder = true; - else - descendingOrder = bool.Parse(strDescendingOrder); - - DateTime? start; - string strStart = request.QueryString["start"]; - if (string.IsNullOrEmpty(strStart)) - start = null; - else + DateTime? start = null; + string strStart = request.QueryOrForm("start"); + if (!string.IsNullOrEmpty(strStart)) start = DateTime.Parse(strStart, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal); - DateTime? end; - string strEnd = request.QueryString["end"]; - if (string.IsNullOrEmpty(strEnd)) - end = null; - else + DateTime? end = null; + string strEnd = request.QueryOrForm("end"); + if (!string.IsNullOrEmpty(strEnd)) end = DateTime.Parse(strEnd, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal); - IPAddress clientIpAddress; - string strClientIpAddress = request.QueryString["clientIpAddress"]; - if (string.IsNullOrEmpty(strClientIpAddress)) - clientIpAddress = null; - else - clientIpAddress = IPAddress.Parse(strClientIpAddress); + IPAddress clientIpAddress = request.GetQueryOrForm("clientIpAddress", IPAddress.Parse, null); - DnsTransportProtocol? protocol; - string strProtocol = request.QueryString["protocol"]; - if (string.IsNullOrEmpty(strProtocol)) - protocol = null; - else + DnsTransportProtocol? protocol = null; + string strProtocol = request.QueryOrForm("protocol"); + if (!string.IsNullOrEmpty(strProtocol)) protocol = Enum.Parse(strProtocol, true); - DnsServerResponseType? responseType; - string strResponseType = request.QueryString["responseType"]; - if (string.IsNullOrEmpty(strResponseType)) - responseType = null; - else + DnsServerResponseType? responseType = null; + string strResponseType = request.QueryOrForm("responseType"); + if (!string.IsNullOrEmpty(strResponseType)) responseType = Enum.Parse(strResponseType, true); - DnsResponseCode? rcode; - string strRcode = request.QueryString["rcode"]; - if (string.IsNullOrEmpty(strRcode)) - rcode = null; - else + DnsResponseCode? rcode = null; + string strRcode = request.QueryOrForm("rcode"); + if (!string.IsNullOrEmpty(strRcode)) rcode = Enum.Parse(strRcode, true); - string qname = request.QueryString["qname"]; - if (string.IsNullOrEmpty(qname)) - qname = null; + string qname = request.GetQueryOrForm("qname", null); - DnsResourceRecordType? qtype; - string strQtype = request.QueryString["qtype"]; - if (string.IsNullOrEmpty(strQtype)) - qtype = null; - else + DnsResourceRecordType? qtype = null; + string strQtype = request.QueryOrForm("qtype"); + if (!string.IsNullOrEmpty(strQtype)) qtype = Enum.Parse(strQtype, true); - DnsClass? qclass; - string strQclass = request.QueryString["qclass"]; - if (string.IsNullOrEmpty(strQclass)) - qclass = null; - else + DnsClass? qclass = null; + string strQclass = request.QueryOrForm("qclass"); + if (!string.IsNullOrEmpty(strQclass)) qclass = Enum.Parse(strQclass, true); DnsLogPage page = await logger.QueryLogsAsync(pageNumber, entriesPerPage, descendingOrder, start, end, clientIpAddress, protocol, responseType, rcode, qname, qtype, qclass); - jsonWriter.WritePropertyName("pageNumber"); - jsonWriter.WriteValue(page.PageNumber); + Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); - jsonWriter.WritePropertyName("totalPages"); - jsonWriter.WriteValue(page.TotalPages); - - jsonWriter.WritePropertyName("totalEntries"); - jsonWriter.WriteValue(page.TotalEntries); + jsonWriter.WriteNumber("pageNumber", page.PageNumber); + jsonWriter.WriteNumber("totalPages", page.TotalPages); + jsonWriter.WriteNumber("totalEntries", page.TotalEntries); jsonWriter.WritePropertyName("entries"); jsonWriter.WriteStartArray(); @@ -232,35 +213,16 @@ namespace DnsServerCore { jsonWriter.WriteStartObject(); - jsonWriter.WritePropertyName("rowNumber"); - jsonWriter.WriteValue(entry.RowNumber); - - jsonWriter.WritePropertyName("timestamp"); - jsonWriter.WriteValue(entry.Timestamp); - - jsonWriter.WritePropertyName("clientIpAddress"); - jsonWriter.WriteValue(entry.ClientIpAddress.ToString()); - - jsonWriter.WritePropertyName("protocol"); - jsonWriter.WriteValue(entry.Protocol.ToString()); - - jsonWriter.WritePropertyName("responseType"); - jsonWriter.WriteValue(entry.ResponseType.ToString()); - - jsonWriter.WritePropertyName("rcode"); - jsonWriter.WriteValue(entry.RCODE.ToString()); - - jsonWriter.WritePropertyName("qname"); - jsonWriter.WriteValue(entry.Question?.Name); - - jsonWriter.WritePropertyName("qtype"); - jsonWriter.WriteValue(entry.Question?.Type.ToString()); - - jsonWriter.WritePropertyName("qclass"); - jsonWriter.WriteValue(entry.Question?.Class.ToString()); - - jsonWriter.WritePropertyName("answer"); - jsonWriter.WriteValue(entry.Answer); + jsonWriter.WriteNumber("rowNumber", entry.RowNumber); + jsonWriter.WriteString("timestamp", entry.Timestamp); + jsonWriter.WriteString("clientIpAddress", entry.ClientIpAddress.ToString()); + jsonWriter.WriteString("protocol", entry.Protocol.ToString()); + jsonWriter.WriteString("responseType", entry.ResponseType.ToString()); + jsonWriter.WriteString("rcode", entry.RCODE.ToString()); + jsonWriter.WriteString("qname", entry.Question?.Name); + jsonWriter.WriteString("qtype", entry.Question?.Type.ToString()); + jsonWriter.WriteString("qclass", entry.Question?.Class.ToString()); + jsonWriter.WriteString("answer", entry.Answer); jsonWriter.WriteEndObject(); } diff --git a/DnsServerCore/WebServiceOtherZonesApi.cs b/DnsServerCore/WebServiceOtherZonesApi.cs index 85f2a58d..7ab8b785 100644 --- a/DnsServerCore/WebServiceOtherZonesApi.cs +++ b/DnsServerCore/WebServiceOtherZonesApi.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -17,12 +17,13 @@ along with this program. If not, see . */ +using DnsServerCore.Auth; using DnsServerCore.Dns.Zones; -using Newtonsoft.Json; -using System; +using Microsoft.AspNetCore.Http; using System.Collections.Generic; using System.IO; using System.Net; +using System.Text.Json; using System.Threading.Tasks; using TechnitiumLibrary.Net; using TechnitiumLibrary.Net.Dns.ResourceRecords; @@ -50,20 +51,30 @@ namespace DnsServerCore #region cache api - public void FlushCache(HttpListenerRequest request) + public void FlushCache(HttpContext context) { + UserSession session = context.GetCurrentSession(); + + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Cache, session.User, PermissionFlag.Delete)) + throw new DnsWebServiceException("Access was denied."); + _dnsWebService.DnsServer.CacheZoneManager.Flush(); - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + _dnsWebService.GetSession(request).User.Username + "] Cache was flushed."); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] Cache was flushed."); } - public void ListCachedZones(HttpListenerRequest request, JsonTextWriter jsonWriter) + public void ListCachedZones(HttpContext context) { - string domain = request.QueryString["domain"]; - if (domain == null) - domain = ""; + UserSession session = context.GetCurrentSession(); - string direction = request.QueryString["direction"]; + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Cache, session.User, PermissionFlag.View)) + throw new DnsWebServiceException("Access was denied."); + + HttpRequest request = context.Request; + + string domain = request.GetQueryOrForm("domain", ""); + + string direction = request.QueryOrForm("direction"); if (direction is not null) direction = direction.ToLower(); @@ -107,8 +118,9 @@ namespace DnsServerCore subZones.Sort(); - jsonWriter.WritePropertyName("domain"); - jsonWriter.WriteValue(domain); + Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); + + jsonWriter.WriteString("domain", domain); jsonWriter.WritePropertyName("zones"); jsonWriter.WriteStartArray(); @@ -117,34 +129,42 @@ namespace DnsServerCore domain = "." + domain; foreach (string subZone in subZones) - jsonWriter.WriteValue(subZone + domain); + jsonWriter.WriteStringValue(subZone + domain); jsonWriter.WriteEndArray(); WebServiceZonesApi.WriteRecordsAsJson(records, jsonWriter, false); } - public void DeleteCachedZone(HttpListenerRequest request) + public void DeleteCachedZone(HttpContext context) { - string domain = request.QueryString["domain"]; - if (string.IsNullOrEmpty(domain)) - throw new DnsWebServiceException("Parameter 'domain' missing."); + UserSession session = context.GetCurrentSession(); + + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Cache, session.User, PermissionFlag.Delete)) + throw new DnsWebServiceException("Access was denied."); + + string domain = context.Request.GetQueryOrForm("domain"); if (_dnsWebService.DnsServer.CacheZoneManager.DeleteZone(domain)) - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + _dnsWebService.GetSession(request).User.Username + "] Cached zone was deleted: " + domain); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] Cached zone was deleted: " + domain); } #endregion #region allowed zones api - public void ListAllowedZones(HttpListenerRequest request, JsonTextWriter jsonWriter) + public void ListAllowedZones(HttpContext context) { - string domain = request.QueryString["domain"]; - if (domain == null) - domain = ""; + UserSession session = context.GetCurrentSession(); - string direction = request.QueryString["direction"]; + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Allowed, session.User, PermissionFlag.View)) + throw new DnsWebServiceException("Access was denied."); + + HttpRequest request = context.Request; + + string domain = request.GetQueryOrForm("domain", ""); + + string direction = request.QueryOrForm("direction"); if (direction is not null) direction = direction.ToLower(); @@ -188,8 +208,9 @@ namespace DnsServerCore subZones.Sort(); - jsonWriter.WritePropertyName("domain"); - jsonWriter.WriteValue(domain); + Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); + + jsonWriter.WriteString("domain", domain); jsonWriter.WritePropertyName("zones"); jsonWriter.WriteStartArray(); @@ -198,100 +219,104 @@ namespace DnsServerCore domain = "." + domain; foreach (string subZone in subZones) - jsonWriter.WriteValue(subZone + domain); + jsonWriter.WriteStringValue(subZone + domain); jsonWriter.WriteEndArray(); - WebServiceZonesApi.WriteRecordsAsJson(new List(records), jsonWriter, false); + WebServiceZonesApi.WriteRecordsAsJson(records, jsonWriter, true); } - public async Task ImportAllowedZonesAsync(HttpListenerRequest request) + public void ImportAllowedZones(HttpContext context) { - if (!request.ContentType.StartsWith("application/x-www-form-urlencoded")) - throw new DnsWebServiceException("Invalid content type. Expected application/x-www-form-urlencoded."); + UserSession session = context.GetCurrentSession(); - string formRequest; - using (StreamReader sR = new StreamReader(request.InputStream, request.ContentEncoding)) + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Allowed, session.User, PermissionFlag.Modify)) + throw new DnsWebServiceException("Access was denied."); + + HttpRequest request = context.Request; + + string allowedZones = request.GetQueryOrForm("allowedZones"); + string[] allowedZonesList = allowedZones.Split(','); + bool added = false; + + foreach (string allowedZone in allowedZonesList) { - formRequest = await sR.ReadToEndAsync(); + if (_dnsWebService.DnsServer.AllowedZoneManager.AllowZone(allowedZone)) + added = true; } - string[] formParts = formRequest.Split('&'); - - foreach (string formPart in formParts) + if (added) { - if (formPart.StartsWith("allowedZones=")) - { - string value = Uri.UnescapeDataString(formPart.Substring(13)); - string[] allowedZones = value.Split(','); - bool added = false; - - foreach (string allowedZone in allowedZones) - { - if (_dnsWebService.DnsServer.AllowedZoneManager.AllowZone(allowedZone)) - added = true; - } - - if (added) - { - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + _dnsWebService.GetSession(request).User.Username + "] Total " + allowedZones.Length + " zones were imported into allowed zone successfully."); - _dnsWebService.DnsServer.AllowedZoneManager.SaveZoneFile(); - } - - return; - } - } - - throw new DnsWebServiceException("Parameter 'allowedZones' missing."); - } - - public void ExportAllowedZones(HttpListenerResponse response) - { - IReadOnlyList zoneInfoList = _dnsWebService.DnsServer.AllowedZoneManager.ListZones(); - - response.ContentType = "text/plain"; - response.AddHeader("Content-Disposition", "attachment;filename=AllowedZones.txt"); - - using (StreamWriter sW = new StreamWriter(new BufferedStream(response.OutputStream))) - { - foreach (AuthZoneInfo zoneInfo in zoneInfoList) - sW.WriteLine(zoneInfo.Name); - } - } - - public void DeleteAllowedZone(HttpListenerRequest request) - { - string domain = request.QueryString["domain"]; - if (string.IsNullOrEmpty(domain)) - throw new DnsWebServiceException("Parameter 'domain' missing."); - - if (_dnsWebService.DnsServer.AllowedZoneManager.DeleteZone(domain)) - { - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + _dnsWebService.GetSession(request).User.Username + "] Allowed zone was deleted: " + domain); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] Total " + allowedZonesList.Length + " zones were imported into allowed zone successfully."); _dnsWebService.DnsServer.AllowedZoneManager.SaveZoneFile(); } } - public void FlushAllowedZone(HttpListenerRequest request) + public async Task ExportAllowedZonesAsync(HttpContext context) { + UserSession session = context.GetCurrentSession(); + + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Allowed, session.User, PermissionFlag.View)) + throw new DnsWebServiceException("Access was denied."); + + IReadOnlyList zoneInfoList = _dnsWebService.DnsServer.AllowedZoneManager.GetAllZones(); + + HttpResponse response = context.Response; + + response.ContentType = "text/plain"; + response.Headers.ContentDisposition = "attachment;filename=AllowedZones.txt"; + + await using (StreamWriter sW = new StreamWriter(response.Body)) + { + foreach (AuthZoneInfo zoneInfo in zoneInfoList) + await sW.WriteLineAsync(zoneInfo.Name); + } + } + + public void DeleteAllowedZone(HttpContext context) + { + UserSession session = context.GetCurrentSession(); + + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Allowed, session.User, PermissionFlag.Delete)) + throw new DnsWebServiceException("Access was denied."); + + string domain = context.Request.GetQueryOrForm("domain"); + + if (_dnsWebService.DnsServer.AllowedZoneManager.DeleteZone(domain)) + { + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] Allowed zone was deleted: " + domain); + _dnsWebService.DnsServer.AllowedZoneManager.SaveZoneFile(); + } + } + + public void FlushAllowedZone(HttpContext context) + { + UserSession session = context.GetCurrentSession(); + + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Allowed, session.User, PermissionFlag.Delete)) + throw new DnsWebServiceException("Access was denied."); + _dnsWebService.DnsServer.AllowedZoneManager.Flush(); - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + _dnsWebService.GetSession(request).User.Username + "] Allowed zone was flushed successfully."); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] Allowed zone was flushed successfully."); _dnsWebService.DnsServer.AllowedZoneManager.SaveZoneFile(); } - public void AllowZone(HttpListenerRequest request) + public void AllowZone(HttpContext context) { - string domain = request.QueryString["domain"]; - if (string.IsNullOrEmpty(domain)) - throw new DnsWebServiceException("Parameter 'domain' missing."); + UserSession session = context.GetCurrentSession(); + + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Allowed, session.User, PermissionFlag.Modify)) + throw new DnsWebServiceException("Access was denied."); + + string domain = context.Request.GetQueryOrForm("domain"); if (IPAddress.TryParse(domain, out IPAddress ipAddress)) domain = ipAddress.GetReverseDomain(); if (_dnsWebService.DnsServer.AllowedZoneManager.AllowZone(domain)) { - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + _dnsWebService.GetSession(request).User.Username + "] Zone was allowed: " + domain); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] Zone was allowed: " + domain); _dnsWebService.DnsServer.AllowedZoneManager.SaveZoneFile(); } } @@ -300,13 +325,18 @@ namespace DnsServerCore #region blocked zones api - public void ListBlockedZones(HttpListenerRequest request, JsonTextWriter jsonWriter) + public void ListBlockedZones(HttpContext context) { - string domain = request.QueryString["domain"]; - if (domain == null) - domain = ""; + UserSession session = context.GetCurrentSession(); - string direction = request.QueryString["direction"]; + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Blocked, session.User, PermissionFlag.View)) + throw new DnsWebServiceException("Access was denied."); + + HttpRequest request = context.Request; + + string domain = request.GetQueryOrForm("domain", ""); + + string direction = request.QueryOrForm("direction"); if (direction is not null) direction = direction.ToLower(); @@ -350,8 +380,9 @@ namespace DnsServerCore subZones.Sort(); - jsonWriter.WritePropertyName("domain"); - jsonWriter.WriteValue(domain); + Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); + + jsonWriter.WriteString("domain", domain); jsonWriter.WritePropertyName("zones"); jsonWriter.WriteStartArray(); @@ -360,100 +391,104 @@ namespace DnsServerCore domain = "." + domain; foreach (string subZone in subZones) - jsonWriter.WriteValue(subZone + domain); + jsonWriter.WriteStringValue(subZone + domain); jsonWriter.WriteEndArray(); - WebServiceZonesApi.WriteRecordsAsJson(new List(records), jsonWriter, false); + WebServiceZonesApi.WriteRecordsAsJson(records, jsonWriter, true); } - public async Task ImportBlockedZonesAsync(HttpListenerRequest request) + public void ImportBlockedZones(HttpContext context) { - if (!request.ContentType.StartsWith("application/x-www-form-urlencoded")) - throw new DnsWebServiceException("Invalid content type. Expected application/x-www-form-urlencoded."); + UserSession session = context.GetCurrentSession(); - string formRequest; - using (StreamReader sR = new StreamReader(request.InputStream, request.ContentEncoding)) + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Blocked, session.User, PermissionFlag.Modify)) + throw new DnsWebServiceException("Access was denied."); + + HttpRequest request = context.Request; + + string blockedZones = request.GetQueryOrForm("blockedZones"); + string[] blockedZonesList = blockedZones.Split(','); + bool added = false; + + foreach (string blockedZone in blockedZonesList) { - formRequest = await sR.ReadToEndAsync(); + if (_dnsWebService.DnsServer.BlockedZoneManager.BlockZone(blockedZone)) + added = true; } - string[] formParts = formRequest.Split('&'); - - foreach (string formPart in formParts) + if (added) { - if (formPart.StartsWith("blockedZones=")) - { - string value = Uri.UnescapeDataString(formPart.Substring(13)); - string[] blockedZones = value.Split(','); - bool added = false; - - foreach (string blockedZone in blockedZones) - { - if (_dnsWebService.DnsServer.BlockedZoneManager.BlockZone(blockedZone)) - added = true; - } - - if (added) - { - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + _dnsWebService.GetSession(request).User.Username + "] Total " + blockedZones.Length + " zones were imported into blocked zone successfully."); - _dnsWebService.DnsServer.BlockedZoneManager.SaveZoneFile(); - } - - return; - } - } - - throw new DnsWebServiceException("Parameter 'blockedZones' missing."); - } - - public void ExportBlockedZones(HttpListenerResponse response) - { - IReadOnlyList zoneInfoList = _dnsWebService.DnsServer.BlockedZoneManager.ListZones(); - - response.ContentType = "text/plain"; - response.AddHeader("Content-Disposition", "attachment;filename=BlockedZones.txt"); - - using (StreamWriter sW = new StreamWriter(new BufferedStream(response.OutputStream))) - { - foreach (AuthZoneInfo zoneInfo in zoneInfoList) - sW.WriteLine(zoneInfo.Name); - } - } - - public void DeleteBlockedZone(HttpListenerRequest request) - { - string domain = request.QueryString["domain"]; - if (string.IsNullOrEmpty(domain)) - throw new DnsWebServiceException("Parameter 'domain' missing."); - - if (_dnsWebService.DnsServer.BlockedZoneManager.DeleteZone(domain)) - { - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + _dnsWebService.GetSession(request).User.Username + "] Blocked zone was deleted: " + domain); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] Total " + blockedZonesList.Length + " zones were imported into blocked zone successfully."); _dnsWebService.DnsServer.BlockedZoneManager.SaveZoneFile(); } } - public void FlushBlockedZone(HttpListenerRequest request) + public async Task ExportBlockedZonesAsync(HttpContext context) { + UserSession session = context.GetCurrentSession(); + + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Blocked, session.User, PermissionFlag.View)) + throw new DnsWebServiceException("Access was denied."); + + IReadOnlyList zoneInfoList = _dnsWebService.DnsServer.BlockedZoneManager.GetAllZones(); + + HttpResponse response = context.Response; + + response.ContentType = "text/plain"; + response.Headers.ContentDisposition = "attachment;filename=BlockedZones.txt"; + + await using (StreamWriter sW = new StreamWriter(response.Body)) + { + foreach (AuthZoneInfo zoneInfo in zoneInfoList) + await sW.WriteLineAsync(zoneInfo.Name); + } + } + + public void DeleteBlockedZone(HttpContext context) + { + UserSession session = context.GetCurrentSession(); + + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Blocked, session.User, PermissionFlag.Delete)) + throw new DnsWebServiceException("Access was denied."); + + string domain = context.Request.GetQueryOrForm("domain"); + + if (_dnsWebService.DnsServer.BlockedZoneManager.DeleteZone(domain)) + { + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] Blocked zone was deleted: " + domain); + _dnsWebService.DnsServer.BlockedZoneManager.SaveZoneFile(); + } + } + + public void FlushBlockedZone(HttpContext context) + { + UserSession session = context.GetCurrentSession(); + + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Blocked, session.User, PermissionFlag.Delete)) + throw new DnsWebServiceException("Access was denied."); + _dnsWebService.DnsServer.BlockedZoneManager.Flush(); - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + _dnsWebService.GetSession(request).User.Username + "] Blocked zone was flushed successfully."); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] Blocked zone was flushed successfully."); _dnsWebService.DnsServer.BlockedZoneManager.SaveZoneFile(); } - public void BlockZone(HttpListenerRequest request) + public void BlockZone(HttpContext context) { - string domain = request.QueryString["domain"]; - if (string.IsNullOrEmpty(domain)) - throw new DnsWebServiceException("Parameter 'domain' missing."); + UserSession session = context.GetCurrentSession(); + + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Blocked, session.User, PermissionFlag.Modify)) + throw new DnsWebServiceException("Access was denied."); + + string domain = context.Request.GetQueryOrForm("domain"); if (IPAddress.TryParse(domain, out IPAddress ipAddress)) domain = ipAddress.GetReverseDomain(); if (_dnsWebService.DnsServer.BlockedZoneManager.BlockZone(domain)) { - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + _dnsWebService.GetSession(request).User.Username + "] Domain was added to blocked zone: " + domain); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] Domain was added to blocked zone: " + domain); _dnsWebService.DnsServer.BlockedZoneManager.SaveZoneFile(); } } diff --git a/DnsServerCore/WebServiceSettingsApi.cs b/DnsServerCore/WebServiceSettingsApi.cs new file mode 100644 index 00000000..22ff70fb --- /dev/null +++ b/DnsServerCore/WebServiceSettingsApi.cs @@ -0,0 +1,1820 @@ +/* +Technitium DNS Server +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +using DnsServerCore.Auth; +using DnsServerCore.Dns; +using Microsoft.AspNetCore.Http; +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using TechnitiumLibrary; +using TechnitiumLibrary.Net; +using TechnitiumLibrary.Net.Dns; +using TechnitiumLibrary.Net.Dns.ResourceRecords; +using TechnitiumLibrary.Net.Proxy; + +namespace DnsServerCore +{ + sealed class WebServiceSettingsApi : IDisposable + { + #region variables + + readonly static RandomNumberGenerator _rng = RandomNumberGenerator.Create(); + + readonly DnsWebService _dnsWebService; + + Timer _blockListUpdateTimer; + DateTime _blockListLastUpdatedOn; + int _blockListUpdateIntervalHours = 24; + const int BLOCK_LIST_UPDATE_TIMER_INITIAL_INTERVAL = 5000; + const int BLOCK_LIST_UPDATE_TIMER_PERIODIC_INTERVAL = 900000; + + Timer _temporaryDisableBlockingTimer; + DateTime _temporaryDisableBlockingTill; + + #endregion + + #region constructor + + public WebServiceSettingsApi(DnsWebService dnsWebService) + { + _dnsWebService = dnsWebService; + } + + #endregion + + #region IDisposable + + bool _disposed; + + public void Dispose() + { + if (_disposed) + return; + + if (_blockListUpdateTimer is not null) + _blockListUpdateTimer.Dispose(); + + if (_temporaryDisableBlockingTimer is not null) + _temporaryDisableBlockingTimer.Dispose(); + + _disposed = true; + } + + #endregion + + #region block list + + private void ForceUpdateBlockLists() + { + Task.Run(async delegate () + { + if (await _dnsWebService.DnsServer.BlockListZoneManager.UpdateBlockListsAsync()) + { + //block lists were updated + //save last updated on time + _blockListLastUpdatedOn = DateTime.UtcNow; + _dnsWebService.SaveConfigFile(); + } + }); + } + + public void StartBlockListUpdateTimer() + { + if (_blockListUpdateTimer is null) + { + _blockListUpdateTimer = new Timer(async delegate (object state) + { + try + { + if (DateTime.UtcNow > _blockListLastUpdatedOn.AddHours(_blockListUpdateIntervalHours)) + { + if (await _dnsWebService.DnsServer.BlockListZoneManager.UpdateBlockListsAsync()) + { + //block lists were updated + //save last updated on time + _blockListLastUpdatedOn = DateTime.UtcNow; + _dnsWebService.SaveConfigFile(); + } + } + } + catch (Exception ex) + { + _dnsWebService._log.Write("DNS Server encountered an error while updating block lists.\r\n" + ex.ToString()); + } + + }, null, BLOCK_LIST_UPDATE_TIMER_INITIAL_INTERVAL, BLOCK_LIST_UPDATE_TIMER_PERIODIC_INTERVAL); + } + } + + public void StopBlockListUpdateTimer() + { + if (_blockListUpdateTimer is not null) + { + _blockListUpdateTimer.Dispose(); + _blockListUpdateTimer = null; + } + } + + public void StopTemporaryDisableBlockingTimer() + { + Timer temporaryDisableBlockingTimer = _temporaryDisableBlockingTimer; + if (temporaryDisableBlockingTimer is not null) + temporaryDisableBlockingTimer.Dispose(); + } + + #endregion + + #region private + + private void RestartService(bool restartDnsService, bool restartWebService) + { + if (restartDnsService) + { + _ = Task.Run(async delegate () + { + _dnsWebService._log.Write("Attempting to restart DNS service."); + + try + { + await _dnsWebService.DnsServer.StopAsync(); + await _dnsWebService.DnsServer.StartAsync(); + + _dnsWebService._log.Write("DNS service was restarted successfully."); + } + catch (Exception ex) + { + _dnsWebService._log.Write("Failed to restart DNS service."); + _dnsWebService._log.Write(ex); + } + }); + } + + if (restartWebService) + { + _ = Task.Run(async delegate () + { + await Task.Delay(2000); //wait for this HTTP response to be delivered before stopping web server + + _dnsWebService._log.Write("Attempting to restart web service."); + + try + { + await _dnsWebService.StopWebServiceAsync(); + await _dnsWebService.TryStartWebServiceAsync(); + + _dnsWebService._log.Write("Web service was restarted successfully."); + } + catch (Exception ex) + { + _dnsWebService._log.Write("Failed to restart web service."); + _dnsWebService._log.Write(ex); + } + }); + } + } + + private static async Task CreateBackupEntryFromFileAsync(ZipArchive backupZip, string sourceFileName, string entryName) + { + using (FileStream fS = new FileStream(sourceFileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) + { + ZipArchiveEntry entry = backupZip.CreateEntry(entryName); + + DateTime lastWrite = File.GetLastWriteTime(sourceFileName); + + // If file to be archived has an invalid last modified time, use the first datetime representable in the Zip timestamp format + // (midnight on January 1, 1980): + if (lastWrite.Year < 1980 || lastWrite.Year > 2107) + lastWrite = new DateTime(1980, 1, 1, 0, 0, 0); + + entry.LastWriteTime = lastWrite; + + using (Stream sE = entry.Open()) + { + await fS.CopyToAsync(sE); + } + } + } + + private void WriteDnsSettings(Utf8JsonWriter jsonWriter) + { + //general + jsonWriter.WriteString("version", _dnsWebService.GetServerVersion()); + jsonWriter.WriteString("dnsServerDomain", _dnsWebService.DnsServer.ServerDomain); + + jsonWriter.WritePropertyName("dnsServerLocalEndPoints"); + jsonWriter.WriteStartArray(); + + foreach (IPEndPoint localEP in _dnsWebService.DnsServer.LocalEndPoints) + jsonWriter.WriteStringValue(localEP.ToString()); + + jsonWriter.WriteEndArray(); + + jsonWriter.WriteNumber("defaultRecordTtl", _dnsWebService._zonesApi.DefaultRecordTtl); + jsonWriter.WriteBoolean("dnsAppsEnableAutomaticUpdate", _dnsWebService._appsApi.EnableAutomaticUpdate); + + jsonWriter.WriteBoolean("preferIPv6", _dnsWebService.DnsServer.PreferIPv6); + + jsonWriter.WriteNumber("udpPayloadSize", _dnsWebService.DnsServer.UdpPayloadSize); + + jsonWriter.WriteBoolean("dnssecValidation", _dnsWebService.DnsServer.DnssecValidation); + + jsonWriter.WriteBoolean("eDnsClientSubnet", _dnsWebService.DnsServer.EDnsClientSubnet); + jsonWriter.WriteNumber("eDnsClientSubnetIPv4PrefixLength", _dnsWebService.DnsServer.EDnsClientSubnetIPv4PrefixLength); + jsonWriter.WriteNumber("eDnsClientSubnetIPv6PrefixLength", _dnsWebService.DnsServer.EDnsClientSubnetIPv6PrefixLength); + + jsonWriter.WriteNumber("qpmLimitRequests", _dnsWebService.DnsServer.QpmLimitRequests); + jsonWriter.WriteNumber("qpmLimitErrors", _dnsWebService.DnsServer.QpmLimitErrors); + jsonWriter.WriteNumber("qpmLimitSampleMinutes", _dnsWebService.DnsServer.QpmLimitSampleMinutes); + jsonWriter.WriteNumber("qpmLimitIPv4PrefixLength", _dnsWebService.DnsServer.QpmLimitIPv4PrefixLength); + jsonWriter.WriteNumber("qpmLimitIPv6PrefixLength", _dnsWebService.DnsServer.QpmLimitIPv6PrefixLength); + + jsonWriter.WriteNumber("clientTimeout", _dnsWebService.DnsServer.ClientTimeout); + jsonWriter.WriteNumber("tcpSendTimeout", _dnsWebService.DnsServer.TcpSendTimeout); + jsonWriter.WriteNumber("tcpReceiveTimeout", _dnsWebService.DnsServer.TcpReceiveTimeout); + jsonWriter.WriteNumber("quicIdleTimeout", _dnsWebService.DnsServer.QuicIdleTimeout); + jsonWriter.WriteNumber("quicMaxInboundStreams", _dnsWebService.DnsServer.QuicMaxInboundStreams); + jsonWriter.WriteNumber("listenBacklog", _dnsWebService.DnsServer.ListenBacklog); + + //web service + jsonWriter.WritePropertyName("webServiceLocalAddresses"); + jsonWriter.WriteStartArray(); + + foreach (IPAddress localAddress in _dnsWebService._webServiceLocalAddresses) + { + if (localAddress.AddressFamily == AddressFamily.InterNetworkV6) + jsonWriter.WriteStringValue("[" + localAddress.ToString() + "]"); + else + jsonWriter.WriteStringValue(localAddress.ToString()); + } + + jsonWriter.WriteEndArray(); + + jsonWriter.WriteNumber("webServiceHttpPort", _dnsWebService._webServiceHttpPort); + jsonWriter.WriteBoolean("webServiceEnableTls", _dnsWebService._webServiceEnableTls); + jsonWriter.WriteBoolean("webServiceHttpToTlsRedirect", _dnsWebService._webServiceHttpToTlsRedirect); + jsonWriter.WriteBoolean("webServiceUseSelfSignedTlsCertificate", _dnsWebService._webServiceUseSelfSignedTlsCertificate); + jsonWriter.WriteNumber("webServiceTlsPort", _dnsWebService._webServiceTlsPort); + jsonWriter.WriteString("webServiceTlsCertificatePath", _dnsWebService._webServiceTlsCertificatePath); + jsonWriter.WriteString("webServiceTlsCertificatePassword", "************"); + + //optional protocols + jsonWriter.WriteBoolean("enableDnsOverHttp", _dnsWebService.DnsServer.EnableDnsOverHttp); + jsonWriter.WriteBoolean("enableDnsOverTls", _dnsWebService.DnsServer.EnableDnsOverTls); + jsonWriter.WriteBoolean("enableDnsOverHttps", _dnsWebService.DnsServer.EnableDnsOverHttps); + jsonWriter.WriteBoolean("enableDnsOverQuic", _dnsWebService.DnsServer.EnableDnsOverQuic); + jsonWriter.WriteNumber("dnsOverHttpPort", _dnsWebService.DnsServer.DnsOverHttpPort); + jsonWriter.WriteNumber("dnsOverTlsPort", _dnsWebService.DnsServer.DnsOverTlsPort); + jsonWriter.WriteNumber("dnsOverHttpsPort", _dnsWebService.DnsServer.DnsOverHttpsPort); + jsonWriter.WriteNumber("dnsOverQuicPort", _dnsWebService.DnsServer.DnsOverQuicPort); + jsonWriter.WriteString("dnsTlsCertificatePath", _dnsWebService._dnsTlsCertificatePath); + jsonWriter.WriteString("dnsTlsCertificatePassword", "************"); + + //tsig + jsonWriter.WritePropertyName("tsigKeys"); + { + jsonWriter.WriteStartArray(); + + if (_dnsWebService.DnsServer.TsigKeys is not null) + { + foreach (KeyValuePair tsigKey in _dnsWebService.DnsServer.TsigKeys) + { + jsonWriter.WriteStartObject(); + + jsonWriter.WriteString("keyName", tsigKey.Key); + jsonWriter.WriteString("sharedSecret", tsigKey.Value.SharedSecret); + jsonWriter.WriteString("algorithmName", tsigKey.Value.AlgorithmName); + + jsonWriter.WriteEndObject(); + } + } + + jsonWriter.WriteEndArray(); + } + + //recursion + jsonWriter.WriteString("recursion", _dnsWebService.DnsServer.Recursion.ToString()); + + jsonWriter.WritePropertyName("recursionDeniedNetworks"); + { + jsonWriter.WriteStartArray(); + + if (_dnsWebService.DnsServer.RecursionDeniedNetworks is not null) + { + foreach (NetworkAddress networkAddress in _dnsWebService.DnsServer.RecursionDeniedNetworks) + jsonWriter.WriteStringValue(networkAddress.ToString()); + } + + jsonWriter.WriteEndArray(); + } + + jsonWriter.WritePropertyName("recursionAllowedNetworks"); + { + jsonWriter.WriteStartArray(); + + if (_dnsWebService.DnsServer.RecursionAllowedNetworks is not null) + { + foreach (NetworkAddress networkAddress in _dnsWebService.DnsServer.RecursionAllowedNetworks) + jsonWriter.WriteStringValue(networkAddress.ToString()); + } + + jsonWriter.WriteEndArray(); + } + + jsonWriter.WriteBoolean("randomizeName", _dnsWebService.DnsServer.RandomizeName); + jsonWriter.WriteBoolean("qnameMinimization", _dnsWebService.DnsServer.QnameMinimization); + jsonWriter.WriteBoolean("nsRevalidation", _dnsWebService.DnsServer.NsRevalidation); + + jsonWriter.WriteNumber("resolverRetries", _dnsWebService.DnsServer.ResolverRetries); + jsonWriter.WriteNumber("resolverTimeout", _dnsWebService.DnsServer.ResolverTimeout); + jsonWriter.WriteNumber("resolverMaxStackCount", _dnsWebService.DnsServer.ResolverMaxStackCount); + + //cache + jsonWriter.WriteBoolean("saveCache", _dnsWebService._saveCache); + jsonWriter.WriteBoolean("serveStale", _dnsWebService.DnsServer.ServeStale); + jsonWriter.WriteNumber("serveStaleTtl", _dnsWebService.DnsServer.CacheZoneManager.ServeStaleTtl); + + jsonWriter.WriteNumber("cacheMaximumEntries", _dnsWebService.DnsServer.CacheZoneManager.MaximumEntries); + jsonWriter.WriteNumber("cacheMinimumRecordTtl", _dnsWebService.DnsServer.CacheZoneManager.MinimumRecordTtl); + jsonWriter.WriteNumber("cacheMaximumRecordTtl", _dnsWebService.DnsServer.CacheZoneManager.MaximumRecordTtl); + jsonWriter.WriteNumber("cacheNegativeRecordTtl", _dnsWebService.DnsServer.CacheZoneManager.NegativeRecordTtl); + jsonWriter.WriteNumber("cacheFailureRecordTtl", _dnsWebService.DnsServer.CacheZoneManager.FailureRecordTtl); + + jsonWriter.WriteNumber("cachePrefetchEligibility", _dnsWebService.DnsServer.CachePrefetchEligibility); + jsonWriter.WriteNumber("cachePrefetchTrigger", _dnsWebService.DnsServer.CachePrefetchTrigger); + jsonWriter.WriteNumber("cachePrefetchSampleIntervalInMinutes", _dnsWebService.DnsServer.CachePrefetchSampleIntervalInMinutes); + jsonWriter.WriteNumber("cachePrefetchSampleEligibilityHitsPerHour", _dnsWebService.DnsServer.CachePrefetchSampleEligibilityHitsPerHour); + + //blocking + jsonWriter.WriteBoolean("enableBlocking", _dnsWebService.DnsServer.EnableBlocking); + jsonWriter.WriteBoolean("allowTxtBlockingReport", _dnsWebService.DnsServer.AllowTxtBlockingReport); + + if (!_dnsWebService.DnsServer.EnableBlocking && (DateTime.UtcNow < _temporaryDisableBlockingTill)) + jsonWriter.WriteString("temporaryDisableBlockingTill", _temporaryDisableBlockingTill); + + jsonWriter.WriteString("blockingType", _dnsWebService.DnsServer.BlockingType.ToString()); + + jsonWriter.WritePropertyName("customBlockingAddresses"); + jsonWriter.WriteStartArray(); + + foreach (DnsARecordData record in _dnsWebService.DnsServer.CustomBlockingARecords) + jsonWriter.WriteStringValue(record.Address.ToString()); + + foreach (DnsAAAARecordData record in _dnsWebService.DnsServer.CustomBlockingAAAARecords) + jsonWriter.WriteStringValue(record.Address.ToString()); + + jsonWriter.WriteEndArray(); + + jsonWriter.WritePropertyName("blockListUrls"); + + if ((_dnsWebService.DnsServer.BlockListZoneManager.AllowListUrls.Count == 0) && (_dnsWebService.DnsServer.BlockListZoneManager.BlockListUrls.Count == 0)) + { + jsonWriter.WriteNullValue(); + } + else + { + jsonWriter.WriteStartArray(); + + foreach (Uri allowListUrl in _dnsWebService.DnsServer.BlockListZoneManager.AllowListUrls) + jsonWriter.WriteStringValue("!" + allowListUrl.AbsoluteUri); + + foreach (Uri blockListUrl in _dnsWebService.DnsServer.BlockListZoneManager.BlockListUrls) + jsonWriter.WriteStringValue(blockListUrl.AbsoluteUri); + + jsonWriter.WriteEndArray(); + } + + jsonWriter.WriteNumber("blockListUpdateIntervalHours", _blockListUpdateIntervalHours); + + if (_blockListUpdateTimer is not null) + { + DateTime blockListNextUpdatedOn = _blockListLastUpdatedOn.AddHours(_blockListUpdateIntervalHours); + + jsonWriter.WriteString("blockListNextUpdatedOn", blockListNextUpdatedOn); + } + + //proxy & forwarders + jsonWriter.WritePropertyName("proxy"); + if (_dnsWebService.DnsServer.Proxy == null) + { + jsonWriter.WriteNullValue(); + } + else + { + jsonWriter.WriteStartObject(); + + NetProxy proxy = _dnsWebService.DnsServer.Proxy; + + jsonWriter.WriteString("type", proxy.Type.ToString()); + jsonWriter.WriteString("address", proxy.Address); + jsonWriter.WriteNumber("port", proxy.Port); + + NetworkCredential credential = proxy.Credential; + if (credential != null) + { + jsonWriter.WriteString("username", credential.UserName); + jsonWriter.WriteString("password", credential.Password); + } + + jsonWriter.WritePropertyName("bypass"); + jsonWriter.WriteStartArray(); + + foreach (NetProxyBypassItem item in proxy.BypassList) + jsonWriter.WriteStringValue(item.Value); + + jsonWriter.WriteEndArray(); + + jsonWriter.WriteEndObject(); + } + + jsonWriter.WritePropertyName("forwarders"); + + DnsTransportProtocol forwarderProtocol = DnsTransportProtocol.Udp; + + if (_dnsWebService.DnsServer.Forwarders == null) + { + jsonWriter.WriteNullValue(); + } + else + { + forwarderProtocol = _dnsWebService.DnsServer.Forwarders[0].Protocol; + + jsonWriter.WriteStartArray(); + + foreach (NameServerAddress forwarder in _dnsWebService.DnsServer.Forwarders) + jsonWriter.WriteStringValue(forwarder.OriginalAddress); + + jsonWriter.WriteEndArray(); + } + + jsonWriter.WriteString("forwarderProtocol", forwarderProtocol.ToString()); + + jsonWriter.WriteNumber("forwarderRetries", _dnsWebService.DnsServer.ForwarderRetries); + jsonWriter.WriteNumber("forwarderTimeout", _dnsWebService.DnsServer.ForwarderTimeout); + jsonWriter.WriteNumber("forwarderConcurrency", _dnsWebService.DnsServer.ForwarderConcurrency); + + //logging + jsonWriter.WriteBoolean("enableLogging", _dnsWebService._log.EnableLogging); + jsonWriter.WriteBoolean("logQueries", _dnsWebService.DnsServer.QueryLogManager != null); + jsonWriter.WriteBoolean("useLocalTime", _dnsWebService._log.UseLocalTime); + jsonWriter.WriteString("logFolder", _dnsWebService._log.LogFolder); + jsonWriter.WriteNumber("maxLogFileDays", _dnsWebService._log.MaxLogFileDays); + jsonWriter.WriteNumber("maxStatFileDays", _dnsWebService.DnsServer.StatsManager.MaxStatFileDays); + } + + #endregion + + #region public + + public void GetDnsSettings(HttpContext context) + { + UserSession session = context.GetCurrentSession(); + + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Settings, session.User, PermissionFlag.View)) + throw new DnsWebServiceException("Access was denied."); + + Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); + WriteDnsSettings(jsonWriter); + } + + public void SetDnsSettings(HttpContext context) + { + UserSession session = context.GetCurrentSession(); + + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Settings, session.User, PermissionFlag.Modify)) + throw new DnsWebServiceException("Access was denied."); + + bool serverDomainChanged = false; + bool restartDnsService = false; + bool restartWebService = false; + bool blockListUrlsUpdated = false; + int oldWebServiceHttpPort = _dnsWebService._webServiceHttpPort; + + HttpRequest request = context.Request; + + //general + if (request.TryGetQueryOrForm("dnsServerDomain", out string dnsServerDomain)) + { + if (!_dnsWebService.DnsServer.ServerDomain.Equals(dnsServerDomain, StringComparison.OrdinalIgnoreCase)) + { + _dnsWebService.DnsServer.ServerDomain = dnsServerDomain; + serverDomainChanged = true; + } + } + + string dnsServerLocalEndPoints = request.QueryOrForm("dnsServerLocalEndPoints"); + if (dnsServerLocalEndPoints is not null) + { + if (dnsServerLocalEndPoints.Length == 0) + dnsServerLocalEndPoints = "0.0.0.0:53,[::]:53"; + + IPEndPoint[] localEndPoints = dnsServerLocalEndPoints.Split(IPEndPoint.Parse, ','); + if (localEndPoints.Length > 0) + { + foreach (IPEndPoint localEndPoint in localEndPoints) + { + if (localEndPoint.Port == 0) + localEndPoint.Port = 53; + } + + if (_dnsWebService.DnsServer.LocalEndPoints.Count != localEndPoints.Length) + { + restartDnsService = true; + } + else + { + foreach (IPEndPoint currentLocalEP in _dnsWebService.DnsServer.LocalEndPoints) + { + if (!localEndPoints.Contains(currentLocalEP)) + { + restartDnsService = true; + break; + } + } + } + + _dnsWebService.DnsServer.LocalEndPoints = localEndPoints; + } + } + + if (request.TryGetQueryOrForm("defaultRecordTtl", uint.Parse, out uint defaultRecordTtl)) + _dnsWebService._zonesApi.DefaultRecordTtl = defaultRecordTtl; + + if (request.TryGetQueryOrForm("dnsAppsEnableAutomaticUpdate", bool.Parse, out bool dnsAppsEnableAutomaticUpdate)) + _dnsWebService._appsApi.EnableAutomaticUpdate = dnsAppsEnableAutomaticUpdate; + + if (request.TryGetQueryOrForm("preferIPv6", bool.Parse, out bool preferIPv6)) + _dnsWebService.DnsServer.PreferIPv6 = preferIPv6; + + if (request.TryGetQueryOrForm("udpPayloadSize", ushort.Parse, out ushort udpPayloadSize)) + _dnsWebService.DnsServer.UdpPayloadSize = udpPayloadSize; + + if (request.TryGetQueryOrForm("dnssecValidation", bool.Parse, out bool dnssecValidation)) + _dnsWebService.DnsServer.DnssecValidation = dnssecValidation; + + if (request.TryGetQueryOrForm("eDnsClientSubnet", bool.Parse, out bool eDnsClientSubnet)) + _dnsWebService.DnsServer.EDnsClientSubnet = eDnsClientSubnet; + + if (request.TryGetQueryOrForm("eDnsClientSubnetIPv4PrefixLength", byte.Parse, out byte eDnsClientSubnetIPv4PrefixLength)) + _dnsWebService.DnsServer.EDnsClientSubnetIPv4PrefixLength = eDnsClientSubnetIPv4PrefixLength; + + if (request.TryGetQueryOrForm("eDnsClientSubnetIPv6PrefixLength", byte.Parse, out byte eDnsClientSubnetIPv6PrefixLength)) + _dnsWebService.DnsServer.EDnsClientSubnetIPv6PrefixLength = eDnsClientSubnetIPv6PrefixLength; + + if (request.TryGetQueryOrForm("qpmLimitRequests", int.Parse, out int qpmLimitRequests)) + _dnsWebService.DnsServer.QpmLimitRequests = qpmLimitRequests; + + if (request.TryGetQueryOrForm("qpmLimitErrors", int.Parse, out int qpmLimitErrors)) + _dnsWebService.DnsServer.QpmLimitErrors = qpmLimitErrors; + + if (request.TryGetQueryOrForm("qpmLimitSampleMinutes", int.Parse, out int qpmLimitSampleMinutes)) + _dnsWebService.DnsServer.QpmLimitSampleMinutes = qpmLimitSampleMinutes; + + if (request.TryGetQueryOrForm("qpmLimitIPv4PrefixLength", int.Parse, out int qpmLimitIPv4PrefixLength)) + _dnsWebService.DnsServer.QpmLimitIPv4PrefixLength = qpmLimitIPv4PrefixLength; + + if (request.TryGetQueryOrForm("qpmLimitIPv6PrefixLength", int.Parse, out int qpmLimitIPv6PrefixLength)) + _dnsWebService.DnsServer.QpmLimitIPv6PrefixLength = qpmLimitIPv6PrefixLength; + + if (request.TryGetQueryOrForm("clientTimeout", int.Parse, out int clientTimeout)) + _dnsWebService.DnsServer.ClientTimeout = clientTimeout; + + if (request.TryGetQueryOrForm("tcpSendTimeout", int.Parse, out int tcpSendTimeout)) + _dnsWebService.DnsServer.TcpSendTimeout = tcpSendTimeout; + + if (request.TryGetQueryOrForm("tcpReceiveTimeout", int.Parse, out int tcpReceiveTimeout)) + _dnsWebService.DnsServer.TcpReceiveTimeout = tcpReceiveTimeout; + + if (request.TryGetQueryOrForm("quicIdleTimeout", int.Parse, out int quicIdleTimeout)) + _dnsWebService.DnsServer.QuicIdleTimeout = quicIdleTimeout; + + if (request.TryGetQueryOrForm("quicMaxInboundStreams", int.Parse, out int quicMaxInboundStreams)) + _dnsWebService.DnsServer.QuicMaxInboundStreams = quicMaxInboundStreams; + + if (request.TryGetQueryOrForm("listenBacklog", int.Parse, out int listenBacklog)) + _dnsWebService.DnsServer.ListenBacklog = listenBacklog; + + //web service + string webServiceLocalAddresses = request.QueryOrForm("webServiceLocalAddresses"); + if (webServiceLocalAddresses is not null) + { + if (webServiceLocalAddresses.Length == 0) + webServiceLocalAddresses = "0.0.0.0,[::]"; + + IPAddress[] localAddresses = webServiceLocalAddresses.Split(IPAddress.Parse, ','); + if (localAddresses.Length > 0) + { + if (_dnsWebService._webServiceLocalAddresses.Count != localAddresses.Length) + { + restartWebService = true; + } + else + { + foreach (IPAddress currentlocalAddress in _dnsWebService._webServiceLocalAddresses) + { + if (!localAddresses.Contains(currentlocalAddress)) + { + restartWebService = true; + break; + } + } + } + + _dnsWebService._webServiceLocalAddresses = DnsServer.GetValidKestralLocalAddresses(localAddresses); + } + } + + if (request.TryGetQueryOrForm("webServiceHttpPort", int.Parse, out int webServiceHttpPort)) + { + if (_dnsWebService._webServiceHttpPort != webServiceHttpPort) + { + _dnsWebService._webServiceHttpPort = webServiceHttpPort; + restartWebService = true; + } + } + + if (request.TryGetQueryOrForm("webServiceEnableTls", bool.Parse, out bool webServiceEnableTls)) + { + if (_dnsWebService._webServiceEnableTls != webServiceEnableTls) + { + _dnsWebService._webServiceEnableTls = webServiceEnableTls; + restartWebService = true; + } + } + + if (request.TryGetQueryOrForm("webServiceHttpToTlsRedirect", bool.Parse, out bool webServiceHttpToTlsRedirect)) + { + if (_dnsWebService._webServiceHttpToTlsRedirect != webServiceHttpToTlsRedirect) + { + _dnsWebService._webServiceHttpToTlsRedirect = webServiceHttpToTlsRedirect; + restartWebService = true; + } + } + + if (request.TryGetQueryOrForm("webServiceUseSelfSignedTlsCertificate", bool.Parse, out bool webServiceUseSelfSignedTlsCertificate)) + _dnsWebService._webServiceUseSelfSignedTlsCertificate = webServiceUseSelfSignedTlsCertificate; + + if (request.TryGetQueryOrForm("webServiceTlsPort", int.Parse, out int webServiceTlsPort)) + { + if (_dnsWebService._webServiceTlsPort != webServiceTlsPort) + { + _dnsWebService._webServiceTlsPort = webServiceTlsPort; + restartWebService = true; + } + } + + string webServiceTlsCertificatePath = request.QueryOrForm("webServiceTlsCertificatePath"); + if (webServiceTlsCertificatePath is not null) + { + if (webServiceTlsCertificatePath.Length == 0) + { + _dnsWebService._webServiceTlsCertificatePath = null; + _dnsWebService._webServiceTlsCertificatePassword = ""; + } + else + { + string webServiceTlsCertificatePassword = request.QueryOrForm("webServiceTlsCertificatePassword"); + + if ((webServiceTlsCertificatePassword is null) || (webServiceTlsCertificatePassword == "************")) + webServiceTlsCertificatePassword = _dnsWebService._webServiceTlsCertificatePassword; + + if ((webServiceTlsCertificatePath != _dnsWebService._webServiceTlsCertificatePath) || (webServiceTlsCertificatePassword != _dnsWebService._webServiceTlsCertificatePassword)) + { + _dnsWebService.LoadWebServiceTlsCertificate(webServiceTlsCertificatePath, webServiceTlsCertificatePassword); + + _dnsWebService._webServiceTlsCertificatePath = webServiceTlsCertificatePath; + _dnsWebService._webServiceTlsCertificatePassword = webServiceTlsCertificatePassword; + + _dnsWebService.StartTlsCertificateUpdateTimer(); + } + } + } + + //optional protocols + if (request.TryGetQueryOrForm("enableDnsOverHttp", bool.Parse, out bool enableDnsOverHttp)) + { + if (_dnsWebService.DnsServer.EnableDnsOverHttp != enableDnsOverHttp) + { + _dnsWebService.DnsServer.EnableDnsOverHttp = enableDnsOverHttp; + restartDnsService = true; + } + } + + if (request.TryGetQueryOrForm("enableDnsOverTls", bool.Parse, out bool enableDnsOverTls)) + { + if (_dnsWebService.DnsServer.EnableDnsOverTls != enableDnsOverTls) + { + _dnsWebService.DnsServer.EnableDnsOverTls = enableDnsOverTls; + restartDnsService = true; + } + } + + if (request.TryGetQueryOrForm("enableDnsOverHttps", bool.Parse, out bool enableDnsOverHttps)) + { + if (_dnsWebService.DnsServer.EnableDnsOverHttps != enableDnsOverHttps) + { + _dnsWebService.DnsServer.EnableDnsOverHttps = enableDnsOverHttps; + restartDnsService = true; + } + } + + if (request.TryGetQueryOrForm("enableDnsOverQuic", bool.Parse, out bool enableDnsOverQuic)) + { + if (_dnsWebService.DnsServer.EnableDnsOverQuic != enableDnsOverQuic) + { + if (enableDnsOverQuic) + DnsWebService.ValidateQuicSupport(); + + _dnsWebService.DnsServer.EnableDnsOverQuic = enableDnsOverQuic; + restartDnsService = true; + } + } + + if (request.TryGetQueryOrForm("dnsOverHttpPort", int.Parse, out int dnsOverHttpPort)) + { + if (_dnsWebService.DnsServer.DnsOverHttpPort != dnsOverHttpPort) + { + _dnsWebService.DnsServer.DnsOverHttpPort = dnsOverHttpPort; + restartDnsService = true; + } + } + + if (request.TryGetQueryOrForm("dnsOverTlsPort", int.Parse, out int dnsOverTlsPort)) + { + if (_dnsWebService.DnsServer.DnsOverTlsPort != dnsOverTlsPort) + { + _dnsWebService.DnsServer.DnsOverTlsPort = dnsOverTlsPort; + restartDnsService = true; + } + } + + if (request.TryGetQueryOrForm("dnsOverHttpsPort", int.Parse, out int dnsOverHttpsPort)) + { + if (_dnsWebService.DnsServer.DnsOverHttpsPort != dnsOverHttpsPort) + { + _dnsWebService.DnsServer.DnsOverHttpsPort = dnsOverHttpsPort; + restartDnsService = true; + } + } + + if (request.TryGetQueryOrForm("dnsOverQuicPort", int.Parse, out int dnsOverQuicPort)) + { + if (_dnsWebService.DnsServer.DnsOverQuicPort != dnsOverQuicPort) + { + _dnsWebService.DnsServer.DnsOverQuicPort = dnsOverQuicPort; + restartDnsService = true; + } + } + + string dnsTlsCertificatePath = request.QueryOrForm("dnsTlsCertificatePath"); + if (dnsTlsCertificatePath is not null) + { + if (dnsTlsCertificatePath.Length == 0) + { + if (!string.IsNullOrEmpty(_dnsWebService._dnsTlsCertificatePath) && (_dnsWebService.DnsServer.EnableDnsOverTls || _dnsWebService.DnsServer.EnableDnsOverHttps || _dnsWebService.DnsServer.EnableDnsOverQuic)) + restartDnsService = true; + + _dnsWebService.DnsServer.Certificate = null; + _dnsWebService._dnsTlsCertificatePath = null; + _dnsWebService._dnsTlsCertificatePassword = ""; + } + else + { + string strDnsTlsCertificatePassword = request.QueryOrForm("dnsTlsCertificatePassword"); + + if ((strDnsTlsCertificatePassword is null) || (strDnsTlsCertificatePassword == "************")) + strDnsTlsCertificatePassword = _dnsWebService._dnsTlsCertificatePassword; + + if ((dnsTlsCertificatePath != _dnsWebService._dnsTlsCertificatePath) || (strDnsTlsCertificatePassword != _dnsWebService._dnsTlsCertificatePassword)) + { + _dnsWebService.LoadDnsTlsCertificate(dnsTlsCertificatePath, strDnsTlsCertificatePassword); + + if (string.IsNullOrEmpty(_dnsWebService._dnsTlsCertificatePath) && (_dnsWebService.DnsServer.EnableDnsOverTls || _dnsWebService.DnsServer.EnableDnsOverHttps || _dnsWebService.DnsServer.EnableDnsOverQuic)) + restartDnsService = true; + + _dnsWebService._dnsTlsCertificatePath = dnsTlsCertificatePath; + _dnsWebService._dnsTlsCertificatePassword = strDnsTlsCertificatePassword; + + _dnsWebService.StartTlsCertificateUpdateTimer(); + } + } + } + + //tsig + string strTsigKeys = request.QueryOrForm("tsigKeys"); + if (strTsigKeys is not null) + { + if ((strTsigKeys.Length == 0) || strTsigKeys.Equals("false", StringComparison.OrdinalIgnoreCase)) + { + _dnsWebService.DnsServer.TsigKeys = null; + } + else + { + string[] strTsigKeyParts = strTsigKeys.Split('|'); + Dictionary tsigKeys = new Dictionary(strTsigKeyParts.Length); + + for (int i = 0; i < strTsigKeyParts.Length; i += 3) + { + string keyName = strTsigKeyParts[i + 0].ToLower(); + string sharedSecret = strTsigKeyParts[i + 1]; + string algorithmName = strTsigKeyParts[i + 2]; + + if (sharedSecret.Length == 0) + { + byte[] key = new byte[32]; + _rng.GetBytes(key); + + tsigKeys.Add(keyName, new TsigKey(keyName, Convert.ToBase64String(key), algorithmName)); + } + else + { + tsigKeys.Add(keyName, new TsigKey(keyName, sharedSecret, algorithmName)); + } + } + + _dnsWebService.DnsServer.TsigKeys = tsigKeys; + } + } + + //recursion + if (request.TryGetQueryOrFormEnum("recursion", out DnsServerRecursion recursion)) + _dnsWebService.DnsServer.Recursion = recursion; + + string recursionDeniedNetworks = request.QueryOrForm("recursionDeniedNetworks"); + if (recursionDeniedNetworks is not null) + { + if ((recursionDeniedNetworks.Length == 0) || recursionDeniedNetworks.Equals("false", StringComparison.OrdinalIgnoreCase)) + _dnsWebService.DnsServer.RecursionDeniedNetworks = null; + else + _dnsWebService.DnsServer.RecursionDeniedNetworks = recursionDeniedNetworks.Split(NetworkAddress.Parse, ','); + } + + string recursionAllowedNetworks = request.QueryOrForm("recursionAllowedNetworks"); + if (recursionAllowedNetworks is not null) + { + if ((recursionAllowedNetworks.Length == 0) || recursionAllowedNetworks.Equals("false", StringComparison.OrdinalIgnoreCase)) + _dnsWebService.DnsServer.RecursionAllowedNetworks = null; + else + _dnsWebService.DnsServer.RecursionAllowedNetworks = recursionAllowedNetworks.Split(NetworkAddress.Parse, ','); + } + + if (request.TryGetQueryOrForm("randomizeName", bool.Parse, out bool randomizeName)) + _dnsWebService.DnsServer.RandomizeName = randomizeName; + + if (request.TryGetQueryOrForm("qnameMinimization", bool.Parse, out bool qnameMinimization)) + _dnsWebService.DnsServer.QnameMinimization = qnameMinimization; + + if (request.TryGetQueryOrForm("nsRevalidation", bool.Parse, out bool nsRevalidation)) + _dnsWebService.DnsServer.NsRevalidation = nsRevalidation; + + if (request.TryGetQueryOrForm("resolverRetries", int.Parse, out int resolverRetries)) + _dnsWebService.DnsServer.ResolverRetries = resolverRetries; + + if (request.TryGetQueryOrForm("resolverTimeout", int.Parse, out int resolverTimeout)) + _dnsWebService.DnsServer.ResolverTimeout = resolverTimeout; + + if (request.TryGetQueryOrForm("resolverMaxStackCount", int.Parse, out int resolverMaxStackCount)) + _dnsWebService.DnsServer.ResolverMaxStackCount = resolverMaxStackCount; + + //cache + if (request.TryGetQueryOrForm("saveCache", bool.Parse, out bool saveCache)) + { + if (!saveCache) + _dnsWebService.DnsServer.CacheZoneManager.DeleteCacheZoneFile(); + + _dnsWebService._saveCache = saveCache; + } + + if (request.TryGetQueryOrForm("serveStale", bool.Parse, out bool serveStale)) + _dnsWebService.DnsServer.ServeStale = serveStale; + + if (request.TryGetQueryOrForm("serveStaleTtl", uint.Parse, out uint serveStaleTtl)) + _dnsWebService.DnsServer.CacheZoneManager.ServeStaleTtl = serveStaleTtl; + + if (request.TryGetQueryOrForm("cacheMaximumEntries", long.Parse, out long cacheMaximumEntries)) + _dnsWebService.DnsServer.CacheZoneManager.MaximumEntries = cacheMaximumEntries; + + if (request.TryGetQueryOrForm("cacheMinimumRecordTtl", uint.Parse, out uint cacheMinimumRecordTtl)) + _dnsWebService.DnsServer.CacheZoneManager.MinimumRecordTtl = cacheMinimumRecordTtl; + + if (request.TryGetQueryOrForm("cacheMaximumRecordTtl", uint.Parse, out uint cacheMaximumRecordTtl)) + _dnsWebService.DnsServer.CacheZoneManager.MaximumRecordTtl = cacheMaximumRecordTtl; + + if (request.TryGetQueryOrForm("cacheNegativeRecordTtl", uint.Parse, out uint cacheNegativeRecordTtl)) + _dnsWebService.DnsServer.CacheZoneManager.NegativeRecordTtl = cacheNegativeRecordTtl; + + if (request.TryGetQueryOrForm("cacheFailureRecordTtl", uint.Parse, out uint cacheFailureRecordTtl)) + _dnsWebService.DnsServer.CacheZoneManager.FailureRecordTtl = cacheFailureRecordTtl; + + if (request.TryGetQueryOrForm("cachePrefetchEligibility", int.Parse, out int cachePrefetchEligibility)) + _dnsWebService.DnsServer.CachePrefetchEligibility = cachePrefetchEligibility; + + if (request.TryGetQueryOrForm("cachePrefetchTrigger", int.Parse, out int cachePrefetchTrigger)) + _dnsWebService.DnsServer.CachePrefetchTrigger = cachePrefetchTrigger; + + if (request.TryGetQueryOrForm("cachePrefetchSampleIntervalInMinutes", int.Parse, out int cachePrefetchSampleIntervalInMinutes)) + _dnsWebService.DnsServer.CachePrefetchSampleIntervalInMinutes = cachePrefetchSampleIntervalInMinutes; + + if (request.TryGetQueryOrForm("cachePrefetchSampleEligibilityHitsPerHour", int.Parse, out int cachePrefetchSampleEligibilityHitsPerHour)) + _dnsWebService.DnsServer.CachePrefetchSampleEligibilityHitsPerHour = cachePrefetchSampleEligibilityHitsPerHour; + + //blocking + if (request.TryGetQueryOrForm("enableBlocking", bool.Parse, out bool enableBlocking)) + { + _dnsWebService.DnsServer.EnableBlocking = enableBlocking; + if (_dnsWebService.DnsServer.EnableBlocking) + { + if (_temporaryDisableBlockingTimer is not null) + _temporaryDisableBlockingTimer.Dispose(); + } + } + + if (request.TryGetQueryOrForm("allowTxtBlockingReport", bool.Parse, out bool allowTxtBlockingReport)) + _dnsWebService.DnsServer.AllowTxtBlockingReport = allowTxtBlockingReport; + + if (request.TryGetQueryOrFormEnum("blockingType", out DnsServerBlockingType blockingType)) + _dnsWebService.DnsServer.BlockingType = blockingType; + + string customBlockingAddresses = request.QueryOrForm("customBlockingAddresses"); + if (customBlockingAddresses is not null) + { + if ((customBlockingAddresses.Length == 0) || customBlockingAddresses.Equals("false", StringComparison.OrdinalIgnoreCase)) + { + _dnsWebService.DnsServer.CustomBlockingARecords = null; + _dnsWebService.DnsServer.CustomBlockingAAAARecords = null; + } + else + { + string[] strAddresses = customBlockingAddresses.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + + List dnsARecords = new List(); + List dnsAAAARecords = new List(); + + foreach (string strAddress in strAddresses) + { + if (IPAddress.TryParse(strAddress, out IPAddress customAddress)) + { + switch (customAddress.AddressFamily) + { + case AddressFamily.InterNetwork: + dnsARecords.Add(new DnsARecordData(customAddress)); + break; + + case AddressFamily.InterNetworkV6: + dnsAAAARecords.Add(new DnsAAAARecordData(customAddress)); + break; + } + } + } + + _dnsWebService.DnsServer.CustomBlockingARecords = dnsARecords; + _dnsWebService.DnsServer.CustomBlockingAAAARecords = dnsAAAARecords; + } + } + + string blockListUrls = request.QueryOrForm("blockListUrls"); + if (blockListUrls is not null) + { + if ((blockListUrls.Length == 0) || blockListUrls.Equals("false", StringComparison.OrdinalIgnoreCase)) + { + _dnsWebService.DnsServer.BlockListZoneManager.AllowListUrls.Clear(); + _dnsWebService.DnsServer.BlockListZoneManager.BlockListUrls.Clear(); + _dnsWebService.DnsServer.BlockListZoneManager.Flush(); + } + else + { + string[] blockListUrlList = blockListUrls.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + + if (oldWebServiceHttpPort != _dnsWebService._webServiceHttpPort) + { + for (int i = 0; i < blockListUrlList.Length; i++) + { + if (blockListUrlList[i].Contains("http://localhost:" + oldWebServiceHttpPort + "/blocklist.txt")) + { + blockListUrlList[i] = "http://localhost:" + _dnsWebService._webServiceHttpPort + "/blocklist.txt"; + blockListUrlsUpdated = true; + break; + } + } + } + + if (!blockListUrlsUpdated) + { + if (blockListUrlList.Length != (_dnsWebService.DnsServer.BlockListZoneManager.AllowListUrls.Count + _dnsWebService.DnsServer.BlockListZoneManager.BlockListUrls.Count)) + { + blockListUrlsUpdated = true; + } + else + { + foreach (string strBlockListUrl in blockListUrlList) + { + if (strBlockListUrl.StartsWith("!")) + { + string strAllowListUrl = strBlockListUrl.Substring(1); + + if (!_dnsWebService.DnsServer.BlockListZoneManager.AllowListUrls.Contains(new Uri(strAllowListUrl))) + { + blockListUrlsUpdated = true; + break; + } + } + else + { + if (!_dnsWebService.DnsServer.BlockListZoneManager.BlockListUrls.Contains(new Uri(strBlockListUrl))) + { + blockListUrlsUpdated = true; + break; + } + } + } + } + } + + if (blockListUrlsUpdated) + { + _dnsWebService.DnsServer.BlockListZoneManager.AllowListUrls.Clear(); + _dnsWebService.DnsServer.BlockListZoneManager.BlockListUrls.Clear(); + + foreach (string strBlockListUrl in blockListUrlList) + { + if (strBlockListUrl.StartsWith("!")) + { + Uri allowListUrl = new Uri(strBlockListUrl.Substring(1)); + + if (!_dnsWebService.DnsServer.BlockListZoneManager.AllowListUrls.Contains(allowListUrl)) + _dnsWebService.DnsServer.BlockListZoneManager.AllowListUrls.Add(allowListUrl); + } + else + { + Uri blockListUrl = new Uri(strBlockListUrl); + + if (!_dnsWebService.DnsServer.BlockListZoneManager.BlockListUrls.Contains(blockListUrl)) + _dnsWebService.DnsServer.BlockListZoneManager.BlockListUrls.Add(blockListUrl); + } + } + } + } + } + + if (request.TryGetQueryOrForm("blockListUpdateIntervalHours", int.Parse, out int blockListUpdateIntervalHours)) + { + if ((blockListUpdateIntervalHours < 0) || (blockListUpdateIntervalHours > 168)) + throw new DnsWebServiceException("Parameter `blockListUpdateIntervalHours` must be between 1 hour and 168 hours (7 days) or 0 to disable automatic update."); + + _blockListUpdateIntervalHours = blockListUpdateIntervalHours; + } + + //proxy & forwarders + if (request.TryGetQueryOrFormEnum("proxyType", out NetProxyType proxyType)) + { + if (proxyType == NetProxyType.None) + { + _dnsWebService.DnsServer.Proxy = null; + } + else + { + NetworkCredential credential = null; + + if (request.TryGetQueryOrForm("proxyUsername", out string proxyUsername)) + credential = new NetworkCredential(proxyUsername, request.QueryOrForm("proxyPassword")); + + _dnsWebService.DnsServer.Proxy = NetProxy.CreateProxy(proxyType, request.QueryOrForm("proxyAddress"), int.Parse(request.QueryOrForm("proxyPort")), credential); + + if (request.TryGetQueryOrForm("proxyBypass", out string proxyBypass)) + _dnsWebService.DnsServer.Proxy.BypassList = proxyBypass.Split(delegate (string value) { return new NetProxyBypassItem(value); }, ','); + } + } + + string strForwarders = request.QueryOrForm("forwarders"); + if (strForwarders is not null) + { + if ((strForwarders.Length == 0) || strForwarders.Equals("false", StringComparison.OrdinalIgnoreCase)) + { + _dnsWebService.DnsServer.Forwarders = null; + } + else + { + DnsTransportProtocol forwarderProtocol = request.GetQueryOrFormEnum("forwarderProtocol", DnsTransportProtocol.Udp); + + switch (forwarderProtocol) + { + case DnsTransportProtocol.HttpsJson: + forwarderProtocol = DnsTransportProtocol.Https; + break; + + case DnsTransportProtocol.Quic: + DnsWebService.ValidateQuicSupport(); + break; + } + + _dnsWebService.DnsServer.Forwarders = strForwarders.Split(delegate (string value) + { + NameServerAddress forwarder = NameServerAddress.Parse(value); + + if (forwarder.Protocol != forwarderProtocol) + forwarder = forwarder.ChangeProtocol(forwarderProtocol); + + return forwarder; + }, ','); + } + } + + if (request.TryGetQueryOrForm("forwarderRetries", int.Parse, out int forwarderRetries)) + _dnsWebService.DnsServer.ForwarderRetries = forwarderRetries; + + if (request.TryGetQueryOrForm("forwarderTimeout", int.Parse, out int forwarderTimeout)) + _dnsWebService.DnsServer.ForwarderTimeout = forwarderTimeout; + + if (request.TryGetQueryOrForm("forwarderConcurrency", int.Parse, out int forwarderConcurrency)) + _dnsWebService.DnsServer.ForwarderConcurrency = forwarderConcurrency; + + //logging + if (request.TryGetQueryOrForm("enableLogging", bool.Parse, out bool enableLogging)) + _dnsWebService._log.EnableLogging = enableLogging; + + if (request.TryGetQueryOrForm("logQueries", bool.Parse, out bool logQueries)) + _dnsWebService.DnsServer.QueryLogManager = logQueries ? _dnsWebService._log : null; + + if (request.TryGetQueryOrForm("useLocalTime", bool.Parse, out bool useLocalTime)) + _dnsWebService._log.UseLocalTime = useLocalTime; + + if (request.TryGetQueryOrForm("logFolder", out string logFolder)) + _dnsWebService._log.LogFolder = logFolder; + + if (request.TryGetQueryOrForm("maxLogFileDays", int.Parse, out int maxLogFileDays)) + _dnsWebService._log.MaxLogFileDays = maxLogFileDays; + + if (request.TryGetQueryOrForm("maxStatFileDays", int.Parse, out int maxStatFileDays)) + _dnsWebService.DnsServer.StatsManager.MaxStatFileDays = maxStatFileDays; + + //TLS actions + if ((_dnsWebService._webServiceTlsCertificatePath is null) && (_dnsWebService._dnsTlsCertificatePath is null)) + _dnsWebService.StopTlsCertificateUpdateTimer(); + + _dnsWebService.SelfSignedCertCheck(serverDomainChanged, true); + + if (_dnsWebService._webServiceEnableTls && string.IsNullOrEmpty(_dnsWebService._webServiceTlsCertificatePath) && !_dnsWebService._webServiceUseSelfSignedTlsCertificate) + { + //disable TLS + _dnsWebService._webServiceEnableTls = false; + restartWebService = true; + } + + //blocklist timers + if ((_blockListUpdateIntervalHours > 0) && ((_dnsWebService.DnsServer.BlockListZoneManager.AllowListUrls.Count + _dnsWebService.DnsServer.BlockListZoneManager.BlockListUrls.Count) > 0)) + { + if (blockListUrlsUpdated || (_blockListUpdateTimer is null)) + ForceUpdateBlockLists(); + + StartBlockListUpdateTimer(); + } + else + { + StopBlockListUpdateTimer(); + } + + //save config + _dnsWebService.SaveConfigFile(); + _dnsWebService._log.Save(); + + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] DNS Settings were updated successfully."); + + Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); + WriteDnsSettings(jsonWriter); + + RestartService(restartDnsService, restartWebService); + } + + public void GetTsigKeyNames(HttpContext context) + { + UserSession session = context.GetCurrentSession(); + + if ( + !_dnsWebService._authManager.IsPermitted(PermissionSection.Settings, session.User, PermissionFlag.View) && + !_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, session.User, PermissionFlag.Modify) + ) + { + throw new DnsWebServiceException("Access was denied."); + } + + Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); + + jsonWriter.WritePropertyName("tsigKeyNames"); + { + jsonWriter.WriteStartArray(); + + if (_dnsWebService.DnsServer.TsigKeys is not null) + { + foreach (KeyValuePair tsigKey in _dnsWebService.DnsServer.TsigKeys) + jsonWriter.WriteStringValue(tsigKey.Key); + } + + jsonWriter.WriteEndArray(); + } + } + + public async Task BackupSettingsAsync(HttpContext context) + { + UserSession session = context.GetCurrentSession(); + + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Settings, session.User, PermissionFlag.Delete)) + throw new DnsWebServiceException("Access was denied."); + + HttpRequest request = context.Request; + + bool blockLists = request.GetQueryOrForm("blockLists", bool.Parse, false); + bool logs = request.GetQueryOrForm("logs", bool.Parse, false); + bool scopes = request.GetQueryOrForm("scopes", bool.Parse, false); + bool apps = request.GetQueryOrForm("apps", bool.Parse, false); + bool stats = request.GetQueryOrForm("stats", bool.Parse, false); + bool zones = request.GetQueryOrForm("zones", bool.Parse, false); + bool allowedZones = request.GetQueryOrForm("allowedZones", bool.Parse, false); + bool blockedZones = request.GetQueryOrForm("blockedZones", bool.Parse, false); + bool dnsSettings = request.GetQueryOrForm("dnsSettings", bool.Parse, false); + bool authConfig = request.GetQueryOrForm("authConfig", bool.Parse, false); + bool logSettings = request.GetQueryOrForm("logSettings", bool.Parse, false); + + string tmpFile = Path.GetTempFileName(); + try + { + using (FileStream backupZipStream = new FileStream(tmpFile, FileMode.Create, FileAccess.ReadWrite)) + { + //create backup zip + using (ZipArchive backupZip = new ZipArchive(backupZipStream, ZipArchiveMode.Create, true, Encoding.UTF8)) + { + if (blockLists) + { + string[] blockListFiles = Directory.GetFiles(Path.Combine(_dnsWebService._configFolder, "blocklists"), "*", SearchOption.TopDirectoryOnly); + foreach (string blockListFile in blockListFiles) + { + string entryName = "blocklists/" + Path.GetFileName(blockListFile); + backupZip.CreateEntryFromFile(blockListFile, entryName); + } + } + + if (logs) + { + string[] logFiles = Directory.GetFiles(_dnsWebService._log.LogFolderAbsolutePath, "*.log", SearchOption.TopDirectoryOnly); + foreach (string logFile in logFiles) + { + string entryName = "logs/" + Path.GetFileName(logFile); + + if (logFile.Equals(_dnsWebService._log.CurrentLogFile, StringComparison.OrdinalIgnoreCase)) + { + await CreateBackupEntryFromFileAsync(backupZip, logFile, entryName); + } + else + { + backupZip.CreateEntryFromFile(logFile, entryName); + } + } + } + + if (scopes) + { + string[] scopeFiles = Directory.GetFiles(Path.Combine(_dnsWebService._configFolder, "scopes"), "*.scope", SearchOption.TopDirectoryOnly); + foreach (string scopeFile in scopeFiles) + { + string entryName = "scopes/" + Path.GetFileName(scopeFile); + backupZip.CreateEntryFromFile(scopeFile, entryName); + } + } + + if (apps) + { + string[] appFiles = Directory.GetFiles(Path.Combine(_dnsWebService._configFolder, "apps"), "*", SearchOption.AllDirectories); + foreach (string appFile in appFiles) + { + string entryName = appFile.Substring(_dnsWebService._configFolder.Length); + + if (Path.DirectorySeparatorChar != '/') + entryName = entryName.Replace(Path.DirectorySeparatorChar, '/'); + + entryName = entryName.TrimStart('/'); + + await CreateBackupEntryFromFileAsync(backupZip, appFile, entryName); + } + } + + if (stats) + { + string[] hourlyStatsFiles = Directory.GetFiles(Path.Combine(_dnsWebService._configFolder, "stats"), "*.stat", SearchOption.TopDirectoryOnly); + foreach (string hourlyStatsFile in hourlyStatsFiles) + { + string entryName = "stats/" + Path.GetFileName(hourlyStatsFile); + backupZip.CreateEntryFromFile(hourlyStatsFile, entryName); + } + + string[] dailyStatsFiles = Directory.GetFiles(Path.Combine(_dnsWebService._configFolder, "stats"), "*.dstat", SearchOption.TopDirectoryOnly); + foreach (string dailyStatsFile in dailyStatsFiles) + { + string entryName = "stats/" + Path.GetFileName(dailyStatsFile); + backupZip.CreateEntryFromFile(dailyStatsFile, entryName); + } + } + + if (zones) + { + string[] zoneFiles = Directory.GetFiles(Path.Combine(_dnsWebService._configFolder, "zones"), "*.zone", SearchOption.TopDirectoryOnly); + foreach (string zoneFile in zoneFiles) + { + string entryName = "zones/" + Path.GetFileName(zoneFile); + backupZip.CreateEntryFromFile(zoneFile, entryName); + } + } + + if (allowedZones) + { + string allowedZonesFile = Path.Combine(_dnsWebService._configFolder, "allowed.config"); + + if (File.Exists(allowedZonesFile)) + backupZip.CreateEntryFromFile(allowedZonesFile, "allowed.config"); + } + + if (blockedZones) + { + string blockedZonesFile = Path.Combine(_dnsWebService._configFolder, "blocked.config"); + + if (File.Exists(blockedZonesFile)) + backupZip.CreateEntryFromFile(blockedZonesFile, "blocked.config"); + } + + if (dnsSettings) + { + string dnsSettingsFile = Path.Combine(_dnsWebService._configFolder, "dns.config"); + + if (File.Exists(dnsSettingsFile)) + backupZip.CreateEntryFromFile(dnsSettingsFile, "dns.config"); + } + + if (authConfig) + { + string authSettingsFile = Path.Combine(_dnsWebService._configFolder, "auth.config"); + + if (File.Exists(authSettingsFile)) + backupZip.CreateEntryFromFile(authSettingsFile, "auth.config"); + } + + if (logSettings) + { + string logSettingsFile = Path.Combine(_dnsWebService._configFolder, "log.config"); + + if (File.Exists(logSettingsFile)) + backupZip.CreateEntryFromFile(logSettingsFile, "log.config"); + } + } + + //send zip file + backupZipStream.Position = 0; + + HttpResponse response = context.Response; + + response.ContentType = "application/zip"; + response.ContentLength = backupZipStream.Length; + response.Headers.ContentDisposition = "attachment;filename=DnsServerBackup.zip"; + + using (Stream output = response.Body) + { + await backupZipStream.CopyToAsync(output); + } + } + } + finally + { + try + { + File.Delete(tmpFile); + } + catch (Exception ex) + { + _dnsWebService._log.Write(ex); + } + } + + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] Settings backup zip file was exported."); + } + + public async Task RestoreSettingsAsync(HttpContext context) + { + UserSession session = context.GetCurrentSession(); + + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Settings, session.User, PermissionFlag.Delete)) + throw new DnsWebServiceException("Access was denied."); + + HttpRequest request = context.Request; + + bool blockLists = request.GetQueryOrForm("blockLists", bool.Parse, false); + bool logs = request.GetQueryOrForm("logs", bool.Parse, false); + bool scopes = request.GetQueryOrForm("scopes", bool.Parse, false); + bool apps = request.GetQueryOrForm("apps", bool.Parse, false); + bool stats = request.GetQueryOrForm("stats", bool.Parse, false); + bool zones = request.GetQueryOrForm("zones", bool.Parse, false); + bool allowedZones = request.GetQueryOrForm("allowedZones", bool.Parse, false); + bool blockedZones = request.GetQueryOrForm("blockedZones", bool.Parse, false); + bool dnsSettings = request.GetQueryOrForm("dnsSettings", bool.Parse, false); + bool authConfig = request.GetQueryOrForm("authConfig", bool.Parse, false); + bool logSettings = request.GetQueryOrForm("logSettings", bool.Parse, false); + bool deleteExistingFiles = request.GetQueryOrForm("deleteExistingFiles", bool.Parse, false); + + if (!request.HasFormContentType || (request.Form.Files.Count == 0)) + throw new DnsWebServiceException("DNS backup zip file is missing."); + + //write to temp file + string tmpFile = Path.GetTempFileName(); + try + { + using (FileStream fS = new FileStream(tmpFile, FileMode.Create, FileAccess.ReadWrite)) + { + await request.Form.Files[0].CopyToAsync(fS); + + fS.Position = 0; + using (ZipArchive backupZip = new ZipArchive(fS, ZipArchiveMode.Read, false, Encoding.UTF8)) + { + if (logSettings || logs) + { + //stop logging + _dnsWebService._log.StopLogging(); + } + + try + { + if (logSettings) + { + ZipArchiveEntry entry = backupZip.GetEntry("log.config"); + if (entry != null) + entry.ExtractToFile(Path.Combine(_dnsWebService._configFolder, entry.Name), true); + + //reload config + _dnsWebService._log.LoadConfig(); + } + + if (logs) + { + if (deleteExistingFiles) + { + //delete existing log files + string[] logFiles = Directory.GetFiles(_dnsWebService._log.LogFolderAbsolutePath, "*.log", SearchOption.TopDirectoryOnly); + foreach (string logFile in logFiles) + { + File.Delete(logFile); + } + } + + //extract log files from backup + foreach (ZipArchiveEntry entry in backupZip.Entries) + { + if (entry.FullName.StartsWith("logs/")) + entry.ExtractToFile(Path.Combine(_dnsWebService._log.LogFolderAbsolutePath, entry.Name), true); + } + } + } + finally + { + if (logSettings || logs) + { + //start logging + if (_dnsWebService._log.EnableLogging) + _dnsWebService._log.StartLogging(); + } + } + + if (authConfig) + { + ZipArchiveEntry entry = backupZip.GetEntry("auth.config"); + if (entry != null) + entry.ExtractToFile(Path.Combine(_dnsWebService._configFolder, entry.Name), true); + + //reload auth config + _dnsWebService._authManager.LoadConfigFile(session); + } + + if (blockLists) + { + if (deleteExistingFiles) + { + //delete existing block list files + string[] blockListFiles = Directory.GetFiles(Path.Combine(_dnsWebService._configFolder, "blocklists"), "*", SearchOption.TopDirectoryOnly); + foreach (string blockListFile in blockListFiles) + { + File.Delete(blockListFile); + } + } + + //extract block list files from backup + foreach (ZipArchiveEntry entry in backupZip.Entries) + { + if (entry.FullName.StartsWith("blocklists/")) + entry.ExtractToFile(Path.Combine(_dnsWebService._configFolder, "blocklists", entry.Name), true); + } + } + + if (dnsSettings) + { + ZipArchiveEntry entry = backupZip.GetEntry("dns.config"); + if (entry != null) + entry.ExtractToFile(Path.Combine(_dnsWebService._configFolder, entry.Name), true); + + //reload settings and block list zone + _dnsWebService.LoadConfigFile(); + + if ((_blockListUpdateIntervalHours > 0) && (_dnsWebService.DnsServer.BlockListZoneManager.BlockListUrls.Count > 0)) + { + ThreadPool.QueueUserWorkItem(delegate (object state) + { + try + { + _dnsWebService.DnsServer.BlockListZoneManager.LoadBlockLists(); + StartBlockListUpdateTimer(); + } + catch (Exception ex) + { + _dnsWebService._log.Write(ex); + } + }); + } + else + { + StopBlockListUpdateTimer(); + } + } + + if (apps) + { + //unload apps + _dnsWebService.DnsServer.DnsApplicationManager.UnloadAllApplications(); + + if (deleteExistingFiles) + { + //delete existing apps + string appFolder = Path.Combine(_dnsWebService._configFolder, "apps"); + if (Directory.Exists(appFolder)) + Directory.Delete(appFolder, true); + + //create apps folder + Directory.CreateDirectory(appFolder); + } + + //extract apps files from backup + foreach (ZipArchiveEntry entry in backupZip.Entries) + { + if (entry.FullName.StartsWith("apps/")) + { + string entryPath = entry.FullName; + + if (Path.DirectorySeparatorChar != '/') + entryPath = entryPath.Replace('/', '\\'); + + string filePath = Path.Combine(_dnsWebService._configFolder, entryPath); + + Directory.CreateDirectory(Path.GetDirectoryName(filePath)); + + entry.ExtractToFile(filePath, true); + } + } + + //reload apps + _dnsWebService.DnsServer.DnsApplicationManager.LoadAllApplications(); + } + + if (zones) + { + if (deleteExistingFiles) + { + //delete existing zone files + string[] zoneFiles = Directory.GetFiles(Path.Combine(_dnsWebService._configFolder, "zones"), "*.zone", SearchOption.TopDirectoryOnly); + foreach (string zoneFile in zoneFiles) + { + File.Delete(zoneFile); + } + } + + //extract zone files from backup + foreach (ZipArchiveEntry entry in backupZip.Entries) + { + if (entry.FullName.StartsWith("zones/")) + entry.ExtractToFile(Path.Combine(_dnsWebService._configFolder, "zones", entry.Name), true); + } + + //reload zones + _dnsWebService.DnsServer.AuthZoneManager.LoadAllZoneFiles(); + _dnsWebService.InspectAndFixZonePermissions(); + } + + if (allowedZones) + { + ZipArchiveEntry entry = backupZip.GetEntry("allowed.config"); + if (entry == null) + { + string fileName = Path.Combine(_dnsWebService._configFolder, "allowed.config"); + if (File.Exists(fileName)) + File.Delete(fileName); + } + else + { + entry.ExtractToFile(Path.Combine(_dnsWebService._configFolder, entry.Name), true); + } + + //reload + _dnsWebService.DnsServer.AllowedZoneManager.LoadAllowedZoneFile(); + } + + if (blockedZones) + { + ZipArchiveEntry entry = backupZip.GetEntry("blocked.config"); + if (entry == null) + { + string fileName = Path.Combine(_dnsWebService._configFolder, "allowed.config"); + if (File.Exists(fileName)) + File.Delete(fileName); + } + else + { + entry.ExtractToFile(Path.Combine(_dnsWebService._configFolder, entry.Name), true); + } + + //reload + _dnsWebService.DnsServer.BlockedZoneManager.LoadBlockedZoneFile(); + } + + if (scopes) + { + //stop dhcp server + _dnsWebService.DhcpServer.Stop(); + + try + { + if (deleteExistingFiles) + { + //delete existing scope files + string[] scopeFiles = Directory.GetFiles(Path.Combine(_dnsWebService._configFolder, "scopes"), "*.scope", SearchOption.TopDirectoryOnly); + foreach (string scopeFile in scopeFiles) + { + File.Delete(scopeFile); + } + } + + //extract scope files from backup + foreach (ZipArchiveEntry entry in backupZip.Entries) + { + if (entry.FullName.StartsWith("scopes/")) + entry.ExtractToFile(Path.Combine(_dnsWebService._configFolder, "scopes", entry.Name), true); + } + } + finally + { + //start dhcp server + _dnsWebService.DhcpServer.Start(); + } + } + + if (stats) + { + if (deleteExistingFiles) + { + //delete existing stats files + string[] hourlyStatsFiles = Directory.GetFiles(Path.Combine(_dnsWebService._configFolder, "stats"), "*.stat", SearchOption.TopDirectoryOnly); + foreach (string hourlyStatsFile in hourlyStatsFiles) + { + File.Delete(hourlyStatsFile); + } + + string[] dailyStatsFiles = Directory.GetFiles(Path.Combine(_dnsWebService._configFolder, "stats"), "*.dstat", SearchOption.TopDirectoryOnly); + foreach (string dailyStatsFile in dailyStatsFiles) + { + File.Delete(dailyStatsFile); + } + } + + //extract stats files from backup + foreach (ZipArchiveEntry entry in backupZip.Entries) + { + if (entry.FullName.StartsWith("stats/")) + entry.ExtractToFile(Path.Combine(_dnsWebService._configFolder, "stats", entry.Name), true); + } + + //reload stats + _dnsWebService.DnsServer.StatsManager.ReloadStats(); + } + + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] Settings backup zip file was restored."); + } + } + } + finally + { + try + { + File.Delete(tmpFile); + } + catch (Exception ex) + { + _dnsWebService._log.Write(ex); + } + } + + if (dnsSettings) + RestartService(true, true); + + Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); + WriteDnsSettings(jsonWriter); + } + + public void ForceUpdateBlockLists(HttpContext context) + { + UserSession session = context.GetCurrentSession(); + + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Settings, session.User, PermissionFlag.Modify)) + throw new DnsWebServiceException("Access was denied."); + + ForceUpdateBlockLists(); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] Block list update was triggered."); + } + + public void TemporaryDisableBlocking(HttpContext context) + { + UserSession session = context.GetCurrentSession(); + + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Settings, session.User, PermissionFlag.Modify)) + throw new DnsWebServiceException("Access was denied."); + + int minutes = context.Request.GetQueryOrForm("minutes", int.Parse); + + Timer temporaryDisableBlockingTimer = _temporaryDisableBlockingTimer; + if (temporaryDisableBlockingTimer is not null) + temporaryDisableBlockingTimer.Dispose(); + + Timer newTemporaryDisableBlockingTimer = new Timer(delegate (object state) + { + try + { + _dnsWebService.DnsServer.EnableBlocking = true; + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] Blocking was enabled after " + minutes + " minute(s) being temporarily disabled."); + } + catch (Exception ex) + { + _dnsWebService._log.Write(ex); + } + }); + + Timer originalTimer = Interlocked.CompareExchange(ref _temporaryDisableBlockingTimer, newTemporaryDisableBlockingTimer, temporaryDisableBlockingTimer); + if (ReferenceEquals(originalTimer, temporaryDisableBlockingTimer)) + { + newTemporaryDisableBlockingTimer.Change(minutes * 60 * 1000, Timeout.Infinite); + _dnsWebService.DnsServer.EnableBlocking = false; + _temporaryDisableBlockingTill = DateTime.UtcNow.AddMinutes(minutes); + + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] Blocking was temporarily disabled for " + minutes + " minute(s)."); + } + else + { + newTemporaryDisableBlockingTimer.Dispose(); + } + + Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); + jsonWriter.WriteString("temporaryDisableBlockingTill", _temporaryDisableBlockingTill); + } + + #endregion + + #region properties + + public DateTime BlockListLastUpdatedOn + { + get { return _blockListLastUpdatedOn; } + set { _blockListLastUpdatedOn = value; } + } + + public int BlockListUpdateIntervalHours + { + get { return _blockListUpdateIntervalHours; } + set { _blockListUpdateIntervalHours = value; } + } + + #endregion + } +} diff --git a/DnsServerCore/WebServiceZonesApi.cs b/DnsServerCore/WebServiceZonesApi.cs index b5500db2..22306c33 100644 --- a/DnsServerCore/WebServiceZonesApi.cs +++ b/DnsServerCore/WebServiceZonesApi.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -21,13 +21,16 @@ using DnsServerCore.Auth; using DnsServerCore.Dns; using DnsServerCore.Dns.Dnssec; using DnsServerCore.Dns.ResourceRecords; +using DnsServerCore.Dns.ZoneManagers; using DnsServerCore.Dns.Zones; -using Newtonsoft.Json; +using Microsoft.AspNetCore.Http; using System; using System.Collections.Generic; using System.IO; using System.Net; +using System.Text.Json; using System.Threading.Tasks; +using TechnitiumLibrary; using TechnitiumLibrary.Net; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; @@ -56,7 +59,7 @@ namespace DnsServerCore #region static - public static void WriteRecordsAsJson(List records, JsonTextWriter jsonWriter, bool authoritativeZoneRecords, AuthZoneInfo zoneInfo = null) + public static void WriteRecordsAsJson(List records, Utf8JsonWriter jsonWriter, bool authoritativeZoneRecords, AuthZoneInfo zoneInfo = null) { if (records is null) { @@ -90,59 +93,47 @@ namespace DnsServerCore #region private - private static void WriteRecordAsJson(DnsResourceRecord record, JsonTextWriter jsonWriter, bool authoritativeZoneRecords, AuthZoneInfo zoneInfo = null) + private static void WriteRecordAsJson(DnsResourceRecord record, Utf8JsonWriter jsonWriter, bool authoritativeZoneRecords, AuthZoneInfo zoneInfo = null) { jsonWriter.WriteStartObject(); - if (authoritativeZoneRecords) - { - jsonWriter.WritePropertyName("disabled"); - jsonWriter.WriteValue(record.IsDisabled()); - } - - jsonWriter.WritePropertyName("name"); - jsonWriter.WriteValue(record.Name); - - jsonWriter.WritePropertyName("type"); - jsonWriter.WriteValue(record.Type.ToString()); - - jsonWriter.WritePropertyName("ttl"); - if (authoritativeZoneRecords) - jsonWriter.WriteValue(record.TtlValue); - else - jsonWriter.WriteValue(record.TTL); + jsonWriter.WriteString("name", record.Name); + jsonWriter.WriteString("type", record.Type.ToString()); if (authoritativeZoneRecords) { - string comments = record.GetComments(); + AuthRecordInfo authRecordInfo = record.GetAuthRecordInfo(); + + jsonWriter.WriteNumber("ttl", record.TTL); + jsonWriter.WriteBoolean("disabled", authRecordInfo.Disabled); + + string comments = authRecordInfo.Comments; if (!string.IsNullOrEmpty(comments)) - { - jsonWriter.WritePropertyName("comments"); - jsonWriter.WriteValue(comments); - } + jsonWriter.WriteString("comments", comments); + } + else + { + if (record.IsStale) + jsonWriter.WriteString("ttl", "0 (0 sec)"); + else + jsonWriter.WriteString("ttl", record.TTL + " (" + WebUtilities.GetFormattedTime((int)record.TTL) + ")"); } jsonWriter.WritePropertyName("rData"); jsonWriter.WriteStartObject(); - DnsResourceRecordInfo recordInfo = record.GetRecordInfo(); - switch (record.Type) { case DnsResourceRecordType.A: { if (record.RDATA is DnsARecordData rdata) { - jsonWriter.WritePropertyName("ipAddress"); - jsonWriter.WriteValue(rdata.IPAddress); + jsonWriter.WriteString("ipAddress", rdata.Address.ToString()); } else { - jsonWriter.WritePropertyName("dataType"); - jsonWriter.WriteValue(record.RDATA.GetType().Name); - - jsonWriter.WritePropertyName("data"); - jsonWriter.WriteValue(record.RDATA.ToString()); + jsonWriter.WriteString("dataType", record.RDATA.GetType().Name); + jsonWriter.WriteString("data", record.RDATA.ToString()); } } break; @@ -151,27 +142,18 @@ namespace DnsServerCore { if (record.RDATA is DnsNSRecordData rdata) { - jsonWriter.WritePropertyName("nameServer"); - jsonWriter.WriteValue(rdata.NameServer.Length == 0 ? "." : rdata.NameServer); + jsonWriter.WriteString("nameServer", rdata.NameServer.Length == 0 ? "." : rdata.NameServer); if (!authoritativeZoneRecords) { if (rdata.IsParentSideTtlSet) - { - int parentSideTtl = (int)rdata.ParentSideTtl; - - jsonWriter.WritePropertyName("parentSideTtl"); - jsonWriter.WriteValue(parentSideTtl + " (" + WebUtilities.GetFormattedTime(parentSideTtl) + ")"); - } + jsonWriter.WriteString("parentSideTtl", rdata.ParentSideTtl + " (" + WebUtilities.GetFormattedTime((int)rdata.ParentSideTtl) + ")"); } } else { - jsonWriter.WritePropertyName("dataType"); - jsonWriter.WriteValue(record.RDATA.GetType().Name); - - jsonWriter.WritePropertyName("data"); - jsonWriter.WriteValue(record.RDATA.ToString()); + jsonWriter.WriteString("dataType", record.RDATA.GetType().Name); + jsonWriter.WriteString("data", record.RDATA.ToString()); } } break; @@ -180,16 +162,12 @@ namespace DnsServerCore { if (record.RDATA is DnsCNAMERecordData rdata) { - jsonWriter.WritePropertyName("cname"); - jsonWriter.WriteValue(rdata.Domain.Length == 0 ? "." : rdata.Domain); + jsonWriter.WriteString("cname", rdata.Domain.Length == 0 ? "." : rdata.Domain); } else { - jsonWriter.WritePropertyName("dataType"); - jsonWriter.WriteValue(record.RDATA.GetType().Name); - - jsonWriter.WritePropertyName("data"); - jsonWriter.WriteValue(record.RDATA.ToString()); + jsonWriter.WriteString("dataType", record.RDATA.GetType().Name); + jsonWriter.WriteString("data", record.RDATA.ToString()); } } break; @@ -198,40 +176,26 @@ namespace DnsServerCore { if (record.RDATA is DnsSOARecordData rdata) { - jsonWriter.WritePropertyName("primaryNameServer"); - jsonWriter.WriteValue(rdata.PrimaryNameServer); - - jsonWriter.WritePropertyName("responsiblePerson"); - jsonWriter.WriteValue(rdata.ResponsiblePerson); - - jsonWriter.WritePropertyName("serial"); - jsonWriter.WriteValue(rdata.Serial); - - jsonWriter.WritePropertyName("refresh"); - jsonWriter.WriteValue(rdata.Refresh); - - jsonWriter.WritePropertyName("retry"); - jsonWriter.WriteValue(rdata.Retry); - - jsonWriter.WritePropertyName("expire"); - jsonWriter.WriteValue(rdata.Expire); - - jsonWriter.WritePropertyName("minimum"); - jsonWriter.WriteValue(rdata.Minimum); + jsonWriter.WriteString("primaryNameServer", rdata.PrimaryNameServer); + jsonWriter.WriteString("responsiblePerson", rdata.ResponsiblePerson); + jsonWriter.WriteNumber("serial", rdata.Serial); + jsonWriter.WriteNumber("refresh", rdata.Refresh); + jsonWriter.WriteNumber("retry", rdata.Retry); + jsonWriter.WriteNumber("expire", rdata.Expire); + jsonWriter.WriteNumber("minimum", rdata.Minimum); } else { - jsonWriter.WritePropertyName("dataType"); - jsonWriter.WriteValue(record.RDATA.GetType().Name); - - jsonWriter.WritePropertyName("data"); - jsonWriter.WriteValue(record.RDATA.ToString()); + jsonWriter.WriteString("dataType", record.RDATA.GetType().Name); + jsonWriter.WriteString("data", record.RDATA.ToString()); } if (authoritativeZoneRecords) { - IReadOnlyList primaryNameServers = record.GetPrimaryNameServers(); - if (primaryNameServers.Count > 0) + AuthRecordInfo authRecordInfo = record.GetAuthRecordInfo(); + + IReadOnlyList primaryNameServers = authRecordInfo.PrimaryNameServers; + if (primaryNameServers is not null) { string primaryAddresses = null; @@ -243,21 +207,14 @@ namespace DnsServerCore primaryAddresses = primaryAddresses + ", " + primaryNameServer.OriginalAddress; } - jsonWriter.WritePropertyName("primaryAddresses"); - jsonWriter.WriteValue(primaryAddresses); + jsonWriter.WriteString("primaryAddresses", primaryAddresses); } - if (recordInfo.ZoneTransferProtocol != DnsTransportProtocol.Udp) - { - jsonWriter.WritePropertyName("zoneTransferProtocol"); - jsonWriter.WriteValue(recordInfo.ZoneTransferProtocol.ToString()); - } + if (authRecordInfo.ZoneTransferProtocol != DnsTransportProtocol.Udp) + jsonWriter.WriteString("zoneTransferProtocol", authRecordInfo.ZoneTransferProtocol.ToString()); - if (!string.IsNullOrEmpty(recordInfo.TsigKeyName)) - { - jsonWriter.WritePropertyName("tsigKeyName"); - jsonWriter.WriteValue(recordInfo.TsigKeyName); - } + if (!string.IsNullOrEmpty(authRecordInfo.TsigKeyName)) + jsonWriter.WriteString("tsigKeyName", authRecordInfo.TsigKeyName); } } break; @@ -266,16 +223,12 @@ namespace DnsServerCore { if (record.RDATA is DnsPTRRecordData rdata) { - jsonWriter.WritePropertyName("ptrName"); - jsonWriter.WriteValue(rdata.Domain.Length == 0 ? "." : rdata.Domain); + jsonWriter.WriteString("ptrName", rdata.Domain.Length == 0 ? "." : rdata.Domain); } else { - jsonWriter.WritePropertyName("dataType"); - jsonWriter.WriteValue(record.RDATA.GetType().Name); - - jsonWriter.WritePropertyName("data"); - jsonWriter.WriteValue(record.RDATA.ToString()); + jsonWriter.WriteString("dataType", record.RDATA.GetType().Name); + jsonWriter.WriteString("data", record.RDATA.ToString()); } } break; @@ -284,20 +237,13 @@ namespace DnsServerCore { if (record.RDATA is DnsMXRecordData rdata) { - jsonWriter.WritePropertyName("preference"); - jsonWriter.WriteValue(rdata.Preference); - - jsonWriter.WritePropertyName("exchange"); - jsonWriter.WriteValue(rdata.Exchange.Length == 0 ? "." : rdata.Exchange); - + jsonWriter.WriteNumber("preference", rdata.Preference); + jsonWriter.WriteString("exchange", rdata.Exchange.Length == 0 ? "." : rdata.Exchange); } else { - jsonWriter.WritePropertyName("dataType"); - jsonWriter.WriteValue(record.RDATA.GetType().Name); - - jsonWriter.WritePropertyName("data"); - jsonWriter.WriteValue(record.RDATA.ToString()); + jsonWriter.WriteString("dataType", record.RDATA.GetType().Name); + jsonWriter.WriteString("data", record.RDATA.ToString()); } } break; @@ -306,16 +252,12 @@ namespace DnsServerCore { if (record.RDATA is DnsTXTRecordData rdata) { - jsonWriter.WritePropertyName("text"); - jsonWriter.WriteValue(rdata.Text); + jsonWriter.WriteString("text", rdata.Text); } else { - jsonWriter.WritePropertyName("dataType"); - jsonWriter.WriteValue(record.RDATA.GetType().Name); - - jsonWriter.WritePropertyName("data"); - jsonWriter.WriteValue(record.RDATA.ToString()); + jsonWriter.WriteString("dataType", record.RDATA.GetType().Name); + jsonWriter.WriteString("data", record.RDATA.ToString()); } } break; @@ -324,16 +266,12 @@ namespace DnsServerCore { if (record.RDATA is DnsAAAARecordData rdata) { - jsonWriter.WritePropertyName("ipAddress"); - jsonWriter.WriteValue(rdata.IPAddress); + jsonWriter.WriteString("ipAddress", rdata.Address.ToString()); } else { - jsonWriter.WritePropertyName("dataType"); - jsonWriter.WriteValue(record.RDATA.GetType().Name); - - jsonWriter.WritePropertyName("data"); - jsonWriter.WriteValue(record.RDATA.ToString()); + jsonWriter.WriteString("dataType", record.RDATA.GetType().Name); + jsonWriter.WriteString("data", record.RDATA.ToString()); } } break; @@ -342,25 +280,15 @@ namespace DnsServerCore { if (record.RDATA is DnsSRVRecordData rdata) { - jsonWriter.WritePropertyName("priority"); - jsonWriter.WriteValue(rdata.Priority); - - jsonWriter.WritePropertyName("weight"); - jsonWriter.WriteValue(rdata.Weight); - - jsonWriter.WritePropertyName("port"); - jsonWriter.WriteValue(rdata.Port); - - jsonWriter.WritePropertyName("target"); - jsonWriter.WriteValue(rdata.Target.Length == 0 ? "." : rdata.Target); + jsonWriter.WriteNumber("priority", rdata.Priority); + jsonWriter.WriteNumber("weight", rdata.Weight); + jsonWriter.WriteNumber("port", rdata.Port); + jsonWriter.WriteString("target", rdata.Target.Length == 0 ? "." : rdata.Target); } else { - jsonWriter.WritePropertyName("dataType"); - jsonWriter.WriteValue(record.RDATA.GetType().Name); - - jsonWriter.WritePropertyName("data"); - jsonWriter.WriteValue(record.RDATA.ToString()); + jsonWriter.WriteString("dataType", record.RDATA.GetType().Name); + jsonWriter.WriteString("data", record.RDATA.ToString()); } } break; @@ -369,16 +297,12 @@ namespace DnsServerCore { if (record.RDATA is DnsDNAMERecordData rdata) { - jsonWriter.WritePropertyName("dname"); - jsonWriter.WriteValue(rdata.Domain.Length == 0 ? "." : rdata.Domain); + jsonWriter.WriteString("dname", rdata.Domain.Length == 0 ? "." : rdata.Domain); } else { - jsonWriter.WritePropertyName("dataType"); - jsonWriter.WriteValue(record.RDATA.GetType().Name); - - jsonWriter.WritePropertyName("data"); - jsonWriter.WriteValue(record.RDATA.ToString()); + jsonWriter.WriteString("dataType", record.RDATA.GetType().Name); + jsonWriter.WriteString("data", record.RDATA.ToString()); } } break; @@ -387,25 +311,15 @@ namespace DnsServerCore { if (record.RDATA is DnsDSRecordData rdata) { - jsonWriter.WritePropertyName("keyTag"); - jsonWriter.WriteValue(rdata.KeyTag); - - jsonWriter.WritePropertyName("algorithm"); - jsonWriter.WriteValue(rdata.Algorithm.ToString()); - - jsonWriter.WritePropertyName("digestType"); - jsonWriter.WriteValue(rdata.DigestType.ToString()); - - jsonWriter.WritePropertyName("digest"); - jsonWriter.WriteValue(rdata.Digest); + jsonWriter.WriteNumber("keyTag", rdata.KeyTag); + jsonWriter.WriteString("algorithm", rdata.Algorithm.ToString()); + jsonWriter.WriteString("digestType", rdata.DigestType.ToString()); + jsonWriter.WriteString("digest", Convert.ToHexString(rdata.Digest)); } else { - jsonWriter.WritePropertyName("dataType"); - jsonWriter.WriteValue(record.RDATA.GetType().Name); - - jsonWriter.WritePropertyName("data"); - jsonWriter.WriteValue(record.RDATA.ToString()); + jsonWriter.WriteString("dataType", record.RDATA.GetType().Name); + jsonWriter.WriteString("data", record.RDATA.ToString()); } } break; @@ -414,22 +328,14 @@ namespace DnsServerCore { if (record.RDATA is DnsSSHFPRecordData rdata) { - jsonWriter.WritePropertyName("algorithm"); - jsonWriter.WriteValue(rdata.Algorithm.ToString()); - - jsonWriter.WritePropertyName("fingerprintType"); - jsonWriter.WriteValue(rdata.FingerprintType.ToString()); - - jsonWriter.WritePropertyName("fingerprint"); - jsonWriter.WriteValue(rdata.Fingerprint); + jsonWriter.WriteString("algorithm", rdata.Algorithm.ToString()); + jsonWriter.WriteString("fingerprintType", rdata.FingerprintType.ToString()); + jsonWriter.WriteString("fingerprint", Convert.ToHexString(rdata.Fingerprint)); } else { - jsonWriter.WritePropertyName("dataType"); - jsonWriter.WriteValue(record.RDATA.GetType().Name); - - jsonWriter.WritePropertyName("data"); - jsonWriter.WriteValue(record.RDATA.ToString()); + jsonWriter.WriteString("dataType", record.RDATA.GetType().Name); + jsonWriter.WriteString("data", record.RDATA.ToString()); } } break; @@ -438,40 +344,20 @@ namespace DnsServerCore { if (record.RDATA is DnsRRSIGRecordData rdata) { - jsonWriter.WritePropertyName("typeCovered"); - jsonWriter.WriteValue(rdata.TypeCovered.ToString()); - - jsonWriter.WritePropertyName("algorithm"); - jsonWriter.WriteValue(rdata.Algorithm.ToString()); - - jsonWriter.WritePropertyName("labels"); - jsonWriter.WriteValue(rdata.Labels); - - jsonWriter.WritePropertyName("originalTtl"); - jsonWriter.WriteValue(rdata.OriginalTtl); - - jsonWriter.WritePropertyName("signatureExpiration"); - jsonWriter.WriteValue(rdata.SignatureExpiration); - - jsonWriter.WritePropertyName("signatureInception"); - jsonWriter.WriteValue(rdata.SignatureInception); - - jsonWriter.WritePropertyName("keyTag"); - jsonWriter.WriteValue(rdata.KeyTag); - - jsonWriter.WritePropertyName("signersName"); - jsonWriter.WriteValue(rdata.SignersName.Length == 0 ? "." : rdata.SignersName); - - jsonWriter.WritePropertyName("signature"); - jsonWriter.WriteValue(Convert.ToBase64String(rdata.Signature)); + jsonWriter.WriteString("typeCovered", rdata.TypeCovered.ToString()); + jsonWriter.WriteString("algorithm", rdata.Algorithm.ToString()); + jsonWriter.WriteNumber("labels", rdata.Labels); + jsonWriter.WriteNumber("originalTtl", rdata.OriginalTtl); + jsonWriter.WriteString("signatureExpiration", DateTime.UnixEpoch.AddSeconds(rdata.SignatureExpiration)); + jsonWriter.WriteString("signatureInception", DateTime.UnixEpoch.AddSeconds(rdata.SignatureInception)); + jsonWriter.WriteNumber("keyTag", rdata.KeyTag); + jsonWriter.WriteString("signersName", rdata.SignersName.Length == 0 ? "." : rdata.SignersName); + jsonWriter.WriteString("signature", Convert.ToBase64String(rdata.Signature)); } else { - jsonWriter.WritePropertyName("dataType"); - jsonWriter.WriteValue(record.RDATA.GetType().Name); - - jsonWriter.WritePropertyName("data"); - jsonWriter.WriteValue(record.RDATA.ToString()); + jsonWriter.WriteString("dataType", record.RDATA.GetType().Name); + jsonWriter.WriteString("data", record.RDATA.ToString()); } } break; @@ -480,24 +366,20 @@ namespace DnsServerCore { if (record.RDATA is DnsNSECRecordData rdata) { - jsonWriter.WritePropertyName("nextDomainName"); - jsonWriter.WriteValue(rdata.NextDomainName); + jsonWriter.WriteString("nextDomainName", rdata.NextDomainName); jsonWriter.WritePropertyName("types"); jsonWriter.WriteStartArray(); foreach (DnsResourceRecordType type in rdata.Types) - jsonWriter.WriteValue(type.ToString()); + jsonWriter.WriteStringValue(type.ToString()); jsonWriter.WriteEndArray(); } else { - jsonWriter.WritePropertyName("dataType"); - jsonWriter.WriteValue(record.RDATA.GetType().Name); - - jsonWriter.WritePropertyName("data"); - jsonWriter.WriteValue(record.RDATA.ToString()); + jsonWriter.WriteString("dataType", record.RDATA.GetType().Name); + jsonWriter.WriteString("data", record.RDATA.ToString()); } } break; @@ -506,38 +388,30 @@ namespace DnsServerCore { if (record.RDATA is DnsDNSKEYRecordData rdata) { - jsonWriter.WritePropertyName("flags"); - jsonWriter.WriteValue(rdata.Flags.ToString()); - - jsonWriter.WritePropertyName("protocol"); - jsonWriter.WriteValue(rdata.Protocol); - - jsonWriter.WritePropertyName("algorithm"); - jsonWriter.WriteValue(rdata.Algorithm.ToString()); - - jsonWriter.WritePropertyName("publicKey"); - jsonWriter.WriteValue(rdata.PublicKey.ToString()); - - jsonWriter.WritePropertyName("computedKeyTag"); - jsonWriter.WriteValue(rdata.ComputedKeyTag); + jsonWriter.WriteString("flags", rdata.Flags.ToString()); + jsonWriter.WriteNumber("protocol", rdata.Protocol); + jsonWriter.WriteString("algorithm", rdata.Algorithm.ToString()); + jsonWriter.WriteString("publicKey", rdata.PublicKey.ToString()); + jsonWriter.WriteNumber("computedKeyTag", rdata.ComputedKeyTag); if (authoritativeZoneRecords) { - if (zoneInfo.Type == AuthZoneType.Primary) + if ((zoneInfo is not null) && (zoneInfo.Type == AuthZoneType.Primary)) { - foreach (DnssecPrivateKey dnssecPrivateKey in zoneInfo.DnssecPrivateKeys) + IReadOnlyCollection dnssecPrivateKeys = zoneInfo.DnssecPrivateKeys; + if (dnssecPrivateKeys is not null) { - if (dnssecPrivateKey.KeyTag == rdata.ComputedKeyTag) + foreach (DnssecPrivateKey dnssecPrivateKey in dnssecPrivateKeys) { - jsonWriter.WritePropertyName("dnsKeyState"); - jsonWriter.WriteValue(dnssecPrivateKey.State.ToString()); - - if ((dnssecPrivateKey.KeyType == DnssecPrivateKeyType.KeySigningKey) && (dnssecPrivateKey.State == DnssecPrivateKeyState.Published)) + if (dnssecPrivateKey.KeyTag == rdata.ComputedKeyTag) { - jsonWriter.WritePropertyName("dnsKeyStateReadyBy"); - jsonWriter.WriteValue((zoneInfo.ApexZone as PrimaryZone).GetDnsKeyStateReadyBy(dnssecPrivateKey)); + jsonWriter.WriteString("dnsKeyState", dnssecPrivateKey.State.ToString()); + + if ((dnssecPrivateKey.KeyType == DnssecPrivateKeyType.KeySigningKey) && (dnssecPrivateKey.State == DnssecPrivateKeyState.Published)) + jsonWriter.WriteString("dnsKeyStateReadyBy", (zoneInfo.ApexZone as PrimaryZone).GetDnsKeyStateReadyBy(dnssecPrivateKey)); + + break; } - break; } } } @@ -550,11 +424,8 @@ namespace DnsServerCore { jsonWriter.WriteStartObject(); - jsonWriter.WritePropertyName("digestType"); - jsonWriter.WriteValue("SHA256"); - - jsonWriter.WritePropertyName("digest"); - jsonWriter.WriteValue(rdata.CreateDS(record.Name, DnssecDigestType.SHA256).Digest); + jsonWriter.WriteString("digestType", "SHA256"); + jsonWriter.WriteString("digest", Convert.ToHexString(rdata.CreateDS(record.Name, DnssecDigestType.SHA256).Digest)); jsonWriter.WriteEndObject(); } @@ -562,11 +433,8 @@ namespace DnsServerCore { jsonWriter.WriteStartObject(); - jsonWriter.WritePropertyName("digestType"); - jsonWriter.WriteValue("SHA384"); - - jsonWriter.WritePropertyName("digest"); - jsonWriter.WriteValue(rdata.CreateDS(record.Name, DnssecDigestType.SHA384).Digest); + jsonWriter.WriteString("digestType", "SHA384"); + jsonWriter.WriteString("digest", Convert.ToHexString(rdata.CreateDS(record.Name, DnssecDigestType.SHA384).Digest)); jsonWriter.WriteEndObject(); } @@ -577,11 +445,8 @@ namespace DnsServerCore } else { - jsonWriter.WritePropertyName("dataType"); - jsonWriter.WriteValue(record.RDATA.GetType().Name); - - jsonWriter.WritePropertyName("data"); - jsonWriter.WriteValue(record.RDATA.ToString()); + jsonWriter.WriteString("dataType", record.RDATA.GetType().Name); + jsonWriter.WriteString("data", record.RDATA.ToString()); } } break; @@ -590,36 +455,24 @@ namespace DnsServerCore { if (record.RDATA is DnsNSEC3RecordData rdata) { - jsonWriter.WritePropertyName("hashAlgorithm"); - jsonWriter.WriteValue(rdata.HashAlgorithm.ToString()); - - jsonWriter.WritePropertyName("flags"); - jsonWriter.WriteValue(rdata.Flags.ToString()); - - jsonWriter.WritePropertyName("iterations"); - jsonWriter.WriteValue(rdata.Iterations); - - jsonWriter.WritePropertyName("salt"); - jsonWriter.WriteValue(rdata.Salt); - - jsonWriter.WritePropertyName("nextHashedOwnerName"); - jsonWriter.WriteValue(rdata.NextHashedOwnerName); + jsonWriter.WriteString("hashAlgorithm", rdata.HashAlgorithm.ToString()); + jsonWriter.WriteString("flags", rdata.Flags.ToString()); + jsonWriter.WriteNumber("iterations", rdata.Iterations); + jsonWriter.WriteString("salt", Convert.ToHexString(rdata.Salt)); + jsonWriter.WriteString("nextHashedOwnerName", rdata.NextHashedOwnerName); jsonWriter.WritePropertyName("types"); jsonWriter.WriteStartArray(); foreach (DnsResourceRecordType type in rdata.Types) - jsonWriter.WriteValue(type.ToString()); + jsonWriter.WriteStringValue(type.ToString()); jsonWriter.WriteEndArray(); } else { - jsonWriter.WritePropertyName("dataType"); - jsonWriter.WriteValue(record.RDATA.GetType().Name); - - jsonWriter.WritePropertyName("data"); - jsonWriter.WriteValue(record.RDATA.ToString()); + jsonWriter.WriteString("dataType", record.RDATA.GetType().Name); + jsonWriter.WriteString("data", record.RDATA.ToString()); } } break; @@ -628,25 +481,15 @@ namespace DnsServerCore { if (record.RDATA is DnsNSEC3PARAMRecordData rdata) { - jsonWriter.WritePropertyName("hashAlgorithm"); - jsonWriter.WriteValue(rdata.HashAlgorithm.ToString()); - - jsonWriter.WritePropertyName("flags"); - jsonWriter.WriteValue(rdata.Flags.ToString()); - - jsonWriter.WritePropertyName("iterations"); - jsonWriter.WriteValue(rdata.Iterations); - - jsonWriter.WritePropertyName("salt"); - jsonWriter.WriteValue(rdata.Salt); + jsonWriter.WriteString("hashAlgorithm", rdata.HashAlgorithm.ToString()); + jsonWriter.WriteString("flags", rdata.Flags.ToString()); + jsonWriter.WriteNumber("iterations", rdata.Iterations); + jsonWriter.WriteString("salt", Convert.ToHexString(rdata.Salt)); } else { - jsonWriter.WritePropertyName("dataType"); - jsonWriter.WriteValue(record.RDATA.GetType().Name); - - jsonWriter.WritePropertyName("data"); - jsonWriter.WriteValue(record.RDATA.ToString()); + jsonWriter.WriteString("dataType", record.RDATA.GetType().Name); + jsonWriter.WriteString("data", record.RDATA.ToString()); } } break; @@ -655,25 +498,15 @@ namespace DnsServerCore { if (record.RDATA is DnsTLSARecordData rdata) { - jsonWriter.WritePropertyName("certificateUsage"); - jsonWriter.WriteValue(rdata.CertificateUsage.ToString().Replace('_', '-')); - - jsonWriter.WritePropertyName("selector"); - jsonWriter.WriteValue(rdata.Selector.ToString()); - - jsonWriter.WritePropertyName("matchingType"); - jsonWriter.WriteValue(rdata.MatchingType.ToString().Replace('_', '-')); - - jsonWriter.WritePropertyName("certificateAssociationData"); - jsonWriter.WriteValue(rdata.CertificateAssociationData); + jsonWriter.WriteString("certificateUsage", rdata.CertificateUsage.ToString().Replace('_', '-')); + jsonWriter.WriteString("selector", rdata.Selector.ToString()); + jsonWriter.WriteString("matchingType", rdata.MatchingType.ToString().Replace('_', '-')); + jsonWriter.WriteString("certificateAssociationData", Convert.ToHexString(rdata.CertificateAssociationData)); } else { - jsonWriter.WritePropertyName("dataType"); - jsonWriter.WriteValue(record.RDATA.GetType().Name); - - jsonWriter.WritePropertyName("data"); - jsonWriter.WriteValue(record.RDATA.ToString()); + jsonWriter.WriteString("dataType", record.RDATA.GetType().Name); + jsonWriter.WriteString("data", record.RDATA.ToString()); } } break; @@ -682,22 +515,14 @@ namespace DnsServerCore { if (record.RDATA is DnsCAARecordData rdata) { - jsonWriter.WritePropertyName("flags"); - jsonWriter.WriteValue(rdata.Flags); - - jsonWriter.WritePropertyName("tag"); - jsonWriter.WriteValue(rdata.Tag); - - jsonWriter.WritePropertyName("value"); - jsonWriter.WriteValue(rdata.Value); + jsonWriter.WriteNumber("flags", rdata.Flags); + jsonWriter.WriteString("tag", rdata.Tag); + jsonWriter.WriteString("value", rdata.Value); } else { - jsonWriter.WritePropertyName("dataType"); - jsonWriter.WriteValue(record.RDATA.GetType().Name); - - jsonWriter.WritePropertyName("data"); - jsonWriter.WriteValue(record.RDATA.ToString()); + jsonWriter.WriteString("dataType", record.RDATA.GetType().Name); + jsonWriter.WriteString("data", record.RDATA.ToString()); } } break; @@ -706,16 +531,12 @@ namespace DnsServerCore { if (record.RDATA is DnsANAMERecordData rdata) { - jsonWriter.WritePropertyName("aname"); - jsonWriter.WriteValue(rdata.Domain.Length == 0 ? "." : rdata.Domain); + jsonWriter.WriteString("aname", rdata.Domain.Length == 0 ? "." : rdata.Domain); } else { - jsonWriter.WritePropertyName("dataType"); - jsonWriter.WriteValue(record.RDATA.GetType().Name); - - jsonWriter.WritePropertyName("data"); - jsonWriter.WriteValue(record.RDATA.ToString()); + jsonWriter.WriteString("dataType", record.RDATA.GetType().Name); + jsonWriter.WriteString("data", record.RDATA.ToString()); } } break; @@ -724,31 +545,17 @@ namespace DnsServerCore { if (record.RDATA is DnsForwarderRecordData rdata) { - jsonWriter.WritePropertyName("protocol"); - jsonWriter.WriteValue(rdata.Protocol.ToString()); - - jsonWriter.WritePropertyName("forwarder"); - jsonWriter.WriteValue(rdata.Forwarder); - - jsonWriter.WritePropertyName("dnssecValidation"); - jsonWriter.WriteValue(rdata.DnssecValidation); - - jsonWriter.WritePropertyName("proxyType"); - jsonWriter.WriteValue(rdata.ProxyType.ToString()); + jsonWriter.WriteString("protocol", rdata.Protocol.ToString()); + jsonWriter.WriteString("forwarder", rdata.Forwarder); + jsonWriter.WriteBoolean("dnssecValidation", rdata.DnssecValidation); + jsonWriter.WriteString("proxyType", rdata.ProxyType.ToString()); if (rdata.ProxyType != NetProxyType.None) { - jsonWriter.WritePropertyName("proxyAddress"); - jsonWriter.WriteValue(rdata.ProxyAddress); - - jsonWriter.WritePropertyName("proxyPort"); - jsonWriter.WriteValue(rdata.ProxyPort); - - jsonWriter.WritePropertyName("proxyUsername"); - jsonWriter.WriteValue(rdata.ProxyUsername); - - jsonWriter.WritePropertyName("proxyPassword"); - jsonWriter.WriteValue(rdata.ProxyPassword); + jsonWriter.WriteString("proxyAddress", rdata.ProxyAddress); + jsonWriter.WriteNumber("proxyPort", rdata.ProxyPort); + jsonWriter.WriteString("proxyUsername", rdata.ProxyUsername); + jsonWriter.WriteString("proxyPassword", rdata.ProxyPassword); } } } @@ -758,14 +565,9 @@ namespace DnsServerCore { if (record.RDATA is DnsApplicationRecordData rdata) { - jsonWriter.WritePropertyName("appName"); - jsonWriter.WriteValue(rdata.AppName); - - jsonWriter.WritePropertyName("classPath"); - jsonWriter.WriteValue(rdata.ClassPath); - - jsonWriter.WritePropertyName("data"); - jsonWriter.WriteValue(rdata.Data); + jsonWriter.WriteString("appName", rdata.AppName); + jsonWriter.WriteString("classPath", rdata.ClassPath); + jsonWriter.WriteString("data", rdata.Data); } } break; @@ -774,22 +576,17 @@ namespace DnsServerCore { if (record.RDATA is DnsUnknownRecordData) { - jsonWriter.WritePropertyName("value"); - using (MemoryStream mS = new MemoryStream()) { record.RDATA.WriteTo(mS); - jsonWriter.WriteValue(Convert.ToBase64String(mS.ToArray())); + jsonWriter.WriteString("value", Convert.ToBase64String(mS.ToArray())); } } else { - jsonWriter.WritePropertyName("dataType"); - jsonWriter.WriteValue(record.RDATA.GetType().Name); - - jsonWriter.WritePropertyName("data"); - jsonWriter.WriteValue(record.RDATA.ToString()); + jsonWriter.WriteString("dataType", record.RDATA.GetType().Name); + jsonWriter.WriteString("data", record.RDATA.ToString()); } } break; @@ -797,119 +594,119 @@ namespace DnsServerCore jsonWriter.WriteEndObject(); - IReadOnlyList glueRecords = recordInfo.GlueRecords; - if (glueRecords is not null) - { - string glue = null; + jsonWriter.WriteString("dnssecStatus", record.DnssecStatus.ToString()); - foreach (DnsResourceRecord glueRecord in glueRecords) + if (authoritativeZoneRecords) + { + AuthRecordInfo authRecordInfo = record.GetAuthRecordInfo(); + + IReadOnlyList glueRecords = authRecordInfo.GlueRecords; + if (glueRecords is not null) { - if (glue == null) - glue = glueRecord.RDATA.ToString(); - else - glue = glue + ", " + glueRecord.RDATA.ToString(); + string glue = null; + + foreach (DnsResourceRecord glueRecord in glueRecords) + { + if (glue == null) + glue = glueRecord.RDATA.ToString(); + else + glue = glue + ", " + glueRecord.RDATA.ToString(); + } + + jsonWriter.WriteString("glueRecords", glue); } - jsonWriter.WritePropertyName("glueRecords"); - jsonWriter.WriteValue(glue); + jsonWriter.WriteString("lastUsedOn", authRecordInfo.LastUsedOn); } - - IReadOnlyList rrsigRecords = recordInfo.RRSIGRecords; - IReadOnlyList nsecRecords = recordInfo.NSECRecords; - - if ((rrsigRecords is not null) || (nsecRecords is not null)) + else { - jsonWriter.WritePropertyName("dnssecRecords"); - jsonWriter.WriteStartArray(); + CacheRecordInfo cacheRecordInfo = record.GetCacheRecordInfo(); - if (rrsigRecords is not null) + IReadOnlyList glueRecords = cacheRecordInfo.GlueRecords; + if (glueRecords is not null) { - foreach (DnsResourceRecord rrsigRecord in rrsigRecords) - jsonWriter.WriteValue(rrsigRecord.ToString()); + string glue = null; + + foreach (DnsResourceRecord glueRecord in glueRecords) + { + if (glue == null) + glue = glueRecord.RDATA.ToString(); + else + glue = glue + ", " + glueRecord.RDATA.ToString(); + } + + jsonWriter.WriteString("glueRecords", glue); } - if (nsecRecords is not null) + IReadOnlyList rrsigRecords = cacheRecordInfo.RRSIGRecords; + IReadOnlyList nsecRecords = cacheRecordInfo.NSECRecords; + + if ((rrsigRecords is not null) || (nsecRecords is not null)) { - foreach (DnsResourceRecord nsecRecord in nsecRecords) - jsonWriter.WriteValue(nsecRecord.ToString()); + jsonWriter.WritePropertyName("dnssecRecords"); + jsonWriter.WriteStartArray(); + + if (rrsigRecords is not null) + { + foreach (DnsResourceRecord rrsigRecord in rrsigRecords) + jsonWriter.WriteStringValue(rrsigRecord.ToString()); + } + + if (nsecRecords is not null) + { + foreach (DnsResourceRecord nsecRecord in nsecRecords) + jsonWriter.WriteStringValue(nsecRecord.ToString()); + } + + jsonWriter.WriteEndArray(); } - jsonWriter.WriteEndArray(); + NetworkAddress eDnsClientSubnet = cacheRecordInfo.EDnsClientSubnet; + if (eDnsClientSubnet is not null) + { + jsonWriter.WriteString("eDnsClientSubnet", eDnsClientSubnet.ToString()); + } + + jsonWriter.WriteString("lastUsedOn", cacheRecordInfo.LastUsedOn); } - jsonWriter.WritePropertyName("dnssecStatus"); - jsonWriter.WriteValue(record.DnssecStatus.ToString()); - - NetworkAddress eDnsClientSubnet = recordInfo.EDnsClientSubnet; - if (eDnsClientSubnet is not null) - { - jsonWriter.WritePropertyName("eDnsClientSubnet"); - jsonWriter.WriteValue(eDnsClientSubnet.ToString()); - } - - jsonWriter.WritePropertyName("lastUsedOn"); - jsonWriter.WriteValue(recordInfo.LastUsedOn); - jsonWriter.WriteEndObject(); } - private static void WriteZoneInfoAsJson(AuthZoneInfo zoneInfo, JsonTextWriter jsonWriter) + private static void WriteZoneInfoAsJson(AuthZoneInfo zoneInfo, Utf8JsonWriter jsonWriter) { jsonWriter.WriteStartObject(); - jsonWriter.WritePropertyName("name"); - jsonWriter.WriteValue(zoneInfo.Name); - - jsonWriter.WritePropertyName("type"); - jsonWriter.WriteValue(zoneInfo.Type.ToString()); + jsonWriter.WriteString("name", zoneInfo.Name); + jsonWriter.WriteString("type", zoneInfo.Type.ToString()); switch (zoneInfo.Type) { case AuthZoneType.Primary: - jsonWriter.WritePropertyName("internal"); - jsonWriter.WriteValue(zoneInfo.Internal); - - jsonWriter.WritePropertyName("dnssecStatus"); - jsonWriter.WriteValue(zoneInfo.DnssecStatus.ToString()); + jsonWriter.WriteBoolean("internal", zoneInfo.Internal); + jsonWriter.WriteString("dnssecStatus", zoneInfo.DnssecStatus.ToString()); if (!zoneInfo.Internal) - { - jsonWriter.WritePropertyName("notifyFailed"); - jsonWriter.WriteValue(zoneInfo.NotifyFailed); - } + jsonWriter.WriteBoolean("notifyFailed", zoneInfo.NotifyFailed); + break; case AuthZoneType.Secondary: - jsonWriter.WritePropertyName("dnssecStatus"); - jsonWriter.WriteValue(zoneInfo.DnssecStatus.ToString()); - - jsonWriter.WritePropertyName("expiry"); - jsonWriter.WriteValue(zoneInfo.Expiry); - - jsonWriter.WritePropertyName("isExpired"); - jsonWriter.WriteValue(zoneInfo.IsExpired); - - jsonWriter.WritePropertyName("notifyFailed"); - jsonWriter.WriteValue(zoneInfo.NotifyFailed); - - jsonWriter.WritePropertyName("syncFailed"); - jsonWriter.WriteValue(zoneInfo.SyncFailed); + jsonWriter.WriteString("dnssecStatus", zoneInfo.DnssecStatus.ToString()); + jsonWriter.WriteString("expiry", zoneInfo.Expiry); + jsonWriter.WriteBoolean("isExpired", zoneInfo.IsExpired); + jsonWriter.WriteBoolean("notifyFailed", zoneInfo.NotifyFailed); + jsonWriter.WriteBoolean("syncFailed", zoneInfo.SyncFailed); break; case AuthZoneType.Stub: - jsonWriter.WritePropertyName("expiry"); - jsonWriter.WriteValue(zoneInfo.Expiry); - - jsonWriter.WritePropertyName("isExpired"); - jsonWriter.WriteValue(zoneInfo.IsExpired); - - jsonWriter.WritePropertyName("syncFailed"); - jsonWriter.WriteValue(zoneInfo.SyncFailed); + jsonWriter.WriteString("expiry", zoneInfo.Expiry); + jsonWriter.WriteBoolean("isExpired", zoneInfo.IsExpired); + jsonWriter.WriteBoolean("syncFailed", zoneInfo.SyncFailed); break; } - jsonWriter.WritePropertyName("disabled"); - jsonWriter.WriteValue(zoneInfo.Disabled); + jsonWriter.WriteBoolean("disabled", zoneInfo.Disabled); jsonWriter.WriteEndObject(); } @@ -918,19 +715,39 @@ namespace DnsServerCore #region public - public void ListZones(HttpListenerRequest request, JsonTextWriter jsonWriter) + public void ListZones(HttpContext context) { - List zones = _dnsWebService.DnsServer.AuthZoneManager.ListZones(); - zones.Sort(); + UserSession session = context.GetCurrentSession(); - UserSession session = _dnsWebService.GetSession(request); + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, session.User, PermissionFlag.View)) + throw new DnsWebServiceException("Access was denied."); + + HttpRequest request = context.Request; + Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); + IReadOnlyList zones; + + if (request.TryGetQueryOrForm("pageNumber", int.Parse, out int pageNumber)) + { + int zonesPerPage = request.GetQueryOrForm("zonesPerPage", int.Parse, 10); + + AuthZoneManager.ZonesPage page = _dnsWebService.DnsServer.AuthZoneManager.GetZonesPage(pageNumber, zonesPerPage); + zones = page.Zones; + + jsonWriter.WriteNumber("pageNumber", page.PageNumber); + jsonWriter.WriteNumber("totalPages", page.TotalPages); + jsonWriter.WriteNumber("totalZones", page.TotalZones); + } + else + { + zones = _dnsWebService.DnsServer.AuthZoneManager.GetAllZones(); + } jsonWriter.WritePropertyName("zones"); jsonWriter.WriteStartArray(); foreach (AuthZoneInfo zone in zones) { - if (!_dnsWebService.AuthManager.IsPermitted(PermissionSection.Zones, zone.Name, session.User, PermissionFlag.View)) + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zone.Name, session.User, PermissionFlag.View)) continue; WriteZoneInfoAsJson(zone, jsonWriter); @@ -939,15 +756,16 @@ namespace DnsServerCore jsonWriter.WriteEndArray(); } - public async Task CreateZoneAsync(HttpListenerRequest request, JsonTextWriter jsonWriter) + public async Task CreateZoneAsync(HttpContext context) { - string zoneName = request.QueryString["zone"]; - if (string.IsNullOrEmpty(zoneName)) - zoneName = request.QueryString["domain"]; + UserSession session = context.GetCurrentSession(); - if (string.IsNullOrEmpty(zoneName)) - throw new DnsWebServiceException("Parameter 'zone' missing."); + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, session.User, PermissionFlag.Modify)) + throw new DnsWebServiceException("Access was denied."); + HttpRequest request = context.Request; + + string zoneName = request.GetQueryOrFormAlt("zone", "domain"); if (zoneName.Contains('*')) throw new DnsWebServiceException("Domain name for a zone cannot contain wildcard character."); @@ -966,11 +784,7 @@ namespace DnsServerCore zoneName = zoneName.Substring(0, zoneName.Length - 1); } - AuthZoneType type = AuthZoneType.Primary; - string strType = request.QueryString["type"]; - if (!string.IsNullOrEmpty(strType)) - type = Enum.Parse(strType, true); - + AuthZoneType type = request.GetQueryOrFormEnum("type", AuthZoneType.Primary); AuthZoneInfo zoneInfo; switch (type) @@ -981,97 +795,66 @@ namespace DnsServerCore if (zoneInfo is null) throw new DnsWebServiceException("Zone already exists: " + zoneName); - UserSession session = _dnsWebService.GetSession(request); - //set permissions - _dnsWebService.AuthManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, session.User, PermissionFlag.ViewModifyDelete); - _dnsWebService.AuthManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _dnsWebService.AuthManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete); - _dnsWebService.AuthManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _dnsWebService.AuthManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.ViewModifyDelete); - _dnsWebService.AuthManager.SaveConfigFile(); + _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, session.User, PermissionFlag.ViewModifyDelete); + _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete); + _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.ViewModifyDelete); + _dnsWebService._authManager.SaveConfigFile(); - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + session.User.Username + "] Authoritative primary zone was created: " + zoneName); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] Authoritative primary zone was created: " + zoneName); _dnsWebService.DnsServer.AuthZoneManager.SaveZoneFile(zoneInfo.Name); } break; case AuthZoneType.Secondary: { - string primaryNameServerAddresses = request.QueryString["primaryNameServerAddresses"]; - if (string.IsNullOrEmpty(primaryNameServerAddresses)) - primaryNameServerAddresses = null; + string primaryNameServerAddresses = request.GetQueryOrForm("primaryNameServerAddresses", null); + DnsTransportProtocol zoneTransferProtocol = request.GetQueryOrFormEnum("zoneTransferProtocol", DnsTransportProtocol.Tcp); + string tsigKeyName = request.GetQueryOrForm("tsigKeyName", null); - DnsTransportProtocol zoneTransferProtocol; - - string strZoneTransferProtocol = request.QueryString["zoneTransferProtocol"]; - if (string.IsNullOrEmpty(strZoneTransferProtocol)) - zoneTransferProtocol = DnsTransportProtocol.Tcp; - else - zoneTransferProtocol = Enum.Parse(strZoneTransferProtocol, true); - - string tsigKeyName = request.QueryString["tsigKeyName"]; - if (string.IsNullOrEmpty(tsigKeyName)) - tsigKeyName = null; + if (zoneTransferProtocol == DnsTransportProtocol.Quic) + DnsWebService.ValidateQuicSupport(); zoneInfo = await _dnsWebService.DnsServer.AuthZoneManager.CreateSecondaryZoneAsync(zoneName, primaryNameServerAddresses, zoneTransferProtocol, tsigKeyName); if (zoneInfo is null) throw new DnsWebServiceException("Zone already exists: " + zoneName); - UserSession session = _dnsWebService.GetSession(request); - //set permissions - _dnsWebService.AuthManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, session.User, PermissionFlag.ViewModifyDelete); - _dnsWebService.AuthManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _dnsWebService.AuthManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete); - _dnsWebService.AuthManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _dnsWebService.AuthManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.ViewModifyDelete); - _dnsWebService.AuthManager.SaveConfigFile(); + _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, session.User, PermissionFlag.ViewModifyDelete); + _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete); + _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.ViewModifyDelete); + _dnsWebService._authManager.SaveConfigFile(); - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + session.User.Username + "] Authoritative secondary zone was created: " + zoneName); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] Authoritative secondary zone was created: " + zoneName); _dnsWebService.DnsServer.AuthZoneManager.SaveZoneFile(zoneInfo.Name); } break; case AuthZoneType.Stub: { - string strPrimaryNameServerAddresses = request.QueryString["primaryNameServerAddresses"]; - if (string.IsNullOrEmpty(strPrimaryNameServerAddresses)) - strPrimaryNameServerAddresses = null; + string primaryNameServerAddresses = request.GetQueryOrForm("primaryNameServerAddresses", null); - zoneInfo = await _dnsWebService.DnsServer.AuthZoneManager.CreateStubZoneAsync(zoneName, strPrimaryNameServerAddresses); + zoneInfo = await _dnsWebService.DnsServer.AuthZoneManager.CreateStubZoneAsync(zoneName, primaryNameServerAddresses); if (zoneInfo is null) throw new DnsWebServiceException("Zone already exists: " + zoneName); - UserSession session = _dnsWebService.GetSession(request); - //set permissions - _dnsWebService.AuthManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, session.User, PermissionFlag.ViewModifyDelete); - _dnsWebService.AuthManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _dnsWebService.AuthManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete); - _dnsWebService.AuthManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _dnsWebService.AuthManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.ViewModifyDelete); - _dnsWebService.AuthManager.SaveConfigFile(); + _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, session.User, PermissionFlag.ViewModifyDelete); + _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete); + _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.ViewModifyDelete); + _dnsWebService._authManager.SaveConfigFile(); - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + session.User.Username + "] Stub zone was created: " + zoneName); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] Stub zone was created: " + zoneName); _dnsWebService.DnsServer.AuthZoneManager.SaveZoneFile(zoneInfo.Name); } break; case AuthZoneType.Forwarder: { - DnsTransportProtocol forwarderProtocol = DnsTransportProtocol.Udp; - string strForwarderProtocol = request.QueryString["protocol"]; - if (!string.IsNullOrEmpty(strForwarderProtocol)) - forwarderProtocol = Enum.Parse(strForwarderProtocol, true); - - string strForwarder = request.QueryString["forwarder"]; - if (string.IsNullOrEmpty(strForwarder)) - throw new DnsWebServiceException("Parameter 'forwarder' missing."); - - bool dnssecValidation = false; - string strDnssecValidation = request.QueryString["dnssecValidation"]; - if (!string.IsNullOrEmpty(strDnssecValidation)) - dnssecValidation = bool.Parse(strDnssecValidation); - - NetProxyType proxyType = NetProxyType.None; - string strProxyType = request.QueryString["proxyType"]; - if (!string.IsNullOrEmpty(strProxyType)) - proxyType = Enum.Parse(strProxyType, true); + DnsTransportProtocol forwarderProtocol = request.GetQueryOrFormEnum("protocol", DnsTransportProtocol.Udp); + string forwarder = request.GetQueryOrForm("forwarder"); + bool dnssecValidation = request.GetQueryOrForm("dnssecValidation", bool.Parse, false); + NetProxyType proxyType = request.GetQueryOrFormEnum("proxyType", NetProxyType.None); string proxyAddress = null; ushort proxyPort = 0; @@ -1080,32 +863,34 @@ namespace DnsServerCore if (proxyType != NetProxyType.None) { - proxyAddress = request.QueryString["proxyAddress"]; - if (string.IsNullOrEmpty(proxyAddress)) - throw new DnsWebServiceException("Parameter 'proxyAddress' missing."); - - string strProxyPort = request.QueryString["proxyPort"]; - if (string.IsNullOrEmpty(strProxyPort)) - throw new DnsWebServiceException("Parameter 'proxyPort' missing."); - - proxyPort = ushort.Parse(strProxyPort); - proxyUsername = request.QueryString["proxyUsername"]; - proxyPassword = request.QueryString["proxyPassword"]; + proxyAddress = request.GetQueryOrForm("proxyAddress"); + proxyPort = request.GetQueryOrForm("proxyPort", ushort.Parse); + proxyUsername = request.QueryOrForm("proxyUsername"); + proxyPassword = request.QueryOrForm("proxyPassword"); } - zoneInfo = _dnsWebService.DnsServer.AuthZoneManager.CreateForwarderZone(zoneName, forwarderProtocol, strForwarder, dnssecValidation, proxyType, proxyAddress, proxyPort, proxyUsername, proxyPassword, null); + switch (forwarderProtocol) + { + case DnsTransportProtocol.HttpsJson: + forwarderProtocol = DnsTransportProtocol.Https; + break; + + case DnsTransportProtocol.Quic: + DnsWebService.ValidateQuicSupport(); + break; + } + + zoneInfo = _dnsWebService.DnsServer.AuthZoneManager.CreateForwarderZone(zoneName, forwarderProtocol, forwarder, dnssecValidation, proxyType, proxyAddress, proxyPort, proxyUsername, proxyPassword, null); if (zoneInfo is null) throw new DnsWebServiceException("Zone already exists: " + zoneName); - UserSession session = _dnsWebService.GetSession(request); - //set permissions - _dnsWebService.AuthManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, session.User, PermissionFlag.ViewModifyDelete); - _dnsWebService.AuthManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _dnsWebService.AuthManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete); - _dnsWebService.AuthManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _dnsWebService.AuthManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.ViewModifyDelete); - _dnsWebService.AuthManager.SaveConfigFile(); + _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, session.User, PermissionFlag.ViewModifyDelete); + _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete); + _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.ViewModifyDelete); + _dnsWebService._authManager.SaveConfigFile(); - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + session.User.Username + "] Forwarder zone was created: " + zoneName); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] Forwarder zone was created: " + zoneName); _dnsWebService.DnsServer.AuthZoneManager.SaveZoneFile(zoneInfo.Name); } break; @@ -1117,43 +902,30 @@ namespace DnsServerCore //delete cache for this zone to allow rebuilding cache data as needed by stub or forwarder zones _dnsWebService.DnsServer.CacheZoneManager.DeleteZone(zoneInfo.Name); - jsonWriter.WritePropertyName("domain"); - jsonWriter.WriteValue(string.IsNullOrEmpty(zoneInfo.Name) ? "." : zoneInfo.Name); + Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); + jsonWriter.WriteString("domain", string.IsNullOrEmpty(zoneInfo.Name) ? "." : zoneInfo.Name); } - public void SignPrimaryZone(HttpListenerRequest request) + public void SignPrimaryZone(HttpContext context) { - string zoneName = request.QueryString["zone"]; - if (string.IsNullOrEmpty(zoneName)) - throw new DnsWebServiceException("Parameter 'zone' missing."); + UserSession session = context.GetCurrentSession(); - zoneName = zoneName.TrimEnd('.'); - - UserSession session = _dnsWebService.GetSession(request); - - if (!_dnsWebService.AuthManager.IsPermitted(PermissionSection.Zones, zoneName, session.User, PermissionFlag.Delete)) + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, session.User, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); - string algorithm = request.QueryString["algorithm"]; - if (string.IsNullOrEmpty(algorithm)) - throw new DnsWebServiceException("Parameter 'algorithm' missing."); + HttpRequest request = context.Request; - uint dnsKeyTtl; - string strDnsKeyTtl = request.QueryString["dnsKeyTtl"]; - if (string.IsNullOrEmpty(strDnsKeyTtl)) - dnsKeyTtl = 24 * 60 * 60; - else - dnsKeyTtl = uint.Parse(strDnsKeyTtl); + string zoneName = request.GetQueryOrForm("zone").TrimEnd('.'); - ushort zskRolloverDays; - string strZskRolloverDays = request.QueryString["zskRolloverDays"]; - if (string.IsNullOrEmpty(strZskRolloverDays)) - zskRolloverDays = 90; - else - zskRolloverDays = ushort.Parse(strZskRolloverDays); + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneName, session.User, PermissionFlag.Delete)) + throw new DnsWebServiceException("Access was denied."); + + string algorithm = request.GetQueryOrForm("algorithm"); + uint dnsKeyTtl = request.GetQueryOrForm("dnsKeyTtl", uint.Parse, 24 * 60 * 60); + ushort zskRolloverDays = request.GetQueryOrForm("zskRolloverDays", ushort.Parse, 90); bool useNSEC3 = false; - string strNxProof = request.QueryString["nxProof"]; + string strNxProof = request.QueryOrForm("nxProof"); if (!string.IsNullOrEmpty(strNxProof)) { switch (strNxProof.ToUpper()) @@ -1176,32 +948,16 @@ namespace DnsServerCore if (useNSEC3) { - string strIterations = request.QueryString["iterations"]; - if (!string.IsNullOrEmpty(strIterations)) - iterations = ushort.Parse(strIterations); - - string strSaltLength = request.QueryString["saltLength"]; - if (!string.IsNullOrEmpty(strSaltLength)) - saltLength = byte.Parse(strSaltLength); + iterations = request.GetQueryOrForm("iterations", ushort.Parse, 0); + saltLength = request.GetQueryOrForm("saltLength", byte.Parse, 0); } switch (algorithm.ToUpper()) { case "RSA": - string hashAlgorithm = request.QueryString["hashAlgorithm"]; - if (string.IsNullOrEmpty(hashAlgorithm)) - throw new DnsWebServiceException("Parameter 'hashAlgorithm' missing."); - - string strKSKKeySize = request.QueryString["kskKeySize"]; - if (string.IsNullOrEmpty(strKSKKeySize)) - throw new DnsWebServiceException("Parameter 'kskKeySize' missing."); - - string strZSKKeySize = request.QueryString["zskKeySize"]; - if (string.IsNullOrEmpty(strZSKKeySize)) - throw new DnsWebServiceException("Parameter 'zskKeySize' missing."); - - int kskKeySize = int.Parse(strKSKKeySize); - int zskKeySize = int.Parse(strZSKKeySize); + string hashAlgorithm = request.GetQueryOrForm("hashAlgorithm"); + int kskKeySize = request.GetQueryOrForm("kskKeySize", int.Parse); + int zskKeySize = request.GetQueryOrForm("zskKeySize", int.Parse); if (useNSEC3) _dnsWebService.DnsServer.AuthZoneManager.SignPrimaryZoneWithRsaNSEC3(zoneName, hashAlgorithm, kskKeySize, zskKeySize, iterations, saltLength, dnsKeyTtl, zskRolloverDays); @@ -1211,9 +967,7 @@ namespace DnsServerCore break; case "ECDSA": - string curve = request.QueryString["curve"]; - if (string.IsNullOrEmpty(curve)) - throw new DnsWebServiceException("Parameter 'curve' missing."); + string curve = request.GetQueryOrForm("curve"); if (useNSEC3) _dnsWebService.DnsServer.AuthZoneManager.SignPrimaryZoneWithEcdsaNSEC3(zoneName, curve, iterations, saltLength, dnsKeyTtl, zskRolloverDays); @@ -1226,42 +980,42 @@ namespace DnsServerCore throw new NotSupportedException("Algorithm is not supported: " + algorithm); } - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + session.User.Username + "] Primary zone was signed successfully: " + zoneName); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] Primary zone was signed successfully: " + zoneName); _dnsWebService.DnsServer.AuthZoneManager.SaveZoneFile(zoneName); } - public void UnsignPrimaryZone(HttpListenerRequest request) + public void UnsignPrimaryZone(HttpContext context) { - string zoneName = request.QueryString["zone"]; - if (string.IsNullOrEmpty(zoneName)) - throw new DnsWebServiceException("Parameter 'zone' missing."); + UserSession session = context.GetCurrentSession(); - zoneName = zoneName.TrimEnd('.'); + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, session.User, PermissionFlag.Modify)) + throw new DnsWebServiceException("Access was denied."); - UserSession session = _dnsWebService.GetSession(request); + string zoneName = context.Request.GetQueryOrForm("zone").TrimEnd('.'); - if (!_dnsWebService.AuthManager.IsPermitted(PermissionSection.Zones, zoneName, session.User, PermissionFlag.Delete)) + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneName, session.User, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); _dnsWebService.DnsServer.AuthZoneManager.UnsignPrimaryZone(zoneName); - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + session.User.Username + "] Primary zone was unsigned successfully: " + zoneName); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] Primary zone was unsigned successfully: " + zoneName); _dnsWebService.DnsServer.AuthZoneManager.SaveZoneFile(zoneName); } - public void GetPrimaryZoneDnssecProperties(HttpListenerRequest request, JsonTextWriter jsonWriter) + public void GetPrimaryZoneDnssecProperties(HttpContext context) { - string zoneName = request.QueryString["zone"]; - if (string.IsNullOrEmpty(zoneName)) - throw new DnsWebServiceException("Parameter 'zone' missing."); + UserSession session = context.GetCurrentSession(); - zoneName = zoneName.TrimEnd('.'); + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, session.User, PermissionFlag.Modify)) + throw new DnsWebServiceException("Access was denied."); + + string zoneName = context.Request.GetQueryOrForm("zone").TrimEnd('.'); AuthZoneInfo zoneInfo = _dnsWebService.DnsServer.AuthZoneManager.GetAuthZoneInfo(zoneName); if (zoneInfo is null) - throw new DnsWebServiceException("No such zone was found: " + zoneName); + throw new DnsWebServiceException("No such authoritative zone was found: " + zoneName); if (zoneInfo.Internal) throw new DnsWebServiceException("Access was denied to manage internal DNS Server zone."); @@ -1269,40 +1023,27 @@ namespace DnsServerCore if (zoneInfo.Type != AuthZoneType.Primary) throw new DnsWebServiceException("The zone must be a primary zone."); - UserSession session = _dnsWebService.GetSession(request); - - if (!_dnsWebService.AuthManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, session.User, PermissionFlag.View)) + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, session.User, PermissionFlag.View)) throw new DnsWebServiceException("Access was denied."); - jsonWriter.WritePropertyName("name"); - jsonWriter.WriteValue(zoneInfo.Name); + Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); - jsonWriter.WritePropertyName("type"); - jsonWriter.WriteValue(zoneInfo.Type.ToString()); - - jsonWriter.WritePropertyName("internal"); - jsonWriter.WriteValue(zoneInfo.Internal); - - jsonWriter.WritePropertyName("disabled"); - jsonWriter.WriteValue(zoneInfo.Disabled); - - jsonWriter.WritePropertyName("dnssecStatus"); - jsonWriter.WriteValue(zoneInfo.DnssecStatus.ToString()); + jsonWriter.WriteString("name", zoneInfo.Name); + jsonWriter.WriteString("type", zoneInfo.Type.ToString()); + jsonWriter.WriteBoolean("internal", zoneInfo.Internal); + jsonWriter.WriteBoolean("disabled", zoneInfo.Disabled); + jsonWriter.WriteString("dnssecStatus", zoneInfo.DnssecStatus.ToString()); if (zoneInfo.DnssecStatus == AuthZoneDnssecStatus.SignedWithNSEC3) { IReadOnlyList nsec3ParamRecords = zoneInfo.GetApexRecords(DnsResourceRecordType.NSEC3PARAM); DnsNSEC3PARAMRecordData nsec3Param = nsec3ParamRecords[0].RDATA as DnsNSEC3PARAMRecordData; - jsonWriter.WritePropertyName("nsec3Iterations"); - jsonWriter.WriteValue(nsec3Param.Iterations); - - jsonWriter.WritePropertyName("nsec3SaltLength"); - jsonWriter.WriteValue(nsec3Param.SaltValue.Length); + jsonWriter.WriteNumber("nsec3Iterations", nsec3Param.Iterations); + jsonWriter.WriteNumber("nsec3SaltLength", nsec3Param.Salt.Length); } - jsonWriter.WritePropertyName("dnsKeyTtl"); - jsonWriter.WriteValue(zoneInfo.DnsKeyTtl); + jsonWriter.WriteNumber("dnsKeyTtl", zoneInfo.DnsKeyTtl); jsonWriter.WritePropertyName("dnssecPrivateKeys"); jsonWriter.WriteStartArray(); @@ -1325,13 +1066,9 @@ namespace DnsServerCore { jsonWriter.WriteStartObject(); - jsonWriter.WritePropertyName("keyTag"); - jsonWriter.WriteValue(dnssecPrivateKey.KeyTag); + jsonWriter.WriteNumber("keyTag", dnssecPrivateKey.KeyTag); + jsonWriter.WriteString("keyType", dnssecPrivateKey.KeyType.ToString()); - jsonWriter.WritePropertyName("keyType"); - jsonWriter.WriteValue(dnssecPrivateKey.KeyType.ToString()); - - jsonWriter.WritePropertyName("algorithm"); switch (dnssecPrivateKey.Algorithm) { case DnssecAlgorithm.RSAMD5: @@ -1339,25 +1076,22 @@ namespace DnsServerCore case DnssecAlgorithm.RSASHA1_NSEC3_SHA1: case DnssecAlgorithm.RSASHA256: case DnssecAlgorithm.RSASHA512: - jsonWriter.WriteValue(dnssecPrivateKey.Algorithm.ToString() + " (" + (dnssecPrivateKey as DnssecRsaPrivateKey).KeySize + " bits)"); + jsonWriter.WriteString("algorithm", dnssecPrivateKey.Algorithm.ToString() + " (" + (dnssecPrivateKey as DnssecRsaPrivateKey).KeySize + " bits)"); break; default: - jsonWriter.WriteValue(dnssecPrivateKey.Algorithm.ToString()); + jsonWriter.WriteString("algorithm", dnssecPrivateKey.Algorithm.ToString()); break; } - jsonWriter.WritePropertyName("state"); - jsonWriter.WriteValue(dnssecPrivateKey.State.ToString()); + jsonWriter.WriteString("state", dnssecPrivateKey.State.ToString()); + jsonWriter.WriteString("stateChangedOn", dnssecPrivateKey.StateChangedOn); - jsonWriter.WritePropertyName("stateChangedOn"); - jsonWriter.WriteValue(dnssecPrivateKey.StateChangedOn); + if ((dnssecPrivateKey.KeyType == DnssecPrivateKeyType.KeySigningKey) && (dnssecPrivateKey.State == DnssecPrivateKeyState.Published)) + jsonWriter.WriteString("stateReadyBy", (zoneInfo.ApexZone as PrimaryZone).GetDnsKeyStateReadyBy(dnssecPrivateKey)); - jsonWriter.WritePropertyName("isRetiring"); - jsonWriter.WriteValue(dnssecPrivateKey.IsRetiring); - - jsonWriter.WritePropertyName("rolloverDays"); - jsonWriter.WriteValue(dnssecPrivateKey.RolloverDays); + jsonWriter.WriteBoolean("isRetiring", dnssecPrivateKey.IsRetiring); + jsonWriter.WriteNumber("rolloverDays", dnssecPrivateKey.RolloverDays); jsonWriter.WriteEndObject(); } @@ -1366,162 +1100,125 @@ namespace DnsServerCore jsonWriter.WriteEndArray(); } - public void ConvertPrimaryZoneToNSEC(HttpListenerRequest request) + public void ConvertPrimaryZoneToNSEC(HttpContext context) { - string zoneName = request.QueryString["zone"]; - if (string.IsNullOrEmpty(zoneName)) - throw new DnsWebServiceException("Parameter 'zone' missing."); + UserSession session = context.GetCurrentSession(); - zoneName = zoneName.TrimEnd('.'); + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, session.User, PermissionFlag.Modify)) + throw new DnsWebServiceException("Access was denied."); - UserSession session = _dnsWebService.GetSession(request); + string zoneName = context.Request.GetQueryOrForm("zone").TrimEnd('.'); - if (!_dnsWebService.AuthManager.IsPermitted(PermissionSection.Zones, zoneName, session.User, PermissionFlag.Delete)) + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneName, session.User, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); _dnsWebService.DnsServer.AuthZoneManager.ConvertPrimaryZoneToNSEC(zoneName); - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + session.User.Username + "] Primary zone was converted to NSEC successfully: " + zoneName); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] Primary zone was converted to NSEC successfully: " + zoneName); _dnsWebService.DnsServer.AuthZoneManager.SaveZoneFile(zoneName); } - public void ConvertPrimaryZoneToNSEC3(HttpListenerRequest request) + public void ConvertPrimaryZoneToNSEC3(HttpContext context) { - string zoneName = request.QueryString["zone"]; - if (string.IsNullOrEmpty(zoneName)) - throw new DnsWebServiceException("Parameter 'zone' missing."); + UserSession session = context.GetCurrentSession(); - zoneName = zoneName.TrimEnd('.'); - - UserSession session = _dnsWebService.GetSession(request); - - if (!_dnsWebService.AuthManager.IsPermitted(PermissionSection.Zones, zoneName, session.User, PermissionFlag.Delete)) + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, session.User, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); - ushort iterations = 0; - string strIterations = request.QueryString["iterations"]; - if (!string.IsNullOrEmpty(strIterations)) - iterations = ushort.Parse(strIterations); + HttpRequest request = context.Request; - byte saltLength = 0; - string strSaltLength = request.QueryString["saltLength"]; - if (!string.IsNullOrEmpty(strSaltLength)) - saltLength = byte.Parse(strSaltLength); + string zoneName = request.GetQueryOrForm("zone").TrimEnd('.'); + + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneName, session.User, PermissionFlag.Delete)) + throw new DnsWebServiceException("Access was denied."); + + ushort iterations = request.GetQueryOrForm("iterations", ushort.Parse, 0); + byte saltLength = request.GetQueryOrForm("saltLength", byte.Parse, 0); _dnsWebService.DnsServer.AuthZoneManager.ConvertPrimaryZoneToNSEC3(zoneName, iterations, saltLength); - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + session.User.Username + "] Primary zone was converted to NSEC3 successfully: " + zoneName); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] Primary zone was converted to NSEC3 successfully: " + zoneName); _dnsWebService.DnsServer.AuthZoneManager.SaveZoneFile(zoneName); } - public void UpdatePrimaryZoneNSEC3Parameters(HttpListenerRequest request) + public void UpdatePrimaryZoneNSEC3Parameters(HttpContext context) { - string zoneName = request.QueryString["zone"]; - if (string.IsNullOrEmpty(zoneName)) - throw new DnsWebServiceException("Parameter 'zone' missing."); + UserSession session = context.GetCurrentSession(); - zoneName = zoneName.TrimEnd('.'); - - UserSession session = _dnsWebService.GetSession(request); - - if (!_dnsWebService.AuthManager.IsPermitted(PermissionSection.Zones, zoneName, session.User, PermissionFlag.Delete)) + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, session.User, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); - ushort iterations = 0; - string strIterations = request.QueryString["iterations"]; - if (!string.IsNullOrEmpty(strIterations)) - iterations = ushort.Parse(strIterations); + HttpRequest request = context.Request; - byte saltLength = 0; - string strSaltLength = request.QueryString["saltLength"]; - if (!string.IsNullOrEmpty(strSaltLength)) - saltLength = byte.Parse(strSaltLength); + string zoneName = request.GetQueryOrForm("zone").TrimEnd('.'); + + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneName, session.User, PermissionFlag.Delete)) + throw new DnsWebServiceException("Access was denied."); + + ushort iterations = request.GetQueryOrForm("iterations", ushort.Parse, 0); + byte saltLength = request.GetQueryOrForm("saltLength", byte.Parse, 0); _dnsWebService.DnsServer.AuthZoneManager.UpdatePrimaryZoneNSEC3Parameters(zoneName, iterations, saltLength); - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + session.User.Username + "] Primary zone NSEC3 parameters were updated successfully: " + zoneName); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] Primary zone NSEC3 parameters were updated successfully: " + zoneName); _dnsWebService.DnsServer.AuthZoneManager.SaveZoneFile(zoneName); } - public void UpdatePrimaryZoneDnssecDnsKeyTtl(HttpListenerRequest request) + public void UpdatePrimaryZoneDnssecDnsKeyTtl(HttpContext context) { - string zoneName = request.QueryString["zone"]; - if (string.IsNullOrEmpty(zoneName)) - throw new DnsWebServiceException("Parameter 'zone' missing."); + UserSession session = context.GetCurrentSession(); - zoneName = zoneName.TrimEnd('.'); - - UserSession session = _dnsWebService.GetSession(request); - - if (!_dnsWebService.AuthManager.IsPermitted(PermissionSection.Zones, zoneName, session.User, PermissionFlag.Delete)) + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, session.User, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); - string strDnsKeyTtl = request.QueryString["ttl"]; - if (string.IsNullOrEmpty(strDnsKeyTtl)) - throw new DnsWebServiceException("Parameter 'ttl' missing."); + HttpRequest request = context.Request; - uint dnsKeyTtl = uint.Parse(strDnsKeyTtl); + string zoneName = request.GetQueryOrForm("zone").TrimEnd('.'); + + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneName, session.User, PermissionFlag.Delete)) + throw new DnsWebServiceException("Access was denied."); + + uint dnsKeyTtl = request.GetQueryOrForm("ttl", uint.Parse); _dnsWebService.DnsServer.AuthZoneManager.UpdatePrimaryZoneDnsKeyTtl(zoneName, dnsKeyTtl); - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + session.User.Username + "] Primary zone DNSKEY TTL was updated successfully: " + zoneName); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] Primary zone DNSKEY TTL was updated successfully: " + zoneName); _dnsWebService.DnsServer.AuthZoneManager.SaveZoneFile(zoneName); } - public void GenerateAndAddPrimaryZoneDnssecPrivateKey(HttpListenerRequest request) + public void GenerateAndAddPrimaryZoneDnssecPrivateKey(HttpContext context) { - string zoneName = request.QueryString["zone"]; - if (string.IsNullOrEmpty(zoneName)) - throw new DnsWebServiceException("Parameter 'zone' missing."); + UserSession session = context.GetCurrentSession(); - zoneName = zoneName.TrimEnd('.'); - - UserSession session = _dnsWebService.GetSession(request); - - if (!_dnsWebService.AuthManager.IsPermitted(PermissionSection.Zones, zoneName, session.User, PermissionFlag.Delete)) + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, session.User, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); - string strKeyType = request.QueryString["keyType"]; - if (string.IsNullOrEmpty(strKeyType)) - throw new DnsWebServiceException("Parameter 'keyType' missing."); + HttpRequest request = context.Request; - DnssecPrivateKeyType keyType = Enum.Parse(strKeyType, true); + string zoneName = request.GetQueryOrForm("zone").TrimEnd('.'); - ushort rolloverDays; - string strRolloverDays = request.QueryString["rolloverDays"]; - if (string.IsNullOrEmpty(strRolloverDays)) - rolloverDays = (ushort)(keyType == DnssecPrivateKeyType.ZoneSigningKey ? 90 : 0); - else - rolloverDays = ushort.Parse(strRolloverDays); + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneName, session.User, PermissionFlag.Delete)) + throw new DnsWebServiceException("Access was denied."); - string algorithm = request.QueryString["algorithm"]; - if (string.IsNullOrEmpty(algorithm)) - throw new DnsWebServiceException("Parameter 'algorithm' missing."); + DnssecPrivateKeyType keyType = request.GetQueryOrFormEnum("keyType"); + ushort rolloverDays = request.GetQueryOrForm("rolloverDays", ushort.Parse, (ushort)(keyType == DnssecPrivateKeyType.ZoneSigningKey ? 90 : 0)); + string algorithm = request.GetQueryOrForm("algorithm"); switch (algorithm.ToUpper()) { case "RSA": - string hashAlgorithm = request.QueryString["hashAlgorithm"]; - if (string.IsNullOrEmpty(hashAlgorithm)) - throw new DnsWebServiceException("Parameter 'hashAlgorithm' missing."); - - string strKeySize = request.QueryString["keySize"]; - if (string.IsNullOrEmpty(strKeySize)) - throw new DnsWebServiceException("Parameter 'keySize' missing."); - - int keySize = int.Parse(strKeySize); + string hashAlgorithm = request.GetQueryOrForm("hashAlgorithm"); + int keySize = request.GetQueryOrForm("keySize", int.Parse); _dnsWebService.DnsServer.AuthZoneManager.GenerateAndAddPrimaryZoneDnssecRsaPrivateKey(zoneName, keyType, hashAlgorithm, keySize, rolloverDays); break; case "ECDSA": - string curve = request.QueryString["curve"]; - if (string.IsNullOrEmpty(curve)) - throw new DnsWebServiceException("Parameter 'curve' missing."); + string curve = request.GetQueryOrForm("curve"); _dnsWebService.DnsServer.AuthZoneManager.GenerateAndAddPrimaryZoneDnssecEcdsaPrivateKey(zoneName, keyType, curve, rolloverDays); break; @@ -1530,200 +1227,174 @@ namespace DnsServerCore throw new NotSupportedException("Algorithm is not supported: " + algorithm); } - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + session.User.Username + "] DNSSEC private key was generated and added to the primary zone successfully: " + zoneName); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] DNSSEC private key was generated and added to the primary zone successfully: " + zoneName); _dnsWebService.DnsServer.AuthZoneManager.SaveZoneFile(zoneName); } - public void UpdatePrimaryZoneDnssecPrivateKey(HttpListenerRequest request) + public void UpdatePrimaryZoneDnssecPrivateKey(HttpContext context) { - string zoneName = request.QueryString["zone"]; - if (string.IsNullOrEmpty(zoneName)) - throw new DnsWebServiceException("Parameter 'zone' missing."); + UserSession session = context.GetCurrentSession(); - zoneName = zoneName.TrimEnd('.'); - - UserSession session = _dnsWebService.GetSession(request); - - if (!_dnsWebService.AuthManager.IsPermitted(PermissionSection.Zones, zoneName, session.User, PermissionFlag.Delete)) + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, session.User, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); - string strKeyTag = request.QueryString["keyTag"]; - if (string.IsNullOrEmpty(strKeyTag)) - throw new DnsWebServiceException("Parameter 'keyTag' missing."); + HttpRequest request = context.Request; - ushort keyTag = ushort.Parse(strKeyTag); + string zoneName = request.GetQueryOrForm("zone").TrimEnd('.'); - string strRolloverDays = request.QueryString["rolloverDays"]; - if (string.IsNullOrEmpty(strRolloverDays)) - throw new DnsWebServiceException("Parameter 'rolloverDays' missing."); + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneName, session.User, PermissionFlag.Delete)) + throw new DnsWebServiceException("Access was denied."); - ushort rolloverDays = ushort.Parse(strRolloverDays); + ushort keyTag = request.GetQueryOrForm("keyTag", ushort.Parse); + ushort rolloverDays = request.GetQueryOrForm("rolloverDays", ushort.Parse); _dnsWebService.DnsServer.AuthZoneManager.UpdatePrimaryZoneDnssecPrivateKey(zoneName, keyTag, rolloverDays); - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + session.User.Username + "] Primary zone DNSSEC private key config was updated successfully: " + zoneName); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] Primary zone DNSSEC private key config was updated successfully: " + zoneName); _dnsWebService.DnsServer.AuthZoneManager.SaveZoneFile(zoneName); } - public void DeletePrimaryZoneDnssecPrivateKey(HttpListenerRequest request) + public void DeletePrimaryZoneDnssecPrivateKey(HttpContext context) { - string zoneName = request.QueryString["zone"]; - if (string.IsNullOrEmpty(zoneName)) - throw new DnsWebServiceException("Parameter 'zone' missing."); + UserSession session = context.GetCurrentSession(); - zoneName = zoneName.TrimEnd('.'); - - UserSession session = _dnsWebService.GetSession(request); - - if (!_dnsWebService.AuthManager.IsPermitted(PermissionSection.Zones, zoneName, session.User, PermissionFlag.Delete)) + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, session.User, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); - string strKeyTag = request.QueryString["keyTag"]; - if (string.IsNullOrEmpty(strKeyTag)) - throw new DnsWebServiceException("Parameter 'keyTag' missing."); + HttpRequest request = context.Request; - ushort keyTag = ushort.Parse(strKeyTag); + string zoneName = request.GetQueryOrForm("zone").TrimEnd('.'); + + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneName, session.User, PermissionFlag.Delete)) + throw new DnsWebServiceException("Access was denied."); + + ushort keyTag = request.GetQueryOrForm("keyTag", ushort.Parse); _dnsWebService.DnsServer.AuthZoneManager.DeletePrimaryZoneDnssecPrivateKey(zoneName, keyTag); - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + session.User.Username + "] DNSSEC private key was deleted from primary zone successfully: " + zoneName); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] DNSSEC private key was deleted from primary zone successfully: " + zoneName); _dnsWebService.DnsServer.AuthZoneManager.SaveZoneFile(zoneName); } - public void PublishAllGeneratedPrimaryZoneDnssecPrivateKeys(HttpListenerRequest request) + public void PublishAllGeneratedPrimaryZoneDnssecPrivateKeys(HttpContext context) { - string zoneName = request.QueryString["zone"]; - if (string.IsNullOrEmpty(zoneName)) - throw new DnsWebServiceException("Parameter 'zone' missing."); + UserSession session = context.GetCurrentSession(); - zoneName = zoneName.TrimEnd('.'); + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, session.User, PermissionFlag.Modify)) + throw new DnsWebServiceException("Access was denied."); - UserSession session = _dnsWebService.GetSession(request); + string zoneName = context.Request.GetQueryOrForm("zone").TrimEnd('.'); - if (!_dnsWebService.AuthManager.IsPermitted(PermissionSection.Zones, zoneName, session.User, PermissionFlag.Delete)) + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneName, session.User, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); _dnsWebService.DnsServer.AuthZoneManager.PublishAllGeneratedPrimaryZoneDnssecPrivateKeys(zoneName); - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + session.User.Username + "] All DNSSEC private keys from the primary zone were published successfully: " + zoneName); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] All DNSSEC private keys from the primary zone were published successfully: " + zoneName); _dnsWebService.DnsServer.AuthZoneManager.SaveZoneFile(zoneName); } - public void RolloverPrimaryZoneDnsKey(HttpListenerRequest request) + public void RolloverPrimaryZoneDnsKey(HttpContext context) { - string zoneName = request.QueryString["zone"]; - if (string.IsNullOrEmpty(zoneName)) - throw new DnsWebServiceException("Parameter 'zone' missing."); + UserSession session = context.GetCurrentSession(); - zoneName = zoneName.TrimEnd('.'); - - UserSession session = _dnsWebService.GetSession(request); - - if (!_dnsWebService.AuthManager.IsPermitted(PermissionSection.Zones, zoneName, session.User, PermissionFlag.Delete)) + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, session.User, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); - string strKeyTag = request.QueryString["keyTag"]; - if (string.IsNullOrEmpty(strKeyTag)) - throw new DnsWebServiceException("Parameter 'keyTag' missing."); + HttpRequest request = context.Request; - ushort keyTag = ushort.Parse(strKeyTag); + string zoneName = request.GetQueryOrForm("zone").TrimEnd('.'); + + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneName, session.User, PermissionFlag.Delete)) + throw new DnsWebServiceException("Access was denied."); + + ushort keyTag = request.GetQueryOrForm("keyTag", ushort.Parse); _dnsWebService.DnsServer.AuthZoneManager.RolloverPrimaryZoneDnsKey(zoneName, keyTag); - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + session.User.Username + "] The DNSKEY (" + keyTag + ") from the primary zone was rolled over successfully: " + zoneName); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] The DNSKEY (" + keyTag + ") from the primary zone was rolled over successfully: " + zoneName); _dnsWebService.DnsServer.AuthZoneManager.SaveZoneFile(zoneName); } - public void RetirePrimaryZoneDnsKey(HttpListenerRequest request) + public void RetirePrimaryZoneDnsKey(HttpContext context) { - string zoneName = request.QueryString["zone"]; - if (string.IsNullOrEmpty(zoneName)) - throw new DnsWebServiceException("Parameter 'zone' missing."); + UserSession session = context.GetCurrentSession(); - zoneName = zoneName.TrimEnd('.'); - - UserSession session = _dnsWebService.GetSession(request); - - if (!_dnsWebService.AuthManager.IsPermitted(PermissionSection.Zones, zoneName, session.User, PermissionFlag.Delete)) + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, session.User, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); - string strKeyTag = request.QueryString["keyTag"]; - if (string.IsNullOrEmpty(strKeyTag)) - throw new DnsWebServiceException("Parameter 'keyTag' missing."); + HttpRequest request = context.Request; - ushort keyTag = ushort.Parse(strKeyTag); + string zoneName = request.GetQueryOrForm("zone").TrimEnd('.'); + + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneName, session.User, PermissionFlag.Delete)) + throw new DnsWebServiceException("Access was denied."); + + ushort keyTag = request.GetQueryOrForm("keyTag", ushort.Parse); _dnsWebService.DnsServer.AuthZoneManager.RetirePrimaryZoneDnsKey(zoneName, keyTag); - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + session.User.Username + "] The DNSKEY (" + keyTag + ") from the primary zone was retired successfully: " + zoneName); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] The DNSKEY (" + keyTag + ") from the primary zone was retired successfully: " + zoneName); _dnsWebService.DnsServer.AuthZoneManager.SaveZoneFile(zoneName); } - public void DeleteZone(HttpListenerRequest request) + public void DeleteZone(HttpContext context) { - string zoneName = request.QueryString["zone"]; - if (string.IsNullOrEmpty(zoneName)) - zoneName = request.QueryString["domain"]; + UserSession session = context.GetCurrentSession(); - if (string.IsNullOrEmpty(zoneName)) - throw new DnsWebServiceException("Parameter 'zone' missing."); + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, session.User, PermissionFlag.Delete)) + throw new DnsWebServiceException("Access was denied."); - zoneName = zoneName.TrimEnd('.'); + string zoneName = context.Request.GetQueryOrFormAlt("zone", "domain").TrimEnd('.'); AuthZoneInfo zoneInfo = _dnsWebService.DnsServer.AuthZoneManager.GetAuthZoneInfo(zoneName); if (zoneInfo is null) - throw new DnsWebServiceException("No such zone was found: " + zoneName); + throw new DnsWebServiceException("No such authoritative zone was found: " + zoneName); if (zoneInfo.Internal) throw new DnsWebServiceException("Access was denied to manage internal DNS Server zone."); - UserSession session = _dnsWebService.GetSession(request); - - if (!_dnsWebService.AuthManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, session.User, PermissionFlag.Delete)) + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, session.User, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); if (!_dnsWebService.DnsServer.AuthZoneManager.DeleteZone(zoneInfo.Name)) - throw new DnsWebServiceException("No authoritative zone was not found for domain: " + zoneInfo.Name); + throw new DnsWebServiceException("Failed to delete the zone: " + zoneInfo.Name); - _dnsWebService.AuthManager.RemoveAllPermissions(PermissionSection.Zones, zoneInfo.Name); - _dnsWebService.AuthManager.SaveConfigFile(); + _dnsWebService._authManager.RemoveAllPermissions(PermissionSection.Zones, zoneInfo.Name); + _dnsWebService._authManager.SaveConfigFile(); - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + session.User.Username + "] " + zoneInfo.Type.ToString() + " zone was deleted: " + zoneName); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] " + zoneInfo.Type.ToString() + " zone was deleted: " + zoneName); _dnsWebService.DnsServer.AuthZoneManager.DeleteZoneFile(zoneInfo.Name); } - public void EnableZone(HttpListenerRequest request) + public void EnableZone(HttpContext context) { - string zoneName = request.QueryString["zone"]; - if (string.IsNullOrEmpty(zoneName)) - zoneName = request.QueryString["domain"]; + UserSession session = context.GetCurrentSession(); - if (string.IsNullOrEmpty(zoneName)) - throw new DnsWebServiceException("Parameter 'zone' missing."); + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, session.User, PermissionFlag.Modify)) + throw new DnsWebServiceException("Access was denied."); - zoneName = zoneName.TrimEnd('.'); + string zoneName = context.Request.GetQueryOrFormAlt("zone", "domain").TrimEnd('.'); AuthZoneInfo zoneInfo = _dnsWebService.DnsServer.AuthZoneManager.GetAuthZoneInfo(zoneName); if (zoneInfo is null) - throw new DnsWebServiceException("No authoritative zone was not found for domain: " + zoneName); + throw new DnsWebServiceException("No such authoritative zone was found: " + zoneName); if (zoneInfo.Internal) throw new DnsWebServiceException("Access was denied to manage internal DNS Server zone."); - UserSession session = _dnsWebService.GetSession(request); - - if (!_dnsWebService.AuthManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, session.User, PermissionFlag.Modify)) + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, session.User, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); zoneInfo.Disabled = false; - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + session.User.Username + "] " + zoneInfo.Type.ToString() + " zone was enabled: " + zoneInfo.Name); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] " + zoneInfo.Type.ToString() + " zone was enabled: " + zoneInfo.Name); _dnsWebService.DnsServer.AuthZoneManager.SaveZoneFile(zoneInfo.Name); @@ -1731,97 +1402,78 @@ namespace DnsServerCore _dnsWebService.DnsServer.CacheZoneManager.DeleteZone(zoneInfo.Name); } - public void DisableZone(HttpListenerRequest request) + public void DisableZone(HttpContext context) { - string zoneName = request.QueryString["zone"]; - if (string.IsNullOrEmpty(zoneName)) - zoneName = request.QueryString["domain"]; + UserSession session = context.GetCurrentSession(); - if (string.IsNullOrEmpty(zoneName)) - throw new DnsWebServiceException("Parameter 'zone' missing."); + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, session.User, PermissionFlag.Modify)) + throw new DnsWebServiceException("Access was denied."); - zoneName = zoneName.TrimEnd('.'); + string zoneName = context.Request.GetQueryOrFormAlt("zone", "domain").TrimEnd('.'); AuthZoneInfo zoneInfo = _dnsWebService.DnsServer.AuthZoneManager.GetAuthZoneInfo(zoneName); if (zoneInfo is null) - throw new DnsWebServiceException("No authoritative zone was not found for domain: " + zoneName); + throw new DnsWebServiceException("No such authoritative zone was found: " + zoneName); if (zoneInfo.Internal) throw new DnsWebServiceException("Access was denied to manage internal DNS Server zone."); - UserSession session = _dnsWebService.GetSession(request); - - if (!_dnsWebService.AuthManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, session.User, PermissionFlag.Modify)) + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, session.User, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); zoneInfo.Disabled = true; - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + session.User.Username + "] " + zoneInfo.Type.ToString() + " zone was disabled: " + zoneInfo.Name); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] " + zoneInfo.Type.ToString() + " zone was disabled: " + zoneInfo.Name); _dnsWebService.DnsServer.AuthZoneManager.SaveZoneFile(zoneInfo.Name); } - public void GetZoneOptions(HttpListenerRequest request, JsonTextWriter jsonWriter) + public void GetZoneOptions(HttpContext context) { - string zoneName = request.QueryString["zone"]; - if (string.IsNullOrEmpty(zoneName)) - zoneName = request.QueryString["domain"]; + UserSession session = context.GetCurrentSession(); - if (string.IsNullOrEmpty(zoneName)) - throw new DnsWebServiceException("Parameter 'zone' missing."); + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, session.User, PermissionFlag.Modify)) + throw new DnsWebServiceException("Access was denied."); - zoneName = zoneName.TrimEnd('.'); + HttpRequest request = context.Request; - bool includeAvailableTsigKeyNames; - string strIncludeAvailableTsigKeyNames = request.QueryString["includeAvailableTsigKeyNames"]; - if (string.IsNullOrEmpty(strIncludeAvailableTsigKeyNames)) - includeAvailableTsigKeyNames = false; - else - includeAvailableTsigKeyNames = bool.Parse(strIncludeAvailableTsigKeyNames); + string zoneName = request.GetQueryOrFormAlt("zone", "domain").TrimEnd('.'); + bool includeAvailableTsigKeyNames = request.GetQueryOrForm("includeAvailableTsigKeyNames", bool.Parse, false); AuthZoneInfo zoneInfo = _dnsWebService.DnsServer.AuthZoneManager.GetAuthZoneInfo(zoneName); if (zoneInfo is null) - throw new DnsWebServiceException("No such zone was found: " + zoneName); + throw new DnsWebServiceException("No such authoritative zone was found: " + zoneName); if (zoneInfo.Internal) throw new DnsWebServiceException("Access was denied to manage internal DNS Server zone."); - UserSession session = _dnsWebService.GetSession(request); - - if (!_dnsWebService.AuthManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, session.User, PermissionFlag.View)) + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, session.User, PermissionFlag.View)) throw new DnsWebServiceException("Access was denied."); - jsonWriter.WritePropertyName("name"); - jsonWriter.WriteValue(zoneInfo.Name); + Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); - jsonWriter.WritePropertyName("type"); - jsonWriter.WriteValue(zoneInfo.Type.ToString()); + jsonWriter.WriteString("name", zoneInfo.Name); + jsonWriter.WriteString("type", zoneInfo.Type.ToString()); switch (zoneInfo.Type) { case AuthZoneType.Primary: - jsonWriter.WritePropertyName("internal"); - jsonWriter.WriteValue(zoneInfo.Internal); - - jsonWriter.WritePropertyName("dnssecStatus"); - jsonWriter.WriteValue(zoneInfo.DnssecStatus.ToString()); + jsonWriter.WriteBoolean("internal", zoneInfo.Internal); + jsonWriter.WriteString("dnssecStatus", zoneInfo.DnssecStatus.ToString()); break; case AuthZoneType.Secondary: - jsonWriter.WritePropertyName("dnssecStatus"); - jsonWriter.WriteValue(zoneInfo.DnssecStatus.ToString()); + jsonWriter.WriteString("dnssecStatus", zoneInfo.DnssecStatus.ToString()); break; } - jsonWriter.WritePropertyName("disabled"); - jsonWriter.WriteValue(zoneInfo.Disabled); + jsonWriter.WriteBoolean("disabled", zoneInfo.Disabled); switch (zoneInfo.Type) { case AuthZoneType.Primary: case AuthZoneType.Secondary: - jsonWriter.WritePropertyName("zoneTransfer"); - jsonWriter.WriteValue(zoneInfo.ZoneTransfer.ToString()); + jsonWriter.WriteString("zoneTransfer", zoneInfo.ZoneTransfer.ToString()); jsonWriter.WritePropertyName("zoneTransferNameServers"); { @@ -1830,7 +1482,7 @@ namespace DnsServerCore if (zoneInfo.ZoneTransferNameServers is not null) { foreach (IPAddress nameServer in zoneInfo.ZoneTransferNameServers) - jsonWriter.WriteValue(nameServer.ToString()); + jsonWriter.WriteStringValue(nameServer.ToString()); } jsonWriter.WriteEndArray(); @@ -1843,14 +1495,13 @@ namespace DnsServerCore if (zoneInfo.ZoneTransferTsigKeyNames is not null) { foreach (KeyValuePair tsigKeyName in zoneInfo.ZoneTransferTsigKeyNames) - jsonWriter.WriteValue(tsigKeyName.Key); + jsonWriter.WriteStringValue(tsigKeyName.Key); } jsonWriter.WriteEndArray(); } - jsonWriter.WritePropertyName("notify"); - jsonWriter.WriteValue(zoneInfo.Notify.ToString()); + jsonWriter.WriteString("notify", zoneInfo.Notify.ToString()); jsonWriter.WritePropertyName("notifyNameServers"); { @@ -1859,7 +1510,7 @@ namespace DnsServerCore if (zoneInfo.NotifyNameServers is not null) { foreach (IPAddress nameServer in zoneInfo.NotifyNameServers) - jsonWriter.WriteValue(nameServer.ToString()); + jsonWriter.WriteStringValue(nameServer.ToString()); } jsonWriter.WriteEndArray(); @@ -1871,8 +1522,7 @@ namespace DnsServerCore switch (zoneInfo.Type) { case AuthZoneType.Primary: - jsonWriter.WritePropertyName("update"); - jsonWriter.WriteValue(zoneInfo.Update.ToString()); + jsonWriter.WriteString("update", zoneInfo.Update.ToString()); jsonWriter.WritePropertyName("updateIpAddresses"); { @@ -1881,7 +1531,7 @@ namespace DnsServerCore if (zoneInfo.UpdateIpAddresses is not null) { foreach (IPAddress updateIpAddress in zoneInfo.UpdateIpAddresses) - jsonWriter.WriteValue(updateIpAddress.ToString()); + jsonWriter.WriteStringValue(updateIpAddress.ToString()); } jsonWriter.WriteEndArray(); @@ -1899,17 +1549,14 @@ namespace DnsServerCore { jsonWriter.WriteStartObject(); - jsonWriter.WritePropertyName("tsigKeyName"); - jsonWriter.WriteValue(updateSecurityPolicy.Key); - - jsonWriter.WritePropertyName("domain"); - jsonWriter.WriteValue(policy.Key); + jsonWriter.WriteString("tsigKeyName", updateSecurityPolicy.Key); + jsonWriter.WriteString("domain", policy.Key); jsonWriter.WritePropertyName("allowedTypes"); jsonWriter.WriteStartArray(); foreach (DnsResourceRecordType allowedType in policy.Value) - jsonWriter.WriteValue(allowedType.ToString()); + jsonWriter.WriteStringValue(allowedType.ToString()); jsonWriter.WriteEndArray(); @@ -1932,7 +1579,7 @@ namespace DnsServerCore if (_dnsWebService.DnsServer.TsigKeys is not null) { foreach (KeyValuePair tsigKey in _dnsWebService.DnsServer.TsigKeys) - jsonWriter.WriteValue(tsigKey.Key); + jsonWriter.WriteStringValue(tsigKey.Key); } jsonWriter.WriteEndArray(); @@ -1940,64 +1587,50 @@ namespace DnsServerCore } } - public void SetZoneOptions(HttpListenerRequest request) + public void SetZoneOptions(HttpContext context) { - string zoneName = request.QueryString["zone"]; - if (string.IsNullOrEmpty(zoneName)) - zoneName = request.QueryString["domain"]; + UserSession session = context.GetCurrentSession(); - if (string.IsNullOrEmpty(zoneName)) - throw new DnsWebServiceException("Parameter 'zone' missing."); + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, session.User, PermissionFlag.Modify)) + throw new DnsWebServiceException("Access was denied."); - zoneName = zoneName.TrimEnd('.'); + HttpRequest request = context.Request; + + string zoneName = request.GetQueryOrFormAlt("zone", "domain").TrimEnd('.'); AuthZoneInfo zoneInfo = _dnsWebService.DnsServer.AuthZoneManager.GetAuthZoneInfo(zoneName); if (zoneInfo is null) - throw new DnsWebServiceException("No authoritative zone was not found for domain: " + zoneName); + throw new DnsWebServiceException("No such authoritative zone was found: " + zoneName); if (zoneInfo.Internal) throw new DnsWebServiceException("Access was denied to manage internal DNS Server zone."); - UserSession session = _dnsWebService.GetSession(request); - - if (!_dnsWebService.AuthManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, session.User, PermissionFlag.Delete)) + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, session.User, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); - string strDisabled = request.QueryString["disabled"]; - if (!string.IsNullOrEmpty(strDisabled)) - zoneInfo.Disabled = bool.Parse(strDisabled); + if (request.TryGetQueryOrForm("disabled", bool.Parse, out bool disabled)) + zoneInfo.Disabled = disabled; switch (zoneInfo.Type) { case AuthZoneType.Primary: case AuthZoneType.Secondary: - string strZoneTransfer = request.QueryString["zoneTransfer"]; - if (!string.IsNullOrEmpty(strZoneTransfer)) - zoneInfo.ZoneTransfer = Enum.Parse(strZoneTransfer, true); + if (request.TryGetQueryOrFormEnum("zoneTransfer", out AuthZoneTransfer zoneTransfer)) + zoneInfo.ZoneTransfer = zoneTransfer; - string strZoneTransferNameServers = request.QueryString["zoneTransferNameServers"]; - if (!string.IsNullOrEmpty(strZoneTransferNameServers)) + string strZoneTransferNameServers = request.QueryOrForm("zoneTransferNameServers"); + if (strZoneTransferNameServers is not null) { - if (strZoneTransferNameServers == "false") - { + if ((strZoneTransferNameServers.Length == 0) || strZoneTransferNameServers.Equals("false", StringComparison.OrdinalIgnoreCase)) zoneInfo.ZoneTransferNameServers = null; - } else - { - string[] strNameServers = strZoneTransferNameServers.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - IPAddress[] nameServers = new IPAddress[strNameServers.Length]; - - for (int i = 0; i < strNameServers.Length; i++) - nameServers[i] = IPAddress.Parse(strNameServers[i]); - - zoneInfo.ZoneTransferNameServers = nameServers; - } + zoneInfo.ZoneTransferNameServers = strZoneTransferNameServers.Split(IPAddress.Parse, ','); } - string strZoneTransferTsigKeyNames = request.QueryString["zoneTransferTsigKeyNames"]; - if (!string.IsNullOrEmpty(strZoneTransferTsigKeyNames)) + string strZoneTransferTsigKeyNames = request.QueryOrForm("zoneTransferTsigKeyNames"); + if (strZoneTransferTsigKeyNames is not null) { - if (strZoneTransferTsigKeyNames == "false") + if ((strZoneTransferTsigKeyNames.Length == 0) || strZoneTransferTsigKeyNames.Equals("false", StringComparison.OrdinalIgnoreCase)) { zoneInfo.ZoneTransferTsigKeyNames = null; } @@ -2013,27 +1646,16 @@ namespace DnsServerCore } } - string strNotify = request.QueryString["notify"]; - if (!string.IsNullOrEmpty(strNotify)) - zoneInfo.Notify = Enum.Parse(strNotify, true); + if (request.TryGetQueryOrFormEnum("notify", out AuthZoneNotify notify)) + zoneInfo.Notify = notify; - string strNotifyNameServers = request.QueryString["notifyNameServers"]; - if (!string.IsNullOrEmpty(strNotifyNameServers)) + string strNotifyNameServers = request.QueryOrForm("notifyNameServers"); + if (strNotifyNameServers is not null) { - if (strNotifyNameServers == "false") - { + if ((strNotifyNameServers.Length == 0) || strNotifyNameServers.Equals("false", StringComparison.OrdinalIgnoreCase)) zoneInfo.NotifyNameServers = null; - } else - { - string[] strNameServers = strNotifyNameServers.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - IPAddress[] nameServers = new IPAddress[strNameServers.Length]; - - for (int i = 0; i < strNameServers.Length; i++) - nameServers[i] = IPAddress.Parse(strNameServers[i]); - - zoneInfo.NotifyNameServers = nameServers; - } + zoneInfo.NotifyNameServers = strNotifyNameServers.Split(IPAddress.Parse, ','); } break; } @@ -2041,33 +1663,22 @@ namespace DnsServerCore switch (zoneInfo.Type) { case AuthZoneType.Primary: - string strUpdate = request.QueryString["update"]; - if (!string.IsNullOrEmpty(strUpdate)) - zoneInfo.Update = Enum.Parse(strUpdate, true); + if (request.TryGetQueryOrFormEnum("update", out AuthZoneUpdate update)) + zoneInfo.Update = update; - string strUpdateIpAddresses = request.QueryString["updateIpAddresses"]; - if (!string.IsNullOrEmpty(strUpdateIpAddresses)) + string strUpdateIpAddresses = request.QueryOrForm("updateIpAddresses"); + if (strUpdateIpAddresses is not null) { - if (strUpdateIpAddresses == "false") - { + if ((strUpdateIpAddresses.Length == 0) || strUpdateIpAddresses.Equals("false", StringComparison.OrdinalIgnoreCase)) zoneInfo.UpdateIpAddresses = null; - } else - { - string[] strIpAddresses = strUpdateIpAddresses.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - IPAddress[] ipAddresses = new IPAddress[strIpAddresses.Length]; - - for (int i = 0; i < strIpAddresses.Length; i++) - ipAddresses[i] = IPAddress.Parse(strIpAddresses[i]); - - zoneInfo.UpdateIpAddresses = ipAddresses; - } + zoneInfo.UpdateIpAddresses = strUpdateIpAddresses.Split(IPAddress.Parse, ','); } - string strUpdateSecurityPolicies = request.QueryString["updateSecurityPolicies"]; - if (!string.IsNullOrEmpty(strUpdateSecurityPolicies)) + string strUpdateSecurityPolicies = request.QueryOrForm("updateSecurityPolicies"); + if (strUpdateSecurityPolicies is not null) { - if (strUpdateSecurityPolicies == "false") + if ((strUpdateSecurityPolicies.Length == 0) || strUpdateSecurityPolicies.Equals("false", StringComparison.OrdinalIgnoreCase)) { zoneInfo.UpdateSecurityPolicies = null; } @@ -2107,32 +1718,28 @@ namespace DnsServerCore break; } - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + session.User.Username + "] " + zoneInfo.Type.ToString() + " zone options were updated successfully: " + zoneInfo.Name); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] " + zoneInfo.Type.ToString() + " zone options were updated successfully: " + zoneInfo.Name); _dnsWebService.DnsServer.AuthZoneManager.SaveZoneFile(zoneInfo.Name); } - public void ResyncZone(HttpListenerRequest request) + public void ResyncZone(HttpContext context) { - string zoneName = request.QueryString["zone"]; - if (string.IsNullOrEmpty(zoneName)) - zoneName = request.QueryString["domain"]; + UserSession session = context.GetCurrentSession(); - if (string.IsNullOrEmpty(zoneName)) - throw new DnsWebServiceException("Parameter 'zone' missing."); + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, session.User, PermissionFlag.Modify)) + throw new DnsWebServiceException("Access was denied."); - zoneName = zoneName.TrimEnd('.'); + string zoneName = context.Request.GetQueryOrFormAlt("zone", "domain").TrimEnd('.'); AuthZoneInfo zoneInfo = _dnsWebService.DnsServer.AuthZoneManager.GetAuthZoneInfo(zoneName); if (zoneInfo is null) - throw new DnsWebServiceException("No authoritative zone was not found for domain: " + zoneName); + throw new DnsWebServiceException("No such authoritative zone was found: " + zoneName); if (zoneInfo.Internal) throw new DnsWebServiceException("Access was denied to manage internal DNS Server zone."); - UserSession session = _dnsWebService.GetSession(request); - - if (!_dnsWebService.AuthManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, session.User, PermissionFlag.Modify)) + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, session.User, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); switch (zoneInfo.Type) @@ -2147,51 +1754,32 @@ namespace DnsServerCore } } - public void AddRecord(HttpListenerRequest request, JsonTextWriter jsonWriter) + public void AddRecord(HttpContext context) { - string domain = request.QueryString["domain"]; - if (string.IsNullOrEmpty(domain)) - throw new DnsWebServiceException("Parameter 'domain' missing."); + HttpRequest request = context.Request; - domain = domain.TrimEnd('.'); + string domain = request.GetQueryOrForm("domain").TrimEnd('.'); - string zoneName = request.QueryString["zone"]; + string zoneName = request.QueryOrForm("zone"); if (zoneName is not null) zoneName = zoneName.TrimEnd('.'); AuthZoneInfo zoneInfo = _dnsWebService.DnsServer.AuthZoneManager.FindAuthZoneInfo(string.IsNullOrEmpty(zoneName) ? domain : zoneName); if (zoneInfo is null) - throw new DnsWebServiceException("No authoritative zone was not found for domain: " + domain); + throw new DnsWebServiceException("No such authoritative zone was found: " + domain); if (zoneInfo.Internal) throw new DnsWebServiceException("Access was denied to manage internal DNS Server zone."); - UserSession session = _dnsWebService.GetSession(request); + UserSession session = context.GetCurrentSession(); - if (!_dnsWebService.AuthManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, session.User, PermissionFlag.Modify)) + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, session.User, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); - string strType = request.QueryString["type"]; - if (string.IsNullOrEmpty(strType)) - throw new DnsWebServiceException("Parameter 'type' missing."); - - DnsResourceRecordType type = Enum.Parse(strType, true); - - string value = request.QueryString["value"]; - - uint ttl; - string strTtl = request.QueryString["ttl"]; - if (string.IsNullOrEmpty(strTtl)) - ttl = _defaultRecordTtl; - else - ttl = uint.Parse(strTtl); - - bool overwrite = false; - string strOverwrite = request.QueryString["overwrite"]; - if (!string.IsNullOrEmpty(strOverwrite)) - overwrite = bool.Parse(strOverwrite); - - string comments = request.QueryString["comments"]; + DnsResourceRecordType type = request.GetQueryOrFormEnum("type"); + uint ttl = request.GetQueryOrForm("ttl", uint.Parse, _defaultRecordTtl); + bool overwrite = request.GetQueryOrForm("overwrite", bool.Parse, false); + string comments = request.QueryOrForm("comments"); DnsResourceRecord newRecord; @@ -2200,27 +1788,15 @@ namespace DnsServerCore case DnsResourceRecordType.A: case DnsResourceRecordType.AAAA: { - string strIPAddress = request.QueryString["ipAddress"]; - if (string.IsNullOrEmpty(strIPAddress)) - { - if (string.IsNullOrEmpty(value)) - throw new DnsWebServiceException("Parameter 'ipAddress' missing."); - - strIPAddress = value; - } - + string strIPAddress = request.GetQueryOrFormAlt("ipAddress", "value"); IPAddress ipAddress; if (strIPAddress.Equals("request-ip-address")) - ipAddress = DnsWebService.GetRequestRemoteEndPoint(request).Address; + ipAddress = context.GetRemoteEndPoint().Address; else ipAddress = IPAddress.Parse(strIPAddress); - bool ptr = false; - string strPtr = request.QueryString["ptr"]; - if (!string.IsNullOrEmpty(strPtr)) - ptr = bool.Parse(strPtr); - + bool ptr = request.GetQueryOrForm("ptr", bool.Parse, false); if (ptr) { string ptrDomain = Zone.GetReverseZone(ipAddress, type == DnsResourceRecordType.A ? 32 : 128); @@ -2228,11 +1804,7 @@ namespace DnsServerCore AuthZoneInfo reverseZoneInfo = _dnsWebService.DnsServer.AuthZoneManager.FindAuthZoneInfo(ptrDomain); if (reverseZoneInfo is null) { - bool createPtrZone = false; - string strCreatePtrZone = request.QueryString["createPtrZone"]; - if (!string.IsNullOrEmpty(strCreatePtrZone)) - createPtrZone = bool.Parse(strCreatePtrZone); - + bool createPtrZone = request.GetQueryOrForm("createPtrZone", bool.Parse, false); if (!createPtrZone) throw new DnsServerException("No reverse zone available to add PTR record."); @@ -2243,10 +1815,10 @@ namespace DnsServerCore throw new DnsServerException("Failed to create reverse zone to add PTR record: " + ptrZone); //set permissions - _dnsWebService.AuthManager.SetPermission(PermissionSection.Zones, reverseZoneInfo.Name, session.User, PermissionFlag.ViewModifyDelete); - _dnsWebService.AuthManager.SetPermission(PermissionSection.Zones, reverseZoneInfo.Name, _dnsWebService.AuthManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete); - _dnsWebService.AuthManager.SetPermission(PermissionSection.Zones, reverseZoneInfo.Name, _dnsWebService.AuthManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.ViewModifyDelete); - _dnsWebService.AuthManager.SaveConfigFile(); + _dnsWebService._authManager.SetPermission(PermissionSection.Zones, reverseZoneInfo.Name, session.User, PermissionFlag.ViewModifyDelete); + _dnsWebService._authManager.SetPermission(PermissionSection.Zones, reverseZoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete); + _dnsWebService._authManager.SetPermission(PermissionSection.Zones, reverseZoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.ViewModifyDelete); + _dnsWebService._authManager.SaveConfigFile(); } if (reverseZoneInfo.Internal) @@ -2265,7 +1837,7 @@ namespace DnsServerCore newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsAAAARecordData(ipAddress)); if (!string.IsNullOrEmpty(comments)) - newRecord.SetComments(comments); + newRecord.GetAuthRecordInfo().Comments = comments; if (overwrite) _dnsWebService.DnsServer.AuthZoneManager.SetRecord(zoneInfo.Name, newRecord); @@ -2276,26 +1848,16 @@ namespace DnsServerCore case DnsResourceRecordType.NS: { - string nameServer = request.QueryString["nameServer"]; - if (string.IsNullOrEmpty(nameServer)) - { - if (string.IsNullOrEmpty(value)) - throw new DnsWebServiceException("Parameter 'nameServer' missing."); + string nameServer = request.GetQueryOrFormAlt("nameServer", "value").TrimEnd('.'); + string glueAddresses = request.GetQueryOrForm("glue", null); - nameServer = value; - } + newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsNSRecordData(nameServer)); - string glueAddresses = request.QueryString["glue"]; - if (string.IsNullOrEmpty(glueAddresses)) - glueAddresses = null; - - newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsNSRecordData(nameServer.TrimEnd('.'))); - - if (glueAddresses != null) + if (!string.IsNullOrEmpty(glueAddresses)) newRecord.SetGlueRecords(glueAddresses); if (!string.IsNullOrEmpty(comments)) - newRecord.SetComments(comments); + newRecord.GetAuthRecordInfo().Comments = comments; if (overwrite) _dnsWebService.DnsServer.AuthZoneManager.SetRecord(zoneInfo.Name, newRecord); @@ -2313,19 +1875,12 @@ namespace DnsServerCore throw new DnsWebServiceException("Record already exists. Use overwrite option if you wish to overwrite existing records."); } - string cname = request.QueryString["cname"]; - if (string.IsNullOrEmpty(cname)) - { - if (string.IsNullOrEmpty(value)) - throw new DnsWebServiceException("Parameter 'cname' missing."); + string cname = request.GetQueryOrFormAlt("cname", "value").TrimEnd('.'); - cname = value; - } - - newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsCNAMERecordData(cname.TrimEnd('.'))); + newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsCNAMERecordData(cname)); if (!string.IsNullOrEmpty(comments)) - newRecord.SetComments(comments); + newRecord.GetAuthRecordInfo().Comments = comments; _dnsWebService.DnsServer.AuthZoneManager.SetRecord(zoneInfo.Name, newRecord); } @@ -2333,19 +1888,12 @@ namespace DnsServerCore case DnsResourceRecordType.PTR: { - string ptrName = request.QueryString["ptrName"]; - if (string.IsNullOrEmpty(ptrName)) - { - if (string.IsNullOrEmpty(value)) - throw new DnsWebServiceException("Parameter 'ptrName' missing."); + string ptrName = request.GetQueryOrFormAlt("ptrName", "value").TrimEnd('.'); - ptrName = value; - } - - newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsPTRRecordData(ptrName.TrimEnd('.'))); + newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsPTRRecordData(ptrName)); if (!string.IsNullOrEmpty(comments)) - newRecord.SetComments(comments); + newRecord.GetAuthRecordInfo().Comments = comments; if (overwrite) _dnsWebService.DnsServer.AuthZoneManager.SetRecord(zoneInfo.Name, newRecord); @@ -2356,23 +1904,13 @@ namespace DnsServerCore case DnsResourceRecordType.MX: { - string exchange = request.QueryString["exchange"]; - if (string.IsNullOrEmpty(exchange)) - { - if (string.IsNullOrEmpty(value)) - throw new DnsWebServiceException("Parameter 'exchange' missing."); + ushort preference = request.GetQueryOrForm("preference", ushort.Parse); + string exchange = request.GetQueryOrFormAlt("exchange", "value").TrimEnd('.'); - exchange = value; - } - - string preference = request.QueryString["preference"]; - if (string.IsNullOrEmpty(preference)) - throw new DnsWebServiceException("Parameter 'preference' missing."); - - newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsMXRecordData(ushort.Parse(preference), exchange.TrimEnd('.'))); + newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsMXRecordData(preference, exchange)); if (!string.IsNullOrEmpty(comments)) - newRecord.SetComments(comments); + newRecord.GetAuthRecordInfo().Comments = comments; if (overwrite) _dnsWebService.DnsServer.AuthZoneManager.SetRecord(zoneInfo.Name, newRecord); @@ -2383,19 +1921,12 @@ namespace DnsServerCore case DnsResourceRecordType.TXT: { - string text = request.QueryString["text"]; - if (string.IsNullOrEmpty(text)) - { - if (string.IsNullOrEmpty(value)) - throw new DnsWebServiceException("Parameter 'text' missing."); - - text = value; - } + string text = request.GetQueryOrFormAlt("text", "value"); newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsTXTRecordData(text)); if (!string.IsNullOrEmpty(comments)) - newRecord.SetComments(comments); + newRecord.GetAuthRecordInfo().Comments = comments; if (overwrite) _dnsWebService.DnsServer.AuthZoneManager.SetRecord(zoneInfo.Name, newRecord); @@ -2406,31 +1937,15 @@ namespace DnsServerCore case DnsResourceRecordType.SRV: { - string priority = request.QueryString["priority"]; - if (string.IsNullOrEmpty(priority)) - throw new DnsWebServiceException("Parameter 'priority' missing."); + ushort priority = request.GetQueryOrForm("priority", ushort.Parse); + ushort weight = request.GetQueryOrForm("weight", ushort.Parse); + ushort port = request.GetQueryOrForm("port", ushort.Parse); + string target = request.GetQueryOrFormAlt("target", "value").TrimEnd('.'); - string weight = request.QueryString["weight"]; - if (string.IsNullOrEmpty(weight)) - throw new DnsWebServiceException("Parameter 'weight' missing."); - - string port = request.QueryString["port"]; - if (string.IsNullOrEmpty(port)) - throw new DnsWebServiceException("Parameter 'port' missing."); - - string target = request.QueryString["target"]; - if (string.IsNullOrEmpty(target)) - { - if (string.IsNullOrEmpty(value)) - throw new DnsWebServiceException("Parameter 'target' missing."); - - target = value; - } - - newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsSRVRecordData(ushort.Parse(priority), ushort.Parse(weight), ushort.Parse(port), target.TrimEnd('.'))); + newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsSRVRecordData(priority, weight, port, target)); if (!string.IsNullOrEmpty(comments)) - newRecord.SetComments(comments); + newRecord.GetAuthRecordInfo().Comments = comments; if (overwrite) _dnsWebService.DnsServer.AuthZoneManager.SetRecord(zoneInfo.Name, newRecord); @@ -2448,19 +1963,12 @@ namespace DnsServerCore throw new DnsWebServiceException("Record already exists. Use overwrite option if you wish to overwrite existing records."); } - string dname = request.QueryString["dname"]; - if (string.IsNullOrEmpty(dname)) - { - if (string.IsNullOrEmpty(value)) - throw new DnsWebServiceException("Parameter 'dname' missing."); + string dname = request.GetQueryOrFormAlt("dname", "value").TrimEnd('.'); - dname = value; - } - - newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsDNAMERecordData(dname.TrimEnd('.'))); + newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsDNAMERecordData(dname)); if (!string.IsNullOrEmpty(comments)) - newRecord.SetComments(comments); + newRecord.GetAuthRecordInfo().Comments = comments; _dnsWebService.DnsServer.AuthZoneManager.SetRecord(zoneInfo.Name, newRecord); } @@ -2468,31 +1976,15 @@ namespace DnsServerCore case DnsResourceRecordType.DS: { - string strKeyTag = request.QueryString["keyTag"]; - if (string.IsNullOrEmpty(strKeyTag)) - throw new DnsWebServiceException("Parameter 'keyTag' missing."); + ushort keyTag = request.GetQueryOrForm("keyTag", ushort.Parse); + DnssecAlgorithm algorithm = Enum.Parse(request.GetQueryOrForm("algorithm").Replace('-', '_'), true); + DnssecDigestType digestType = Enum.Parse(request.GetQueryOrForm("digestType").Replace('-', '_'), true); + byte[] digest = request.GetQueryOrFormAlt("digest", "value", Convert.FromHexString); - string strAlgorithm = request.QueryString["algorithm"]; - if (string.IsNullOrEmpty(strAlgorithm)) - throw new DnsWebServiceException("Parameter 'algorithm' missing."); - - string strDigestType = request.QueryString["digestType"]; - if (string.IsNullOrEmpty(strDigestType)) - throw new DnsWebServiceException("Parameter 'digestType' missing."); - - string digest = request.QueryString["digest"]; - if (string.IsNullOrEmpty(digest)) - { - if (string.IsNullOrEmpty(value)) - throw new DnsWebServiceException("Parameter 'digest' missing."); - - digest = value; - } - - newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsDSRecordData(ushort.Parse(strKeyTag), Enum.Parse(strAlgorithm.Replace('-', '_'), true), Enum.Parse(strDigestType.Replace('-', '_'), true), Convert.FromHexString(digest))); + newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsDSRecordData(keyTag, algorithm, digestType, digest)); if (!string.IsNullOrEmpty(comments)) - newRecord.SetComments(comments); + newRecord.GetAuthRecordInfo().Comments = comments; if (overwrite) _dnsWebService.DnsServer.AuthZoneManager.SetRecord(zoneInfo.Name, newRecord); @@ -2503,22 +1995,14 @@ namespace DnsServerCore case DnsResourceRecordType.SSHFP: { - string strAlgorithm = request.QueryString["sshfpAlgorithm"]; - if (string.IsNullOrEmpty(strAlgorithm)) - throw new DnsWebServiceException("Parameter 'sshfpAlgorithm' missing."); + DnsSSHFPAlgorithm sshfpAlgorithm = request.GetQueryOrFormEnum("sshfpAlgorithm"); + DnsSSHFPFingerprintType sshfpFingerprintType = request.GetQueryOrFormEnum("sshfpFingerprintType"); + byte[] sshfpFingerprint = request.GetQueryOrForm("sshfpFingerprint", Convert.FromHexString); - string strFingerprintType = request.QueryString["sshfpFingerprintType"]; - if (string.IsNullOrEmpty(strFingerprintType)) - throw new DnsWebServiceException("Parameter 'sshfpFingerprintType' missing."); - - string strFingerprint = request.QueryString["sshfpFingerprint"]; - if (string.IsNullOrEmpty(strFingerprint)) - throw new DnsWebServiceException("Parameter 'sshfpFingerprint' missing."); - - newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsSSHFPRecordData(Enum.Parse(strAlgorithm, true), Enum.Parse(strFingerprintType, true), Convert.FromHexString(strFingerprint))); + newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsSSHFPRecordData(sshfpAlgorithm, sshfpFingerprintType, sshfpFingerprint)); if (!string.IsNullOrEmpty(comments)) - newRecord.SetComments(comments); + newRecord.GetAuthRecordInfo().Comments = comments; if (overwrite) _dnsWebService.DnsServer.AuthZoneManager.SetRecord(zoneInfo.Name, newRecord); @@ -2529,26 +2013,15 @@ namespace DnsServerCore case DnsResourceRecordType.TLSA: { - string strCertificateUsage = request.QueryString["tlsaCertificateUsage"]; - if (string.IsNullOrEmpty(strCertificateUsage)) - throw new DnsWebServiceException("Parameter 'tlsaCertificateUsage' missing."); + DnsTLSACertificateUsage tlsaCertificateUsage = Enum.Parse(request.GetQueryOrForm("tlsaCertificateUsage").Replace('-', '_'), true); + DnsTLSASelector tlsaSelector = request.GetQueryOrFormEnum("tlsaSelector"); + DnsTLSAMatchingType tlsaMatchingType = Enum.Parse(request.GetQueryOrForm("tlsaMatchingType").Replace('-', '_'), true); + string tlsaCertificateAssociationData = request.GetQueryOrForm("tlsaCertificateAssociationData"); - string strSelector = request.QueryString["tlsaSelector"]; - if (string.IsNullOrEmpty(strSelector)) - throw new DnsWebServiceException("Parameter 'tlsaSelector' missing."); - - string strMatchingType = request.QueryString["tlsaMatchingType"]; - if (string.IsNullOrEmpty(strMatchingType)) - throw new DnsWebServiceException("Parameter 'tlsaMatchingType' missing."); - - string strCertificateAssociationData = request.QueryString["tlsaCertificateAssociationData"]; - if (string.IsNullOrEmpty(strCertificateAssociationData)) - throw new DnsWebServiceException("Parameter 'tlsaCertificateAssociationData' missing."); - - newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsTLSARecordData(Enum.Parse(strCertificateUsage.Replace('-', '_'), true), Enum.Parse(strSelector, true), Enum.Parse(strMatchingType.Replace('-', '_'), true), strCertificateAssociationData)); + newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsTLSARecordData(tlsaCertificateUsage, tlsaSelector, tlsaMatchingType, tlsaCertificateAssociationData)); if (!string.IsNullOrEmpty(comments)) - newRecord.SetComments(comments); + newRecord.GetAuthRecordInfo().Comments = comments; if (overwrite) _dnsWebService.DnsServer.AuthZoneManager.SetRecord(zoneInfo.Name, newRecord); @@ -2559,21 +2032,14 @@ namespace DnsServerCore case DnsResourceRecordType.CAA: { - string flags = request.QueryString["flags"]; - if (string.IsNullOrEmpty(flags)) - throw new DnsWebServiceException("Parameter 'flags' missing."); + byte flags = request.GetQueryOrForm("flags", byte.Parse); + string tag = request.GetQueryOrForm("tag"); + string value = request.GetQueryOrForm("value"); - string tag = request.QueryString["tag"]; - if (string.IsNullOrEmpty(tag)) - throw new DnsWebServiceException("Parameter 'tag' missing."); - - if (string.IsNullOrEmpty(value)) - throw new DnsWebServiceException("Parameter 'value' missing."); - - newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsCAARecordData(byte.Parse(flags), tag, value)); + newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsCAARecordData(flags, tag, value)); if (!string.IsNullOrEmpty(comments)) - newRecord.SetComments(comments); + newRecord.GetAuthRecordInfo().Comments = comments; if (overwrite) _dnsWebService.DnsServer.AuthZoneManager.SetRecord(zoneInfo.Name, newRecord); @@ -2584,19 +2050,12 @@ namespace DnsServerCore case DnsResourceRecordType.ANAME: { - string aname = request.QueryString["aname"]; - if (string.IsNullOrEmpty(aname)) - { - if (string.IsNullOrEmpty(value)) - throw new DnsWebServiceException("Parameter 'aname' missing."); + string aname = request.GetQueryOrFormAlt("aname", "value").TrimEnd('.'); - aname = value; - } - - newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsANAMERecordData(aname.TrimEnd('.'))); + newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsANAMERecordData(aname)); if (!string.IsNullOrEmpty(comments)) - newRecord.SetComments(comments); + newRecord.GetAuthRecordInfo().Comments = comments; if (overwrite) _dnsWebService.DnsServer.AuthZoneManager.SetRecord(zoneInfo.Name, newRecord); @@ -2607,24 +2066,9 @@ namespace DnsServerCore case DnsResourceRecordType.FWD: { - DnsTransportProtocol protocol = DnsTransportProtocol.Udp; - string strProtocol = request.QueryString["protocol"]; - if (!string.IsNullOrEmpty(strProtocol)) - protocol = Enum.Parse(strProtocol, true); - - string forwarder = request.QueryString["forwarder"]; - if (string.IsNullOrEmpty(forwarder)) - { - if (string.IsNullOrEmpty(value)) - throw new DnsWebServiceException("Parameter 'forwarder' missing."); - - forwarder = value; - } - - bool dnssecValidation = false; - string strDnssecValidation = request.QueryString["dnssecValidation"]; - if (!string.IsNullOrEmpty(strDnssecValidation)) - dnssecValidation = bool.Parse(strDnssecValidation); + DnsTransportProtocol protocol = request.GetQueryOrFormEnum("protocol", DnsTransportProtocol.Udp); + string forwarder = request.GetQueryOrFormAlt("forwarder", "value"); + bool dnssecValidation = request.GetQueryOrForm("dnssecValidation", bool.Parse, false); NetProxyType proxyType = NetProxyType.None; string proxyAddress = null; @@ -2634,30 +2078,20 @@ namespace DnsServerCore if (!forwarder.Equals("this-server")) { - string strProxyType = request.QueryString["proxyType"]; - if (!string.IsNullOrEmpty(strProxyType)) - proxyType = Enum.Parse(strProxyType, true); - + proxyType = request.GetQueryOrFormEnum("proxyType", NetProxyType.None); if (proxyType != NetProxyType.None) { - proxyAddress = request.QueryString["proxyAddress"]; - if (string.IsNullOrEmpty(proxyAddress)) - throw new DnsWebServiceException("Parameter 'proxyAddress' missing."); - - string strProxyPort = request.QueryString["proxyPort"]; - if (string.IsNullOrEmpty(strProxyPort)) - throw new DnsWebServiceException("Parameter 'proxyPort' missing."); - - proxyPort = ushort.Parse(strProxyPort); - proxyUsername = request.QueryString["proxyUsername"]; - proxyPassword = request.QueryString["proxyPassword"]; + proxyAddress = request.GetQueryOrForm("proxyAddress"); + proxyPort = request.GetQueryOrForm("proxyPort", ushort.Parse); + proxyUsername = request.QueryOrForm("proxyUsername"); + proxyPassword = request.QueryOrForm("proxyPassword"); } } newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsForwarderRecordData(protocol, forwarder, dnssecValidation, proxyType, proxyAddress, proxyPort, proxyUsername, proxyPassword)); if (!string.IsNullOrEmpty(comments)) - newRecord.SetComments(comments); + newRecord.GetAuthRecordInfo().Comments = comments; if (overwrite) _dnsWebService.DnsServer.AuthZoneManager.SetRecord(zoneInfo.Name, newRecord); @@ -2668,22 +2102,9 @@ namespace DnsServerCore case DnsResourceRecordType.APP: { - string appName = request.QueryString["appName"]; - if (string.IsNullOrEmpty(appName)) - { - if (string.IsNullOrEmpty(value)) - throw new DnsWebServiceException("Parameter 'appName' missing."); - - appName = value; - } - - string classPath = request.QueryString["classPath"]; - if (string.IsNullOrEmpty(classPath)) - throw new DnsWebServiceException("Parameter 'classPath' missing."); - - string recordData = request.QueryString["recordData"]; - if (string.IsNullOrEmpty(recordData)) - recordData = ""; + string appName = request.GetQueryOrFormAlt("appName", "value"); + string classPath = request.GetQueryOrForm("classPath"); + string recordData = request.GetQueryOrForm("recordData", ""); if (!overwrite) { @@ -2695,7 +2116,7 @@ namespace DnsServerCore newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsApplicationRecordData(appName, classPath, recordData)); if (!string.IsNullOrEmpty(comments)) - newRecord.SetComments(comments); + newRecord.GetAuthRecordInfo().Comments = comments; _dnsWebService.DnsServer.AuthZoneManager.SetRecord(zoneInfo.Name, newRecord); } @@ -2705,10 +2126,12 @@ namespace DnsServerCore throw new DnsWebServiceException("Type not supported for AddRecords()."); } - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + session.User.Username + "] New record was added to authoritative zone {domain: " + domain + "; type: " + type + "; value: " + value + "; ttl: " + ttl + ";}"); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] New record was added to authoritative zone {record: " + newRecord.ToString() + "}"); _dnsWebService.DnsServer.AuthZoneManager.SaveZoneFile(zoneInfo.Name); + Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); + jsonWriter.WritePropertyName("zone"); WriteZoneInfoAsJson(zoneInfo, jsonWriter); @@ -2716,79 +2139,71 @@ namespace DnsServerCore WriteRecordAsJson(newRecord, jsonWriter, true, null); } - public void GetRecords(HttpListenerRequest request, JsonTextWriter jsonWriter) + public void GetRecords(HttpContext context) { - string domain = request.QueryString["domain"]; - if (string.IsNullOrEmpty(domain)) - throw new DnsWebServiceException("Parameter 'domain' missing."); + HttpRequest request = context.Request; - domain = domain.TrimEnd('.'); + string domain = request.GetQueryOrForm("domain").TrimEnd('.'); - AuthZoneInfo zoneInfo = _dnsWebService.DnsServer.AuthZoneManager.FindAuthZoneInfo(domain); - if (zoneInfo is null) - throw new DnsWebServiceException("No authoritative zone was not found for domain: " + domain); - - UserSession session = _dnsWebService.GetSession(request); - - if (!_dnsWebService.AuthManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, session.User, PermissionFlag.View)) - throw new DnsWebServiceException("Access was denied."); - - jsonWriter.WritePropertyName("zone"); - WriteZoneInfoAsJson(zoneInfo, jsonWriter); - - List records = new List(); - _dnsWebService.DnsServer.AuthZoneManager.ListAllRecords(domain, records); - - WriteRecordsAsJson(records, jsonWriter, true, zoneInfo); - } - - public void DeleteRecord(HttpListenerRequest request) - { - string domain = request.QueryString["domain"]; - if (string.IsNullOrEmpty(domain)) - throw new DnsWebServiceException("Parameter 'domain' missing."); - - domain = domain.TrimEnd('.'); - - string zoneName = request.QueryString["zone"]; + string zoneName = request.QueryOrForm("zone"); if (zoneName is not null) zoneName = zoneName.TrimEnd('.'); AuthZoneInfo zoneInfo = _dnsWebService.DnsServer.AuthZoneManager.FindAuthZoneInfo(string.IsNullOrEmpty(zoneName) ? domain : zoneName); if (zoneInfo is null) - throw new DnsWebServiceException("No authoritative zone was not found for domain: " + domain); + throw new DnsWebServiceException("No such authoritative zone was found: " + domain); + + UserSession session = context.GetCurrentSession(); + + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, session.User, PermissionFlag.View)) + throw new DnsWebServiceException("Access was denied."); + + bool listZone = request.GetQueryOrForm("listZone", bool.Parse, false); + + List records = new List(); + + if (listZone) + _dnsWebService.DnsServer.AuthZoneManager.ListAllZoneRecords(zoneInfo.Name, records); + else + _dnsWebService.DnsServer.AuthZoneManager.ListAllRecords(zoneInfo.Name, domain, records); + + Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); + + jsonWriter.WritePropertyName("zone"); + WriteZoneInfoAsJson(zoneInfo, jsonWriter); + + WriteRecordsAsJson(records, jsonWriter, true, zoneInfo); + } + + public void DeleteRecord(HttpContext context) + { + HttpRequest request = context.Request; + + string domain = request.GetQueryOrForm("domain").TrimEnd('.'); + + string zoneName = request.QueryOrForm("zone"); + if (zoneName is not null) + zoneName = zoneName.TrimEnd('.'); + + AuthZoneInfo zoneInfo = _dnsWebService.DnsServer.AuthZoneManager.FindAuthZoneInfo(string.IsNullOrEmpty(zoneName) ? domain : zoneName); + if (zoneInfo is null) + throw new DnsWebServiceException("No such authoritative zone was found: " + domain); if (zoneInfo.Internal) throw new DnsWebServiceException("Access was denied to manage internal DNS Server zone."); - UserSession session = _dnsWebService.GetSession(request); + UserSession session = context.GetCurrentSession(); - if (!_dnsWebService.AuthManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, session.User, PermissionFlag.Delete)) + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, session.User, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); - string strType = request.QueryString["type"]; - if (string.IsNullOrEmpty(strType)) - throw new DnsWebServiceException("Parameter 'type' missing."); - - DnsResourceRecordType type = Enum.Parse(strType, true); - - string value = request.QueryString["value"]; - + DnsResourceRecordType type = request.GetQueryOrFormEnum("type"); switch (type) { case DnsResourceRecordType.A: case DnsResourceRecordType.AAAA: { - string strIPAddress = request.QueryString["ipAddress"]; - if (string.IsNullOrEmpty(strIPAddress)) - { - if (string.IsNullOrEmpty(value)) - throw new DnsWebServiceException("Parameter 'ipAddress' missing."); - - strIPAddress = value; - } - - IPAddress ipAddress = IPAddress.Parse(strIPAddress); + IPAddress ipAddress = IPAddress.Parse(request.GetQueryOrFormAlt("ipAddress", "value")); if (type == DnsResourceRecordType.A) _dnsWebService.DnsServer.AuthZoneManager.DeleteRecord(zoneInfo.Name, domain, type, new DnsARecordData(ipAddress)); @@ -2819,14 +2234,7 @@ namespace DnsServerCore case DnsResourceRecordType.NS: { - string nameServer = request.QueryString["nameServer"]; - if (string.IsNullOrEmpty(nameServer)) - { - if (string.IsNullOrEmpty(value)) - throw new DnsWebServiceException("Parameter 'nameServer' missing."); - - nameServer = value; - } + string nameServer = request.GetQueryOrFormAlt("nameServer", "value").TrimEnd('.'); _dnsWebService.DnsServer.AuthZoneManager.DeleteRecord(zoneInfo.Name, domain, type, new DnsNSRecordData(nameServer)); } @@ -2838,14 +2246,7 @@ namespace DnsServerCore case DnsResourceRecordType.PTR: { - string ptrName = request.QueryString["ptrName"]; - if (string.IsNullOrEmpty(ptrName)) - { - if (string.IsNullOrEmpty(value)) - throw new DnsWebServiceException("Parameter 'ptrName' missing."); - - ptrName = value; - } + string ptrName = request.GetQueryOrFormAlt("ptrName", "value").TrimEnd('.'); _dnsWebService.DnsServer.AuthZoneManager.DeleteRecord(zoneInfo.Name, domain, type, new DnsPTRRecordData(ptrName)); } @@ -2853,33 +2254,16 @@ namespace DnsServerCore case DnsResourceRecordType.MX: { - string preference = request.QueryString["preference"]; - if (string.IsNullOrEmpty(preference)) - throw new DnsWebServiceException("Parameter 'preference' missing."); + ushort preference = request.GetQueryOrForm("preference", ushort.Parse); + string exchange = request.GetQueryOrFormAlt("exchange", "value").TrimEnd('.'); - string exchange = request.QueryString["exchange"]; - if (string.IsNullOrEmpty(exchange)) - { - if (string.IsNullOrEmpty(value)) - throw new DnsWebServiceException("Parameter 'exchange' missing."); - - exchange = value; - } - - _dnsWebService.DnsServer.AuthZoneManager.DeleteRecord(zoneInfo.Name, domain, type, new DnsMXRecordData(ushort.Parse(preference), exchange)); + _dnsWebService.DnsServer.AuthZoneManager.DeleteRecord(zoneInfo.Name, domain, type, new DnsMXRecordData(preference, exchange)); } break; case DnsResourceRecordType.TXT: { - string text = request.QueryString["text"]; - if (string.IsNullOrEmpty(text)) - { - if (string.IsNullOrEmpty(value)) - throw new DnsWebServiceException("Parameter 'text' missing."); - - text = value; - } + string text = request.GetQueryOrFormAlt("text", "value"); _dnsWebService.DnsServer.AuthZoneManager.DeleteRecord(zoneInfo.Name, domain, type, new DnsTXTRecordData(text)); } @@ -2887,28 +2271,12 @@ namespace DnsServerCore case DnsResourceRecordType.SRV: { - string priority = request.QueryString["priority"]; - if (string.IsNullOrEmpty(priority)) - throw new DnsWebServiceException("Parameter 'priority' missing."); + ushort priority = request.GetQueryOrForm("priority", ushort.Parse); + ushort weight = request.GetQueryOrForm("weight", ushort.Parse); + ushort port = request.GetQueryOrForm("port", ushort.Parse); + string target = request.GetQueryOrFormAlt("target", "value").TrimEnd('.'); - string weight = request.QueryString["weight"]; - if (string.IsNullOrEmpty(weight)) - throw new DnsWebServiceException("Parameter 'weight' missing."); - - string port = request.QueryString["port"]; - if (string.IsNullOrEmpty(port)) - throw new DnsWebServiceException("Parameter 'port' missing."); - - string target = request.QueryString["target"]; - if (string.IsNullOrEmpty(target)) - { - if (string.IsNullOrEmpty(value)) - throw new DnsWebServiceException("Parameter 'target' missing."); - - target = value; - } - - _dnsWebService.DnsServer.AuthZoneManager.DeleteRecord(zoneInfo.Name, domain, type, new DnsSRVRecordData(ushort.Parse(priority), ushort.Parse(weight), ushort.Parse(port), target)); + _dnsWebService.DnsServer.AuthZoneManager.DeleteRecord(zoneInfo.Name, domain, type, new DnsSRVRecordData(priority, weight, port, target)); } break; @@ -2918,98 +2286,49 @@ namespace DnsServerCore case DnsResourceRecordType.DS: { - string strKeyTag = request.QueryString["keyTag"]; - if (string.IsNullOrEmpty(strKeyTag)) - throw new DnsWebServiceException("Parameter 'keyTag' missing."); + ushort keyTag = request.GetQueryOrForm("keyTag", ushort.Parse); + DnssecAlgorithm algorithm = Enum.Parse(request.GetQueryOrForm("algorithm").Replace('-', '_'), true); + DnssecDigestType digestType = Enum.Parse(request.GetQueryOrForm("digestType").Replace('-', '_'), true); + byte[] digest = Convert.FromHexString(request.GetQueryOrFormAlt("digest", "value")); - string strAlgorithm = request.QueryString["algorithm"]; - if (string.IsNullOrEmpty(strAlgorithm)) - throw new DnsWebServiceException("Parameter 'algorithm' missing."); - - string strDigestType = request.QueryString["digestType"]; - if (string.IsNullOrEmpty(strDigestType)) - throw new DnsWebServiceException("Parameter 'digestType' missing."); - - string digest = request.QueryString["digest"]; - if (string.IsNullOrEmpty(digest)) - { - if (string.IsNullOrEmpty(value)) - throw new DnsWebServiceException("Parameter 'digest' missing."); - - digest = value; - } - - _dnsWebService.DnsServer.AuthZoneManager.DeleteRecord(zoneInfo.Name, domain, type, new DnsDSRecordData(ushort.Parse(strKeyTag), Enum.Parse(strAlgorithm.Replace('-', '_'), true), Enum.Parse(strDigestType.Replace('-', '_'), true), Convert.FromHexString(digest))); + _dnsWebService.DnsServer.AuthZoneManager.DeleteRecord(zoneInfo.Name, domain, type, new DnsDSRecordData(keyTag, algorithm, digestType, digest)); } break; case DnsResourceRecordType.SSHFP: { - string strAlgorithm = request.QueryString["sshfpAlgorithm"]; - if (string.IsNullOrEmpty(strAlgorithm)) - throw new DnsWebServiceException("Parameter 'sshfpAlgorithm' missing."); + DnsSSHFPAlgorithm sshfpAlgorithm = request.GetQueryOrFormEnum("sshfpAlgorithm"); + DnsSSHFPFingerprintType sshfpFingerprintType = request.GetQueryOrFormEnum("sshfpFingerprintType"); + byte[] sshfpFingerprint = request.GetQueryOrForm("sshfpFingerprint", Convert.FromHexString); - string strFingerprintType = request.QueryString["sshfpFingerprintType"]; - if (string.IsNullOrEmpty(strFingerprintType)) - throw new DnsWebServiceException("Parameter 'sshfpFingerprintType' missing."); - - string strFingerprint = request.QueryString["sshfpFingerprint"]; - if (string.IsNullOrEmpty(strFingerprint)) - throw new DnsWebServiceException("Parameter 'sshfpFingerprint' missing."); - - _dnsWebService.DnsServer.AuthZoneManager.DeleteRecord(zoneInfo.Name, domain, type, new DnsSSHFPRecordData(Enum.Parse(strAlgorithm, true), Enum.Parse(strFingerprintType, true), Convert.FromHexString(strFingerprint))); + _dnsWebService.DnsServer.AuthZoneManager.DeleteRecord(zoneInfo.Name, domain, type, new DnsSSHFPRecordData(sshfpAlgorithm, sshfpFingerprintType, sshfpFingerprint)); } break; case DnsResourceRecordType.TLSA: { - string strCertificateUsage = request.QueryString["tlsaCertificateUsage"]; - if (string.IsNullOrEmpty(strCertificateUsage)) - throw new DnsWebServiceException("Parameter 'tlsaCertificateUsage' missing."); + DnsTLSACertificateUsage tlsaCertificateUsage = Enum.Parse(request.GetQueryOrForm("tlsaCertificateUsage").Replace('-', '_'), true); + DnsTLSASelector tlsaSelector = request.GetQueryOrFormEnum("tlsaSelector"); + DnsTLSAMatchingType tlsaMatchingType = Enum.Parse(request.GetQueryOrForm("tlsaMatchingType").Replace('-', '_'), true); + string tlsaCertificateAssociationData = request.GetQueryOrForm("tlsaCertificateAssociationData"); - string strSelector = request.QueryString["tlsaSelector"]; - if (string.IsNullOrEmpty(strSelector)) - throw new DnsWebServiceException("Parameter 'tlsaSelector' missing."); - - string strMatchingType = request.QueryString["tlsaMatchingType"]; - if (string.IsNullOrEmpty(strMatchingType)) - throw new DnsWebServiceException("Parameter 'tlsaMatchingType' missing."); - - string strCertificateAssociationData = request.QueryString["tlsaCertificateAssociationData"]; - if (string.IsNullOrEmpty(strCertificateAssociationData)) - throw new DnsWebServiceException("Parameter 'tlsaCertificateAssociationData' missing."); - - _dnsWebService.DnsServer.AuthZoneManager.DeleteRecord(zoneInfo.Name, domain, type, new DnsTLSARecordData(Enum.Parse(strCertificateUsage.Replace('-', '_'), true), Enum.Parse(strSelector, true), Enum.Parse(strMatchingType.Replace('-', '_'), true), strCertificateAssociationData)); + _dnsWebService.DnsServer.AuthZoneManager.DeleteRecord(zoneInfo.Name, domain, type, new DnsTLSARecordData(tlsaCertificateUsage, tlsaSelector, tlsaMatchingType, tlsaCertificateAssociationData)); } break; case DnsResourceRecordType.CAA: { - string flags = request.QueryString["flags"]; - if (string.IsNullOrEmpty(flags)) - throw new DnsWebServiceException("Parameter 'flags' missing."); + byte flags = request.GetQueryOrForm("flags", byte.Parse); + string tag = request.GetQueryOrForm("tag"); + string value = request.GetQueryOrForm("value"); - string tag = request.QueryString["tag"]; - if (string.IsNullOrEmpty(tag)) - throw new DnsWebServiceException("Parameter 'tag' missing."); - - if (string.IsNullOrEmpty(value)) - throw new DnsWebServiceException("Parameter 'value' missing."); - - _dnsWebService.DnsServer.AuthZoneManager.DeleteRecord(zoneInfo.Name, domain, type, new DnsCAARecordData(byte.Parse(flags), tag, value)); + _dnsWebService.DnsServer.AuthZoneManager.DeleteRecord(zoneInfo.Name, domain, type, new DnsCAARecordData(flags, tag, value)); } break; case DnsResourceRecordType.ANAME: { - string aname = request.QueryString["aname"]; - if (string.IsNullOrEmpty(aname)) - { - if (string.IsNullOrEmpty(value)) - throw new DnsWebServiceException("Parameter 'aname' missing."); - - aname = value; - } + string aname = request.GetQueryOrFormAlt("aname", "value").TrimEnd('.'); _dnsWebService.DnsServer.AuthZoneManager.DeleteRecord(zoneInfo.Name, domain, type, new DnsANAMERecordData(aname)); } @@ -3017,20 +2336,10 @@ namespace DnsServerCore case DnsResourceRecordType.FWD: { - string strProtocol = request.QueryString["protocol"]; - if (string.IsNullOrEmpty(strProtocol)) - strProtocol = "Udp"; + DnsTransportProtocol protocol = request.GetQueryOrFormEnum("protocol", DnsTransportProtocol.Udp); + string forwarder = request.GetQueryOrFormAlt("forwarder", "value"); - string forwarder = request.QueryString["forwarder"]; - if (string.IsNullOrEmpty(forwarder)) - { - if (string.IsNullOrEmpty(value)) - throw new DnsWebServiceException("Parameter 'forwarder' missing."); - - forwarder = value; - } - - _dnsWebService.DnsServer.AuthZoneManager.DeleteRecord(zoneInfo.Name, domain, type, new DnsForwarderRecordData(Enum.Parse(strProtocol, true), forwarder)); + _dnsWebService.DnsServer.AuthZoneManager.DeleteRecord(zoneInfo.Name, domain, type, new DnsForwarderRecordData(protocol, forwarder)); } break; @@ -3042,64 +2351,40 @@ namespace DnsServerCore throw new DnsWebServiceException("Type not supported for DeleteRecord()."); } - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + session.User.Username + "] Record was deleted from authoritative zone {domain: " + domain + "; type: " + type + ";}"); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] Record was deleted from authoritative zone {domain: " + domain + "; type: " + type + ";}"); _dnsWebService.DnsServer.AuthZoneManager.SaveZoneFile(zoneInfo.Name); } - public void UpdateRecord(HttpListenerRequest request, JsonTextWriter jsonWriter) + public void UpdateRecord(HttpContext context) { - string strType = request.QueryString["type"]; - if (string.IsNullOrEmpty(strType)) - throw new DnsWebServiceException("Parameter 'type' missing."); + HttpRequest request = context.Request; - DnsResourceRecordType type = Enum.Parse(strType, true); + string domain = request.GetQueryOrForm("domain").TrimEnd('.'); - string domain = request.QueryString["domain"]; - if (string.IsNullOrEmpty(domain)) - throw new DnsWebServiceException("Parameter 'domain' missing."); - - domain = domain.TrimEnd('.'); - - string zoneName = request.QueryString["zone"]; + string zoneName = request.QueryOrForm("zone"); if (zoneName is not null) zoneName = zoneName.TrimEnd('.'); AuthZoneInfo zoneInfo = _dnsWebService.DnsServer.AuthZoneManager.FindAuthZoneInfo(string.IsNullOrEmpty(zoneName) ? domain : zoneName); if (zoneInfo is null) - throw new DnsWebServiceException("No authoritative zone was not found for domain: " + domain); + throw new DnsWebServiceException("No such authoritative zone was found: " + domain); if (zoneInfo.Internal) throw new DnsWebServiceException("Access was denied to manage internal DNS Server zone."); - UserSession session = _dnsWebService.GetSession(request); + UserSession session = context.GetCurrentSession(); - if (!_dnsWebService.AuthManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, session.User, PermissionFlag.Modify)) + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, session.User, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); - string newDomain = request.QueryString["newDomain"]; - if (string.IsNullOrEmpty(newDomain)) - newDomain = domain; - - newDomain = newDomain.TrimEnd('.'); - - uint ttl; - string strTtl = request.QueryString["ttl"]; - if (string.IsNullOrEmpty(strTtl)) - ttl = _defaultRecordTtl; - else - ttl = uint.Parse(strTtl); - - string value = request.QueryString["value"]; - string newValue = request.QueryString["newValue"]; - - bool disable = false; - string strDisable = request.QueryString["disable"]; - if (!string.IsNullOrEmpty(strDisable)) - disable = bool.Parse(strDisable); - - string comments = request.QueryString["comments"]; + string newDomain = request.GetQueryOrForm("newDomain", domain).TrimEnd('.'); + uint ttl = request.GetQueryOrForm("ttl", uint.Parse, _defaultRecordTtl); + bool disable = request.GetQueryOrForm("disable", bool.Parse, false); + string comments = request.QueryOrForm("comments"); + DnsResourceRecordType type = request.GetQueryOrFormEnum("type"); + DnsResourceRecord oldRecord = null; DnsResourceRecord newRecord; switch (type) @@ -3107,67 +2392,41 @@ namespace DnsServerCore case DnsResourceRecordType.A: case DnsResourceRecordType.AAAA: { - string strIPAddress = request.QueryString["ipAddress"]; - if (string.IsNullOrEmpty(strIPAddress)) - { - if (string.IsNullOrEmpty(value)) - throw new DnsWebServiceException("Parameter 'ipAddress' missing."); - - strIPAddress = value; - } - - IPAddress oldIpAddress = IPAddress.Parse(strIPAddress); - - string strNewIPAddress = request.QueryString["newIpAddress"]; - if (string.IsNullOrEmpty(strNewIPAddress)) - { - if (string.IsNullOrEmpty(newValue)) - newValue = strIPAddress; - - strNewIPAddress = newValue; - } - IPAddress newIpAddress = IPAddress.Parse(strNewIPAddress); - - bool ptr = false; - string strPtr = request.QueryString["ptr"]; - if (!string.IsNullOrEmpty(strPtr)) - ptr = bool.Parse(strPtr); + IPAddress ipAddress = IPAddress.Parse(request.GetQueryOrFormAlt("ipAddress", "value")); + IPAddress newIpAddress = IPAddress.Parse(request.GetQueryOrFormAlt("newIpAddress", "newValue", ipAddress.ToString())); + bool ptr = request.GetQueryOrForm("ptr", bool.Parse, false); if (ptr) { - string ptrDomain = Zone.GetReverseZone(newIpAddress, type == DnsResourceRecordType.A ? 32 : 128); + string newPtrDomain = Zone.GetReverseZone(newIpAddress, type == DnsResourceRecordType.A ? 32 : 128); - AuthZoneInfo reverseZoneInfo = _dnsWebService.DnsServer.AuthZoneManager.FindAuthZoneInfo(ptrDomain); - if (reverseZoneInfo == null) + AuthZoneInfo newReverseZoneInfo = _dnsWebService.DnsServer.AuthZoneManager.FindAuthZoneInfo(newPtrDomain); + if (newReverseZoneInfo is null) { - bool createPtrZone = false; - string strCreatePtrZone = request.QueryString["createPtrZone"]; - if (!string.IsNullOrEmpty(strCreatePtrZone)) - createPtrZone = bool.Parse(strCreatePtrZone); - + bool createPtrZone = request.GetQueryOrForm("createPtrZone", bool.Parse, false); if (!createPtrZone) throw new DnsServerException("No reverse zone available to add PTR record."); string ptrZone = Zone.GetReverseZone(newIpAddress, type == DnsResourceRecordType.A ? 24 : 64); - reverseZoneInfo = _dnsWebService.DnsServer.AuthZoneManager.CreatePrimaryZone(ptrZone, _dnsWebService.DnsServer.ServerDomain, false); - if (reverseZoneInfo is null) + newReverseZoneInfo = _dnsWebService.DnsServer.AuthZoneManager.CreatePrimaryZone(ptrZone, _dnsWebService.DnsServer.ServerDomain, false); + if (newReverseZoneInfo is null) throw new DnsServerException("Failed to create reverse zone to add PTR record: " + ptrZone); //set permissions - _dnsWebService.AuthManager.SetPermission(PermissionSection.Zones, reverseZoneInfo.Name, session.User, PermissionFlag.ViewModifyDelete); - _dnsWebService.AuthManager.SetPermission(PermissionSection.Zones, reverseZoneInfo.Name, _dnsWebService.AuthManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete); - _dnsWebService.AuthManager.SetPermission(PermissionSection.Zones, reverseZoneInfo.Name, _dnsWebService.AuthManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.ViewModifyDelete); - _dnsWebService.AuthManager.SaveConfigFile(); + _dnsWebService._authManager.SetPermission(PermissionSection.Zones, newReverseZoneInfo.Name, session.User, PermissionFlag.ViewModifyDelete); + _dnsWebService._authManager.SetPermission(PermissionSection.Zones, newReverseZoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete); + _dnsWebService._authManager.SetPermission(PermissionSection.Zones, newReverseZoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.ViewModifyDelete); + _dnsWebService._authManager.SaveConfigFile(); } - if (reverseZoneInfo.Internal) - throw new DnsServerException("Reverse zone '" + reverseZoneInfo.Name + "' is an internal zone."); + if (newReverseZoneInfo.Internal) + throw new DnsServerException("Reverse zone '" + newReverseZoneInfo.Name + "' is an internal zone."); - if (reverseZoneInfo.Type != AuthZoneType.Primary) - throw new DnsServerException("Reverse zone '" + reverseZoneInfo.Name + "' is not a primary zone."); + if (newReverseZoneInfo.Type != AuthZoneType.Primary) + throw new DnsServerException("Reverse zone '" + newReverseZoneInfo.Name + "' is not a primary zone."); - string oldPtrDomain = Zone.GetReverseZone(oldIpAddress, type == DnsResourceRecordType.A ? 32 : 128); + string oldPtrDomain = Zone.GetReverseZone(ipAddress, type == DnsResourceRecordType.A ? 32 : 128); AuthZoneInfo oldReverseZoneInfo = _dnsWebService.DnsServer.AuthZoneManager.FindAuthZoneInfo(oldPtrDomain); if ((oldReverseZoneInfo != null) && !oldReverseZoneInfo.Internal && (oldReverseZoneInfo.Type == AuthZoneType.Primary)) @@ -3178,28 +2437,26 @@ namespace DnsServerCore } //add new PTR record and save reverse zone - _dnsWebService.DnsServer.AuthZoneManager.SetRecords(reverseZoneInfo.Name, ptrDomain, DnsResourceRecordType.PTR, ttl, new DnsPTRRecordData[] { new DnsPTRRecordData(domain) }); - _dnsWebService.DnsServer.AuthZoneManager.SaveZoneFile(reverseZoneInfo.Name); + _dnsWebService.DnsServer.AuthZoneManager.SetRecords(newReverseZoneInfo.Name, newPtrDomain, DnsResourceRecordType.PTR, ttl, new DnsPTRRecordData[] { new DnsPTRRecordData(domain) }); + _dnsWebService.DnsServer.AuthZoneManager.SaveZoneFile(newReverseZoneInfo.Name); } - DnsResourceRecord oldRecord; - if (type == DnsResourceRecordType.A) { - oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsARecordData(oldIpAddress)); + oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsARecordData(ipAddress)); newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsARecordData(newIpAddress)); } else { - oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsAAAARecordData(oldIpAddress)); + oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsAAAARecordData(ipAddress)); newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsAAAARecordData(newIpAddress)); } if (disable) - newRecord.Disable(); + newRecord.GetAuthRecordInfo().Disabled = true; if (!string.IsNullOrEmpty(comments)) - newRecord.SetComments(comments); + newRecord.GetAuthRecordInfo().Comments = comments; _dnsWebService.DnsServer.AuthZoneManager.UpdateRecord(zoneInfo.Name, oldRecord, newRecord); } @@ -3207,35 +2464,19 @@ namespace DnsServerCore case DnsResourceRecordType.NS: { - string nameServer = request.QueryString["nameServer"]; - if (string.IsNullOrEmpty(nameServer)) - { - if (string.IsNullOrEmpty(value)) - throw new DnsWebServiceException("Parameter 'nameServer' missing."); + string nameServer = request.GetQueryOrFormAlt("nameServer", "value").TrimEnd('.'); + string newNameServer = request.GetQueryOrFormAlt("newNameServer", "newValue", nameServer).TrimEnd('.'); - nameServer = value; - } - - string newNameServer = request.QueryString["newNameServer"]; - if (string.IsNullOrEmpty(newNameServer)) - { - if (string.IsNullOrEmpty(newValue)) - newValue = nameServer; - - newNameServer = newValue; - } - - DnsResourceRecord oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsNSRecordData(nameServer.TrimEnd('.'))); - newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsNSRecordData(newNameServer.TrimEnd('.'))); + oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsNSRecordData(nameServer)); + newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsNSRecordData(newNameServer)); if (disable) - newRecord.Disable(); + newRecord.GetAuthRecordInfo().Disabled = true; if (!string.IsNullOrEmpty(comments)) - newRecord.SetComments(comments); + newRecord.GetAuthRecordInfo().Comments = comments; - string glueAddresses = request.QueryString["glue"]; - if (!string.IsNullOrEmpty(glueAddresses)) + if (request.TryGetQueryOrForm("glue", out string glueAddresses)) newRecord.SetGlueRecords(glueAddresses); _dnsWebService.DnsServer.AuthZoneManager.UpdateRecord(zoneInfo.Name, oldRecord, newRecord); @@ -3244,23 +2485,16 @@ namespace DnsServerCore case DnsResourceRecordType.CNAME: { - string cname = request.QueryString["cname"]; - if (string.IsNullOrEmpty(cname)) - { - if (string.IsNullOrEmpty(value)) - throw new DnsWebServiceException("Parameter 'cname' missing."); + string cname = request.GetQueryOrFormAlt("cname", "value").TrimEnd('.'); - cname = value; - } - - DnsResourceRecord oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsCNAMERecordData(cname.TrimEnd('.'))); - newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsCNAMERecordData(cname.TrimEnd('.'))); + oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsCNAMERecordData(cname)); + newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsCNAMERecordData(cname)); if (disable) - newRecord.Disable(); + newRecord.GetAuthRecordInfo().Disabled = true; if (!string.IsNullOrEmpty(comments)) - newRecord.SetComments(comments); + newRecord.GetAuthRecordInfo().Comments = comments; _dnsWebService.DnsServer.AuthZoneManager.UpdateRecord(zoneInfo.Name, oldRecord, newRecord); } @@ -3268,64 +2502,68 @@ namespace DnsServerCore case DnsResourceRecordType.SOA: { - string primaryNameServer = request.QueryString["primaryNameServer"]; - if (string.IsNullOrEmpty(primaryNameServer)) - throw new DnsWebServiceException("Parameter 'primaryNameServer' missing."); + string primaryNameServer = request.GetQueryOrForm("primaryNameServer").TrimEnd('.'); + string responsiblePerson = request.GetQueryOrForm("responsiblePerson").TrimEnd('.'); + uint serial = request.GetQueryOrForm("serial", uint.Parse); + uint refresh = request.GetQueryOrForm("refresh", uint.Parse); + uint retry = request.GetQueryOrForm("retry", uint.Parse); + uint expire = request.GetQueryOrForm("expire", uint.Parse); + uint minimum = request.GetQueryOrForm("minimum", uint.Parse); - string responsiblePerson = request.QueryString["responsiblePerson"]; - if (string.IsNullOrEmpty(responsiblePerson)) - throw new DnsWebServiceException("Parameter 'responsiblePerson' missing."); - - string serial = request.QueryString["serial"]; - if (string.IsNullOrEmpty(serial)) - throw new DnsWebServiceException("Parameter 'serial' missing."); - - string refresh = request.QueryString["refresh"]; - if (string.IsNullOrEmpty(refresh)) - throw new DnsWebServiceException("Parameter 'refresh' missing."); - - string retry = request.QueryString["retry"]; - if (string.IsNullOrEmpty(retry)) - throw new DnsWebServiceException("Parameter 'retry' missing."); - - string expire = request.QueryString["expire"]; - if (string.IsNullOrEmpty(expire)) - throw new DnsWebServiceException("Parameter 'expire' missing."); - - string minimum = request.QueryString["minimum"]; - if (string.IsNullOrEmpty(minimum)) - throw new DnsWebServiceException("Parameter 'minimum' missing."); - - DnsResourceRecord newSOARecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsSOARecordData(primaryNameServer.TrimEnd('.'), responsiblePerson.TrimEnd('.'), uint.Parse(serial), uint.Parse(refresh), uint.Parse(retry), uint.Parse(expire), uint.Parse(minimum))); + DnsResourceRecord newSOARecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsSOARecordData(primaryNameServer, responsiblePerson, serial, refresh, retry, expire, minimum)); switch (zoneInfo.Type) { case AuthZoneType.Secondary: - case AuthZoneType.Stub: - string primaryAddresses = request.QueryString["primaryAddresses"]; - if (!string.IsNullOrEmpty(primaryAddresses)) - newSOARecord.SetPrimaryNameServers(primaryAddresses); + { + AuthRecordInfo recordInfo = newSOARecord.GetAuthRecordInfo(); + if (request.TryGetQueryOrFormEnum("zoneTransferProtocol", out DnsTransportProtocol zoneTransferProtocol)) + { + if (zoneTransferProtocol == DnsTransportProtocol.Quic) + DnsWebService.ValidateQuicSupport(); + + recordInfo.ZoneTransferProtocol = zoneTransferProtocol; + } + + if (request.TryGetQueryOrForm("primaryAddresses", out string primaryAddresses)) + { + recordInfo.PrimaryNameServers = primaryAddresses.Split(delegate (string address) + { + NameServerAddress nameServer = NameServerAddress.Parse(address); + + if (nameServer.Protocol != zoneTransferProtocol) + nameServer = nameServer.ChangeProtocol(zoneTransferProtocol); + + return nameServer; + }, ','); + } + + if (request.TryGetQueryOrForm("tsigKeyName", out string tsigKeyName)) + recordInfo.TsigKeyName = tsigKeyName; + } + break; + + case AuthZoneType.Stub: + { + if (request.TryGetQueryOrForm("primaryAddresses", out string primaryAddresses)) + { + newSOARecord.GetAuthRecordInfo().PrimaryNameServers = primaryAddresses.Split(delegate (string address) + { + NameServerAddress nameServer = NameServerAddress.Parse(address); + + if (nameServer.Protocol != DnsTransportProtocol.Udp) + nameServer = nameServer.ChangeProtocol(DnsTransportProtocol.Udp); + + return nameServer; + }, ','); + } + } break; } - if (zoneInfo.Type == AuthZoneType.Secondary) - { - DnsResourceRecordInfo recordInfo = newSOARecord.GetRecordInfo(); - - string zoneTransferProtocol = request.QueryString["zoneTransferProtocol"]; - if (string.IsNullOrEmpty(zoneTransferProtocol)) - recordInfo.ZoneTransferProtocol = DnsTransportProtocol.Tcp; - else - recordInfo.ZoneTransferProtocol = Enum.Parse(zoneTransferProtocol, true); - - string tsigKeyName = request.QueryString["tsigKeyName"]; - if (!string.IsNullOrEmpty(tsigKeyName)) - recordInfo.TsigKeyName = tsigKeyName; - } - if (!string.IsNullOrEmpty(comments)) - newSOARecord.SetComments(comments); + newSOARecord.GetAuthRecordInfo().Comments = comments; _dnsWebService.DnsServer.AuthZoneManager.SetRecord(zoneInfo.Name, newSOARecord); @@ -3335,32 +2573,17 @@ namespace DnsServerCore case DnsResourceRecordType.PTR: { - string ptrName = request.QueryString["ptrName"]; - if (string.IsNullOrEmpty(ptrName)) - { - if (string.IsNullOrEmpty(value)) - throw new DnsWebServiceException("Parameter 'ptrName' missing."); + string ptrName = request.GetQueryOrFormAlt("ptrName", "value").TrimEnd('.'); + string newPtrName = request.GetQueryOrFormAlt("newPtrName", "newValue", ptrName).TrimEnd('.'); - ptrName = value; - } - - string newPtrName = request.QueryString["newPtrName"]; - if (string.IsNullOrEmpty(newPtrName)) - { - if (string.IsNullOrEmpty(newValue)) - newValue = ptrName; - - newPtrName = newValue; - } - - DnsResourceRecord oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsPTRRecordData(ptrName.TrimEnd('.'))); - newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsPTRRecordData(newPtrName.TrimEnd('.'))); + oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsPTRRecordData(ptrName)); + newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsPTRRecordData(newPtrName)); if (disable) - newRecord.Disable(); + newRecord.GetAuthRecordInfo().Disabled = true; if (!string.IsNullOrEmpty(comments)) - newRecord.SetComments(comments); + newRecord.GetAuthRecordInfo().Comments = comments; _dnsWebService.DnsServer.AuthZoneManager.UpdateRecord(zoneInfo.Name, oldRecord, newRecord); } @@ -3368,40 +2591,20 @@ namespace DnsServerCore case DnsResourceRecordType.MX: { - string preference = request.QueryString["preference"]; - if (string.IsNullOrEmpty(preference)) - preference = "1"; + ushort preference = request.GetQueryOrForm("preference", ushort.Parse); + ushort newPreference = request.GetQueryOrForm("newPreference", ushort.Parse, preference); - string newPreference = request.QueryString["newPreference"]; - if (string.IsNullOrEmpty(newPreference)) - newPreference = preference; + string exchange = request.GetQueryOrFormAlt("exchange", "value").TrimEnd('.'); + string newExchange = request.GetQueryOrFormAlt("newExchange", "newValue", exchange).TrimEnd('.'); - string exchange = request.QueryString["exchange"]; - if (string.IsNullOrEmpty(exchange)) - { - if (string.IsNullOrEmpty(value)) - throw new DnsWebServiceException("Parameter 'exchange' missing."); - - exchange = value; - } - - string newExchange = request.QueryString["newExchange"]; - if (string.IsNullOrEmpty(newExchange)) - { - if (string.IsNullOrEmpty(newValue)) - newValue = exchange; - - newExchange = newValue; - } - - DnsResourceRecord oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsMXRecordData(ushort.Parse(preference), exchange.TrimEnd('.'))); - newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsMXRecordData(ushort.Parse(newPreference), newExchange.TrimEnd('.'))); + oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsMXRecordData(preference, exchange)); + newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsMXRecordData(newPreference, newExchange)); if (disable) - newRecord.Disable(); + newRecord.GetAuthRecordInfo().Disabled = true; if (!string.IsNullOrEmpty(comments)) - newRecord.SetComments(comments); + newRecord.GetAuthRecordInfo().Comments = comments; _dnsWebService.DnsServer.AuthZoneManager.UpdateRecord(zoneInfo.Name, oldRecord, newRecord); } @@ -3409,32 +2612,17 @@ namespace DnsServerCore case DnsResourceRecordType.TXT: { - string text = request.QueryString["text"]; - if (string.IsNullOrEmpty(text)) - { - if (string.IsNullOrEmpty(value)) - throw new DnsWebServiceException("Parameter 'text' missing."); + string text = request.GetQueryOrFormAlt("text", "value"); + string newText = request.GetQueryOrFormAlt("newText", "newValue", text); - text = value; - } - - string newText = request.QueryString["newText"]; - if (string.IsNullOrEmpty(newText)) - { - if (string.IsNullOrEmpty(newValue)) - newValue = text; - - newText = newValue; - } - - DnsResourceRecord oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsTXTRecordData(text)); + oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsTXTRecordData(text)); newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsTXTRecordData(newText)); if (disable) - newRecord.Disable(); + newRecord.GetAuthRecordInfo().Disabled = true; if (!string.IsNullOrEmpty(comments)) - newRecord.SetComments(comments); + newRecord.GetAuthRecordInfo().Comments = comments; _dnsWebService.DnsServer.AuthZoneManager.UpdateRecord(zoneInfo.Name, oldRecord, newRecord); } @@ -3442,56 +2630,26 @@ namespace DnsServerCore case DnsResourceRecordType.SRV: { - string priority = request.QueryString["priority"]; - if (string.IsNullOrEmpty(priority)) - throw new DnsWebServiceException("Parameter 'priority' missing."); + ushort priority = request.GetQueryOrForm("priority", ushort.Parse); + ushort newPriority = request.GetQueryOrForm("newPriority", ushort.Parse, priority); - string newPriority = request.QueryString["newPriority"]; - if (string.IsNullOrEmpty(newPriority)) - newPriority = priority; + ushort weight = request.GetQueryOrForm("weight", ushort.Parse); + ushort newWeight = request.GetQueryOrForm("newWeight", ushort.Parse, weight); - string weight = request.QueryString["weight"]; - if (string.IsNullOrEmpty(weight)) - throw new DnsWebServiceException("Parameter 'weight' missing."); + ushort port = request.GetQueryOrForm("port", ushort.Parse); + ushort newPort = request.GetQueryOrForm("newPort", ushort.Parse, port); - string newWeight = request.QueryString["newWeight"]; - if (string.IsNullOrEmpty(newWeight)) - newWeight = weight; + string target = request.GetQueryOrFormAlt("target", "value").TrimEnd('.'); + string newTarget = request.GetQueryOrFormAlt("newTarget", "newValue", target).TrimEnd('.'); - string port = request.QueryString["port"]; - if (string.IsNullOrEmpty(port)) - throw new DnsWebServiceException("Parameter 'port' missing."); - - string newPort = request.QueryString["newPort"]; - if (string.IsNullOrEmpty(newPort)) - newPort = port; - - string target = request.QueryString["target"]; - if (string.IsNullOrEmpty(target)) - { - if (string.IsNullOrEmpty(value)) - throw new DnsWebServiceException("Parameter 'target' missing."); - - target = value; - } - - string newTarget = request.QueryString["newTarget"]; - if (string.IsNullOrEmpty(newTarget)) - { - if (string.IsNullOrEmpty(newValue)) - newValue = target; - - newTarget = newValue; - } - - DnsResourceRecord oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsSRVRecordData(ushort.Parse(priority), ushort.Parse(weight), ushort.Parse(port), target.TrimEnd('.'))); - newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsSRVRecordData(ushort.Parse(newPriority), ushort.Parse(newWeight), ushort.Parse(newPort), newTarget.TrimEnd('.'))); + oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsSRVRecordData(priority, weight, port, target)); + newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsSRVRecordData(newPriority, newWeight, newPort, newTarget)); if (disable) - newRecord.Disable(); + newRecord.GetAuthRecordInfo().Disabled = true; if (!string.IsNullOrEmpty(comments)) - newRecord.SetComments(comments); + newRecord.GetAuthRecordInfo().Comments = comments; _dnsWebService.DnsServer.AuthZoneManager.UpdateRecord(zoneInfo.Name, oldRecord, newRecord); } @@ -3499,23 +2657,16 @@ namespace DnsServerCore case DnsResourceRecordType.DNAME: { - string dname = request.QueryString["dname"]; - if (string.IsNullOrEmpty(dname)) - { - if (string.IsNullOrEmpty(value)) - throw new DnsWebServiceException("Parameter 'dname' missing."); + string dname = request.GetQueryOrFormAlt("dname", "value").TrimEnd('.'); - dname = value; - } - - DnsResourceRecord oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsDNAMERecordData(dname.TrimEnd('.'))); - newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsDNAMERecordData(dname.TrimEnd('.'))); + oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsDNAMERecordData(dname)); + newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsDNAMERecordData(dname)); if (disable) - newRecord.Disable(); + newRecord.GetAuthRecordInfo().Disabled = true; if (!string.IsNullOrEmpty(comments)) - newRecord.SetComments(comments); + newRecord.GetAuthRecordInfo().Comments = comments; _dnsWebService.DnsServer.AuthZoneManager.UpdateRecord(zoneInfo.Name, oldRecord, newRecord); } @@ -3523,56 +2674,26 @@ namespace DnsServerCore case DnsResourceRecordType.DS: { - string strKeyTag = request.QueryString["keyTag"]; - if (string.IsNullOrEmpty(strKeyTag)) - throw new DnsWebServiceException("Parameter 'keyTag' missing."); + ushort keyTag = request.GetQueryOrForm("keyTag", ushort.Parse); + ushort newKeyTag = request.GetQueryOrForm("newKeyTag", ushort.Parse, keyTag); - string strNewKeyTag = request.QueryString["newKeyTag"]; - if (string.IsNullOrEmpty(strNewKeyTag)) - strNewKeyTag = strKeyTag; + DnssecAlgorithm algorithm = Enum.Parse(request.GetQueryOrForm("algorithm").Replace('-', '_'), true); + DnssecAlgorithm newAlgorithm = Enum.Parse(request.GetQueryOrForm("newAlgorithm", algorithm.ToString()).Replace('-', '_'), true); - string strAlgorithm = request.QueryString["algorithm"]; - if (string.IsNullOrEmpty(strAlgorithm)) - throw new DnsWebServiceException("Parameter 'algorithm' missing."); + DnssecDigestType digestType = Enum.Parse(request.GetQueryOrForm("digestType").Replace('-', '_'), true); + DnssecDigestType newDigestType = Enum.Parse(request.GetQueryOrForm("newDigestType", digestType.ToString()).Replace('-', '_'), true); - string strNewAlgorithm = request.QueryString["newAlgorithm"]; - if (string.IsNullOrEmpty(strNewAlgorithm)) - strNewAlgorithm = strAlgorithm; + byte[] digest = request.GetQueryOrFormAlt("digest", "value", Convert.FromHexString); + byte[] newDigest = request.GetQueryOrFormAlt("newDigest", "newValue", Convert.FromHexString, digest); - string strDigestType = request.QueryString["digestType"]; - if (string.IsNullOrEmpty(strDigestType)) - throw new DnsWebServiceException("Parameter 'digestType' missing."); - - string strNewDigestType = request.QueryString["newDigestType"]; - if (string.IsNullOrEmpty(strNewDigestType)) - strNewDigestType = strDigestType; - - string digest = request.QueryString["digest"]; - if (string.IsNullOrEmpty(digest)) - { - if (string.IsNullOrEmpty(value)) - throw new DnsWebServiceException("Parameter 'digest' missing."); - - digest = value; - } - - string newDigest = request.QueryString["newDigest"]; - if (string.IsNullOrEmpty(newDigest)) - { - if (string.IsNullOrEmpty(newValue)) - newValue = digest; - - newDigest = newValue; - } - - DnsResourceRecord oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsDSRecordData(ushort.Parse(strKeyTag), Enum.Parse(strAlgorithm.Replace('-', '_'), true), Enum.Parse(strDigestType.Replace('-', '_'), true), Convert.FromHexString(digest))); - newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsDSRecordData(ushort.Parse(strNewKeyTag), Enum.Parse(strNewAlgorithm.Replace('-', '_'), true), Enum.Parse(strNewDigestType.Replace('-', '_'), true), Convert.FromHexString(newDigest))); + oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsDSRecordData(keyTag, algorithm, digestType, digest)); + newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsDSRecordData(newKeyTag, newAlgorithm, newDigestType, newDigest)); if (disable) - newRecord.Disable(); + newRecord.GetAuthRecordInfo().Disabled = true; if (!string.IsNullOrEmpty(comments)) - newRecord.SetComments(comments); + newRecord.GetAuthRecordInfo().Comments = comments; _dnsWebService.DnsServer.AuthZoneManager.UpdateRecord(zoneInfo.Name, oldRecord, newRecord); } @@ -3580,38 +2701,23 @@ namespace DnsServerCore case DnsResourceRecordType.SSHFP: { - string strAlgorithm = request.QueryString["sshfpAlgorithm"]; - if (string.IsNullOrEmpty(strAlgorithm)) - throw new DnsWebServiceException("Parameter 'sshfpAlgorithm' missing."); + DnsSSHFPAlgorithm sshfpAlgorithm = request.GetQueryOrFormEnum("sshfpAlgorithm"); + DnsSSHFPAlgorithm newSshfpAlgorithm = request.GetQueryOrFormEnum("newSshfpAlgorithm", sshfpAlgorithm); - string strNewAlgorithm = request.QueryString["newSshfpAlgorithm"]; - if (string.IsNullOrEmpty(strNewAlgorithm)) - strNewAlgorithm = strAlgorithm; + DnsSSHFPFingerprintType sshfpFingerprintType = request.GetQueryOrFormEnum("sshfpFingerprintType"); + DnsSSHFPFingerprintType newSshfpFingerprintType = request.GetQueryOrFormEnum("newSshfpFingerprintType", sshfpFingerprintType); - string strFingerprintType = request.QueryString["sshfpFingerprintType"]; - if (string.IsNullOrEmpty(strFingerprintType)) - throw new DnsWebServiceException("Parameter 'sshfpFingerprintType' missing."); + byte[] sshfpFingerprint = request.GetQueryOrForm("sshfpFingerprint", Convert.FromHexString); + byte[] newSshfpFingerprint = request.GetQueryOrForm("newSshfpFingerprint", Convert.FromHexString, sshfpFingerprint); - string strNewFingerprintType = request.QueryString["newSshfpFingerprintType"]; - if (string.IsNullOrEmpty(strNewFingerprintType)) - strNewFingerprintType = strFingerprintType; - - string strFingerprint = request.QueryString["sshfpFingerprint"]; - if (string.IsNullOrEmpty(strFingerprint)) - throw new DnsWebServiceException("Parameter 'sshfpFingerprint' missing."); - - string strNewFingerprint = request.QueryString["newSshfpFingerprint"]; - if (string.IsNullOrEmpty(strNewFingerprint)) - strNewFingerprint = strFingerprint; - - DnsResourceRecord oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsSSHFPRecordData(Enum.Parse(strAlgorithm, true), Enum.Parse(strFingerprintType, true), Convert.FromHexString(strFingerprint))); - newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsSSHFPRecordData(Enum.Parse(strNewAlgorithm, true), Enum.Parse(strNewFingerprintType, true), Convert.FromHexString(strNewFingerprint))); + oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsSSHFPRecordData(sshfpAlgorithm, sshfpFingerprintType, sshfpFingerprint)); + newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsSSHFPRecordData(newSshfpAlgorithm, newSshfpFingerprintType, newSshfpFingerprint)); if (disable) - newRecord.Disable(); + newRecord.GetAuthRecordInfo().Disabled = true; if (!string.IsNullOrEmpty(comments)) - newRecord.SetComments(comments); + newRecord.GetAuthRecordInfo().Comments = comments; _dnsWebService.DnsServer.AuthZoneManager.UpdateRecord(zoneInfo.Name, oldRecord, newRecord); } @@ -3619,46 +2725,26 @@ namespace DnsServerCore case DnsResourceRecordType.TLSA: { - string strCertificateUsage = request.QueryString["tlsaCertificateUsage"]; - if (string.IsNullOrEmpty(strCertificateUsage)) - throw new DnsWebServiceException("Parameter 'tlsaCertificateUsage' missing."); + DnsTLSACertificateUsage tlsaCertificateUsage = Enum.Parse(request.GetQueryOrForm("tlsaCertificateUsage").Replace('-', '_'), true); + DnsTLSACertificateUsage newTlsaCertificateUsage = Enum.Parse(request.GetQueryOrForm("newTlsaCertificateUsage", tlsaCertificateUsage.ToString()).Replace('-', '_'), true); - string strNewCertificateUsage = request.QueryString["newTlsaCertificateUsage"]; - if (string.IsNullOrEmpty(strNewCertificateUsage)) - strNewCertificateUsage = strCertificateUsage; + DnsTLSASelector tlsaSelector = request.GetQueryOrFormEnum("tlsaSelector"); + DnsTLSASelector newTlsaSelector = request.GetQueryOrFormEnum("newTlsaSelector", tlsaSelector); - string strSelector = request.QueryString["tlsaSelector"]; - if (string.IsNullOrEmpty(strSelector)) - throw new DnsWebServiceException("Parameter 'tlsaSelector' missing."); + DnsTLSAMatchingType tlsaMatchingType = Enum.Parse(request.GetQueryOrForm("tlsaMatchingType").Replace('-', '_'), true); + DnsTLSAMatchingType newTlsaMatchingType = Enum.Parse(request.GetQueryOrForm("newTlsaMatchingType", tlsaMatchingType.ToString()).Replace('-', '_'), true); - string strNewSelector = request.QueryString["newTlsaSelector"]; - if (string.IsNullOrEmpty(strNewSelector)) - strNewSelector = strSelector; + string tlsaCertificateAssociationData = request.GetQueryOrForm("tlsaCertificateAssociationData"); + string newTlsaCertificateAssociationData = request.GetQueryOrForm("newTlsaCertificateAssociationData", tlsaCertificateAssociationData); - string strMatchingType = request.QueryString["tlsaMatchingType"]; - if (string.IsNullOrEmpty(strMatchingType)) - throw new DnsWebServiceException("Parameter 'tlsaMatchingType' missing."); - - string strNewMatchingType = request.QueryString["newTlsaMatchingType"]; - if (string.IsNullOrEmpty(strNewMatchingType)) - strNewMatchingType = strMatchingType; - - string strCertificateAssociationData = request.QueryString["tlsaCertificateAssociationData"]; - if (string.IsNullOrEmpty(strCertificateAssociationData)) - throw new DnsWebServiceException("Parameter 'tlsaCertificateAssociationData' missing."); - - string strNewCertificateAssociationData = request.QueryString["newTlsaCertificateAssociationData"]; - if (string.IsNullOrEmpty(strNewCertificateAssociationData)) - strNewCertificateAssociationData = strCertificateAssociationData; - - DnsResourceRecord oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsTLSARecordData(Enum.Parse(strCertificateUsage.Replace('-', '_'), true), Enum.Parse(strSelector, true), Enum.Parse(strMatchingType.Replace('-', '_'), true), strCertificateAssociationData)); - newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsTLSARecordData(Enum.Parse(strNewCertificateUsage.Replace('-', '_'), true), Enum.Parse(strNewSelector, true), Enum.Parse(strNewMatchingType.Replace('-', '_'), true), strNewCertificateAssociationData)); + oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsTLSARecordData(tlsaCertificateUsage, tlsaSelector, tlsaMatchingType, tlsaCertificateAssociationData)); + newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsTLSARecordData(newTlsaCertificateUsage, newTlsaSelector, newTlsaMatchingType, newTlsaCertificateAssociationData)); if (disable) - newRecord.Disable(); + newRecord.GetAuthRecordInfo().Disabled = true; if (!string.IsNullOrEmpty(comments)) - newRecord.SetComments(comments); + newRecord.GetAuthRecordInfo().Comments = comments; _dnsWebService.DnsServer.AuthZoneManager.UpdateRecord(zoneInfo.Name, oldRecord, newRecord); } @@ -3666,36 +2752,23 @@ namespace DnsServerCore case DnsResourceRecordType.CAA: { - string flags = request.QueryString["flags"]; - if (string.IsNullOrEmpty(flags)) - throw new DnsWebServiceException("Parameter 'flags' missing."); + byte flags = request.GetQueryOrForm("flags", byte.Parse); + byte newFlags = request.GetQueryOrForm("newFlags", byte.Parse, flags); - string newFlags = request.QueryString["newFlags"]; - if (string.IsNullOrEmpty(newFlags)) - newFlags = flags; + string tag = request.GetQueryOrForm("tag"); + string newTag = request.GetQueryOrForm("newTag", tag); - string tag = request.QueryString["tag"]; - if (string.IsNullOrEmpty(tag)) - throw new DnsWebServiceException("Parameter 'tag' missing."); + string value = request.GetQueryOrForm("value"); + string newValue = request.GetQueryOrForm("newValue", value); - string newTag = request.QueryString["newTag"]; - if (string.IsNullOrEmpty(newTag)) - newTag = tag; - - if (string.IsNullOrEmpty(value)) - throw new DnsWebServiceException("Parameter 'value' missing."); - - if (string.IsNullOrEmpty(newValue)) - newValue = value; - - DnsResourceRecord oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsCAARecordData(byte.Parse(flags), tag, value)); - newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsCAARecordData(byte.Parse(newFlags), newTag, newValue)); + oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsCAARecordData(flags, tag, value)); + newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsCAARecordData(newFlags, newTag, newValue)); if (disable) - newRecord.Disable(); + newRecord.GetAuthRecordInfo().Disabled = true; if (!string.IsNullOrEmpty(comments)) - newRecord.SetComments(comments); + newRecord.GetAuthRecordInfo().Comments = comments; _dnsWebService.DnsServer.AuthZoneManager.UpdateRecord(zoneInfo.Name, oldRecord, newRecord); } @@ -3703,32 +2776,17 @@ namespace DnsServerCore case DnsResourceRecordType.ANAME: { - string aname = request.QueryString["aname"]; - if (string.IsNullOrEmpty(aname)) - { - if (string.IsNullOrEmpty(value)) - throw new DnsWebServiceException("Parameter 'aname' missing."); + string aname = request.GetQueryOrFormAlt("aname", "value").TrimEnd('.'); + string newAName = request.GetQueryOrFormAlt("newAName", "newValue", aname).TrimEnd('.'); - aname = value; - } - - string newAName = request.QueryString["newAName"]; - if (string.IsNullOrEmpty(newAName)) - { - if (string.IsNullOrEmpty(newValue)) - newValue = aname; - - newAName = newValue; - } - - DnsResourceRecord oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsANAMERecordData(aname.TrimEnd('.'))); - newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsANAMERecordData(newAName.TrimEnd('.'))); + oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsANAMERecordData(aname)); + newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsANAMERecordData(newAName)); if (disable) - newRecord.Disable(); + newRecord.GetAuthRecordInfo().Disabled = true; if (!string.IsNullOrEmpty(comments)) - newRecord.SetComments(comments); + newRecord.GetAuthRecordInfo().Comments = comments; _dnsWebService.DnsServer.AuthZoneManager.UpdateRecord(zoneInfo.Name, oldRecord, newRecord); } @@ -3736,38 +2794,13 @@ namespace DnsServerCore case DnsResourceRecordType.FWD: { - DnsTransportProtocol protocol = DnsTransportProtocol.Udp; - string strProtocol = request.QueryString["protocol"]; - if (!string.IsNullOrEmpty(strProtocol)) - protocol = Enum.Parse(strProtocol, true); + DnsTransportProtocol protocol = request.GetQueryOrFormEnum("protocol", DnsTransportProtocol.Udp); + DnsTransportProtocol newProtocol = request.GetQueryOrFormEnum("newProtocol", protocol); - DnsTransportProtocol newProtocol = protocol; - string strNewProtocol = request.QueryString["newProtocol"]; - if (!string.IsNullOrEmpty(strNewProtocol)) - newProtocol = Enum.Parse(strNewProtocol, true); + string forwarder = request.GetQueryOrFormAlt("forwarder", "value"); + string newForwarder = request.GetQueryOrFormAlt("newForwarder", "newValue", forwarder); - string forwarder = request.QueryString["forwarder"]; - if (string.IsNullOrEmpty(forwarder)) - { - if (string.IsNullOrEmpty(value)) - throw new DnsWebServiceException("Parameter 'forwarder' missing."); - - forwarder = value; - } - - string newForwarder = request.QueryString["newForwarder"]; - if (string.IsNullOrEmpty(newForwarder)) - { - if (string.IsNullOrEmpty(newValue)) - newValue = forwarder; - - newForwarder = newValue; - } - - bool dnssecValidation = false; - string strDnssecValidation = request.QueryString["dnssecValidation"]; - if (!string.IsNullOrEmpty(strDnssecValidation)) - dnssecValidation = bool.Parse(strDnssecValidation); + bool dnssecValidation = request.GetQueryOrForm("dnssecValidation", bool.Parse, false); NetProxyType proxyType = NetProxyType.None; string proxyAddress = null; @@ -3777,34 +2810,35 @@ namespace DnsServerCore if (!newForwarder.Equals("this-server")) { - string strProxyType = request.QueryString["proxyType"]; - if (!string.IsNullOrEmpty(strProxyType)) - proxyType = Enum.Parse(strProxyType, true); - + proxyType = request.GetQueryOrFormEnum("proxyType", NetProxyType.None); if (proxyType != NetProxyType.None) { - proxyAddress = request.QueryString["proxyAddress"]; - if (string.IsNullOrEmpty(proxyAddress)) - throw new DnsWebServiceException("Parameter 'proxyAddress' missing."); - - string strProxyPort = request.QueryString["proxyPort"]; - if (string.IsNullOrEmpty(strProxyPort)) - throw new DnsWebServiceException("Parameter 'proxyPort' missing."); - - proxyPort = ushort.Parse(strProxyPort); - proxyUsername = request.QueryString["proxyUsername"]; - proxyPassword = request.QueryString["proxyPassword"]; + proxyAddress = request.GetQueryOrForm("proxyAddress"); + proxyPort = request.GetQueryOrForm("proxyPort", ushort.Parse); + proxyUsername = request.QueryOrForm("proxyUsername"); + proxyPassword = request.QueryOrForm("proxyPassword"); } } - DnsResourceRecord oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsForwarderRecordData(protocol, forwarder)); + switch (newProtocol) + { + case DnsTransportProtocol.HttpsJson: + newProtocol = DnsTransportProtocol.Https; + break; + + case DnsTransportProtocol.Quic: + DnsWebService.ValidateQuicSupport(); + break; + } + + oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsForwarderRecordData(protocol, forwarder)); newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsForwarderRecordData(newProtocol, newForwarder, dnssecValidation, proxyType, proxyAddress, proxyPort, proxyUsername, proxyPassword)); if (disable) - newRecord.Disable(); + newRecord.GetAuthRecordInfo().Disabled = true; if (!string.IsNullOrEmpty(comments)) - newRecord.SetComments(comments); + newRecord.GetAuthRecordInfo().Comments = comments; _dnsWebService.DnsServer.AuthZoneManager.UpdateRecord(zoneInfo.Name, oldRecord, newRecord); } @@ -3812,31 +2846,18 @@ namespace DnsServerCore case DnsResourceRecordType.APP: { - string appName = request.QueryString["appName"]; - if (string.IsNullOrEmpty(appName)) - { - if (string.IsNullOrEmpty(value)) - throw new DnsWebServiceException("Parameter 'appName' missing."); + string appName = request.GetQueryOrFormAlt("appName", "value"); + string classPath = request.GetQueryOrForm("classPath"); + string recordData = request.GetQueryOrForm("recordData", ""); - appName = value; - } - - string classPath = request.QueryString["classPath"]; - if (string.IsNullOrEmpty(classPath)) - throw new DnsWebServiceException("Parameter 'classPath' missing."); - - string recordData = request.QueryString["recordData"]; - if (string.IsNullOrEmpty(recordData)) - recordData = ""; - - DnsResourceRecord oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsApplicationRecordData(appName, classPath, recordData)); + oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsApplicationRecordData(appName, classPath, recordData)); newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsApplicationRecordData(appName, classPath, recordData)); if (disable) - newRecord.Disable(); + newRecord.GetAuthRecordInfo().Disabled = true; if (!string.IsNullOrEmpty(comments)) - newRecord.SetComments(comments); + newRecord.GetAuthRecordInfo().Comments = comments; _dnsWebService.DnsServer.AuthZoneManager.UpdateRecord(zoneInfo.Name, oldRecord, newRecord); } @@ -3846,10 +2867,12 @@ namespace DnsServerCore throw new DnsWebServiceException("Type not supported for UpdateRecords()."); } - _dnsWebService.Log.Write(DnsWebService.GetRequestRemoteEndPoint(request), "[" + session.User.Username + "] Record was updated for authoritative zone {oldDomain: " + domain + "; domain: " + newDomain + "; type: " + type + "; oldValue: " + value + "; value: " + newValue + "; ttl: " + ttl + "; disabled: " + disable + ";}"); + _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] Record was updated for authoritative zone {" + (oldRecord is null ? "" : "oldRecord: " + oldRecord.ToString() + "; ") + "newRecord: " + newRecord.ToString() + "}"); _dnsWebService.DnsServer.AuthZoneManager.SaveZoneFile(zoneInfo.Name); + Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); + jsonWriter.WritePropertyName("zone"); WriteZoneInfoAsJson(zoneInfo, jsonWriter); diff --git a/DnsServerCore/dohwww/js/jquery.min.js b/DnsServerCore/dohwww/js/jquery.min.js index c4c6022f..b5329e9a 100644 --- a/DnsServerCore/dohwww/js/jquery.min.js +++ b/DnsServerCore/dohwww/js/jquery.min.js @@ -1,2 +1,2 @@ -/*! jQuery v3.6.0 | (c) OpenJS Foundation and other contributors | jquery.org/license */ -!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.0",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var _t,zt=[],Ut=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=zt.pop()||S.expando+"_"+wt.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Ut.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ut.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Ut,"$1"+r):!1!==e.jsonp&&(e.url+=(Tt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,zt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((_t=E.implementation.createHTMLDocument("").body).innerHTML="
",2===_t.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=Fe(y.pixelPosition,function(e,t){if(t)return t=We(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,S)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&v(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!y||!y.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ve(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=E)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{if(d.cssSupportsSelector&&!CSS.supports("selector(:is("+c+"))"))throw new Error;return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===E&&e.removeAttribute("id")}}}return g(t.replace(B,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[E]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ye(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ve(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,S=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.cssSupportsSelector=ce(function(){return CSS.supports("selector(*)")&&C.querySelectorAll(":is(:jqfake)")&&!CSS.supports("selector(:is(*,:jqfake))")}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=E,!C.getElementsByName||!C.getElementsByName(E).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&S){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&S){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&S)return t.getElementsByClassName(e)},s=[],y=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&y.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||y.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+E+"-]").length||y.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||y.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||y.push(":checked"),e.querySelectorAll("a#"+E+"+*").length||y.push(".#.+[+~]"),e.querySelectorAll("\\\f"),y.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&y.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&y.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&y.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),y.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),d.cssSupportsSelector||y.push(":has"),y=y.length&&new RegExp(y.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),v=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType&&e.documentElement||e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&v(p,e)?-1:t==C||t.ownerDocument==p&&v(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&S&&!N[t+" "]&&(!s||!s.test(t))&&(!y||!y.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?E.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?E.grep(e,function(e){return e===n!==r}):"string"!=typeof n?E.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(E.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof E?t[0]:t,E.merge(this,E.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:S,!0)),N.test(r[1])&&E.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=S.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(E):E.makeArray(e,this)}).prototype=E.fn,D=E(S);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}E.fn.extend({has:function(e){var t=E(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=S.createDocumentFragment().appendChild(S.createElement("div")),(fe=S.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),v.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",v.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",v.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ye(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?E.merge([e],n):n}function ve(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&E(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),S.head.appendChild(r[0])},abort:function(){i&&i()}}});var Ut,Xt=[],Vt=/(=)\?(?=&|$)|\?\?/;E.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Xt.pop()||E.expando+"_"+Ct.guid++;return this[e]=!0,e}}),E.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Vt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Vt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Vt,"$1"+r):!1!==e.jsonp&&(e.url+=(St.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||E.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?E(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Xt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),v.createHTMLDocument=((Ut=S.implementation.createHTMLDocument("").body).innerHTML="
",2===Ut.childNodes.length),E.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(v.createHTMLDocument?((r=(t=S.implementation.createHTMLDocument("")).createElement("base")).href=S.location.href,t.head.appendChild(r)):t=S),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&E(o).remove(),E.merge([],i.childNodes)));var r,i,o},E.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(E.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},E.expr.pseudos.animated=function(t){return E.grep(E.timers,function(e){return t===e.elem}).length},E.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=E.css(e,"position"),c=E(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=E.css(e,"top"),u=E.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,E.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},E.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){E.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===E.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===E.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=E(e).offset()).top+=E.css(e,"borderTopWidth",!0),i.left+=E.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-E.css(r,"marginTop",!0),left:t.left-i.left-E.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===E.css(e,"position"))e=e.offsetParent;return e||re})}}),E.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;E.fn[t]=function(e){return B(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),E.each(["top","left"],function(e,n){E.cssHooks[n]=_e(v.pixelPosition,function(e,t){if(t)return t=Be(e,n),Pe.test(t)?E(e).position()[n]+"px":t})}),E.each({Height:"height",Width:"width"},function(a,s){E.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){E.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return B(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?E.css(e,t,i):E.style(e,t,n,i)},s,n?e:void 0,n)}})}),E.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){E.fn[t]=function(e){return this.on(t,e)}}),E.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),E.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){E.fn[n]=function(e,t){return 0 - - - - - @@ -145,8 +140,8 @@ @@ -336,31 +331,93 @@
-
+
-
+
+
+
+
+ + +
+
+
+
+ + +
+
+ + +
+ +
+
+
+ +
+
+ 0 zones +
+
+ +
+
+
+ - - - - - + + + + + + - - - - + + + + +
ZoneTypeDNSSECStatusExpiry#ZoneTypeDNSSECStatusExpiry
Total Records: 0
+
+
+ 0 zones +
+
+ +
+
+
+
@@ -402,20 +459,77 @@
+
+
+
+ + +
+
+ + +
+ +
+
+
+ +
+
+ 0 records +
+
+ +
+
+
+ - - - - + + + + + - - + + + +
NameTypeTTLData#NameTypeTTLData
Total Records: 0
+
+
+ 0 records +
+
+ +
+
+
+
@@ -550,7 +664,7 @@ - + @@ -569,7 +683,7 @@
Note! The DNS Server local end point changes will be automatically applied and so you do not need to manually restart the main service.
@@ -932,6 +1063,33 @@
The amount of time a TCP socket must wait for data before closing the connection. This option will apply for DNS requests being received by the DNS Server over TCP, TLS, or HTTPS transports.
+ +
+ +
+ + milliseconds (valid range 1000-90000; default 60000) +
+
The time interval after which an idle QUIC connection will be closed. This option applies only to QUIC transport protocol.
+
+ +
+ +
+ + (valid range 1-1000; default 100) +
+
The max number of inbound bidirectional streams that can be accepted per QUIC connection. This option applies only to QUIC transport protocol.
+
+ +
+ +
+ + (default 100) +
+
The maximum number of pending connections. This option applies to TCP, TLS, and QUIC transport protocols.
+
@@ -942,13 +1100,14 @@
-
Local addresses are the network interface IP addresses you want the web service to listen for requests. The default values work for most scenarios so, do not change these defaults unless you have a requirement for the web service to listen on specific networks.
+
Local addresses are the network interface IP addresses you want the web service to listen for requests. ANY addresses (0.0.0.0 & [::]) cannot be used together with unicast IP addresses. The default values work for most scenarios so, do not change these defaults unless you have a requirement for the web service to listen on specific networks.
- + + (default 5380)
Specify the TCP port number for this web console over HTTP protocol.
@@ -979,7 +1138,8 @@
- + + (default 53443)
Specify the TCP port number for this web console over TLS protocol.
@@ -1003,6 +1163,7 @@

Note! The web service port changes will be automatically applied and so you do not need to manually restart the main service. This web page will be automatically redirected to the new web console URL after saving settings. The HTTPS protocol will be enabled only when a TLS certificate is configured.

When using a reverse proxy with the Web Service, you need to add X-Real-IP header to the proxy request with the IP address of the client to allow the Web server to know the real IP address of the client originating the request. For example, if you are using nginx as the reverse proxy, you can add proxy_set_header X-Real-IP $remote_addr; to make it work.

+

The web service uses Kestral web server which supports both HTTP/2 and HTTP/3 protocols when TLS certificate is configured. HTTP/3 protocol support is not available on all platforms. On Windows, it is available only on Windows 11 (build 22000 or later) and Windows Server 2022. On Linux, it requires libmsquic and openssl v1.1.1 to be installed.

Use the following openssl command to convert your TLS certificate that is in PEM format to PKCS #12 certificate (.pfx) format:

openssl pkcs12 -export -out "example.com.pfx" -inkey "privkey.pem" -in "cert.pem" -certfile "chain.pem"
@@ -1016,27 +1177,70 @@
-
Enable this option to accept DNS-over-HTTP requests for both wire and json response formats. It must be used with a TLS terminating reverse proxy like nginx and will work only on private networks.
+
Enable this option to accept DNS-over-HTTP requests. It must be used with a TLS terminating reverse proxy like nginx and will work only on private networks. Enabling this option also allows automatic TLS certificate renewal with HTTP challenge (webroot) for DNS-over-HTTPS service when DNS-over-HTTP port is set to 80.
Enable this option to accept DNS-over-TLS requests.
-
Enable this option to accept DNS-over-HTTPS requests for both wire and json response formats.
+
Enable this option to accept DNS-over-HTTPS requests.
+ +
+ +
+
Enable this option to accept DNS-over-QUIC requests.
+
+ +
+ + (default 80) +
+
Specify the TCP port number for DNS-over-HTTP protocol.
+
+ +
+ +
+ + (default 853) +
+
Specify the TCP port number for DNS-over-TLS protocol.
+
+ +
+ +
+ + (default 443) +
+
Specify the TCP port number for DNS-over-HTTPS protocol.
+
+ +
+ +
+ + (default 853) +
+
Specify the UDP port number for DNS-over-QUIC protocol.
+
+
@@ -1054,10 +1258,11 @@
-

Note! These optional DNS server protocol changes will be automatically applied and so you do not need to manually restart the main service. The DNS-over-TLS and DNS-over-HTTPS protocols will be enabled only when a TLS certificate is configured.

+

Note! These optional DNS server protocol changes will be automatically applied and so you do not need to manually restart the main service. The DNS-over-TLS, DNS-over-QUIC, and DNS-over-HTTPS protocols will be enabled only when a TLS certificate is configured.

These optional DNS server protocols are used to host these as a service. You do not need to enable these optional protocols to use them with Forwarders or Conditional Forwarder Zones.

-

For DNS-over-HTTP, use http://localhost:8053/dns-query with a TLS terminating reverse proxy like nginx. For DNS-over-TLS, use tls-certificate-domain:853 and for DNS-over-HTTPS use https://tls-certificate-domain/dns-query to configure supported DNS clients.

+

For DNS-over-HTTP, use http://localhost:8053/dns-query with a TLS terminating reverse proxy like nginx. For DNS-over-TLS, use tls-certificate-domain:853, for DNS-over-QUIC, use tls-certificate-domain:853, and for DNS-over-HTTPS use https://tls-certificate-domain/dns-query to configure supported DNS clients.

When using a reverse proxy with the DNS-over-HTTP service, you need to add X-Real-IP header to the proxy request with the IP address of the client to allow the DNS server to know the real IP address of the client originating the request. For example, if you are using nginx as the reverse proxy, you can add proxy_set_header X-Real-IP $remote_addr; to make it work.

+

DNS-over-QUIC protocol support is not available on all platforms. On Windows, it is available only on Windows 11 (build 22000 or later) and Windows Server 2022. On Linux, it requires libmsquic and openssl v1.1.1 to be installed.

Use the following openssl command to convert your TLS certificate that is in PEM format to PKCS #12 certificate (.pfx) format:

openssl pkcs12 -export -out "example.com.pfx" -inkey "privkey.pem" -in "cert.pem" -certfile "chain.pem"
@@ -1145,7 +1350,7 @@ Randomize Name
-
Enables QNAME randomization when using UDP as the transport protocol to improve security.
+
Enables QNAME case randomization when using UDP as the transport protocol to improve security.
+
+
+ +
+
+ +
+
Enable this option to save DNS cache on disk when the DNS server stops. The saved cache will be loaded next time the DNS server starts.
+
+
+ +
Note! The DNS server will attempt to save cache to disk when it stops which may take time depending on the cache size. If the DNS server takes a lot of time to stop then it may lead to the OS killing the DNS server process causing an incomplete cache to be stored on disk.
+
+
@@ -1323,7 +1544,7 @@ Allow TXT Blocking Report
-
Specifies if the DNS Server should respond with TXT records containing a blocked domain report for TXT type requests.
+
Specifies if the DNS Server should respond with TXT records containing a blocked domain report for TXT type requests. This option also enables Extended DNS Error blocked domain report in response for requests that support EDNS.
@@ -1347,14 +1568,14 @@
Uses 0.0.0.0 and :: IP addresses for blocked domain names.
Uses NX Domain response for blocked domain names.
@@ -1374,7 +1595,7 @@
- +
@@ -1403,10 +1624,8 @@ - - - - + +
@@ -1433,7 +1652,7 @@
Click the 'Update Now' button to reset the next update schedule and force download and update of the block lists.
-
DNS Server will use the data returned by the block list URLs to update the block list zone automatically. The expected file format is standard hosts file format or plain text file containing list of domains to block.
+
Note! DNS Server will use the data returned by the block list URLs to update the block list zone automatically. The expected file format is standard hosts file format, plain text file containing list of domains to block, wildcard block list file format, or Adblock Plus file format.
@@ -1530,7 +1749,6 @@ - @@ -1563,6 +1781,26 @@ + + + + + + + + + + + + + + + + + + + +
Enter forwarder DNS Server IP addresses or URLs one below another in above text field or use the Quick Select list to select desired forwarder.
@@ -1598,8 +1836,8 @@
@@ -1735,13 +1973,13 @@
Installed AppsInstalled Apps
- - - - - - - + + + + + + + @@ -1767,10 +2005,10 @@
ScopeMAC AddressIP AddressHost NameLease ObtainedLease ExpiresScopeMAC AddressIP AddressHost NameLease ObtainedLease Expires
- - - - + + + + @@ -1991,7 +2229,7 @@
-
The bootstrap TFTP server IP address to be used by the clients. If not specified, the DHCP server's IP address is used.
+
The IP address of next server (TFTP) to use in bootstrap by the clients. If not specified, the DHCP server's IP address is used. (siaddr)
@@ -1999,15 +2237,15 @@
-
The optional bootstrap TFTP server host name to be used by the clients to identify the TFTP server.
+
The optional bootstrap server host name to be used by the clients to identify the TFTP server. (sname/Option 66)
- +
-
The boot file name stored on the bootstrap TFTP server to be used by the clients.
+
The boot file name stored on the bootstrap TFTP server to be used by the clients. (file/Option 67)
@@ -2026,7 +2264,7 @@
NameScope Range/Subnet MaskNetwork/BroadcastInterfaceNameScope Range/Subnet MaskNetwork/BroadcastInterface
-
The vendor specific information option (43) to be sent to the clients that match the vendor class identifier option (60) in the request. The vendor class identifier can be empty string to match any identifier, or matched exactly, or match a substring, for example substring(vendor-class-identifier,0,9)=="PXEClient". The vendor specific information must be a colon (:) separated hex string, for example 06:01:03:0A:04:00:50:58:45:09:14:00:00:11:52:61:73:70:62:65:72:72:79:20:50:69:20:42:6F:6F:74:FF.
+
The Vendor Specific Information (option 43) to be sent to the clients that match the Vendor Class Identifier (option 60) in the request. The Cendor Class Identifier can be empty string to match any identifier, or matched exactly, or match a substring, for example substring(vendor-class-identifier,0,9)=="PXEClient". The Vendor Specific Information must be either a colon (:) separated hex string or a normal hex string, for example 06:01:03:0A:04:00:50:58:45:09:14:00:00:11:52:61:73:70:62:65:72:72:79:20:50:69:20:42:6F:6F:74:FF OR 0601030A0400505845091400001152617370626572727920506920426F6F74FF.
@@ -2040,6 +2278,35 @@ +
+
+ +
+ +
+
The TFTP Server Address or the VoIP Configuration Server Address. (Option 150)
+
+
+ +
+
+ +
+ + + + + + + + + +
CodeHex Value
+
+
This feature allows you to define DHCP options that are not yet directly supported. To add an option, use the DHCP option code defined for it and enter the value in either a colon (:) separated hex string or a normal hex string format, for example C0:A8:01:01 OR C0A80101.
+
+
+
@@ -2138,11 +2405,11 @@ - - - - - + + + + + @@ -2168,11 +2435,11 @@
UsernameSessionLast SeenRemote AddressUser AgentUsernameSessionLast SeenRemote AddressUser Agent
- - - - - + + + + + @@ -2198,8 +2465,8 @@
UsernameDisplay NameStatusPrevious LoginRecent LoginUsernameDisplay NameStatusRecent LoginPrevious Login
- - + + @@ -2219,7 +2486,7 @@
NameDescriptionNameDescription
- + @@ -2314,27 +2581,17 @@
-
- - - - -
+
-
- - - - -
+
- +
@@ -2345,7 +2602,7 @@ - +
@@ -2357,6 +2614,8 @@ + + @@ -2380,12 +2639,12 @@
- +
- +
@@ -2439,13 +2698,14 @@
+ -
SectionSection User Permissions Group Permissions Type Class Answer
+
Found: 0 logs @@ -2479,7 +2739,7 @@

Technitium DNS Server

Version

- Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com)
+ Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com)
This program comes with ABSOLUTELY NO WARRANTY. This is free software, and you are welcome to redistribute it under certain conditions.

Source code available under GNU General Public License v3.0 on  GitHub

@@ -2497,7 +2757,6 @@

For support, send an email to support@technitium.com.

Follow @technitium@mastodon.social on Mastodon.
- Follow @technitium on Twitter.
Checkout Technitium Blog.

@@ -2505,9 +2764,9 @@

Donate

-

Make a contribution to Technitium by becoming a Patron and help making new software, updates, and features possible.

+

Make a contribution to Technitium and help making new software, updates, and features possible.

- Become A Patron Now! + Donate Now!

@@ -2564,7 +2823,7 @@ - + @@ -2581,10 +2840,10 @@
GroupGroup
- - - - + + + + @@ -2859,6 +3118,12 @@ ns1.example.com ([2001:db8::]) XFR-over-TLS +
+ +
@@ -2900,8 +3165,8 @@ ns1.example.com ([2001:db8::])
@@ -3170,6 +3435,12 @@ ns1.example.com ([2001:db8::]) XFR-over-TLS +
+ +
@@ -3397,8 +3668,8 @@ MII...
@@ -3556,7 +3827,7 @@ MII...
@@ -3886,7 +4157,7 @@ MII...
- (valid range 0-32, recommended 0) + bytes (valid range 0-32, recommended 0)
The number of bytes of random salt to generate to be used with the NSEC3 hash computation. It is recommended to not use salt by setting the length to 0 [RFC 9276]. @@ -3898,7 +4169,7 @@ MII...
- (default 3600) + seconds (default 3600)
The TTL value to be used for DNSKEY records. A lower value will allow quicker addition or rollover to a new DNS Key at the cost of increased frequency of DNSKEY queries by resolvers. @@ -3976,13 +4247,13 @@ MII...
SessionLast SeenRemote AddressUser AgentSessionLast SeenRemote AddressUser Agent
- - - - - - - + + + + + + + @@ -4119,7 +4390,7 @@ MII...
- (valid range 0-32, recommended 0) + bytes (valid range 0-32, recommended 0)
The number of bytes of random salt to generate to be used with the NSEC3 hash computation. It is recommended to not use salt by setting the length to 0 [RFC 9276]. @@ -4140,7 +4411,7 @@ MII...
- (default 3600) + seconds (default 3600)
@@ -4232,7 +4503,7 @@ MII...
Key TagKey TypeAlgorithmStateState ChangedRolloverKey TagKey TypeAlgorithmStateState ChangedRollover (days)
- + @@ -4769,10 +5040,10 @@ MII...
Store AppsStore Apps
- - - - + + + + @@ -4897,10 +5168,10 @@ MII...
SessionLast SeenRemote AddressUser AgentSessionLast SeenRemote AddressUser Agent
- - - - + + + + @@ -4919,10 +5190,10 @@ MII...
UsernameViewModifyDeleteUsernameViewModifyDelete
- - - - + + + + diff --git a/DnsServerCore/www/js/apps.js b/DnsServerCore/www/js/apps.js index c601c4e3..c1ff2ef1 100644 --- a/DnsServerCore/www/js/apps.js +++ b/DnsServerCore/www/js/apps.js @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -344,6 +344,7 @@ function installApp() { url: "/api/apps/install?token=" + sessionData.token + "&name=" + encodeURIComponent(appName), method: "POST", data: formData, + dataContentType: false, processData: false, success: function (responseJSON) { $("#modalInstallApp").modal("hide"); @@ -385,6 +386,7 @@ function updateApp() { url: "/api/apps/update?token=" + sessionData.token + "&name=" + encodeURIComponent(appName), method: "POST", data: formData, + dataContentType: false, processData: false, success: function (responseJSON) { $("#modalUpdateApp").modal("hide"); diff --git a/DnsServerCore/www/js/auth.js b/DnsServerCore/www/js/auth.js index b77c9b45..9320c567 100644 --- a/DnsServerCore/www/js/auth.js +++ b/DnsServerCore/www/js/auth.js @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -804,8 +804,8 @@ function getAdminUsersRowHtml(id, user) { var tableHtmlRows = "").append(a("").append(a("").append(a("").append(a("
GroupViewModifyDeleteGroupViewModifyDelete
" + htmlEncode(user.username) + "" + htmlEncode(user.displayName) + "" + status + "" + - htmlEncode(moment(user.previousSessionLoggedOn).local().format("YYYY-MM-DD HH:mm:ss")) + " from " + htmlEncode(user.previousSessionRemoteAddress) + "" + - htmlEncode(moment(user.recentSessionLoggedOn).local().format("YYYY-MM-DD HH:mm:ss")) + " from " + htmlEncode(user.recentSessionRemoteAddress); + htmlEncode(moment(user.recentSessionLoggedOn).local().format("YYYY-MM-DD HH:mm:ss")) + " from " + htmlEncode(user.recentSessionRemoteAddress) + "" + + htmlEncode(moment(user.previousSessionLoggedOn).local().format("YYYY-MM-DD HH:mm:ss")) + " from " + htmlEncode(user.previousSessionRemoteAddress); tableHtmlRows += "
    "; tableHtmlRows += "
  • View Details
  • "; diff --git a/DnsServerCore/www/js/bootstrap-datetimepicker.min.js b/DnsServerCore/www/js/bootstrap-datetimepicker.min.js deleted file mode 100644 index a953fe0c..00000000 --- a/DnsServerCore/www/js/bootstrap-datetimepicker.min.js +++ /dev/null @@ -1,214 +0,0 @@ -/*! version : 4.17.42 - ========================================================= - bootstrap-datetimejs - https://github.com/Eonasdan/bootstrap-datetimepicker - Copyright (c) 2015 Jonathan Peterson - ========================================================= - */ -/* - The MIT License (MIT) - - Copyright (c) 2015 Jonathan Peterson - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. - */ -/*global define:false */ -/*global exports:false */ -/*global require:false */ -/*global jQuery:false */ -/*global moment:false */ -!function(a){"use strict";if("function"==typeof define&&define.amd) -// AMD is used - Register as an anonymous module. -define(["jquery","moment"],a);else if("object"==typeof exports)a(require("jquery"),require("moment"));else{ -// Neither AMD nor CommonJS used. Use global variables. -if("undefined"==typeof jQuery)throw"bootstrap-datetimepicker requires jQuery to be loaded first";if("undefined"==typeof moment)throw"bootstrap-datetimepicker requires Moment.js to be loaded first";a(jQuery,moment)}}(function(a,b){"use strict";if(!b)throw new Error("bootstrap-datetimepicker requires Moment.js to be loaded first");var c=function(c,d){var e,f,g,h,i,j,k,l={},m=!0,n=!1,o=!1,p=0,q=[{clsName:"days",navFnc:"M",navStep:1},{clsName:"months",navFnc:"y",navStep:1},{clsName:"years",navFnc:"y",navStep:10},{clsName:"decades",navFnc:"y",navStep:100}],r=["days","months","years","decades"],s=["top","bottom","auto"],t=["left","right","auto"],u=["default","top","bottom"],v={up:38,38:"up",down:40,40:"down",left:37,37:"left",right:39,39:"right",tab:9,9:"tab",escape:27,27:"escape",enter:13,13:"enter",pageUp:33,33:"pageUp",pageDown:34,34:"pageDown",shift:16,16:"shift",control:17,17:"control",space:32,32:"space",t:84,84:"t","delete":46,46:"delete"},w={},/******************************************************************************** - * - * Private functions - * - ********************************************************************************/ -x=function(){return void 0!==b.tz&&void 0!==d.timeZone&&null!==d.timeZone&&""!==d.timeZone},y=function(a){var c;return c=void 0===a||null===a?b():x()?b.tz(a,j,d.useStrict,d.timeZone):b(a,j,d.useStrict),x()&&c.tz(d.timeZone),c},z=function(a){if("string"!=typeof a||a.length>1)throw new TypeError("isEnabled expects a single character string parameter");switch(a){case"y":return-1!==i.indexOf("Y");case"M":return-1!==i.indexOf("M");case"d":return-1!==i.toLowerCase().indexOf("d");case"h":case"H":return-1!==i.toLowerCase().indexOf("h");case"m":return-1!==i.indexOf("m");case"s":return-1!==i.indexOf("s");default:return!1}},A=function(){return z("h")||z("m")||z("s")},B=function(){return z("y")||z("M")||z("d")},C=function(){var b=a("
").addClass("prev").attr("data-action","previous").append(a("").addClass(d.icons.previous))).append(a("").addClass("picker-switch").attr("data-action","pickerSwitch").attr("colspan",d.calendarWeeks?"6":"5")).append(a("").addClass("next").attr("data-action","next").append(a("").addClass(d.icons.next)))),c=a("
").attr("colspan",d.calendarWeeks?"8":"7")));return[a("
").addClass("datepicker-days").append(a("").addClass("table-condensed").append(b).append(a(""))),a("
").addClass("datepicker-months").append(a("
").addClass("table-condensed").append(b.clone()).append(c.clone())),a("
").addClass("datepicker-years").append(a("
").addClass("table-condensed").append(b.clone()).append(c.clone())),a("
").addClass("datepicker-decades").append(a("
").addClass("table-condensed").append(b.clone()).append(c.clone()))]},D=function(){var b=a(""),c=a(""),e=a("");return z("h")&&(b.append(a("
").append(a("").attr({href:"#",tabindex:"-1",title:d.tooltips.incrementHour}).addClass("btn").attr("data-action","incrementHours").append(a("").addClass(d.icons.up)))),c.append(a("").append(a("").addClass("timepicker-hour").attr({"data-time-component":"hours",title:d.tooltips.pickHour}).attr("data-action","showHours"))),e.append(a("").append(a("").attr({href:"#",tabindex:"-1",title:d.tooltips.decrementHour}).addClass("btn").attr("data-action","decrementHours").append(a("").addClass(d.icons.down))))),z("m")&&(z("h")&&(b.append(a("").addClass("separator")),c.append(a("").addClass("separator").html(":")),e.append(a("").addClass("separator"))),b.append(a("").append(a("").attr({href:"#",tabindex:"-1",title:d.tooltips.incrementMinute}).addClass("btn").attr("data-action","incrementMinutes").append(a("").addClass(d.icons.up)))),c.append(a("").append(a("").addClass("timepicker-minute").attr({"data-time-component":"minutes",title:d.tooltips.pickMinute}).attr("data-action","showMinutes"))),e.append(a("").append(a("").attr({href:"#",tabindex:"-1",title:d.tooltips.decrementMinute}).addClass("btn").attr("data-action","decrementMinutes").append(a("").addClass(d.icons.down))))),z("s")&&(z("m")&&(b.append(a("").addClass("separator")),c.append(a("").addClass("separator").html(":")),e.append(a("").addClass("separator"))),b.append(a("").append(a("").attr({href:"#",tabindex:"-1",title:d.tooltips.incrementSecond}).addClass("btn").attr("data-action","incrementSeconds").append(a("").addClass(d.icons.up)))),c.append(a("").append(a("").addClass("timepicker-second").attr({"data-time-component":"seconds",title:d.tooltips.pickSecond}).attr("data-action","showSeconds"))),e.append(a("").append(a("").attr({href:"#",tabindex:"-1",title:d.tooltips.decrementSecond}).addClass("btn").attr("data-action","decrementSeconds").append(a("").addClass(d.icons.down))))),h||(b.append(a("").addClass("separator")),c.append(a("").append(a("").addClass("separator"))),a("
").addClass("timepicker-picker").append(a("").addClass("table-condensed").append([b,c,e]))},E=function(){var b=a("
").addClass("timepicker-hours").append(a("
").addClass("table-condensed")),c=a("
").addClass("timepicker-minutes").append(a("
").addClass("table-condensed")),d=a("
").addClass("timepicker-seconds").append(a("
").addClass("table-condensed")),e=[D()];return z("h")&&e.push(b),z("m")&&e.push(c),z("s")&&e.push(d),e},F=function(){var b=[];return d.showTodayButton&&b.push(a("" + htmlEncode(responseJSON.response.entries[i].answer) + + ""; } var paginationHtml = ""; diff --git a/DnsServerCore/www/js/main.js b/DnsServerCore/www/js/main.js index 28f3570c..6523349f 100644 --- a/DnsServerCore/www/js/main.js +++ b/DnsServerCore/www/js/main.js @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -236,12 +236,12 @@ $(function () { var itemText = $(this).text(); $(this).closest('.dropdown').find('input').val(itemText); - if ((itemText.indexOf("TLS") !== -1) || (itemText.indexOf(":853") !== -1)) + if (itemText.indexOf("QUIC") !== -1) + $("#optDnsClientProtocol").val("QUIC"); + else if ((itemText.indexOf("TLS") !== -1) || (itemText.indexOf(":853") !== -1)) $("#optDnsClientProtocol").val("TLS"); - else if (itemText.indexOf("HTTPS-JSON") !== -1) - $("#optDnsClientProtocol").val("HttpsJson"); else if ((itemText.indexOf("HTTPS") !== -1) || (itemText.indexOf("http://") !== -1) || (itemText.indexOf("https://") !== -1)) - $("#optDnsClientProtocol").val("Https"); + $("#optDnsClientProtocol").val("HTTPS"); else { switch ($("#optDnsClientProtocol").val()) { case "UDP": @@ -304,20 +304,40 @@ $(function () { $("#txtWebServiceTlsCertificatePassword").prop("disabled", !webServiceEnableTls); }); + $("#chkEnableDnsOverHttp").click(function () { + var enableDnsOverHttp = $("#chkEnableDnsOverHttp").prop("checked"); + + $("#txtDnsOverHttpPort").prop("disabled", !enableDnsOverHttp); + }); + $("#chkEnableDnsOverTls").click(function () { var enableDnsOverTls = $("#chkEnableDnsOverTls").prop("checked"); var enableDnsOverHttps = $("#chkEnableDnsOverHttps").prop("checked"); + var enableDnsOverQuic = $("#chkEnableDnsOverQuic").prop("checked"); - $("#txtDnsTlsCertificatePath").prop("disabled", !enableDnsOverTls && !enableDnsOverHttps); - $("#txtDnsTlsCertificatePassword").prop("disabled", !enableDnsOverTls && !enableDnsOverHttps); + $("#txtDnsOverTlsPort").prop("disabled", !enableDnsOverTls); + $("#txtDnsTlsCertificatePath").prop("disabled", !enableDnsOverTls && !enableDnsOverHttps && !enableDnsOverQuic); + $("#txtDnsTlsCertificatePassword").prop("disabled", !enableDnsOverTls && !enableDnsOverHttps && !enableDnsOverQuic); }); $("#chkEnableDnsOverHttps").click(function () { var enableDnsOverTls = $("#chkEnableDnsOverTls").prop("checked"); var enableDnsOverHttps = $("#chkEnableDnsOverHttps").prop("checked"); + var enableDnsOverQuic = $("#chkEnableDnsOverQuic").prop("checked"); - $("#txtDnsTlsCertificatePath").prop("disabled", !enableDnsOverTls && !enableDnsOverHttps); - $("#txtDnsTlsCertificatePassword").prop("disabled", !enableDnsOverTls && !enableDnsOverHttps); + $("#txtDnsOverHttpsPort").prop("disabled", !enableDnsOverHttps); + $("#txtDnsTlsCertificatePath").prop("disabled", !enableDnsOverTls && !enableDnsOverHttps && !enableDnsOverQuic); + $("#txtDnsTlsCertificatePassword").prop("disabled", !enableDnsOverTls && !enableDnsOverHttps && !enableDnsOverQuic); + }); + + $("#chkEnableDnsOverQuic").click(function () { + var enableDnsOverTls = $("#chkEnableDnsOverTls").prop("checked"); + var enableDnsOverHttps = $("#chkEnableDnsOverHttps").prop("checked"); + var enableDnsOverQuic = $("#chkEnableDnsOverQuic").prop("checked"); + + $("#txtDnsOverQuicPort").prop("disabled", !enableDnsOverQuic); + $("#txtDnsTlsCertificatePath").prop("disabled", !enableDnsOverTls && !enableDnsOverHttps && !enableDnsOverQuic); + $("#txtDnsTlsCertificatePassword").prop("disabled", !enableDnsOverTls && !enableDnsOverHttps && !enableDnsOverQuic); }); $("#chkEnableLogging").click(function () { @@ -347,9 +367,7 @@ $(function () { case "default": var defaultList = ""; - defaultList += "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts" + "\n"; - defaultList += "https://s3.amazonaws.com/lists.disconnect.me/simple_tracking.txt" + "\n"; - defaultList += "https://s3.amazonaws.com/lists.disconnect.me/simple_ad.txt" + "\n"; + defaultList += "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts\n"; $("#txtBlockListUrls").val(defaultList); break; @@ -471,11 +489,6 @@ $(function () { $("#rdForwarderProtocolHttps").prop("checked", true); break; - case "google-json": - $("#txtForwarders").val("https://dns.google/dns-query (8.8.8.8)\r\nhttps://dns.google/dns-query (8.8.4.4)"); - $("#rdForwarderProtocolHttpsJson").prop("checked", true); - break; - case "quad9-udp": $("#txtForwarders").val("9.9.9.9"); @@ -621,6 +634,98 @@ $(function () { break; + case "adguard-udp": + $("#txtForwarders").val("94.140.14.14\r\n94.140.15.15"); + $("#rdForwarderProtocolUdp").prop("checked", true); + break; + + case "adguard-udp-ipv6": + $("#txtForwarders").val("[2a10:50c0::ad1:ff]\r\n[2a10:50c0::ad2:ff]"); + $("#rdForwarderProtocolUdp").prop("checked", true); + break; + + case "adguard-tcp": + $("#txtForwarders").val("94.140.14.14\r\n94.140.15.15"); + $("#rdForwarderProtocolTcp").prop("checked", true); + break; + + case "adguard-tcp-ipv6": + $("#txtForwarders").val("[2a10:50c0::ad1:ff]\r\n[2a10:50c0::ad2:ff]"); + $("#rdForwarderProtocolTcp").prop("checked", true); + break; + + case "adguard-tls": + $("#txtForwarders").val("dns.adguard-dns.com (94.140.14.14:853)\r\ndns.adguard-dns.com (94.140.15.15:853)"); + $("#rdForwarderProtocolTls").prop("checked", true); + break; + + case "adguard-tls-ipv6": + $("#txtForwarders").val("dns.adguard-dns.com ([2a10:50c0::ad1:ff]:853)\r\ndns.adguard-dns.com ([2a10:50c0::ad2:ff]:853)"); + $("#rdForwarderProtocolTls").prop("checked", true); + break; + + case "adguard-https": + $("#txtForwarders").val("https://dns.adguard-dns.com/dns-query"); + $("#rdForwarderProtocolHttps").prop("checked", true); + break; + + case "adguard-quic": + $("#txtForwarders").val("dns.adguard-dns.com (94.140.14.14:853)\r\ndns.adguard-dns.com (94.140.15.15:853)"); + $("#rdForwarderProtocolQuic").prop("checked", true); + break; + + case "adguard-quic-ipv6": + $("#txtForwarders").val("dns.adguard-dns.com ([2a10:50c0::ad1:ff]:853)\r\ndns.adguard-dns.com ([2a10:50c0::ad2:ff]:853)"); + $("#rdForwarderProtocolQuic").prop("checked", true); + break; + + + case "adguard-f-udp": + $("#txtForwarders").val("94.140.14.15\r\n94.140.15.16"); + $("#rdForwarderProtocolUdp").prop("checked", true); + break; + + case "adguard-f-udp-ipv6": + $("#txtForwarders").val("[2a10:50c0::bad1:ff]\r\n[2a10:50c0::bad2:ff]"); + $("#rdForwarderProtocolUdp").prop("checked", true); + break; + + case "adguard-f-tcp": + $("#txtForwarders").val("94.140.14.15\r\n94.140.15.16"); + $("#rdForwarderProtocolTcp").prop("checked", true); + break; + + case "adguard-f-tcp-ipv6": + $("#txtForwarders").val("[2a10:50c0::bad1:ff]\r\n[2a10:50c0::bad2:ff]"); + $("#rdForwarderProtocolTcp").prop("checked", true); + break; + + case "adguard-f-tls": + $("#txtForwarders").val("family.adguard-dns.com (94.140.14.15:853)\r\nfamily.adguard-dns.com (94.140.15.16:853)"); + $("#rdForwarderProtocolTls").prop("checked", true); + break; + + case "adguard-f-tls-ipv6": + $("#txtForwarders").val("family.adguard-dns.com ([2a10:50c0::bad1:ff]:853)\r\nfamily.adguard-dns.com ([2a10:50c0::bad2:ff]:853)"); + $("#rdForwarderProtocolTls").prop("checked", true); + break; + + case "adguard-f-https": + $("#txtForwarders").val("https://family.adguard-dns.com/dns-query"); + $("#rdForwarderProtocolHttps").prop("checked", true); + break; + + case "adguard-f-quic": + $("#txtForwarders").val("family.adguard-dns.com (94.140.14.15:853)\r\nfamily.adguard-dns.com (94.140.15.16:853)"); + $("#rdForwarderProtocolQuic").prop("checked", true); + break; + + case "adguard-f-quic-ipv6": + $("#txtForwarders").val("family.adguard-dns.com ([2a10:50c0::bad1:ff]:853)\r\nfamily.adguard-dns.com ([2a10:50c0::bad2:ff]:853)"); + $("#rdForwarderProtocolQuic").prop("checked", true); + break; + + case "none": $("#txtForwarders").val(""); $("#rdForwarderProtocolUdp").prop("checked", true); @@ -628,12 +733,6 @@ $(function () { } }); - $("#dpCustomDayWiseStart").datepicker(); - $("#dpCustomDayWiseStart").datepicker("option", "dateFormat", "yy-m-d"); - - $("#dpCustomDayWiseEnd").datepicker(); - $("#dpCustomDayWiseEnd").datepicker("option", "dateFormat", "yy-m-d"); - $("input[type=radio][name=rdStatType]").change(function () { var type = $('input[name=rdStatType]:checked').val(); if (type === "custom") { @@ -661,8 +760,6 @@ $(function () { $("#btnCustomDayWise").click(function () { refreshDashboard(); }); - - $("#lblDoHHost").text(window.location.hostname + ":8053"); }); function checkForUpdate() { @@ -744,12 +841,13 @@ function loadDnsSettings() { $("#lblAboutVersion").text(responseJSON.response.version); checkForReverseProxy(responseJSON); + //general $("#txtDnsServerDomain").val(responseJSON.response.dnsServerDomain); $("#lblDnsServerDomain").text(" - " + responseJSON.response.dnsServerDomain); var dnsServerLocalEndPoints = responseJSON.response.dnsServerLocalEndPoints; if (dnsServerLocalEndPoints == null) { - $("#txtDnsServerLocalEndPoints").val("0.0.0.0:53\r\n[::]:53"); + $("#txtDnsServerLocalEndPoints").val(""); } else { var value = ""; @@ -760,9 +858,35 @@ function loadDnsSettings() { $("#txtDnsServerLocalEndPoints").val(value); } + $("#txtDefaultRecordTtl").val(responseJSON.response.defaultRecordTtl); + $("#txtAddEditRecordTtl").attr("placeholder", responseJSON.response.defaultRecordTtl); + $("#chkDnsAppsEnableAutomaticUpdate").prop("checked", responseJSON.response.dnsAppsEnableAutomaticUpdate); + + $("#chkPreferIPv6").prop("checked", responseJSON.response.preferIPv6); + $("#txtEdnsUdpPayloadSize").val(responseJSON.response.udpPayloadSize); + $("#chkDnssecValidation").prop("checked", responseJSON.response.dnssecValidation); + + $("#chkEDnsClientSubnet").prop("checked", responseJSON.response.eDnsClientSubnet); + $("#txtEDnsClientSubnetIPv4PrefixLength").val(responseJSON.response.eDnsClientSubnetIPv4PrefixLength); + $("#txtEDnsClientSubnetIPv6PrefixLength").val(responseJSON.response.eDnsClientSubnetIPv6PrefixLength); + + $("#txtQpmLimitRequests").val(responseJSON.response.qpmLimitRequests); + $("#txtQpmLimitErrors").val(responseJSON.response.qpmLimitErrors); + $("#txtQpmLimitSampleMinutes").val(responseJSON.response.qpmLimitSampleMinutes); + $("#txtQpmLimitIPv4PrefixLength").val(responseJSON.response.qpmLimitIPv4PrefixLength); + $("#txtQpmLimitIPv6PrefixLength").val(responseJSON.response.qpmLimitIPv6PrefixLength); + + $("#txtClientTimeout").val(responseJSON.response.clientTimeout); + $("#txtTcpSendTimeout").val(responseJSON.response.tcpSendTimeout); + $("#txtTcpReceiveTimeout").val(responseJSON.response.tcpReceiveTimeout); + $("#txtQuicIdleTimeout").val(responseJSON.response.quicIdleTimeout); + $("#txtQuicMaxInboundStreams").val(responseJSON.response.quicMaxInboundStreams); + $("#txtListenBacklog").val(responseJSON.response.listenBacklog); + + //web service var webServiceLocalAddresses = responseJSON.response.webServiceLocalAddresses; if (webServiceLocalAddresses == null) { - $("#txtWebServiceLocalAddresses").val("0.0.0.0\r\n[::]"); + $("#txtWebServiceLocalAddresses").val(""); } else { var value = ""; @@ -792,12 +916,24 @@ function loadDnsSettings() { else $("#txtWebServiceTlsCertificatePassword").val(responseJSON.response.webServiceTlsCertificatePassword); + //optional protocols $("#chkEnableDnsOverHttp").prop("checked", responseJSON.response.enableDnsOverHttp); $("#chkEnableDnsOverTls").prop("checked", responseJSON.response.enableDnsOverTls); $("#chkEnableDnsOverHttps").prop("checked", responseJSON.response.enableDnsOverHttps); + $("#chkEnableDnsOverQuic").prop("checked", responseJSON.response.enableDnsOverQuic); - $("#txtDnsTlsCertificatePath").prop("disabled", !responseJSON.response.enableDnsOverTls && !responseJSON.response.enableDnsOverHttps); - $("#txtDnsTlsCertificatePassword").prop("disabled", !responseJSON.response.enableDnsOverTls && !responseJSON.response.enableDnsOverHttps); + $("#txtDnsOverHttpPort").prop("disabled", !responseJSON.response.enableDnsOverHttp); + $("#txtDnsOverTlsPort").prop("disabled", !responseJSON.response.enableDnsOverTls); + $("#txtDnsOverHttpsPort").prop("disabled", !responseJSON.response.enableDnsOverHttps); + $("#txtDnsOverQuicPort").prop("disabled", !responseJSON.response.enableDnsOverQuic); + + $("#txtDnsOverHttpPort").val(responseJSON.response.dnsOverHttpPort); + $("#txtDnsOverTlsPort").val(responseJSON.response.dnsOverTlsPort); + $("#txtDnsOverHttpsPort").val(responseJSON.response.dnsOverHttpsPort); + $("#txtDnsOverQuicPort").val(responseJSON.response.dnsOverQuicPort); + + $("#txtDnsTlsCertificatePath").prop("disabled", !responseJSON.response.enableDnsOverTls && !responseJSON.response.enableDnsOverHttps && !responseJSON.response.enableDnsOverQuic); + $("#txtDnsTlsCertificatePassword").prop("disabled", !responseJSON.response.enableDnsOverTls && !responseJSON.response.enableDnsOverHttps && !responseJSON.response.enableDnsOverQuic); $("#txtDnsTlsCertificatePath").val(responseJSON.response.dnsTlsCertificatePath); @@ -806,6 +942,11 @@ function loadDnsSettings() { else $("#txtDnsTlsCertificatePassword").val(responseJSON.response.dnsTlsCertificatePassword); + $("#lblDoHHost").text(window.location.hostname + ":" + responseJSON.response.dnsOverHttpPort); + $("#lblDoTHost").text("tls-certificate-domain:" + responseJSON.response.dnsOverTlsPort); + $("#lblDoQHost").text("tls-certificate-domain:" + responseJSON.response.dnsOverQuicPort); + + //tsig $("#tableTsigKeys").html(""); if (responseJSON.response.tsigKeys != null) { @@ -814,40 +955,7 @@ function loadDnsSettings() { } } - $("#txtDefaultRecordTtl").val(responseJSON.response.defaultRecordTtl); - $("#txtAddEditRecordTtl").attr("placeholder", responseJSON.response.defaultRecordTtl); - $("#chkDnsAppsEnableAutomaticUpdate").prop("checked", responseJSON.response.dnsAppsEnableAutomaticUpdate); - - $("#chkPreferIPv6").prop("checked", responseJSON.response.preferIPv6); - $("#txtEdnsUdpPayloadSize").val(responseJSON.response.udpPayloadSize); - $("#chkDnssecValidation").prop("checked", responseJSON.response.dnssecValidation); - - $("#chkEDnsClientSubnet").prop("checked", responseJSON.response.eDnsClientSubnet); - $("#txtEDnsClientSubnetIPv4PrefixLength").val(responseJSON.response.eDnsClientSubnetIPv4PrefixLength); - $("#txtEDnsClientSubnetIPv6PrefixLength").val(responseJSON.response.eDnsClientSubnetIPv6PrefixLength); - - $("#txtResolverRetries").val(responseJSON.response.resolverRetries); - $("#txtResolverTimeout").val(responseJSON.response.resolverTimeout); - $("#txtResolverMaxStackCount").val(responseJSON.response.resolverMaxStackCount); - - $("#txtForwarderRetries").val(responseJSON.response.forwarderRetries); - $("#txtForwarderTimeout").val(responseJSON.response.forwarderTimeout); - $("#txtForwarderConcurrency").val(responseJSON.response.forwarderConcurrency); - - $("#txtClientTimeout").val(responseJSON.response.clientTimeout); - $("#txtTcpSendTimeout").val(responseJSON.response.tcpSendTimeout); - $("#txtTcpReceiveTimeout").val(responseJSON.response.tcpReceiveTimeout); - - $("#chkEnableLogging").prop("checked", responseJSON.response.enableLogging); - $("#chkLogQueries").prop("disabled", !responseJSON.response.enableLogging); - $("#chkUseLocalTime").prop("disabled", !responseJSON.response.enableLogging); - $("#txtLogFolderPath").prop("disabled", !responseJSON.response.enableLogging); - $("#chkLogQueries").prop("checked", responseJSON.response.logQueries); - $("#chkUseLocalTime").prop("checked", responseJSON.response.useLocalTime); - $("#txtLogFolderPath").val(responseJSON.response.logFolder); - $("#txtMaxLogFileDays").val(responseJSON.response.maxLogFileDays); - $("#txtMaxStatFileDays").val(responseJSON.response.maxStatFileDays); - + //recursion $("#txtRecursionDeniedNetworks").prop("disabled", true); $("#txtRecursionAllowedNetworks").prop("disabled", true); @@ -894,11 +1002,12 @@ function loadDnsSettings() { $("#chkQnameMinimization").prop("checked", responseJSON.response.qnameMinimization); $("#chkNsRevalidation").prop("checked", responseJSON.response.nsRevalidation); - $("#txtQpmLimitRequests").val(responseJSON.response.qpmLimitRequests); - $("#txtQpmLimitErrors").val(responseJSON.response.qpmLimitErrors); - $("#txtQpmLimitSampleMinutes").val(responseJSON.response.qpmLimitSampleMinutes); - $("#txtQpmLimitIPv4PrefixLength").val(responseJSON.response.qpmLimitIPv4PrefixLength); - $("#txtQpmLimitIPv6PrefixLength").val(responseJSON.response.qpmLimitIPv6PrefixLength); + $("#txtResolverRetries").val(responseJSON.response.resolverRetries); + $("#txtResolverTimeout").val(responseJSON.response.resolverTimeout); + $("#txtResolverMaxStackCount").val(responseJSON.response.resolverMaxStackCount); + + //cache + $("#chkSaveCache").prop("checked", responseJSON.response.saveCache); $("#chkServeStale").prop("checked", responseJSON.response.serveStale); $("#txtServeStaleTtl").prop("disabled", !responseJSON.response.serveStale); @@ -915,6 +1024,84 @@ function loadDnsSettings() { $("#txtCachePrefetchSampleIntervalInMinutes").val(responseJSON.response.cachePrefetchSampleIntervalInMinutes); $("#txtCachePrefetchSampleEligibilityHitsPerHour").val(responseJSON.response.cachePrefetchSampleEligibilityHitsPerHour); + //blocking + $("#chkEnableBlocking").prop("checked", responseJSON.response.enableBlocking); + $("#chkAllowTxtBlockingReport").prop("checked", responseJSON.response.allowTxtBlockingReport); + + if (responseJSON.response.temporaryDisableBlockingTill == null) + $("#lblTemporaryDisableBlockingTill").text("Not Set"); + else + $("#lblTemporaryDisableBlockingTill").text(moment(responseJSON.response.temporaryDisableBlockingTill).local().format("YYYY-MM-DD HH:mm:ss")); + + $("#txtTemporaryDisableBlockingMinutes").val(""); + + $("#txtCustomBlockingAddresses").prop("disabled", true); + + switch (responseJSON.response.blockingType) { + case "NxDomain": + $("#rdBlockingTypeNxDomain").prop("checked", true); + break; + + case "CustomAddress": + $("#rdBlockingTypeCustomAddress").prop("checked", true); + $("#txtCustomBlockingAddresses").prop("disabled", false); + break; + + case "AnyAddress": + default: + $("#rdBlockingTypeAnyAddress").prop("checked", true); + break; + } + + { + var value = ""; + + for (var i = 0; i < responseJSON.response.customBlockingAddresses.length; i++) + value += responseJSON.response.customBlockingAddresses[i] + "\r\n"; + + $("#txtCustomBlockingAddresses").val(value); + } + + var blockListUrls = responseJSON.response.blockListUrls; + if (blockListUrls == null) { + $("#txtBlockListUrls").val(""); + $("#btnUpdateBlockListsNow").prop("disabled", true); + } + else { + var value = ""; + + for (var i = 0; i < blockListUrls.length; i++) + value += blockListUrls[i] + "\r\n"; + + $("#txtBlockListUrls").val(value); + $("#btnUpdateBlockListsNow").prop("disabled", false); + } + + $("#optQuickBlockList").val("blank"); + + //fix custom block list url in case port changes + { + var optCustomLocalBlockList = $("#optCustomLocalBlockList"); + + optCustomLocalBlockList.attr("value", "http://localhost:" + responseJSON.response.webServiceHttpPort + "/blocklist.txt"); + optCustomLocalBlockList.text("Custom Local Block List (http://localhost:" + responseJSON.response.webServiceHttpPort + "/blocklist.txt)"); + } + + $("#txtBlockListUpdateIntervalHours").val(responseJSON.response.blockListUpdateIntervalHours); + + if (responseJSON.response.blockListNextUpdatedOn == null) { + $("#lblBlockListNextUpdatedOn").text("Not Scheduled"); + } + else { + var blockListNextUpdatedOn = moment(responseJSON.response.blockListNextUpdatedOn); + + if (moment().utc().isBefore(blockListNextUpdatedOn)) + $("#lblBlockListNextUpdatedOn").text(blockListNextUpdatedOn.local().format("YYYY-MM-DD HH:mm:ss")); + else + $("#lblBlockListNextUpdatedOn").text("Updating Now"); + } + + //proxy & forwarders var proxy = responseJSON.response.proxy; if (proxy === null) { $("#rdProxyTypeNone").prop("checked", true); @@ -995,8 +1182,8 @@ function loadDnsSettings() { $("#rdForwarderProtocolHttps").prop("checked", true); break; - case "httpsjson": - $("#rdForwarderProtocolHttpsJson").prop("checked", true); + case "quic": + $("#rdForwarderProtocolQuic").prop("checked", true); break; default: @@ -1004,81 +1191,21 @@ function loadDnsSettings() { break; } - $("#chkEnableBlocking").prop("checked", responseJSON.response.enableBlocking); - $("#chkAllowTxtBlockingReport").prop("checked", responseJSON.response.allowTxtBlockingReport); + $("#txtForwarderRetries").val(responseJSON.response.forwarderRetries); + $("#txtForwarderTimeout").val(responseJSON.response.forwarderTimeout); + $("#txtForwarderConcurrency").val(responseJSON.response.forwarderConcurrency); - if (responseJSON.response.temporaryDisableBlockingTill == null) - $("#lblTemporaryDisableBlockingTill").text("Not Set"); - else - $("#lblTemporaryDisableBlockingTill").text(moment(responseJSON.response.temporaryDisableBlockingTill).local().format("YYYY-MM-DD HH:mm:ss")); + //logging + $("#chkEnableLogging").prop("checked", responseJSON.response.enableLogging); + $("#chkLogQueries").prop("disabled", !responseJSON.response.enableLogging); + $("#chkUseLocalTime").prop("disabled", !responseJSON.response.enableLogging); + $("#txtLogFolderPath").prop("disabled", !responseJSON.response.enableLogging); - $("#txtTemporaryDisableBlockingMinutes").val(""); - - $("#txtCustomBlockingAddresses").prop("disabled", true); - - switch (responseJSON.response.blockingType) { - case "NxDomain": - $("#rdBlockingTypeNxDomain").prop("checked", true); - break; - - case "CustomAddress": - $("#rdBlockingTypeCustomAddress").prop("checked", true); - $("#txtCustomBlockingAddresses").prop("disabled", false); - break; - - case "AnyAddress": - default: - $("#rdBlockingTypeAnyAddress").prop("checked", true); - break; - } - - { - var value = ""; - - for (var i = 0; i < responseJSON.response.customBlockingAddresses.length; i++) - value += responseJSON.response.customBlockingAddresses[i] + "\r\n"; - - $("#txtCustomBlockingAddresses").val(value); - } - - var blockListUrls = responseJSON.response.blockListUrls; - if (blockListUrls == null) { - $("#txtBlockListUrls").val(""); - $("#btnUpdateBlockListsNow").prop("disabled", true); - } - else { - var value = ""; - - for (var i = 0; i < blockListUrls.length; i++) - value += blockListUrls[i] + "\r\n"; - - $("#txtBlockListUrls").val(value); - $("#btnUpdateBlockListsNow").prop("disabled", false); - } - - $("#optQuickBlockList").val("blank"); - - //fix custom block list url in case port changes - { - var optCustomLocalBlockList = $("#optCustomLocalBlockList"); - - optCustomLocalBlockList.attr("value", "http://localhost:" + responseJSON.response.webServiceHttpPort + "/blocklist.txt"); - optCustomLocalBlockList.text("Custom Local Block List (http://localhost:" + responseJSON.response.webServiceHttpPort + "/blocklist.txt)"); - } - - $("#txtBlockListUpdateIntervalHours").val(responseJSON.response.blockListUpdateIntervalHours); - - if (responseJSON.response.blockListNextUpdatedOn == null) { - $("#lblBlockListNextUpdatedOn").text("Not Scheduled"); - } - else { - var blockListNextUpdatedOn = moment(responseJSON.response.blockListNextUpdatedOn); - - if (moment().utc().isBefore(blockListNextUpdatedOn)) - $("#lblBlockListNextUpdatedOn").text(blockListNextUpdatedOn.local().format("YYYY-MM-DD HH:mm:ss")); - else - $("#lblBlockListNextUpdatedOn").text("Updating Now"); - } + $("#chkLogQueries").prop("checked", responseJSON.response.logQueries); + $("#chkUseLocalTime").prop("checked", responseJSON.response.useLocalTime); + $("#txtLogFolderPath").val(responseJSON.response.logFolder); + $("#txtMaxLogFileDays").val(responseJSON.response.maxLogFileDays); + $("#txtMaxStatFileDays").val(responseJSON.response.maxStatFileDays); divDnsSettingsLoader.hide(); divDnsSettings.show(); @@ -1091,6 +1218,7 @@ function loadDnsSettings() { } function saveDnsSettings() { + //general var dnsServerDomain = $("#txtDnsServerDomain").val(); if ((dnsServerDomain === null) || (dnsServerDomain === "")) { @@ -1106,38 +1234,6 @@ function saveDnsSettings() { else $("#txtDnsServerLocalEndPoints").val(dnsServerLocalEndPoints.replace(/,/g, "\n")); - var webServiceLocalAddresses = cleanTextList($("#txtWebServiceLocalAddresses").val()); - - if ((webServiceLocalAddresses.length === 0) || (webServiceLocalAddresses === ",")) - webServiceLocalAddresses = "0.0.0.0,[::]"; - else - $("#txtWebServiceLocalAddresses").val(webServiceLocalAddresses.replace(/,/g, "\n")); - - var webServiceHttpPort = $("#txtWebServiceHttpPort").val(); - - if ((webServiceHttpPort === null) || (webServiceHttpPort === "")) - webServiceHttpPort = 5380; - - var webServiceEnableTls = $("#chkWebServiceEnableTls").prop("checked"); - var webServiceHttpToTlsRedirect = $("#chkWebServiceHttpToTlsRedirect").prop("checked"); - var webServiceUseSelfSignedTlsCertificate = $("#chkWebServiceUseSelfSignedTlsCertificate").prop("checked"); - var webServiceTlsPort = $("#txtWebServiceTlsPort").val(); - var webServiceTlsCertificatePath = $("#txtWebServiceTlsCertificatePath").val(); - var webServiceTlsCertificatePassword = $("#txtWebServiceTlsCertificatePassword").val(); - - var enableDnsOverHttp = $("#chkEnableDnsOverHttp").prop('checked'); - var enableDnsOverTls = $("#chkEnableDnsOverTls").prop('checked'); - var enableDnsOverHttps = $("#chkEnableDnsOverHttps").prop('checked'); - var dnsTlsCertificatePath = $("#txtDnsTlsCertificatePath").val(); - var dnsTlsCertificatePassword = $("#txtDnsTlsCertificatePassword").val(); - - var tsigKeys = serializeTableData($("#tableTsigKeys"), 3); - if (tsigKeys === false) - return; - - if (tsigKeys.length === 0) - tsigKeys = false; - var defaultRecordTtl = $("#txtDefaultRecordTtl").val(); var dnsAppsEnableAutomaticUpdate = $("#chkDnsAppsEnableAutomaticUpdate").prop('checked'); var preferIPv6 = $("#chkPreferIPv6").prop('checked'); @@ -1160,96 +1256,6 @@ function saveDnsSettings() { return; } - var resolverRetries = $("#txtResolverRetries").val(); - if ((resolverRetries == null) || (resolverRetries === "")) { - showAlert("warning", "Missing!", "Please enter a value for Resolver Retries."); - $("#txtResolverRetries").focus(); - return; - } - - var resolverTimeout = $("#txtResolverTimeout").val(); - if ((resolverTimeout == null) || (resolverTimeout === "")) { - showAlert("warning", "Missing!", "Please enter a value for Resolver Timeout."); - $("#txtResolverTimeout").focus(); - return; - } - - var resolverMaxStackCount = $("#txtResolverMaxStackCount").val(); - if ((resolverMaxStackCount == null) || (resolverMaxStackCount === "")) { - showAlert("warning", "Missing!", "Please enter a value for Resolver Max Stack Count."); - $("#txtResolverMaxStackCount").focus(); - return; - } - - var forwarderRetries = $("#txtForwarderRetries").val(); - if ((forwarderRetries == null) || (forwarderRetries === "")) { - showAlert("warning", "Missing!", "Please enter a value for Forwarder Retries."); - $("#txtForwarderRetries").focus(); - return; - } - - var forwarderTimeout = $("#txtForwarderTimeout").val(); - if ((forwarderTimeout == null) || (forwarderTimeout === "")) { - showAlert("warning", "Missing!", "Please enter a value for Forwarder Timeout."); - $("#txtForwarderTimeout").focus(); - return; - } - - var forwarderConcurrency = $("#txtForwarderConcurrency").val(); - if ((forwarderConcurrency == null) || (forwarderConcurrency === "")) { - showAlert("warning", "Missing!", "Please enter a value for Forwarder Concurrency."); - $("#txtForwarderConcurrency").focus(); - return; - } - - var clientTimeout = $("#txtClientTimeout").val(); - if ((clientTimeout == null) || (clientTimeout === "")) { - showAlert("warning", "Missing!", "Please enter a value for Client Timeout."); - $("#txtClientTimeout").focus(); - return; - } - - var tcpSendTimeout = $("#txtTcpSendTimeout").val(); - if ((tcpSendTimeout == null) || (tcpSendTimeout === "")) { - showAlert("warning", "Missing!", "Please enter a value for TCP Send Timeout."); - $("#txtTcpSendTimeout").focus(); - return; - } - - var tcpReceiveTimeout = $("#txtTcpReceiveTimeout").val(); - if ((tcpReceiveTimeout == null) || (tcpReceiveTimeout === "")) { - showAlert("warning", "Missing!", "Please enter a value for TCP Receive Timeout."); - $("#txtTcpReceiveTimeout").focus(); - return; - } - - var enableLogging = $("#chkEnableLogging").prop('checked'); - var logQueries = $("#chkLogQueries").prop('checked'); - var useLocalTime = $("#chkUseLocalTime").prop('checked'); - var logFolder = $("#txtLogFolderPath").val(); - var maxLogFileDays = $("#txtMaxLogFileDays").val(); - var maxStatFileDays = $("#txtMaxStatFileDays").val(); - - var recursion = $("input[name=rdRecursion]:checked").val(); - - var recursionDeniedNetworks = cleanTextList($("#txtRecursionDeniedNetworks").val()); - - if ((recursionDeniedNetworks.length === 0) || (recursionDeniedNetworks === ",")) - recursionDeniedNetworks = false; - else - $("#txtRecursionDeniedNetworks").val(recursionDeniedNetworks.replace(/,/g, "\n")); - - var recursionAllowedNetworks = cleanTextList($("#txtRecursionAllowedNetworks").val()); - - if ((recursionAllowedNetworks.length === 0) || (recursionAllowedNetworks === ",")) - recursionAllowedNetworks = false; - else - $("#txtRecursionAllowedNetworks").val(recursionAllowedNetworks.replace(/,/g, "\n")); - - var randomizeName = $("#chkRandomizeName").prop('checked'); - var qnameMinimization = $("#chkQnameMinimization").prop('checked'); - var nsRevalidation = $("#chkNsRevalidation").prop('checked'); - var qpmLimitRequests = $("#txtQpmLimitRequests").val(); if ((qpmLimitRequests == null) || (qpmLimitRequests === "")) { showAlert("warning", "Missing!", "Please enter Queries Per Minute (QPM) request limit value."); @@ -1285,6 +1291,158 @@ function saveDnsSettings() { return; } + var clientTimeout = $("#txtClientTimeout").val(); + if ((clientTimeout == null) || (clientTimeout === "")) { + showAlert("warning", "Missing!", "Please enter a value for Client Timeout."); + $("#txtClientTimeout").focus(); + return; + } + + var tcpSendTimeout = $("#txtTcpSendTimeout").val(); + if ((tcpSendTimeout == null) || (tcpSendTimeout === "")) { + showAlert("warning", "Missing!", "Please enter a value for TCP Send Timeout."); + $("#txtTcpSendTimeout").focus(); + return; + } + + var tcpReceiveTimeout = $("#txtTcpReceiveTimeout").val(); + if ((tcpReceiveTimeout == null) || (tcpReceiveTimeout === "")) { + showAlert("warning", "Missing!", "Please enter a value for TCP Receive Timeout."); + $("#txtTcpReceiveTimeout").focus(); + return; + } + + var quicIdleTimeout = $("#txtQuicIdleTimeout").val(); + if ((quicIdleTimeout == null) || (quicIdleTimeout === "")) { + showAlert("warning", "Missing!", "Please enter a value for QUIC Idle Timeout."); + $("#txtQuicIdleTimeout").focus(); + return; + } + + var quicMaxInboundStreams = $("#txtQuicMaxInboundStreams").val(); + if ((quicMaxInboundStreams == null) || (quicMaxInboundStreams === "")) { + showAlert("warning", "Missing!", "Please enter a value for QUIC Max Inbound Streams."); + $("#txtQuicMaxInboundStreams").focus(); + return; + } + + var listenBacklog = $("#txtListenBacklog").val(); + if ((listenBacklog == null) || (listenBacklog === "")) { + showAlert("warning", "Missing!", "Please enter a value for Listen Backlog."); + $("#txtListenBacklog").focus(); + return; + } + + //web service + var webServiceLocalAddresses = cleanTextList($("#txtWebServiceLocalAddresses").val()); + + if ((webServiceLocalAddresses.length === 0) || (webServiceLocalAddresses === ",")) + webServiceLocalAddresses = "0.0.0.0,[::]"; + else + $("#txtWebServiceLocalAddresses").val(webServiceLocalAddresses.replace(/,/g, "\n")); + + var webServiceHttpPort = $("#txtWebServiceHttpPort").val(); + + if ((webServiceHttpPort === null) || (webServiceHttpPort === "")) + webServiceHttpPort = 5380; + + var webServiceEnableTls = $("#chkWebServiceEnableTls").prop("checked"); + var webServiceHttpToTlsRedirect = $("#chkWebServiceHttpToTlsRedirect").prop("checked"); + var webServiceUseSelfSignedTlsCertificate = $("#chkWebServiceUseSelfSignedTlsCertificate").prop("checked"); + var webServiceTlsPort = $("#txtWebServiceTlsPort").val(); + var webServiceTlsCertificatePath = $("#txtWebServiceTlsCertificatePath").val(); + var webServiceTlsCertificatePassword = $("#txtWebServiceTlsCertificatePassword").val(); + + //optional protocols + var enableDnsOverHttp = $("#chkEnableDnsOverHttp").prop("checked"); + var enableDnsOverTls = $("#chkEnableDnsOverTls").prop("checked"); + var enableDnsOverHttps = $("#chkEnableDnsOverHttps").prop("checked"); + var enableDnsOverQuic = $("#chkEnableDnsOverQuic").prop("checked"); + + var dnsOverHttpPort = $("#txtDnsOverHttpPort").val(); + if ((dnsOverHttpPort == null) || (dnsOverHttpPort === "")) { + showAlert("warning", "Missing!", "Please enter a value for DNS-over-HTTP Port."); + $("#txtDnsOverHttpPort").focus(); + return; + } + + var dnsOverTlsPort = $("#txtDnsOverTlsPort").val(); + if ((dnsOverTlsPort == null) || (dnsOverTlsPort === "")) { + showAlert("warning", "Missing!", "Please enter a value for DNS-over-TLS Port."); + $("#txtDnsOverTlsPort").focus(); + return; + } + + var dnsOverHttpsPort = $("#txtDnsOverHttpsPort").val(); + if ((dnsOverHttpsPort == null) || (dnsOverHttpsPort === "")) { + showAlert("warning", "Missing!", "Please enter a value for DNS-over-HTTPS Port."); + $("#txtDnsOverHttpsPort").focus(); + return; + } + + var dnsOverQuicPort = $("#txtDnsOverQuicPort").val(); + if ((dnsOverQuicPort == null) || (dnsOverQuicPort === "")) { + showAlert("warning", "Missing!", "Please enter a value for DNS-over-QUIC Port."); + $("#txtDnsOverQuicPort").focus(); + return; + } + + var dnsTlsCertificatePath = $("#txtDnsTlsCertificatePath").val(); + var dnsTlsCertificatePassword = $("#txtDnsTlsCertificatePassword").val(); + + //tsig + var tsigKeys = serializeTableData($("#tableTsigKeys"), 3); + if (tsigKeys === false) + return; + + if (tsigKeys.length === 0) + tsigKeys = false; + + //recursion + var recursion = $("input[name=rdRecursion]:checked").val(); + + var recursionDeniedNetworks = cleanTextList($("#txtRecursionDeniedNetworks").val()); + + if ((recursionDeniedNetworks.length === 0) || (recursionDeniedNetworks === ",")) + recursionDeniedNetworks = false; + else + $("#txtRecursionDeniedNetworks").val(recursionDeniedNetworks.replace(/,/g, "\n")); + + var recursionAllowedNetworks = cleanTextList($("#txtRecursionAllowedNetworks").val()); + + if ((recursionAllowedNetworks.length === 0) || (recursionAllowedNetworks === ",")) + recursionAllowedNetworks = false; + else + $("#txtRecursionAllowedNetworks").val(recursionAllowedNetworks.replace(/,/g, "\n")); + + var randomizeName = $("#chkRandomizeName").prop('checked'); + var qnameMinimization = $("#chkQnameMinimization").prop('checked'); + var nsRevalidation = $("#chkNsRevalidation").prop('checked'); + + var resolverRetries = $("#txtResolverRetries").val(); + if ((resolverRetries == null) || (resolverRetries === "")) { + showAlert("warning", "Missing!", "Please enter a value for Resolver Retries."); + $("#txtResolverRetries").focus(); + return; + } + + var resolverTimeout = $("#txtResolverTimeout").val(); + if ((resolverTimeout == null) || (resolverTimeout === "")) { + showAlert("warning", "Missing!", "Please enter a value for Resolver Timeout."); + $("#txtResolverTimeout").focus(); + return; + } + + var resolverMaxStackCount = $("#txtResolverMaxStackCount").val(); + if ((resolverMaxStackCount == null) || (resolverMaxStackCount === "")) { + showAlert("warning", "Missing!", "Please enter a value for Resolver Max Stack Count."); + $("#txtResolverMaxStackCount").focus(); + return; + } + + //cache + var saveCache = $("#chkSaveCache").prop("checked"); + var serveStale = $("#chkServeStale").prop("checked"); var serveStaleTtl = $("#txtServeStaleTtl").val(); @@ -1351,6 +1509,28 @@ function saveDnsSettings() { return; } + //blocking + var enableBlocking = $("#chkEnableBlocking").prop("checked"); + var allowTxtBlockingReport = $("#chkAllowTxtBlockingReport").prop("checked"); + + var blockingType = $("input[name=rdBlockingType]:checked").val(); + + var customBlockingAddresses = cleanTextList($("#txtCustomBlockingAddresses").val()); + if ((customBlockingAddresses.length === 0) || customBlockingAddresses === ",") + customBlockingAddresses = false; + else + $("#txtCustomBlockingAddresses").val(customBlockingAddresses.replace(/,/g, "\n") + "\n"); + + var blockListUrls = cleanTextList($("#txtBlockListUrls").val()); + + if ((blockListUrls.length === 0) || (blockListUrls === ",")) + blockListUrls = false; + else + $("#txtBlockListUrls").val(blockListUrls.replace(/,/g, "\n") + "\n"); + + var blockListUpdateIntervalHours = $("#txtBlockListUpdateIntervalHours").val(); + + //proxy & forwarders var proxy; var proxyType = $('input[name=rdProxyType]:checked').val().toLowerCase(); if (proxyType === "none") { @@ -1392,43 +1572,55 @@ function saveDnsSettings() { var forwarderProtocol = $('input[name=rdForwarderProtocol]:checked').val(); - var enableBlocking = $("#chkEnableBlocking").prop("checked"); - var allowTxtBlockingReport = $("#chkAllowTxtBlockingReport").prop("checked"); + var forwarderRetries = $("#txtForwarderRetries").val(); + if ((forwarderRetries == null) || (forwarderRetries === "")) { + showAlert("warning", "Missing!", "Please enter a value for Forwarder Retries."); + $("#txtForwarderRetries").focus(); + return; + } - var blockingType = $("input[name=rdBlockingType]:checked").val(); + var forwarderTimeout = $("#txtForwarderTimeout").val(); + if ((forwarderTimeout == null) || (forwarderTimeout === "")) { + showAlert("warning", "Missing!", "Please enter a value for Forwarder Timeout."); + $("#txtForwarderTimeout").focus(); + return; + } - var customBlockingAddresses = cleanTextList($("#txtCustomBlockingAddresses").val()); - if ((customBlockingAddresses.length === 0) || customBlockingAddresses === ",") - customBlockingAddresses = false; - else - $("#txtCustomBlockingAddresses").val(customBlockingAddresses.replace(/,/g, "\n") + "\n"); + var forwarderConcurrency = $("#txtForwarderConcurrency").val(); + if ((forwarderConcurrency == null) || (forwarderConcurrency === "")) { + showAlert("warning", "Missing!", "Please enter a value for Forwarder Concurrency."); + $("#txtForwarderConcurrency").focus(); + return; + } - var blockListUrls = cleanTextList($("#txtBlockListUrls").val()); - - if ((blockListUrls.length === 0) || (blockListUrls === ",")) - blockListUrls = false; - else - $("#txtBlockListUrls").val(blockListUrls.replace(/,/g, "\n") + "\n"); - - var blockListUpdateIntervalHours = $("#txtBlockListUpdateIntervalHours").val(); + //logging + var enableLogging = $("#chkEnableLogging").prop('checked'); + var logQueries = $("#chkLogQueries").prop('checked'); + var useLocalTime = $("#chkUseLocalTime").prop('checked'); + var logFolder = $("#txtLogFolderPath").val(); + var maxLogFileDays = $("#txtMaxLogFileDays").val(); + var maxStatFileDays = $("#txtMaxStatFileDays").val(); + //send request var btn = $("#btnSaveDnsSettings").button('loading'); HTTPRequest({ - url: "/api/settings/set?token=" + sessionData.token + "&dnsServerDomain=" + dnsServerDomain + "&dnsServerLocalEndPoints=" + encodeURIComponent(dnsServerLocalEndPoints) - + "&webServiceLocalAddresses=" + encodeURIComponent(webServiceLocalAddresses) + "&webServiceHttpPort=" + webServiceHttpPort + "&webServiceEnableTls=" + webServiceEnableTls + "&webServiceHttpToTlsRedirect=" + webServiceHttpToTlsRedirect + "&webServiceUseSelfSignedTlsCertificate=" + webServiceUseSelfSignedTlsCertificate + "&webServiceTlsPort=" + webServiceTlsPort + "&webServiceTlsCertificatePath=" + encodeURIComponent(webServiceTlsCertificatePath) + "&webServiceTlsCertificatePassword=" + encodeURIComponent(webServiceTlsCertificatePassword) - + "&enableDnsOverHttp=" + enableDnsOverHttp + "&enableDnsOverTls=" + enableDnsOverTls + "&enableDnsOverHttps=" + enableDnsOverHttps + "&dnsTlsCertificatePath=" + encodeURIComponent(dnsTlsCertificatePath) + "&dnsTlsCertificatePassword=" + encodeURIComponent(dnsTlsCertificatePassword) - + "&tsigKeys=" + encodeURIComponent(tsigKeys) + url: "/api/settings/set", + method: "POST", + data: "token=" + sessionData.token + "&dnsServerDomain=" + dnsServerDomain + "&dnsServerLocalEndPoints=" + encodeURIComponent(dnsServerLocalEndPoints) + "&defaultRecordTtl=" + defaultRecordTtl + "&dnsAppsEnableAutomaticUpdate=" + dnsAppsEnableAutomaticUpdate + "&preferIPv6=" + preferIPv6 + "&udpPayloadSize=" + udpPayloadSize + "&dnssecValidation=" + dnssecValidation + "&eDnsClientSubnet=" + eDnsClientSubnet + "&eDnsClientSubnetIPv4PrefixLength=" + eDnsClientSubnetIPv4PrefixLength + "&eDnsClientSubnetIPv6PrefixLength=" + eDnsClientSubnetIPv6PrefixLength - + "&resolverRetries=" + resolverRetries + "&resolverTimeout=" + resolverTimeout + "&resolverMaxStackCount=" + resolverMaxStackCount - + "&forwarderRetries=" + forwarderRetries + "&forwarderTimeout=" + forwarderTimeout + "&forwarderConcurrency=" + forwarderConcurrency - + "&clientTimeout=" + clientTimeout + "&tcpSendTimeout=" + tcpSendTimeout + "&tcpReceiveTimeout=" + tcpReceiveTimeout - + "&enableLogging=" + enableLogging + "&logQueries=" + logQueries + "&useLocalTime=" + useLocalTime + "&logFolder=" + encodeURIComponent(logFolder) + "&maxLogFileDays=" + maxLogFileDays + "&maxStatFileDays=" + maxStatFileDays - + "&recursion=" + recursion + "&recursionDeniedNetworks=" + encodeURIComponent(recursionDeniedNetworks) + "&recursionAllowedNetworks=" + encodeURIComponent(recursionAllowedNetworks) + "&randomizeName=" + randomizeName + "&qnameMinimization=" + qnameMinimization + "&nsRevalidation=" + nsRevalidation + "&qpmLimitRequests=" + qpmLimitRequests + "&qpmLimitErrors=" + qpmLimitErrors + "&qpmLimitSampleMinutes=" + qpmLimitSampleMinutes + "&qpmLimitIPv4PrefixLength=" + qpmLimitIPv4PrefixLength + "&qpmLimitIPv6PrefixLength=" + qpmLimitIPv6PrefixLength - + "&serveStale=" + serveStale + "&serveStaleTtl=" + serveStaleTtl + "&cacheMaximumEntries=" + cacheMaximumEntries + "&cacheMinimumRecordTtl=" + cacheMinimumRecordTtl + "&cacheMaximumRecordTtl=" + cacheMaximumRecordTtl + "&cacheNegativeRecordTtl=" + cacheNegativeRecordTtl + "&cacheFailureRecordTtl=" + cacheFailureRecordTtl + "&cachePrefetchEligibility=" + cachePrefetchEligibility + "&cachePrefetchTrigger=" + cachePrefetchTrigger + "&cachePrefetchSampleIntervalInMinutes=" + cachePrefetchSampleIntervalInMinutes + "&cachePrefetchSampleEligibilityHitsPerHour=" + cachePrefetchSampleEligibilityHitsPerHour - + proxy + "&forwarders=" + encodeURIComponent(forwarders) + "&forwarderProtocol=" + forwarderProtocol + "&enableBlocking=" + enableBlocking + "&allowTxtBlockingReport=" + allowTxtBlockingReport + "&blockingType=" + blockingType + "&customBlockingAddresses=" + encodeURIComponent(customBlockingAddresses) + "&blockListUrls=" + encodeURIComponent(blockListUrls) + "&blockListUpdateIntervalHours=" + blockListUpdateIntervalHours, + + "&clientTimeout=" + clientTimeout + "&tcpSendTimeout=" + tcpSendTimeout + "&tcpReceiveTimeout=" + tcpReceiveTimeout + "&quicIdleTimeout=" + quicIdleTimeout + "&quicMaxInboundStreams=" + quicMaxInboundStreams + "&listenBacklog=" + listenBacklog + + "&webServiceLocalAddresses=" + encodeURIComponent(webServiceLocalAddresses) + "&webServiceHttpPort=" + webServiceHttpPort + "&webServiceEnableTls=" + webServiceEnableTls + "&webServiceHttpToTlsRedirect=" + webServiceHttpToTlsRedirect + "&webServiceUseSelfSignedTlsCertificate=" + webServiceUseSelfSignedTlsCertificate + "&webServiceTlsPort=" + webServiceTlsPort + "&webServiceTlsCertificatePath=" + encodeURIComponent(webServiceTlsCertificatePath) + "&webServiceTlsCertificatePassword=" + encodeURIComponent(webServiceTlsCertificatePassword) + + "&enableDnsOverHttp=" + enableDnsOverHttp + "&enableDnsOverTls=" + enableDnsOverTls + "&enableDnsOverHttps=" + enableDnsOverHttps + "&enableDnsOverQuic=" + enableDnsOverQuic + "&dnsOverHttpPort=" + dnsOverHttpPort + "&dnsOverTlsPort=" + dnsOverTlsPort + "&dnsOverHttpsPort=" + dnsOverHttpsPort + "&dnsOverQuicPort=" + dnsOverQuicPort + "&dnsTlsCertificatePath=" + encodeURIComponent(dnsTlsCertificatePath) + "&dnsTlsCertificatePassword=" + encodeURIComponent(dnsTlsCertificatePassword) + + "&tsigKeys=" + encodeURIComponent(tsigKeys) + + "&recursion=" + recursion + "&recursionDeniedNetworks=" + encodeURIComponent(recursionDeniedNetworks) + "&recursionAllowedNetworks=" + encodeURIComponent(recursionAllowedNetworks) + "&randomizeName=" + randomizeName + "&qnameMinimization=" + qnameMinimization + "&nsRevalidation=" + nsRevalidation + "&resolverRetries=" + resolverRetries + "&resolverTimeout=" + resolverTimeout + "&resolverMaxStackCount=" + resolverMaxStackCount + + "&saveCache=" + saveCache + "&serveStale=" + serveStale + "&serveStaleTtl=" + serveStaleTtl + "&cacheMaximumEntries=" + cacheMaximumEntries + "&cacheMinimumRecordTtl=" + cacheMinimumRecordTtl + "&cacheMaximumRecordTtl=" + cacheMaximumRecordTtl + "&cacheNegativeRecordTtl=" + cacheNegativeRecordTtl + "&cacheFailureRecordTtl=" + cacheFailureRecordTtl + "&cachePrefetchEligibility=" + cachePrefetchEligibility + "&cachePrefetchTrigger=" + cachePrefetchTrigger + "&cachePrefetchSampleIntervalInMinutes=" + cachePrefetchSampleIntervalInMinutes + "&cachePrefetchSampleEligibilityHitsPerHour=" + cachePrefetchSampleEligibilityHitsPerHour + + "&enableBlocking=" + enableBlocking + "&allowTxtBlockingReport=" + allowTxtBlockingReport + "&blockingType=" + blockingType + "&customBlockingAddresses=" + encodeURIComponent(customBlockingAddresses) + "&blockListUrls=" + encodeURIComponent(blockListUrls) + "&blockListUpdateIntervalHours=" + blockListUpdateIntervalHours + + proxy + "&forwarders=" + encodeURIComponent(forwarders) + "&forwarderProtocol=" + forwarderProtocol + "&forwarderRetries=" + forwarderRetries + "&forwarderTimeout=" + forwarderTimeout + "&forwarderConcurrency=" + forwarderConcurrency + + "&enableLogging=" + enableLogging + "&logQueries=" + logQueries + "&useLocalTime=" + useLocalTime + "&logFolder=" + encodeURIComponent(logFolder) + "&maxLogFileDays=" + maxLogFileDays + "&maxStatFileDays=" + maxStatFileDays, + processData: false, success: function (responseJSON) { document.title = responseJSON.response.dnsServerDomain + " - " + "Technitium DNS Server v" + responseJSON.response.version; $("#lblDnsServerDomain").text(" - " + responseJSON.response.dnsServerDomain); @@ -1436,6 +1628,20 @@ function saveDnsSettings() { $("#txtAddEditRecordTtl").attr("placeholder", responseJSON.response.defaultRecordTtl); + //reload local addresses + var webServiceLocalAddresses = responseJSON.response.webServiceLocalAddresses; + if (webServiceLocalAddresses == null) { + $("#txtWebServiceLocalAddresses").val(""); + } + else { + var value = ""; + + for (var i = 0; i < webServiceLocalAddresses.length; i++) + value += webServiceLocalAddresses[i] + "\r\n"; + + $("#txtWebServiceLocalAddresses").val(value); + } + //reset tls state $("#chkWebServiceEnableTls").prop("checked", responseJSON.response.webServiceEnableTls); $("#chkWebServiceHttpToTlsRedirect").prop("disabled", !responseJSON.response.webServiceEnableTls); @@ -1444,6 +1650,11 @@ function saveDnsSettings() { $("#txtWebServiceTlsCertificatePath").prop("disabled", !responseJSON.response.webServiceEnableTls); $("#txtWebServiceTlsCertificatePassword").prop("disabled", !responseJSON.response.webServiceEnableTls); + //optional protocols + $("#lblDoHHost").text(window.location.hostname + ":" + responseJSON.response.dnsOverHttpPort); + $("#lblDoTHost").text("tls-certificate-domain:" + responseJSON.response.dnsOverTlsPort); + $("#lblDoQHost").text("tls-certificate-domain:" + responseJSON.response.dnsOverQuicPort); + //reload tsig keys $("#tableTsigKeys").html(""); @@ -2223,6 +2434,7 @@ function resetBackupSettingsModal() { $("#chkBackupAllowedZones").prop("checked", true); $("#chkBackupBlockedZones").prop("checked", true); $("#chkBackupScopes").prop("checked", true); + $("#chkBackupApps").prop("checked", true); $("#chkBackupStats").prop("checked", true); $("#chkBackupLogs").prop("checked", false); $("#chkBackupBlockLists").prop("checked", true); @@ -2266,6 +2478,7 @@ function resetRestoreSettingsModal() { $("#chkRestoreAllowedZones").prop("checked", true); $("#chkRestoreBlockedZones").prop("checked", true); $("#chkRestoreScopes").prop("checked", true); + $("#chkRestoreApps").prop("checked", true); $("#chkRestoreStats").prop("checked", true); $("#chkRestoreLogs").prop("checked", false); $("#chkRestoreBlockLists").prop("checked", true); @@ -2311,6 +2524,7 @@ function restoreSettings() { url: "/api/settings/restore?token=" + sessionData.token + "&blockLists=" + blockLists + "&logs=" + logs + "&scopes=" + scopes + "&apps=" + apps + "&stats=" + stats + "&zones=" + zones + "&allowedZones=" + allowedZones + "&blockedZones=" + blockedZones + "&dnsSettings=" + dnsSettings + "&authConfig=" + authConfig + "&logSettings=" + logSettings + "&deleteExistingFiles=" + deleteExistingFiles, method: "POST", data: formData, + dataContentType: false, processData: false, success: function (responseJSON) { document.title = responseJSON.response.dnsServerDomain + " - " + "Technitium DNS Server v" + responseJSON.response.version; diff --git a/DnsServerCore/www/js/zone.js b/DnsServerCore/www/js/zone.js index c50f6716..555528c9 100644 --- a/DnsServerCore/www/js/zone.js +++ b/DnsServerCore/www/js/zone.js @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -18,6 +18,7 @@ along with this program. If not, see . */ var zoneOptionsAvailableTsigKeyNames; +var editZoneRecords; $(function () { $("input[type=radio][name=rdAddZoneType]").change(function () { @@ -64,11 +65,11 @@ $(function () { break; case "Tls": + case "Quic": $("#txtAddZoneForwarder").attr("placeholder", "dns.quad9.net (9.9.9.9:853)") break; case "Https": - case "HttpsJson": $("#txtAddZoneForwarder").attr("placeholder", "https://cloudflare-dns.com/dns-query (1.1.1.1)") break; } @@ -269,16 +270,47 @@ $(function () { break; } }); + + $("#optZonesPerPage").change(function () { + localStorage.setItem("optZonesPerPage", $("#optZonesPerPage").val()); + }); + + var optZonesPerPage = localStorage.getItem("optZonesPerPage"); + if (optZonesPerPage != null) + $("#optZonesPerPage").val(optZonesPerPage); + + $("#optEditZoneRecordsPerPage").change(function () { + localStorage.setItem("optEditZoneRecordsPerPage", $("#optEditZoneRecordsPerPage").val()); + }); + + var optEditZoneRecordsPerPage = localStorage.getItem("optEditZoneRecordsPerPage"); + if (optEditZoneRecordsPerPage != null) + $("#optEditZoneRecordsPerPage").val(optEditZoneRecordsPerPage); }); -function refreshZones(checkDisplay) { +function refreshZones(checkDisplay, pageNumber) { if (checkDisplay == null) checkDisplay = false; var divViewZones = $("#divViewZones"); - if (checkDisplay && (divViewZones.css('display') === "none")) - return; + if (checkDisplay) { + if (divViewZones.css("display") === "none") + return; + + if (($("#tableZonesBody").html().length > 0) && !$("#mainPanelTabPaneZones").hasClass("active")) + return; + } + + if (pageNumber == null) { + pageNumber = $("#txtZonesPageNumber").val(); + if (pageNumber == "") + pageNumber = 1; + } + + var zonesPerPage = Number($("#optZonesPerPage").val()); + if (zonesPerPage < 1) + zonesPerPage = 10; var divViewZonesLoader = $("#divViewZonesLoader"); var divEditZone = $("#divEditZone"); @@ -288,9 +320,11 @@ function refreshZones(checkDisplay) { divViewZonesLoader.show(); HTTPRequest({ - url: "/api/zones/list?token=" + sessionData.token, + url: "/api/zones/list?token=" + sessionData.token + "&pageNumber=" + pageNumber + "&zonesPerPage=" + zonesPerPage, success: function (responseJSON) { var zones = responseJSON.response.zones; + var firstRowNumber = ((responseJSON.response.pageNumber - 1) * zonesPerPage) + 1; + var lastRowNumber = firstRowNumber + (zones.length - 1); var tableHtmlRows = ""; for (var i = 0; i < zones.length; i++) { @@ -349,7 +383,8 @@ function refreshZones(checkDisplay) { break; } - tableHtmlRows += ""; + tableHtmlRows += ""; + tableHtmlRows += ""; tableHtmlRows += ""; tableHtmlRows += ""; tableHtmlRows += ""; @@ -379,12 +414,54 @@ function refreshZones(checkDisplay) { tableHtmlRows += ""; } + var paginationHtml = ""; + + if (responseJSON.response.pageNumber > 1) { + paginationHtml += "
  • «
  • "; + paginationHtml += "
  • "; + } + + var pageStart = responseJSON.response.pageNumber - 5; + if (pageStart < 1) + pageStart = 1; + + var pageEnd = pageStart + 9; + if (pageEnd > responseJSON.response.totalPages) { + var endDiff = pageEnd - responseJSON.response.totalPages; + pageEnd = responseJSON.response.totalPages; + + pageStart -= endDiff; + if (pageStart < 1) + pageStart = 1; + } + + for (var i = pageStart; i <= pageEnd; i++) { + if (i == responseJSON.response.pageNumber) + paginationHtml += "
  • " + i + "
  • "; + else + paginationHtml += "
  • " + i + "
  • "; + } + + if (responseJSON.response.pageNumber < responseJSON.response.totalPages) { + paginationHtml += "
  • "; + paginationHtml += "
  • »
  • "; + } + + var statusHtml; + + if (responseJSON.response.zones.length > 0) + statusHtml = firstRowNumber + "-" + lastRowNumber + " (" + responseJSON.response.zones.length + ") of " + responseJSON.response.totalZones + " zones (page " + responseJSON.response.pageNumber + " of " + responseJSON.response.totalPages + ")"; + else + statusHtml = "0 zones"; + + $("#txtZonesPageNumber").val(responseJSON.response.pageNumber); $("#tableZonesBody").html(tableHtmlRows); - if (zones.length > 0) - $("#tableZonesFooter").html(""); - else - $("#tableZonesFooter").html(""); + $("#tableZonesTopStatus").html(statusHtml); + $("#tableZonesTopPagination").html(paginationHtml); + + $("#tableZonesFooterStatus").html(statusHtml); + $("#tableZonesFooterPagination").html(paginationHtml); divViewZonesLoader.hide(); divViewZones.show(); @@ -545,14 +622,7 @@ function deleteZoneMenu(objMenuItem) { HTTPRequest({ url: "/api/zones/delete?token=" + sessionData.token + "&zone=" + zone, success: function (responseJSON) { - $("#trZone" + id).remove(); - - var totalZones = $('#tableZones >tbody >tr').length; - - if (totalZones > 0) - $("#tableZonesFooter").html(""); - else - $("#tableZonesFooter").html(""); + refreshZones(); showAlert("success", "Zone Deleted!", "Zone '" + zone + "' was deleted successfully."); }, @@ -1235,7 +1305,19 @@ function toggleHideDnssecRecords(hideDnssecRecords) { showEditZone($("#titleEditZone").attr("data-zone")); } -function showEditZone(zone) { +function showEditZone(zone, showPageNumber) { + if (zone == null) { + zone = $("#txtZonesEdit").val(); + if (zone === "") { + showAlert("warning", "Missing!", "Please enter a zone name to start editing."); + $("#txtZonesEdit").focus(); + return; + } + } + + if (showPageNumber == null) + showPageNumber = 1; + var divViewZonesLoader = $("#divViewZonesLoader"); var divViewZones = $("#divViewZones"); var divEditZone = $("#divEditZone"); @@ -1245,8 +1327,12 @@ function showEditZone(zone) { divViewZonesLoader.show(); HTTPRequest({ - url: "/api/zones/records/get?token=" + sessionData.token + "&domain=" + zone, + url: "/api/zones/records/get?token=" + sessionData.token + "&domain=" + zone + "&zone=" + zone + "&listZone=true", success: function (responseJSON) { + zone = responseJSON.response.zone.name; + if (zone === "") + zone = "."; + var zoneType; if (responseJSON.response.zone.internal) zoneType = "Internal"; @@ -1466,12 +1552,14 @@ function showEditZone(zone) { break; } - var records = responseJSON.response.records; - var tableHtmlRows = ""; - var recordCount = 0; + if (!zoneHideDnssecRecords || (responseJSON.response.zone.dnssecStatus === "Unsigned")) { + editZoneRecords = responseJSON.response.records; + } + else { + var records = responseJSON.response.records; + editZoneRecords = []; - for (var i = 0; i < records.length; i++) { - if (zoneHideDnssecRecords) { + for (var i = 0; i < records.length; i++) { switch (records[i].type.toUpperCase()) { case "RRSIG": case "NSEC": @@ -1479,21 +1567,19 @@ function showEditZone(zone) { case "NSEC3": case "NSEC3PARAM": continue; + + default: + editZoneRecords.push(records[i]); + break; } } - - recordCount++; - tableHtmlRows += getZoneRecordRowHtml(i, zone, zoneType, records[i]); } $("#titleEditZone").text(zone === "." ? "" : zone); $("#titleEditZone").attr("data-zone", zone); - $("#tableEditZoneBody").html(tableHtmlRows); + $("#titleEditZone").attr("data-zone-type", zoneType); - if (recordCount > 0) - $("#tableEditZoneFooter").html(""); - else - $("#tableEditZoneFooter").html(""); + showEditZonePage(showPageNumber); divViewZonesLoader.hide(); divEditZone.show(); @@ -1509,7 +1595,84 @@ function showEditZone(zone) { }); } -function getZoneRecordRowHtml(id, zone, zoneType, record) { +function showEditZonePage(pageNumber) { + if (pageNumber == null) + pageNumber = Number($("#txtEditZonePageNumber").val()); + + if (pageNumber == 0) + pageNumber = 1; + + var recordsPerPage = Number($("#optEditZoneRecordsPerPage").val()); + if (recordsPerPage < 1) + recordsPerPage = 10; + + var totalRecords = editZoneRecords.length; + var totalPages = Math.floor(totalRecords / recordsPerPage) + (totalRecords % recordsPerPage > 0 ? 1 : 0); + + if ((pageNumber > totalPages) || (pageNumber < 0)) + pageNumber = totalPages; + + var start = (pageNumber - 1) * recordsPerPage; + var end = Math.min(start + recordsPerPage, totalRecords); + + var tableHtmlRows = ""; + var zone = $("#titleEditZone").attr("data-zone"); + var zoneType = $("#titleEditZone").attr("data-zone-type"); + + for (var i = start; i < end; i++) + tableHtmlRows += getZoneRecordRowHtml(i, zone, zoneType, editZoneRecords[i]); + + var paginationHtml = ""; + + if (pageNumber > 1) { + paginationHtml += "
  • «
  • "; + paginationHtml += "
  • "; + } + + var pageStart = pageNumber - 5; + if (pageStart < 1) + pageStart = 1; + + var pageEnd = pageStart + 9; + if (pageEnd > totalPages) { + var endDiff = pageEnd - totalPages; + pageEnd = totalPages; + + pageStart -= endDiff; + if (pageStart < 1) + pageStart = 1; + } + + for (var i = pageStart; i <= pageEnd; i++) { + if (i == pageNumber) + paginationHtml += "
  • " + i + "
  • "; + else + paginationHtml += "
  • " + i + "
  • "; + } + + if (pageNumber < totalPages) { + paginationHtml += "
  • "; + paginationHtml += "
  • »
  • "; + } + + var statusHtml; + + if (editZoneRecords.length > 0) + statusHtml = (start + 1) + "-" + end + " (" + (end - start) + ") of " + editZoneRecords.length + " records (page " + pageNumber + " of " + totalPages + ")"; + else + statusHtml = "0 records"; + + $("#txtEditZonePageNumber").val(pageNumber); + $("#tableEditZoneBody").html(tableHtmlRows); + + $("#tableEditZoneTopStatus").html(statusHtml); + $("#tableEditZoneTopPagination").html(paginationHtml); + + $("#tableEditZoneFooterStatus").html(statusHtml); + $("#tableEditZoneFooterPagination").html(paginationHtml); +} + +function getZoneRecordRowHtml(index, zone, zoneType, record) { var name = record.name.toLowerCase(); if (name === "") name = "."; @@ -1519,7 +1682,7 @@ function getZoneRecordRowHtml(id, zone, zoneType, record) { else name = name.replace("." + zone, ""); - var tableHtmlRow = ""; + var tableHtmlRow = ""; tableHtmlRow += ""; tableHtmlRow += ""; @@ -2022,11 +2185,11 @@ function getZoneRecordRowHtml(id, zone, zoneType, record) { } else { tableHtmlRow += ""; + tableHtmlRow += "
    "; + tableHtmlRow += ""; + tableHtmlRow += ""; + tableHtmlRow += ""; + tableHtmlRow += ""; } tableHtmlRow += ""; @@ -2221,9 +2384,9 @@ function modifyAddRecordFormByType(addMode) { $("#divAddEditRecordDataMx").hide(); $("#divAddEditRecordDataSrv").hide(); $("#divAddEditRecordDataDs").hide(); - $("#divAddEditRecordDataDs").hide(); $("#divAddEditRecordDataSshfp").hide(); $("#divAddEditRecordDataTlsa").hide(); + $("#divAddEditRecordDataCaa").hide(); $("#divAddEditRecordDataForwarder").hide(); $("#divAddEditRecordDataApplication").hide(); @@ -2722,24 +2885,15 @@ function addRecord() { $("#modalAddEditRecord").modal("hide"); if (overwrite) { - showEditZone(zone); + var currentPageNumber = Number($("#txtEditZonePageNumber").val()); + showEditZone(zone, currentPageNumber); } else { - var zoneType; - if (responseJSON.response.zone.internal) - zoneType = "Internal"; - else - zoneType = responseJSON.response.zone.type; + //update local array + editZoneRecords.unshift(responseJSON.response.addedRecord); - var id = Math.floor(Math.random() * 1000000); - var tableHtmlRow = getZoneRecordRowHtml(id, zone, zoneType, responseJSON.response.addedRecord); - $("#tableEditZoneBody").prepend(tableHtmlRow); - - var recordCount = $('#tableEditZone >tbody >tr').length; - if (recordCount > 0) - $("#tableEditZoneFooter").html(""); - else - $("#tableEditZoneFooter").html(""); + //show page + showEditZonePage(1); } showAlert("success", "Record Added!", "Resource record was added successfully."); @@ -2764,11 +2918,11 @@ function updateAddEditFormForwarderPlaceholder() { break; case "Tls": + case "Quic": $("#txtAddEditRecordDataForwarder").attr("placeholder", "dns.quad9.net (9.9.9.9:853)") break; case "Https": - case "HttpsJson": $("#txtAddEditRecordDataForwarder").attr("placeholder", "https://cloudflare-dns.com/dns-query (1.1.1.1)") break; } @@ -2901,6 +3055,10 @@ function showEditRecordModal(objBtn) { $("#rdEditRecordDataSoaZoneTransferProtocolTls").prop("checked", true); break; + case "quic": + $("#rdEditRecordDataSoaZoneTransferProtocolQuic").prop("checked", true); + break; + case "tcp": default: $("#rdEditRecordDataSoaZoneTransferProtocolTcp").prop("checked", true); @@ -3062,8 +3220,8 @@ function updateRecord() { var btn = $("#btnAddEditRecord"); var divAddEditRecordAlert = $("#divAddEditRecordAlert"); - var id = btn.attr("data-id"); - var divData = $("#data" + id); + var index = Number(btn.attr("data-id")); + var divData = $("#data" + index); var zone = $("#titleEditZone").attr("data-zone"); var type = divData.attr("data-record-type"); @@ -3514,14 +3672,18 @@ function updateRecord() { success: function (responseJSON) { $("#modalAddEditRecord").modal("hide"); + //update local array + editZoneRecords[index] = responseJSON.response.updatedRecord; + + //show record var zoneType; if (responseJSON.response.zone.internal) zoneType = "Internal"; else zoneType = responseJSON.response.zone.type; - var tableHtmlRow = getZoneRecordRowHtml(id, zone, zoneType, responseJSON.response.updatedRecord); - $("#trZoneRecord" + id).replaceWith(tableHtmlRow); + var tableHtmlRow = getZoneRecordRowHtml(index, zone, zoneType, responseJSON.response.updatedRecord); + $("#trZoneRecord" + index).replaceWith(tableHtmlRow); showAlert("success", "Record Updated!", "Resource record was updated successfully."); }, @@ -3538,8 +3700,8 @@ function updateRecord() { function updateRecordState(objBtn, disable) { var btn = $(objBtn); - var id = btn.attr("data-id"); - var divData = $("#data" + id); + var index = Number(btn.attr("data-id")); + var divData = $("#data" + index); var type = divData.attr("data-record-type"); var domain = divData.attr("data-record-name"); @@ -3632,18 +3794,21 @@ function updateRecordState(objBtn, disable) { success: function (responseJSON) { btn.button('reset'); + //update local arrays + editZoneRecords[index] = responseJSON.response.updatedRecord; + //set new state divData.attr("data-record-disabled", disable); if (disable) { - $("#btnEnableRecord" + id).show(); - $("#btnDisableRecord" + id).hide(); + $("#btnEnableRecord" + index).show(); + $("#btnDisableRecord" + index).hide(); showAlert("success", "Record Disabled!", "Resource record was disabled successfully."); } else { - $("#btnEnableRecord" + id).hide(); - $("#btnDisableRecord" + id).show(); + $("#btnEnableRecord" + index).hide(); + $("#btnDisableRecord" + index).show(); showAlert("success", "Record Enabled!", "Resource record was enabled successfully."); } @@ -3659,8 +3824,8 @@ function updateRecordState(objBtn, disable) { function deleteRecord(objBtn) { var btn = $(objBtn); - var id = btn.attr("data-id"); - var divData = $("#data" + id); + var index = btn.attr("data-id"); + var divData = $("#data" + index); var zone = $("#titleEditZone").attr("data-zone"); var domain = divData.attr("data-record-name"); @@ -3730,13 +3895,11 @@ function deleteRecord(objBtn) { HTTPRequest({ url: apiUrl, success: function (responseJSON) { - $("#trZoneRecord" + id).remove(); + //update local array + editZoneRecords.splice(index, 1); - var recordCount = $('#tableEditZone >tbody >tr').length; - if (recordCount > 0) - $("#tableEditZoneFooter").html(""); - else - $("#tableEditZoneFooter").html(""); + //show page + showEditZonePage(); showAlert("success", "Record Deleted!", "Resource record was deleted successfully."); }, @@ -3954,8 +4117,12 @@ function refreshDnssecProperties(divDnssecPropertiesLoader) { + "" + "" + "" - + "" - + "
    ").append(a("").attr({"data-action":"today",title:d.tooltips.today}).append(a("").addClass(d.icons.today)))),!d.sideBySide&&B()&&A()&&b.push(a("").append(a("").attr({"data-action":"togglePicker",title:d.tooltips.selectTime}).append(a("").addClass(d.icons.time)))),d.showClear&&b.push(a("").append(a("").attr({"data-action":"clear",title:d.tooltips.clear}).append(a("").addClass(d.icons.clear)))),d.showClose&&b.push(a("").append(a("").attr({"data-action":"close",title:d.tooltips.close}).append(a("").addClass(d.icons.close)))),a("").addClass("table-condensed").append(a("").append(a("").append(b)))},G=function(){var b=a("
    ").addClass("bootstrap-datetimepicker-widget dropdown-menu"),c=a("
    ").addClass("datepicker").append(C()),e=a("
    ").addClass("timepicker").append(E()),f=a("
      ").addClass("list-unstyled"),g=a("
    • ").addClass("picker-switch"+(d.collapse?" accordion-toggle":"")).append(F());return d.inline&&b.removeClass("dropdown-menu"),h&&b.addClass("usetwentyfour"),z("s")&&!h&&b.addClass("wider"),d.sideBySide&&B()&&A()?(b.addClass("timepicker-sbs"),"top"===d.toolbarPlacement&&b.append(g),b.append(a("
      ").addClass("row").append(c.addClass("col-md-6")).append(e.addClass("col-md-6"))),"bottom"===d.toolbarPlacement&&b.append(g),b):("top"===d.toolbarPlacement&&f.append(g),B()&&f.append(a("
    • ").addClass(d.collapse&&A()?"collapse in":"").append(c)),"default"===d.toolbarPlacement&&f.append(g),A()&&f.append(a("
    • ").addClass(d.collapse&&B()?"collapse":"").append(e)),"bottom"===d.toolbarPlacement&&f.append(g),b.append(f))},H=function(){var b,e={};return b=c.is("input")||d.inline?c.data():c.find("input").data(),b.dateOptions&&b.dateOptions instanceof Object&&(e=a.extend(!0,e,b.dateOptions)),a.each(d,function(a){var c="date"+a.charAt(0).toUpperCase()+a.slice(1);void 0!==b[c]&&(e[a]=b[c])}),e},I=function(){var b,e=(n||c).position(),f=(n||c).offset(),g=d.widgetPositioning.vertical,h=d.widgetPositioning.horizontal;if(d.widgetParent)b=d.widgetParent.append(o);else if(c.is("input"))b=c.after(o).parent();else{if(d.inline)return void(b=c.append(o));b=c,c.children().first().after(o)}if( -// Top and bottom logic -"auto"===g&&(g=f.top+1.5*o.height()>=a(window).height()+a(window).scrollTop()&&o.height()+c.outerHeight()a(window).width()?"right":"left"),"top"===g?o.addClass("top").removeClass("bottom"):o.addClass("bottom").removeClass("top"),"right"===h?o.addClass("pull-right"):o.removeClass("pull-right"), -// find the first parent element that has a relative css positioning -"relative"!==b.css("position")&&(b=b.parents().filter(function(){return"relative"===a(this).css("position")}).first()),0===b.length)throw new Error("datetimepicker component should be placed within a relative positioned container");o.css({top:"top"===g?"auto":e.top+c.outerHeight(),bottom:"top"===g?b.outerHeight()-(b===c?0:e.top):"auto",left:"left"===h?b===c?0:e.left:"auto",right:"left"===h?"auto":b.outerWidth()-c.outerWidth()-(b===c?0:e.left)})},J=function(a){"dp.change"===a.type&&(a.date&&a.date.isSame(a.oldDate)||!a.date&&!a.oldDate)||c.trigger(a)},K=function(a){"y"===a&&(a="YYYY"),J({type:"dp.update",change:a,viewDate:f.clone()})},L=function(a){o&&(a&&(k=Math.max(p,Math.min(3,k+a))),o.find(".datepicker > div").hide().filter(".datepicker-"+q[k].clsName).show())},M=function(){var b=a("
    "),c=f.clone().startOf("w").startOf("d");for(d.calendarWeeks===!0&&b.append(a(""),d.calendarWeeks&&c.append('"),k.push(c)),g="",b.isBefore(f,"M")&&(g+=" old"),b.isAfter(f,"M")&&(g+=" new"),b.isSame(e,"d")&&!m&&(g+=" active"),R(b,"d")||(g+=" disabled"),b.isSame(y(),"d")&&(g+=" today"),0!==b.day()&&6!==b.day()||(g+=" weekend"),c.append('"),b.add(1,"d");i.find("tbody").empty().append(k),T(),U(),V()}},X=function(){var b=o.find(".timepicker-hours table"),c=f.clone().startOf("d"),d=[],e=a("");for(f.hour()>11&&!h&&c.hour(12);c.isSame(f,"d")&&(h||f.hour()<12&&c.hour()<12||f.hour()>11);)c.hour()%4===0&&(e=a(""),d.push(e)),e.append('"),c.add(1,"h");b.empty().append(d)},Y=function(){for(var b=o.find(".timepicker-minutes table"),c=f.clone().startOf("h"),e=[],g=a(""),h=1===d.stepping?5:d.stepping;f.isSame(c,"h");)c.minute()%(4*h)===0&&(g=a(""),e.push(g)),g.append('"),c.add(h,"m");b.empty().append(e)},Z=function(){for(var b=o.find(".timepicker-seconds table"),c=f.clone().startOf("m"),d=[],e=a("");f.isSame(c,"m");)c.second()%20===0&&(e=a(""),d.push(e)),e.append('"),c.add(5,"s");b.empty().append(d)},$=function(){var a,b,c=o.find(".timepicker span[data-time-component]");h||(a=o.find(".timepicker [data-action=togglePeriod]"),b=e.clone().add(e.hours()>=12?-12:12,"h"),a.text(e.format("A")),R(b,"h")?a.removeClass("disabled"):a.addClass("disabled")),c.filter("[data-time-component=hours]").text(e.format(h?"HH":"hh")),c.filter("[data-time-component=minutes]").text(e.format("mm")),c.filter("[data-time-component=seconds]").text(e.format("ss")),X(),Y(),Z()},_=function(){o&&(W(),$())},aa=function(a){var b=m?null:e; -// case of calling setValue(null or false) -// case of calling setValue(null or false) -//viewDate = date.clone(); // TODO this doesn't work right on first use -return a?(a=a.clone().locale(d.locale),x()&&a.tz(d.timeZone),1!==d.stepping&&a.minutes(Math.round(a.minutes()/d.stepping)*d.stepping).seconds(0),void(R(a)?(e=a,g.val(e.format(i)),c.data("date",e.format(i)),m=!1,_(),J({type:"dp.change",date:e.clone(),oldDate:b})):(d.keepInvalid?J({type:"dp.change",date:a,oldDate:b}):g.val(m?"":e.format(i)),J({type:"dp.error",date:a,oldDate:b})))):(m=!0,g.val(""),c.data("date",""),J({type:"dp.change",date:!1,oldDate:b}),void _())},/** - * Hides the widget. Possibly will emit dp.hide - */ -ba=function(){var b=!1; -// Ignore event if in the middle of a picker transition -return o?(o.find(".collapse").each(function(){var c=a(this).data("collapse");return c&&c.transitioning?(b=!0,!1):!0}),b?l:(n&&n.hasClass("btn")&&n.toggleClass("active"),o.hide(),a(window).off("resize",I),o.off("click","[data-action]"),o.off("mousedown",!1),o.remove(),o=!1,J({type:"dp.hide",date:e.clone()}),g.blur(),k=0,f=e.clone(),l)):l},ca=function(){aa(null)},da=function(a){ -//inputDate.locale(options.locale); -return void 0===d.parseInputDate?b.isMoment(a)||(a=y(a)):a=d.parseInputDate(a),a},/******************************************************************************** - * - * Widget UI interaction functions - * - ********************************************************************************/ -ea={next:function(){var a=q[k].navFnc;f.add(q[k].navStep,a),W(),K(a)},previous:function(){var a=q[k].navFnc;f.subtract(q[k].navStep,a),W(),K(a)},pickerSwitch:function(){L(1)},selectMonth:function(b){var c=a(b.target).closest("tbody").find("span").index(a(b.target));f.month(c),k===p?(aa(e.clone().year(f.year()).month(f.month())),d.inline||ba()):(L(-1),W()),K("M")},selectYear:function(b){var c=parseInt(a(b.target).text(),10)||0;f.year(c),k===p?(aa(e.clone().year(f.year())),d.inline||ba()):(L(-1),W()),K("YYYY")},selectDecade:function(b){var c=parseInt(a(b.target).data("selection"),10)||0;f.year(c),k===p?(aa(e.clone().year(f.year())),d.inline||ba()):(L(-1),W()),K("YYYY")},selectDay:function(b){var c=f.clone();a(b.target).is(".old")&&c.subtract(1,"M"),a(b.target).is(".new")&&c.add(1,"M"),aa(c.date(parseInt(a(b.target).text(),10))),A()||d.keepOpen||d.inline||ba()},incrementHours:function(){var a=e.clone().add(1,"h");R(a,"h")&&aa(a)},incrementMinutes:function(){var a=e.clone().add(d.stepping,"m");R(a,"m")&&aa(a)},incrementSeconds:function(){var a=e.clone().add(1,"s");R(a,"s")&&aa(a)},decrementHours:function(){var a=e.clone().subtract(1,"h");R(a,"h")&&aa(a)},decrementMinutes:function(){var a=e.clone().subtract(d.stepping,"m");R(a,"m")&&aa(a)},decrementSeconds:function(){var a=e.clone().subtract(1,"s");R(a,"s")&&aa(a)},togglePeriod:function(){aa(e.clone().add(e.hours()>=12?-12:12,"h"))},togglePicker:function(b){var c,e=a(b.target),f=e.closest("ul"),g=f.find(".in"),h=f.find(".collapse:not(.in)");if(g&&g.length){if(c=g.data("collapse"),c&&c.transitioning)return;g.collapse?(// if collapse plugin is available through bootstrap.js then use it -g.collapse("hide"),h.collapse("show")):(// otherwise just toggle in class on the two views -g.removeClass("in"),h.addClass("in")),e.is("span")?e.toggleClass(d.icons.time+" "+d.icons.date):e.find("span").toggleClass(d.icons.time+" "+d.icons.date)}},showPicker:function(){o.find(".timepicker > div:not(.timepicker-picker)").hide(),o.find(".timepicker .timepicker-picker").show()},showHours:function(){o.find(".timepicker .timepicker-picker").hide(),o.find(".timepicker .timepicker-hours").show()},showMinutes:function(){o.find(".timepicker .timepicker-picker").hide(),o.find(".timepicker .timepicker-minutes").show()},showSeconds:function(){o.find(".timepicker .timepicker-picker").hide(),o.find(".timepicker .timepicker-seconds").show()},selectHour:function(b){var c=parseInt(a(b.target).text(),10);h||(e.hours()>=12?12!==c&&(c+=12):12===c&&(c=0)),aa(e.clone().hours(c)),ea.showPicker.call(l)},selectMinute:function(b){aa(e.clone().minutes(parseInt(a(b.target).text(),10))),ea.showPicker.call(l)},selectSecond:function(b){aa(e.clone().seconds(parseInt(a(b.target).text(),10))),ea.showPicker.call(l)},clear:ca,today:function(){var a=y();R(a,"d")&&aa(a)},close:ba},fa=function(b){return a(b.currentTarget).is(".disabled")?!1:(ea[a(b.currentTarget).data("action")].apply(l,arguments),!1)},/** - * Shows the widget. Possibly will emit dp.show and dp.change - */ -ga=function(){var b,c={year:function(a){return a.month(0).date(1).hours(0).seconds(0).minutes(0)},month:function(a){return a.date(1).hours(0).seconds(0).minutes(0)},day:function(a){return a.hours(0).seconds(0).minutes(0)},hour:function(a){return a.seconds(0).minutes(0)},minute:function(a){return a.seconds(0)}};// this handles clicks on the widget -return g.prop("disabled")||!d.ignoreReadonly&&g.prop("readonly")||o?l:(void 0!==g.val()&&0!==g.val().trim().length?aa(da(g.val().trim())):m&&d.useCurrent&&(d.inline||g.is("input")&&0===g.val().trim().length)&&(b=y(),"string"==typeof d.useCurrent&&(b=c[d.useCurrent](b)),aa(b)),o=G(),M(),S(),o.find(".timepicker-hours").hide(),o.find(".timepicker-minutes").hide(),o.find(".timepicker-seconds").hide(),_(),L(),a(window).on("resize",I),o.on("click","[data-action]",fa),o.on("mousedown",!1),n&&n.hasClass("btn")&&n.toggleClass("active"),I(),o.show(),d.focusOnShow&&!g.is(":focus")&&g.focus(),J({type:"dp.show"}),l)},/** - * Shows or hides the widget - */ -ha=function(){return o?ba():ga()},ia=function(a){var b,c,e,f,g=null,h=[],i={},j=a.which,k="p";w[j]=k;for(b in w)w.hasOwnProperty(b)&&w[b]===k&&(h.push(b),parseInt(b,10)!==j&&(i[b]=!0));for(b in d.keyBinds)if(d.keyBinds.hasOwnProperty(b)&&"function"==typeof d.keyBinds[b]&&(e=b.split(" "),e.length===h.length&&v[j]===e[e.length-1])){for(f=!0,c=e.length-2;c>=0;c--)if(!(v[e[c]]in i)){f=!1;break}if(f){g=d.keyBinds[b];break}}g&&(g.call(l,o),a.stopPropagation(),a.preventDefault())},ja=function(a){w[a.which]="r",a.stopPropagation(),a.preventDefault()},ka=function(b){var c=a(b.target).val().trim(),d=c?da(c):null;return aa(d),b.stopImmediatePropagation(),!1},la=function(){g.on({change:ka,blur:d.debug?"":ba,keydown:ia,keyup:ja,focus:d.allowInputToggle?ga:""}),c.is("input")?g.on({focus:ga}):n&&(n.on("click",ha),n.on("mousedown",!1))},ma=function(){g.off({change:ka,blur:blur,keydown:ia,keyup:ja,focus:d.allowInputToggle?ba:""}),c.is("input")?g.off({focus:ga}):n&&(n.off("click",ha),n.off("mousedown",!1))},na=function(b){ -// Store given enabledDates and disabledDates as keys. -// This way we can check their existence in O(1) time instead of looping through whole array. -// (for example: options.enabledDates['2014-02-27'] === true) -var c={};return a.each(b,function(){var a=da(this);a.isValid()&&(c[a.format("YYYY-MM-DD")]=!0)}),Object.keys(c).length?c:!1},oa=function(b){ -// Store given enabledHours and disabledHours as keys. -// This way we can check their existence in O(1) time instead of looping through whole array. -// (for example: options.enabledHours['2014-02-27'] === true) -var c={};return a.each(b,function(){c[this]=!0}),Object.keys(c).length?c:!1},pa=function(){var a=d.format||"L LT";i=a.replace(/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g,function(a){var b=e.localeData().longDateFormat(a)||a;return b.replace(/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g,function(a){//temp fix for #740 -return e.localeData().longDateFormat(a)||a})}),j=d.extraFormats?d.extraFormats.slice():[],j.indexOf(a)<0&&j.indexOf(i)<0&&j.push(i),h=i.toLowerCase().indexOf("a")<1&&i.replace(/\[.*?\]/g,"").indexOf("h")<1,z("y")&&(p=2),z("M")&&(p=1),z("d")&&(p=0),k=Math.max(p,k),m||aa(e)}; -// initializing element and component attributes -if(/******************************************************************************** - * - * Public API functions - * ===================== - * - * Important: Do not expose direct references to private objects or the options - * object to the outer world. Always return a clone when returning values or make - * a clone when setting a private variable. - * - ********************************************************************************/ -l.destroy=function(){ -///Destroys the widget and removes all attached event listeners -ba(),ma(),c.removeData("DateTimePicker"),c.removeData("date")},l.toggle=ha,l.show=ga,l.hide=ba,l.disable=function(){ -///Disables the input element, the component is attached to, by adding a disabled="true" attribute to it. -///If the widget was visible before that call it is hidden. Possibly emits dp.hide -return ba(),n&&n.hasClass("btn")&&n.addClass("disabled"),g.prop("disabled",!0),l},l.enable=function(){ -///Enables the input element, the component is attached to, by removing disabled attribute from it. -return n&&n.hasClass("btn")&&n.removeClass("disabled"),g.prop("disabled",!1),l},l.ignoreReadonly=function(a){if(0===arguments.length)return d.ignoreReadonly;if("boolean"!=typeof a)throw new TypeError("ignoreReadonly () expects a boolean parameter");return d.ignoreReadonly=a,l},l.options=function(b){if(0===arguments.length)return a.extend(!0,{},d);if(!(b instanceof Object))throw new TypeError("options() options parameter should be an object");return a.extend(!0,d,b),a.each(d,function(a,b){if(void 0===l[a])throw new TypeError("option "+a+" is not recognized!");l[a](b)}),l},l.date=function(a){ -/// -///Returns the component's model current date, a moment object or null if not set. -///date.clone() -/// -/// -///Sets the components model current moment to it. Passing a null value unsets the components model current moment. Parsing of the newDate parameter is made using moment library with the options.format and options.useStrict components configuration. -///Takes string, Date, moment, null parameter. -/// -if(0===arguments.length)return m?null:e.clone();if(!(null===a||"string"==typeof a||b.isMoment(a)||a instanceof Date))throw new TypeError("date() parameter must be one of [null, string, moment or Date]");return aa(null===a?null:da(a)),l},l.format=function(a){ -///test su -///info about para -///returns foo -if(0===arguments.length)return d.format;if("string"!=typeof a&&("boolean"!=typeof a||a!==!1))throw new TypeError("format() expects a string or boolean:false parameter "+a);return d.format=a,i&&pa(),l},l.timeZone=function(a){if(0===arguments.length)return d.timeZone;if("string"!=typeof a)throw new TypeError("newZone() expects a string parameter");return d.timeZone=a,l},l.dayViewHeaderFormat=function(a){if(0===arguments.length)return d.dayViewHeaderFormat;if("string"!=typeof a)throw new TypeError("dayViewHeaderFormat() expects a string parameter");return d.dayViewHeaderFormat=a,l},l.extraFormats=function(a){if(0===arguments.length)return d.extraFormats;if(a!==!1&&!(a instanceof Array))throw new TypeError("extraFormats() expects an array or false parameter");return d.extraFormats=a,j&&pa(),l},l.disabledDates=function(b){ -/// -///Returns an array with the currently set disabled dates on the component. -///options.disabledDates -/// -/// -///Setting this takes precedence over options.minDate, options.maxDate configuration. Also calling this function removes the configuration of -///options.enabledDates if such exist. -///Takes an [ string or Date or moment ] of values and allows the user to select only from those days. -/// -if(0===arguments.length)return d.disabledDates?a.extend({},d.disabledDates):d.disabledDates;if(!b)return d.disabledDates=!1,_(),l;if(!(b instanceof Array))throw new TypeError("disabledDates() expects an array parameter");return d.disabledDates=na(b),d.enabledDates=!1,_(),l},l.enabledDates=function(b){ -/// -///Returns an array with the currently set enabled dates on the component. -///options.enabledDates -/// -/// -///Setting this takes precedence over options.minDate, options.maxDate configuration. Also calling this function removes the configuration of options.disabledDates if such exist. -///Takes an [ string or Date or moment ] of values and allows the user to select only from those days. -/// -if(0===arguments.length)return d.enabledDates?a.extend({},d.enabledDates):d.enabledDates;if(!b)return d.enabledDates=!1,_(),l;if(!(b instanceof Array))throw new TypeError("enabledDates() expects an array parameter");return d.enabledDates=na(b),d.disabledDates=!1,_(),l},l.daysOfWeekDisabled=function(a){if(0===arguments.length)return d.daysOfWeekDisabled.splice(0);if("boolean"==typeof a&&!a)return d.daysOfWeekDisabled=!1,_(),l;if(!(a instanceof Array))throw new TypeError("daysOfWeekDisabled() expects an array parameter");if(d.daysOfWeekDisabled=a.reduce(function(a,b){return b=parseInt(b,10),b>6||0>b||isNaN(b)?a:(-1===a.indexOf(b)&&a.push(b),a)},[]).sort(),d.useCurrent&&!d.keepInvalid){for(var b=0;!R(e,"d");){if(e.add(1,"d"),7===b)throw"Tried 7 times to find a valid date";b++}aa(e)}return _(),l},l.maxDate=function(a){if(0===arguments.length)return d.maxDate?d.maxDate.clone():d.maxDate;if("boolean"==typeof a&&a===!1)return d.maxDate=!1,_(),l;"string"==typeof a&&("now"!==a&&"moment"!==a||(a=y()));var b=da(a);if(!b.isValid())throw new TypeError("maxDate() Could not parse date parameter: "+a);if(d.minDate&&b.isBefore(d.minDate))throw new TypeError("maxDate() date parameter is before options.minDate: "+b.format(i));return d.maxDate=b,d.useCurrent&&!d.keepInvalid&&e.isAfter(a)&&aa(d.maxDate),f.isAfter(b)&&(f=b.clone().subtract(d.stepping,"m")),_(),l},l.minDate=function(a){if(0===arguments.length)return d.minDate?d.minDate.clone():d.minDate;if("boolean"==typeof a&&a===!1)return d.minDate=!1,_(),l;"string"==typeof a&&("now"!==a&&"moment"!==a||(a=y()));var b=da(a);if(!b.isValid())throw new TypeError("minDate() Could not parse date parameter: "+a);if(d.maxDate&&b.isAfter(d.maxDate))throw new TypeError("minDate() date parameter is after options.maxDate: "+b.format(i));return d.minDate=b,d.useCurrent&&!d.keepInvalid&&e.isBefore(a)&&aa(d.minDate),f.isBefore(b)&&(f=b.clone().add(d.stepping,"m")),_(),l},l.defaultDate=function(a){ -/// -///Returns a moment with the options.defaultDate option configuration or false if not set -///date.clone() -/// -/// -///Will set the picker's inital date. If a boolean:false value is passed the options.defaultDate parameter is cleared. -///Takes a string, Date, moment, boolean:false -/// -if(0===arguments.length)return d.defaultDate?d.defaultDate.clone():d.defaultDate;if(!a)return d.defaultDate=!1,l;"string"==typeof a&&(a="now"===a||"moment"===a?y():y(a));var b=da(a);if(!b.isValid())throw new TypeError("defaultDate() Could not parse date parameter: "+a);if(!R(b))throw new TypeError("defaultDate() date passed is invalid according to component setup validations");return d.defaultDate=b,(d.defaultDate&&d.inline||""===g.val().trim())&&aa(d.defaultDate),l},l.locale=function(a){if(0===arguments.length)return d.locale;if(!b.localeData(a))throw new TypeError("locale() locale "+a+" is not loaded from moment locales!");return d.locale=a,e.locale(d.locale),f.locale(d.locale),i&&pa(),o&&(ba(),ga()),l},l.stepping=function(a){return 0===arguments.length?d.stepping:(a=parseInt(a,10),(isNaN(a)||1>a)&&(a=1),d.stepping=a,l)},l.useCurrent=function(a){var b=["year","month","day","hour","minute"];if(0===arguments.length)return d.useCurrent;if("boolean"!=typeof a&&"string"!=typeof a)throw new TypeError("useCurrent() expects a boolean or string parameter");if("string"==typeof a&&-1===b.indexOf(a.toLowerCase()))throw new TypeError("useCurrent() expects a string parameter of "+b.join(", "));return d.useCurrent=a,l},l.collapse=function(a){if(0===arguments.length)return d.collapse;if("boolean"!=typeof a)throw new TypeError("collapse() expects a boolean parameter");return d.collapse===a?l:(d.collapse=a,o&&(ba(),ga()),l)},l.icons=function(b){if(0===arguments.length)return a.extend({},d.icons);if(!(b instanceof Object))throw new TypeError("icons() expects parameter to be an Object");return a.extend(d.icons,b),o&&(ba(),ga()),l},l.tooltips=function(b){if(0===arguments.length)return a.extend({},d.tooltips);if(!(b instanceof Object))throw new TypeError("tooltips() expects parameter to be an Object");return a.extend(d.tooltips,b),o&&(ba(),ga()),l},l.useStrict=function(a){if(0===arguments.length)return d.useStrict;if("boolean"!=typeof a)throw new TypeError("useStrict() expects a boolean parameter");return d.useStrict=a,l},l.sideBySide=function(a){if(0===arguments.length)return d.sideBySide;if("boolean"!=typeof a)throw new TypeError("sideBySide() expects a boolean parameter");return d.sideBySide=a,o&&(ba(),ga()),l},l.viewMode=function(a){if(0===arguments.length)return d.viewMode;if("string"!=typeof a)throw new TypeError("viewMode() expects a string parameter");if(-1===r.indexOf(a))throw new TypeError("viewMode() parameter must be one of ("+r.join(", ")+") value");return d.viewMode=a,k=Math.max(r.indexOf(a),p),L(),l},l.toolbarPlacement=function(a){if(0===arguments.length)return d.toolbarPlacement;if("string"!=typeof a)throw new TypeError("toolbarPlacement() expects a string parameter");if(-1===u.indexOf(a))throw new TypeError("toolbarPlacement() parameter must be one of ("+u.join(", ")+") value");return d.toolbarPlacement=a,o&&(ba(),ga()),l},l.widgetPositioning=function(b){if(0===arguments.length)return a.extend({},d.widgetPositioning);if("[object Object]"!=={}.toString.call(b))throw new TypeError("widgetPositioning() expects an object variable");if(b.horizontal){if("string"!=typeof b.horizontal)throw new TypeError("widgetPositioning() horizontal variable must be a string");if(b.horizontal=b.horizontal.toLowerCase(),-1===t.indexOf(b.horizontal))throw new TypeError("widgetPositioning() expects horizontal parameter to be one of ("+t.join(", ")+")");d.widgetPositioning.horizontal=b.horizontal}if(b.vertical){if("string"!=typeof b.vertical)throw new TypeError("widgetPositioning() vertical variable must be a string");if(b.vertical=b.vertical.toLowerCase(),-1===s.indexOf(b.vertical))throw new TypeError("widgetPositioning() expects vertical parameter to be one of ("+s.join(", ")+")");d.widgetPositioning.vertical=b.vertical}return _(),l},l.calendarWeeks=function(a){if(0===arguments.length)return d.calendarWeeks;if("boolean"!=typeof a)throw new TypeError("calendarWeeks() expects parameter to be a boolean value");return d.calendarWeeks=a,_(),l},l.showTodayButton=function(a){if(0===arguments.length)return d.showTodayButton;if("boolean"!=typeof a)throw new TypeError("showTodayButton() expects a boolean parameter");return d.showTodayButton=a,o&&(ba(),ga()),l},l.showClear=function(a){if(0===arguments.length)return d.showClear;if("boolean"!=typeof a)throw new TypeError("showClear() expects a boolean parameter");return d.showClear=a,o&&(ba(),ga()),l},l.widgetParent=function(b){if(0===arguments.length)return d.widgetParent;if("string"==typeof b&&(b=a(b)),null!==b&&"string"!=typeof b&&!(b instanceof a))throw new TypeError("widgetParent() expects a string or a jQuery object parameter");return d.widgetParent=b,o&&(ba(),ga()),l},l.keepOpen=function(a){if(0===arguments.length)return d.keepOpen;if("boolean"!=typeof a)throw new TypeError("keepOpen() expects a boolean parameter");return d.keepOpen=a,l},l.focusOnShow=function(a){if(0===arguments.length)return d.focusOnShow;if("boolean"!=typeof a)throw new TypeError("focusOnShow() expects a boolean parameter");return d.focusOnShow=a,l},l.inline=function(a){if(0===arguments.length)return d.inline;if("boolean"!=typeof a)throw new TypeError("inline() expects a boolean parameter");return d.inline=a,l},l.clear=function(){return ca(),l},l.keyBinds=function(a){return 0===arguments.length?d.keyBinds:(d.keyBinds=a,l)},l.getMoment=function(a){return y(a)},l.debug=function(a){if("boolean"!=typeof a)throw new TypeError("debug() expects a boolean parameter");return d.debug=a,l},l.allowInputToggle=function(a){if(0===arguments.length)return d.allowInputToggle;if("boolean"!=typeof a)throw new TypeError("allowInputToggle() expects a boolean parameter");return d.allowInputToggle=a,l},l.showClose=function(a){if(0===arguments.length)return d.showClose;if("boolean"!=typeof a)throw new TypeError("showClose() expects a boolean parameter");return d.showClose=a,l},l.keepInvalid=function(a){if(0===arguments.length)return d.keepInvalid;if("boolean"!=typeof a)throw new TypeError("keepInvalid() expects a boolean parameter");return d.keepInvalid=a,l},l.datepickerInput=function(a){if(0===arguments.length)return d.datepickerInput;if("string"!=typeof a)throw new TypeError("datepickerInput() expects a string parameter");return d.datepickerInput=a,l},l.parseInputDate=function(a){if(0===arguments.length)return d.parseInputDate;if("function"!=typeof a)throw new TypeError("parseInputDate() sholud be as function");return d.parseInputDate=a,l},l.disabledTimeIntervals=function(b){ -/// -///Returns an array with the currently set disabled dates on the component. -///options.disabledTimeIntervals -/// -/// -///Setting this takes precedence over options.minDate, options.maxDate configuration. Also calling this function removes the configuration of -///options.enabledDates if such exist. -///Takes an [ string or Date or moment ] of values and allows the user to select only from those days. -/// -if(0===arguments.length)return d.disabledTimeIntervals?a.extend({},d.disabledTimeIntervals):d.disabledTimeIntervals;if(!b)return d.disabledTimeIntervals=!1,_(),l;if(!(b instanceof Array))throw new TypeError("disabledTimeIntervals() expects an array parameter");return d.disabledTimeIntervals=b,_(),l},l.disabledHours=function(b){ -/// -///Returns an array with the currently set disabled hours on the component. -///options.disabledHours -/// -/// -///Setting this takes precedence over options.minDate, options.maxDate configuration. Also calling this function removes the configuration of -///options.enabledHours if such exist. -///Takes an [ int ] of values and disallows the user to select only from those hours. -/// -if(0===arguments.length)return d.disabledHours?a.extend({},d.disabledHours):d.disabledHours;if(!b)return d.disabledHours=!1,_(),l;if(!(b instanceof Array))throw new TypeError("disabledHours() expects an array parameter");if(d.disabledHours=oa(b),d.enabledHours=!1,d.useCurrent&&!d.keepInvalid){for(var c=0;!R(e,"h");){if(e.add(1,"h"),24===c)throw"Tried 24 times to find a valid date";c++}aa(e)}return _(),l},l.enabledHours=function(b){ -/// -///Returns an array with the currently set enabled hours on the component. -///options.enabledHours -/// -/// -///Setting this takes precedence over options.minDate, options.maxDate configuration. Also calling this function removes the configuration of options.disabledHours if such exist. -///Takes an [ int ] of values and allows the user to select only from those hours. -/// -if(0===arguments.length)return d.enabledHours?a.extend({},d.enabledHours):d.enabledHours;if(!b)return d.enabledHours=!1,_(),l;if(!(b instanceof Array))throw new TypeError("enabledHours() expects an array parameter");if(d.enabledHours=oa(b),d.disabledHours=!1,d.useCurrent&&!d.keepInvalid){for(var c=0;!R(e,"h");){if(e.add(1,"h"),24===c)throw"Tried 24 times to find a valid date";c++}aa(e)}return _(),l},/** - * Returns the component's model current viewDate, a moment object or null if not set. Passing a null value unsets the components model current moment. Parsing of the newDate parameter is made using moment library with the options.format and options.useStrict components configuration. - * @param {Takes string, viewDate, moment, null parameter.} newDate - * @returns {viewDate.clone()} - */ -l.viewDate=function(a){if(0===arguments.length)return f.clone();if(!a)return f=e.clone(),l;if(!("string"==typeof a||b.isMoment(a)||a instanceof Date))throw new TypeError("viewDate() parameter must be one of [string, moment or Date]");return f=da(a),K(),l},c.is("input"))g=c;else if(g=c.find(d.datepickerInput),0===g.length)g=c.find("input");else if(!g.is("input"))throw new Error('CSS class "'+d.datepickerInput+'" cannot be applied to non input element');if(c.hasClass("input-group")&&( -// in case there is more then one 'input-group-addon' Issue #48 -n=0===c.find(".datepickerbutton").length?c.find(".input-group-addon"):c.find(".datepickerbutton")),!d.inline&&!g.is("input"))throw new Error("Could not initialize DateTimePicker without an input element"); -// Set defaults for date here now instead of in var declaration -return e=y(),f=e.clone(),a.extend(!0,d,H()),l.options(d),pa(),la(),g.prop("disabled")&&l.disable(),g.is("input")&&0!==g.val().trim().length?aa(da(g.val().trim())):d.defaultDate&&void 0===g.attr("placeholder")&&aa(d.defaultDate),d.inline&&ga(),l};/******************************************************************************** - * - * jQuery plugin constructor and defaults object - * - ********************************************************************************/ -/** - * See (http://jquery.com/). - * @name jQuery - * @class - * See the jQuery Library (http://jquery.com/) for full details. This just - * documents the function and classes that are added to jQuery by this plug-in. - */ -/** - * See (http://jquery.com/) - * @name fn - * @class - * See the jQuery Library (http://jquery.com/) for full details. This just - * documents the function and classes that are added to jQuery by this plug-in. - * @memberOf jQuery - */ -/** - * Show comments - * @class datetimepicker - * @memberOf jQuery.fn - */ -a.fn.datetimepicker=function(b){b=b||{};var d,e=Array.prototype.slice.call(arguments,1),f=!0,g=["destroy","hide","show","toggle"];if("object"==typeof b)return this.each(function(){var d=a(this);d.data("DateTimePicker")||(b=a.extend(!0,{},a.fn.datetimepicker.defaults,b),d.data("DateTimePicker",c(d,b)))});if("string"==typeof b)return this.each(function(){var c=a(this),g=c.data("DateTimePicker");if(!g)throw new Error('bootstrap-datetimepicker("'+b+'") method was called on an element that is not using DateTimePicker');d=g[b].apply(g,e),f=d===g}),f||a.inArray(b,g)>-1?this:d;throw new TypeError("Invalid arguments for DateTimePicker: "+b)},a.fn.datetimepicker.defaults={timeZone:"",format:!1,dayViewHeaderFormat:"MMMM YYYY",extraFormats:!1,stepping:1,minDate:!1,maxDate:!1,useCurrent:!0,collapse:!0,locale:b.locale(),defaultDate:!1,disabledDates:!1,enabledDates:!1,icons:{time:"glyphicon glyphicon-time",date:"glyphicon glyphicon-calendar",up:"glyphicon glyphicon-chevron-up",down:"glyphicon glyphicon-chevron-down",previous:"glyphicon glyphicon-chevron-left",next:"glyphicon glyphicon-chevron-right",today:"glyphicon glyphicon-screenshot",clear:"glyphicon glyphicon-trash",close:"glyphicon glyphicon-remove"},tooltips:{today:"Go to today",clear:"Clear selection",close:"Close the picker",selectMonth:"Select Month",prevMonth:"Previous Month",nextMonth:"Next Month",selectYear:"Select Year",prevYear:"Previous Year",nextYear:"Next Year",selectDecade:"Select Decade",prevDecade:"Previous Decade",nextDecade:"Next Decade",prevCentury:"Previous Century",nextCentury:"Next Century",pickHour:"Pick Hour",incrementHour:"Increment Hour",decrementHour:"Decrement Hour",pickMinute:"Pick Minute",incrementMinute:"Increment Minute",decrementMinute:"Decrement Minute",pickSecond:"Pick Second",incrementSecond:"Increment Second",decrementSecond:"Decrement Second",togglePeriod:"Toggle Period",selectTime:"Select Time"},useStrict:!1,sideBySide:!1,daysOfWeekDisabled:!1,calendarWeeks:!1,viewMode:"days",toolbarPlacement:"default",showTodayButton:!1,showClear:!1,showClose:!1,widgetPositioning:{horizontal:"auto",vertical:"auto"},widgetParent:null,ignoreReadonly:!1,keepOpen:!1,focusOnShow:!0,inline:!1,keepInvalid:!1,datepickerInput:".datepickerinput",keyBinds:{up:function(a){if(a){var b=this.date()||this.getMoment();a.find(".datepicker").is(":visible")?this.date(b.clone().subtract(7,"d")):this.date(b.clone().add(this.stepping(),"m"))}},down:function(a){if(!a)return void this.show();var b=this.date()||this.getMoment();a.find(".datepicker").is(":visible")?this.date(b.clone().add(7,"d")):this.date(b.clone().subtract(this.stepping(),"m"))},"control up":function(a){if(a){var b=this.date()||this.getMoment();a.find(".datepicker").is(":visible")?this.date(b.clone().subtract(1,"y")):this.date(b.clone().add(1,"h"))}},"control down":function(a){if(a){var b=this.date()||this.getMoment();a.find(".datepicker").is(":visible")?this.date(b.clone().add(1,"y")):this.date(b.clone().subtract(1,"h"))}},left:function(a){if(a){var b=this.date()||this.getMoment();a.find(".datepicker").is(":visible")&&this.date(b.clone().subtract(1,"d"))}},right:function(a){if(a){var b=this.date()||this.getMoment();a.find(".datepicker").is(":visible")&&this.date(b.clone().add(1,"d"))}},pageUp:function(a){if(a){var b=this.date()||this.getMoment();a.find(".datepicker").is(":visible")&&this.date(b.clone().subtract(1,"M"))}},pageDown:function(a){if(a){var b=this.date()||this.getMoment();a.find(".datepicker").is(":visible")&&this.date(b.clone().add(1,"M"))}},enter:function(){this.hide()},escape:function(){this.hide()}, -//tab: function (widget) { //this break the flow of the form. disabling for now -// var toggle = widget.find('.picker-switch a[data-action="togglePicker"]'); -// if(toggle.length > 0) toggle.click(); -//}, -"control space":function(a){a.find(".timepicker").is(":visible")&&a.find('.btn[data-action="togglePeriod"]').click()},t:function(){this.date(this.getMoment())},"delete":function(){this.clear()}},debug:!1,allowInputToggle:!1,disabledTimeIntervals:!1,disabledHours:!1,enabledHours:!1,viewDate:!1},"undefined"!=typeof module&&(module.exports=a.fn.datetimepicker)}); \ No newline at end of file diff --git a/DnsServerCore/www/js/common.js b/DnsServerCore/www/js/common.js index b953a25b..b1618774 100644 --- a/DnsServerCore/www/js/common.js +++ b/DnsServerCore/www/js/common.js @@ -252,7 +252,7 @@ function sortTable(tableId, n) { } function serializeTableData(table, columns, objAlertPlaceholder) { - var data = table.find('input:text, input:checkbox, input:hidden, select'); + var data = table.find('input:text, :input[type="number"], input:checkbox, input:hidden, select'); var output = ""; for (var i = 0; i < data.length; i += columns) { diff --git a/DnsServerCore/www/js/dhcp.js b/DnsServerCore/www/js/dhcp.js index 3e61fac4..a7313b45 100644 --- a/DnsServerCore/www/js/dhcp.js +++ b/DnsServerCore/www/js/dhcp.js @@ -250,6 +250,16 @@ function addDhcpScopeVendorInfoRow(identifier, information) { $("#tableDhcpScopeVendorInfo").append(tableHtmlRows); } +function addDhcpScopeGenericOptionsRow(optionCode, hexValue) { + var id = Math.floor(Math.random() * 10000); + + var tableHtmlRows = ""; + tableHtmlRows += ""; + tableHtmlRows += ""; + + $("#tableDhcpScopeGenericOptions").append(tableHtmlRows); +} + function addDhcpScopeExclusionRow(startingAddress, endingAddress) { var id = Math.floor(Math.random() * 10000); @@ -303,9 +313,12 @@ function clearDhcpScopeForm() { $("#tableDhcpScopeStaticRoutes").html(""); $("#tableDhcpScopeVendorInfo").html(""); $("#txtDhcpScopeCAPWAPApIpAddresses").val(""); + $("#txtDhcpScopeTftpServerAddresses").val(""); + $("#tableDhcpScopeGenericOptions").html(""); $("#tableDhcpScopeExclusions").html(""); $("#tableDhcpScopeReservedLeases").html(""); $("#chkAllowOnlyReservedLeases").prop("checked", false); + $("#chkBlockLocallyAdministeredMacAddresses").prop("checked", false); $("#btnSaveDhcpScope").button('reset'); } @@ -393,13 +406,22 @@ function showEditDhcpScope(scopeName) { if (responseJSON.response.vendorInfo != null) { for (var i = 0; i < responseJSON.response.vendorInfo.length; i++) { - addDhcpScopeVendorInfoRow(responseJSON.response.vendorInfo[i].identifier, responseJSON.response.vendorInfo[i].information) + addDhcpScopeVendorInfoRow(responseJSON.response.vendorInfo[i].identifier, responseJSON.response.vendorInfo[i].information); } } if (responseJSON.response.capwapAcIpAddresses != null) $("#txtDhcpScopeCAPWAPApIpAddresses").val(responseJSON.response.capwapAcIpAddresses.join("\n")); + if (responseJSON.response.tftpServerAddresses != null) + $("#txtDhcpScopeTftpServerAddresses").val(responseJSON.response.tftpServerAddresses.join("\n")); + + if (responseJSON.response.genericOptions != null) { + for (var i = 0; i < responseJSON.response.genericOptions.length; i++) { + addDhcpScopeGenericOptionsRow(responseJSON.response.genericOptions[i].code, responseJSON.response.genericOptions[i].value); + } + } + if (responseJSON.response.exclusions != null) { for (var i = 0; i < responseJSON.response.exclusions.length; i++) { addDhcpScopeExclusionRow(responseJSON.response.exclusions[i].startingAddress, responseJSON.response.exclusions[i].endingAddress); @@ -474,6 +496,12 @@ function saveDhcpScope() { var capwapAcIpAddresses = cleanTextList($("#txtDhcpScopeCAPWAPApIpAddresses").val()); + var tftpServerAddresses = cleanTextList($("#txtDhcpScopeTftpServerAddresses").val()); + + var genericOptions = serializeTableData($("#tableDhcpScopeGenericOptions"), 2); + if (genericOptions === false) + return; + var exclusions = serializeTableData($("#tableDhcpScopeExclusions"), 2); if (exclusions === false) return; @@ -492,7 +520,7 @@ function saveDhcpScope() { "&leaseTimeDays=" + leaseTimeDays + "&leaseTimeHours=" + leaseTimeHours + "&leaseTimeMinutes=" + leaseTimeMinutes + "&offerDelayTime=" + offerDelayTime + "&pingCheckEnabled=" + pingCheckEnabled + "&pingCheckTimeout=" + pingCheckTimeout + "&pingCheckRetries=" + pingCheckRetries + "&domainName=" + encodeURIComponent(domainName) + "&domainSearchList=" + encodeURIComponent(domainSearchList) + "&dnsUpdates=" + dnsUpdates + "&dnsTtl=" + dnsTtl + "&serverAddress=" + encodeURIComponent(serverAddress) + "&serverHostName=" + encodeURIComponent(serverHostName) + "&bootFileName=" + encodeURIComponent(bootFileName) + "&routerAddress=" + encodeURIComponent(routerAddress) + "&useThisDnsServer=" + useThisDnsServer + (useThisDnsServer ? "" : "&dnsServers=" + encodeURIComponent(dnsServers)) + "&winsServers=" + encodeURIComponent(winsServers) + "&ntpServers=" + encodeURIComponent(ntpServers) + "&ntpServerDomainNames=" + encodeURIComponent(ntpServerDomainNames) + - "&staticRoutes=" + encodeURIComponent(staticRoutes) + "&vendorInfo=" + encodeURIComponent(vendorInfo) + "&capwapAcIpAddresses=" + encodeURIComponent(capwapAcIpAddresses) + "&exclusions=" + encodeURIComponent(exclusions) + "&reservedLeases=" + encodeURIComponent(reservedLeases) + "&allowOnlyReservedLeases=" + allowOnlyReservedLeases + "&blockLocallyAdministeredMacAddresses=" + blockLocallyAdministeredMacAddresses, + "&staticRoutes=" + encodeURIComponent(staticRoutes) + "&vendorInfo=" + encodeURIComponent(vendorInfo) + "&capwapAcIpAddresses=" + encodeURIComponent(capwapAcIpAddresses) + "&tftpServerAddresses=" + encodeURIComponent(tftpServerAddresses) + "&genericOptions=" + encodeURIComponent(genericOptions) + "&exclusions=" + encodeURIComponent(exclusions) + "&reservedLeases=" + encodeURIComponent(reservedLeases) + "&allowOnlyReservedLeases=" + allowOnlyReservedLeases + "&blockLocallyAdministeredMacAddresses=" + blockLocallyAdministeredMacAddresses, success: function (responseJSON) { refreshDhcpScopes(); btn.button('reset'); diff --git a/DnsServerCore/www/js/jquery-ui.min.js b/DnsServerCore/www/js/jquery-ui.min.js deleted file mode 100644 index ab370a90..00000000 --- a/DnsServerCore/www/js/jquery-ui.min.js +++ /dev/null @@ -1,6 +0,0 @@ -/*! jQuery UI - v1.12.1 - 2021-02-13 -* http://jqueryui.com -* Includes: keycode.js, widgets/datepicker.js -* Copyright jQuery Foundation and other contributors; Licensed MIT */ - -!function(e){"function"==typeof define&&define.amd?define(["jquery"],e):e(jQuery)}(function(M){M.ui=M.ui||{};var r;M.ui.version="1.12.1",M.ui.keyCode={BACKSPACE:8,COMMA:188,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,LEFT:37,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SPACE:32,TAB:9,UP:38};function e(){this._curInst=null,this._keyEvent=!1,this._disabledInputs=[],this._datepickerShowing=!1,this._inDialog=!1,this._mainDivId="ui-datepicker-div",this._inlineClass="ui-datepicker-inline",this._appendClass="ui-datepicker-append",this._triggerClass="ui-datepicker-trigger",this._dialogClass="ui-datepicker-dialog",this._disableClass="ui-datepicker-disabled",this._unselectableClass="ui-datepicker-unselectable",this._currentClass="ui-datepicker-current-day",this._dayOverClass="ui-datepicker-days-cell-over",this.regional=[],this.regional[""]={closeText:"Done",prevText:"Prev",nextText:"Next",currentText:"Today",monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],dayNames:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],dayNamesShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],dayNamesMin:["Su","Mo","Tu","We","Th","Fr","Sa"],weekHeader:"Wk",dateFormat:"mm/dd/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""},this._defaults={showOn:"focus",showAnim:"fadeIn",showOptions:{},defaultDate:null,appendText:"",buttonText:"...",buttonImage:"",buttonImageOnly:!1,hideIfNoPrevNext:!1,navigationAsDateFormat:!1,gotoCurrent:!1,changeMonth:!1,changeYear:!1,yearRange:"c-10:c+10",showOtherMonths:!1,selectOtherMonths:!1,showWeek:!1,calculateWeek:this.iso8601Week,shortYearCutoff:"+10",minDate:null,maxDate:null,duration:"fast",beforeShowDay:null,beforeShow:null,onSelect:null,onChangeMonthYear:null,onClose:null,numberOfMonths:1,showCurrentAtPos:0,stepMonths:1,stepBigMonths:12,altField:"",altFormat:"",constrainInput:!0,showButtonPanel:!1,autoSize:!1,disabled:!1},M.extend(this._defaults,this.regional[""]),this.regional.en=M.extend(!0,{},this.regional[""]),this.regional["en-US"]=M.extend(!0,{},this.regional.en),this.dpDiv=a(M("
    "))}function a(e){var t="button, .ui-datepicker-prev, .ui-datepicker-next, .ui-datepicker-calendar td a";return e.on("mouseout",t,function(){M(this).removeClass("ui-state-hover"),-1!==this.className.indexOf("ui-datepicker-prev")&&M(this).removeClass("ui-datepicker-prev-hover"),-1!==this.className.indexOf("ui-datepicker-next")&&M(this).removeClass("ui-datepicker-next-hover")}).on("mouseover",t,n)}function n(){M.datepicker._isDisabledDatepicker((r.inline?r.dpDiv.parent():r.input)[0])||(M(this).parents(".ui-datepicker-calendar").find("a").removeClass("ui-state-hover"),M(this).addClass("ui-state-hover"),-1!==this.className.indexOf("ui-datepicker-prev")&&M(this).addClass("ui-datepicker-prev-hover"),-1!==this.className.indexOf("ui-datepicker-next")&&M(this).addClass("ui-datepicker-next-hover"))}function o(e,t){for(var a in M.extend(e,t),t)null==t[a]&&(e[a]=t[a]);return e}M.extend(M.ui,{datepicker:{version:"1.12.1"}}),M.extend(e.prototype,{markerClassName:"hasDatepicker",maxRows:4,_widgetDatepicker:function(){return this.dpDiv},setDefaults:function(e){return o(this._defaults,e||{}),this},_attachDatepicker:function(e,t){var a,i=e.nodeName.toLowerCase(),s="div"===i||"span"===i;e.id||(this.uuid+=1,e.id="dp"+this.uuid),(a=this._newInst(M(e),s)).settings=M.extend({},t||{}),"input"===i?this._connectDatepicker(e,a):s&&this._inlineDatepicker(e,a)},_newInst:function(e,t){return{id:e[0].id.replace(/([^A-Za-z0-9_\-])/g,"\\\\$1"),input:e,selectedDay:0,selectedMonth:0,selectedYear:0,drawMonth:0,drawYear:0,inline:t,dpDiv:t?a(M("
    ")):this.dpDiv}},_connectDatepicker:function(e,t){var a=M(e);t.append=M([]),t.trigger=M([]),a.hasClass(this.markerClassName)||(this._attachments(a,t),a.addClass(this.markerClassName).on("keydown",this._doKeyDown).on("keypress",this._doKeyPress).on("keyup",this._doKeyUp),this._autoSize(t),M.data(e,"datepicker",t),t.settings.disabled&&this._disableDatepicker(e))},_attachments:function(e,t){var a,i=this._get(t,"appendText"),s=this._get(t,"isRTL");t.append&&t.append.remove(),i&&(t.append=M(""+i+""),e[s?"before":"after"](t.append)),e.off("focus",this._showDatepicker),t.trigger&&t.trigger.remove(),"focus"!==(a=this._get(t,"showOn"))&&"both"!==a||e.on("focus",this._showDatepicker),"button"!==a&&"both"!==a||(i=this._get(t,"buttonText"),a=this._get(t,"buttonImage"),t.trigger=M(this._get(t,"buttonImageOnly")?M("").addClass(this._triggerClass).attr({src:a,alt:i,title:i}):M("").addClass(this._triggerClass).html(a?M("").attr({src:a,alt:i,title:i}):i)),e[s?"before":"after"](t.trigger),t.trigger.on("click",function(){return M.datepicker._datepickerShowing&&M.datepicker._lastInput===e[0]?M.datepicker._hideDatepicker():(M.datepicker._datepickerShowing&&M.datepicker._lastInput!==e[0]&&M.datepicker._hideDatepicker(),M.datepicker._showDatepicker(e[0])),!1}))},_autoSize:function(e){var t,a,i,s,r,n;this._get(e,"autoSize")&&!e.inline&&(r=new Date(2009,11,20),(n=this._get(e,"dateFormat")).match(/[DM]/)&&(t=function(e){for(s=i=a=0;sa&&(a=e[s].length,i=s);return i},r.setMonth(t(this._get(e,n.match(/MM/)?"monthNames":"monthNamesShort"))),r.setDate(t(this._get(e,n.match(/DD/)?"dayNames":"dayNamesShort"))+20-r.getDay())),e.input.attr("size",this._formatDate(e,r).length))},_inlineDatepicker:function(e,t){var a=M(e);a.hasClass(this.markerClassName)||(a.addClass(this.markerClassName).append(t.dpDiv),M.data(e,"datepicker",t),this._setDate(t,this._getDefaultDate(t),!0),this._updateDatepicker(t),this._updateAlternate(t),t.settings.disabled&&this._disableDatepicker(e),t.dpDiv.css("display","block"))},_dialogDatepicker:function(e,t,a,i,s){var r,n=this._dialogInst;return n||(this.uuid+=1,r="dp"+this.uuid,this._dialogInput=M(""),this._dialogInput.on("keydown",this._doKeyDown),M("body").append(this._dialogInput),(n=this._dialogInst=this._newInst(this._dialogInput,!1)).settings={},M.data(this._dialogInput[0],"datepicker",n)),o(n.settings,i||{}),t=t&&t.constructor===Date?this._formatDate(n,t):t,this._dialogInput.val(t),this._pos=s?s.length?s:[s.pageX,s.pageY]:null,this._pos||(r=document.documentElement.clientWidth,i=document.documentElement.clientHeight,t=document.documentElement.scrollLeft||document.body.scrollLeft,s=document.documentElement.scrollTop||document.body.scrollTop,this._pos=[r/2-100+t,i/2-150+s]),this._dialogInput.css("left",this._pos[0]+20+"px").css("top",this._pos[1]+"px"),n.settings.onSelect=a,this._inDialog=!0,this.dpDiv.addClass(this._dialogClass),this._showDatepicker(this._dialogInput[0]),M.blockUI&&M.blockUI(this.dpDiv),M.data(this._dialogInput[0],"datepicker",n),this},_destroyDatepicker:function(e){var t,a=M(e),i=M.data(e,"datepicker");a.hasClass(this.markerClassName)&&(t=e.nodeName.toLowerCase(),M.removeData(e,"datepicker"),"input"===t?(i.append.remove(),i.trigger.remove(),a.removeClass(this.markerClassName).off("focus",this._showDatepicker).off("keydown",this._doKeyDown).off("keypress",this._doKeyPress).off("keyup",this._doKeyUp)):"div"!==t&&"span"!==t||a.removeClass(this.markerClassName).empty(),r===i&&(r=null))},_enableDatepicker:function(t){var e,a=M(t),i=M.data(t,"datepicker");a.hasClass(this.markerClassName)&&("input"===(e=t.nodeName.toLowerCase())?(t.disabled=!1,i.trigger.filter("button").each(function(){this.disabled=!1}).end().filter("img").css({opacity:"1.0",cursor:""})):"div"!==e&&"span"!==e||((a=a.children("."+this._inlineClass)).children().removeClass("ui-state-disabled"),a.find("select.ui-datepicker-month, select.ui-datepicker-year").prop("disabled",!1)),this._disabledInputs=M.map(this._disabledInputs,function(e){return e===t?null:e}))},_disableDatepicker:function(t){var e,a=M(t),i=M.data(t,"datepicker");a.hasClass(this.markerClassName)&&("input"===(e=t.nodeName.toLowerCase())?(t.disabled=!0,i.trigger.filter("button").each(function(){this.disabled=!0}).end().filter("img").css({opacity:"0.5",cursor:"default"})):"div"!==e&&"span"!==e||((a=a.children("."+this._inlineClass)).children().addClass("ui-state-disabled"),a.find("select.ui-datepicker-month, select.ui-datepicker-year").prop("disabled",!0)),this._disabledInputs=M.map(this._disabledInputs,function(e){return e===t?null:e}),this._disabledInputs[this._disabledInputs.length]=t)},_isDisabledDatepicker:function(e){if(!e)return!1;for(var t=0;td&&io&&st;)--z<0&&(z=11,B--);for(e.drawMonth=z,e.drawYear=B,P=this._get(e,"prevText"),P=E?this.formatDate(P,this._daylightSavingAdjust(new Date(B,z-T,1)),this._getFormatConfig(e)):P,a=this._canAdjustMonth(e,-1,B,z)?""+P+"":O?"":""+P+"",P=this._get(e,"nextText"),P=E?this.formatDate(P,this._daylightSavingAdjust(new Date(B,z+T,1)),this._getFormatConfig(e)):P,i=this._canAdjustMonth(e,1,B,z)?""+P+"":O?"":""+P+"",O=this._get(e,"currentText"),P=this._get(e,"gotoCurrent")&&e.currentDay?W:A,O=E?this.formatDate(O,P,this._getFormatConfig(e)):O,E=e.inline?"":"",E=j?"
    "+(K?E:"")+(this._isInRange(e,P)?"":"")+(K?"":E)+"
    ":"",s=parseInt(this._get(e,"firstDay"),10),s=isNaN(s)?0:s,r=this._get(e,"showWeek"),n=this._get(e,"dayNames"),d=this._get(e,"dayNamesMin"),o=this._get(e,"monthNames"),c=this._get(e,"monthNamesShort"),l=this._get(e,"beforeShowDay"),h=this._get(e,"showOtherMonths"),u=this._get(e,"selectOtherMonths"),p=this._getDefaultDate(e),g="",f=0;f"+(/all|left/.test(b)&&0===f?K?i:a:"")+(/all|right/.test(b)&&0===f?K?a:i:"")+this._generateMonthYearHeader(e,z,B,H,U,0
    ").addClass("cw").text("#"));c.isBefore(f.clone().endOf("w"));)b.append(a("").addClass("dow").text(c.format("dd"))),c.add(1,"d");o.find(".datepicker-days thead").append(b)},N=function(a){return d.disabledDates[a.format("YYYY-MM-DD")]===!0},O=function(a){return d.enabledDates[a.format("YYYY-MM-DD")]===!0},P=function(a){return d.disabledHours[a.format("H")]===!0},Q=function(a){return d.enabledHours[a.format("H")]===!0},R=function(b,c){if(!b.isValid())return!1;if(d.disabledDates&&"d"===c&&N(b))return!1;if(d.enabledDates&&"d"===c&&!O(b))return!1;if(d.minDate&&b.isBefore(d.minDate,c))return!1;if(d.maxDate&&b.isAfter(d.maxDate,c))return!1;if(d.daysOfWeekDisabled&&"d"===c&&-1!==d.daysOfWeekDisabled.indexOf(b.day()))return!1;if(d.disabledHours&&("h"===c||"m"===c||"s"===c)&&P(b))return!1;if(d.enabledHours&&("h"===c||"m"===c||"s"===c)&&!Q(b))return!1;if(d.disabledTimeIntervals&&("h"===c||"m"===c||"s"===c)){var e=!1;if(a.each(d.disabledTimeIntervals,function(){return b.isBetween(this[0],this[1])?(e=!0,!1):void 0}),e)return!1}return!0},S=function(){for(var b=[],c=f.clone().startOf("y").startOf("d");c.isSame(f,"y");)b.push(a("").attr("data-action","selectMonth").addClass("month").text(c.format("MMM"))),c.add(1,"M");o.find(".datepicker-months td").empty().append(b)},T=function(){var b=o.find(".datepicker-months"),c=b.find("th"),g=b.find("tbody").find("span");c.eq(0).find("span").attr("title",d.tooltips.prevYear),c.eq(1).attr("title",d.tooltips.selectYear),c.eq(2).find("span").attr("title",d.tooltips.nextYear),b.find(".disabled").removeClass("disabled"),R(f.clone().subtract(1,"y"),"y")||c.eq(0).addClass("disabled"),c.eq(1).text(f.year()),R(f.clone().add(1,"y"),"y")||c.eq(2).addClass("disabled"),g.removeClass("active"),e.isSame(f,"y")&&!m&&g.eq(e.month()).addClass("active"),g.each(function(b){R(f.clone().month(b),"M")||a(this).addClass("disabled")})},U=function(){var a=o.find(".datepicker-years"),b=a.find("th"),c=f.clone().subtract(5,"y"),g=f.clone().add(6,"y"),h="";for(b.eq(0).find("span").attr("title",d.tooltips.prevDecade),b.eq(1).attr("title",d.tooltips.selectDecade),b.eq(2).find("span").attr("title",d.tooltips.nextDecade),a.find(".disabled").removeClass("disabled"),d.minDate&&d.minDate.isAfter(c,"y")&&b.eq(0).addClass("disabled"),b.eq(1).text(c.year()+"-"+g.year()),d.maxDate&&d.maxDate.isBefore(g,"y")&&b.eq(2).addClass("disabled");!c.isAfter(g,"y");)h+=''+c.year()+"",c.add(1,"y");a.find("td").html(h)},V=function(){var a,c=o.find(".datepicker-decades"),g=c.find("th"),h=b({y:f.year()-f.year()%100-1}),i=h.clone().add(100,"y"),j=h.clone(),k=!1,l=!1,m="";for(g.eq(0).find("span").attr("title",d.tooltips.prevCentury),g.eq(2).find("span").attr("title",d.tooltips.nextCentury),c.find(".disabled").removeClass("disabled"),(h.isSame(b({y:1900}))||d.minDate&&d.minDate.isAfter(h,"y"))&&g.eq(0).addClass("disabled"),g.eq(1).text(h.year()+"-"+i.year()),(h.isSame(b({y:2e3}))||d.maxDate&&d.maxDate.isBefore(i,"y"))&&g.eq(2).addClass("disabled");!h.isAfter(i,"y");)a=h.year()+12,k=d.minDate&&d.minDate.isAfter(h,"y")&&d.minDate.year()<=a,l=d.maxDate&&d.maxDate.isAfter(h,"y")&&d.maxDate.year()<=a,m+=''+(h.year()+1)+" - "+(h.year()+12)+"",h.add(12,"y");m+="",c.find("td").html(m),g.eq(1).text(j.year()+1+"-"+h.year())},W=function(){var b,c,g,h,i=o.find(".datepicker-days"),j=i.find("th"),k=[];if(B()){for(j.eq(0).find("span").attr("title",d.tooltips.prevMonth),j.eq(1).attr("title",d.tooltips.selectMonth),j.eq(2).find("span").attr("title",d.tooltips.nextMonth),i.find(".disabled").removeClass("disabled"),j.eq(1).text(f.format(d.dayViewHeaderFormat)),R(f.clone().subtract(1,"M"),"M")||j.eq(0).addClass("disabled"),R(f.clone().add(1,"M"),"M")||j.eq(2).addClass("disabled"),b=f.clone().startOf("M").startOf("w").startOf("d"),h=0;42>h;h++)//always display 42 days (should show 6 weeks) -0===b.weekday()&&(c=a("
    '+b.week()+"'+b.date()+"
    '+c.format(h?"HH":"hh")+"
    '+c.format("mm")+"
    '+c.format("ss")+"
    ",v=r?"":"",_=0;_<7;_++)v+="";for(y+=v+"",w=this._getDaysInMonth(B,z),B===e.selectedYear&&z===e.selectedMonth&&(e.selectedDay=Math.min(e.selectedDay,w)),b=(this._getFirstDayOfMonth(B,z)-s+7)%7,w=Math.ceil((b+w)/7),C=L&&this.maxRows>w?this.maxRows:w,this.maxRows=C,I=this._daylightSavingAdjust(new Date(B,z,1-b)),x=0;x",Y=r?"":"",_=0;_<7;_++)S=l?l.apply(e.input?e.input[0]:null,[I]):[!0,""],F=(N=I.getMonth()!==z)&&!u||!S[0]||H&&I"+(N&&!h?" ":F?""+I.getDate()+"":""+I.getDate()+"")+"",I.setDate(I.getDate()+1),I=this._daylightSavingAdjust(I);y+=Y+""}11<++z&&(z=0,B++),k+=y+="
    "+this._get(e,"weekHeader")+""+d[M]+"
    "+this._get(e,"calculateWeek")(I)+"
    "+(L?""+(0":""):"")}g+=k}return g+=E,e._keyEvent=!1,g},_generateMonthYearHeader:function(e,t,a,i,s,r,n,d){var o,c,l,h,u,p,g,_=this._get(e,"changeMonth"),f=this._get(e,"changeYear"),k=this._get(e,"showMonthAfterYear"),D="
    ",m="";if(r||!_)m+=""+n[t]+"";else{for(o=i&&i.getFullYear()===a,c=s&&s.getFullYear()===a,m+=""}if(k||(D+=m+(!r&&_&&f?"":" ")),!e.yearshtml)if(e.yearshtml="",r||!f)D+=""+a+"";else{for(h=this._get(e,"yearRange").split(":"),u=(new Date).getFullYear(),p=(n=function(e){e=e.match(/c[+\-].*/)?a+parseInt(e.substring(1),10):e.match(/[+\-].*/)?u+parseInt(e,10):parseInt(e,10);return isNaN(e)?u:e})(h[0]),g=Math.max(p,n(h[1]||"")),p=i?Math.max(p,i.getFullYear()):p,g=s?Math.min(g,s.getFullYear()):g,e.yearshtml+="",D+=e.yearshtml,e.yearshtml=null}return D+=this._get(e,"yearSuffix"),k&&(D+=(!r&&_&&f?"":" ")+m),D+="
    "},_adjustInstDate:function(e,t,a){var i=e.selectedYear+("Y"===a?t:0),s=e.selectedMonth+("M"===a?t:0),t=Math.min(e.selectedDay,this._getDaysInMonth(i,s))+("D"===a?t:0),t=this._restrictMinMax(e,this._daylightSavingAdjust(new Date(i,s,t)));e.selectedDay=t.getDate(),e.drawMonth=e.selectedMonth=t.getMonth(),e.drawYear=e.selectedYear=t.getFullYear(),"M"!==a&&"Y"!==a||this._notifyChange(e)},_restrictMinMax:function(e,t){var a=this._getMinMaxDate(e,"min"),e=this._getMinMaxDate(e,"max"),t=a&&t=a.getTime())&&(!i||t.getTime()<=i.getTime())&&(!s||t.getFullYear()>=s)&&(!r||t.getFullYear()<=r)},_getFormatConfig:function(e){var t=this._get(e,"shortYearCutoff");return{shortYearCutoff:t="string"!=typeof t?t:(new Date).getFullYear()%100+parseInt(t,10),dayNamesShort:this._get(e,"dayNamesShort"),dayNames:this._get(e,"dayNames"),monthNamesShort:this._get(e,"monthNamesShort"),monthNames:this._get(e,"monthNames")}},_formatDate:function(e,t,a,i){t||(e.currentDay=e.selectedDay,e.currentMonth=e.selectedMonth,e.currentYear=e.selectedYear);t=t?"object"==typeof t?t:this._daylightSavingAdjust(new Date(i,a,t)):this._daylightSavingAdjust(new Date(e.currentYear,e.currentMonth,e.currentDay));return this.formatDate(this._get(e,"dateFormat"),t,this._getFormatConfig(e))}}),M.fn.datepicker=function(e){if(!this.length)return this;M.datepicker.initialized||(M(document).on("mousedown",M.datepicker._checkExternalClick),M.datepicker.initialized=!0),0===M("#"+M.datepicker._mainDivId).length&&M("body").append(M.datepicker.dpDiv);var t=Array.prototype.slice.call(arguments,1);return"string"==typeof e&&("isDisabled"===e||"getDate"===e||"widget"===e)||"option"===e&&2===arguments.length&&"string"==typeof arguments[1]?M.datepicker["_"+e+"Datepicker"].apply(M.datepicker,[this[0]].concat(t)):this.each(function(){"string"==typeof e?M.datepicker["_"+e+"Datepicker"].apply(M.datepicker,[this].concat(t)):M.datepicker._attachDatepicker(this,e)})},M.datepicker=new e,M.datepicker.initialized=!1,M.datepicker.uuid=(new Date).getTime(),M.datepicker.version="1.12.1";M.datepicker}); \ No newline at end of file diff --git a/DnsServerCore/www/js/jquery.min.js b/DnsServerCore/www/js/jquery.min.js index c4c6022f..b5329e9a 100644 --- a/DnsServerCore/www/js/jquery.min.js +++ b/DnsServerCore/www/js/jquery.min.js @@ -1,2 +1,2 @@ -/*! jQuery v3.6.0 | (c) OpenJS Foundation and other contributors | jquery.org/license */ -!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.0",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
    "],col:[2,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var _t,zt=[],Ut=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=zt.pop()||S.expando+"_"+wt.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Ut.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ut.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Ut,"$1"+r):!1!==e.jsonp&&(e.url+=(Tt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,zt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((_t=E.implementation.createHTMLDocument("").body).innerHTML="
    ",2===_t.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=Fe(y.pixelPosition,function(e,t){if(t)return t=We(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,S)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&v(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!y||!y.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ve(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=E)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{if(d.cssSupportsSelector&&!CSS.supports("selector(:is("+c+"))"))throw new Error;return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===E&&e.removeAttribute("id")}}}return g(t.replace(B,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[E]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ye(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ve(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,S=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.cssSupportsSelector=ce(function(){return CSS.supports("selector(*)")&&C.querySelectorAll(":is(:jqfake)")&&!CSS.supports("selector(:is(*,:jqfake))")}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=E,!C.getElementsByName||!C.getElementsByName(E).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&S){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&S){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&S)return t.getElementsByClassName(e)},s=[],y=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&y.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||y.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+E+"-]").length||y.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||y.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||y.push(":checked"),e.querySelectorAll("a#"+E+"+*").length||y.push(".#.+[+~]"),e.querySelectorAll("\\\f"),y.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&y.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&y.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&y.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),y.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),d.cssSupportsSelector||y.push(":has"),y=y.length&&new RegExp(y.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),v=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType&&e.documentElement||e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&v(p,e)?-1:t==C||t.ownerDocument==p&&v(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&S&&!N[t+" "]&&(!s||!s.test(t))&&(!y||!y.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?E.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?E.grep(e,function(e){return e===n!==r}):"string"!=typeof n?E.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(E.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof E?t[0]:t,E.merge(this,E.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:S,!0)),N.test(r[1])&&E.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=S.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(E):E.makeArray(e,this)}).prototype=E.fn,D=E(S);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}E.fn.extend({has:function(e){var t=E(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=S.createDocumentFragment().appendChild(S.createElement("div")),(fe=S.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),v.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",v.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",v.option=!!ce.lastChild;var ge={thead:[1,"","
    "],col:[2,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],_default:[0,"",""]};function ye(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?E.merge([e],n):n}function ve(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&E(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),S.head.appendChild(r[0])},abort:function(){i&&i()}}});var Ut,Xt=[],Vt=/(=)\?(?=&|$)|\?\?/;E.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Xt.pop()||E.expando+"_"+Ct.guid++;return this[e]=!0,e}}),E.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Vt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Vt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Vt,"$1"+r):!1!==e.jsonp&&(e.url+=(St.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||E.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?E(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Xt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),v.createHTMLDocument=((Ut=S.implementation.createHTMLDocument("").body).innerHTML="
    ",2===Ut.childNodes.length),E.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(v.createHTMLDocument?((r=(t=S.implementation.createHTMLDocument("")).createElement("base")).href=S.location.href,t.head.appendChild(r)):t=S),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&E(o).remove(),E.merge([],i.childNodes)));var r,i,o},E.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(E.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},E.expr.pseudos.animated=function(t){return E.grep(E.timers,function(e){return t===e.elem}).length},E.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=E.css(e,"position"),c=E(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=E.css(e,"top"),u=E.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,E.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},E.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){E.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===E.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===E.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=E(e).offset()).top+=E.css(e,"borderTopWidth",!0),i.left+=E.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-E.css(r,"marginTop",!0),left:t.left-i.left-E.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===E.css(e,"position"))e=e.offsetParent;return e||re})}}),E.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;E.fn[t]=function(e){return B(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),E.each(["top","left"],function(e,n){E.cssHooks[n]=_e(v.pixelPosition,function(e,t){if(t)return t=Be(e,n),Pe.test(t)?E(e).position()[n]+"px":t})}),E.each({Height:"height",Width:"width"},function(a,s){E.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){E.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return B(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?E.css(e,t,i):E.style(e,t,n,i)},s,n?e:void 0,n)}})}),E.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){E.fn[t]=function(e){return this.on(t,e)}}),E.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),E.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){E.fn[n]=function(e,t){return 0. */ $(function () { - $('#dtpQueryLogStart').datetimepicker({ format: "YYYY-MM-DD HH:mm:ss" }); - $('#dtpQueryLogEnd').datetimepicker({ format: "YYYY-MM-DD HH:mm:ss" }); - $("#optQueryLogsAppName").change(function () { if (appsList == null) return; @@ -42,6 +39,14 @@ $(function () { $("#optQueryLogsClassPath").html(optClassPaths); $("#txtAddEditRecordDataData").val(""); }); + + $("#optQueryLogsEntriesPerPage").change(function () { + localStorage.setItem("optQueryLogsEntriesPerPage", $("#optQueryLogsEntriesPerPage").val()); + }); + + var optQueryLogsEntriesPerPage = localStorage.getItem("optQueryLogsEntriesPerPage"); + if (optQueryLogsEntriesPerPage != null) + $("#optQueryLogsEntriesPerPage").val(optQueryLogsEntriesPerPage); }); function refreshLogsTab() { @@ -287,7 +292,10 @@ function queryLogs(pageNumber) { if (pageNumber == null) pageNumber = $("#txtQueryLogPageNumber").val(); - var entriesPerPage = $("#optQueryLogsEntriesPerPage").val(); + var entriesPerPage = Number($("#optQueryLogsEntriesPerPage").val()); + if (entriesPerPage < 1) + entriesPerPage = 10; + var descendingOrder = $("#optQueryLogsDescendingOrder").val(); var start = $("#txtQueryLogStart").val(); @@ -328,7 +336,22 @@ function queryLogs(pageNumber) { htmlEncode(responseJSON.response.entries[i].qname == "" ? "." : responseJSON.response.entries[i].qname) + "
    " + (responseJSON.response.entries[i].qtype == null ? "" : responseJSON.response.entries[i].qtype) + "" + (responseJSON.response.entries[i].qclass == null ? "" : responseJSON.response.entries[i].qclass) + "" + - htmlEncode(responseJSON.response.entries[i].answer) + "
      "; + + switch (responseJSON.response.entries[i].responseType.toLowerCase()) { + case "blocked": + case "upstreamblocked": + case "cacheblocked": + tableHtml += "
    • Allow Domain
    • "; + break; + + default: + tableHtml += "
    • Block Domain
    • "; + break; + } + + tableHtml += "
    " + htmlEncode(name === "." ? "" : name) + "
    " + (firstRowNumber + i) + "" + htmlEncode(name === "." ? "" : name) + "" + type + "" + dnssecStatus + "" + status + "
    Total Zones: " + zones.length + "
    No Zones Found
    Total Zones: " + totalZones + "
    No Zones Found
    Total Records: " + recordCount + "
    No Records Found
    " + htmlEncode(name) + "
    " + (index + 1) + "" + htmlEncode(name) + "" + record.type + "" + record.ttl + ""; - tableHtmlRow += "
    "; - tableHtmlRow += ""; - tableHtmlRow += ""; - tableHtmlRow += ""; - tableHtmlRow += "
    Total Records: " + recordCount + "
    No Records Found
    Total Records: " + recordCount + "
    No Records Found
    " + responseJSON.response.dnssecPrivateKeys[i].keyType + "" + responseJSON.response.dnssecPrivateKeys[i].algorithm + "" + responseJSON.response.dnssecPrivateKeys[i].state + "" + moment(responseJSON.response.dnssecPrivateKeys[i].stateChangedOn).local().format("YYYY-MM-DD HH:mm") + ""; + + "" + moment(responseJSON.response.dnssecPrivateKeys[i].stateChangedOn).local().format("YYYY-MM-DD HH:mm"); + + if (responseJSON.response.dnssecPrivateKeys[i].stateReadyBy != null) + tableHtmlRows += "
    (ready by: " + moment(responseJSON.response.dnssecPrivateKeys[i].stateReadyBy).local().format("YYYY-MM-DD HH:mm") + ")"; + + tableHtmlRows += "
    "; if (responseJSON.response.dnssecPrivateKeys[i].keyType === "ZoneSigningKey") { switch (responseJSON.response.dnssecPrivateKeys[i].state) { @@ -3986,15 +4153,19 @@ function refreshDnssecProperties(divDnssecPropertiesLoader) { switch (responseJSON.response.dnssecPrivateKeys[i].state) { case "Generated": - tableHtmlRows += ""; + tableHtmlRows += "
      "; + tableHtmlRows += "
    • Delete
    • "; + tableHtmlRows += "
    "; foundGeneratedKey = true; break; case "Ready": case "Active": if (!responseJSON.response.dnssecPrivateKeys[i].isRetiring) { - tableHtmlRows += ""; - tableHtmlRows += ""; + tableHtmlRows += "
      "; + tableHtmlRows += "
    • Rollover
    • "; + tableHtmlRows += "
    • Retire
    • "; + tableHtmlRows += "
    "; } break; } @@ -4076,16 +4247,17 @@ function updateDnssecPrivateKey(keyTag, objBtn) { }); } -function deleteDnssecPrivateKey(keyTag, objBtn) { - if (!confirm("Are you sure to permanently delete the private key?")) +function deleteDnssecPrivateKey(keyTag, id) { + if (!confirm("Are you sure to permanently delete the private key (" + keyTag + ")?")) return; - var btn = $(objBtn); - var id = btn.attr("data-id"); var divDnssecPropertiesAlert = $("#divDnssecPropertiesAlert"); var zone = $("#lblDnssecPropertiesZoneName").attr("data-zone"); - btn.button('loading'); + var btn = $("#btnDnssecPropertiesDnsKeyRowOption" + id); + var originalBtnHtml = btn.html(); + btn.prop("disabled", true); + btn.html(""); HTTPRequest({ url: "/api/zones/dnssec/properties/deletePrivateKey?token=" + sessionData.token + "&zone=" + zone + "&keyTag=" + keyTag, @@ -4094,7 +4266,8 @@ function deleteDnssecPrivateKey(keyTag, objBtn) { showAlert("success", "Private Key Deleted!", "The DNSSEC private key was deleted successfully.", divDnssecPropertiesAlert); }, error: function () { - btn.button('reset'); + btn.prop("disabled", false); + btn.html(originalBtnHtml); }, invalidToken: function () { $("#modalDnssecProperties").modal("hide"); @@ -4104,15 +4277,17 @@ function deleteDnssecPrivateKey(keyTag, objBtn) { }); } -function rolloverDnssecDnsKey(keyTag, objBtn) { - if (!confirm("Are you sure you want to rollover the DNS Key?")) +function rolloverDnssecDnsKey(keyTag, id) { + if (!confirm("Are you sure you want to rollover the DNS Key (" + keyTag + ")?")) return; - var btn = $(objBtn); var divDnssecPropertiesAlert = $("#divDnssecPropertiesAlert"); var zone = $("#lblDnssecPropertiesZoneName").attr("data-zone"); - btn.button('loading'); + var btn = $("#btnDnssecPropertiesDnsKeyRowOption" + id); + var originalBtnHtml = btn.html(); + btn.prop("disabled", true); + btn.html(""); HTTPRequest({ url: "/api/zones/dnssec/properties/rolloverDnsKey?token=" + sessionData.token + "&zone=" + zone + "&keyTag=" + keyTag, @@ -4121,7 +4296,8 @@ function rolloverDnssecDnsKey(keyTag, objBtn) { showAlert("success", "Rollover Done!", "The DNS Key was rolled over successfully.", divDnssecPropertiesAlert); }, error: function () { - btn.button('reset'); + btn.prop("disabled", false); + btn.html(originalBtnHtml); }, invalidToken: function () { $("#modalDnssecProperties").modal("hide"); @@ -4131,15 +4307,17 @@ function rolloverDnssecDnsKey(keyTag, objBtn) { }); } -function retireDnssecDnsKey(keyTag, objBtn) { - if (!confirm("Are you sure you want to retire the DNS Key?")) +function retireDnssecDnsKey(keyTag, id) { + if (!confirm("Are you sure you want to retire the DNS Key (" + keyTag + ")?")) return; - var btn = $(objBtn); var divDnssecPropertiesAlert = $("#divDnssecPropertiesAlert"); var zone = $("#lblDnssecPropertiesZoneName").attr("data-zone"); - btn.button('loading'); + var btn = $("#btnDnssecPropertiesDnsKeyRowOption" + id); + var originalBtnHtml = btn.html(); + btn.prop("disabled", true); + btn.html(""); HTTPRequest({ url: "/api/zones/dnssec/properties/retireDnsKey?token=" + sessionData.token + "&zone=" + zone + "&keyTag=" + keyTag, @@ -4148,7 +4326,8 @@ function retireDnssecDnsKey(keyTag, objBtn) { showAlert("success", "DNS Key Retired!", "The DNS Key was retired successfully.", divDnssecPropertiesAlert); }, error: function () { - btn.button('reset'); + btn.prop("disabled", false); + btn.html(originalBtnHtml); }, invalidToken: function () { $("#modalDnssecProperties").modal("hide"); diff --git a/DnsServerSystemTrayApp/DnsProvider.cs b/DnsServerSystemTrayApp/DnsProvider.cs index 72fa6d6b..870108c4 100644 --- a/DnsServerSystemTrayApp/DnsProvider.cs +++ b/DnsServerSystemTrayApp/DnsProvider.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -52,7 +52,7 @@ namespace DnsServerSystemTrayApp int count = bR.ReadInt32(); for (int i = 0; i < count; i++) - this.Addresses.Add(IPAddressExtension.ReadFrom(bR)); + this.Addresses.Add(IPAddressExtensions.ReadFrom(bR)); } #endregion diff --git a/DnsServerSystemTrayApp/DnsServerSystemTrayApp.csproj b/DnsServerSystemTrayApp/DnsServerSystemTrayApp.csproj index 2a3df257..d5c37327 100644 --- a/DnsServerSystemTrayApp/DnsServerSystemTrayApp.csproj +++ b/DnsServerSystemTrayApp/DnsServerSystemTrayApp.csproj @@ -11,7 +11,7 @@ DnsServerSystemTrayApp Shreyas Zare logo2.ico - 4.0 + 4.0.1 Technitium Technitium DNS Server diff --git a/DnsServerSystemTrayApp/frmAbout.resx b/DnsServerSystemTrayApp/frmAbout.resx index c1e97335..b509627c 100644 --- a/DnsServerSystemTrayApp/frmAbout.resx +++ b/DnsServerSystemTrayApp/frmAbout.resx @@ -57,10 +57,49 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + True + + + True + + + True + + + True + - Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) + Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program comes with ABSOLUTELY NO WARRANTY. This is free software, and you are welcome to redistribute it under certain conditions. Click link below for details: + + True + + + True + + + True + + + True + + + True + + + True + + + True + + + True + + + True + diff --git a/DnsServerWindowsService/DnsServerWindowsService.csproj b/DnsServerWindowsService/DnsServerWindowsService.csproj index f2b42956..c62ea5c5 100644 --- a/DnsServerWindowsService/DnsServerWindowsService.csproj +++ b/DnsServerWindowsService/DnsServerWindowsService.csproj @@ -8,7 +8,7 @@ DnsServerWindowsService DnsService logo2.ico - 10.0.1 + 11.0 Shreyas Zare Technitium Technitium DNS Server @@ -29,7 +29,7 @@ - + diff --git a/DnsServerWindowsService/DnsServiceWorker.cs b/DnsServerWindowsService/DnsServiceWorker.cs index 2f6d367d..ec8ce504 100644 --- a/DnsServerWindowsService/DnsServiceWorker.cs +++ b/DnsServerWindowsService/DnsServiceWorker.cs @@ -1,6 +1,6 @@ /* Technitium DNS Server -Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -42,19 +42,16 @@ namespace DnsServerWindowsService _service = new DnsWebService(configFolder, new Uri("https://go.technitium.com/?id=43"), new Uri("https://go.technitium.com/?id=44")); } - public override Task StartAsync(CancellationToken cancellationToken) + public override async Task StartAsync(CancellationToken cancellationToken) { CheckFirewallEntries(); - _service.Start(); - - return Task.CompletedTask; + await _service.StartAsync(); } - public override Task StopAsync(CancellationToken cancellationToken) + public override async Task StopAsync(CancellationToken cancellationToken) { - _service.Stop(); - return Task.CompletedTask; + await _service.StopAsync(); } public override void Dispose() diff --git a/DnsServerWindowsSetup/DnsServerSetup.iss b/DnsServerWindowsSetup/DnsServerSetup.iss index acdac1f5..b4115872 100644 --- a/DnsServerWindowsSetup/DnsServerSetup.iss +++ b/DnsServerWindowsSetup/DnsServerSetup.iss @@ -2,7 +2,7 @@ ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! #define MyAppName "Technitium DNS Server" -#define MyAppVersion "10.0.1" +#define MyAppVersion "11.0" #define MyAppPublisher "Technitium" #define MyAppURL "https://technitium.com/dns/" #define MyAppExeName "DnsServerSystemTrayApp.exe" @@ -18,8 +18,8 @@ AppPublisher={#MyAppPublisher} AppPublisherURL={#MyAppURL} AppSupportURL={#MyAppURL} AppUpdatesURL={#MyAppURL} -VersionInfoVersion=2.1.0.0 -VersionInfoCopyright="Copyright (C) 2022 Technitium" +VersionInfoVersion=2.2.0.0 +VersionInfoCopyright="Copyright (C) 2023 Technitium" DefaultDirName={commonpf32}\Technitium\DNS Server DefaultGroupName={#MyAppName} DisableProgramGroupPage=yes diff --git a/DnsServerWindowsSetup/dotnet.iss b/DnsServerWindowsSetup/dotnet.iss index 92368de3..d88b8bb0 100644 --- a/DnsServerWindowsSetup/dotnet.iss +++ b/DnsServerWindowsSetup/dotnet.iss @@ -305,12 +305,24 @@ end; { Check if dotnet is installed } +function IsAspDotNetInstalled: Boolean; +var + ResultCode: Integer; +begin + Result := false; + Exec('cmd.exe', '/c dotnet --list-runtimes | find /n "Microsoft.AspNetCore.App 7.0.3"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); + if ResultCode = 0 then + begin + Result := true; + end; +end; + function IsDotNetDesktopInstalled: Boolean; var ResultCode: Integer; begin Result := false; - Exec('cmd.exe', '/c dotnet --list-runtimes | find /n "Microsoft.WindowsDesktop.App 7.0.0"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); + Exec('cmd.exe', '/c dotnet --list-runtimes | find /n "Microsoft.WindowsDesktop.App 7.0.3"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); if ResultCode = 0 then begin Result := true; @@ -320,12 +332,21 @@ end; { if dotnet is not installed then add it for download } procedure CheckDotnetDependency; begin + if not IsAspDotNetInstalled then + begin + AddDependency('aspdotnet70' + GetArchitectureSuffix + '.exe', + '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart', + 'ASP.NET Core Runtime 7.0.3' + GetArchitectureTitle, + GetString('https://download.visualstudio.microsoft.com/download/pr/4bf0f350-f947-408b-9ee4-539313b85634/b17087052d6192b5d59735ae6f208c19/aspnetcore-runtime-7.0.3-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/d37efccc-2ba1-4fc9-a1ef-a8e1e77fb681/b9a20fc29ff05f18d81620ec88ade699/aspnetcore-runtime-7.0.3-win-x64.exe'), + '', False, False, False); + end; + if not IsDotNetDesktopInstalled then begin AddDependency('dotnet70desktop' + GetArchitectureSuffix + '.exe', '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart', - '.NET Desktop Runtime 7.0.0' + GetArchitectureTitle, - GetString('https://download.visualstudio.microsoft.com/download/pr/d05a833c-2cf9-4d06-89ae-a0f3e10c5c91/c668ff42e23c2f67aa3d80227860585f/windowsdesktop-runtime-7.0.0-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/5b2fbe00-507e-450e-8b52-43ab052aadf2/79d54c3a19ce3fce314f2367cf4e3b21/windowsdesktop-runtime-7.0.0-win-x64.exe'), + '.NET Desktop Runtime 7.0.3' + GetArchitectureTitle, + GetString('https://download.visualstudio.microsoft.com/download/pr/fb8bf100-9e1c-472c-8bc8-aa16fff44f53/8d36f5a56edff8620f9c63c1e73ba88c/windowsdesktop-runtime-7.0.3-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/3ebf014d-fcb9-4200-b3fe-76ba2000b027/840f2f95833ce400a9949e35f1581d28/windowsdesktop-runtime-7.0.3-win-x64.exe'), '', False, False, False); end; end; diff --git a/Dockerfile b/Dockerfile index 41da4498..59d7695c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/runtime:7.0 +FROM mcr.microsoft.com/dotnet/aspnet:7.0 LABEL product="Technitium DNS Server" LABEL vendor="Technitium" LABEL email="support@technitium.com" @@ -10,13 +10,16 @@ WORKDIR /etc/dns/ COPY ./DnsServerApp/bin/Release/publish/ . EXPOSE 5380/tcp +EXPOSE 53443/tcp EXPOSE 53/udp EXPOSE 53/tcp -EXPOSE 67/udp +EXPOSE 853/udp EXPOSE 853/tcp +EXPOSE 443/udp EXPOSE 443/tcp EXPOSE 80/tcp EXPOSE 8053/tcp +EXPOSE 67/udp VOLUME ["/etc/dns/config"] diff --git a/README.md b/README.md index 6dad9101..9474c1d2 100644 --- a/README.md +++ b/README.md @@ -29,17 +29,18 @@ Be it a home network or an organization's network, having a locally running DNS - Installs in just a minute and works out-of-the-box with zero configuration. - Block ads & malware using one or more block list URLs. - High performance DNS server based on async IO that can serve millions of requests per minute even on a commodity desktop PC hardware (load tested on Intel i7-8700 CPU with more than 100,000 request/second over Gigabit Ethernet). -- Self host [DNS-over-TLS](https://en.wikipedia.org/wiki/DNS_over_TLS) and [DNS-over-HTTPS](https://en.wikipedia.org/wiki/DNS_over_HTTPS) DNS service on your network. -- Use public DNS resolvers like Cloudflare, Google & Quad9 with [DNS-over-TLS](https://en.wikipedia.org/wiki/DNS_over_TLS) and [DNS-over-HTTPS](https://en.wikipedia.org/wiki/DNS_over_HTTPS) protocols as forwarders. +- Self host [DNS-over-TLS](https://www.rfc-editor.org/rfc/rfc7858.html), [DNS-over-HTTPS](https://www.rfc-editor.org/rfc/rfc8484.html), and [DNS-over-QUIC](https://www.ietf.org/rfc/rfc9250.html) DNS services on your network. +- DNS-over-HTTPS implementation supports HTTP/1.1, HTTP/2, and HTTP/3 transport protocols. +- Use public DNS resolvers like Cloudflare, Google, Quad9, and AdGuard with [DNS-over-TLS](https://www.rfc-editor.org/rfc/rfc7858.html), [DNS-over-HTTPS](https://www.rfc-editor.org/rfc/rfc8484.html), or [DNS-over-QUIC](https://www.ietf.org/rfc/rfc9250.html) protocols as forwarders. - Advanced caching with features like serve stale, prefetching and auto prefetching. - Supports working as an authoritative as well as a recursive DNS server. -- DNSSEC validation support with RSA & ECDSA algorithms for recursive resolver, forwarders, and conditional forwarders. -- DNSSEC support for all supported DNS transport protocols including encrypted DNS protocols (DoT, DoH, & DoH JSON). +- DNSSEC validation support with RSA & ECDSA algorithms for recursive resolver, forwarders, and conditional forwarders with NSEC and NSEC3 support. +- DNSSEC support for all supported DNS transport protocols including encrypted DNS protocols. - DANE TLSA [RFC 6698](https://datatracker.ietf.org/doc/html/rfc6698) record type support. This includes support for automatically generating the hash values using certificates in PEM format. - SSHFP [RFC 4255](https://www.rfc-editor.org/rfc/rfc4255.html) record type support. - CNAME cloaking feature to block domain names that resolve to CNAME which are blocked. - QNAME minimization support in recursive resolver [RFC 9156](https://www.rfc-editor.org/rfc/rfc9156.html). -- QNAME randomization support for UDP transport protocol [draft-vixie-dnsext-dns0x20-00](https://datatracker.ietf.org/doc/html/draft-vixie-dnsext-dns0x20-00). +- QNAME case randomization support for UDP transport protocol [draft-vixie-dnsext-dns0x20-00](https://datatracker.ietf.org/doc/html/draft-vixie-dnsext-dns0x20-00). - DNAME record [RFC 6672](https://datatracker.ietf.org/doc/html/rfc6672) support. - ANAME propriety record support to allow using CNAME like feature at zone apex (CNAME flattening). Supports multiple ANAME records at both zone apex and sub domains. - APP propriety record support that allows custom DNS Apps to directly handle DNS requests and return a custom DNS response based on any business logic. @@ -47,14 +48,19 @@ Be it a home network or an organization's network, having a locally running DNS - Support for REGEX based block lists with different block lists for different client IP addresses or subnet using Advanced Blocking DNS App. - Primary, Secondary, Stub, and Conditional Forwarder zone support. - Static stub zone support implemented in Conditional Forwarder zone to force a domain name to resolve via given name servers using NS records. +- Bulk conditional forwarding support using Advanced Forwarding DNS App. - DNSSEC signed zones support with RSA & ECDSA algorithms. +- DNSSEC support for both NSEC and NSEC3. +- Zone transfer with AXFR and IXFR [RFC 1995](https://www.rfc-editor.org/rfc/rfc1995.html) support. - Zone transfer over TLS (XFR-over-TLS) [RFC 9103](https://www.rfc-editor.org/rfc/rfc9103.html) support. +- Zone transfer over QUIC (XFR-over-QUIC) [RFC 9250](https://www.ietf.org/rfc/rfc9250.html) support. - Dynamic DNS Updates [RFC 2136](https://www.rfc-editor.org/rfc/rfc2136) support with security policy. - Secret key transaction authentication (TSIG) [RFC 8945](https://datatracker.ietf.org/doc/html/rfc8945) support for zone transfers. - EDNS(0) [RFC6891](https://datatracker.ietf.org/doc/html/rfc6891) support. - EDNS Client Subnet (ECS) [RFC 7871](https://datatracker.ietf.org/doc/html/rfc7871) support for recursive resolution and forwarding. - Extended DNS Errors [RFC 8914](https://datatracker.ietf.org/doc/html/rfc8914) support. - DNS64 function [RFC 6147](https://www.rfc-editor.org/rfc/rfc6147) support for use by IPv6 only clients using the DNS64 App. +- Support to host DNSBL / RBL block lists [RFC 5782](https://www.rfc-editor.org/rfc/rfc5782). - Multi-user role based access with non-expiring API token support. - Self host your domain names on your own DNS server. - Wildcard sub domain support. @@ -93,9 +99,9 @@ For support, send an email to support@technitium.com. For any issues, feedback, Join [/r/technitium](https://www.reddit.com/r/technitium/) on Reddit. # Donate -Make contribution to Technitium by becoming a Patron and help making new software, updates, and features possible. +Make contribution to Technitium and help making new software, updates, and features possible. -[Become a Patron now!](https://www.patreon.com/technitium) +[Donate Now!](https://www.patreon.com/technitium) # Blog Posts - [Technitium Blog: Technitium DNS Server v10 Released!](https://blog.technitium.com/2022/11/technitium-dns-server-v10-released.html) (Nov 2022) diff --git a/docker-compose.yml b/docker-compose.yml index 7cc8cb55..036cf532 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,14 +7,17 @@ services: # For DHCP deployments, use "host" network mode and remove all the port mappings, including the ports array by commenting them # network_mode: "host" ports: - - "5380:5380/tcp" #DNS web console + - "5380:5380/tcp" #DNS web console (HTTP) + # - "53443:53443/tcp" #DNS web console (HTTPS) - "53:53/udp" #DNS service - "53:53/tcp" #DNS service - # - "67:67/udp" #DHCP service + # - "853:853/udp" #DNS-over-QUIC service # - "853:853/tcp" #DNS-over-TLS service - # - "443:443/tcp" #DNS-over-HTTPS service - # - "80:80/tcp" #DNS-over-HTTPS service certbot certificate renewal - # - "8053:8053/tcp" #DNS-over-HTTPS using reverse proxy + # - "443:443/udp" #DNS-over-HTTPS service (HTTP/3) + # - "443:443/tcp" #DNS-over-HTTPS service (HTTP/1.1, HTTP/2) + # - "80:80/tcp" #DNS-over-HTTP service (use with reverse proxy or certbot certificate renewal) + # - "8053:8053/tcp" #DNS-over-HTTP service (use with reverse proxy) + # - "67:67/udp" #DHCP service environment: - DNS_SERVER_DOMAIN=dns-server #The primary domain name used by this DNS Server to identify itself. # - DNS_SERVER_ADMIN_PASSWORD=password #DNS web console admin user password.