From 94a0cfcba3350403d94af0e3ee3748ee0ec8ace1 Mon Sep 17 00:00:00 2001 From: fuero <108815+fuero@users.noreply.github.com> Date: Wed, 21 Aug 2024 15:21:39 +0200 Subject: [PATCH] New Feature: HASH() adds hashing functions to dnsconfig.js language (#3085) Co-authored-by: Tom Limoncelli --- commands/types/dnscontrol.d.ts | 29 +++++++++++++ documentation/SUMMARY.md | 1 + .../top-level-functions/HASH.md | 36 ++++++++++++++++ pkg/js/hash.go | 41 +++++++++++++++++++ pkg/js/js.go | 1 + pkg/js/js_test.go | 1 + pkg/js/parse_tests/051-HASH.js | 1 + pkg/js/parse_tests/051-HASH.json | 12 ++++++ 8 files changed, 122 insertions(+) create mode 100644 documentation/language-reference/top-level-functions/HASH.md create mode 100644 pkg/js/hash.go create mode 100644 pkg/js/parse_tests/051-HASH.js create mode 100644 pkg/js/parse_tests/051-HASH.json diff --git a/commands/types/dnscontrol.d.ts b/commands/types/dnscontrol.d.ts index c38198d22..ade2786c3 100644 --- a/commands/types/dnscontrol.d.ts +++ b/commands/types/dnscontrol.d.ts @@ -1136,6 +1136,35 @@ declare function DnsProvider(name: string, nsCount?: number): DomainModifier; */ declare function FRAME(name: string, target: string, ...modifiers: RecordModifier[]): DomainModifier; +/** + * `HASH` hashes `value` using the hashing algorithm given in `algorithm` + * (accepted values `SHA1`, `SHA256`, and `SHA512`) and returns the hex encoded + * hash value. + * + * example `HASH("SHA1", "abc")` returns `a9993e364706816aba3e25717850c26c9cd0d89d`. + * + * `HASH()`'s primary use case is for managing [catalog zones](https://datatracker.ietf.org/doc/html/rfc9432): + * + * > a method for automatic DNS zone provisioning among DNS primary and secondary name + * > servers by storing and transferring the catalog of zones to be provisioned as one + * > or more regular DNS zones. + * + * Here's an example of a catalog zone: + * + * ```javascript + * foo_name_suffix = HASH("SHA1", "foo.name") + ".zones" + * D("catalog.example" + * [...] + * , TXT("version", "2") + * , PTR(foo_name_suffix, "foo.name.") + * , A("primaries.ext." + foo_name_suffix, "192.168.1.1") + * ) + * ``` + * + * @see https://docs.dnscontrol.org/language-reference/top-level-functions/hash + */ +declare function HASH(algorithm: "SHA1" | "SHA256" | "SHA512", value: string): string; + /** * HTTPS adds an HTTPS record to a domain. The name should be the relative label for the record. Use `@` for the domain apex. The HTTPS record is a special form of the SVCB resource record. * diff --git a/documentation/SUMMARY.md b/documentation/SUMMARY.md index efa9cdd8b..6e5e0cbcd 100644 --- a/documentation/SUMMARY.md +++ b/documentation/SUMMARY.md @@ -20,6 +20,7 @@ * [DOMAIN_ELSEWHERE_AUTO](language-reference/top-level-functions/DOMAIN_ELSEWHERE_AUTO.md) * [D_EXTEND](language-reference/top-level-functions/D_EXTEND.md) * [FETCH](language-reference/top-level-functions/FETCH.md) + * [HASH](language-reference/top-level-functions/HASH.md) * [IP](language-reference/top-level-functions/IP.md) * [NewDnsProvider](language-reference/top-level-functions/NewDnsProvider.md) * [NewRegistrar](language-reference/top-level-functions/NewRegistrar.md) diff --git a/documentation/language-reference/top-level-functions/HASH.md b/documentation/language-reference/top-level-functions/HASH.md new file mode 100644 index 000000000..61d1fd91a --- /dev/null +++ b/documentation/language-reference/top-level-functions/HASH.md @@ -0,0 +1,36 @@ +--- +name: HASH +parameters: + - algorithm + - value +parameter_types: + algorithm: '"SHA1" | "SHA256" | "SHA512"' + value: string +ts_return: string +--- + +`HASH` hashes `value` using the hashing algorithm given in `algorithm` +(accepted values `SHA1`, `SHA256`, and `SHA512`) and returns the hex encoded +hash value. + +example `HASH("SHA1", "abc")` returns `a9993e364706816aba3e25717850c26c9cd0d89d`. + +`HASH()`'s primary use case is for managing [catalog zones](https://datatracker.ietf.org/doc/html/rfc9432): + +> a method for automatic DNS zone provisioning among DNS primary and secondary name +> servers by storing and transferring the catalog of zones to be provisioned as one +> or more regular DNS zones. + +Here's an example of a catalog zone: + +{% code title="dnsconfig.js" %} +```javascript +foo_name_suffix = HASH("SHA1", "foo.name") + ".zones" +D("catalog.example" + [...] + , TXT("version", "2") + , PTR(foo_name_suffix, "foo.name.") + , A("primaries.ext." + foo_name_suffix, "192.168.1.1") +) +``` +{% endcode %} diff --git a/pkg/js/hash.go b/pkg/js/hash.go new file mode 100644 index 000000000..620cc424c --- /dev/null +++ b/pkg/js/hash.go @@ -0,0 +1,41 @@ +package js + +import ( + "crypto/sha1" + "crypto/sha256" + "crypto/sha512" + "encoding/hex" + "fmt" + + "github.com/robertkrimen/otto" +) + +// Exposes sha1, sha256, and sha512 hashing functions to Javascript +func hashFunc(call otto.FunctionCall) otto.Value { + if len(call.ArgumentList) != 2 { + throw(call.Otto, "require takes exactly two arguments") + } + algorithm := call.Argument(0).String() // The algorithm to use for hashing + value := call.Argument(1).String() // The value to hash + result := otto.Value{} + fmt.Printf("%s\n", value) + + switch algorithm { + case "SHA1", "sha1": + tmp := sha1.New() + tmp.Write([]byte(value)) + fmt.Printf("%s\n", hex.EncodeToString(tmp.Sum(nil))) + result, _ = otto.ToValue(hex.EncodeToString(tmp.Sum(nil))) + case "SHA256", "sha256": + tmp := sha256.New() + tmp.Write([]byte(value)) + result, _ = otto.ToValue(hex.EncodeToString(tmp.Sum(nil))) + case "SHA512", "sha512": + tmp := sha512.New() + tmp.Write([]byte(value)) + result, _ = otto.ToValue(hex.EncodeToString(tmp.Sum(nil))) + default: + throw(call.Otto, fmt.Sprintf("invalid algorithm %s given", algorithm)) + } + return result +} diff --git a/pkg/js/js.go b/pkg/js/js.go index 7112e48f2..06d9a7d30 100644 --- a/pkg/js/js.go +++ b/pkg/js/js.go @@ -74,6 +74,7 @@ func ExecuteJavascriptString(script []byte, devMode bool, variables map[string]s vm.Set("REVCOMPAT", reverseCompat) vm.Set("glob", listFiles) // used for require_glob() vm.Set("PANIC", jsPanic) + vm.Set("HASH", hashFunc) // add cli variables to otto for key, value := range variables { diff --git a/pkg/js/js_test.go b/pkg/js/js_test.go index ae09ba00b..0bd35a11b 100644 --- a/pkg/js/js_test.go +++ b/pkg/js/js_test.go @@ -141,6 +141,7 @@ func TestErrors(t *testing.T) { {"Bad cidr", `D(reverse("foo.com"), "reg")`}, {"Dup domains", `D("example.org", "reg"); D("example.org", "reg")`}, {"Bad NAMESERVER", `D("example.com","reg", NAMESERVER("@","ns1.foo.com."))`}, + {"Bad Hash function", `D(HASH("123", "abc"),"reg")`}, } for _, tst := range tests { t.Run(tst.desc, func(t *testing.T) { diff --git a/pkg/js/parse_tests/051-HASH.js b/pkg/js/parse_tests/051-HASH.js new file mode 100644 index 000000000..cb75995a5 --- /dev/null +++ b/pkg/js/parse_tests/051-HASH.js @@ -0,0 +1 @@ +D(HASH("SHA1", "abc"), "reg") diff --git a/pkg/js/parse_tests/051-HASH.json b/pkg/js/parse_tests/051-HASH.json new file mode 100644 index 000000000..5d11fa116 --- /dev/null +++ b/pkg/js/parse_tests/051-HASH.json @@ -0,0 +1,12 @@ +{ + "registrars": [], + "dns_providers": [], + "domains": [ + { + "name": "a9993e364706816aba3e25717850c26c9cd0d89d", + "registrar": "reg", + "dnsProviders": {}, + "records": [] + } + ] +}