Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle disconnect route of Websocket #548

Merged
merged 5 commits into from
Apr 16, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
88 changes: 45 additions & 43 deletions events/apigw.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,63 +133,65 @@ type APIGatewayV2HTTPResponse struct {

// APIGatewayRequestIdentity contains identity information for the request caller.
type APIGatewayRequestIdentity struct {
CognitoIdentityPoolID string `json:"cognitoIdentityPoolId"`
AccountID string `json:"accountId"`
CognitoIdentityID string `json:"cognitoIdentityId"`
Caller string `json:"caller"`
APIKey string `json:"apiKey"`
APIKeyID string `json:"apiKeyId"`
AccessKey string `json:"accessKey"`
CognitoIdentityPoolID string `json:"cognitoIdentityPoolId,omitempty"`
AccountID string `json:"accountId,omitempty"`
CognitoIdentityID string `json:"cognitoIdentityId,omitempty"`
Caller string `json:"caller,omitempty"`
APIKey string `json:"apiKey,omitempty"`
APIKeyID string `json:"apiKeyId,omitempty"`
AccessKey string `json:"accessKey,omitempty"`
SourceIP string `json:"sourceIp"`
CognitoAuthenticationType string `json:"cognitoAuthenticationType"`
CognitoAuthenticationProvider string `json:"cognitoAuthenticationProvider"`
UserArn string `json:"userArn"` //nolint: stylecheck
CognitoAuthenticationType string `json:"cognitoAuthenticationType,omitempty"`
CognitoAuthenticationProvider string `json:"cognitoAuthenticationProvider,omitempty"`
UserArn string `json:"userArn,omitempty"` //nolint: stylecheck
UserAgent string `json:"userAgent"`
User string `json:"user"`
User string `json:"user,omitempty"`
}

// APIGatewayWebsocketProxyRequest contains data coming from the API Gateway proxy
type APIGatewayWebsocketProxyRequest struct {
Resource string `json:"resource"` // The resource path defined in API Gateway
Path string `json:"path"` // The url path for the caller
Resource string `json:"resource,omitempty"` // The resource path defined in API Gateway
Path string `json:"path,omitempty"` // The url path for the caller
HTTPMethod string `json:"httpMethod,omitempty"`
Headers map[string]string `json:"headers"`
MultiValueHeaders map[string][]string `json:"multiValueHeaders"`
QueryStringParameters map[string]string `json:"queryStringParameters"`
MultiValueQueryStringParameters map[string][]string `json:"multiValueQueryStringParameters"`
PathParameters map[string]string `json:"pathParameters"`
StageVariables map[string]string `json:"stageVariables"`
Headers map[string]string `json:"headers,omitempty"`
MultiValueHeaders map[string][]string `json:"multiValueHeaders,omitempty"`
QueryStringParameters map[string]string `json:"queryStringParameters,omitempty"`
MultiValueQueryStringParameters map[string][]string `json:"multiValueQueryStringParameters,omitempty"`
PathParameters map[string]string `json:"pathParameters,omitempty"`
StageVariables map[string]string `json:"stageVariables,omitempty"`
RequestContext APIGatewayWebsocketProxyRequestContext `json:"requestContext"`
Body string `json:"body"`
IsBase64Encoded bool `json:"isBase64Encoded,omitempty"`
Body string `json:"body,omitempty"`
IsBase64Encoded bool `json:"isBase64Encoded"`
}

// APIGatewayWebsocketProxyRequestContext contains the information to identify
// the AWS account and resources invoking the Lambda function. It also includes
// Cognito identity information for the caller.
type APIGatewayWebsocketProxyRequestContext struct {
AccountID string `json:"accountId"`
ResourceID string `json:"resourceId"`
Stage string `json:"stage"`
RequestID string `json:"requestId"`
Identity APIGatewayRequestIdentity `json:"identity"`
ResourcePath string `json:"resourcePath"`
Authorizer interface{} `json:"authorizer"`
HTTPMethod string `json:"httpMethod"`
APIID string `json:"apiId"` // The API Gateway rest API Id
ConnectedAt int64 `json:"connectedAt"`
ConnectionID string `json:"connectionId"`
DomainName string `json:"domainName"`
Error string `json:"error"`
EventType string `json:"eventType"`
ExtendedRequestID string `json:"extendedRequestId"`
IntegrationLatency string `json:"integrationLatency"`
MessageDirection string `json:"messageDirection"`
MessageID interface{} `json:"messageId"`
RequestTime string `json:"requestTime"`
RequestTimeEpoch int64 `json:"requestTimeEpoch"`
RouteKey string `json:"routeKey"`
Status string `json:"status"`
AccountID string `json:"accountId,omitempty"`
ResourceID string `json:"resourceId,omitempty"`
Stage string `json:"stage"`
RequestID string `json:"requestId"`
Identity APIGatewayRequestIdentity `json:"identity"`
ResourcePath string `json:"resourcePath,omitempty"`
Authorizer interface{} `json:"authorizer,omitempty"`
HTTPMethod string `json:"httpMethod,omitempty"`
APIID string `json:"apiId"` // The API Gateway rest API Id
ConnectedAt int64 `json:"connectedAt"`
ConnectionID string `json:"connectionId"`
DomainName string `json:"domainName"`
Error string `json:"error,omitempty"`
EventType string `json:"eventType"`
ExtendedRequestID string `json:"extendedRequestId"`
IntegrationLatency string `json:"integrationLatency,omitempty"`
MessageDirection string `json:"messageDirection"`
MessageID string `json:"messageId,omitempty"`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MessageID needs to stay as interface{} for back-compat

RequestTime string `json:"requestTime"`
RequestTimeEpoch int64 `json:"requestTimeEpoch"`
RouteKey string `json:"routeKey"`
Status string `json:"status,omitempty"`
DisconnectStatusCode int64 `json:"disconnectStatusCode,omitempty"`
DisconnectReason *string `json:"disconnectReason,omitempty"`
Comment on lines +193 to +194
Copy link
Collaborator

@bmoffatt bmoffatt Jan 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't find a reference to these fields in the AWS Docs, nor in the event libraries for .NET or Java.

Do you happen to have a reference handy? My search skills are failing me right now.

Copy link

@tinhda tinhda Jan 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, I found this sample online, you can also recreate this by looking into the websocket api gateway log in cloudwatch. I couldn't find anything in the aws documents neither.

    Host: '3xcls223mg.execute-api.eu-west-1.amazonaws.com',
    'x-api-key': '',
    'X-Forwarded-For': '',
    'x-restapi': ''
  },
  multiValueHeaders: {
    Host: [ '3xcls223mg.execute-api.eu-west-1.amazonaws.com' ],
    'x-api-key': [ '' ],
    'X-Forwarded-For': [ '' ],
    'x-restapi': [ '' ]
  },
  requestContext: {
    routeKey: '$disconnect',
    disconnectStatusCode: 1006,
    eventType: 'DISCONNECT',
    extendedRequestId: 'fvLJ9EMQDoEFXSw=',
    requestTime: '22/May/2021:15:38:26 +0000',
    messageDirection: 'IN',
    disconnectReason: 'Connection Closed Abnormally',
    stage: 'updatedevice',
    connectedAt: 1621697605538,
    requestTimeEpoch: 1621697906882,
    identity: {
      userAgent: 'arduino-WebSocket-Client',
      sourceIp: '137.97.106.23'
    },
    requestId: 'fvLJ9EMQDoEFXSw=',
    domainName: '3xcls223mg.execute-api.eu-west-1.amazonaws.com',
    connectionId: 'fvKa4d88joECFDw=',
    apiId: '3xcls223mg'
  },
  isBase64Encoded: false
}

Link to the sample: Links2004/arduinoWebSockets#667

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also found here: boto/boto3#3789

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately I'll need a reference that comes from docs.aws.amazon.com

I'll otherwise make time to walk myself through the developer guide so that I can reproduce the test payloads in this PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there anything else I can do?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's perfect! Thanks for following up!

}

// APIGatewayCustomAuthorizerRequestTypeRequestIdentity contains identity information for the request caller including certificate information if using mTLS.
Expand Down
46 changes: 46 additions & 0 deletions events/apigw_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,52 @@ func TestApiGatewayWebsocketRequestMarshaling(t *testing.T) {
assert.JSONEq(t, string(inputJSON), string(outputJSON))
}

func TestApiGatewayWebsocketRequestSendMessageMarshaling(t *testing.T) {

// read json from file
inputJSON, err := ioutil.ReadFile("./testdata/apigw-websocket-request-send-message.json")
if err != nil {
t.Errorf("could not open test file. details: %v", err)
}

// de-serialize into Go object
var inputEvent APIGatewayWebsocketProxyRequest
if err := json.Unmarshal(inputJSON, &inputEvent); err != nil {
t.Errorf("could not unmarshal event. details: %v", err)
}

// serialize to json
outputJSON, err := json.Marshal(inputEvent)
if err != nil {
t.Errorf("could not marshal event. details: %v", err)
}

assert.JSONEq(t, string(inputJSON), string(outputJSON))
}

func TestApiGatewayWebsocketRequestDisconnectMarshaling(t *testing.T) {

// read json from file
inputJSON, err := ioutil.ReadFile("./testdata/apigw-websocket-request-disconnect.json")
if err != nil {
t.Errorf("could not open test file. details: %v", err)
}

// de-serialize into Go object
var inputEvent APIGatewayWebsocketProxyRequest
if err := json.Unmarshal(inputJSON, &inputEvent); err != nil {
t.Errorf("could not unmarshal event. details: %v", err)
}

// serialize to json
outputJSON, err := json.Marshal(inputEvent)
if err != nil {
t.Errorf("could not marshal event. details: %v", err)
}

assert.JSONEq(t, string(inputJSON), string(outputJSON))
}

func TestApiGatewayWebsocketRequestMalformedJson(t *testing.T) {
test.TestMalformedJson(t, APIGatewayWebsocketProxyRequest{})
}
Expand Down
35 changes: 35 additions & 0 deletions events/testdata/apigw-websocket-request-disconnect.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"headers": {
"Host": "dl7ptocha9.execute-api.ap-northeast-1.amazonaws.com",
"x-api-key": "",
"X-Forwarded-For": "",
"x-restapi": ""
},
"multiValueHeaders": {
"Host": [ "dl7ptocha9.execute-api.ap-northeast-1.amazonaws.com" ],
"x-api-key": [ "" ],
"X-Forwarded-For": [ "" ],
"x-restapi": [ "" ]
},
"requestContext": {
"routeKey": "$disconnect",
"disconnectStatusCode": 1001,
"eventType": "DISCONNECT",
"extendedRequestId": "R1koeHsUtjMFbRw=",
"requestTime": "20/Jan/2024:11:55:08 +0000",
"messageDirection": "IN",
"disconnectReason": "",
"stage": "prod",
"connectedAt": 1705751697419,
"requestTimeEpoch": 1705751708326,
"identity": {
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36",
"sourceIp": "49.105.91.154"
},
"requestId": "R1koeHsUtjMFbRw=",
"domainName": "dl7ptocha9.execute-api.ap-northeast-1.amazonaws.com",
"connectionId": "R1kmxc2VNjMCFIA=",
"apiId": "dl7ptocha9"
},
"isBase64Encoded": false
}
23 changes: 23 additions & 0 deletions events/testdata/apigw-websocket-request-send-message.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"requestContext": {
"routeKey": "$default",
"messageId": "R1knPc2ntjMCFIA=",
"eventType": "MESSAGE",
"extendedRequestId": "R1knPH17NjMFftw=",
"requestTime": "20/Jan/2024:11:55:00 +0000",
"messageDirection": "IN",
"stage": "prod",
"connectedAt": 1705751697419,
"requestTimeEpoch": 1705751700453,
"identity": {
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36",
"sourceIp": "49.105.91.154"
},
"requestId": "R1knPH17NjMFftw=",
"domainName": "dl7ptocha9.execute-api.ap-northeast-1.amazonaws.com",
"connectionId": "R1kmxc2VNjMCFIA=",
"apiId": "gy415nuibc"
},
"body": "{\r\n\t\"a\": 1\r\n}",
"isBase64Encoded": false
}
4 changes: 2 additions & 2 deletions events/testdata/apigw-websocket-request.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,11 @@
"extendedRequestId": "TWegAcC4EowCHnA=",
"integrationLatency": "123",
"messageDirection": "IN",
"messageId": null,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"messageId": null should remain in the testdata

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"messageId" field is not documented for CONNECT eventType.

https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api-integration-requests.html#api-gateway-simple-proxy-for-lambda-input-format-websocket

And I couldn't find out a way to keep this field here in the testdata and also to keep the MessageID field of the APIGatewayWebsocketProxyRequestContext struct as interface{}.
If I can remove this field from the testdata, the MessageID field can stay as interface{}.

What should I do?

"requestTime": "07/Jan/2019:09:20:57 +0000",
"requestTimeEpoch": 0,
"routeKey": "$connect",
"status": "*"
},
"body": "{\r\n\t\"a\": 1\r\n}"
"body": "{\r\n\t\"a\": 1\r\n}",
"isBase64Encoded": false
}
Loading