aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README19
-rw-r--r--client/index.html37
-rw-r--r--client/js/radio_emulator.js188
-rwxr-xr-xcreate_list.sh119
-rwxr-xr-xcreate_metadata_show.sh82
-rwxr-xr-xcreate_static_list.sh59
-rwxr-xr-xserver/index.js171
7 files changed, 675 insertions, 0 deletions
diff --git a/README b/README
index 9c60619..0c4fd29 100644
--- a/README
+++ b/README
@@ -1 +1,20 @@
That's a small proof-of-concept for radio-station-emulator
+
+For obvious reasons, we don't guarantee that the program/list creator won't be
+exploitable. That's why we mentioned the role of Radio Executive. The part that
+is automated, it's only for proof-of-concept. We could assume that new shows are
+only inserted by the Program Manager or Radio Executive, depending on how the
+real radio station works.
+
+Client implementation:
+- ./client/index.html
+- ./client/js/radio_emulator.js
+
+Server implementation:
+- ./server/index.js
+
+Helping scripts:
+- ./create_list.sh
+- ./create_metadata_show.sh
+- ./create_static_list.sh
+
diff --git a/client/index.html b/client/index.html
new file mode 100644
index 0000000..1cfcd5d
--- /dev/null
+++ b/client/index.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ </head>
+ <body>
+ <h1>Radio station example</h1>
+ <div>
+ <p>Hit play!</p>
+ <button onclick="sync_radio()">Sync!</button>
+ <div class="radio-player">
+ <audio preload="auto" controls style="width: 100%;">
+ <source src=""/>
+ </audio>
+ </div>
+ </div>
+ <div>
+ <h2>Artist</h2>
+ <p class="artist"></p>
+ <h2>Title</h2>
+ <p class="title"></p>
+ <h2>You are here for (seconds)</h2>
+ <p class="you-are-here"></p>
+ <h2>Started on</h2>
+ <p class="started-on"></p>
+ <h2>Current time</h2>
+ <p class="current-time"></p>
+ <h2>Hash of current show</h2>
+ <p class="current-show-hash"></p>
+ <h2>Hash of list</h2>
+ <p class="list-hash"></p>
+ </div>
+ <div>
+ <a href="./data.html" target="_blank">Data</a>
+ </div>
+ <script src="./js/radio_emulator.js"></script>
+ </body>
+</html>
diff --git a/client/js/radio_emulator.js b/client/js/radio_emulator.js
new file mode 100644
index 0000000..98138e7
--- /dev/null
+++ b/client/js/radio_emulator.js
@@ -0,0 +1,188 @@
+// Radio Emulator
+// Kaotisk Hund 2024
+//
+// A simple co-mechanism to pretend playing a live radio as it would happen for
+// a radio station that mostly plays prerecoded shows.
+//
+// Client side implementation
+// Let's remind here the structures we are waiting for:
+// 1. Hash
+// 2. 0
+// 3. audio/ogg file
+// 4. application/json file
+//
+// 1. Hash
+// Can be an SHA512sum or SHA256sum, we don't do checks, we only ask the hash
+// accompanied by what do we think it is.
+//
+// 2. 0
+// When nothing exists on the radio station we are visiting.
+//
+// 3. audio/ogg file
+// An audio file to play.
+//
+// 4. application/json file
+// Could be one of the following:
+// - list
+// - show_info
+//
+
+var audioElement = document.querySelector('audio');
+var sourceElement = document.querySelector('source');
+var currentTimeP = document.querySelector('.current-time');
+var listStartedP = document.querySelector('.started-on');
+var currentShowHash = document.querySelector('.current-show-hash');
+var listHash = document.querySelector('.list-hash');
+var artistP = document.querySelector('.artist');
+var titleP = document.querySelector('.title');
+var radioPlayerDiv = document.querySelector('.radio-player');
+var youAreHere = document.querySelector('.you-are-here');
+
+var seconds_here = 0;
+function increaseSeconds()
+{
+ seconds_here = seconds_here+1;
+ youAreHere.innerText = seconds_here;
+ return seconds_here;
+}
+function getSecondsHere()
+{
+ return seconds_here;
+}
+
+setInterval(increaseSeconds, 1000);
+
+
+function FetchJSON( url, callback, params )
+{
+ const request = new XMLHttpRequest();
+ request.addEventListener("load", ()=>{
+ var json = JSON.parse(request.response);
+ if(request.status !== 404){
+ callback(json, params);
+ } else {
+ console.log(`ERROR ${request.status} while loading ${url}`);
+ }
+ });
+ request.addEventListener("error", ()=>{
+ console.log("An error occured. While loading: "+url+" for "+callback+".");
+ });
+ // ProgressBar update: Interesting but not needed
+ // request.addEventListener("progress", (event)=>{
+ // if (event.lengthComputable && progressPlaceholder){
+ // httpProgressPlaceholder.value = (event.loaded / event.total) * 100;
+ // } else {
+ // httpProgressPlaceholder.value = 0;
+ // // console.log("Supposingly zeroed progressPlaceholder");
+ // }
+ // });
+ request.addEventListener("abort", ()=>{
+ console.log("Request aborted.");
+ });
+ request.open("GET", url);
+ request.send();
+}
+
+function genericCallback(json, params)
+{
+ console.log('genericCallback');
+ console.log(json);
+ console.log(params);
+}
+
+var calledLoadShowCallback = 0;
+
+function stopHereAndReflect()
+{
+
+}
+
+function loadShowCallback(json, params)
+{
+ const [ list, now_on_sequence, element, hash_of_list ] = params;
+// if ( calledLoadShowCallback === 0 )
+// {
+// calledLoadShowCallback++;
+ console.log('loadShowCallback');
+ console.log(json);
+ console.log(element);
+ console.log(params);
+ listStartedP.innerText = list.started_on;
+ listHash.innerText = hash_of_list;
+ currentShowHash.innerText = json.hash;
+ artistP.innerText = json.artist;
+ titleP.innerText = json.title;
+ // audioElement.pause();
+ sourceElement.src = "http://z.kaotisk-hund.com:8010/v0/audio/ogg/" + json.hash + "#t=" + Math.floor((now_on_sequence - element.starts_on)/1000);
+ sourceElement.type = json.mimetype;
+ audioElement.load();
+ console.log('plays here: '+(now_on_sequence - element.starts_on)/1000);
+ audioElement.addEventListener('canplaythrough', function(){
+ console.log('CAN PLAY THROUGH');
+ if ( calledLoadShowCallback < 100 )
+ {
+ calledLoadShowCallback++;
+ currentTimeP.innerText = ((now_on_sequence - element.starts_on)/1000);
+ audioElement.currentTime = ((now_on_sequence - element.starts_on)/1000);
+ audioElement.play();
+ }
+ });
+ audioElement.addEventListener('ended', function(){
+ location.reload();
+ //FetchJSON('http://z.kaotisk-hund.com:8010/v0/list', hashCallback, [ new Date().getTime()]);
+ });
+ sync_radio();
+ setTimeout(sync_radio, 30000);
+
+// } else {
+// return 0;
+// }
+}
+
+function sync_radio()
+{
+ var value = currentTimeP.innerText;
+ var new_now = parseFloat(value) + getSecondsHere();
+ console.log("Trying to sync @ "+ value + " + " +getSecondsHere() + " = " + new_now);
+ if ( value !== "" )
+ {
+ audioElement.currentTime = new_now;
+ }
+}
+
+function listCallback(json, params)
+{
+ console.log('listCallback');
+ var [ now, hash_of_list ] = params;
+ // var now = Date.now();
+ var delta_time = now - json.started_on;
+ var min_times_played = Math.floor( delta_time / json.duration );
+ var max_times_to_be_played = delta_time / json.duration;
+ var Dt = max_times_to_be_played - min_times_played;
+ var now_on_sequence = Dt * json.duration;
+ console.log(`now_on_sequence: ${now_on_sequence}, Dt: ${Dt}`)
+
+ previous = { starts_on: 0 };
+ json.list.forEach((element)=>{
+ if ( now_on_sequence < element.starts_on && now_on_sequence > previous.starts_on){
+ } else {
+ now_on_sequence = now_on_sequence - previous.starts_on;
+ console.log(now_on_sequence);
+ previous = element;
+ console.log(element);
+ FetchJSON("http://z.kaotisk-hund.com:8010/v0/application/json/" + element.hash, loadShowCallback, [json, now_on_sequence, element, hash_of_list]);
+ }
+ });
+}
+
+function hashCallback(json, params)
+{
+ var [ now ] = params;
+ console.log('hashCallback');
+ FetchJSON('http://z.kaotisk-hund.com:8010/v0/application/json/' + json.latest_list, listCallback, [now, json.latest_list]);
+}
+
+FetchJSON('http://z.kaotisk-hund.com:8010/v0/list', hashCallback, [ new Date().getTime() ]);
+
+
+
diff --git a/create_list.sh b/create_list.sh
new file mode 100755
index 0000000..d39fdfa
--- /dev/null
+++ b/create_list.sh
@@ -0,0 +1,119 @@
+#!/bin/bash
+# Creates a list from application/json files
+# Kaotisk Hund 2024
+#
+# Convention:
+# {
+# "started_on":...,
+# "duration":...,
+# "list":[
+# {
+# "index":...,
+# "hash":...,
+# "duration":...,
+# "starts_on":...
+# },
+# ...
+# ]
+# }
+#
+# Just for convenience, we will simply make up a list of files found in hashes
+# we will be searching for files smaller than 4096 bytes. An extra check with
+# `file` to find 'JSON text data'.
+json_file_list="$(mktemp)"
+find hashes -type f -size -4096 | sort | while read filepath
+do
+ file ${filepath} | grep 'JSON text data' > /dev/null 2>&1
+ if [ $? -eq 0 ]
+ then
+ if [ "$(cat ${filepath} | jq -r '.type')" != "show" ]
+ then
+ echo "Not a show: ${filepath}"
+ continue
+ fi
+ if [ "$(cat ${filepath} | jq -r '.duration')" == "null" ]
+ then
+ echo "No duration field: ${filepath}"
+ exit 1
+ fi
+ if [ ! $(cat ${filepath} | jq -r '.created_on') -gt 0 ]
+ then
+ echo "No created_on field: ${filepath}"
+ exit 1
+ fi
+ if [ ! $(cat ${filepath} | jq -r '.published_on') -gt 0 ]
+ then
+ echo "No published_on field: ${filepath}"
+ exit 1
+ fi
+ if [ ! -n "$(cat ${filepath} | jq -r '.hash')" ]
+ then
+ echo "No hash field: ${filepath}"
+ exit 1
+ fi
+ if [ ! -n "$(cat ${filepath} | jq -r '.filename')" ]
+ then
+ echo "No file_extension field: ${filepath}"
+ exit 1
+ fi
+ if [ ! -n "$(cat ${filepath} | jq -r '.file_extension')" ]
+ then
+ echo "No file_extension field: ${filepath}"
+ exit 1
+ fi
+ if [ ! -n "$(cat ${filepath} | jq -r '.mimetype')" ]
+ then
+ echo "No mimetype field: ${filepath}"
+ exit 1
+ fi
+
+ # Optional so we don't check for those
+ # cat ${filepath} | jq -r '.artist'
+ # cat ${filepath} | jq -r '.title'
+ echo ${filepath} >> ${json_file_list}
+ fi
+done
+
+cat ${json_file_list}
+
+new_list_file="$(mktemp)"
+index=0
+starts_on=0
+total_duration="$(mktemp)"
+echo -n 0 > ${total_duration}
+( echo '{'
+echo '"type":"list",'
+echo '"started_on":"'$(( $(date -u +%s) - 1800 ))000'",'
+echo '"list":['
+cat ${json_file_list} | while read cur_file
+do
+ if [ -f "${cur_file}" ] && [ -n "${cur_file}" ]
+ then
+ echo '//'${cur_file}'//' >&2
+ cat ${cur_file} | jq >&2
+ if [ ${index} -gt 0 ]
+ then
+ echo ','
+ fi
+ echo '{'
+ echo '"index":"'${index}'",'
+ hash_string="$(cat ${cur_file} | jq -r '.hash')"
+ echo '"hash":"'$(basename ${cur_file})'",'
+ duration="$(cat ${cur_file} | jq -r '.duration')"
+ echo '"duration":"'${duration}'",'
+ echo '"starts_on":"'${starts_on}'"}'
+
+ starts_on=$(( ${duration} + ${starts_on} ))
+ index=$(( ${index} + 1 ))
+ echo -n $(( $(cat ${total_duration}) + ${duration} )) > ${total_duration}
+ fi
+done
+echo '],'
+echo '"duration":"'$(cat ${total_duration})'"'
+echo '}' ) | jq -c -M > ${new_list_file}
+sha_live=$(sha512sum ${new_list_file}|cut -d ' ' -f 1)
+cat ${new_list_file} | jq
+echo '{"latest_list":"'${sha_live}'"}' > ./hashes/list
+mv ${new_list_file} ./hashes/${sha_live}
+#rm ${new_list_file}
+rm ${json_file_list}
diff --git a/create_metadata_show.sh b/create_metadata_show.sh
new file mode 100755
index 0000000..e447661
--- /dev/null
+++ b/create_metadata_show.sh
@@ -0,0 +1,82 @@
+#!/bin/bash
+# Creates metadata for a show
+# Kaotisk Hund 2024
+#
+# Output should be:
+# {
+# "artist":...,
+# "title":...,
+# "duration":...,
+# "published_on":...,
+# "created_on":...,
+# "hash":...,
+# "mimetype":...,
+# "filename":...,
+# "file-extension":...
+# }
+#
+# Accept one argument, the targeted file
+if [ ! -z $1 ] && [ -n "$1" ]
+then
+ filename="$1"
+else
+ echo "No filename given"
+ exit 1
+fi
+# Check if file exists
+if [ ! -f "${filename}" ]
+then
+ echo "File doesn't exist"
+ exit 1
+fi
+# ❯ file QmNcDk7jn8pGtt3UWZnKDPSGmGwFNkBUyXHCD5H9bdZ2Le.ogx
+# QmNcDk7jn8pGtt3UWZnKDPSGmGwFNkBUyXHCD5H9bdZ2Le.ogx: Ogg data, Vorbis audio, stereo, 44100 Hz, ~192000 bps, created by: Xiph.Org libVorbis I
+#
+file ${filename} | grep 'Ogg data, Vorbis audio,' 1>/dev/null 2>&1
+if [ $? -eq 0 ]
+then
+ mimetype='audio/ogg'
+else
+ echo "Unknown file type"
+ exit 1
+fi
+hashstring="$(sha512sum ${filename}|cut -d ' ' -f 1)"
+if [ -f "./hashes/${hashstring}" ]
+then
+ echo "File already exists"
+ exit 1
+fi
+
+artist="$(ogginfo ${filename} | grep -i ARTIST | cut -d '=' -f 2-)"
+title="$(ogginfo ${filename} | grep -i TITLE | cut -d '=' -f 2-)"
+duration="$(ogginfo ${filename} | grep 'Playback length' | cut -d ':' -f 2-)"
+published_on="$(date -u +%s)000"
+dmins="$(echo ${duration}|cut -d 'm' -f 1)"
+dsecs="$(echo ${duration}|cut -d ':' -f 2|cut -d '.' -f 1)"
+dmil="$(echo ${duration}|cut -d ':' -f 2|cut -d '.' -f 2|cut -d 's' -f 1)"
+duration="$(echo -n $(($(( ${dmins} * 60)) + ${dsecs}))${dmil})"
+created_on="$(echo -n $((${published_on} - ${duration})))"
+file_extension="$(echo -n $filename|rev|cut -d '.' -f 1|rev)"
+
+temp_file="$(mktemp)"
+
+(
+cat >&1 <<EOF
+{
+ "type":"show",
+ "artist":"${artist}",
+ "title":"${title}",
+ "published_on":"${published_on}",
+ "created_on":"${created_on}",
+ "duration":"${duration}",
+ "hash":"${hashstring}",
+ "mimetype":"${mimetype}",
+ "file_extension":"${file_extension}",
+ "filename":"${filename}"
+}
+EOF
+)| jq -c -M > ${temp_file}
+show_hash="$(sha512sum ${temp_file} | cut -d ' ' -f 1)"
+
+cp ${filename} ./hashes/${hashstring}
+mv ${temp_file} ./hashes/${show_hash}
diff --git a/create_static_list.sh b/create_static_list.sh
new file mode 100755
index 0000000..059eabd
--- /dev/null
+++ b/create_static_list.sh
@@ -0,0 +1,59 @@
+#!/bin/bash
+
+cat >&1 <<EOF
+<!DOCTYPE>
+<html>
+ <head>
+ <title>Title</title>
+ </head>
+ <body>
+ <h1>Radio index</h1>
+EOF
+
+echo '<h2>Lists</h2><ul>'
+find hashes -type f | while read filepath
+do
+ hash_string="$(basename ${filepath})"
+ file ${filepath} | grep 'JSON text data' > /dev/null 2>&1
+ if [ $? -eq 0 ]
+ then
+ if [ "$(cat ${filepath} | jq -r '.type')" == "list" ]
+ then
+ echo '<li><a href="http://z.kaotisk-hund.com:8010/v0/application/json/'${hash_string}'">'${hash_string}'</a></li>'
+ fi
+ fi
+done
+echo '</ul>'
+
+echo '<h2>Shows</h2><ul>'
+find hashes -type f | while read filepath
+do
+ hash_string="$(basename ${filepath})"
+ file ${filepath} | grep 'JSON text data' > /dev/null 2>&1
+ if [ $? -eq 0 ]
+ then
+ if [ "$(cat ${filepath} | jq -r '.type')" == "show" ]
+ then
+ echo '<li><a href="http://z.kaotisk-hund.com:8010/v0/application/json/'${hash_string}'">'${hash_string}'</a></li>'
+ fi
+ continue
+ fi
+done
+echo '</ul>'
+
+echo '<h2>audio/ogg</h2><ul>'
+find hashes -type f | while read filepath
+do
+ hash_string="$(basename ${filepath})"
+ file ${filepath} | grep 'Ogg data, Vorbis audio' > /dev/null 2>&1
+ if [ $? -eq 0 ]
+ then
+ echo '<li><a href="http://z.kaotisk-hund.com:8010/v0/audio/ogg/'${hash_string}'">'${hash_string}'</a></li>'
+ continue
+ fi
+done
+echo '</ul>'
+cat >&1 <<EOF
+ </body>
+</html>
+EOF
diff --git a/server/index.js b/server/index.js
new file mode 100755
index 0000000..4f2e8b8
--- /dev/null
+++ b/server/index.js
@@ -0,0 +1,171 @@
+const http = require("node:http");
+const fs = require("node:fs");
+
+// const welcomeMessage = require("./routes/default/index.js");
+// const getNodeInfo = require('./routes/getNodeInfo/index.js');
+// const getPeers = require('./routes/getPeers/index.js');
+// const getZblock = require('./routes/getZblock/index.js');
+// const getZlatest = require('./routes/getZLatest/index.js');
+// const getSblock = require('./routes/getSBlock/index.js');
+// const getSlatest = require('./routes/getSLatest/index.js');
+
+// const akLogMessage = require('./lib/akLogMessage');
+function akLogMessage(type, message)
+{
+ console.log(type+": "+message);
+}
+
+const serverOptions = { keepAliveTimeout: 60000 };
+
+function printRequest(req)
+{
+ console.log(req.connection.remoteAddress);
+ console.log(req.headers);
+ console.log(req.method, req.url);
+ console.log('HTTP/' + req.httpVersion);
+}
+
+function respondError(res, errorMessage)
+{
+ res.writeHead(404, { 'Content-Type': 'application/json'});
+ res.end(JSON.stringify({
+ error: errorMessage
+ }));
+}
+
+function respondJSON(res, hash)
+{
+ test = /[0-9a-f]{128}/
+ if (test.test(hash))
+ {
+ res.writeHead(200, { 'Content-Type': 'application/json'});
+ res.end(JSON.stringify(JSON.parse(fs.readFileSync('../hashes/' + hash))));
+ } else {
+ respondError(res, 'Not hash');
+ }
+}
+
+function testRootRoute(req, res)
+{
+ notImplemented(req, res);
+}
+
+function testRoute(req, res)
+{
+ respondError(res, "Mpla mpla");
+}
+
+function applicationRoutes(req, res, args)
+{
+ if (args[4] === 'undefined')
+ {
+ respondError(res, 'Bye');
+ return;
+ }
+ switch(args[3])
+ {
+ case 'json': respondJSON(res, args[4]); break;
+ default: notImplemented(req, res);
+ }
+}
+
+// Audio returns
+function respondOGG(res, hash)
+{
+ res.writeHead(200, { 'Content-Type': 'audio/ogg'});
+ test = /[0-9a-f]{128}/
+ console.log(test.test(hash))
+ res.end(fs.readFileSync('../hashes/' + hash));
+}
+
+function audioRoutes(req, res, args)
+{
+ if (args.length < 4 || args[4] === '')
+ {
+ respondError(res, 'No hash');
+ return;
+ }
+ if (args.length < 3 || args[3] === '')
+ {
+ respondError(res, 'No filetype/extension');
+ return;
+ }
+ switch(args[3])
+ {
+ case 'ogg': respondOGG(res, args[4]); break;
+ default: notImplemented(req, res);
+ }
+}
+
+function getLatestList(req, res)
+{
+ res.writeHead(200, { 'Content-Type': 'application/json'});
+ res.end(JSON.stringify(JSON.parse(fs.readFileSync('../hashes/list'))));
+}
+
+function getRoutes(req, res)
+{
+ var args = req.url.split('/');
+ if (args[1] === 'v0' && args.length > 2 && args[2] !== ""){
+ switch(args[2])
+ {
+ case 'test': testRoute(req, res); break;
+ case 'list': getLatestList(req, res); break;
+ case 'application': applicationRoutes(req, res, args); break;
+ case 'audio': audioRoutes(req, res, args); break;
+ default: notImplemented(req, res);
+ }
+ }
+ else {
+ notImplemented(req, res);
+ }
+}
+
+function postRoutes(req, res)
+{
+ switch(req.url)
+ {
+ default: notImplemented(req, res);
+ }
+}
+
+function notImplemented(req, res)
+{
+ res.writeHead(404, { 'Content-Type': 'application/json'});
+ res.end(JSON.stringify({
+ url: req.url,
+ error: 'not implemented'
+ }));
+}
+
+function processMethod(req, res)
+{
+ switch(req.method)
+ {
+ case 'GET': getRoutes(req, res); break;
+ case 'POST': postRoutes(req, res); break;
+ default: notImplemented(req, res);
+ }
+}
+
+function checkIfAllowedIP(address)
+{
+ return address.startsWith('fc') ? true : false;
+}
+
+function requestParser(req, res)
+{
+ printRequest(req);
+ akLogMessage('INFO', `Incoming from [${req.connection.remoteAddress}]:${req.socket._peername.port} @ ${req.headers.host}${req.url}`);
+ if (checkIfAllowedIP(req.connection.remoteAddress)){
+ res.setHeader('Access-Control-Allow-Origin', '*');
+ processMethod(req, res);
+ }
+ else {
+ res.end();
+ }
+}
+
+const server = http.createServer(serverOptions, requestParser);
+
+server.listen(8010);