aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShulhan <ms@kilabit.info>2025-01-16 02:26:20 +0700
committerShulhan <ms@kilabit.info>2025-01-16 02:33:44 +0700
commit01c317a295ba743c17064e4656fac22d993c1174 (patch)
tree4a043d7a2810bca808bf6c0fb4fd6b632e7069ac
parent44479b6a3e2e80738e74a7daae6e72dfcee77502 (diff)
downloadpakakeh.go-01c317a295ba743c17064e4656fac22d993c1174.tar.xz
lib/play: handle writing files outside the [GoOptions.Root]
Any request to Run or Test Go code that requires writing new files will be joined with the [GoOptions.Root] first. If the final absolute path does not have Root as the prefix it will return an error [os.ErrPermission].
-rw-r--r--lib/play/go.go22
-rw-r--r--lib/play/http_example_test.go50
-rw-r--r--lib/play/http_test.go74
-rw-r--r--lib/play/play.go25
-rw-r--r--lib/play/play_example_test.go57
-rw-r--r--lib/play/play_test.go153
-rw-r--r--lib/play/request.go21
-rw-r--r--lib/play/testdata/httpHandleTest_test.txt2
8 files changed, 250 insertions, 154 deletions
diff --git a/lib/play/go.go b/lib/play/go.go
index 835ac32a..d1fb47e8 100644
--- a/lib/play/go.go
+++ b/lib/play/go.go
@@ -5,6 +5,8 @@
package play
import (
+ "fmt"
+ "path/filepath"
"time"
)
@@ -14,6 +16,9 @@ type GoOptions struct {
// Default to [os.UserCacheDir] if its not set.
Root string
+ // absRoot the absolute path of Root directory.
+ absRoot string
+
// Version define the Go tool version in go.mod to be used to run the
// code.
// Default to package [GoVersion] if its not set.
@@ -25,9 +30,14 @@ type GoOptions struct {
Timeout time.Duration
}
-func (opts *GoOptions) init() {
+func (opts *GoOptions) init() (err error) {
if len(opts.Root) == 0 {
opts.Root = userCacheDir
+ } else {
+ opts.absRoot, err = filepath.Abs(opts.Root)
+ if err != nil {
+ return err
+ }
}
if len(opts.Version) == 0 {
opts.Version = GoVersion
@@ -35,6 +45,7 @@ func (opts *GoOptions) init() {
if opts.Timeout <= 0 {
opts.Timeout = Timeout
}
+ return nil
}
// Go define the type that can format, run, and test Go code.
@@ -43,10 +54,13 @@ type Go struct {
}
// NewGo create and initialize new Go tools.
-func NewGo(opts GoOptions) (playgo *Go) {
- opts.init()
+func NewGo(opts GoOptions) (playgo *Go, err error) {
+ err = opts.init()
+ if err != nil {
+ return nil, fmt.Errorf(`NewGo: %w`, err)
+ }
playgo = &Go{
opts: opts,
}
- return playgo
+ return playgo, nil
}
diff --git a/lib/play/http_example_test.go b/lib/play/http_example_test.go
index 03f2aa85..5268dbe7 100644
--- a/lib/play/http_example_test.go
+++ b/lib/play/http_example_test.go
@@ -12,6 +12,7 @@ import (
"log"
"net/http"
"net/http/httptest"
+ "os"
"regexp"
)
@@ -22,13 +23,19 @@ func main() {
fmt.Println("Hello, world")
}
`
+ var playgo *Go
+ var err error
+ playgo, err = NewGo(GoOptions{
+ Root: os.TempDir(),
+ })
+ if err != nil {
+ log.Fatal(err)
+ }
+
var req = Request{
Body: codeIndentMissingImport,
}
- var (
- rawbody []byte
- err error
- )
+ var rawbody []byte
rawbody, err = json.Marshal(&req)
if err != nil {
log.Fatal(err)
@@ -39,7 +46,6 @@ func main() {
bytes.NewReader(rawbody))
httpreq.Header.Set(`Content-Type`, `application/json`)
- var playgo = NewGo(GoOptions{})
var mux = http.NewServeMux()
mux.HandleFunc(`POST /api/play/format`, playgo.HTTPHandleFormat)
mux.ServeHTTP(resprec, httpreq)
@@ -64,27 +70,30 @@ func main() {
fmt.Println("Hello, world")
}
`
+ var playgo *Go
+ var err error
+ playgo, err = NewGo(GoOptions{
+ Root: os.TempDir(),
+ })
+ if err != nil {
+ log.Fatal(err)
+ }
+
var req = Request{
Body: code,
}
- var (
- rawbody []byte
- err error
- )
+ var rawbody []byte
rawbody, err = json.Marshal(&req)
if err != nil {
log.Fatal(err)
}
var resprec = httptest.NewRecorder()
-
var httpreq = httptest.NewRequest(`POST`, `/api/play/run`,
bytes.NewReader(rawbody))
httpreq.Header.Set(`Content-Type`, `application/json`)
- var playgo = NewGo(GoOptions{})
var mux = http.NewServeMux()
-
mux.HandleFunc(`POST /api/play/run`, playgo.HTTPHandleRun)
mux.ServeHTTP(resprec, httpreq)
@@ -110,20 +119,25 @@ func TestSum(t *testing.T) {
t.Fatalf("got %d, want 6", total)
}
}`
+ var playgo *Go
+ var err error
+ playgo, err = NewGo(GoOptions{
+ Root: `testdata/`,
+ })
+ if err != nil {
+ log.Fatal(err)
+ }
+
var req = Request{
Body: code,
- File: `testdata/test_test.go`,
+ File: `/test_test.go`,
}
- var (
- rawbody []byte
- err error
- )
+ var rawbody []byte
rawbody, err = json.Marshal(&req)
if err != nil {
log.Fatal(err)
}
- var playgo = NewGo(GoOptions{})
var mux = http.NewServeMux()
mux.HandleFunc(`POST /api/play/test`, playgo.HTTPHandleTest)
diff --git a/lib/play/http_test.go b/lib/play/http_test.go
index 47ce1377..a659aa56 100644
--- a/lib/play/http_test.go
+++ b/lib/play/http_test.go
@@ -22,10 +22,8 @@ func TestGo_HTTPHandleFormat(t *testing.T) {
contentType string
}
- var (
- tdata *test.Data
- err error
- )
+ var tdata *test.Data
+ var err error
tdata, err = test.LoadData(`testdata/httpHandleFormat_test.txt`)
if err != nil {
t.Fatal(err)
@@ -41,16 +39,20 @@ func TestGo_HTTPHandleFormat(t *testing.T) {
contentType: libhttp.ContentTypeJSON,
}}
- var (
- playgo = NewGo(GoOptions{})
+ var playgo *Go
+ playgo, err = NewGo(GoOptions{
+ Root: t.TempDir(),
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
- req Request
- tcase testCase
- rawb []byte
- )
+ var tcase testCase
for _, tcase = range listCase {
+ var req Request
req.Body = string(tdata.Input[tcase.tag])
+ var rawb []byte
rawb, err = json.Marshal(&req)
if err != nil {
t.Fatal(err)
@@ -83,10 +85,8 @@ func TestGo_HTTPHandleRun(t *testing.T) {
req Request
}
- var (
- tdata *test.Data
- err error
- )
+ var tdata *test.Data
+ var err error
tdata, err = test.LoadData(`testdata/httpHandleRun_test.txt`)
if err != nil {
t.Fatal(err)
@@ -112,15 +112,19 @@ func TestGo_HTTPHandleRun(t *testing.T) {
},
}}
- var (
- playgo = NewGo(GoOptions{})
+ var playgo *Go
+ playgo, err = NewGo(GoOptions{
+ Root: t.TempDir(),
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
- tcase testCase
- rawb []byte
- )
+ var tcase testCase
for _, tcase = range listCase {
tcase.req.Body = string(tdata.Input[tcase.tag])
+ var rawb []byte
rawb, err = json.Marshal(&tcase.req)
if err != nil {
t.Fatal(err)
@@ -154,40 +158,44 @@ func TestGo_HTTPHandleTest(t *testing.T) {
req Request
}
- var (
- tdata *test.Data
- err error
- )
+ var tdata *test.Data
+ var err error
tdata, err = test.LoadData(`testdata/httpHandleTest_test.txt`)
if err != nil {
t.Fatal(err)
}
+ var playgo *Go
+ playgo, err = NewGo(GoOptions{
+ Root: `testdata/`,
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+
var listCase = []testCase{{
tag: `noContentType`,
}, {
tag: `ok`,
contentType: libhttp.ContentTypeJSON,
req: Request{
- File: `testdata/test_test.go`,
+ File: `/test_test.go`,
},
}, {
tag: `invalidFile`,
contentType: libhttp.ContentTypeJSON,
req: Request{
- File: `testdata/notexist/test_test.go`,
+ File: `/notexist/test_test.go`,
},
}}
- var (
- playgo = NewGo(GoOptions{})
- rexDuration = regexp.MustCompile(`(?m)\\t(\d+\.\d+)s`)
- tcase testCase
- rawb []byte
- )
+ var rexDuration = regexp.MustCompile(`(?m)\\t(\d+\.\d+)s`)
+ var empty = []byte(``)
+ var tcase testCase
for _, tcase = range listCase {
tcase.req.Body = string(tdata.Input[tcase.tag])
+ var rawb []byte
rawb, err = json.Marshal(&tcase.req)
if err != nil {
t.Fatal(err)
@@ -207,7 +215,9 @@ func TestGo_HTTPHandleTest(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- rawb = bytes.ReplaceAll(rawb, []byte("\r"), []byte(""))
+ rawb = bytes.ReplaceAll(rawb, []byte("\r"), empty)
+ rawb = bytes.ReplaceAll(rawb, []byte(playgo.opts.absRoot),
+ empty)
rawb = rexDuration.ReplaceAll(rawb, []byte(" Xs"))
var exp = string(tdata.Output[tcase.tag])
diff --git a/lib/play/play.go b/lib/play/play.go
index 81a5ae79..ada006c1 100644
--- a/lib/play/play.go
+++ b/lib/play/play.go
@@ -88,6 +88,7 @@ import (
"fmt"
"os"
"path/filepath"
+ "strings"
"time"
"golang.org/x/tools/imports"
@@ -161,9 +162,11 @@ func (playgo *Go) Run(req *Request) (out []byte, err error) {
return nil, nil
}
err = req.writes(playgo.opts)
- if err != nil {
- return nil, fmt.Errorf(`%s: %w`, logp, err)
- }
+ } else {
+ err = req.initUnsafe(playgo.opts)
+ }
+ if err != nil {
+ return nil, fmt.Errorf(`%s: %w`, logp, err)
}
cmd = newCommand(playgo.opts, req)
@@ -188,11 +191,23 @@ func (playgo *Go) Test(req *Request) (out []byte, err error) {
if len(req.File) == 0 {
return nil, ErrEmptyFile
}
+
+ var absPathFile = filepath.Join(playgo.opts.absRoot, req.File)
+ if !strings.HasPrefix(absPathFile, playgo.opts.absRoot) {
+ return nil, fmt.Errorf(`%s: File %q is outside Root: %w`,
+ logp, req.File, os.ErrPermission)
+ }
+
if len(req.UnsafeRun) == 0 {
- req.UnsafeRun = filepath.Dir(req.File)
+ req.UnsafeRun = filepath.Dir(absPathFile)
+ } else {
+ err = req.initUnsafe(playgo.opts)
+ if err != nil {
+ return nil, fmt.Errorf(`%s: %w`, logp, err)
+ }
}
- err = os.WriteFile(req.File, []byte(req.Body), 0600)
+ err = os.WriteFile(absPathFile, []byte(req.Body), 0600)
if err != nil {
return nil, fmt.Errorf(`%s: %w`, logp, err)
}
diff --git a/lib/play/play_example_test.go b/lib/play/play_example_test.go
index d87c54ff..e2a93f1d 100644
--- a/lib/play/play_example_test.go
+++ b/lib/play/play_example_test.go
@@ -7,6 +7,7 @@ package play
import (
"fmt"
"log"
+ "os"
"regexp"
)
@@ -17,15 +18,19 @@ func main() {
fmt.Println("Hello, world")
}
`
+ var playgo *Go
+ var err error
+ playgo, err = NewGo(GoOptions{
+ Root: os.TempDir(),
+ })
+ if err != nil {
+ log.Fatal(err)
+ }
+
var req = Request{
Body: codeIndentMissingImport,
}
- var (
- playgo = NewGo(GoOptions{})
-
- out []byte
- err error
- )
+ var out []byte
out, err = playgo.Format(req)
if err != nil {
log.Fatal(err)
@@ -50,18 +55,22 @@ func main() {
fmt.Println("Hello, world")
}`
+ var playgo *Go
+ var err error
+ playgo, err = NewGo(GoOptions{
+ Root: os.TempDir(),
+ })
+ if err != nil {
+ log.Fatal(err)
+ }
+
var req = Request{
Body: codeRun,
}
- var (
- playgo = NewGo(GoOptions{})
-
- out []byte
- err error
- )
+ var out []byte
out, err = playgo.Run(&req)
if err != nil {
- fmt.Printf(`error: %s`, err)
+ log.Fatal(err)
}
fmt.Printf(`%s`, out)
@@ -79,19 +88,25 @@ func TestSum(t *testing.T) {
t.Fatalf("got %d, want 6", total)
}
}`
+ var rexDuration = regexp.MustCompile(`(?m)\s+(\d+\.\d+)s$`)
+
+ var playgo *Go
+ var err error
+ playgo, err = NewGo(GoOptions{
+ Root: `testdata/`,
+ })
+ if err != nil {
+ log.Fatal(err)
+ }
+
var req = Request{
Body: codeTest,
- File: `testdata/test_test.go`,
+ File: `/test_test.go`,
}
- var (
- playgo = NewGo(GoOptions{})
- rexDuration = regexp.MustCompile(`(?m)\s+(\d+\.\d+)s$`)
- out []byte
- err error
- )
+ var out []byte
out, err = playgo.Test(&req)
if err != nil {
- fmt.Printf(`error: %s`, err)
+ log.Fatal(err)
}
// Replace the test duration.
out = rexDuration.ReplaceAll(out, []byte(" Xs"))
diff --git a/lib/play/play_test.go b/lib/play/play_test.go
index 110ffca3..59c6eaba 100644
--- a/lib/play/play_test.go
+++ b/lib/play/play_test.go
@@ -25,24 +25,27 @@ func TestMain(m *testing.M) {
}
func TestGo_Format(t *testing.T) {
- var (
- tdata *test.Data
- err error
- )
+ var tdata *test.Data
+ var err error
tdata, err = test.LoadData(`testdata/format_test.txt`)
if err != nil {
t.Fatal(err)
}
- var (
- playgo = Go{}
- name string
- input []byte
- )
- for name, input = range tdata.Input {
- var req Request
+ var playgo *Go
+ playgo, err = NewGo(GoOptions{
+ Root: t.TempDir(),
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
- req.Body = string(input)
+ var name string
+ var input []byte
+ for name, input = range tdata.Input {
+ var req = Request{
+ Body: string(input),
+ }
var exp = string(tdata.Output[name])
var got []byte
@@ -65,10 +68,8 @@ func TestGo_Run(t *testing.T) {
req Request
}
- var (
- tdata *test.Data
- err error
- )
+ var tdata *test.Data
+ var err error
tdata, err = test.LoadData(`testdata/run_test.txt`)
if err != nil {
t.Fatal(err)
@@ -79,22 +80,27 @@ func TestGo_Run(t *testing.T) {
}, {
tag: `noimport`,
}}
- var (
- playgo = NewGo(GoOptions{})
- tcase testCase
- exp string
- got []byte
- )
+
+ var playgo *Go
+ playgo, err = NewGo(GoOptions{
+ Root: t.TempDir(),
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ var tcase testCase
for _, tcase = range listCase {
tcase.req.Body = string(tdata.Input[tcase.tag])
+ var got []byte
got, err = playgo.Run(&tcase.req)
if err != nil {
var tagError = tcase.tag + `Error`
- exp = string(tdata.Output[tagError])
+ var exp = string(tdata.Output[tagError])
test.Assert(t, tagError, exp, err.Error())
}
- exp = string(tdata.Output[tcase.tag])
+ var exp = string(tdata.Output[tcase.tag])
test.Assert(t, tcase.tag, exp, string(got))
}
}
@@ -104,21 +110,16 @@ func TestGo_Run(t *testing.T) {
// The second Run, run normal code.
// On the second Run, the first Run should be cancelled or killed.
func TestGo_Run_Overlap(t *testing.T) {
- var (
- tdata *test.Data
- err error
- )
-
+ var tdata *test.Data
+ var err error
tdata, err = test.LoadData(`testdata/run_overlap_test.txt`)
if err != nil {
t.Fatal(err)
}
// First Run.
- var (
- sid = `overlap`
- runwg sync.WaitGroup
- )
+ var sid = `overlap`
+ var runwg sync.WaitGroup
runwg.Add(1)
go testRunOverlap(t, &runwg, tdata, `run1`, sid)
@@ -157,26 +158,29 @@ func testRunOverlap(t *testing.T, runwg *sync.WaitGroup, tdata *test.Data,
// the top and call it using defer fix the issue.
defer runwg.Done()
- var (
- playgo = NewGo(GoOptions{})
- req = &Request{
- cookieSid: &http.Cookie{
- Value: sid,
- },
- Body: string(tdata.Input[runName]),
- }
- exp string
- out []byte
- err error
- )
+ var playgo *Go
+ var err error
+ playgo, err = NewGo(GoOptions{
+ Root: t.TempDir(),
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ var req = &Request{
+ cookieSid: &http.Cookie{
+ Value: sid,
+ },
+ Body: string(tdata.Input[runName]),
+ }
+ var out []byte
out, err = playgo.Run(req)
if err != nil {
- exp = string(tdata.Output[runName+`-error`])
+ var exp = string(tdata.Output[runName+`-error`])
test.Assert(t, runName+` error`, exp, err.Error())
}
- exp = string(tdata.Output[runName+`-output`])
+ var exp = string(tdata.Output[runName+`-output`])
// On Inspiron PC, the test run and can be checked using
// [test.Assert].
@@ -192,14 +196,19 @@ func testRunOverlap(t *testing.T, runwg *sync.WaitGroup, tdata *test.Data,
func TestGo_Run_UnsafeRun(t *testing.T) {
var req = &Request{
- UnsafeRun: `testdata/unsafe_run/cmd/forum`,
+ UnsafeRun: `/unsafe_run/cmd/forum`,
}
- var (
- playgo = NewGo(GoOptions{})
- out []byte
- err error
- )
+ var playgo *Go
+ var err error
+ playgo, err = NewGo(GoOptions{
+ Root: `testdata/`,
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ var out []byte
out, err = playgo.Run(req)
if err != nil {
t.Fatal(err)
@@ -217,53 +226,61 @@ func TestGo_Test(t *testing.T) {
treq Request
}
- var (
- tdata *test.Data
- err error
- )
+ var tdata *test.Data
+ var err error
tdata, err = test.LoadData(`testdata/test_test.txt`)
if err != nil {
t.Fatal(err)
}
+ var playgo *Go
+ playgo, err = NewGo(GoOptions{
+ Root: `testdata/`,
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+
var listCase = []testCase{{
tag: `ok`,
treq: Request{
- File: `testdata/test_test.go`,
+ File: `/test_test.go`,
},
}, {
tag: `fail`,
treq: Request{
- File: `testdata/test_test.go`,
+ File: `/test_test.go`,
},
}, {
tag: `buildFailed`,
treq: Request{
- File: `testdata/test_test.go`,
+ File: `/test_test.go`,
},
}, {
tag: `emptyFile`,
expError: ErrEmptyFile.Error(),
+ }, {
+ tag: `ErrPermission`,
+ treq: Request{
+ File: `../../etc/outside`,
+ },
+ expError: `Test: File "../../etc/outside" is outside Root: permission denied`,
}}
var rexDuration = regexp.MustCompile(`(?m)\s+(\d+\.\d+)s$`)
-
- var (
- playgo = NewGo(GoOptions{})
- tcase testCase
- exp string
- got []byte
- )
+ var tcase testCase
for _, tcase = range listCase {
tcase.treq.Body = string(tdata.Input[tcase.tag])
tcase.treq.init(playgo.opts)
+ var got []byte
got, err = playgo.Test(&tcase.treq)
if err != nil {
test.Assert(t, tcase.tag, tcase.expError, err.Error())
}
got = rexDuration.ReplaceAll(got, []byte(" Xs"))
- exp = string(tdata.Output[tcase.tag])
+
+ var exp = string(tdata.Output[tcase.tag])
test.Assert(t, tcase.tag, exp, string(got))
}
}
diff --git a/lib/play/request.go b/lib/play/request.go
index 94e5f04b..c38e9674 100644
--- a/lib/play/request.go
+++ b/lib/play/request.go
@@ -7,9 +7,11 @@ package play
import (
"crypto/sha256"
"encoding/hex"
+ "fmt"
"net/http"
"os"
"path/filepath"
+ "strings"
libbytes "git.sr.ht/~shulhan/pakakeh.go/lib/bytes"
)
@@ -61,13 +63,22 @@ func (req *Request) init(opts GoOptions) {
}
}
+func (req *Request) initUnsafe(opts GoOptions) (err error) {
+ var absUnsafe = filepath.Join(opts.absRoot, req.UnsafeRun)
+ if !strings.HasPrefix(absUnsafe, opts.absRoot) {
+ return fmt.Errorf(`UnsafeRun %q is outside Root: %w`,
+ req.UnsafeRun, os.ErrPermission)
+ }
+ req.UnsafeRun = absUnsafe
+ return nil
+}
+
// generateSid generate session ID from the first 16 hex of SHA256 hash of
// request body plus current epoch in.
func (req *Request) generateSid() string {
- var (
- plain = []byte(req.Body)
- epoch = now()
- )
+ var plain = []byte(req.Body)
+ var epoch = now()
+
plain = libbytes.AppendInt64(plain, epoch)
var cipher = sha256.Sum256(plain)
var dst = make([]byte, hex.EncodedLen(len(cipher)))
@@ -78,7 +89,7 @@ func (req *Request) generateSid() string {
// writes write the go.mod and main.go files inside the unsafe directory.
func (req *Request) writes(opts GoOptions) (err error) {
- req.UnsafeRun = filepath.Join(opts.Root, `goplay`, req.cookieSid.Value)
+ req.UnsafeRun = filepath.Join(opts.absRoot, `goplay`, req.cookieSid.Value)
err = os.MkdirAll(req.UnsafeRun, 0700)
if err != nil {
diff --git a/lib/play/testdata/httpHandleTest_test.txt b/lib/play/testdata/httpHandleTest_test.txt
index 5fffc365..004d6c76 100644
--- a/lib/play/testdata/httpHandleTest_test.txt
+++ b/lib/play/testdata/httpHandleTest_test.txt
@@ -50,4 +50,4 @@ HTTP/1.1 500 Internal Server Error
Connection: close
Content-Type: application/json
-{"message":"Test: open testdata/notexist/test_test.go: no such file or directory","name":"ERR_INTERNAL","code":500}
+{"message":"Test: open /notexist/test_test.go: no such file or directory","name":"ERR_INTERNAL","code":500}