diff options
-rw-r--r-- | README | 19 | ||||
-rw-r--r-- | client/index.html | 37 | ||||
-rw-r--r-- | client/js/radio_emulator.js | 188 | ||||
-rwxr-xr-x | create_list.sh | 119 | ||||
-rwxr-xr-x | create_metadata_show.sh | 82 | ||||
-rwxr-xr-x | create_static_list.sh | 59 | ||||
-rwxr-xr-x | server/index.js | 171 |
7 files changed, 675 insertions, 0 deletions
@@ -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); |