Skip to content

Commit

Permalink
Merge pull request #23 from seqsense/add-iot-wss-presigner
Browse files Browse the repository at this point in the history
Add AWS IoT websocket URL presigner
  • Loading branch information
at-wat authored Nov 9, 2018
2 parents 5f2b96d + 913b3a2 commit 6dd710d
Show file tree
Hide file tree
Showing 2 changed files with 172 additions and 0 deletions.
95 changes: 95 additions & 0 deletions presigner/presign.go
Original file line number Diff line number Diff line change
@@ -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
}
77 changes: 77 additions & 0 deletions presigner/presign_test.go
Original file line number Diff line number Diff line change
@@ -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)
}

0 comments on commit 6dd710d

Please sign in to comment.