Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
41 changes: 19 additions & 22 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ on:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
# Lets this workflow be run manually against any branch (Actions tab ->
# "Run workflow", or `gh workflow run`), without needing to push/PR to
# main first. Doesn't change push/pull_request behavior at all.
workflow_dispatch: {}

jobs:
build:
Expand All @@ -28,6 +32,10 @@ jobs:
run: npm run build --if-present
working-directory: ./middlewareNode

- name: Test middlewareNode
run: npm test
working-directory: ./middlewareNode

# chessServer
- name: Install dependencies (chessServer)
run: npm ci
Expand Down Expand Up @@ -59,31 +67,20 @@ jobs:
run: npm ci
working-directory: ./react-ystemandchess

- name: Create environment file
run: |
mkdir -p src/core/environments
cat <<EOF > src/core/environments/environment.ts
export const environment = {
production: false,
agora: {
appId: '${{ secrets.APP_ID }}',
},
email: {
user: '${{ secrets.EMAIL_USER || '' }}',
pass: '${{ secrets.EMAIL_PASS || '' }}'
},
urls: {
middlewareURL: '${{ secrets.MIDDLEWARE_URL }}',
stockFishURL: '${{ secrets.STOCKFISH_URL }}',
chessServer: '${{ secrets.CHESS_SERVER }}',
},
};
EOF
working-directory: ./react-ystemandchess

- name: Build react-ystemandchess
run: CI=false npm run build --if-present
working-directory: ./react-ystemandchess
env:
# These are read by src/environments/environment.prod.js, which is
# what the app actually imports (via src/environments/index.js).
# The previous "Create environment file" step wrote to
# src/core/environments/environment.ts, a path nothing in the app
# imports, so these secrets never reached the built app.
REACT_APP_MIDDLEWARE_URL: ${{ secrets.MIDDLEWARE_URL || 'https://ystemandchess.com/middleware' }}
REACT_APP_STOCKFISH_SERVER_URL: ${{ secrets.STOCKFISH_URL || 'https://ystemandchess.com/stockfishserver' }}
REACT_APP_CHESS_SERVER_URL: ${{ secrets.CHESS_SERVER || 'https://ystemandchess.com/chessserver' }}
REACT_APP_CHESS_CLIENT_URL: ${{ secrets.CHESS_CLIENT_URL || 'https://ystemandchess.com/chessclient' }}
REACT_APP_AGORA_APP_ID: ${{ secrets.APP_ID || '' }}

- name: Test react-ystemandchess
run: npm test
Expand Down
5 changes: 5 additions & 0 deletions chessServer/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
NODE_ENV=development
PORT=3001
CORS_ORIGIN=http://localhost:3000,http://localhost:3002
ALLOWED_ORIGINS=
MIDDLEWARE_URL=http://localhost:8000
1 change: 1 addition & 0 deletions chessServer/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions chessServer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"license": "ISC",
"dependencies": {
"chess.js": "^1.0.0-beta.8",
"cors": "^2.8.5",
"dotenv": "^8.6.0",
"express": "^4.21.0",
"jest": "^29.7.0",
Expand Down
46 changes: 37 additions & 9 deletions chessServer/src/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
require("dotenv").config();

const validateEnvironment = require("./validateEnvironment");
validateEnvironment();

const express = require("express");
const http = require("http");
const socketIo = require("socket.io");
Expand All @@ -13,28 +16,53 @@ const server = http.createServer(app);
// Add logging functionaility to the server
app.use(morgan("dev")); // dev -> preset format

const allowedOriginsSetting = process.env.CORS_ORIGIN || process.env.ALLOWED_ORIGINS;
const allowedOrigins = allowedOriginsSetting
const isProduction = process.env.NODE_ENV === "production";

const allowedOriginsSetting =
process.env.CORS_ORIGIN || process.env.ALLOWED_ORIGINS;

const allowedOrigins = (allowedOriginsSetting
? allowedOriginsSetting.split(",").map((o) => o.trim())
: [
"https://ystemandchess.com",
"https://www.ystemandchess.com",
"http://localhost:3000",
"http://localhost:3002",
"http://localhost:4200",
];
...(isProduction
? []
: [
"http://localhost:3000",
"http://localhost:3002",
"http://localhost:4200",
]),
]
).map((origin) => {
if (origin !== "*" && origin.endsWith("/")) {
const normalized = origin.slice(0, -1);
console.warn(
`CORS: "${origin}" has a trailing slash, which never matches a browser's Origin header — using "${normalized}" instead`
);
return normalized;
}
return origin;
});

const hasWildcard = allowedOrigins.includes("*");

const corsOptions = {
origin: function (origin, callback) {
if (!origin) return callback(null, true);
if (allowedOrigins.indexOf(origin) !== -1 || allowedOrigins.includes("*")) {
if (hasWildcard) {
return callback(null, true);
}
if (allowedOrigins.indexOf(origin) !== -1) {
callback(null, true);
} else {
callback(new Error(`Origin ${origin} not allowed by CORS`));
console.warn(`CORS: rejected origin ${origin}`);
callback(null, false);
}
},
methods: ["GET", "POST"],
credentials: true,
// When wildcard is configured, disallow credentials to prevent unsafe CORS configuration
credentials: !hasWildcard,
};

// Apply CORS middleware to handle cross-origin requests
Expand Down
67 changes: 67 additions & 0 deletions chessServer/src/tests/cors.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* Verifies the CORS origin/credentials behavior in src/index.js.
* index.js can't be imported directly (it calls server.listen() as a
* module-load side effect), so this mirrors its corsOptions logic in an
* isolated app, the same approach the other tests in this file already use
* for their own standalone servers. Keep this in sync with src/index.js if
* that logic changes.
*/

const express = require("express");
const cors = require("cors");
const request = require("supertest");

function buildApp(allowedOriginsCsv) {
const allowedOrigins = allowedOriginsCsv.split(",").map((o) => o.trim());
const hasWildcard = allowedOrigins.includes("*");

const app = express();
app.use(
cors({
origin: function (origin, callback) {
if (!origin) return callback(null, true);
if (hasWildcard) {
return callback(null, true);
}
if (allowedOrigins.indexOf(origin) !== -1) {
callback(null, true);
} else {
callback(null, false);
}
},
methods: ["GET", "POST"],
credentials: !hasWildcard,
})
);
app.get("/ping", (req, res) => res.json({ ok: true }));
return app;
}

describe("chessServer CORS origin/credentials behavior", () => {
test("allowed origin (no wildcard): credentials allowed, header set to the origin", async () => {
const app = buildApp("https://ystemandchess.com,http://localhost:3000");
const res = await request(app).get("/ping").set("Origin", "http://localhost:3000");

expect(res.status).toBe(200);
expect(res.headers["access-control-allow-origin"]).toBe("http://localhost:3000");
expect(res.headers["access-control-allow-credentials"]).toBe("true");
});

test("disallowed origin (no wildcard): rejected without a 500, no CORS header", async () => {
const app = buildApp("https://ystemandchess.com,http://localhost:3000");
const res = await request(app).get("/ping").set("Origin", "https://evil.example.com");

expect(res.status).not.toBe(500);
expect(res.status).toBe(200);
expect(res.headers["access-control-allow-origin"]).toBeUndefined();
});

test("wildcard configured: any origin allowed, but credentials are disabled", async () => {
const app = buildApp("*");
const res = await request(app).get("/ping").set("Origin", "https://anything.example.com");

expect(res.status).toBe(200);
expect(res.headers["access-control-allow-origin"]).toBe("https://anything.example.com");
expect(res.headers["access-control-allow-credentials"]).toBeUndefined();
});
});
36 changes: 36 additions & 0 deletions chessServer/src/validateEnvironment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
const REQUIRED_PRODUCTION_VARS = [
"MIDDLEWARE_URL",
];

function hasCorsConfiguration() {
return Boolean(
process.env.CORS_ORIGIN?.trim() ||
process.env.ALLOWED_ORIGINS?.trim()
);
}

function validateEnvironment() {
if (process.env.NODE_ENV !== "production") {
return;
}

const missing = REQUIRED_PRODUCTION_VARS.filter((name) => {
const value = process.env[name];
return !value || !value.trim();
});

if (!hasCorsConfiguration()) {
missing.push("CORS_ORIGIN or ALLOWED_ORIGINS");
}

if (missing.length > 0) {
console.error(
`[chessServer] Missing required production environment variables: ${missing.join(", ")}`
);
process.exit(1);
}

console.log("[chessServer] Required production environment variables validated");
}

module.exports = validateEnvironment;
21 changes: 15 additions & 6 deletions deploy/dev/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,42 +12,51 @@ services:
- stockfishserver

react-app:
build: ../../react-ystemandchess
build:
context: ../../react-ystemandchess
args:
REACT_APP_CHESS_SERVER_URL: http://localhost:3001
REACT_APP_MIDDLEWARE_URL: http://localhost:8000
REACT_APP_STOCKFISH_SERVER_URL: http://localhost:9324
REACT_APP_AGORA_APP_ID: ""
container_name: react-app
ports:
- "3000:3000"
environment:
- REACT_APP_CHESS_SERVER_URL=http://localhost:3001
- REACT_APP_MIDDLEWARE_URL=http://localhost:8000
- REACT_APP_STOCKFISH_URL=http://localhost:8080

middlewarenode:
build: ../../middlewareNode
container_name: middlewarenode
ports:
- "8000:8000"
environment:
- NODE_ENV=development
- PORT=8000
- INDEX_KEY=dev-index-key-replace-in-prod
- CORS_ORIGIN=http://localhost,http://localhost:3000
- SESSION_SECRET=dev-secret-replace-in-prod
# Analytics rate limit — requests per 15-minute window per IP (default: 100)
- ANALYTICS_RATE_LIMIT_MAX=100
- LEADERBOARD_RATE_LIMIT_MAX=60

chessserver:
build: ../../chessServer
container_name: chessserver
ports:
- "3001:3001"
environment:
- NODE_ENV=development
- PORT=3001
- MIDDLEWARE_URL=http://middlewarenode:8000
- CORS_ORIGIN=http://localhost,http://localhost:3000,http://localhost:3002

stockfishserver:
build: ../../stockfishServer
container_name: stockfishserver
ports:
- "9324:9324"
environment:
- NODE_ENV=development
- PORT=9324
- CORS_ORIGIN=http://localhost,http://localhost:3000

# Usage: docker-compose up --build
# Access at: http://localhost
Loading
Loading