Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 17 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,37 +40,48 @@ The following Secrets and Variables must be configured under the "Actions" conte
- REACT_APP_SITE_NAME

## API
### Variables

| Variable | Value |
| ------------ | ------------ |
| ENABLE_CORS | Default is false, set to true when testing otherwise you will get CORS failures. In prod this should be handled by your proxy |
| RATE_LIMIT_S | The duration of your rate limit expressed in seconds |
RATE_LIMIT_MAX | How many requests a client can make in RATE_LIMIT_S before being blocked |
| FILE_SIZE_MB | How large of a file can be processed. This same size should be configured on your proxy for a more reliable failure. |

### Local development
On a Windows host with Docker installed and using Windows containers execute the following from inside the `api` directory to build and launch the [WinDebug-Container](https://github.com/PipeItToDevNull/WinDebug-Container) based API.

```bash
docker build -t api . ; docker run --rm -it -e ENABLE_CORS=true -p 3000:3000 api
docker build -t api . ; docker run --rm -it -e ENABLE_CORS=true -e FILE_SIZE_MB=15 -e RATE_LIMIT_S=60 -e RATE_LIMIT_MAX=10 -p 3001:3000 api
```

You may need process isolation if you get the error `hcs::CreateComputeSystem \\: The request is not supported.`

```bash
docker build --isolation=process -t api . ; docker run --isolation=process --rm -it -e ENABLE_CORS=true -p 3000:3000 api
docker build --isolation=process -t api . ; docker run --isolation=process --rm -it -e ENABLE_CORS=true -p 3001:3000 api
```

Once launched use `REACT_APP_API_URL=http://localhost:3000` in your `.env` and launch your local development SWA.
Once launched use `REACT_APP_API_URL=http://localhost:3001` in your `.env` and launch your local development SWA.

### Deployment
Using an nginx reverse proxy to apply CORS (don't run ENABLE_CORS=true in prod) and SSL is the best method, the nginx default max client upload size of 10MB is fine for this appliction.
Using an nginx reverse proxy to apply CORS and SSL is the best method, the nginx default max client upload size of 10MB is fine for this appliction.

You want to declare a volume for `C:\app\results`, an example command for deployment is below.

```bash
docker run -d --restart unless-stopped --name webdbg-api -v webdbg-results:C:\app\results -p 3000:3000 ghcr.io/r-techsupport/webdbg-api:latest
docker run -d --restart unless-stopped --name webdbg-api -v webdbg-results:C:\app\results -e FILE_SIZE_MB=10 -e RATE_LIMIT_S=60 -e RATE_LIMIT_MAX=5 -p 3000:3000 ghcr.io/r-techsupport/webdbg-api:latest
```

You will want to increase the `proxy_read_timeout` if using nginx or the equivilant for your soltuion. Set it to at least 120s if not 300s.

### PUT endpoint usage
With a file
```bash
curl.exe -X PUT http://localhost:3000/analyze-dmp -F "dmpFile=@path/to/test.dmp"
```

With a URL
With a URL, if you have special characters in your URL like `&` you will need to encode it before submitting.
```bash
curl -X PUT http://localhost:3000/analyze-dmp -F "url=http://example.com/file.dmp"
```
Expand Down
25 changes: 19 additions & 6 deletions api/analyze.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,20 +60,33 @@ const processResult = (dmp, rawContent) => {
const analysisLines = infos[1].split('\n').filter(line => !line.includes('*') && !line.includes("Debugging Details:"));
const analysis = analysisLines.join('\n').trim();

// Extracting bugcheck and arguments
const bugcheckMatch = analysis.match(/\(([^)]+)\)/);
const bugcheck = bugcheckMatch ? bugcheckMatch[1] : null;
// Extract human name and code, e.g. "MEMORY_MANAGEMENT (1a)"
// bugcheckHuman => "MEMORY_MANAGEMENT", bugcheck => "1a"
let bugcheckHuman = null;
let bugcheck = null;
const bcMatch = analysis.match(/^\s*([A-Za-z0-9 _-]+?)\s*\(\s*([0-9A-Fa-fx]+)\s*\)/m);
if (bcMatch) {
bugcheckHuman = bcMatch[1].trim();
bugcheck = bcMatch[2].trim();
} else {
// fallback: capture any code-like value in parentheses
const bugcheckOnlyMatch = analysis.match(/\(([0-9A-Fa-fx]+)\)/);
if (bugcheckOnlyMatch) bugcheck = bugcheckOnlyMatch[1].trim();
}

// Arguments like: Arg1: 00000000...
const argMatches = analysis.match(/Arg\d:\s*([0-9a-fA-Fx]+)/g);
const args = argMatches ? argMatches.map(arg => arg.split(':')[1].trim()) : [];

const argMatches = analysis.match(/Arg\d: ([0-9a-fA-Fx]+)/g);
const args = argMatches ? argMatches.map(arg => arg.split(': ')[1]) : [];
logger.info(`Bugcheck: ${bugcheck}, Args: ${args}`);
logger.info(`Bugcheck: ${bugcheck} (${bugcheckHuman || 'unknown'}), Args: ${args}`);

// Output object creation
const output = {
dmp: dmp, // Include the dmp file path
dmpInfo: dmpInfo,
analysis: analysis,
bugcheck: bugcheck,
bugcheckHuman: bugcheckHuman,
args: args,
rawContent: rawContent
};
Expand Down
34 changes: 28 additions & 6 deletions api/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,28 @@ if (!fs.existsSync(resultsDir)) {
fs.mkdirSync(resultsDir);
}

// Read file size, rate limit window, and max hits from environment variables
const FILE_SIZE_MB = Number.isNaN(parseInt(process.env.FILE_SIZE_MB, 10))
? 10
: parseInt(process.env.FILE_SIZE_MB, 10); // Default is 10 MB

const RATE_LIMIT_S = Number.isNaN(parseInt(process.env.RATE_LIMIT_S, 10))
? 180
: parseInt(process.env.RATE_LIMIT_S, 10); // Default 180 seconds (3 minutes)

const RATE_LIMIT_MAX = Number.isNaN(parseInt(process.env.RATE_LIMIT_MAX, 10))
? 10
: parseInt(process.env.RATE_LIMIT_MAX, 10); // Default 10 requests per window

// Convert to bytes and milliseconds
const FILE_SIZE_BYTES = FILE_SIZE_MB * 1024 * 1024;
const RATE_LIMIT_MS = RATE_LIMIT_S * 1000;

// Log the configured limits
logger.info(`File size limit: ${FILE_SIZE_MB}MB (${FILE_SIZE_BYTES} bytes)`);
logger.info(`Rate limit window: ${RATE_LIMIT_S} seconds (${RATE_LIMIT_MS} ms), max ${RATE_LIMIT_MAX} requests per window`);


// Configure multer for file uploads
const storage = multer.diskStorage({
destination: (req, file, cb) => {
Expand All @@ -72,19 +94,19 @@ if (process.env.ENABLE_CORS === 'true') {
});
}

// Size limit of 10M
// Size limit from environment variable (default 10MB)
const upload = multer({
storage: storage,
limits: { fileSize: 10 * 1024 * 1024 }
limits: { fileSize: FILE_SIZE_BYTES }
});

// Add security headers to all responses
app.use(helmet()); // Add security headers to all responses

// Rate limiting middleware to prevent abuse
const limiter = rateLimit({
windowMs: 3 * 60 * 1000, // 3 minutes
max: 10 // limit each IP to 10 requests per windowMs
windowMs: RATE_LIMIT_MS, // from environment variable
max: RATE_LIMIT_MAX // limit each IP to X requests per windowMs
});
app.use(limiter);

Expand Down Expand Up @@ -374,8 +396,8 @@ app.use(async (req, res, next) => {
app.use((err, req, res, next) => {
if (err instanceof multer.MulterError) {
if (err.code === 'LIMIT_FILE_SIZE') {
logger.warn('File size exceeds the limit of 10MB');
return res.status(400).send('File size exceeds the limit of 10MB');
logger.warn(`File size exceeds the limit of ${FILE_SIZE_MB}MB`);
return res.status(400).send(`File size exceeds the limit of ${FILE_SIZE_MB}MB`);
}
}

Expand Down
37 changes: 26 additions & 11 deletions api/post-process.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { exec } from 'child_process';
import winston from 'winston';
import fs from 'fs';
import path from 'path';
import { fileURLToPath, pathToFileURL } from 'url';

// Configure Winston logger
const logger = winston.createLogger({
Expand All @@ -14,15 +17,26 @@ const logger = winston.createLogger({
]
});

// Configuration object for bugcheck commands
const bugcheckCommands = {
'9f': (parser, dmp, args) => `${parser} -z ${dmp} -c "k; !devstack ${args[1]} ; q"`,
'133': (parser, dmp) => `${parser} -z ${dmp} -c "k; !dpcwatchdog ; q"`,
// Add more bugcheck commands here as needed
// '<bugcheck>': (dmp, args) => `${parser} -z ${dmp} -c "k; <commands to run> ; q"`,
// Args can be used in a command ${args[#]}
// Arg counts start at 0 so "Arg1" is ${args[0]}
};
const bugcheckCommands = {};
const processorsDir = path.join(path.dirname(fileURLToPath(import.meta.url)), 'post-processors');

try {
const files = fs.readdirSync(processorsDir).filter(f => f.endsWith('.js'));
for (const file of files) {
const name = path.basename(file, '.js');
const filePath = path.join(processorsDir, file);
try {
// eslint-disable-next-line no-await-in-loop
const mod = await import(pathToFileURL(filePath).href);
bugcheckCommands[name] = mod.default;
logger.info(`Loaded post-processor: ${name} -> ${filePath}`);
} catch (err) {
logger.error(`Failed to load post-processor ${filePath}: ${err.stack || err}`);
}
}
} catch (err) {
logger.error(`Unable to read post-processors directory "${processorsDir}": ${err.stack || err}`);
}

// Function to execute a command and return a promise
const executeCommand = (command) => {
Expand All @@ -42,8 +56,9 @@ const executeCommand = (command) => {
// Function to perform additional operations on the analysis results
const postProcessResults = async (results, parser) => {
for (const result of results) {
const commandGenerator = bugcheckCommands[result.bugcheck];
const commandGenerator = bugcheckCommands[String(result.bugcheck || '').toLowerCase()];
if (commandGenerator) {
// commandGenerator expected signature: (parser, dmp, args) => string
const command = commandGenerator(parser, result.dmp, result.args);
logger.info(`Executing command: ${command}`);
try {
Expand All @@ -55,7 +70,7 @@ const postProcessResults = async (results, parser) => {
logger.error(`An error occured while post-processing the file: ${error}`);
}
} else {
result.post = "No post processing configured for this bugcheck" // Add a null post key if no command is run
result.post = "No post processing configured for this bugcheck";
logger.info(`No command for bugcheck: ${result.bugcheck}`);
}
}
Expand Down
4 changes: 4 additions & 0 deletions api/post-processors/133.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Generate command for bugcheck 133 (DPC_WATCHDOG_VIOLATION)
export default (parser, dmp) => {
return `${parser} -z ${dmp} -c "k; !dpcwatchdog ; q"`;
};
5 changes: 5 additions & 0 deletions api/post-processors/9f.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Generate command for bugcheck 9f (PAGE_FAULT_IN_NONPAGED_AREA / example)
export default (parser, dmp, args = []) => {
const devstackArg = args[1] ? args[1] : '';
return `${parser} -z ${dmp} -c "k; !devstack ${devstackArg} ; q"`;
};
Loading
Loading