diff options
| author | Shulhan <ms@kilabit.info> | 2025-01-16 02:26:20 +0700 |
|---|---|---|
| committer | Shulhan <ms@kilabit.info> | 2025-01-16 02:33:44 +0700 |
| commit | 01c317a295ba743c17064e4656fac22d993c1174 (patch) | |
| tree | 4a043d7a2810bca808bf6c0fb4fd6b632e7069ac | |
| parent | 44479b6a3e2e80738e74a7daae6e72dfcee77502 (diff) | |
| download | pakakeh.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.go | 22 | ||||
| -rw-r--r-- | lib/play/http_example_test.go | 50 | ||||
| -rw-r--r-- | lib/play/http_test.go | 74 | ||||
| -rw-r--r-- | lib/play/play.go | 25 | ||||
| -rw-r--r-- | lib/play/play_example_test.go | 57 | ||||
| -rw-r--r-- | lib/play/play_test.go | 153 | ||||
| -rw-r--r-- | lib/play/request.go | 21 | ||||
| -rw-r--r-- | lib/play/testdata/httpHandleTest_test.txt | 2 |
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} |
