diff --git a/presigner/presign.go b/presigner/presign.go new file mode 100644 index 00000000..7053189b --- /dev/null +++ b/presigner/presign.go @@ -0,0 +1,95 @@ +// Copyright 2018 SEQSENSE, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* +Package presigner implements AWS v4 presigner wrapper for AWS IoT websocket connection. +This presigner wrapper works around the AWS IoT websocket's problem in presigned URL with SESSION_TOKEN. +*/ +package presigner + +import ( + "bytes" + "errors" + "fmt" + "net/http" + "net/url" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/client" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/signer/v4" +) + +// Presigner is an AWS v4 signer wrapper for AWS IoT. +type Presigner struct { + clientConfig client.Config +} + +const serviceName = "iotdevicegateway" + +// New returns new AWS v4 signer wrapper for AWS IoT. +func New(p client.ConfigProvider, cfgs ...*aws.Config) *Presigner { + return &Presigner{ + clientConfig: p.ClientConfig(serviceName, cfgs...), + } +} + +// PresignWssNow generates presigned AWS IoT websocket URL for specified endpoint hostname. +// The URL is valid from now until 24 hours later which is the limit of AWS IoT Websocket connection. +func (a *Presigner) PresignWssNow(endpoint string) (string, error) { + return a.PresignWss(endpoint, time.Hour*24, time.Now()) +} + +// PresignWss generates presigned AWS IoT websocket URL for specified endpoint hostname. +func (a *Presigner) PresignWss(endpoint string, expire time.Duration, from time.Time) (string, error) { + if a.clientConfig.SigningRegion == "" { + return "", errors.New("Region is not specified") + } + cred, err := a.clientConfig.Config.Credentials.Get() + if err != nil { + return "", err + } + sessionToken := cred.SessionToken + + signer := v4.NewSigner( + credentials.NewStaticCredentials(cred.AccessKeyID, cred.SecretAccessKey, ""), + ) + + body := bytes.NewReader([]byte{}) + + originalURL, err := url.Parse(fmt.Sprintf("wss://%s/mqtt", endpoint)) + if err != nil { + return "", err + } + + req := &http.Request{ + Method: "GET", + URL: originalURL, + } + _, err = signer.Presign( + req, body, + a.clientConfig.SigningName, a.clientConfig.SigningRegion, + expire, from, + ) + if err != nil { + return "", err + } + + ret := req.URL.String() + if sessionToken != "" { + ret = ret + "&X-Amz-Security-Token=" + url.QueryEscape(sessionToken) + } + return ret, nil +} diff --git a/presigner/presign_test.go b/presigner/presign_test.go new file mode 100644 index 00000000..06055857 --- /dev/null +++ b/presigner/presign_test.go @@ -0,0 +1,77 @@ +// Copyright 2018 SEQSENSE, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package presigner + +import ( + "fmt" + "os" + "testing" + "time" + + "github.com/aws/aws-sdk-go/aws/session" +) + +func TestPresignWss(t *testing.T) { + os.Clearenv() + os.Setenv("AWS_ACCESS_KEY_ID", "AKAAAAAAAAAAAAAAAAAA") + os.Setenv("AWS_SECRET_ACCESS_KEY", "1111111111111111111111111111111111111111") + os.Setenv("AWS_SESSION_TOKEN", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + os.Setenv("AWS_REGION", "world-1") + + const expected = "wss://test.iot.world-1.amazonaws.com/mqtt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKAAAAAAAAAAAAAAAAAA%2F19700101%2Fworld-1%2Fiotdevicegateway%2Faws4_request&X-Amz-Date=19700101T000000Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host&X-Amz-Signature=4cfbc8acc899f7aac3153cd17c94204d6989f86d8cb1173e46143512270c89c2&X-Amz-Security-Token=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + sess := session.Must(session.NewSession()) + ps := New(sess) + wssURL, err := ps.PresignWss("test.iot.world-1.amazonaws.com", time.Hour*24, time.Unix(0, 0)) + if err != nil { + t.Error(err) + } + + if wssURL != expected { + t.Errorf("Presigned URL is wrong if session token is provided.\nactual: %s\nexpected: %s", + wssURL, expected) + } +} + +func TestPresignWss_WithoutSessionToken(t *testing.T) { + os.Clearenv() + os.Setenv("AWS_ACCESS_KEY_ID", "AKAAAAAAAAAAAAAAAAAA") + os.Setenv("AWS_SECRET_ACCESS_KEY", "1111111111111111111111111111111111111111") + os.Setenv("AWS_REGION", "world-1") + + const expected = "wss://test.iot.world-1.amazonaws.com/mqtt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKAAAAAAAAAAAAAAAAAA%2F19700101%2Fworld-1%2Fiotdevicegateway%2Faws4_request&X-Amz-Date=19700101T000000Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host&X-Amz-Signature=4cfbc8acc899f7aac3153cd17c94204d6989f86d8cb1173e46143512270c89c2" + + sess := session.Must(session.NewSession()) + ps := New(sess) + wssURL, err := ps.PresignWss("test.iot.world-1.amazonaws.com", time.Hour*24, time.Unix(0, 0)) + if err != nil { + t.Error(err) + } + + if wssURL != expected { + t.Errorf("Presigned URL is wrong if session token is provided.\nactual: %s\nexpected: %s", + wssURL, expected) + } +} + +func ExamplePresigner_PresignWssNow() { + sess := session.Must(session.NewSession()) + ps := New(sess) + wssURL, err := ps.PresignWssNow("test.iot.world-1.amazonaws.com") + if err != nil { + panic(err) + } + fmt.Printf("%s\n", wssURL) +}