Skip to content

Commit a30651a

Browse files
committed
implement gateway customization by allowing content-type allow and block lists, code cleanup, some refactoring
1 parent e9096eb commit a30651a

8 files changed

Lines changed: 204 additions & 21 deletions

File tree

README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,31 @@ tasks:
125125
platforms: ['linux', 'darwin'] # overrides the default
126126
```
127127
128+
#### Configuration: Settings: Gateway
129+
130+
The `gateway` field of the `settings` section of the configuration file is used to customize the behavior of the http request gateway when accessing remote files or urls. Both `blocked` and `allowed` content types can be specified as exact strings or wildcards, but both fields are optional.
131+
132+
The default value for `blocked` is to not block any content types, and the default value for `allowed` is to allow all content types.
133+
134+
```yaml
135+
settings:
136+
gateway:
137+
content-types:
138+
blocked:
139+
- audio/*
140+
- font/*
141+
- image/*
142+
- multipart/*
143+
- video/*
144+
allowed:
145+
- application/json
146+
- application/vnd.api+json
147+
- application/vnd.github.*
148+
- application/xml
149+
- application/yaml
150+
- text/*
151+
```
152+
128153
#### Configuration: Settings: Domains
129154

130155
The `domains` section of the configuration file is used to specify a list of domain names that can be accessed when downloading remote files

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ require (
5151
github.com/posthog/posthog-go v0.0.0-20230801140217-d607812dee69
5252
github.com/rs/zerolog v1.30.0
5353
github.com/ryanuber/go-glob v1.0.0
54+
github.com/stoewer/go-strcase v1.3.0
5455
go.etcd.io/bbolt v1.3.7
5556
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
5657
)

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIH
9999
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
100100
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
101101
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
102+
github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs=
103+
github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
102104
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
103105
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
104106
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=

lib/app/workflow.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,14 @@ type WorkflowSettings struct {
4545
Cache *WorkflowSettingsCache `yaml:"cache"`
4646
Domains *WorkflowSettingsDomains `yaml:"domains"`
4747
AnonymousStatistics *bool `yaml:"anonymous-stats"`
48+
Gateway *WorkflowSettingsGateway `yaml:"gateway"`
49+
}
50+
type GatewayContentTypes struct {
51+
Blocked []string `yaml:"blocked"`
52+
Allowed []string `yaml:"allowed"`
53+
}
54+
type WorkflowSettingsGateway struct {
55+
ContentTypes *GatewayContentTypes `yaml:"content-types"`
4856
}
4957

5058
type WorkflowSettingsDomains struct {
@@ -200,6 +208,20 @@ func (workflow *StackupWorkflow) configureDefaultSettings() {
200208

201209
App.Gateway.SetAllowedDomains(workflow.Settings.Domains.Allowed)
202210

211+
if workflow.Settings.Gateway == nil {
212+
workflow.Settings.Gateway = &WorkflowSettingsGateway{
213+
ContentTypes: &GatewayContentTypes{
214+
Blocked: []string{},
215+
Allowed: []string{},
216+
},
217+
}
218+
}
219+
220+
if workflow.Settings.Gateway != nil && workflow.Settings.Gateway.ContentTypes != nil {
221+
App.Gateway.SetDomainContentTypes("*", workflow.Settings.Gateway.ContentTypes.Allowed)
222+
App.Gateway.SetBlockedContentTypes("*", workflow.Settings.Gateway.ContentTypes.Blocked)
223+
}
224+
203225
if workflow.Settings.Cache.TtlMinutes <= 0 {
204226
workflow.Settings.Cache.TtlMinutes = 5
205227
}
@@ -249,6 +271,12 @@ func (workflow *StackupWorkflow) createMissingSettingsSection() {
249271
Platforms: []string{"windows", "linux", "darwin"},
250272
},
251273
},
274+
Gateway: &WorkflowSettingsGateway{
275+
ContentTypes: &GatewayContentTypes{
276+
Blocked: []string{},
277+
Allowed: []string{"*"},
278+
},
279+
},
252280
}
253281
}
254282
}
@@ -390,6 +418,10 @@ func (workflow *StackupWorkflow) handleDataNotCached(found bool, data *cache.Cac
390418
return nil
391419
}
392420

421+
if err != nil {
422+
return err
423+
}
424+
393425
include.Hash = checksums.CalculateSha256Hash(include.Contents)
394426
expires := carbon.Now().AddMinutes(App.Workflow.Settings.Cache.TtlMinutes)
395427
now := carbon.Now()
@@ -451,8 +483,24 @@ func (workflow *StackupWorkflow) copySettingsFromIncludedTemplate(template *Incl
451483
if template.Settings.Defaults != nil {
452484
workflow.Settings.Defaults = template.Settings.Defaults
453485
}
486+
if workflow.Settings.Gateway != nil && workflow.Settings.Gateway.ContentTypes != nil {
487+
for _, contentType := range workflow.Settings.Gateway.ContentTypes.Blocked {
488+
workflow.Settings.Gateway.ContentTypes.Blocked = append(workflow.Settings.Gateway.ContentTypes.Blocked, contentType)
489+
}
490+
for _, contentType := range workflow.Settings.Gateway.ContentTypes.Allowed {
491+
workflow.Settings.Gateway.ContentTypes.Allowed = append(workflow.Settings.Gateway.ContentTypes.Allowed, contentType)
492+
}
493+
}
494+
if template.Settings.Gateway != nil {
495+
workflow.Settings.Gateway = template.Settings.Gateway
496+
}
454497
}
455498

456499
workflow.Settings.Domains.Allowed = utils.GetUniqueStrings(workflow.Settings.Domains.Allowed)
457500
App.Gateway.SetAllowedDomains(workflow.Settings.Domains.Allowed)
501+
502+
workflow.Settings.Gateway.ContentTypes.Allowed = utils.GetUniqueStrings(workflow.Settings.Gateway.ContentTypes.Allowed)
503+
workflow.Settings.Gateway.ContentTypes.Blocked = utils.GetUniqueStrings(workflow.Settings.Gateway.ContentTypes.Blocked)
504+
App.Gateway.SetDomainContentTypes("*", workflow.Settings.Gateway.ContentTypes.Allowed)
505+
App.Gateway.SetBlockedContentTypes("*", workflow.Settings.Gateway.ContentTypes.Blocked)
458506
}

lib/app/workflowInclude.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ func (include *WorkflowInclude) Process() {
6262
}
6363

6464
if err = include.Workflow.handleDataNotCached(include.FromCache, data, include); err != nil {
65-
fmt.Println(err)
65+
support.FailureMessageWithXMark("remote include (rejected: " + err.Error() + "): " + include.DisplayName())
6666
return
6767
}
6868

@@ -71,7 +71,7 @@ func (include *WorkflowInclude) Process() {
7171
}
7272

7373
if err = include.Workflow.loadRemoteFileInclude(include); err != nil {
74-
fmt.Println(err)
74+
support.FailureMessageWithXMark("remote include (rejected: " + err.Error() + "): " + include.DisplayName())
7575
return
7676
}
7777

lib/gateway/gateway.go

Lines changed: 92 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,32 @@ type GatewayUrlRequestMiddleware struct {
1717
Handler func(g *Gateway, link string) error
1818
}
1919

20+
type GatewayUrlResponseMiddleware struct {
21+
Name string
22+
Handler func(g *Gateway, resp *http.Response) error
23+
}
2024
type Gateway struct {
21-
Enabled bool
22-
AllowedDomains []string
23-
DeniedDomains []string
24-
Middleware []*GatewayUrlRequestMiddleware
25-
DomainHeaders *sync.Map
25+
Enabled bool
26+
AllowedDomains []string
27+
DeniedDomains []string
28+
Middleware []*GatewayUrlRequestMiddleware
29+
PostMiddleware []*GatewayUrlResponseMiddleware
30+
DomainHeaders *sync.Map
31+
DomainContentTypes *sync.Map
32+
BlockedContentTypes *sync.Map
2633
}
2734

2835
// New initializes the gateway with deny/allow lists
2936
func New(deniedDomains, allowedDomains []string) *Gateway {
3037
result := Gateway{
31-
Enabled: true,
32-
DeniedDomains: deniedDomains,
33-
AllowedDomains: allowedDomains,
34-
Middleware: []*GatewayUrlRequestMiddleware{},
35-
DomainHeaders: &sync.Map{},
38+
Enabled: true,
39+
DeniedDomains: deniedDomains,
40+
AllowedDomains: allowedDomains,
41+
Middleware: []*GatewayUrlRequestMiddleware{},
42+
PostMiddleware: []*GatewayUrlResponseMiddleware{},
43+
DomainHeaders: &sync.Map{},
44+
DomainContentTypes: &sync.Map{},
45+
BlockedContentTypes: &sync.Map{},
3646
}
3747

3848
result.Initialize()
@@ -46,6 +56,7 @@ func (g *Gateway) Initialize() {
4656

4757
g.AddMiddleware(&ValidateUrlMiddleware)
4858
g.AddMiddleware(&VerifyFileTypeMiddleware)
59+
g.AddPostMiddleware(&VerifyContentType)
4960

5061
g.Enable()
5162
}
@@ -61,15 +72,64 @@ func (g *Gateway) SetDeniedDomains(domains []string) {
6172
}
6273

6374
func (g *Gateway) SetDomainHeaders(domain string, headers []string) {
64-
for _, header := range headers {
65-
g.DomainHeaders.Store(domain, header)
66-
}
75+
g.DomainHeaders.Store(domain, headers)
76+
}
77+
78+
func (g *Gateway) GetDomainHeaders(domain string) []string {
79+
result := []string{}
80+
81+
g.DomainHeaders.Range(func(key, value any) bool {
82+
if glob.Glob(key.(string), domain) {
83+
result = append(result, value.([]string)...)
84+
}
85+
return true
86+
})
87+
88+
return result
89+
}
90+
91+
func (g *Gateway) SetBlockedContentTypes(domain string, contentTypes []string) {
92+
g.BlockedContentTypes.Store(domain, contentTypes)
93+
}
94+
95+
func (g *Gateway) GetBlockedContentTypes(domain string) []string {
96+
result := []string{}
97+
98+
g.BlockedContentTypes.Range(func(key, value any) bool {
99+
if glob.Glob(key.(string), domain) {
100+
result = append(result, value.([]string)...)
101+
}
102+
return true
103+
})
104+
105+
return result
106+
}
107+
108+
func (g *Gateway) SetDomainContentTypes(domain string, contentTypes []string) {
109+
g.DomainContentTypes.Store(domain, contentTypes)
110+
}
111+
112+
func (g *Gateway) GetDomainContentTypes(domain string) []string {
113+
result := []string{}
114+
115+
g.DomainContentTypes.Range(func(key, value any) bool {
116+
if glob.Glob(key.(string), domain) {
117+
result = append(result, value.([]string)...)
118+
}
119+
return true
120+
})
121+
122+
return result
67123
}
68124

69125
func (g *Gateway) AddMiddleware(mw *GatewayUrlRequestMiddleware) {
70126
g.Middleware = append(g.Middleware, mw)
71127
}
72128

129+
func (g *Gateway) AddPostMiddleware(mw *GatewayUrlResponseMiddleware) {
130+
g.PostMiddleware = append(g.PostMiddleware, mw)
131+
}
132+
73133
// The `runUrlRequestPipeline` function is a method of the `Gateway` struct. It iterates over the
74134
// `Middleware` slice of the `Gateway` struct and executes each middleware function in order. Each
75135
// middleware function takes a `Gateway` instance and a URL `link` as parameters and returns an error.
@@ -87,6 +147,16 @@ func (g *Gateway) runUrlRequestPipeline(link string) error {
87147
return nil
88148
}
89149

150+
func (g *Gateway) runResponsePipeline(resp *http.Response) error {
151+
for _, mw := range g.PostMiddleware {
152+
err := (*mw).Handler(g, resp)
153+
if err != nil {
154+
return err
155+
}
156+
}
157+
return nil
158+
}
159+
90160
func (g *Gateway) Allowed(link string) bool {
91161
return g.runUrlRequestPipeline(link) == nil
92162
}
@@ -142,14 +212,15 @@ func (g *Gateway) GetUrl(urlStr string, headers ...string) (string, error) {
142212
return "", err
143213
}
144214

145-
// remove the header items that are empty strings:
146215
var tempHeaders []string = []string{"User-Agent: stackup/1.0"}
147216

148217
g.DomainHeaders.Range(func(key, value any) bool {
149218
parsed, _ := url.Parse(urlStr)
150219
if glob.Glob(key.(string), parsed.Hostname()) {
151-
header := os.ExpandEnv(value.(string))
152-
tempHeaders = append(tempHeaders, header)
220+
for _, header := range value.([]string) {
221+
header := os.ExpandEnv(header)
222+
tempHeaders = append(tempHeaders, header)
223+
}
153224
}
154225
return true
155226
})
@@ -180,6 +251,11 @@ func (g *Gateway) GetUrl(urlStr string, headers ...string) (string, error) {
180251
}
181252
defer resp.Body.Close()
182253

254+
err = g.runResponsePipeline(resp)
255+
if err != nil {
256+
return "", err
257+
}
258+
183259
if resp.StatusCode >= 400 {
184260
return "", fmt.Errorf("HTTP error: %d", resp.StatusCode)
185261
}

lib/gateway/middleware.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package gateway
22

33
import (
44
"errors"
5+
"net/http"
56
"net/url"
67
"path"
78
"strings"
@@ -10,8 +11,41 @@ import (
1011
var (
1112
ValidateUrlMiddleware = GatewayUrlRequestMiddleware{Name: "validateUrl", Handler: validateUrlHandler}
1213
VerifyFileTypeMiddleware = GatewayUrlRequestMiddleware{Name: "verifyFileType", Handler: verifyFileTypeHandler}
14+
VerifyContentType = GatewayUrlResponseMiddleware{Name: "verifyContentType", Handler: verifyContentTypeHandler}
1315
)
1416

17+
func verifyContentTypeHandler(g *Gateway, resp *http.Response) error {
18+
if !g.Enabled {
19+
return nil
20+
}
21+
22+
contentType, _, _ := strings.Cut(resp.Header.Get("Content-Type"), ";")
23+
24+
// allowedTypes := []string{
25+
// "application/json", "application/javascript", "application/x-yaml", "application/x-yml", "application/yaml", "application/yml",
26+
// "text/*",
27+
// "application/octet-stream", "application/zip", "application/x-zip-compressed",
28+
// "application/x-gzip", "application/gzip", "application/x-tar", "application/tar",
29+
// "application/x-bzip2", "application/bzip2", "application/x-bzip", "application/bzip",
30+
// "application/x-xz", "application/x-lzma", "application/lzma",
31+
// "application/vnd.debian.binary-package", //.deb
32+
// "application/pgp-signature", // .sig
33+
// }
34+
35+
blockedTypes := g.GetBlockedContentTypes(resp.Request.URL.Hostname())
36+
37+
if g.checkArrayForMatch(&blockedTypes, contentType) {
38+
return errors.New("content type blocked")
39+
}
40+
41+
allowedTypes := g.GetDomainContentTypes(resp.Request.URL.Hostname())
42+
if g.checkArrayForMatch(&allowedTypes, contentType) || len(allowedTypes) == 0 {
43+
return nil
44+
}
45+
46+
return errors.New("content type '" + contentType + "' not allowed")
47+
}
48+
1549
func validateUrlHandler(g *Gateway, link string) error {
1650
if !g.Enabled {
1751
return nil

lib/projectinfo/project.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package projectinfo
22

33
import (
4-
"fmt"
54
"os"
65
"path"
76
"path/filepath"
@@ -52,8 +51,6 @@ func (p *Project) Name() string {
5251

5352
baseDir, err := findProjectBaseDir(cwd)
5453

55-
fmt.Printf("baseDir: %s\n", baseDir)
56-
5754
if err != nil {
5855
if absCwd, err := filepath.Abs(cwd); err == nil {
5956
return p.cacheName(path.Base(absCwd))

0 commit comments

Comments
 (0)