Skip to content

Commit 11ab39f

Browse files
support to load swaggers from an extension (#639)
* feat: support to download differnt kinds of ext * support to load swaggers from an extension * docs(extension): add swagger data extension and update documentation - Add swagger data extension to the extensions list - Update documentation to include new extension usage * Potential fix for code scanning alert no. 69: Arbitrary file access during archive extraction ("Zip Slip") Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Rick <[email protected]> --------- Signed-off-by: Rick <[email protected]> Co-authored-by: rick <[email protected]> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
1 parent 2f409ca commit 11ab39f

File tree

14 files changed

+274
-17
lines changed

14 files changed

+274
-17
lines changed

cmd/extension.go

+3
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ type extensionOption struct {
3131
ociDownloader downloader.PlatformAwareOCIDownloader
3232
output string
3333
registry string
34+
kind string
3435
tag string
3536
os string
3637
arch string
@@ -53,6 +54,7 @@ func createExtensionCommand(ociDownloader downloader.PlatformAwareOCIDownloader)
5354
flags.StringVarP(&opt.output, "output", "", ".", "The target directory")
5455
flags.StringVarP(&opt.tag, "tag", "", "", "The extension image tag, try to find the latest one if this is empty")
5556
flags.StringVarP(&opt.registry, "registry", "", "", "The target extension image registry, supported: docker.io, ghcr.io")
57+
flags.StringVarP(&opt.kind, "kind", "", "store", "The extension kind")
5658
flags.StringVarP(&opt.os, "os", "", runtime.GOOS, "The OS")
5759
flags.StringVarP(&opt.arch, "arch", "", runtime.GOARCH, "The architecture")
5860
flags.DurationVarP(&opt.timeout, "timeout", "", time.Minute, "The timeout of downloading")
@@ -66,6 +68,7 @@ func (o *extensionOption) runE(cmd *cobra.Command, args []string) (err error) {
6668
o.ociDownloader.WithRegistry(o.registry)
6769
o.ociDownloader.WithImagePrefix(o.imagePrefix)
6870
o.ociDownloader.WithTimeout(o.timeout)
71+
o.ociDownloader.WithKind(o.kind)
6972
o.ociDownloader.WithContext(cmd.Context())
7073

7174
for _, arg := range args {

cmd/server.go

+11
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"context"
2323
"errors"
2424
"fmt"
25+
"github.com/linuxsuren/api-testing/pkg/apispec"
2526
"net"
2627
"net/http"
2728
"os"
@@ -302,6 +303,15 @@ func (o *serverOption) runE(cmd *cobra.Command, args []string) (err error) {
302303
_ = o.httpServer.Shutdown(ctx)
303304
}()
304305

306+
go func() {
307+
err := apispec.DownloadSwaggerData("", extDownloader)
308+
if err != nil {
309+
fmt.Println("failed to download swagger data", err)
310+
} else {
311+
fmt.Println("success to download swagger data")
312+
}
313+
}()
314+
305315
mux := runtime.NewServeMux(runtime.WithMetadata(server.MetadataStoreFunc),
306316
runtime.WithMarshalerOption("application/json+pretty", &runtime.JSONPb{
307317
MarshalOptions: protojson.MarshalOptions{
@@ -342,6 +352,7 @@ func (o *serverOption) runE(cmd *cobra.Command, args []string) (err error) {
342352
mux.HandlePath(http.MethodGet, "/get", o.getAtestBinary)
343353
mux.HandlePath(http.MethodPost, "/runner/{suite}/{case}", service.WebRunnerHandler)
344354
mux.HandlePath(http.MethodGet, "/api/v1/sbom", service.SBomHandler)
355+
mux.HandlePath(http.MethodGet, "/api/v1/swaggers", apispec.SwaggersHandler)
345356

346357
postRequestProxyFunc := postRequestProxy(o.skyWalking)
347358
mux.HandlePath(http.MethodPost, "/browser/{app}", postRequestProxyFunc)

console/atest-ui/src/views/TestSuite.vue

+6-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { reactive, ref, watch } from 'vue'
44
import { Edit, CopyDocument, Delete } from '@element-plus/icons-vue'
55
import type { FormInstance, FormRules } from 'element-plus'
66
import type { Suite, TestCase, Pair } from './types'
7-
import { NewSuggestedAPIsQuery, GetHTTPMethods } from './types'
7+
import { NewSuggestedAPIsQuery, GetHTTPMethods, SwaggerSuggestion } from './types'
88
import EditButton from '../components/EditButton.vue'
99
import { Cache } from './cache'
1010
import { useI18n } from 'vue-i18n'
@@ -20,6 +20,7 @@ const props = defineProps({
2020
})
2121
const emit = defineEmits(['updated'])
2222
let querySuggestedAPIs = NewSuggestedAPIsQuery(Cache.GetCurrentStore().name, props.name!)
23+
const querySwaggers = SwaggerSuggestion()
2324
2425
const suite = ref({
2526
name: '',
@@ -325,7 +326,10 @@ const renameTestSuite = (name: string) => {
325326
</el-select>
326327
</td>
327328
<td>
328-
<el-input class="mx-1" v-model="suite.spec.url" placeholder="API Spec URL"></el-input>
329+
<el-autocomplete
330+
v-model="suite.spec.url"
331+
:fetch-suggestions="querySwaggers"
332+
/>
329333
</td>
330334
</tr>
331335
</table>

console/atest-ui/src/views/net.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -620,6 +620,12 @@ function GetSuggestedAPIs(name: string,
620620
.then(callback)
621621
}
622622

623+
function GetSwaggers(callback: (d: any) => void) {
624+
fetch(`/api/v1/swaggers`, {})
625+
.then(DefaultResponseProcess)
626+
.then(callback)
627+
}
628+
623629
function ReloadMockServer(config: any) {
624630
const requestOptions = {
625631
method: 'POST',
@@ -812,7 +818,7 @@ export const API = {
812818
CreateOrUpdateStore, GetStores, DeleteStore, VerifyStore,
813819
FunctionsQuery,
814820
GetSecrets, DeleteSecret, CreateOrUpdateSecret,
815-
GetSuggestedAPIs,
821+
GetSuggestedAPIs, GetSwaggers,
816822
ReloadMockServer, GetMockConfig, SBOM, DataQuery,
817823
getToken
818824
}

console/atest-ui/src/views/types.ts

+22
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,28 @@ export function NewSuggestedAPIsQuery(store: string, suite: string) {
102102
})
103103
}
104104
}
105+
106+
interface SwaggerItem {
107+
value: string
108+
}
109+
110+
export function SwaggerSuggestion() {
111+
return function (queryString: string, cb: (arg: any) => void) {
112+
API.GetSwaggers((e) => {
113+
var swaggers = [] as SwaggerItem[]
114+
e.forEach((item: string) => {
115+
swaggers.push({
116+
"value": `atest://${item}`
117+
})
118+
})
119+
120+
const results = queryString ? swaggers.filter((item: SwaggerItem) => {
121+
return item.value.toLowerCase().indexOf(queryString.toLowerCase()) != -1
122+
}) : swaggers
123+
cb(results.slice(0, 10))
124+
})
125+
}
126+
}
105127
export function CreateFilter(queryString: string) {
106128
return (v: Pair) => {
107129
return v.value.toLowerCase().indexOf(queryString.toLowerCase()) !== -1

docs/site/content/zh/latest/tasks/extension.md

+6
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,9 @@ atest extension orm
3131
```shell
3232
atest extension orm --registry ghcr.io --timeout 2ms
3333
```
34+
35+
想要下载其他类型的插件的话,可以使用下面的命令:
36+
37+
```shell
38+
atest extension --kind data swagger
39+
```

extensions/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Ports in extensions:
1111
| Monitor | [docker-monitor](https://github.com/LinuxSuRen/atest-ext-monitor-docker) | |
1212
| Agent | [collector](https://github.com/LinuxSuRen/atest-ext-collector) | |
1313
| Secret | [Vault](https://github.com/LinuxSuRen/api-testing-vault-extension) | |
14+
| Data | [Swagger](https://github.com/LinuxSuRen/atest-ext-data-swagger) | |
1415

1516
## Contribute a new extension
1617

pkg/apispec/remote_swagger.go

+134
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/*
2+
Copyright 2025 API Testing Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package apispec
18+
19+
import (
20+
"archive/tar"
21+
"compress/gzip"
22+
"encoding/json"
23+
"fmt"
24+
"github.com/linuxsuren/api-testing/pkg/downloader"
25+
"github.com/linuxsuren/api-testing/pkg/util/home"
26+
"io"
27+
"net/http"
28+
"os"
29+
"path/filepath"
30+
"strings"
31+
)
32+
33+
func DownloadSwaggerData(output string, dw downloader.PlatformAwareOCIDownloader) (err error) {
34+
dw.WithKind("data")
35+
dw.WithOS("")
36+
37+
var reader io.Reader
38+
if reader, err = dw.Download("swagger", "", ""); err != nil {
39+
return
40+
}
41+
42+
extFile := dw.GetTargetFile()
43+
44+
if output == "" {
45+
output = home.GetUserDataDir()
46+
}
47+
if err = os.MkdirAll(filepath.Dir(output), 0755); err != nil {
48+
return
49+
}
50+
51+
targetFile := filepath.Base(extFile)
52+
fmt.Println("start to save", filepath.Join(output, targetFile))
53+
if err = downloader.WriteTo(reader, output, targetFile); err == nil {
54+
err = decompressData(filepath.Join(output, targetFile))
55+
}
56+
return
57+
}
58+
59+
func SwaggersHandler(w http.ResponseWriter, _ *http.Request,
60+
_ map[string]string) {
61+
swaggers := GetSwaggerList()
62+
if data, err := json.Marshal(swaggers); err == nil {
63+
_, _ = w.Write(data)
64+
} else {
65+
w.WriteHeader(http.StatusInternalServerError)
66+
}
67+
}
68+
69+
func GetSwaggerList() (swaggers []string) {
70+
dataDir := home.GetUserDataDir()
71+
_ = filepath.WalkDir(dataDir, func(path string, d os.DirEntry, err error) error {
72+
if err != nil {
73+
return err
74+
}
75+
76+
if !d.IsDir() && filepath.Ext(path) == ".json" {
77+
swaggers = append(swaggers, filepath.Base(path))
78+
}
79+
return nil
80+
})
81+
return
82+
}
83+
84+
func decompressData(dataFile string) (err error) {
85+
var file *os.File
86+
file, err = os.Open(dataFile)
87+
if err != nil {
88+
return
89+
}
90+
defer file.Close()
91+
92+
var gzipReader *gzip.Reader
93+
gzipReader, err = gzip.NewReader(file)
94+
if err != nil {
95+
return
96+
}
97+
defer gzipReader.Close()
98+
99+
tarReader := tar.NewReader(gzipReader)
100+
101+
for {
102+
header, err := tarReader.Next()
103+
if err == io.EOF {
104+
break // 退出循环
105+
}
106+
if err != nil {
107+
panic(err)
108+
}
109+
110+
// Ensure the file path does not contain directory traversal sequences
111+
if strings.Contains(header.Name, "..") {
112+
fmt.Printf("Skipping entry with unsafe path: %s\n", header.Name)
113+
continue
114+
}
115+
116+
destPath := filepath.Join(filepath.Dir(dataFile), filepath.Base(header.Name))
117+
118+
switch header.Typeflag {
119+
case tar.TypeReg:
120+
destFile, err := os.OpenFile(destPath, os.O_CREATE|os.O_WRONLY, os.FileMode(header.Mode))
121+
if err != nil {
122+
panic(err)
123+
}
124+
defer destFile.Close()
125+
126+
if _, err := io.Copy(destFile, tarReader); err != nil {
127+
panic(err)
128+
}
129+
default:
130+
fmt.Printf("Skipping entry type %c: %s\n", header.Typeflag, header.Name)
131+
}
132+
}
133+
return
134+
}

pkg/apispec/swagger.go

+17
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,11 @@ package apispec
1818

1919
import (
2020
"github.com/go-openapi/spec"
21+
"github.com/linuxsuren/api-testing/pkg/util/home"
2122
"io"
2223
"net/http"
24+
"os"
25+
"path/filepath"
2326
"regexp"
2427
"strings"
2528
)
@@ -123,13 +126,27 @@ func ParseToSwagger(data []byte) (swagger *spec.Swagger, err error) {
123126
}
124127

125128
func ParseURLToSwagger(swaggerURL string) (swagger *spec.Swagger, err error) {
129+
if strings.HasPrefix(swaggerURL, "atest://") {
130+
swaggerURL = strings.ReplaceAll(swaggerURL, "atest://", "")
131+
swagger, err = ParseFileToSwagger(filepath.Join(home.GetUserDataDir(), swaggerURL))
132+
return
133+
}
134+
126135
var resp *http.Response
127136
if resp, err = http.Get(swaggerURL); err == nil && resp != nil && resp.StatusCode == http.StatusOK {
128137
swagger, err = ParseStreamToSwagger(resp.Body)
129138
}
130139
return
131140
}
132141

142+
func ParseFileToSwagger(dataFile string) (swagger *spec.Swagger, err error) {
143+
var data []byte
144+
if data, err = os.ReadFile(dataFile); err == nil {
145+
swagger, err = ParseToSwagger(data)
146+
}
147+
return
148+
}
149+
133150
func ParseStreamToSwagger(stream io.Reader) (swagger *spec.Swagger, err error) {
134151
var data []byte
135152
if data, err = io.ReadAll(stream); err == nil {

0 commit comments

Comments
 (0)