Compare commits
7 Commits
1dcd49fd0d
...
master
Author | SHA1 | Date | |
---|---|---|---|
d942833b06 | |||
13d8b7f87e | |||
5dd7a230fb | |||
2d477bb77e | |||
4b117a4743 | |||
74619915ca | |||
727f2c671e |
@@ -22,7 +22,7 @@ fetch("https://cloudflare-dns.com/dns-query?ct=application/dns-json&type=TXT&nam
|
|||||||
|
|
||||||
The content itself is served over DNS, using CoreDNS, with these contents:
|
The content itself is served over DNS, using CoreDNS, with these contents:
|
||||||
|
|
||||||
```js
|
```hcl
|
||||||
hod.experiments.jacobkiers.net.:53 {
|
hod.experiments.jacobkiers.net.:53 {
|
||||||
log
|
log
|
||||||
auto hod.experiments.jacobkiers.net. {
|
auto hod.experiments.jacobkiers.net. {
|
||||||
@@ -34,7 +34,7 @@ hod.experiments.jacobkiers.net.:53 {
|
|||||||
|
|
||||||
This feeds into a zone file, which looks like this:
|
This feeds into a zone file, which looks like this:
|
||||||
|
|
||||||
```js
|
```dns
|
||||||
$TTL 5m ; Default TTL
|
$TTL 5m ; Default TTL
|
||||||
@ IN SOA experiments.jacobkiers.net. postmaster.jacobkiers.net. (
|
@ IN SOA experiments.jacobkiers.net. postmaster.jacobkiers.net. (
|
||||||
2021081612 ; serial
|
2021081612 ; serial
|
||||||
@@ -73,7 +73,7 @@ In short: just because I could. It was one of those ideas I was wondering idly a
|
|||||||
### Has it any practical use?
|
### 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
|
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 😉
|
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.
|
It would be an interesting experiment to see how feasible that is.
|
||||||
|
|
||||||
|
21
content/scripts/verifier.js
Normal file
21
content/scripts/verifier.js
Normal 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.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
145
index.html
145
index.html
@@ -1,145 +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)
|
|
||||||
document.getElementById("nojs").style.visibility = "hidden";
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<script>
|
|
||||||
readUrl("posts-2021-08-17-serving-blog-content-over-dns-md");
|
|
||||||
|
|
||||||
</script>
|
|
||||||
<div id="post"></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>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
7
public/equilibrium-light.min.css
vendored
Normal file
7
public/equilibrium-light.min.css
vendored
Normal 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
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
178
public/index.html
Normal 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>
|
@@ -1,116 +1,325 @@
|
|||||||
<?php declare(strict_types=1);
|
<?php declare(strict_types=1);
|
||||||
|
if (!defined('EOL')) {
|
||||||
|
define('EOL', "\n");
|
||||||
|
}
|
||||||
|
|
||||||
class Content
|
class ContentFile
|
||||||
{
|
{
|
||||||
public string $data;
|
private string $filePath;
|
||||||
public string $mimeType;
|
private string $data;
|
||||||
public string $fileName;
|
private string $fileName;
|
||||||
public string $hash;
|
|
||||||
private array $metadata = [];
|
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)
|
public function __construct(string $file)
|
||||||
{
|
{
|
||||||
|
$this->filePath = $file;
|
||||||
$this->fileName = basename(dirname($file)).'/'.basename($file);
|
$this->fileName = basename(dirname($file)).'/'.basename($file);
|
||||||
$this->data = file_get_contents($file);
|
$this->data = file_get_contents($file);
|
||||||
$this->hash = md5($this->data);
|
|
||||||
|
|
||||||
$this->parseMetadata();
|
$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->buildMetadataRecord(count($chunks), $hash)];
|
||||||
$records[] = "; {$this->getDnsName()}";
|
foreach ($chunks as $id => $chunk) {
|
||||||
$records[] = $this->getDnsName().$this->buildMiddle().'"'.$this->buildIndexString(count($chunks)).'"';
|
|
||||||
|
|
||||||
foreach($chunks as $id => $chunk)
|
|
||||||
{
|
|
||||||
$records[] = $this->buildRecord($id, $chunk);
|
$records[] = $this->buildRecord($id, $chunk);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return $records;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __toString(): string
|
||||||
|
{
|
||||||
$result = "";
|
$result = "";
|
||||||
foreach ($records as $record)
|
|
||||||
{
|
foreach ($this->toRecords() as $record) {
|
||||||
$result .= $record . PHP_EOL;
|
$result .= $record.EOL;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getDnsName(): string
|
public function getDnsName(): string
|
||||||
{
|
{
|
||||||
return str_replace(['/', '\\', '.'], '-', $this->fileName);
|
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));
|
$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
|
private function mimeType(): string
|
||||||
{
|
|
||||||
$name = "$id.{$this->hash}";
|
|
||||||
|
|
||||||
return "$name".$this->buildMiddle().'"'.$data.'"';
|
|
||||||
}
|
|
||||||
|
|
||||||
private function mimeType()
|
|
||||||
{
|
{
|
||||||
$extension = pathinfo($this->fileName, PATHINFO_EXTENSION);
|
$extension = pathinfo($this->fileName, PATHINFO_EXTENSION);
|
||||||
|
|
||||||
switch($extension)
|
switch ($extension) {
|
||||||
{
|
|
||||||
case "js":
|
case "js":
|
||||||
return "text/javascript";
|
return "text/javascript";
|
||||||
case "md":
|
case "md":
|
||||||
return "text/markdown";
|
return "text/markdown";
|
||||||
|
|
||||||
default:
|
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 [];
|
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 [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$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,
|
$this->data,
|
||||||
strlen($frontmatter_chars) + 1,
|
strlen($front_matter_chars) + 1,
|
||||||
$end_of_frontmatter - strlen($frontmatter_chars) - 2
|
$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);
|
$lines = explode(EOL, $front_matter);
|
||||||
foreach($lines as $line) {
|
foreach ($lines as $line) {
|
||||||
$eq_pos = strpos($line, "=");
|
$eq_pos = strpos($line, "=");
|
||||||
$key = trim(substr($line, 0, $eq_pos - 1));
|
$key = trim(substr($line, 0, $eq_pos - 1));
|
||||||
$value = trim(substr($line, $eq_pos + 1));
|
$value = trim(substr($line, $eq_pos + 1));
|
||||||
$this->metadata[$key] = $value;
|
$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 = [];
|
$matches = [];
|
||||||
preg_match("#(\s+)(\d{10})(.*)#", $line, $matches);
|
preg_match("#(\s+)(\d{10})(.*)#", $line, $matches);
|
||||||
@@ -118,7 +327,7 @@ function update_serial_line(string $line)
|
|||||||
|
|
||||||
$today = date("Ymd");
|
$today = date("Ymd");
|
||||||
$day = substr($serial, 0, 8);
|
$day = substr($serial, 0, 8);
|
||||||
$nr = (int) substr($serial, -2);
|
$nr = (int)substr($serial, -2);
|
||||||
|
|
||||||
if ($day === $today) {
|
if ($day === $today) {
|
||||||
$nr++;
|
$nr++;
|
||||||
@@ -132,41 +341,68 @@ function update_serial_line(string $line)
|
|||||||
return $start.$new_serial.$last;
|
return $start.$new_serial.$last;
|
||||||
}
|
}
|
||||||
|
|
||||||
$zone_file = "zones/db.hod.experiments.jacobkiers.net";
|
$zone_file_path = "zones/db.hod.experiments.jacobkiers.net";
|
||||||
if (!file_exists($zone_file)) {
|
if (!file_exists($zone_file_path)) {
|
||||||
fwrite(STDERR, "The zone file {$zone_file} does not exist!");
|
fwrite(STDERR, "The zone file {$zone_file_path} does not exist!");
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
$lines = file($zone_file);
|
$lines = file($zone_file);
|
||||||
|
|
||||||
foreach($lines as $index => &$line)
|
$index = 0;
|
||||||
{
|
foreach ($lines as $index => &$line) {
|
||||||
if (str_contains($line, "; serial")) {
|
if (str_contains($line, "; serial")) {
|
||||||
$matches = [];
|
$matches = [];
|
||||||
preg_match("#(\s+)(\d{10})(.*)#", $line, $matches);
|
preg_match("#(\s+)(\d{10})(.*)#", $line, $matches);
|
||||||
list($_, $start, $serial, $last) = $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;
|
echo $zone_file_contents;
|
||||||
|
|
||||||
$it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator(__DIR__."/content"));
|
$it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator(__DIR__."/content"));
|
||||||
foreach ($it as $file)
|
foreach ($it as $file) {
|
||||||
{
|
if ($file->isDir()) {
|
||||||
if ($file->isDir()) continue;
|
continue;
|
||||||
if (str_contains($file->getPathname(), "ignore")) continue;
|
}
|
||||||
|
if (str_contains($file->getPathname(), "ignore")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$bootstrap = new Content($file->getPathname());
|
$content = new ContentFile($file->getPathname());
|
||||||
$zone_file_contents .= PHP_EOL.$bootstrap->toRecords().PHP_EOL;
|
$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);
|
@@ -1,19 +1,18 @@
|
|||||||
;
|
$TTL 300 ; Default TTL
|
||||||
$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
|
|
||||||
)
|
|
||||||
|
|
||||||
$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
|
; domain name servers
|
||||||
;
|
;
|
||||||
@ IN NS home.kie.rs.
|
@ IN NS experiments.jacobkiers.net.
|
||||||
|
|
||||||
|
|
||||||
;; START BLOG RECORDS
|
;; START BLOG RECORDS
|
||||||
|
Reference in New Issue
Block a user