Skip to content

Commit

Permalink
feat: sanitise the domain name before launching the tui (#90)
Browse files Browse the repository at this point in the history
  • Loading branch information
purpleclay authored Sep 14, 2022
1 parent f7b7d85 commit 6aa4363
Show file tree
Hide file tree
Showing 10 changed files with 177 additions and 126 deletions.
36 changes: 8 additions & 28 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,38 +41,20 @@ jobs:
with:
fetch-depth: 0

- name: Set up Python runtime
uses: actions/setup-python@v4
- name: GHCR Login
uses: docker/login-action@v2
with:
python-version: 3.x
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GH_GHCR }}

- name: Set up build cache
uses: actions/cache@v3
id: cache
with:
key: ${{ runner.os }}-${{ hashFiles('.cache/**') }}
path: .cache

- name: Install Python dependencies
run: |
pip install \
"mkdocs-git-committers-plugin-2>=0.4.3" \
"mkdocs-git-revision-date-localized-plugin>=1.0" \
"mkdocs-minify-plugin>=0.3" \
"mkdocs-redirects>=1.0"
- name: Install MkDocs Insiders
if: github.event.repository.fork == false
env:
GH_TOKEN: ${{ secrets.GH_MKDOCS }}
run: |
pip install git+https://${GH_TOKEN}@github.com/squidfunk/mkdocs-material-insiders.git
- run: docker pull ghcr.io/purpleclay/mkdocs-material-insiders

- name: Build
run: mkdocs build
env:
GH_TOKEN: ${{ secrets.GH_MKDOCS }}
GH_GOOGLE_ANALYTICS_KEY: ${{ secrets.GH_GOOGLE_ANALYTICS_KEY }}
run: docker run --rm -i -e GH_TOKEN=${GH_TOKEN} -e GH_GOOGLE_ANALYTICS_KEY=${GH_GOOGLE_ANALYTICS_KEY} -v ${PWD}:/docs ghcr.io/purpleclay/mkdocs-material-insiders build

- name: HTML Test
uses: wjdp/htmltest-action@master
Expand All @@ -94,6 +76,4 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GH_MKDOCS }}
GH_GOOGLE_ANALYTICS_KEY: ${{ secrets.GH_GOOGLE_ANALYTICS_KEY }}
run: |
mkdocs gh-deploy --force
mkdocs --version
run: docker run --rm -i -e GH_TOKEN=${GH_TOKEN} -e GH_GOOGLE_ANALYTICS_KEY=${GH_GOOGLE_ANALYTICS_KEY} -v ${PWD}:/docs ghcr.io/purpleclay/mkdocs-material-insiders gh-deploy --force
3 changes: 0 additions & 3 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ run:
linters:
disable-all: true
enable:
- deadcode
- dupl
- gofumpt
- goimports
Expand All @@ -15,9 +14,7 @@ linters:
- nakedret
- revive
- staticcheck
- structcheck
- unused
- varcheck

linters-settings:
dupl:
Expand Down
2 changes: 2 additions & 0 deletions .sonarcloud.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Exclude tests from copy-paste detection as SonarCloud doesn't appear to like table driven tests
sonar.cpd.exclusions=**/*_test.go
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# dns53

Dynamic DNS within Amazon Route53. Expose your EC2 quickly, easily and privately.
Dynamic DNS within Amazon Route53. Expose your EC2 quickly, easily, and privately.

[![Build status](https://img.shields.io/github/workflow/status/purpleclay/dns53/ci?style=flat-square&logo=go)](https://github.com/purpleclay/dns53/actions?workflow=ci)
[![License MIT](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](/LICENSE)
Expand Down
81 changes: 59 additions & 22 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,20 @@ SOFTWARE.
package cmd

import (
"bytes"
"context"
"errors"
"io"
"regexp"
"strings"
"text/template"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
awsimds "github.com/aws/aws-sdk-go-v2/feature/ec2/imds"
awsr53 "github.com/aws/aws-sdk-go-v2/service/route53"
tea "github.com/charmbracelet/bubbletea"
"github.com/gobeam/stringy"
"github.com/purpleclay/dns53/internal/imds"
"github.com/purpleclay/dns53/internal/r53"
"github.com/purpleclay/dns53/internal/tui"
Expand Down Expand Up @@ -66,7 +70,11 @@ type globalOptions struct {
AWSProfile string
}

var globalOpts = &globalOptions{}
var (
globalOpts = &globalOptions{}

domainRegex = regexp.MustCompile("[^a-zA-Z0-9-.]+")
)

type options struct {
phzID string
Expand All @@ -76,6 +84,10 @@ type options struct {
func Execute(out io.Writer) error {
opts := options{}

// Capture in PreRun lifecycle
var cfg aws.Config
var metadata imds.Metadata

rootCmd := &cobra.Command{
Use: "dns53",
Short: `Dynamic DNS within Amazon Route 53. Expose your EC2 quickly, easily and privately within a Route
Expand All @@ -84,31 +96,34 @@ func Execute(out io.Writer) error {
Example: examples,
SilenceUsage: true,
SilenceErrors: true,
RunE: func(cmd *cobra.Command, args []string) error {
cfg, err := awsConfig(globalOpts)
PreRunE: func(cmd *cobra.Command, args []string) error {
var err error

cfg, err = awsConfig(globalOpts)
if err != nil {
return err
}

// If a custom domain name has been provided, check that it can be resolved from IMDS
imdsClient := imds.NewFromAPI(awsimds.NewFromConfig(cfg))
if metadata, err = imdsClient.InstanceMetadata(context.Background()); err != nil {
return err
}

if opts.domainName != "" {
if err := domainNameSupported(opts.domainName, imdsClient); err != nil {
return err
}
if opts.domainName == "" {
return nil
}

model, err := tui.Dashboard(tui.DashboardOptions{
opts.domainName, err = resolveDomainName(opts.domainName, metadata)
return err
},
RunE: func(cmd *cobra.Command, args []string) error {
model := tui.Dashboard(tui.DashboardOptions{
R53Client: r53.NewFromAPI(awsr53.NewFromConfig(cfg)),
IMDSClient: imdsClient,
Metadata: metadata,
Version: version,
PhzID: opts.phzID,
DomainName: opts.domainName,
})
if err != nil {
return err
}

return tea.NewProgram(model, tea.WithAltScreen()).Start()
},
Expand Down Expand Up @@ -143,23 +158,45 @@ func awsConfig(opts *globalOptions) (aws.Config, error) {
return config.LoadDefaultConfig(context.Background(), optsFn...)
}

func domainNameSupported(domain string, imdsClient *imds.Client) error {
func resolveDomainName(domain string, metadata imds.Metadata) (string, error) {
dmn := strings.ReplaceAll(domain, " ", "")
if strings.Contains(dmn, "{{.Name}}") {
metadata, err := imdsClient.InstanceMetadata(context.Background())
if err != nil {
return err
}

if strings.Contains(dmn, "{{.Name}}") {
if metadata.Name == "" {
return errors.New(`to use metadata within a custom domain name, please enable IMDS instance tags support
return "", errors.New(`to use metadata within a custom domain name, please enable IMDS instance tags support
for your EC2 instance:
$ dns53 imds --instance-metadata-tags on
Or read the official AWS documentation at:
Or read the official AWS documentation at:
https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Using_Tags.html#allow-access-to-tags-in-IMDS`)
}

name := stringy.New(metadata.Name)
metadata.Name = name.KebabCase().ToLower()
}

// Sanitise the copy of the metadata before resolving the template
metadata.IPv4 = strings.ReplaceAll(metadata.IPv4, ".", "-")

// Execute the domain template
tmpl, err := template.New("domain").Parse(domain)
if err != nil {
return "", err
}

var out bytes.Buffer
if err := tmpl.Execute(&out, metadata); err != nil {
return "", err
}
return nil
dmn = out.String()

// Final tidy up of the domain
dmn = strings.ReplaceAll(dmn, "--", "-")
dmn = strings.ReplaceAll(dmn, "..", ".")
dmn = strings.Trim(dmn, "-")
dmn = strings.Trim(dmn, ".")
dmn = domainRegex.ReplaceAllString(dmn, "")

return dmn, nil
}
89 changes: 74 additions & 15 deletions cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import (
"testing"

"github.com/purpleclay/dns53/internal/imds"
"github.com/purpleclay/dns53/internal/imds/imdsstub"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand All @@ -41,41 +40,101 @@ func TestAWSConfig(t *testing.T) {
assert.Equal(t, "eu-west-2", cfg.Region)
}

func TestDomainNameSupported(t *testing.T) {
func TestResolveDomainName(t *testing.T) {
metadata := imds.Metadata{
Name: "my-ec2",
}

tests := []struct {
name string
domain string
name string
domain string
expected string
}{
{
name: "NoTemplating",
domain: "custom.domain",
name: "NoTemplating",
domain: "custom.domain",
expected: "custom.domain",
},
{
name: "WithNameField",
domain: "custom.{{.Name}}",
expected: "custom.my-ec2",
},
{
name: "WithNameField",
domain: "custom.{{.Name}}",
name: "WithNameFieldSpaces",
domain: "custom.{{ .Name }}",
expected: "custom.my-ec2",
},
{
name: "WithNameFieldSpaces",
domain: "custom.{{ .Name }}",
name: "ReplacesDoubleHyphens",
domain: "another--custom.domain",
expected: "another-custom.domain",
},
{
name: "ReplacesDoubleDots",
domain: "my-custom123..domain",
expected: "my-custom123.domain",
},
{
name: "RemoveLeadingTrailingHyphen",
domain: "-this-is-a-custom.domain-",
expected: "this-is-a-custom.domain",
},
{
name: "RemoveLeadingTrailingDot",
domain: ".a-custom.domain.",
expected: "a-custom.domain",
},
{
name: "TrimUnsupportedCharacters",
domain: "custom@#.doma**in-123",
expected: "custom.domain-123",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := domainNameSupported(tt.domain, imds.NewFromAPI(imdsstub.New(t)))
domain, err := resolveDomainName(tt.domain, metadata)

require.NoError(t, err)
require.Equal(t, tt.expected, domain)
})
}
}

func TestDomainNameSupportedNoInstanceTags(t *testing.T) {
err := domainNameSupported("custom.{{.Name}}", imds.NewFromAPI(imdsstub.NewWithoutTags(t)))
func TestResolveDomainNameNoInstanceTags(t *testing.T) {
_, err := resolveDomainName("custom.{{.Name}}", imds.Metadata{})

assert.EqualError(t, err, `to use metadata within a custom domain name, please enable IMDS instance tags support
assert.EqualError(t, err, `to use metadata within a custom domain name, please enable IMDS instance tags support
for your EC2 instance:
$ dns53 imds --instance-metadata-tags on
Or read the official AWS documentation at:
Or read the official AWS documentation at:
https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Using_Tags.html#allow-access-to-tags-in-IMDS`)
}

func TestResolveDomainNameTransformsNameTagToKebabCase(t *testing.T) {
domain, err := resolveDomainName("first.custom.{{.Name}}", imds.Metadata{Name: "MyEc2 123"})

require.NoError(t, err)
assert.Equal(t, "first.custom.my-ec2-123", domain)
}

func TestResolveDomainNameStripsLeadingTrailingHyphenFromNameTag(t *testing.T) {
domain, err := resolveDomainName("second.custom.{{.Name}}", imds.Metadata{Name: "-MyEc2 123-"})

require.NoError(t, err)
assert.Equal(t, "second.custom.my-ec2-123", domain)
}

func TestResolveDomainNameInvalidGoTemplate(t *testing.T) {
_, err := resolveDomainName("custom.{{.Name}", imds.Metadata{Name: "MyEc2 123"})

assert.Error(t, err)
}

func TestResolveDomainNameUnrecognisedTemplateFields(t *testing.T) {
_, err := resolveDomainName("custom.{{.Unknown}}", imds.Metadata{})

assert.Error(t, err)
}
13 changes: 13 additions & 0 deletions docs/configure/custom-domain.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,16 @@ A templated domain leverages the text templating capabilities of the Go language
```sh
dns53 --domain-name "{{.IPv4}}.{{.Region}}"
```

## Domain Validation

A custom domain must be valid before assigning it to your EC2 instance. A series of checks must pass.

A domain must:

- not contain leading or trailing hyphens (`-`) and dots (`.`)
- not contain consecutive hyphens (`--`) or dots (`..`)
- not contain whitespace (` `)
- only contain valid characters from the sequence `[A-Za-z0-9-.]`

`dns53` will automatically clean any domain name in an attempt to enforce these validation checks.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ require (
github.com/charmbracelet/bubbles v0.14.0
github.com/charmbracelet/bubbletea v0.22.1
github.com/charmbracelet/lipgloss v0.6.0
github.com/gobeam/stringy v0.0.5
github.com/muesli/mango-cobra v1.2.0
github.com/muesli/roff v0.1.0
github.com/purpleclay/testcontainers-imds v0.7.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,8 @@ github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8w
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gobeam/stringy v0.0.5 h1:TvxQGSAqr/qF0SBVxa8Q67WWIo7bCWS0bM101WOd52g=
github.com/gobeam/stringy v0.0.5/go.mod h1:W3620X9dJHf2FSZF5fRnWekHcHQjwmCz8ZQ2d1qloqE=
github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM=
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
Expand Down
Loading

0 comments on commit 6aa4363

Please sign in to comment.