From 4f3941785243c295072a56e687b9b5954a912ec8 Mon Sep 17 00:00:00 2001 From: Kristy Fournier Date: Wed, 21 Jan 2026 19:27:49 -0500 Subject: [PATCH] added hashing, updated parts of readme to explain env file also added some tabby stuff, but its not done yet --- Client/index.html | 12 ++++----- Client/scripts.js | 60 +++++++++++++++++++++++++++++++++------------ Server/webbyBits.py | 4 +-- readme.md | 25 ++++++++++--------- wishlist.md | 2 +- 5 files changed, 65 insertions(+), 38 deletions(-) diff --git a/Client/index.html b/Client/index.html index f89e974..975e2de 100644 --- a/Client/index.html +++ b/Client/index.html @@ -59,7 +59,6 @@ changes visibility with JS-->

Opposite of light mode

-

Server IP:

IP of the device running the song server

@@ -107,14 +106,13 @@ changes visibility with JS-->
- settings
- Playlist - Play pause - Skip - Search - + Playlist + Play pause + Skip + Search
+ settings diff --git a/Client/scripts.js b/Client/scripts.js index 52b8dda..e3f4e07 100644 --- a/Client/scripts.js +++ b/Client/scripts.js @@ -5,7 +5,12 @@ let adminPass = ""; const ERR_NO_ADMIN = "401"; // gonna use this later to refactor const VALID_FILE_EXT = ["mp3","flac","wav"]; +const params = new URLSearchParams(location.search); + let darkmodetemp = getCookie("darkmode"); +darkmodetemp = params.get("darkmode") +if(darkmodetemp === "") { +} if (darkmodetemp === "true") { // i know this is gonna cause weird blinking // maybe the dark mode function should be loaded before any content, would that work? @@ -29,6 +34,7 @@ async function getFromServer(bodyInfo, source="",password=adminPass) { // the currently set password is always included in every request bodyInfo["password"] = password; } + // console.log(bodyInfo); const response = await fetch("http://"+ip+"/"+source, { method: "POST", body: JSON.stringify(bodyInfo), @@ -327,18 +333,20 @@ async function submitSong(songid) { } } function checkWhatSongWasClicked(e) { - itemId = e.srcElement.id; - if ((itemId.length-itemId.lastIndexOf("image") == 5) && itemId.lastIndexOf("image")!=-1) { - itemId = itemId.slice(0,-6) + if(e.type == "click" || e.key == "Enter") { + itemId = e.srcElement.id; + if ((itemId.length-itemId.lastIndexOf("image") == 5) && itemId.lastIndexOf("image")!=-1) { + itemId = itemId.slice(0,-6) + } + //i feel like later kristy won't apreciate this + //one of my files was "file.MP3" so it didn't work + //windows be like + let filenameSep = itemId.split('.') + + if (VALID_FILE_EXT.includes(filenameSep[filenameSep.length-1].toLowerCase())) { + submitSong(itemId); + } } - //i feel like later kristy won't apreciate this - //one of my files was "file.MP3" so it didn't work - //windows be like - let filenameSep = itemId.split('.') - - if (VALID_FILE_EXT.includes(filenameSep[filenameSep.length-1].toLowerCase())) { - submitSong(itemId); - } } function toggleDark(e) { @@ -355,11 +363,31 @@ function toggleDark(e) { } -function adminPassEnter(e) { +async function sha256(message) { + // Encode the message as UTF-8 + const msgBuffer = new TextEncoder().encode(message); + + // Hash the message + const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer); + + // Convert ArrayBuffer to hex string + const hashArray = Array.from(new Uint8Array(hashBuffer)); + const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); + + return hashHex; +} + +async function adminPassEnter(e) { if (e.key == "Enter") { - e.preventDefault(); - adminPass=document.getElementById("adminpasswordbox").value - alertText("Admin Password Updated") + e.preventDefault(); + let enteredpass = document.getElementById("adminpasswordbox").value; + if(enteredpass === "") { + adminPass = ""; // an empty pass is technically meant to represent not having one + // this isn't stritly necesarry but i dont wanna break anything that might depend on this being true + } else { + adminPass= await sha256(document.getElementById("adminpasswordbox").value); + } + alertText("Admin Password Updated"); } } @@ -424,6 +452,7 @@ document.getElementById("adminpasswordbox").addEventListener('keydown',function( document.getElementById("admincheckholder").addEventListener('click',function(e){submitPerms(e)}); document.getElementById("partymode-button").addEventListener('click',function(){controlButton("pm")}) //sets the fact that clicking a song needs to return its id to the function to find it +document.getElementById("songlist").addEventListener('keydown', function(e){checkWhatSongWasClicked(e)}); document.getElementById("songlist").addEventListener('click', function(e){checkWhatSongWasClicked(e)}); //makes the controls look mostly normal on all screens, best solution i could find, idk man @@ -435,7 +464,6 @@ document.getElementById("darkmode-button").addEventListener('click',function(){t //using this allows the creator of the link for, a qr code for example, to set the ip before distributing the code, and it would all work smoothly //example (http://192.168.1.100:8000/?ip=192.168.1.100:19054 sets the ip to the same host at the default port) //the port must be set manually using this method, but only has to be done once for the url that ends up being shared -let params = new URLSearchParams(location.search); //tries the url first, then the cookie, then the default ip = params.get("ip") diff --git a/Server/webbyBits.py b/Server/webbyBits.py index 7e684b6..236f903 100644 --- a/Server/webbyBits.py +++ b/Server/webbyBits.py @@ -2,7 +2,7 @@ from flask import Flask from flask import request from flask_cors import CORS import sqlite3 as sql -import vlc,threading,time,random,argparse,dotenv,os +import vlc,threading,time,random,argparse,dotenv,os,hashlib # Argparse Stuff parser=argparse.ArgumentParser(description="Options for the Webby Bits") # parser.add_argument('-p','--port',help="Port to host on, not the same as the web (client) port",default='19054') @@ -14,7 +14,7 @@ portTheUserPicked=os.getenv("SERVER_PORT") # This is not great design, and the whole "returning string codes" thing is something to add to the todo list # I mean returning 200 when no return is necesary i think is fine but we'll see ERR_NO_ADMIN = "401" -ADMIN_PASS = args.admin +ADMIN_PASS = hashlib.sha256(bytes(args.admin,'utf-8')).hexdigest() if not(ADMIN_PASS): ADMIN_PASS = None # True = everyone, False = admin only. Change in client while in use. diff --git a/readme.md b/readme.md index e894e44..76a6477 100644 --- a/readme.md +++ b/readme.md @@ -10,6 +10,7 @@ The main advantage compared to doing something similar using Spotify is that you ### Client Setup: The client is a web application that can be hosted on any server, it need not be the same device running the music player. * If the app is being setup for a large group, you can distribute the url (via QR code, for example) with `?ip=YOURSERVERHOSTNAME:19054` set as an attribute after the url. +* You can also add `?darkmode=(true/false)` to set the default colour scheme, but this will be overwritten by the users saved choice in the cookie if they change it themselves ### Server Setup: **Pre-setup:** If you want the songs to have art associated with them, it is all hosted on and retrieved from LastFM, and you will need to sign up for a developer app, and put your key in the database generator \ \ @@ -22,14 +23,17 @@ webbyBits.py ``` 1. Place mp3 files in the `sound/` folder -2. Open `databaseGenerator.py` and put your LastFM API key in at the top or at runtime using `-k APIKey` (*optional*) -3. Run `databaseGenerator.py` +2. Rename `example.env` to `.env` and... + - Set the location where your audio files are (Default: `./sound/`) + - Set the LastFM API key (Optional) + - Change the port of the webbybits server ("Default ) +3. Run `databaseGenerator.py` (Will try to use LastFM API key) * *The `databaseGenerator.py` will index all mp3 files, and save the information to `songDatabase.db`* * *If getting images, this process may take a long time with a large amount of mp3 files* 4. Run `webbyBits.py` - * *The port can be customized at runtime using* `-p portNumber` *as an atribute* + * *The port can be customized by editing the `.env` file* * *You can add an admin password at runtime with* `-a AdminPass` *as an atribute* - * ***NOTE: Do not reuse ANY password for this, it is 100% unsecure. The best option is just a random string you write down once*** + * ***NOTE: Do not reuse ANY password for this, it is hashed but 100% unsecure. The best option is just a random string you write down once*** * This is intended for protecting certain features for small closed events, not for public security You can now connect with the client and use the app as normal. \ @@ -42,25 +46,21 @@ These are specific details on each section of the app, and how to use them - `sound/` contains all mp3 files by default - `databaseGenerator.py` scans through mp3 files and gets information about them - `Filename, Title, Artist, Art, Length` are all saved - - *If the title and artist are not in the mp3 metadata, it looks for a format of* `TITLE_ARTIST.mp3` *then of* `ARTIST - TITLE.mp3` *and otherwise defaults to the file name as the title, and no artist* + - *If the title and artist are not in the file metadata, it looks for a format of* `TITLE_ARTIST.mp3` *then of* `ARTIST - TITLE.mp3` *and otherwise defaults to the file name as the title, and no artist* - Art is retrieved from LastFM - Running with `--mode (update/new)` either updates the current database and adds new songs/removes deleted songs, or recreates the entire database (update is default, and is faster in art mode) - Running with `--art (True/False)` retrieves art from LastFM or doesn't (True is default) - *Can only generate one song / 0.25 seconds, to avoid pinging the LastFM server too much* - - Running with `--apikey (KEYhere)` sets the LastFM key for that run - - If this is set to an empty string (Default) the app runs in non-art mode - - Running with `--directory (directoryOfmp3s)` allows for sound files to be in a different place - - Default `"./sound/"` - - _This setting might be kinda iffy on Linux. You're on Linux just go and edit it if you have issues_ + - Directory to index for music files can be set in the `.env` file - `songDatabase.db` stores all the information about each song in a SQLite database with tables `songs` and `meta` - `webbyBits.py` imports the database, runs all music playing, and accepts all commands from clients - Searches return matching songs - Accepts Play-Pause and Skip commands - Uses port 19054 by default - - `--port (port)` changes the port for that run + - Can be changed in the `.env` file - The default port can be changed in the file - Running with `--admin (admin password)` sets an admin password for moderation on the client - - ***Note: Do not reuse a password, consider this like making whatever this string is public, no security is guaranteed*** + - ***Note: Do not reuse a password, the password is hashed before being sent over the network, but I still wouldn't bet my house on it, no security is guaranteed*** - Anyone who knows the admin password can enter it on the client and change the abilities of any non-admin users (for example to limit skipping) - The total set of features that can be restricted is - Skip track @@ -80,6 +80,7 @@ From left to right: - *No "previous" button is a design decision (It's a feature not a bug)* - The search button opens the search screen (pictured) - The settings button (top right) opens the settings menu + - Dark mode sets a dark mode and stores a cookie to keep you in dark mode after refreshing - Server IP allows you to change the ip that the site connects to - Alert time changes how long error/confirmation messages are shown for (Default 2s) - Party Mode adds new songs to the queue when the queue has only 1 song in it diff --git a/wishlist.md b/wishlist.md index 02df8c5..bd525f7 100644 --- a/wishlist.md +++ b/wishlist.md @@ -8,7 +8,7 @@ - [ ] Verify all if-else sequences are correct and not redundant - [ ] Security Updates - [x] `.env` file for the api keys and other runtime info to be set, rather than in the `.py` files - - [ ] Hashing rather than plaintext sending passwords (that way at least the password text itself isn't transmitted over the network) + - [x] Hashing rather than plaintext sending passwords (that way at least the password text itself isn't transmitted over the network) - [ ] Actually use SSL, for posting (CORS seems like an issue) - [ ] Accessibility - [ ] Better use of semantic HTML tags