Skip to content

Commit da38955

Browse files
authored
[fix] custom http module loader (#88)
* better analysis * update comments * for http tests * test func * clean it * another test * not allowed * fix for windows" g s g " * better refacotr
1 parent e43606f commit da38955

File tree

5 files changed

+235
-92
lines changed

5 files changed

+235
-92
lines changed

.github/workflows/build.yml

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -68,17 +68,25 @@ jobs:
6868
CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }}
6969
run:
7070
bash <(curl -Ls https://coverage.codacy.com/get.sh) report --force-coverage-parser go -r coverage.txt
71-
- name: Analyze
71+
- name: Analyze on Linux
7272
if: ${{ runner.os == 'Linux' }}
7373
run: |
74-
# tokei -- count
74+
# Setup
7575
wget -cqL https://github.com/XAMPPRocky/tokei/releases/download/v12.1.2/tokei-i686-unknown-linux-musl.tar.gz -O tokei.tgz
7676
tar zxf tokei.tgz tokei && chmod +x tokei && $SUDO mv tokei /usr/local/bin && rm tokei.tgz
77-
echo "=== Tokei Result ==="
78-
tokei
79-
# revive -- lint
80-
wget -cqL https://github.com/mgechev/revive/releases/download/v1.2.4/revive_1.2.4_Linux_x86_64.tar.gz -O revive.tgz
77+
wget -cqL https://github.com/mgechev/revive/releases/download/v1.3.7/revive_linux_amd64.tar.gz -O revive.tgz
8178
tar zxf revive.tgz revive && chmod +x revive && $SUDO mv revive /usr/local/bin && rm revive.tgz
82-
wget -cqL https://bitbucket.org/ai69/common/raw/master/revive.toml -O revive.toml
83-
echo "=== Revive Result ==="
84-
revive -config revive.toml -formatter friendly ./...
79+
wget -cqL https://raw.githubusercontent.com/1set/meta/master/revive.toml -O revive.toml
80+
# Analyze
81+
echo "# Analysis on Linux" > $GITHUB_STEP_SUMMARY
82+
uname -a >> $GITHUB_STEP_SUMMARY
83+
# --- count lines of code
84+
echo "## Tokei Result" >> $GITHUB_STEP_SUMMARY
85+
printf '\n```\n' >> $GITHUB_STEP_SUMMARY
86+
tokei >> $GITHUB_STEP_SUMMARY
87+
printf '```\n\n' >> $GITHUB_STEP_SUMMARY
88+
# --- lint
89+
echo "## Revive Result" >> $GITHUB_STEP_SUMMARY
90+
printf '\n```\n' >> $GITHUB_STEP_SUMMARY
91+
revive -config revive.toml -formatter friendly ./... >> $GITHUB_STEP_SUMMARY
92+
printf '```\n\n' >> $GITHUB_STEP_SUMMARY

lib/http/http.go

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -50,24 +50,42 @@ type RequestGuard interface {
5050

5151
// LoadModule creates an http Module
5252
func LoadModule() (starlark.StringDict, error) {
53-
var m = &Module{}
53+
return NewModule().LoadModule()
54+
}
55+
56+
// Module defines the actual HTTP module with methods for making requests.
57+
type Module struct {
58+
cli *http.Client
59+
rg RequestGuard
60+
}
61+
62+
// NewModule creates a new http module with default settings.
63+
func NewModule() *Module {
64+
m := &Module{}
5465
if Client != nil {
5566
m.cli = Client
5667
}
5768
if Guard != nil {
5869
m.rg = Guard
5970
}
60-
ns := starlark.StringDict{
61-
ModuleName: m.Struct(),
62-
}
63-
return ns, nil
71+
return m
6472
}
6573

66-
// Module joins http tools to a dataset, allowing dataset
67-
// to follow along with http requests
68-
type Module struct {
69-
cli *http.Client
70-
rg RequestGuard
74+
// SetClient sets the http client for this module, useful for setting custom clients for testing or multiple loadings.
75+
func (m *Module) SetClient(c *http.Client) {
76+
m.cli = c
77+
}
78+
79+
// SetGuard sets the request guard for this module, useful for setting custom guards for testing or multiple loadings.
80+
func (m *Module) SetGuard(g RequestGuard) {
81+
m.rg = g
82+
}
83+
84+
// LoadModule creates an http Module.
85+
func (m *Module) LoadModule() (starlark.StringDict, error) {
86+
return starlark.StringDict{
87+
ModuleName: m.Struct(),
88+
}, nil
7189
}
7290

7391
// Struct returns this module's methods as a starlark Struct

lib/http/http_test.go

Lines changed: 86 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
1-
package http
1+
package http_test
22

33
import (
44
"encoding/json"
5-
"io/ioutil"
5+
"errors"
66
"net/http"
77
"net/http/httptest"
88
"net/http/httputil"
9-
"strings"
9+
"net/url"
1010
"testing"
1111
"time"
1212

1313
itn "github.com/1set/starlet/internal"
14+
lh "github.com/1set/starlet/lib/http"
1415
"github.com/1set/starlight/convert"
1516
"go.starlark.net/starlark"
1617
"go.starlark.net/starlarktest"
@@ -27,7 +28,7 @@ func TestAsString(t *testing.T) {
2728
}
2829

2930
for i, c := range cases {
30-
got, err := AsString(c.in)
31+
got, err := lh.AsString(c.in)
3132
if !(err == nil && c.err == "" || err != nil && err.Error() == c.err) {
3233
t.Errorf("case %d error mismatch. expected: '%s', got: '%s'", i, c.err, err)
3334
continue
@@ -49,7 +50,7 @@ func TestLoadModule_HTTP_One(t *testing.T) {
4950
defer ts.Close()
5051
starlark.Universe["test_server_url"] = starlark.String(ts.URL)
5152

52-
thread := &starlark.Thread{Load: itn.NewAssertLoader(ModuleName, LoadModule)}
53+
thread := &starlark.Thread{Load: itn.NewAssertLoader(lh.ModuleName, lh.LoadModule)}
5354
starlarktest.SetReporter(thread, t)
5455

5556
code := itn.HereDoc(`
@@ -296,7 +297,7 @@ func TestLoadModule_HTTP(t *testing.T) {
296297
{
297298
name: `POST with UA Set`,
298299
preset: func() {
299-
UserAgent = "GqQdYX3eIJw2DTt"
300+
lh.UserAgent = "GqQdYX3eIJw2DTt"
300301
},
301302
script: itn.HereDoc(`
302303
load('http', 'post')
@@ -575,8 +576,8 @@ func TestLoadModule_HTTP(t *testing.T) {
575576
if tt.preset != nil {
576577
tt.preset()
577578
}
578-
TimeoutSecond = 30.0
579-
res, err := itn.ExecModuleWithErrorTest(t, ModuleName, LoadModule, tt.script, tt.wantErr, nil)
579+
lh.TimeoutSecond = 30.0
580+
res, err := itn.ExecModuleWithErrorTest(t, lh.ModuleName, lh.LoadModule, tt.script, tt.wantErr, nil)
580581
if (err != nil) != (tt.wantErr != "") {
581582
t.Errorf("http(%q) expects error = '%v', actual error = '%v', result = %v", tt.name, tt.wantErr, err, res)
582583
return
@@ -585,65 +586,91 @@ func TestLoadModule_HTTP(t *testing.T) {
585586
}
586587
}
587588

588-
// we're ok with testing private functions if it simplifies the test :)
589-
func TestSetBody(t *testing.T) {
590-
fd := map[string]string{
591-
"foo": "bar baz",
589+
// DomainWhitelistGuard allows requests only to domains in its whitelist.
590+
type DomainWhitelistGuard struct {
591+
whitelist map[string]struct{} // Set of allowed domains
592+
}
593+
594+
// NewDomainWhitelistGuard creates a new DomainWhitelistGuard with the specified domains.
595+
func NewDomainWhitelistGuard(domains []string) *DomainWhitelistGuard {
596+
whitelist := make(map[string]struct{})
597+
for _, domain := range domains {
598+
whitelist[domain] = struct{}{}
592599
}
600+
return &DomainWhitelistGuard{whitelist: whitelist}
601+
}
593602

594-
cases := []struct {
595-
rawBody starlark.String
596-
formData map[string]string
597-
formEncoding starlark.String
598-
jsonData starlark.Value
599-
body string
600-
err string
601-
}{
602-
{starlark.String("hallo"), nil, starlark.String(""), nil, "hallo", ""},
603-
{starlark.String(""), fd, starlark.String(""), nil, "foo=bar+baz", ""},
604-
// TODO - this should check multipart form data is being set
605-
{starlark.String(""), fd, starlark.String("multipart/form-data"), nil, "", ""},
606-
{starlark.String(""), nil, starlark.String(""), starlark.Tuple{starlark.Bool(true), starlark.MakeInt(1), starlark.String("der")}, "[true,1,\"der\"]", ""},
603+
// Allowed checks if the request's domain is in the whitelist.
604+
func (g *DomainWhitelistGuard) Allowed(thread *starlark.Thread, req *http.Request) (*http.Request, error) {
605+
if _, ok := g.whitelist[req.URL.Host]; ok {
606+
// Domain is in the whitelist, allow the request
607+
return req, nil
607608
}
609+
// Domain is not in the whitelist, deny the request
610+
return nil, errors.New("request to this domain is not allowed")
611+
}
608612

609-
for i, c := range cases {
610-
var formData *starlark.Dict
611-
if c.formData != nil {
612-
formData = starlark.NewDict(len(c.formData))
613-
for k, v := range c.formData {
614-
if err := formData.SetKey(starlark.String(k), starlark.String(v)); err != nil {
615-
t.Fatal(err)
616-
}
617-
}
618-
}
613+
func TestLoadModule_CustomLoad(t *testing.T) {
614+
md := lh.NewModule()
615+
proxyURL, _ := url.Parse("http://127.0.0.1:9999")
616+
client := &http.Client{
617+
Transport: &http.Transport{
618+
Proxy: http.ProxyURL(proxyURL),
619+
},
620+
}
621+
md.SetClient(client)
622+
guard := NewDomainWhitelistGuard([]string{"allowed.com"})
623+
md.SetGuard(guard)
619624

620-
req := httptest.NewRequest("get", "https://example.com", nil)
621-
err := setBody(req, c.rawBody, formData, c.formEncoding, c.jsonData)
622-
if !(err == nil && c.err == "" || (err != nil && err.Error() == c.err)) {
623-
t.Errorf("case %d error mismatch. expected: %s, got: %s", i, c.err, err)
624-
continue
625+
httpHand := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
626+
b, err := httputil.DumpRequest(r, true)
627+
if err != nil {
628+
t.Errorf("Error dumping request: %v", err)
625629
}
630+
t.Logf("Web server received request: [[%s]]", b)
631+
time.Sleep(10 * time.Millisecond)
632+
w.Write(b)
633+
})
634+
ts := httptest.NewServer(httpHand)
635+
defer ts.Close()
626636

627-
if strings.HasPrefix(req.Header.Get("Content-Type"), "multipart/form-data;") {
628-
if err := req.ParseMultipartForm(0); err != nil {
629-
t.Fatal(err)
630-
}
631-
632-
for k, v := range c.formData {
633-
fv := req.FormValue(k)
634-
if fv != v {
635-
t.Errorf("case %d error mismatch. expected %s=%s, got: %s", i, k, v, fv)
636-
}
637-
}
638-
} else {
639-
body, err := ioutil.ReadAll(req.Body)
640-
if err != nil {
641-
t.Fatal(err)
637+
tests := []struct {
638+
name string
639+
preset func()
640+
script string
641+
wantErr string
642+
}{
643+
{
644+
name: `Simple GET`,
645+
script: itn.HereDoc(`
646+
load('http', 'get')
647+
res = get("http://allowed.com/hello")
648+
assert.eq(res.status_code, 200)
649+
`),
650+
wantErr: `proxyconnect tcp: dial tcp 127.0.0.1:9999: connect`,
651+
},
652+
{
653+
name: `Not Allowed`,
654+
script: itn.HereDoc(`
655+
load('http', 'get')
656+
res = get("http://topsecret.com/text")
657+
assert.eq(res.status_code, 200)
658+
`),
659+
wantErr: `request to this domain is not allowed`,
660+
},
661+
}
662+
for _, tt := range tests {
663+
t.Run(tt.name, func(t *testing.T) {
664+
if tt.preset != nil {
665+
tt.preset()
642666
}
643-
644-
if string(body) != c.body {
645-
t.Errorf("case %d body mismatch. expected: %s, got: %s", i, c.body, string(body))
667+
res, err := itn.ExecModuleWithErrorTest(t, lh.ModuleName, md.LoadModule, tt.script, tt.wantErr, starlark.StringDict{
668+
"test_server_url": starlark.String(ts.URL),
669+
})
670+
if (err != nil) != (tt.wantErr != "") {
671+
t.Errorf("http(%q) expects error = '%v', actual error = '%v', result = %v", tt.name, tt.wantErr, err, res)
672+
return
646673
}
647-
}
674+
})
648675
}
649676
}

lib/http/internal_test.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package http
2+
3+
import (
4+
"io/ioutil"
5+
"net/http/httptest"
6+
"strings"
7+
"testing"
8+
9+
"go.starlark.net/starlark"
10+
)
11+
12+
// we're ok with testing private functions if it simplifies the test :)
13+
func TestSetBody(t *testing.T) {
14+
fd := map[string]string{
15+
"foo": "bar baz",
16+
}
17+
18+
cases := []struct {
19+
rawBody starlark.String
20+
formData map[string]string
21+
formEncoding starlark.String
22+
jsonData starlark.Value
23+
body string
24+
err string
25+
}{
26+
{starlark.String("hallo"), nil, starlark.String(""), nil, "hallo", ""},
27+
{starlark.String(""), fd, starlark.String(""), nil, "foo=bar+baz", ""},
28+
// TODO - this should check multipart form data is being set
29+
{starlark.String(""), fd, starlark.String("multipart/form-data"), nil, "", ""},
30+
{starlark.String(""), nil, starlark.String(""), starlark.Tuple{starlark.Bool(true), starlark.MakeInt(1), starlark.String("der")}, "[true,1,\"der\"]", ""},
31+
}
32+
33+
for i, c := range cases {
34+
var formData *starlark.Dict
35+
if c.formData != nil {
36+
formData = starlark.NewDict(len(c.formData))
37+
for k, v := range c.formData {
38+
if err := formData.SetKey(starlark.String(k), starlark.String(v)); err != nil {
39+
t.Fatal(err)
40+
}
41+
}
42+
}
43+
44+
req := httptest.NewRequest("get", "https://example.com", nil)
45+
err := setBody(req, c.rawBody, formData, c.formEncoding, c.jsonData)
46+
if !(err == nil && c.err == "" || (err != nil && err.Error() == c.err)) {
47+
t.Errorf("case %d error mismatch. expected: %s, got: %s", i, c.err, err)
48+
continue
49+
}
50+
51+
if strings.HasPrefix(req.Header.Get("Content-Type"), "multipart/form-data;") {
52+
if err := req.ParseMultipartForm(0); err != nil {
53+
t.Fatal(err)
54+
}
55+
56+
for k, v := range c.formData {
57+
fv := req.FormValue(k)
58+
if fv != v {
59+
t.Errorf("case %d error mismatch. expected %s=%s, got: %s", i, k, v, fv)
60+
}
61+
}
62+
} else {
63+
body, err := ioutil.ReadAll(req.Body)
64+
if err != nil {
65+
t.Fatal(err)
66+
}
67+
68+
if string(body) != c.body {
69+
t.Errorf("case %d body mismatch. expected: %s, got: %s", i, c.body, string(body))
70+
}
71+
}
72+
}
73+
}

0 commit comments

Comments
 (0)