From 36c7aa081bdea3e6c8af5750791537b7cd2126be Mon Sep 17 00:00:00 2001 From: Stephen Barriball Date: Mon, 25 Aug 2025 19:56:30 +0100 Subject: [PATCH] Initial created by ChatGPT --- AquaCubeIT.Service.NetFloppy/Dockerfile | 13 + AquaCubeIT.Service.NetFloppy/Program.cs | 319 ++++++++++++++++++ AquaCubeIT.Service.NetFloppy/appsettings.json | 17 + .../docker-compose.yml | 31 ++ Certificates/cert-gen.sh | 8 + Certificates/certs/README-certs.md | 14 + Certificates/certs/gen-certs.sh | 77 +++++ Documentation/Overview.pdf | 93 +++++ Documentation/README.md | 30 ++ Documentation/transcript.txt | 5 + Frontend/FF.CFG | 11 + 11 files changed, 618 insertions(+) create mode 100644 AquaCubeIT.Service.NetFloppy/Dockerfile create mode 100644 AquaCubeIT.Service.NetFloppy/Program.cs create mode 100644 AquaCubeIT.Service.NetFloppy/appsettings.json create mode 100644 AquaCubeIT.Service.NetFloppy/docker-compose.yml create mode 100644 Certificates/cert-gen.sh create mode 100644 Certificates/certs/README-certs.md create mode 100644 Certificates/certs/gen-certs.sh create mode 100644 Documentation/Overview.pdf create mode 100644 Documentation/README.md create mode 100644 Documentation/transcript.txt create mode 100644 Frontend/FF.CFG diff --git a/AquaCubeIT.Service.NetFloppy/Dockerfile b/AquaCubeIT.Service.NetFloppy/Dockerfile new file mode 100644 index 0000000..d61ca02 --- /dev/null +++ b/AquaCubeIT.Service.NetFloppy/Dockerfile @@ -0,0 +1,13 @@ +# Dockerfile for GotekBackend (ASP.NET Core 8) +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /src +COPY . ./ +RUN dotnet publish -c Release -o /out + +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime +WORKDIR /app +COPY --from=build /out ./ +RUN mkdir -p /app/Images /app/Uploaded +ENV ASPNETCORE_URLS="http://0.0.0.0:8080;https://0.0.0.0:8443" +EXPOSE 8080 8443 +ENTRYPOINT ["dotnet", "GotekBackend.dll"] diff --git a/AquaCubeIT.Service.NetFloppy/Program.cs b/AquaCubeIT.Service.NetFloppy/Program.cs new file mode 100644 index 0000000..d92e3d2 --- /dev/null +++ b/AquaCubeIT.Service.NetFloppy/Program.cs @@ -0,0 +1,319 @@ +// Program.cs — ASP.NET Core 8 backend: builds FAT12 on-the-fly (GET) and extracts on POST +using System.Buffers.Binary; +using System.Text; +using System.Linq; +using Microsoft.AspNetCore.Mvc; +using System.Security.Cryptography.X509Certificates; + +var builder = WebApplication.CreateBuilder(args); + +// Require client certificates (mTLS). Configure server cert via appsettings.json or env variables. +builder.WebHost.ConfigureKestrel(k => +{ + k.ConfigureHttpsDefaults(o => + { + o.ClientCertificateMode = Microsoft.AspNetCore.Server.Kestrel.Https.ClientCertificateMode.RequireCertificate; + }); +}); + +var app = builder.Build(); + +// Basic client-cert gate (replace with your CA/allow-list validation) +app.Use(async (ctx, next) => +{ + if (ctx.Connection.ClientCertificate is null) + { + ctx.Response.StatusCode = 401; + await ctx.Response.WriteAsync("client certificate required"); + return; + } + await next(); +}); + +var root = app.Environment.ContentRootPath; +var imagesDir = Path.Combine(root, "Images"); // store ONLY floppy images here (per-tag) +var uploadsDir = Path.Combine(root, "Uploaded"); // optional archive of posted volumes +Directory.CreateDirectory(imagesDir); +Directory.CreateDirectory(uploadsDir); + +// Load FF.CFG from disk (mounted file). Fallback to a sane default if missing. +string FfCfgText = LoadFfCfg(Path.Combine(root, "FF.CFG")) ?? + "host = acorn\r\n" + + "interface = ibmpc\r\n" + + "index-suppression = no\r\n" + + "sound = on\r\n" + + "sound-volume = 19\r\n" + + "display-type = none\r\n"; + +// GET /api/volume?id= -> build tiny FAT12 in memory containing: [, FF.CFG] +app.MapGet("/api/volume", async ([FromQuery] string? id) => +{ + if (!IsHex64(id)) return Results.BadRequest("id required (hex, up to 16 chars)"); + + var imgPath = FindImage(imagesDir, id!); + if (imgPath is null) return Results.NotFound($"No image for id {id}"); + + byte[] payload = await File.ReadAllBytesAsync(imgPath); + string payloadFileName = Path.GetFileName(imgPath); + + byte[] fat = BuildFat12TwoFiles(payload, payloadFileName, Encoding.ASCII.GetBytes(FfCfgText)); + + return Results.File(new MemoryStream(fat), "application/octet-stream", + fileDownloadName: $"{id!.ToLowerInvariant()}.fat.img", enableRangeProcessing: true); +}); + +// POST /api/volume?id= -> receive modified FAT; extract image file; save back to Images/. +app.MapPost("/api/volume", async ([FromQuery] string? id, HttpRequest req) => +{ + if (!IsHex64(id)) return Results.BadRequest("id required (hex, up to 16 chars)"); + + var ts = DateTime.UtcNow.ToString("yyyyMMdd-HHmmss"); + var uploadedPath = Path.Combine(uploadsDir, $"{id!.ToLowerInvariant()}-{ts}.fat.img"); + await using (var fs = File.Create(uploadedPath)) + await req.Body.CopyToAsync(fs); + + try + { + await using var s = File.OpenRead(uploadedPath); + var (name, data) = ExtractFirstNonCfgRootFile(s); + if (name is null || data is null) + return Results.BadRequest("no floppy image found in volume"); + + var ext = Path.GetExtension(name); + if (string.IsNullOrEmpty(ext)) ext = ".img"; + var outPath = Path.Combine(imagesDir, $"{id.ToLowerInvariant()}{ext.ToLowerInvariant()}"); + await File.WriteAllBytesAsync(outPath, data); + return Results.Ok(new { ok = true, saved = outPath }); + } + catch (Exception ex) + { + return Results.BadRequest(new { error = "extract failed", detail = ex.Message }); + } +}); + +app.Run(); + +// ---------- Helpers ---------- + +static string? LoadFfCfg(string path) +{ + try + { + if (File.Exists(path)) return File.ReadAllText(path); + return null; + } + catch { return null; } +} + +static bool IsHex64(string? s) +{ + if (string.IsNullOrWhiteSpace(s) || s.Length > 16) return false; + foreach (var c in s) if (!Uri.IsHexDigit(c)) return false; + return true; +} + +static string? FindImage(string dir, string id) +{ + var stem = id.ToLowerInvariant(); + var exts = new[] { ".adf", ".img", ".st", ".dsk", ".ima", ".adl" }; + foreach (var ext in exts) + { + var p = Path.Combine(dir, stem + ext); + if (File.Exists(p)) return p; + } + var any = Directory.EnumerateFiles(dir) + .FirstOrDefault(f => Path.GetFileNameWithoutExtension(f) + .Equals(stem, StringComparison.OrdinalIgnoreCase)); + return any; +} + +// Build tiny FAT12 with two root files: payloadName + FF.CFG +static byte[] BuildFat12TwoFiles(byte[] payload, string payloadName, byte[] ffCfg) +{ + const int BPS = 512; + const int SPC = 1; + const int RSVD = 1; + const int FATS = 2; + const int ROOT_ENTS = 32; + const byte MEDIA = 0xF8; + + int clus = BPS * SPC; + int clPayload = Math.Max(1, (payload.Length + clus - 1) / clus); + int clCfg = Math.Max(1, (ffCfg.Length + clus - 1) / clus); + int clTotal = clPayload + clCfg; + + int fatEntries = clTotal + 2; + int fatBytes = (fatEntries * 3 + 1) / 2; + int spFAT = (fatBytes + BPS - 1) / BPS; + + int rootSecs = ((ROOT_ENTS * 32) + (BPS - 1)) / BPS; + int dataSecs = clTotal * SPC; + int totalSecs = RSVD + FATS * spFAT + rootSecs + dataSecs; + if (totalSecs >= 0x10000) throw new InvalidOperationException("volume too large"); + + using var ms = new MemoryStream(totalSecs * BPS); + var bw = new BinaryWriter(ms, Encoding.ASCII, leaveOpen:true); + + byte[] bs = new byte[512]; + bs[0]=0xEB; bs[1]=0x3C; bs[2]=0x90; + Encoding.ASCII.GetBytes("MSDOS5.0").CopyTo(bs,3); + WriteLE16(bs,11,(ushort)BPS); + bs[13]=(byte)SPC; + WriteLE16(bs,14,(ushort)RSVD); + bs[16]=(byte)FATS; + WriteLE16(bs,17,(ushort)ROOT_ENTS); + WriteLE16(bs,19,(ushort)totalSecs); + bs[21]=MEDIA; + WriteLE16(bs,22,(ushort)spFAT); + WriteLE16(bs,24,32); WriteLE16(bs,26,64); + WriteLE32(bs,28,0); + bs[36]=0x80; bs[38]=0x29; WriteLE32(bs,39,0x1234ABCD); + Encoding.ASCII.GetBytes("GOTEK VOL ").CopyTo(bs,43); + Encoding.ASCII.GetBytes("FAT12 ").CopyTo(bs,54); + bs[510]=0x55; bs[511]=0xAA; + bw.Write(bs); + + int fatAligned = spFAT * BPS; + byte[] fat = new byte[fatAligned]; + fat[0]=MEDIA; fat[1]=0xFF; fat[2]=0xFF; + + ushort firstPayload=2; + ushort firstCfg=(ushort)(firstPayload+clPayload); + + for(int i=0;i>8); } + static void WriteLE32(byte[] b,int o,uint v){ b[o]=(byte)v; b[o+1]=(byte)(v>>8); b[o+2]=(byte)(v>>16); b[o+3]=(byte)(v>>24); } + static void SetFAT12(byte[] fat, ushort cluster, ushort value){ + int idx = (cluster*3)/2; + if((cluster&1)!=0){ + ushort curr = (ushort)(fat[idx] | (fat[idx+1]<<8)); + curr = (ushort)((curr & 0x00FF) | ((value & 0x0FFF) << 8)); + fat[idx]=(byte)(curr&0xFF); fat[idx+1]=(byte)(curr>>8); + } else { + ushort curr = (ushort)(fat[idx] | (fat[idx+1]<<8)); + curr = (ushort)((curr & 0xF000) | (value & 0x0FFF)); + fat[idx]=(byte)(curr&0xFF); fat[idx+1]=(byte)(curr>>8); + } + } + static byte[] To83(string name){ + name=name.Replace('\\','/'); if(name.Contains('/')) name=name[(name.LastIndexOf('/')+1)..]; + var dot=name.LastIndexOf('.'); + var basePart=dot>0?name[..dot]:name; + var ext=dot>0?name[(dot+1)..]:""; + basePart=basePart.ToUpperInvariant(); + ext=ext.ToUpperInvariant(); + if(string.IsNullOrEmpty(basePart)) basePart="FILE"; + Span out11 = stackalloc byte[11]; out11.Fill((byte)' '); + Encoding.ASCII.GetBytes(basePart.Length>8?basePart[..8]:basePart).CopyTo(out11); + Encoding.ASCII.GetBytes(ext.Length>3?ext[..3]:ext).CopyTo(out11[8..]); + return out11.ToArray(); + } + static void WriteData(BinaryWriter bw, byte[] data, int clusters, int bps){ + int written=0; byte[] pad=new byte[bps]; + for(int i=0;i0) bw.Write(data, written, take); + if(take bpb = stackalloc byte[512]; + if (s.Read(bpb) != 512) throw new InvalidOperationException("short BPB"); + ushort bps = BinaryPrimitives.ReadUInt16LittleEndian(bpb.Slice(11, 2)); + byte spc = bpb[13]; + ushort rsv = BinaryPrimitives.ReadUInt16LittleEndian(bpb.Slice(14, 2)); + byte fats = bpb[16]; + ushort rootEnts = BinaryPrimitives.ReadUInt16LittleEndian(bpb.Slice(17, 2)); + ushort spFat = BinaryPrimitives.ReadUInt16LittleEndian(bpb.Slice(22, 2)); + if (bps != 512) throw new InvalidOperationException("bps!=512 unsupported"); + + uint rootSecs = (uint)((rootEnts * 32 + (bps - 1)) / bps); + uint firstFatSec = rsv; + uint rootDirSec = rsv + fats * spFat; + uint firstDataSec = rootDirSec + rootSecs; + + s.Position = rootDirSec * 512; + byte[] root = new byte[rootSecs * 512]; + if (s.Read(root, 0, root.Length) != root.Length) throw new InvalidOperationException("short root"); + + for (int i = 0; i < root.Length; i += 32) + { + byte first = root[i]; + if (first == 0x00) break; + if (first == 0xE5) continue; + byte attr = root[i + 11]; + if ((attr & 0x08) != 0) continue; // vol label + if ((attr & 0x10) != 0) continue; // dir + + string name = Encoding.ASCII.GetString(root, i, 8).TrimEnd(' '); + string ext = Encoding.ASCII.GetString(root, i + 8, 3).TrimEnd(' '); + string full = ext.Length > 0 ? $"{name}.{ext}" : name; + if (string.Equals(full, "FF.CFG", StringComparison.OrdinalIgnoreCase)) continue; + + ushort firstClus = BinaryPrimitives.ReadUInt16LittleEndian(root.AsSpan(i + 26, 2)); + uint size = BinaryPrimitives.ReadUInt32LittleEndian(root.AsSpan(i + 28, 4)); + + s.Position = firstFatSec * 512; + byte[] fat = new byte[spFat * 512]; + if (s.Read(fat, 0, fat.Length) != fat.Length) throw new InvalidOperationException("short FAT"); + + // FAT12 cluster walk + List clusters = new(); + uint cl = firstClus; + while (cl >= 2 && cl < 0xFF8) + { + clusters.Add(cl); + uint idx = (uint)((cl * 3) / 2); + ushort entry = BitConverter.ToUInt16(fat, (int)idx); + uint next = (cl % 2 == 0) ? (uint)(entry & 0x0FFF) : (uint)(entry >> 4); + cl = next; + if (clusters.Count > 65536) break; + } + + using var ms = new MemoryStream((int)size); + foreach (var c in clusters) + { + ulong sector = firstDataSec + (ulong)((c - 2) * spc); + s.Position = (long)sector * 512; + byte[] tmp = new byte[spc * 512]; + int got = s.Read(tmp, 0, tmp.Length); + if (got <= 0) break; + int copy = (int)Math.Min((long)tmp.Length, (long)size - ms.Length); + ms.Write(tmp, 0, copy); + if (ms.Length >= size) break; + } + return (full, ms.ToArray()); + } + return (null, null); +} diff --git a/AquaCubeIT.Service.NetFloppy/appsettings.json b/AquaCubeIT.Service.NetFloppy/appsettings.json new file mode 100644 index 0000000..894af9a --- /dev/null +++ b/AquaCubeIT.Service.NetFloppy/appsettings.json @@ -0,0 +1,17 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "Kestrel": { + "Certificates": { + "Default": { + "Path": "/certs/server.pfx", + "Password": "${CERT_PASSWORD}" + } + } + } +} \ No newline at end of file diff --git a/AquaCubeIT.Service.NetFloppy/docker-compose.yml b/AquaCubeIT.Service.NetFloppy/docker-compose.yml new file mode 100644 index 0000000..a178500 --- /dev/null +++ b/AquaCubeIT.Service.NetFloppy/docker-compose.yml @@ -0,0 +1,31 @@ +services: + certgen: + image: alpine:3.20 + container_name: certgen + working_dir: /work + volumes: + - ./certs:/work + environment: + CERT_PASSWORD: "${CERT_PASSWORD:-changeit}" + command: ["/bin/sh","-c","apk add --no-cache openssl && chmod +x gen-certs.sh && ./gen-certs.sh"] + + gotek-backend: + build: . + container_name: gotek-backend + depends_on: + certgen: + condition: service_completed_successfully + ports: + - "8080:8080" + - "8443:8443" + environment: + CERT_PASSWORD: "${CERT_PASSWORD:-changeit}" + ASPNETCORE_URLS: "http://0.0.0.0:8080;https://0.0.0.0:8443" + ASPNETCORE_Kestrel__Certificates__Default__Path: "/certs/server.pfx" + ASPNETCORE_Kestrel__Certificates__Default__Password: "${CERT_PASSWORD:-changeit}" + volumes: + - ./Images:/app/Images + - ./Uploaded:/app/Uploaded + - ./certs:/certs:ro + - ./FF.CFG:/app/FF.CFG:ro + restart: unless-stopped diff --git a/Certificates/cert-gen.sh b/Certificates/cert-gen.sh new file mode 100644 index 0000000..fc64bd3 --- /dev/null +++ b/Certificates/cert-gen.sh @@ -0,0 +1,8 @@ +#!/bin/sh +# Generate self-signed development certificate for GotekBackend +# Usage: ./cert-gen.sh [password] +PASS=${1:-changeit} +mkdir -p certs +openssl req -x509 -nodes -days 365 -newkey rsa:2048 -subj "/CN=GotekBackendDev" -keyout certs/server.key -out certs/server.crt +openssl pkcs12 -export -out certs/server.pfx -inkey certs/server.key -in certs/server.crt -password pass:$PASS +echo "Certificate generated at certs/server.pfx with password '$PASS'" diff --git a/Certificates/certs/README-certs.md b/Certificates/certs/README-certs.md new file mode 100644 index 0000000..c4496b2 --- /dev/null +++ b/Certificates/certs/README-certs.md @@ -0,0 +1,14 @@ +# Certificate Setup (Dev) + +This backend requires **mutual TLS** (client certs). For local development, use the provided script: + +```bash +cd certs +./gen-certs.sh # creates CA, server cert (localhost), client cert, and server.pfx +``` + +Outputs: +- `ca.crt` — CA certificate. Embed this in the ESP32 firmware as `SERVER_CA_PEM`. +- `server.pfx` — for Kestrel (password: `CERT_PASSWORD`, defaults to `changeit`). +- `server.crt` / `server.key` — PEM versions. +- `client.crt` / `client.key` — sample device certificate (PEM) and `client.pfx`. diff --git a/Certificates/certs/gen-certs.sh b/Certificates/certs/gen-certs.sh new file mode 100644 index 0000000..20ece3f --- /dev/null +++ b/Certificates/certs/gen-certs.sh @@ -0,0 +1,77 @@ +#!/bin/sh +set -e + +# Defaults +: "${CERT_PASSWORD:=changeit}" +: "${CN_SERVER:=localhost}" +: "${CN_CLIENT:=esp32-device}" + +echo "==> Generating development CA, server, and client certificates (for mTLS)" +echo " Password for server.pfx: ${CERT_PASSWORD}" + +# Clean previous +rm -f ca.key ca.crt server.key server.csr server.crt server.pfx client.key client.csr client.crt client.pfx \ + client.pem client.key.pem server.pem server.key.pem server-chain.crt + +# 1) CA (self-signed) +openssl genrsa -out ca.key 4096 +openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 -subj "/CN=Dev CA" -out ca.crt + +# 2) Server key + CSR with SANs (localhost + 127.0.0.1) +cat > server.cnf < ca.cnf < server-chain.crt + +# 3) Client key/cert for device testing +openssl genrsa -out client.key 2048 +openssl req -new -key client.key -out client.csr -subj "/CN=${CN_CLIENT}" +openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt -days 825 -sha256 + +# PKCS#12 for client (optional) +openssl pkcs12 -export -out client.pfx -inkey client.key -in client.crt -certfile ca.crt -passout pass:${CERT_PASSWORD} + +# Convenience PEM for ESP32 (paste into firmware or convert as needed) +cp client.crt client.pem + +echo "==> Done." +echo "Artifacts created:" +echo " - ca.crt (CA certificate to trust on ESP32)" +echo " - server.pfx (for Kestrel, protected by CERT_PASSWORD)" +echo " - server.crt/server.key (PEM)" +echo " - client.crt/client.key (PEM) and client.pfx (optional)" diff --git a/Documentation/Overview.pdf b/Documentation/Overview.pdf new file mode 100644 index 0000000..b1705dd --- /dev/null +++ b/Documentation/Overview.pdf @@ -0,0 +1,93 @@ +%PDF-1.4 +%“Œ‹ž ReportLab Generated PDF document http://www.reportlab.com +1 0 obj +<< +/F1 2 0 R /F2 3 0 R +>> +endobj +2 0 obj +<< +/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font +>> +endobj +3 0 obj +<< +/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font +>> +endobj +4 0 obj +<< +/Contents 9 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 8 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +5 0 obj +<< +/Contents 10 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 8 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +6 0 obj +<< +/PageMode /UseNone /Pages 8 0 R /Type /Catalog +>> +endobj +7 0 obj +<< +/Author (\(anonymous\)) /CreationDate (D:20250825143255+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20250825143255+00'00') /Producer (ReportLab PDF Library - www.reportlab.com) + /Subject (\(unspecified\)) /Title (\(anonymous\)) /Trapped /False +>> +endobj +8 0 obj +<< +/Count 2 /Kids [ 4 0 R 5 0 R ] /Type /Pages +>> +endobj +9 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2431 +>> +stream +Gat=,CMt+a')eD/;4i#O"9UGf^g?E2*8BO8f1B(#dJ\[()J0oOG[@kk#K$H4pJfIk_66BA&i0'Wa/N"8WpS47+4kss$.0!MqD4ICO8LlG,A0FD4l,u'rd6A*IR5:23M1Opr:VB+4LWY0H/q`RlG.kuL,pD4,9B&PiM/pgq?Ph/k!P2A;C23*$(@p]]*QY!q;9;D:[W2e$q=Q;Zh&;ee4tmLICJB7/tB1.WdH#Ms5A#;otYF=IWB3d4DuNF#C)]jq1(9-i!_.r2Q`"SQ(%8!<0BfoMaX@1;a=d2^Pj:`cpf9k1/];^b41%G+:U5\,,l[S')pCfc4jTjOO1JULH;]c3mRi%s$>Vlhk*-rS,?C\YP91'd^(M;\.'.*dF4Y`\:OH`*(e]TmCKR%qIPK["RSq.Ih$o1d>+tY'bd.#J/2mcLpO6@iXs?jebk?*["Djrc:Qj@t^_<;Ur6$!uOQ$-'in+gGV"u3#+h96-9!V!j-(M@,h^.g1W$"JAE`UMt#8J"=_RPC5aUp$Bp@>a''@I(tn9=dsce4U7PsMg[;^BpL[?'J)NaTgYVi%&:)i'Mt_+Gp14E]b,5h`-RF4@ab&T]Hmh)j6HX!,p68JI8k/Od2`DH+%YMkF=uFP-Hj!2g&djT"LpfUZ(8MFm*oS^ZF=aGZB)q*r8)7T,/llEJ-*[MV\BWH%2.'Q#fGMEXgidg:$&eQoBcnrN9N51Zp#q!]nPKp&ibOgK=C'@%qFtk9/:0[YBft[_+A4g[aTNPlB\1+S>O_J7/fRM"8[n>h:\L7'Mec:gLfms)X_2X#Is^Q]/g84&ks!j/d_(#.p/?+G91=068CeEhDbRKpl_f0_88/I'X!j,>3pM]5#KBogf-W0T9>ROFf.Zgj-I[)AG+B*JhWGFS*_"t@>`>6X"3[dZ_2@iY9NssaFN)@0R=]6C*19OmT,LAt([jbn^%&$:lOkJT>"/AI&C)j]V7P?s.$TV>3D3<#Q4X+p^0Nns?.P<@-dmln>tb\+Ph@T%Zq6mioN(m'o+l.ceeh)=rS6,eF+;e`1)+'jSb[j5]gl]E&(1m9LAGT#3oVSfAUNc^tjc&BF>;=Z[n#7XD9bf;[tjEO(jV%.a-%u_3Uef"KGKWTqFF0#n&Xeq@'BtKUp4i\u1bjP%mer03jtUdu*HuD$u?0k6jY&ib@kmm,5m$"jE3(-uj'6&)@M>Zk7iR;.`^WN/CUdop0q(WHYnmPEd_;L0N5(\,d*`'[sm?JG4QbrHZpUe'Vi2?!3E:o40.-,k31LQId%e(nW&u6_RX&3MVQ6&._4P/"mHB\.Hj]M;(gZVPKF`R'(8fp@nb%NUgJ+5Xh@iEZKBG\,-ffK62EkAYk3..fgMU3F(fSYlrIon$sN5:;ZS5:)Mc#*g5OXdgc4:em6.0K#%eUs=Vrouf:O%_]GC#$JbG\W46`\4G`UUN0,fAYte\?TnRor.gjp&>WST#5b2%XCfjVd.oIT$^;FEDr6`R:s_"lo>Ob*NIGZ[c?YH?63?TkI"r826KeJVsY4+khY-'"If,~>endstream +endobj +10 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 781 +>> +stream +Gas1\?#SFN'RfGR30/05K^dAeVEI`_<@:u&aL%:+;b1fA,I*[(P\%UYf*RH#UfCG.-h]^Tk9C#=4G)i>kJ1p?Ii]`r+\WIV]E,kQB&U_4Mr:u6HA(Tlk`&%j,Hfr./dWG\rWlq=PqQmFXomri6Z:B#A.'suXMSeelM!CL]R#<;("2ZqN\I)Y%.PMcHs>LfEBQ`9'-)nROMSW9[)[L$Qq]q1[k\[Ec^,]-hBg%2#X>!TI7Fp.jlGjMlJW%R`C61U[SM>.^s"tu8fNIHFm!)uWnD-t%Ln:_&m7gFjSE8R.,I#imR]2(E96&eSqDJr*Q>H97@L3nTnLL^/QKl\%TsKEbu@8dO:D2AgL^;;$7l.1a'85KrC6a8QEICfB?%Tarmmco1WZ3_f\O^1OgD&&;S82;N\W`%0'9kH_jg\;J(8o'rQ`MfOWQ@$8_V5m,p<\;+^(LTN+(*!Ce@>S'O.D.)a.9@:UX"KA\hq]&:]MB_H>5?iXau]Jk!_je3aI6R?b!HKiDTa\<%HD\FO&lpSRP/\#[eCiDL6p=h;h^&t-)X+B>uReeBG0;%$#\Me"p;^%C@K&B2MPfJ.1hjOR`tH&rHmgl=2,-V$kaMNjBTT]R@-h(kPX%.TB`oIP;$5-WI-YV_pNb4EUahFN`><~>endstream +endobj +xref +0 11 +0000000000 65535 f +0000000073 00000 n +0000000114 00000 n +0000000221 00000 n +0000000333 00000 n +0000000536 00000 n +0000000740 00000 n +0000000808 00000 n +0000001091 00000 n +0000001156 00000 n +0000003678 00000 n +trailer +<< +/ID +[<9b82c849161aa76ccab15de04da90016><9b82c849161aa76ccab15de04da90016>] +% ReportLab generated PDF document -- digest (http://www.reportlab.com) + +/Info 7 0 R +/Root 6 0 R +/Size 11 +>> +startxref +4550 +%%EOF diff --git a/Documentation/README.md b/Documentation/README.md new file mode 100644 index 0000000..951f4b3 --- /dev/null +++ b/Documentation/README.md @@ -0,0 +1,30 @@ +# GotekBackend (Docker) + +Backend for ESP32-S2/S3 Gotek controller. +- Stores ONLY raw floppy images in `Images/` (e.g. `Images/.adf`). +- Builds a tiny FAT12 volume on-the-fly for GET, with your image + **FF.CFG from disk**. +- Accepts POSTed modified FAT, extracts the floppy image, and overwrites `Images/.`. +- Requires mutual TLS (client certificate) from the ESP32. + +## FF.CFG +Edit `FF.CFG` in the project root. The backend reads it at startup and serves it inside the FAT volume. +Compose mounts it into the container at `/app/FF.CFG` (read-only). + +## Run with Docker Compose +```bash +CERT_PASSWORD=changeit docker compose up --build +``` +This starts a `certgen` one-shot service that creates a dev CA and server cert, then runs the backend. + +## Endpoints +- `GET /api/volume?id=` -> returns a FAT12 image (your disk image + FF.CFG) +- `POST /api/volume?id=` -> receives FAT12, extracts first non-`FF.CFG` file, saves to `Images/` + +## Volumes +- `./Images` -> /app/Images +- `./Uploaded` -> /app/Uploaded +- `./certs` -> /certs (server.pfx + CA + client certs) +- `./FF.CFG` -> /app/FF.CFG (read-only) + +## Environment +- `CERT_PASSWORD` (required): password for `/certs/server.pfx` diff --git a/Documentation/transcript.txt b/Documentation/transcript.txt new file mode 100644 index 0000000..d6b7bac --- /dev/null +++ b/Documentation/transcript.txt @@ -0,0 +1,5 @@ +Conversation transcript summary +- ESP32-S2/S3 firmware with Wi-Fi setup, mTLS, pin-controlled mount/eject, RFID tag ID, /api/status, /api/remount +- Backend stores ONLY images; builds FAT12 on GET and extracts image on POST +- Dockerfile + appsettings.json + docker-compose.yml (with certgen) included +- FF.CFG is loaded from disk and served inside the FAT diff --git a/Frontend/FF.CFG b/Frontend/FF.CFG new file mode 100644 index 0000000..76ce7ac --- /dev/null +++ b/Frontend/FF.CFG @@ -0,0 +1,11 @@ +# FF.CFG - FlashFloppy configuration (served inside FAT by backend) +interface = ibmpc +host = acorn +index-suppression = no + +# Enable speaker / fake disk drive sounds +sound = on +sound-volume = 19 + +# Disable display devices +display-type = none