Skip to content
Open
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
18 changes: 18 additions & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/bin/sh

# Get staged files matching Prettier patterns
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(ts|tsx|js|jsx|json|md|mdx|css|html)$' || true)

# If there are no matching files, exit successfully
if [ -z "$STAGED_FILES" ]; then
exit 0
fi

# Format staged files with Prettier
echo "$STAGED_FILES" | xargs npx prettier --write || {
echo "Prettier formatting failed. Please run 'npm run format' to fix formatting issues."
exit 1
}

# Re-stage formatted files
echo "$STAGED_FILES" | xargs git add
42 changes: 42 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Dependencies
node_modules/
**/node_modules/

# Build outputs
dist/
build/
.output/
*.tsbuildinfo

# Generated files
**/routeTree.gen.ts
**/*.gen.ts
**/drizzle/

# Logs
*.log

# Environment files
.env
.env.local

# Lock files
package-lock.json
yarn.lock
pnpm-lock.yaml

# Database files
*.db
*.sqlite

# OS files
.DS_Store

# IDE
.vscode/
.idea/

# Temporary files
tmp/
temp/
*.tmp
10 changes: 10 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"semi": true,
"trailingComma": "all",
"singleQuote": false,
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"arrowParens": "always",
"endOfLine": "lf"
}
4 changes: 3 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,9 @@ outray/

- Use TypeScript
- Follow existing code patterns
- Run `npm run lint` before committing
- Code formatting is enforced with Prettier
- Run `npm run format` to format all files, or `npm run format:check` to check formatting
- Pre-commit hooks will automatically format staged files before commits

## Pull Requests

Expand Down
52 changes: 29 additions & 23 deletions apps/cli/src/toml-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,10 @@ const globalConfigSchema = Joi.object({
});

const tunnelConfigSchema = Joi.object({
protocol: Joi.string()
.valid("http", "tcp", "udp")
.required()
.messages({
"any.only": "Protocol must be one of: http, tcp, or udp",
"any.required": "Protocol is required (must be http, tcp, or udp)",
}),
protocol: Joi.string().valid("http", "tcp", "udp").required().messages({
"any.only": "Protocol must be one of: http, tcp, or udp",
"any.required": "Protocol is required (must be http, tcp, or udp)",
}),
local_port: portSchema.messages({
"number.base": "Local port must be a number",
"number.integer": "Local port must be an integer",
Expand Down Expand Up @@ -121,19 +118,21 @@ export class TomlConfigParser {
);
}

const { value: config, error: validationError } =
configSchema.validate(rawConfig, {
const { value: config, error: validationError } = configSchema.validate(
rawConfig,
{
abortEarly: false,
allowUnknown: false,
stripUnknown: false,
});
},
);

if (validationError) {
const errorMessages = validationError.details.map(
(detail: Joi.ValidationErrorItem) => {
const pathParts = detail.path;
const tunnelName = pathParts[1];
const fieldName = (pathParts[2] as string);
const fieldName = pathParts[2] as string;

let message = detail.message;

Expand All @@ -142,25 +141,32 @@ export class TomlConfigParser {
if (context?.message) {
message = context.message;
} else {
message = message.replace("contains an invalid value", "has invalid configuration");
message = message.replace(
"contains an invalid value",
"has invalid configuration",
);
}
}

if (fieldName) {
const fieldDisplayName = fieldName.replace(/_/g, " ");
const fullPath = detail.path.join(".");

if (message.includes(`"${fullPath}"`)) {
message = message.replace(`"${fullPath}"`, fieldDisplayName);
} else if (message.includes(fieldName)) {
message = message.replace(new RegExp(fieldName, "g"), fieldDisplayName);
}
if (fieldName) {
const fieldDisplayName = fieldName.replace(/_/g, " ");
const fullPath = detail.path.join(".");

if (message.includes(`"${fullPath}"`)) {
message = message.replace(`"${fullPath}"`, fieldDisplayName);
} else if (message.includes(fieldName)) {
message = message.replace(
new RegExp(fieldName, "g"),
fieldDisplayName,
);
}
}

message = message.replace(/^"/, "").replace(/"$/, "");
message = message.replace(/^"/, "").replace(/"$/, "");

if (tunnelName && typeof tunnelName === "string") {
const capitalizedMessage = message.charAt(0).toUpperCase() + message.slice(1);
const capitalizedMessage =
message.charAt(0).toUpperCase() + message.slice(1);
return `Tunnel "${tunnelName}": ${capitalizedMessage}`;
}

Expand Down
10 changes: 7 additions & 3 deletions apps/tunnel/src/core/HTTPProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,15 +157,17 @@ export class HTTPProxy {
if (metadata.fullCaptureEnabled) {
const captureId = generateId("capture");
const maxBodySize = 1024 * 1024; // 1MB limit

// Prepare request body (truncate if too large)
let requestBody: string | null = null;
let requestBodySize = bodyBuffer.length;
if (bodyBuffer.length > 0) {
if (bodyBuffer.length <= maxBodySize) {
requestBody = bodyBuffer.toString("base64");
} else {
requestBody = bodyBuffer.subarray(0, maxBodySize).toString("base64");
requestBody = bodyBuffer
.subarray(0, maxBodySize)
.toString("base64");
}
}

Expand All @@ -176,7 +178,9 @@ export class HTTPProxy {
if (responseBuffer.length <= maxBodySize) {
responseBody = responseBuffer.toString("base64");
} else {
responseBody = responseBuffer.subarray(0, maxBodySize).toString("base64");
responseBody = responseBuffer
.subarray(0, maxBodySize)
.toString("base64");
responseBodySize = responseBuffer.subarray(0, maxBodySize).length;
}
}
Expand Down
125 changes: 62 additions & 63 deletions apps/tunnel/src/core/PortAllocator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,79 +3,78 @@
* Instead of linear search, maintains a set of available ports.
*/
export class PortAllocator {
private availablePorts: number[] = [];
private usedPorts = new Set<number>();
private min: number;
private max: number;
private availablePorts: number[] = [];
private usedPorts = new Set<number>();
private min: number;
private max: number;

constructor(min: number, max: number) {
this.min = min;
this.max = max;
// Initialize with all ports available (lazy - we shuffle on first access)
this.initializePorts();
}
constructor(min: number, max: number) {
this.min = min;
this.max = max;
// Initialize with all ports available (lazy - we shuffle on first access)
this.initializePorts();
}

private initializePorts(): void {
// Create array of all ports and shuffle for random distribution
const ports: number[] = [];
for (let p = this.min; p <= this.max; p++) {
ports.push(p);
}
// Fisher-Yates shuffle for random port assignment
for (let i = ports.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[ports[i], ports[j]] = [ports[j], ports[i]];
}
this.availablePorts = ports;
private initializePorts(): void {
// Create array of all ports and shuffle for random distribution
const ports: number[] = [];
for (let p = this.min; p <= this.max; p++) {
ports.push(p);
}
// Fisher-Yates shuffle for random port assignment
for (let i = ports.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[ports[i], ports[j]] = [ports[j], ports[i]];
}
this.availablePorts = ports;
}

allocate(requestedPort?: number): number | null {
if (requestedPort !== undefined) {

if (this.usedPorts.has(requestedPort)) {
return null; // Already in use
}
if (!Number.isFinite(requestedPort) || !Number.isInteger(requestedPort)) {
return null;
}
if (requestedPort < this.min || requestedPort > this.max) {
return null; // Out of range
}

const idx = this.availablePorts.indexOf(requestedPort);
if (idx !== -1) {
this.availablePorts.splice(idx, 1);
}
this.usedPorts.add(requestedPort);
return requestedPort;
}

while (this.availablePorts.length > 0) {
const port = this.availablePorts.pop()!;
if (!this.usedPorts.has(port)) {
this.usedPorts.add(port);
return port;
}
}
allocate(requestedPort?: number): number | null {
if (requestedPort !== undefined) {
if (this.usedPorts.has(requestedPort)) {
return null; // Already in use
}
if (!Number.isFinite(requestedPort) || !Number.isInteger(requestedPort)) {
return null;
}
}
if (requestedPort < this.min || requestedPort > this.max) {
return null; // Out of range
}

release(port: number): void {
if (this.usedPorts.has(port)) {
this.usedPorts.delete(port);
this.availablePorts.push(port);
}
const idx = this.availablePorts.indexOf(requestedPort);
if (idx !== -1) {
this.availablePorts.splice(idx, 1);
}
this.usedPorts.add(requestedPort);
return requestedPort;
}

isInUse(port: number): boolean {
return this.usedPorts.has(port);
while (this.availablePorts.length > 0) {
const port = this.availablePorts.pop()!;
if (!this.usedPorts.has(port)) {
this.usedPorts.add(port);
return port;
}
}
return null;
}

availableCount(): number {
return this.availablePorts.length;
release(port: number): void {
if (this.usedPorts.has(port)) {
this.usedPorts.delete(port);
this.availablePorts.push(port);
}
}

usedCount(): number {
return this.usedPorts.size;
}
isInUse(port: number): boolean {
return this.usedPorts.has(port);
}

availableCount(): number {
return this.availablePorts.length;
}

usedCount(): number {
return this.usedPorts.size;
}
}
4 changes: 2 additions & 2 deletions apps/tunnel/src/lib/tigerdata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ class RequestCaptureLogger {
captures.forEach((capture, i) => {
const offset = i * 11;
placeholders.push(
`($${offset + 1}, $${offset + 2}, $${offset + 3}, $${offset + 4}, $${offset + 5}, $${offset + 6}, $${offset + 7}, $${offset + 8}, $${offset + 9}, $${offset + 10}, $${offset + 11})`
`($${offset + 1}, $${offset + 2}, $${offset + 3}, $${offset + 4}, $${offset + 5}, $${offset + 6}, $${offset + 7}, $${offset + 8}, $${offset + 9}, $${offset + 10}, $${offset + 11})`,
);
values.push(
capture.id,
Expand All @@ -180,7 +180,7 @@ class RequestCaptureLogger {
capture.request_body_size,
JSON.stringify(capture.response_headers),
capture.response_body,
capture.response_body_size
capture.response_body_size,
);
});

Expand Down
11 changes: 4 additions & 7 deletions apps/tunnel/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,7 @@ import { TCPProxy } from "./core/TCPProxy";
import { UDPProxy } from "./core/UDPProxy";
import { LogManager } from "./core/LogManager";
import { config } from "./config";
import {
checkTimescaleDBConnection,
shutdownLoggers,
} from "./lib/tigerdata";
import { checkTimescaleDBConnection, shutdownLoggers } from "./lib/tigerdata";

const redis = new Redis(config.redisUrl, {
lazyConnect: true,
Expand Down Expand Up @@ -94,7 +91,7 @@ async function validateDashboardToken(token: string): Promise<{
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${internalApiSecret}`,
Authorization: `Bearer ${internalApiSecret}`,
},
body: JSON.stringify({ token }),
});
Expand Down Expand Up @@ -183,10 +180,10 @@ const shutdown = async () => {
wsHandler.shutdown();
await router.shutdown();
await redis.quit();

// Flush buffered logs and close database connection
await shutdownLoggers();

httpServer.close(() => process.exit(0));
};

Expand Down
Loading