Compare commits

...

11 Commits

Author SHA1 Message Date
d942833b06 Update with tech changes. 2023-05-12 15:55:55 +00:00
13d8b7f87e Update zone file generation
Signed-off-by: Jacob Kiers <jacob@jacobkiers.net>
2021-08-21 13:31:31 +02:00
5dd7a230fb Add more robust styling
Signed-off-by: Jacob Kiers <jacob@jacobkiers.net>
2021-08-21 12:11:24 +02:00
2d477bb77e Add content verification
Signed-off-by: Jacob Kiers <jacob@jacobkiers.net>
2021-08-21 11:22:40 +02:00
4b117a4743 Add support for longer records
This makes it possible to handle more content in a single round.

Signed-off-by: Jacob Kiers <jacob@jacobkiers.net>
2021-08-21 11:17:50 +02:00
74619915ca Add default title to HTML
Signed-off-by: Jacob Kiers <jacob@jacobkiers.net>
2021-08-21 11:15:29 +02:00
727f2c671e Move zone to different NS
Signed-off-by: Jacob Kiers <jacob@jacobkiers.net>
2021-08-21 11:14:41 +02:00
1dcd49fd0d Add information for visitors without Javascript
Signed-off-by: Jacob Kiers <jacob@jacobkiers.net>
2021-08-19 20:34:45 +02:00
b7792607ff Update the blog post
Signed-off-by: Jacob Kiers <jacob@jacobkiers.net>
2021-08-19 09:03:24 +02:00
cf84cc76d9 Fix a typo
Signed-off-by: Jacob Kiers <jacob@yuki.nl>
2021-08-17 09:49:53 +02:00
c094816d76 Some more documentation in the blog post
Signed-off-by: Jacob Kiers <jacob@jacobkiers.net>
2021-08-17 09:37:46 +02:00
9 changed files with 1729 additions and 226 deletions

7
Corefile Normal file
View File

@@ -0,0 +1,7 @@
hod.experiments.jacobkiers.net.:53 {
log
auto hod.experiments.jacobkiers.net. {
directory /etc/coredns/zones/
reload 10s
}
}

View File

@@ -1,12 +1,18 @@
+++
title = Serving blog content over DNS
title = HTML over DNS: Serving Blog Content Over DNS
date = 2021-08-15
author = Jacob Kiers
+++
You might not be able to see it immediately, but the content of this page is verved over DNS.
## What's up?
This works because of the new DNS-over-HTTP support, which, at least at Cloudflare, also has an API.
You might not be able to see it immediately, but the content of this page is served over DNS.
This works because of [DNS over HTTPS] for which there is an [API from Cloudflare].
Comments at [Hacker News, apparently][HN].
## How it works
That API is used to load the contents of this page, essentially like this:
@@ -14,6 +20,64 @@ That API is used to load the contents of this page, essentially like this:
fetch("https://cloudflare-dns.com/dns-query?ct=application/dns-json&type=TXT&name=post.hod.experiments.jacobkiers.net");
```
Please see the [source code] for the details of how it works.
The content itself is served over DNS, using CoreDNS, with these contents:
[source code]: https://github.com/jacobkiers/html-over-dns
```hcl
hod.experiments.jacobkiers.net.:53 {
log
auto hod.experiments.jacobkiers.net. {
directory /etc/coredns/zones/
reload 10s
}
}
```
This feeds into a zone file, which looks like this:
```dns
$TTL 5m ; Default TTL
@ IN SOA experiments.jacobkiers.net. postmaster.jacobkiers.net. (
2021081612 ; serial
1h ; slave refresh interval
15m ; slave retry interval
1w ; slave copy expire time
1h ; NXDOMAIN cache time
)
$ORIGIN hod.experiments.jacobkiers.net.
;
; domain name servers
;
@ IN NS experiments.jacobkiers.net.
;; START BLOG RECORDS
; posts-2021-08-17-serving-blog-content-over-dns-md
posts-2021-08-17-serving-blog-content-over-dns-md 60 IN TXT "t=text/markdown;c=3;h=2fd63f0f408ad1336283999d0487ced9;m=eyJ0aXRsZSI6IlNlcnZpbmcgYmxvZyBjb250ZW50IG92ZXIgRE5TIiwiZGF0ZSI6IjIwMjEtMDgtMTUiLCJhdXRob3IiOiJKYWNvYiBLaWVycyJ9"
0.2fd63f0f408ad1336283999d0487ced9 60 IN TXT "WW91IG1pZ2h0IG5vdCBiZSBhYmxlIHRvIHNlZSBpdCBpbW1lZGlhdGVseSwgYnV0IHRoZSBjb250ZW50IG9mIHRoaXMgcGFnZSBpcyB2ZXJ2ZWQgb3ZlciBETlMuCgpUaGlzIHdvcmtzIGJlY2F1c2Ugb2YgdGhlIG5ldyBETlMtb3Zlci1IVFRQIHN1cHBvcnQsIHdoaWNoLCBhdCBsZWFzdCBhdCBDbG91ZGZsYXJlLCBhbHNvIGhhcy"
1.2fd63f0f408ad1336283999d0487ced9 60 IN TXT "BhbiBBUEkuCgpUaGF0IEFQSSBpcyB1c2VkIHRvIGxvYWQgdGhlIGNvbnRlbnRzIG9mIHRoaXMgcGFnZSwgZXNzZW50aWFsbHkgbGlrZSB0aGlzOgoKYGBganMKZmV0Y2goImh0dHBzOi8vY2xvdWRmbGFyZS1kbnMuY29tL2Rucy1xdWVyeT9jdD1hcHBsaWNhdGlvbi9kbnMtanNvbiZ0eXBlPVRYVCZuYW1lPXBvc3QuaG9kLmV4cGVy"
2.2fd63f0f408ad1336283999d0487ced9 60 IN TXT "aW1lbnRzLmphY29ia2llcnMubmV0Iik7CmBgYAoKUGxlYXNlIHNlZSB0aGUgW3NvdXJjZSBjb2RlXSBmb3IgdGhlIGRldGFpbHMgb2YgaG93IGl0IHdvcmtzLgoKW3NvdXJjZSBjb2RlXTogaHR0cHM6Ly9naXRodWIuY29tL2phY29ia2llcnMvaHRtbC1vdmVyLWRucwo="
```
These records are base64 encoded content, so when concatenated and decoded, they give the content of the posts.
Please see the [source code] for the details.
## FAQ
### Why, though?
In short: just because I could. It was one of those ideas I was wondering idly about, and I decided to just try it.
### Has it any practical use?
It is not intended to have any. Since DNS records are fairly small, serving images or something would quickly start
consuming 100s of requests per second. I wouldn't want to do that to Cloudflare.
It would be an interesting experiment to see how feasible that is.
[source code]: https://github.com/jacobkiers/html-over-dns "Yes, the title is a pun..."
[DNS over HTTPS]: https://en.wikipedia.org/wiki/DNS_over_HTTPS
[API from Cloudflare]: https://developers.cloudflare.com/1.1.1.1/dns-over-https/json-format
[HN]: https://news.ycombinator.com/item?id=28218406

View File

@@ -0,0 +1,21 @@
class Verifier {
/***
* @param content Content
*/
async verify(content) {
if (!window.isSecureContext) return;
if (typeof TextEncoder === "undefined") return;
if (typeof crypto === "undefined") return;
const encoder = new TextEncoder();
const digestBuffer = await crypto.subtle.digest(content.index.hashAlgorithm, encoder.encode(content.content));
const hashArray = Array.from(new Uint8Array(digestBuffer)); // convert buffer to byte array
const digest = hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); // convert bytes to hex string
if (content.index.hash === digest) {
document.getElementById("verification").innerHTML =
"The hash of the content is verified to correspond with the hash" +
" in the metadata. You are now reading exactly what was intended.";
}
}
}

View File

@@ -1,140 +0,0 @@
<!DOCTYPE html charset="UTF-8">
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.2.0/build/styles/default.min.css">
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.2.0/build/highlight.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script>
class Content {
content = "";
type = "";
metaData = {}
constructor(content, type, metaData = {})
{
this.content = content;
this.type = type;
this.metaData = metaData;
}
}
class Index
{
mimeType = "";
chunks = 0;
hash = "";
metaData = {};
constructor(mimeType, chunks, hash, metaData = {})
{
this.mimeType = mimeType;
this.chunks = chunks;
this.hash = hash;
this.metaData = metaData
}
}
const dohServer = "https://cloudflare-dns.com/dns-query?ct=application/dns-json&type=TXT&name=";
const baseDomain = "hod.experiments.jacobkiers.net";
async function readUrl(domain) {
var index = await fetchIndex(`${dohServer}${domain}.${baseDomain}`);
var chunk_promises = [];
for(i = 0; i < index.chunks; i++)
{
chunk_promises[i] = fetchChunk(i, index.hash);
}
var chunks = await Promise.all(chunk_promises);
var content = chunks.reduce((built, current) => built += current);
return handleContent(new Content(atob(content), index.mimeType, index.metaData));
}
async function fetchChunk(id, hash)
{
var domain = `${id}.${hash}.${baseDomain}`;
const json = await fetch(`${dohServer}${domain}`)
.then(response => response.json());
const data = json.Answer[0].data.slice(1, -1);
return data;
}
async function fetchIndex(domain)
{
const response = await fetch(`${dohServer}.${domain}`);
const json = await response.json();
const index = json.Answer[0].data.slice(1, -1);
let ret = {};
let items = index.split(';');
items.forEach(item => {
let md = item.split('=');
let key = md[0];
let value = md[1];
ret[key] = value;
});
const metadata = JSON.parse(atob(ret["m"]));
return new Index(ret["t"], ret["c"], ret["h"], metadata);
}
function handleContent(content)
{
if (!content instanceof Content) {
console.log("Not valid content in handleContent.")
return;
}
switch(content.type)
{
case "text/javascript":
return handleJavascript(content);
case "text/markdown":
return handleMarkdown(content);
default:
console.log(`handleContent() does not know how to parse ${content.type}`);
break;
}
}
function handleJavascript(content)
{
console.log("Got some javascript!", content.content);
}
async function handleMarkdown(content)
{
console.log("Got me some markdown!");
marked.setOptions({
highlight: function(code, lang) {
return hljs.highlight(lang, code).value;
},
// langPrefix: ''
});
if (content.metaData.title != undefined) document.title = content.metaData.title;
document.getElementById("post").innerHTML = marked(content.content);
let title = document.createElement("h1");
title.innerHTML = content.metaData.title;
document.getElementById("post").prepend(title)
}
</script>
</head>
<body>
<script>
readUrl("posts-2021-08-17-serving-blog-content-over-dns-md");
</script>
<div id="post"></div>
</body>
</html>

7
public/equilibrium-light.min.css vendored Normal file
View File

@@ -0,0 +1,7 @@
/*!
Theme: Equilibrium Light
Author: Carlo Abelli
License: ~ MIT (or more permissive) [via base16-schemes-source]
Maintainer: @highlightjs/core-team
Version: 2021.05.0
*/pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{color:#43474e;background:#f5f0e7}.hljs ::selection{color:#d8d4cb}.hljs-comment{color:#73777f}.hljs-tag{color:#5a5f66}.hljs-operator,.hljs-punctuation,.hljs-subst{color:#43474e}.hljs-operator{opacity:.7}.hljs-bullet,.hljs-deletion,.hljs-name,.hljs-selector-tag,.hljs-template-variable,.hljs-variable{color:#d02023}.hljs-attr,.hljs-link,.hljs-literal,.hljs-number,.hljs-symbol,.hljs-variable.constant_{color:#bf3e05}.hljs-class .hljs-title,.hljs-title,.hljs-title.class_{color:#9d6f00}.hljs-strong{font-weight:700;color:#9d6f00}.hljs-addition,.hljs-code,.hljs-string,.hljs-title.class_.inherited__{color:#637200}.hljs-built_in,.hljs-doctag,.hljs-keyword.hljs-atrule,.hljs-quote,.hljs-regexp{color:#007a72}.hljs-attribute,.hljs-function .hljs-title,.hljs-section,.hljs-title.function_,.ruby .hljs-property{color:#0073b5}.diff .hljs-meta,.hljs-keyword,.hljs-template-tag,.hljs-type{color:#4e66b6}.hljs-emphasis{color:#4e66b6;font-style:italic}.hljs-meta,.hljs-meta .hljs-keyword,.hljs-meta .hljs-string{color:#c42775}.hljs-meta .hljs-keyword,.hljs-meta-keyword{font-weight:700}

1131
public/highlight.min.js vendored Normal file

File diff suppressed because one or more lines are too long

178
public/index.html Normal file
View File

@@ -0,0 +1,178 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Blog over DNS</title>
<link rel="stylesheet" href="equilibrium-light.min.css">
<script src="highlight.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script>
class Content {
content = "";
type = "";
metaData = {};
index;
/***
* @param content string
* @param type string
* @param index Index
*/
constructor(content, type, index)
{
this.content = content;
this.type = type;
this.index = index;
this.metaData = index.metaData;
}
}
class Index
{
mimeType = "";
chunks = 0;
hashAlgorithm = "";
hash = "";
metaData = {};
constructor(mimeType, chunks, hash, hashAlgorithm, metaData = {})
{
this.mimeType = mimeType;
this.chunks = chunks;
this.hashAlgorithm = hashAlgorithm;
this.hash = hash;
this.metaData = metaData
}
}
const dohServer = "https://cloudflare-dns.com/dns-query?ct=application/dns-json&type=TXT&name=";
const baseDomain = "hod.experiments.jacobkiers.net";
async function readUrl(domain) {
var index = await fetchIndex(`${domain}.${baseDomain}`);
var chunk_promises = [];
for(i = 0; i < index.chunks; i++)
{
chunk_promises[i] = fetchChunk(i, index.hash);
}
const chunks = await Promise.all(chunk_promises);
const base64 = chunks.reduce((built, current) => built += current);
const content = atob(base64);
return handleContent(new Content(content, index.mimeType, index));
}
async function fetchChunk(id, hash)
{
const domain = `${id}.${hash}.${baseDomain}`;
return await fetchData(domain);
}
async function fetchData(domain)
{
const json = await fetch(`${dohServer}${domain}`, {
method: "GET",
headers: {
"Accept": "application/dns-json"
}
})
.then(response => response.json());
const raw_data = json.Answer[0].data;
const data = raw_data.replaceAll(/[\s\"]/g, '');
return data;
}
async function fetchIndex(domain)
{
const index = await fetchData(domain);
let ret = {};
let items = index.split(';');
items.forEach(item => {
let md = item.split('=');
let key = md[0];
let value = md[1];
ret[key] = value;
});
const metadata = JSON.parse(atob(ret["m"]));
return new Index(ret["t"], ret["c"], ret["h"], ret["ha"], metadata);
}
function handleContent(content)
{
if (!content instanceof Content) {
console.log("Not valid content in handleContent.")
return;
}
switch(content.type)
{
case "text/javascript":
return handleJavascript(content);
case "text/markdown":
return handleMarkdown(content);
default:
console.log(`handleContent() does not know how to parse ${content.type}`);
break;
}
}
function handleJavascript(content)
{
console.log("Got some javascript!");
const scripts = document.getElementById("scripts");
const new_script = document.createElement("script");
new_script.text = content.content;
scripts.appendChild(new_script);
}
async function handleMarkdown(content)
{
console.log("Got me some markdown!");
marked.setOptions({
highlight: function(code, lang) {
const avialable_languages = hljs.listLanguages();
const has_language = hljs.getLanguage(lang);
if (typeof has_language === "undefined") return code;
const result = hljs.highlight(code, { language: lang, ignoreIllegals: true});
return result.value;
},
});
if (typeof content.metaData.title !== "undefined") document.title = content.metaData.title;
document.getElementById("post").innerHTML = marked.parse(content.content);
let title = document.createElement("h1");
title.innerHTML = content.metaData.title;
document.getElementById("post").prepend(title)
document.getElementById("nojs").remove();
if (typeof Verifier !== "undefined") {
await (new Verifier()).verify(content);
}
}
</script>
</head>
<body>
<div id="post"></div>
<div id="verification"></div>
<div id="nojs">
<h1>HTML over DNS</h1>
<p>The content of this page is fetched using DNS over HTTP. Since that requires Javascript, please enable that to see the content.</p>
</div>
<div id="scripts">
<script>
readUrl("scripts-verifier-js").then(
() => readUrl("posts-2021-08-17-serving-blog-content-over-dns-md")
);
</script>
</div>
</body>
</html>

View File

@@ -1,116 +1,325 @@
<?php declare(strict_types=1);
if (!defined('EOL')) {
define('EOL', "\n");
}
class Content
class ContentFile
{
public string $data;
public string $mimeType;
public string $fileName;
public string $hash;
private string $filePath;
private string $data;
private string $fileName;
private array $metadata = [];
const TTL = 60;
#const RECORD_TTL = 60 * 60 * 24 * 30; // 30 days.
const RECORD_TTL = 60; // Keep it short for now, so it is easier to experiment.
const METADATA_TTL = 60; // Keep it short for now, so it is easier to update the content.
const RECORD_LENGTH = 1536;
public function __construct(string $file)
{
$this->filePath = $file;
$this->fileName = basename(dirname($file)).'/'.basename($file);
$this->data = file_get_contents($file);
$this->hash = md5($this->data);
$this->parseMetadata();
}
public function toRecords(): string
/**
* @return Record[]
*/
private function toRecords(): array
{
$chunks = str_split(base64_encode($this->data), 250);
$chunks = $this->calculateChunks();
$hash = $this->calculateHash();
$records = [];
$records[] = "; {$this->getDnsName()}";
$records[] = $this->getDnsName().$this->buildMiddle().'"'.$this->buildIndexString(count($chunks)).'"';
foreach($chunks as $id => $chunk)
{
$records = [$this->buildMetadataRecord(count($chunks), $hash)];
foreach ($chunks as $id => $chunk) {
$records[] = $this->buildRecord($id, $chunk);
}
return $records;
}
public function __toString(): string
{
$result = "";
foreach ($records as $record)
{
$result .= $record . PHP_EOL;
foreach ($this->toRecords() as $record) {
$result .= $record.EOL;
}
return $result;
}
private function getDnsName(): string
public function getDnsName(): string
{
return str_replace(['/', '\\', '.'], '-', $this->fileName);
}
private function buildIndexString(int $chunkCount): string
private function buildMetadataRecord(int $chunkCount, string $hash): Record
{
return (new Record())
->setDnsName($this->getDnsName())
->setTtl(self::METADATA_TTL)
->setContent($this->buildIndexString($chunkCount, $hash));
}
private function buildIndexString(int $chunkCount, $hash): string
{
$metadata = base64_encode(json_encode($this->metadata));
return "t={$this->mimeType()};c={$chunkCount};h={$this->hash};m={$metadata}";
return '"'."t={$this->mimeType()};c={$chunkCount};ha=sha-1;h={$hash};m={$metadata}".'"';
}
private function buildMiddle(): string
private function buildRecord(int $id, string $data): Record
{
return "\t".self::TTL."\tIN\tTXT\t";
$name = "$id.{$this->calculateHash()}";
return (new Record())
->setContent($data)
->setDnsName($name)
->setTtl(self::RECORD_TTL);
}
private function buildRecord(int $id, string $data): string
{
$name = "$id.{$this->hash}";
return "$name".$this->buildMiddle().'"'.$data.'"';
}
private function mimeType()
private function mimeType(): string
{
$extension = pathinfo($this->fileName, PATHINFO_EXTENSION);
switch($extension)
{
switch ($extension) {
case "js":
return "text/javascript";
case "md":
return "text/markdown";
default:
return mime_content_type($this->file);
return mime_content_type($this->filePath);
}
}
private function parseMetadata()
private function parseMetadata(): void
{
if ($this->mimeType() != "text/markdown") return [];
$frontmatter_chars = "+++";
$end_of_frontmatter = strpos($this->data, $frontmatter_chars, 2);
if (false === $end_of_frontmatter) {
var_dump($end_of_frontmatter);
return [];
if ($this->mimeType() != "text/markdown") {
return;
}
$frontmatter = substr(
$front_matter_chars = "+++";
$front_matter_end = strpos($this->data, $front_matter_chars, 2);
if (false === $front_matter_end) {
return;
}
$front_matter = substr(
$this->data,
strlen($frontmatter_chars) + 1,
$end_of_frontmatter - strlen($frontmatter_chars) - 2
strlen($front_matter_chars) + 1,
$front_matter_end - strlen($front_matter_chars) - 2
);
$this->data = trim(substr($this->data, $end_of_frontmatter + strlen($frontmatter_chars))).PHP_EOL;
$this->data = trim(substr($this->data, $front_matter_end + strlen($front_matter_chars))).EOL;
$lines = explode(PHP_EOL, $frontmatter);
foreach($lines as $line) {
$lines = explode(EOL, $front_matter);
foreach ($lines as $line) {
$eq_pos = strpos($line, "=");
$key = trim(substr($line, 0, $eq_pos - 1));
$value = trim(substr($line, $eq_pos + 1));
$this->metadata[$key] = $value;
}
}
private function calculateHash(): string
{
return sha1($this->data);
}
private function calculateChunks(): array
{
return str_split(base64_encode($this->data), self::RECORD_LENGTH);
}
}
function update_serial_line(string $line)
class Record
{
private string $dnsName;
private int $ttl;
private string $content;
public function setDnsName(string $dnsName): self
{
$this->dnsName = $dnsName;
return $this;
}
public function setTtl(int $ttl): self
{
$this->ttl = $ttl;
return $this;
}
public function setContent(string $content): self
{
$this->content = $content;
return $this;
}
public function getRecord(): string
{
return $this->dnsName
."\t".$this->ttl
."\tIN"
."\tTXT\t"
.$this->content;
}
public function __toString(): string
{
return $this->getRecord();
}
}
class StartOfAuthority
{
public function __construct(
private string $origin,
private string $serial,
private string $masterDnsServer,
private string $domainContact,
private int $slaveRefreshInterval,
private int $slaveRetryInterval,
private int $slaveCopyExpireTime,
private int $nxDomainCacheTime
) {
}
public function doNotUserMasterDnsServer(): self
{
$servers = $this->dnsServers;
$this->dnsServers = array_filter(
$servers,
function ($server) {
return $server !== $this->masterDnsServer;
}
);
return $this;
}
public function increaseSerial(): self
{
$matches = [];
preg_match('#(\d{8})(\d{2})#', $this->serial, $matches);
list (, $day, $count) = $matches;
$today = date("Ymd");
if ($today === $day) {
$count++;
} else {
$count = 0;
}
$this->serial = sprintf('%1$s%2$02d', $today, $count);;
return $this;
}
public function __toString(): string
{
return <<<EOF
\$ORIGIN $this->origin ; The zone of this zone file
@ IN SOA $this->masterDnsServer $this->domainContact (
$this->serial ; serial
$this->slaveRefreshInterval ; slave refresh interval
$this->slaveRetryInterval ; slave retry interval
$this->slaveCopyExpireTime ; slave copy expire time
$this->nxDomainCacheTime ; NXDOMAIN cache time
)
;
; domain name servers
;
@ IN NS $this->masterDnsServer
EOF;
}
public static function fromString(string $zonefile): self
{
$matches = [];
$found = 1 === preg_match(
'#\$ORIGIN\s+(?P<origin>[a-z.]+)[^@]+@\s+IN\s+SOA\s+(?P<dns>[a-z.]+)\s+(?P<contact>[a-z.]+)\s+\(\D+\s+(?P<serial>\d+)\D+(?P<srefresh>\d+)\D+(?P<sretry>\d+)\D+(?P<sexpire>\d+)\D+(?P<nxcache>\d+)[^\)]+\)#im',
$zonefile,
$matches
);
if (!$found) {
throw new \Exception("Could not find the SOA record.");
}
return new self(
$matches['origin'],
$matches['serial'],
$matches['dns'],
$matches['contact'],
(int)$matches['srefresh'],
(int)$matches['sretry'],
(int)$matches['sexpire'],
(int)$matches['nxcache']
);
}
}
class ZoneFile
{
private StartOfAuthority $soa;
/** @var ContentFile[] */
private array $files = [];
private int $defaultTTL = 300;
public function __construct(
private string $file
) {
$this->soa = StartOfAuthority::fromString(file_get_contents($this->file));
}
public function addFile(ContentFile ...$files): self
{
foreach ($files as $file) {
$this->files[] = $file;
}
return $this;
}
public function setDefaultTTL(int $defaultTTL): self
{
$this->defaultTTL = $defaultTTL;
return $this;
}
public function __toString(): string
{
$this->soa->increaseSerial();
$zone_file = "\$TTL\t{$this->defaultTTL}\t; Default TTL".EOL.EOL;
$zone_file .= $this->soa->__toString().EOL.EOL.EOL;
$zone_file .= ";;; START BLOG RECORDS";
foreach ($this->files as $file) {
$zone_file .= EOL.EOL.EOL;
$zone_file .= '; '.$file->getDnsName().EOL;
$zone_file .= $file;
}
$zone_file .= EOL;
return $zone_file;
}
}
function update_serial_line(string $line): string
{
$matches = [];
preg_match("#(\s+)(\d{10})(.*)#", $line, $matches);
@@ -118,7 +327,7 @@ function update_serial_line(string $line)
$today = date("Ymd");
$day = substr($serial, 0, 8);
$nr = (int) substr($serial, -2);
$nr = (int)substr($serial, -2);
if ($day === $today) {
$nr++;
@@ -132,41 +341,68 @@ function update_serial_line(string $line)
return $start.$new_serial.$last;
}
$zone_file = "zones/db.hod.experiments.jacobkiers.net";
if (!file_exists($zone_file)) {
fwrite(STDERR, "The zone file {$zone_file} does not exist!");
$zone_file_path = "zones/db.hod.experiments.jacobkiers.net";
if (!file_exists($zone_file_path)) {
fwrite(STDERR, "The zone file {$zone_file_path} does not exist!");
exit(1);
}
/*
$lines = file($zone_file);
foreach($lines as $index => &$line)
{
$index = 0;
foreach ($lines as $index => &$line) {
if (str_contains($line, "; serial")) {
$matches = [];
preg_match("#(\s+)(\d{10})(.*)#", $line, $matches);
list($_, $start, $serial, $last) = $matches;
$line = update_serial_line($line).PHP_EOL;
$line = update_serial_line($line).EOL;
}
if (str_starts_with($line, ";; START BLOG RECORDS")) {
break;
}
if (str_starts_with($line, ";; START BLOG RECORDS")) break;
}
$zone_file_contents = implode("", array_slice($lines, 0, $index+1));
$zone_file_contents = implode("", array_slice($lines, 0, $index + 1));
echo $zone_file_contents;
$it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator(__DIR__."/content"));
foreach ($it as $file)
{
if ($file->isDir()) continue;
if (str_contains($file->getPathname(), "ignore")) continue;
foreach ($it as $file) {
if ($file->isDir()) {
continue;
}
if (str_contains($file->getPathname(), "ignore")) {
continue;
}
$bootstrap = new Content($file->getPathname());
$zone_file_contents .= PHP_EOL.$bootstrap->toRecords().PHP_EOL;
$content = new ContentFile($file->getPathname());
$zone_file_contents .= EOL.$content->__toString().EOL;
}
$zone_file_contents .= PHP_EOL;
$zone_file_contents .= EOL;
#echo $zone_file_contents;
echo $zone_file_contents;
file_put_contents($zone_file, $zone_file_contents);
#file_put_contents($zone_file, $zone_file_contents);
*/
$zone_file = new ZoneFile($zone_file_path);
$it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator(__DIR__."/content"));
foreach ($it as $file) {
if ($file->isDir()) {
continue;
}
if (str_contains($file->getPathname(), "ignore")) {
continue;
}
$content = new ContentFile($file->getPathname());
$zone_file->addFile($content);
}
$zone_file_contents = $zone_file->__toString();
echo $zone_file_contents;
file_put_contents($zone_file_path, $zone_file_contents);

View File

@@ -1,19 +1,18 @@
;
$TTL 5m ; Default TTL
@ IN SOA home.kie.rs. postmaster.kie.rs. (
2021081611 ; serial
1h ; slave refresh interval
15m ; slave retry interval
1w ; slave copy expire time
1h ; NXDOMAIN cache time
)
$TTL 300 ; Default TTL
$ORIGIN hod.experiments.jacobkiers.net.
$ORIGIN hod.experiments.jacobkiers.net. ; The zone of this zone file
@ IN SOA experiments.jacobkiers.net. postmaster.jacobkiers.net. (
2021082109 ; serial
3600 ; slave refresh interval
900 ; slave retry interval
604800 ; slave copy expire time
3600 ; NXDOMAIN cache time
)
;
; domain name servers
;
@ IN NS home.kie.rs.
@ IN NS experiments.jacobkiers.net.
;; START BLOG RECORDS