-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
9 changed files
with
277 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import type { APIGatewayProxyWebsocketHandlerV2 } from 'aws-lambda' | ||
import process from 'node:process' | ||
import { DynamoDBClient, PutItemCommand } from '@aws-sdk/client-dynamodb' | ||
|
||
const dynamoDB = new DynamoDBClient({}) | ||
const TABLE_NAME = process.env.TABLE_NAME! | ||
|
||
export const handler: APIGatewayProxyWebsocketHandlerV2 = async (event) => { | ||
const connectionId = event.requestContext.connectionId | ||
const subdomain = event.queryStringParameters?.subdomain | ||
|
||
if (!subdomain) { | ||
return { | ||
statusCode: 400, | ||
body: 'Subdomain is required', | ||
} | ||
} | ||
|
||
try { | ||
await dynamoDB.send(new PutItemCommand({ | ||
TableName: TABLE_NAME, | ||
Item: { | ||
connectionId: { S: connectionId }, | ||
subdomain: { S: subdomain }, | ||
timestamp: { N: Date.now().toString() }, | ||
}, | ||
})) | ||
|
||
return { statusCode: 200, body: 'Connected' } | ||
} | ||
catch (error) { | ||
console.error('Connection error:', error) | ||
return { statusCode: 500, body: 'Failed to connect' } | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import type { APIGatewayProxyWebsocketHandlerV2 } from 'aws-lambda' | ||
import process from 'node:process' | ||
import { DeleteItemCommand, DynamoDBClient } from '@aws-sdk/client-dynamodb' | ||
|
||
const dynamoDB = new DynamoDBClient({}) | ||
const TABLE_NAME = process.env.TABLE_NAME! | ||
|
||
export const handler: APIGatewayProxyWebsocketHandlerV2 = async (event) => { | ||
const connectionId = event.requestContext.connectionId | ||
|
||
try { | ||
await dynamoDB.send(new DeleteItemCommand({ | ||
TableName: TABLE_NAME, | ||
Key: { | ||
connectionId: { S: connectionId }, | ||
}, | ||
})) | ||
|
||
return { statusCode: 200, body: 'Disconnected' } | ||
} | ||
catch (error) { | ||
console.error('Disconnection error:', error) | ||
return { statusCode: 500, body: 'Failed to disconnect' } | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
import type { APIGatewayProxyHandlerV2 } from 'aws-lambda' | ||
import process from 'node:process' | ||
import { ApiGatewayManagementApiClient, PostToConnectionCommand } from '@aws-sdk/client-apigatewaymanagementapi' | ||
import { DynamoDBClient, QueryCommand } from '@aws-sdk/client-dynamodb' | ||
|
||
const dynamoDB = new DynamoDBClient({}) | ||
const TABLE_NAME = process.env.TABLE_NAME! | ||
|
||
export const handler: APIGatewayProxyHandlerV2 = async (event) => { | ||
const host = event.headers.host || '' | ||
const subdomain = host.split('.')[0] | ||
|
||
if (!subdomain) { | ||
return { | ||
statusCode: 400, | ||
body: 'Invalid subdomain', | ||
} | ||
} | ||
|
||
try { | ||
// Find connection for the subdomain | ||
const connections = await dynamoDB.send(new QueryCommand({ | ||
TableName: TABLE_NAME, | ||
IndexName: 'subdomain-index', | ||
KeyConditionExpression: 'subdomain = :subdomain', | ||
ExpressionAttributeValues: { | ||
':subdomain': { S: subdomain }, | ||
}, | ||
})) | ||
|
||
if (!connections.Items || connections.Items.length === 0) { | ||
return { | ||
statusCode: 404, | ||
body: 'Tunnel not found', | ||
} | ||
} | ||
|
||
const connection = connections.Items[0] | ||
const connectionId = connection.connectionId.S! | ||
|
||
// Create API Gateway Management API client | ||
const endpoint = new URL(process.env.WEBSOCKET_ENDPOINT!) | ||
const apiGateway = new ApiGatewayManagementApiClient({ | ||
endpoint: `https://${endpoint.host}`, | ||
}) | ||
|
||
// Forward request to WebSocket client | ||
const message = { | ||
action: 'request', | ||
connectionId, | ||
data: { | ||
method: event.requestContext.http.method, | ||
path: event.rawPath, | ||
headers: event.headers, | ||
queryStringParameters: event.queryStringParameters, | ||
body: event.body, | ||
}, | ||
} | ||
|
||
try { | ||
await apiGateway.send(new PostToConnectionCommand({ | ||
ConnectionId: connectionId, | ||
Data: Buffer.from(JSON.stringify(message)), | ||
})) | ||
|
||
// Wait for response (in a real implementation, you'd use a response queue or callback) | ||
await new Promise(resolve => setTimeout(resolve, 1000)) | ||
|
||
return { | ||
statusCode: 200, | ||
body: 'Request forwarded', | ||
} | ||
} | ||
catch (error) { | ||
console.error('WebSocket error:', error) | ||
return { | ||
statusCode: 502, | ||
body: 'Failed to forward request', | ||
} | ||
} | ||
} | ||
catch (error) { | ||
console.error('Request handling error:', error) | ||
return { | ||
statusCode: 500, | ||
body: 'Internal server error', | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import process from 'node:process' | ||
import * as cdk from 'aws-cdk-lib' | ||
import { TunnelStack } from './tunnel-stack' | ||
import 'source-map-support/register' | ||
|
||
const app = new cdk.App() | ||
|
||
new TunnelStack(app, 'TunnelStack', { | ||
env: { | ||
account: process.env.CDK_DEFAULT_ACCOUNT, | ||
region: process.env.CDK_DEFAULT_REGION, | ||
}, | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
import type { Construct } from 'constructs' | ||
import * as path from 'node:path' | ||
import * as cdk from 'aws-cdk-lib' | ||
import * as apigatewayv2 from 'aws-cdk-lib/aws-apigatewayv2' | ||
import * as integrations from 'aws-cdk-lib/aws-apigatewayv2-integrations' | ||
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb' | ||
import * as iam from 'aws-cdk-lib/aws-iam' | ||
import * as lambda from 'aws-cdk-lib/aws-lambda' | ||
import * as nodeLambda from 'aws-cdk-lib/aws-lambda-nodejs' | ||
|
||
export class TunnelStack extends cdk.Stack { | ||
constructor(scope: Construct, id: string, props?: cdk.StackProps) { | ||
super(scope, id, props) | ||
|
||
// DynamoDB table for connection tracking | ||
const connectionsTable = new dynamodb.Table(this, 'TunnelConnections', { | ||
partitionKey: { | ||
name: 'connectionId', | ||
type: dynamodb.AttributeType.STRING, | ||
}, | ||
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, | ||
removalPolicy: cdk.RemovalPolicy.DESTROY, // For development - change for production | ||
}) | ||
|
||
// Add GSI for subdomain lookup | ||
connectionsTable.addGlobalSecondaryIndex({ | ||
indexName: 'subdomain-index', | ||
partitionKey: { | ||
name: 'subdomain', | ||
type: dynamodb.AttributeType.STRING, | ||
}, | ||
projectionType: dynamodb.ProjectionType.ALL, | ||
}) | ||
|
||
// WebSocket API | ||
const webSocketApi = new apigatewayv2.WebSocketApi(this, 'TunnelWebSocketApi', { | ||
connectRouteOptions: { | ||
integration: new integrations.WebSocketLambdaIntegration('ConnectIntegration', new nodeLambda.NodejsFunction(this, 'ConnectHandler', { | ||
entry: path.join(__dirname, '../lambda/connect.ts'), | ||
handler: 'handler', | ||
runtime: lambda.Runtime.NODEJS_18_X, | ||
environment: { | ||
TABLE_NAME: connectionsTable.tableName, | ||
}, | ||
})), | ||
}, | ||
disconnectRouteOptions: { | ||
integration: new integrations.WebSocketLambdaIntegration('DisconnectIntegration', new nodeLambda.NodejsFunction(this, 'DisconnectHandler', { | ||
entry: path.join(__dirname, '../lambda/disconnect.ts'), | ||
handler: 'handler', | ||
runtime: lambda.Runtime.NODEJS_18_X, | ||
environment: { | ||
TABLE_NAME: connectionsTable.tableName, | ||
}, | ||
})), | ||
}, | ||
}) | ||
|
||
// WebSocket Stage | ||
const webSocketStage = new apigatewayv2.WebSocketStage(this, 'TunnelWebSocketStage', { | ||
webSocketApi, | ||
stageName: 'prod', | ||
autoDeploy: true, | ||
}) | ||
|
||
// HTTP API | ||
const httpApi = new apigatewayv2.HttpApi(this, 'TunnelHttpApi') | ||
|
||
// Lambda for handling HTTP requests | ||
const httpHandler = new nodeLambda.NodejsFunction(this, 'HttpHandler', { | ||
entry: path.join(__dirname, '../lambda/http.ts'), | ||
handler: 'handler', | ||
runtime: lambda.Runtime.NODEJS_18_X, | ||
environment: { | ||
TABLE_NAME: connectionsTable.tableName, | ||
WEBSOCKET_ENDPOINT: webSocketStage.url, | ||
}, | ||
timeout: cdk.Duration.seconds(30), | ||
}) | ||
|
||
// Add route to HTTP API | ||
httpApi.addRoutes({ | ||
path: '/{proxy+}', | ||
methods: [apigatewayv2.HttpMethod.ANY], | ||
integration: new integrations.HttpLambdaIntegration('HttpIntegration', httpHandler), | ||
}) | ||
|
||
// Grant DynamoDB permissions | ||
connectionsTable.grantReadWriteData(httpHandler) | ||
|
||
// Grant permissions to manage WebSocket connections | ||
httpHandler.addToRolePolicy(new iam.PolicyStatement({ | ||
actions: ['execute-api:ManageConnections'], | ||
resources: [ | ||
`arn:aws:execute-api:${this.region}:${this.account}:${webSocketApi.apiId}/${webSocketStage.stageName}/*`, | ||
], | ||
})) | ||
|
||
// Outputs | ||
// eslint-disable-next-line no-new | ||
new cdk.CfnOutput(this, 'WebSocketApiUrl', { | ||
value: webSocketStage.url, | ||
description: 'WebSocket API URL', | ||
}) | ||
|
||
// eslint-disable-next-line no-new | ||
new cdk.CfnOutput(this, 'HttpApiUrl', { | ||
value: httpApi.url!, | ||
description: 'HTTP API URL', | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.