Initial created by ChatGPT
This commit is contained in:
commit
36c7aa081b
13
AquaCubeIT.Service.NetFloppy/Dockerfile
Normal file
13
AquaCubeIT.Service.NetFloppy/Dockerfile
Normal file
@ -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"]
|
||||
319
AquaCubeIT.Service.NetFloppy/Program.cs
Normal file
319
AquaCubeIT.Service.NetFloppy/Program.cs
Normal file
@ -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=<hex64> -> build tiny FAT12 in memory containing: [<image file>, 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=<hex64> -> receive modified FAT; extract image file; save back to Images/<id>.<ext>
|
||||
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<clPayload;i++){
|
||||
ushort cl=(ushort)(firstPayload+i);
|
||||
ushort val=(ushort)(i==clPayload-1?0x0FFF:cl+1);
|
||||
SetFAT12(fat,cl,val);
|
||||
}
|
||||
for(int i=0;i<clCfg;i++){
|
||||
ushort cl=(ushort)(firstCfg+i);
|
||||
ushort val=(ushort)(i==clCfg-1?0x0FFF:cl+1);
|
||||
SetFAT12(fat,cl,val);
|
||||
}
|
||||
for(int f=0;f<FATS;f++) bw.Write(fat);
|
||||
|
||||
byte[] root = new byte[rootSecs*BPS];
|
||||
var n1 = To83(payloadName);
|
||||
Array.Copy(n1,0,root,0,11);
|
||||
root[11]=0x20; WriteLE16(root,26,firstPayload); WriteLE32(root,28,(uint)payload.Length);
|
||||
var n2 = To83("FF.CFG");
|
||||
Array.Copy(n2,0,root,32,11);
|
||||
root[32+11]=0x20; WriteLE16(root,32+26,firstCfg); WriteLE32(root,32+28,(uint)ffCfg.Length);
|
||||
bw.Write(root);
|
||||
|
||||
WriteData(bw,payload,clPayload,BPS);
|
||||
WriteData(bw,ffCfg,clCfg,BPS);
|
||||
|
||||
bw.Flush();
|
||||
return ms.ToArray();
|
||||
|
||||
static void WriteLE16(byte[] b,int o,ushort v){ b[o]=(byte)v; b[o+1]=(byte)(v>>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<byte> 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;i<clusters;i++){
|
||||
int take=Math.Min(bps, data.Length-written);
|
||||
if(take>0) bw.Write(data, written, take);
|
||||
if(take<bps) bw.Write(pad, 0, bps-take);
|
||||
written += take;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Minimal FAT12 extractor (first non-FF.CFG file from root)
|
||||
static (string? name, byte[]? data) ExtractFirstNonCfgRootFile(Stream s)
|
||||
{
|
||||
Span<byte> 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<uint> 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);
|
||||
}
|
||||
17
AquaCubeIT.Service.NetFloppy/appsettings.json
Normal file
17
AquaCubeIT.Service.NetFloppy/appsettings.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"Kestrel": {
|
||||
"Certificates": {
|
||||
"Default": {
|
||||
"Path": "/certs/server.pfx",
|
||||
"Password": "${CERT_PASSWORD}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
31
AquaCubeIT.Service.NetFloppy/docker-compose.yml
Normal file
31
AquaCubeIT.Service.NetFloppy/docker-compose.yml
Normal file
@ -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
|
||||
8
Certificates/cert-gen.sh
Normal file
8
Certificates/cert-gen.sh
Normal file
@ -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'"
|
||||
14
Certificates/certs/README-certs.md
Normal file
14
Certificates/certs/README-certs.md
Normal file
@ -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`.
|
||||
77
Certificates/certs/gen-certs.sh
Normal file
77
Certificates/certs/gen-certs.sh
Normal file
@ -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 <<EOF
|
||||
[req]
|
||||
default_bits = 2048
|
||||
prompt = no
|
||||
default_md = sha256
|
||||
req_extensions = req_ext
|
||||
distinguished_name = dn
|
||||
|
||||
[dn]
|
||||
CN = ${CN_SERVER}
|
||||
|
||||
[req_ext]
|
||||
subjectAltName = @alt_names
|
||||
|
||||
[alt_names]
|
||||
DNS.1 = localhost
|
||||
IP.1 = 127.0.0.1
|
||||
EOF
|
||||
|
||||
openssl genrsa -out server.key 2048
|
||||
openssl req -new -key server.key -out server.csr -config server.cnf
|
||||
|
||||
# 2b) Sign server CSR with CA
|
||||
cat > ca.cnf <<EOF
|
||||
[ca]
|
||||
default_ca = CA_default
|
||||
[CA_default]
|
||||
copy_extensions = copy
|
||||
EOF
|
||||
|
||||
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 825 -sha256 -extfile server.cnf -extensions req_ext
|
||||
|
||||
# Export server cert+key to PKCS#12 for Kestrel
|
||||
openssl pkcs12 -export -out server.pfx -inkey server.key -in server.crt -certfile ca.crt -passout pass:${CERT_PASSWORD}
|
||||
|
||||
# PEM variants (optional)
|
||||
cp server.crt server.pem
|
||||
cp server.key server.key.pem
|
||||
cat server.crt ca.crt > 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)"
|
||||
93
Documentation/Overview.pdf
Normal file
93
Documentation/Overview.pdf
Normal file
@ -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'rd<m0:JRcDEA*jHoO<\:E!*;RN>6A*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;]c3mR<o5`q3k6)mISR;Q[R#C8H^n#S%T*4dqFRVJh\5?j?)B5mJ`Fubd<jgZpWfl<?k'VoiYQoeYmrPm6-#1-FO29[s4Uk(6j<^k@BG-?K[(D05`MZi%2?24J2Qc2CFT.mM9Gh,T!hH%+pnG8&O'6<=Akj;bHF5$7Hd$b^s;iHc.ME!:F9=p]\;PZ&V$m*nB//7-SQY+us0\^+8Kk?^8_A#:p3\@p$.%)!Y;:`\'m-^a[$8do*-3h@Bl3N/?D=o2%NP4:d9KKp*rfg!LP"=elOX8H*&uk;OAZ%!dfn)naB5`JZR_7@3\-P<:eU9;lO59!.i+bm"SB2M]:^=S#h[sbgRC'osd74>i%s$>Vlhk*-rS,?C\YP91'd^(M;\.'.*dF4Y`\:OH`*(e]TmCKR%qIPK["RSq.Ih$o1d>+tY'bd.#J/2mcLpO6@iXs?jebk?*["Djrc:Qj@t^_<ZQN^].SFT76%M#@p?W6*A0csL(><;Ur6$!uOQ$-'in+gGV"u3#+h96-9!V!j-(M@,h^.g1W$"JAE`UMt#8J"=_RPC5aUp$Bp@><Gl!?r9;Ara^<XuQ[<sqG:$&*1a\E/I$ksbZmfTt_%(eTL[_H<Pi.29oI7,'VZ<XtXNkDm4^q0VBX43&s>a''@<j16(r;>I(tn9=dsce4U7PsMg[;^BpL[?<f%"tbd"IbOV#;'+BFabCW7lSlsO4k6meU]n@Z6!P.fo<UDU5iVH+=lBD@M0QiYi:89a?Ah\_.O@C9@(P\YS!b1V__aP.ce&V@96GebYWq*87W"Y<>'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-*<s,ap^"PZV41P<4!as,;@XK7qm28DQ3qXu(M+LJLj\(5#.GLNB.F(LuCW,/\ApV&fGF#!-[0sR)t\8Iq.DLg95Cl38%>[MV\BWH%2.'Q#fGMEXgidg:$&eQoBcnrN9N51Zp#q!]nPKp&ibOg<T=@pjZm:;T--aQh[=;AGOkZTd(rS2n9](uC(mL,3%$Y+lYait%WQ"6tIl)$+YF5cbsFc4>K=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<rNNF@7!%2k-Jh@Eai7Z5h*.(86]r+'D@>+l.ceeh)=rS6,eF+;e`1)+'jSb[j5]gl]E&(1m9LAGT#3oVSfAUNc^<b'8>tjc&BF>;<YN5<i+@a"JJ`HIWpTH*ZY+U23>=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/"mH<bcT<2Wg+D;,I+T(UP2#pLK`^@rUmnpY[JjA<D$9EAneI-Woo^S`pDGZQ*]5H)+f*A>B\.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$kaMNjB<J<-nUB-*^nEu).IUcN!P0-[9naFUKSU^WZg9[J_o%(Ja5EU:o#U-33TjZ=),?3jF86Z"*c<*L:9j^NDroAs"4NF`8gEP^/I0tu3F`G5+&C:7&D6FtSu'fnQKRW&sa#>TT]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
|
||||
30
Documentation/README.md
Normal file
30
Documentation/README.md
Normal file
@ -0,0 +1,30 @@
|
||||
# GotekBackend (Docker)
|
||||
|
||||
Backend for ESP32-S2/S3 Gotek controller.
|
||||
- Stores ONLY raw floppy images in `Images/` (e.g. `Images/<id>.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/<id>.<ext>`.
|
||||
- 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=<hex64>` -> returns a FAT12 image (your disk image + FF.CFG)
|
||||
- `POST /api/volume?id=<hex64>` -> 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`
|
||||
5
Documentation/transcript.txt
Normal file
5
Documentation/transcript.txt
Normal file
@ -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
|
||||
11
Frontend/FF.CFG
Normal file
11
Frontend/FF.CFG
Normal file
@ -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
|
||||
Loading…
Reference in New Issue
Block a user