new post - dnsimg
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 2m34s

This commit is contained in:
Asher 2025-06-15 14:53:19 +01:00
parent f5c2085fe1
commit f1ff991010
12 changed files with 390 additions and 38 deletions

BIN
public/img1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

BIN
public/img2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 KiB

BIN
public/img3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 758 KiB

BIN
public/img4.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
public/img5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 634 KiB

BIN
public/img6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

@ -40,7 +40,7 @@ export default function ExamplePost() {
document.head.appendChild(metaOgDesc); document.head.appendChild(metaOgDesc);
} }
metaOgDesc.setAttribute('content', description); metaOgDesc.setAttribute('content', description);
}, []); }, [pageTitle]);
return ( return (
<PageTransition> <PageTransition>

View File

@ -0,0 +1,34 @@
{
"data": {"url": "./scatter.csv"},
"transform": [
{
"calculate": "1 - abs(datum.y - 3.141592653589793) / 3.141592653589793",
"as": "closeness_to_pi"
}
],
"mark": {"type": "point", "size": 30},
"width": 300,
"height": 300,
"encoding": {
"x": {
"field": "log10_x",
"title": "Iterations",
"axis": {
"labelOverlap": "greedy",
"labelExpr": "'10^' + datum.value"
}
},
"y": {
"field": "closeness_to_pi",
"title": "Accuracy (% close to π)",
"scale": {
"zero": false,
"reverse": true
},
"axis": {
"labelOverlap": "greedy",
"format": ".1%"
}
}
}
}

View File

@ -0,0 +1,23 @@
import { Metadata } from "next";
// Post content
export const title = "dnsimg - storing images in txt records";
export const description = "I was intrigued by the idea of storing images in DNS records, and I wanted to test out how effectively images could be stored in DNS records. I've always been interested in TXT records because they seem to be a useful way of storing arbitrary data, and in this blog post I'll discuss how I went from an idea to developing the project into almost a protocol sort of method for storing an image on a domain name.";
// Next.js metadata
export const generateMetadata = (): Metadata => {
return {
title: title,
description: description,
openGraph: {
title: title,
description: description,
type: 'article',
},
twitter: {
card: 'summary',
title: title,
description: description,
},
};
};

View File

@ -0,0 +1,244 @@
"use client"
import { title, description } from './metadata';
import Link from 'next/link';
import { PageTransition } from '@/components/PageTransition';
import { useEffect } from 'react';
import BackButton from '@/components/BackButton';
import Image from 'next/image';
import CodeBlock from '../../../../components/code'
// Note: Static metadata is also generated by the generateMetadata export in metadata.ts
// This useEffect hook ensures the title is properly set on the client side
export default function ExamplePost() {
const pageTitle = `${title} | Asher Falcon`;
// Update the title and metadata on the client side
useEffect(() => {
document.title = pageTitle;
// Update meta tags
const metaDescription = document.querySelector('meta[name="description"]');
if (metaDescription) {
metaDescription.setAttribute('content', description);
}
// Update Open Graph tags
let metaOgTitle = document.querySelector('meta[property="og:title"]');
if (!metaOgTitle) {
metaOgTitle = document.createElement('meta');
metaOgTitle.setAttribute('property', 'og:title');
document.head.appendChild(metaOgTitle);
}
metaOgTitle.setAttribute('content', title);
let metaOgDesc = document.querySelector('meta[property="og:description"]');
if (!metaOgDesc) {
metaOgDesc = document.createElement('meta');
metaOgDesc.setAttribute('property', 'og:description');
document.head.appendChild(metaOgDesc);
}
metaOgDesc.setAttribute('content', description);
}, [pageTitle]);
return (
<PageTransition>
<div className='pb-[20px]'>
<div style={{paddingTop: "50px"}} className="relative flex justify-center items-center h-[50px]">
<div className="absolute left-0 flex items-center h-full ml-6">
<BackButton />
</div>
<span className="text-sm sm:text-base md:text-xl lg:text-2xl truncate max-w-[80%]">{title}</span>
</div>
<div style={{paddingTop: "30px"}} className='flex justify-center items-center md:w-[70%] xl:w-[50%] w-[85%] mx-auto'>
<div>
<p>{description}</p>
</div>
</div>
<div style={{paddingTop: "30px",paddingBottom: "30px"}} className='flex flex-col justify-center items-center md:w-[70%] xl:w-[50%] w-[85%] mx-auto'>
<p>So, an image inside DNS? How can it be done? Well the most obvious way and the method I tried here was storing the data inside TXT records. Firstly, we need to find a way to store the image inside the dns records. At first the method I tried was simply getting the hex characters of the data. We get this using the command below: </p>
</div>
<CodeBlock language="bash" code={`xxd -p output.jpg > output.txt`} />
<div style={{paddingTop: "30px",paddingBottom: "30px"}} className='flex flex-col justify-center items-center md:w-[70%] xl:w-[50%] w-[85%] mx-auto'>
<p>This will not be as efficient as storing the data in base64 format, as this will use 2x the space where base64 would only use 1.33x the file size, however for testing I believe it is fine to use for now.</p>
<p>The next hurdle is as you can see below, when I tried to just add all the hex data in one txt record, cloudflare shows us an errror:</p>
</div>
<div style={{paddingTop: "30px",paddingBottom: "30px"}} className='flex flex-col justify-center items-center md:w-[70%] xl:w-[50%] w-[85%] mx-auto'>
<Image src="/img1.png" alt="output" width={600} height={400} />
</div>
<div style={{paddingTop: "30px",paddingBottom: "30px"}} className='flex flex-col justify-center items-center md:w-[70%] xl:w-[50%] w-[85%] mx-auto'>
<p>So, we need to split our hex data into 2048 character chunks. A simple python script can be written to do this and found below:</p>
</div>
<CodeBlock language="python" code={`image = open("output.txt", "r").read()
image = image.replace("\\n", "")
chunks = []
total = int(len(image)/2048)+1
for i in range(total):
chunk = image[i*2048:(i+1)*2048]
print(f"Chunk #{i+1}, size: {len(chunk)}")
chunks.append(chunk)
domain = "asherfalcon.com"
with open(f"{domain}.txt", "a") as dns:
for chunkIndex in range(len(chunks)):
dns.write(f"dnsimg-{chunkIndex+1}.{domain}. 60 IN TXT \"{chunks[chunkIndex]}\"\n")
dns.write(f"dnsimg-count.{domain}. 60 IN TXT \"{len(chunks)}\"\n")`} />
<div style={{paddingTop: "30px",paddingBottom: "30px"}} className='flex flex-col justify-center items-center md:w-[70%] xl:w-[50%] w-[85%] mx-auto'>
<p>This will create a txt record for each chunk of the image, and a &apos;dnsimg-count&apos; record for the total number of chunks. The count is neccesary so that when we want to load the iamge, we know how many chunks exist and how many we need to request. </p>
</div>
<div style={{paddingTop: "30px",paddingBottom: "30px"}} className='flex flex-col justify-center items-center md:w-[70%] xl:w-[50%] w-[85%] mx-auto'>
<p>We can then upload the dns file to cloudflare and import it, which will create all the records for us. After a few minutes using the dig command we can see that the chunks have been stored. Cloudflare splits them up into additional chunks per record but that is not an issue as we can just concatenate them.</p>
</div>
<div style={{paddingTop: "30px",paddingBottom: "30px"}} className='flex flex-col justify-center items-center md:w-[70%] xl:w-[50%] w-[85%] mx-auto'>
<Image src="/img2.png" alt="output" width={600} height={400} />
<Image className='mt-4' src="/img3.png" alt="output" width={600} height={400} />
</div>
<div style={{paddingTop: "30px",paddingBottom: "30px"}} className='flex flex-col justify-center items-center md:w-[70%] xl:w-[50%] w-[85%] mx-auto'>
<p>Now we know that our data is out there, lets try rebuild the image from the dns records. We can write another simple python script to fetch them using dig asynchronously and then concatenate them into a single file, in jpg format. See the script below:</p>
</div>
<CodeBlock language="python" code={`import subprocess
import threading
import sys
class bcolors:
HEADER = '\\\\033[95m'
OKBLUE = '\\\\033[94m'
OKCYAN = '\\\\033[96m'
OKGREEN = '\\\\033[92m'
WARNING = '\\\\033[93m'
FAIL = '\\\\033[91m'
ENDC = '\\\\033[0m'
BOLD = '\\\\033[1m'
UNDERLINE = '\\\\033[4m'
# Replace with your domain
domain = "containerback.com"
# Run the dig command
result = subprocess.run(
["dig", "@8.8.8.8", "+short", f"dnsimg-count.{domain}", "TXT"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
chunks = []
def printStatus():
# "\\\\033[F"+
msg = bcolors.OKBLUE+"["
for i in chunks:
if(i==""):
msg+=bcolors.FAIL+"#"
else:
msg+=bcolors.OKGREEN+"#"
msg+=bcolors.OKBLUE+"]"
print(msg)
def getChunk(chunkIndex):
chunk = subprocess.run(
["dig", "+short", f"dnsimg-{chunkIndex+1}.{domain}", "TXT"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
if(chunk.stdout!=""):
chunkData = chunk.stdout.replace(" ","").replace("\\\\\\"","").replace("\\\\\\n","")
chunks[chunkIndex] = chunkData
else:
print(f"Err {chunkIndex} {chunk.stderr} &apos;{chunk.stdout}&apos;")
# printStatus()
# print(f"Added chunk #{chunkIndex+1} ({len(chunkData)} chars)")
if(result.stdout == ""):
print("No dnsimg found")
else:
size = int(result.stdout[1:-2])
print(f"Found dnsimg with {size} chunks")
chunks = [""]*size
threads = []
for chunkIndex in range(size):
threads.append(threading.Thread(target=getChunk, args=(chunkIndex,)))
for t in threads:
t.start()
for t in threads:
t.join()
printStatus()
printStatus()
with open("dnsimg.jpg", "wb") as output:
output.write(bytes.fromhex("".join(chunks)))`} />
<div style={{paddingTop: "30px",paddingBottom: "30px"}} className='flex flex-col justify-center items-center md:w-[70%] xl:w-[50%] w-[85%] mx-auto'>
<p>When I first tried this I don&apos;t think the records had properly propagated, so I had to wait a few minutes before I could see the image. Look below to see the (slightly) corrupted image created when a few records were missing:</p>
</div>
<div style={{paddingTop: "30px",paddingBottom: "30px"}} className='flex flex-col justify-center items-center md:w-[70%] xl:w-[50%] w-[85%] mx-auto'>
<Image src="/img4.jpg" alt="output" width={600} height={400} />
</div>
<div style={{paddingTop: "30px",paddingBottom: "30px"}} className='flex flex-col justify-center items-center md:w-[70%] xl:w-[50%] w-[85%] mx-auto'>
<p>After waiting another 10 or so minutes, we can run it again and get the full image through! The image is stored in 21 chunks of 2048 characters, and is not a terribly high resolution but it serves as a good first proof of concept:</p>
</div>
<div style={{paddingTop: "30px",paddingBottom: "30px"}} className='flex flex-col justify-center items-center md:w-[70%] xl:w-[50%] w-[85%] mx-auto'>
<Image src="/img5.png" alt="output" width={600} height={400} />
</div>
<div style={{paddingTop: "30px",paddingBottom: "30px"}} className='flex flex-col justify-center items-center md:w-[70%] xl:w-[50%] w-[85%] mx-auto'>
<p>Next I wanted to try some larger images, which mostly worked but I found an upper bound when I tried a (over 1MB) image. Not sure if this is a cloudflare limit or a wider rule but heres the error I got:</p>
</div>
<div style={{paddingTop: "30px",paddingBottom: "30px"}} className='flex flex-col justify-center items-center md:w-[70%] xl:w-[50%] w-[85%] mx-auto'>
<Image src="/img6.png" alt="output" width={600} height={200} />
</div>
<div style={{paddingTop: "30px",paddingBottom: "30px"}} className='flex flex-col justify-center items-center md:w-[70%] xl:w-[50%] w-[85%] mx-auto'>
<p>So, finally I created a lovely web tool you can try out <Link className='text-blue-500' href="https://dnsimg.asherfalcon.com" target="_blank">here</Link> which allows you to type a domain and load its image. I created images on the domains &apos;asherfalcon.com&apos; and &apos;containerback.com&apos; but you should try add images to your own domains! You can use a domain or any subdomain and use the scripts in the repository <Link className='text-blue-500' href="https://git.asherfalcon.com/asher/image_over_dns" target="_blank">here</Link> to create your own image. If you want to see a video of the web tool in action see below:</p>
</div>
<div style={{paddingTop: "30px",paddingBottom: "30px"}} className='flex flex-col justify-center items-center md:w-[70%] xl:w-[50%] w-[85%] mx-auto'>
<iframe
width="560"
height="315"
src="https://www.youtube.com/embed/5BO38soTjjA?start=64"
title="YouTube video player"
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
/>
</div>
<div style={{paddingTop: "30px",paddingBottom: "30px"}} className='flex flex-col justify-center items-center md:w-[70%] xl:w-[50%] w-[85%] mx-auto'>
<p>I hope you enjoyed this blog post! If you have any questions or comments, please feel free to reach out to me <Link href="/contact" className="text-blue-500">here</Link></p>
</div>
</div>
</PageTransition>
);
}

View File

@ -0,0 +1,87 @@
export const charm = `#!/bin/bash
message=$(gum input --placeholder "Commit message")
git add .
git commit -m "\${message}"
`
export const basicScript = `#!/bin/bash
git add .
git commit -m "updated docker-compose.yml"
git push
ssh -o "StrictHostKeyChecking=no" -i ssh.key $CD_USER@$CD_HOST << EOF
sudo su
cd $CD_PATH
docker compose down
git pull
docker compose up -d
exit
EOF
`
export const script = `#!/bin/bash
# A script to redeploy this docker infrastructure using ssh
if [ -f .env ]; then
source .env
else
echo ".env file not found. Please create it and set the necessary environment variables."
exit 1
fi
check_gum() {
if command -v gum &> /dev/null; then
return 0
else
echo "Gum not found, installing..."
return 1
fi
}
install_gum() {
if [[ "$OSTYPE" == "darwin"* ]]; then
/bin/bash -c "$(curl -fsSL
https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
brew install gum
elif [[ -f /etc/os-release ]]; then
source /etc/os-release
if [[ $ID == "ubuntu" ]] || [[ $ID == "debian" ]]; then
sudo apt update
sudo apt install -y gum
elif [[ $ID == "fedora" ]] || [[ $ID == "centos" ]] || [[ $ID == "rhel" ]]; then
sudo dnf install -y gum
else
echo "Unsupported Linux distribution. Please install Gum
manually."
exit 1
fi
else
echo "Unsupported operating system. Please install Gum manually."
exit 1
fi
}
# Main script execution
if ! check_gum; then
if ! install_gum; then
echo "Failed to install Gum. Please install it manually and run
the script again."
exit 1
fi
fi
message=$(gum input --placeholder "Commit message")
git add .
git commit -m "\${message}"
git push
ssh -o "StrictHostKeyChecking=no" -i ssh.key $CD_USER@$CD_HOST << EOF
sudo su
cd $CD_PATH
docker compose down
git pull
docker compose up -d
exit
EOF`

View File

@ -1,49 +1,13 @@
"use client"; "use client";
import { IoMdArrowBack } from "react-icons/io"; import { IoMdArrowBack } from "react-icons/io";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
export default function BackButton() { export default function BackButton() {
const router = useRouter(); const router = useRouter();
const [pageVisits, setPageVisits] = useState<string[]>([]);
useEffect(() => {
// Get page history from session storage
const storedHistory = sessionStorage.getItem('pageHistory');
const history = storedHistory ? JSON.parse(storedHistory) : [];
// Add current page to history if it's not the most recent
const currentPath = window.location.pathname;
if (history.length === 0 || history[history.length - 1] !== currentPath) {
const newHistory = [...history, currentPath];
sessionStorage.setItem('pageHistory', JSON.stringify(newHistory));
setPageVisits(newHistory);
} else {
setPageVisits(history);
}
}, []);
const handleBack = () => { const handleBack = () => {
// If we have at least 2 pages in history (current + previous) router.back();
if (pageVisits.length > 1) {
// Remove current page from history
const newHistory = [...pageVisits];
newHistory.pop();
sessionStorage.setItem('pageHistory', JSON.stringify(newHistory));
// Go to previous page in our history
router.push(newHistory[newHistory.length - 1]);
} else {
// If no internal history, go to home page first
const currentPath = window.location.pathname;
if (currentPath !== '/') {
router.push('/');
} else {
// If already on home page, then go back normally
window.history.back();
}
}
}; };
return ( return (