├── backend ├── embed │ ├── ui │ │ └── .gitkeep │ └── ui.go ├── fakeshell │ ├── prompt.go │ ├── main.go │ ├── commands │ │ └── map.go │ └── menu.go ├── als │ ├── timer │ │ ├── system.go │ │ └── interface_traffic.go │ ├── controller │ │ ├── cache │ │ │ └── interface.go │ │ ├── middleware.go │ │ ├── ping │ │ │ └── ping.go │ │ ├── speedtest │ │ │ ├── librespeed.go │ │ │ ├── fakefile.go │ │ │ └── speedtest_cli.go │ │ ├── session │ │ │ └── session.go │ │ ├── iperf3 │ │ │ └── iperf3.go │ │ └── shell │ │ │ └── shell.go │ ├── als.go │ ├── client │ │ ├── client.go │ │ └── queue.go │ └── route.go ├── main.go ├── http │ └── init.go ├── .gitignore ├── go.work.sum ├── config │ ├── location.go │ ├── load_from_env.go │ ├── ip.go │ └── init.go ├── go.mod └── go.sum ├── ui ├── public │ ├── speedtest_worker.js │ └── favicon.ico ├── src │ ├── assets │ │ └── base.css │ ├── components │ │ ├── Loading.vue │ │ ├── Speedtest.vue │ │ ├── Information.vue │ │ ├── Copy.vue │ │ ├── Utilities │ │ │ ├── Shell.vue │ │ │ ├── Ping.vue │ │ │ ├── IPerf3.vue │ │ │ └── SpeedtestNet.vue │ │ ├── Speedtest │ │ │ ├── FileSpeedtest.vue │ │ │ └── Librespeed.vue │ │ ├── Utilities.vue │ │ └── TrafficDisplay.vue │ ├── main.js │ ├── helper │ │ └── unit.js │ ├── locales │ │ ├── zh-CN.json │ │ └── en-US.json │ ├── config │ │ └── lang.js │ ├── stores │ │ └── app.js │ └── App.vue ├── jsconfig.json ├── .vscode │ └── extensions.json ├── .prettierrc.json ├── .eslintrc.cjs ├── index.html ├── .gitignore ├── README.md ├── package.json └── vite.config.js ├── .gitmodules ├── .gitignore ├── scripts ├── install-speedtest.sh └── install-software.sh ├── .github ├── ISSUE_TEMPLATE │ └── -lang--zh_cn--bug-反馈.md ├── dependabot.yml └── workflows │ ├── docker-image.yml │ └── release.yml ├── .dockerignore ├── Dockerfile ├── LICENSE ├── Dockerfile.cn ├── README_zh_CN.md └── README.md /backend/embed/ui/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/public/speedtest_worker.js: -------------------------------------------------------------------------------- 1 | ../speedtest/speedtest_worker.js -------------------------------------------------------------------------------- /ui/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gytai/als/master/ui/public/favicon.ico -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "ui/speedtest"] 2 | path = ui/speedtest 3 | url = https://github.com/librespeed/speedtest 4 | -------------------------------------------------------------------------------- /backend/embed/ui.go: -------------------------------------------------------------------------------- 1 | package embed 2 | 3 | import "embed" 4 | 5 | //go:embed ui 6 | var UIStaticFiles embed.FS 7 | -------------------------------------------------------------------------------- /ui/src/assets/base.css: -------------------------------------------------------------------------------- 1 | #app { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | font-weight: normal; 6 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | backend/app/webspaces/speedtest-static/* 2 | ui/public/speedtest_worker.js 3 | ui/pnpm-lock.yaml 4 | backend/embed/ui/ 5 | tmp 6 | -------------------------------------------------------------------------------- /ui/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "paths": { 4 | "@/*": ["./src/*"] 5 | } 6 | }, 7 | "exclude": ["node_modules", "dist"] 8 | } 9 | -------------------------------------------------------------------------------- /ui/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "Vue.volar", 4 | "Vue.vscode-typescript-vue-plugin", 5 | "dbaeumer.vscode-eslint", 6 | "esbenp.prettier-vscode" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /ui/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/prettierrc", 3 | "semi": false, 4 | "tabWidth": 2, 5 | "singleQuote": true, 6 | "printWidth": 100, 7 | "trailingComma": "none" 8 | } -------------------------------------------------------------------------------- /scripts/install-speedtest.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | wget -O /tmp/speedtest.tgz https://install.speedtest.net/app/cli/ookla-speedtest-1.2.0-linux-`uname -m`.tgz 3 | tar zxf /tmp/speedtest.tgz -C /tmp 4 | mv /tmp/speedtest /usr/local/bin/speedtest 5 | rm -rf /tmp/* -------------------------------------------------------------------------------- /ui/src/components/Loading.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /ui/src/main.js: -------------------------------------------------------------------------------- 1 | import './assets/base.css' 2 | import { createApp } from 'vue' 3 | import { createPinia } from 'pinia' 4 | import App from './App.vue' 5 | import { setupI18n } from './config/lang.js' 6 | const app = createApp(App) 7 | app.use(setupI18n()) 8 | app.use(createPinia()) 9 | app.mount('#app') 10 | -------------------------------------------------------------------------------- /ui/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | require('@rushstack/eslint-patch/modern-module-resolution') 3 | 4 | module.exports = { 5 | root: true, 6 | 'extends': [ 7 | 'plugin:vue/vue3-essential', 8 | 'eslint:recommended', 9 | '@vue/eslint-config-prettier/skip-formatting' 10 | ], 11 | parserOptions: { 12 | ecmaVersion: 'latest' 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /backend/fakeshell/prompt.go: -------------------------------------------------------------------------------- 1 | package fakeshell 2 | 3 | import "github.com/reeflective/console" 4 | 5 | func setupPrompt(m *console.Menu) { 6 | p := m.Prompt() 7 | 8 | p.Primary = func() string { 9 | prompt := "\x1b[33mALS\x1b[0m > " 10 | return prompt 11 | } 12 | 13 | p.Secondary = func() string { return ">" } 14 | p.Transient = func() string { return "\x1b[1;30m" + ">> " + "\x1b[0m" } 15 | } 16 | -------------------------------------------------------------------------------- /ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Looking glass server 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /backend/als/timer/system.go: -------------------------------------------------------------------------------- 1 | package timer 2 | 3 | import ( 4 | "runtime" 5 | "strconv" 6 | "time" 7 | 8 | "github.com/samlm0/als/v2/als/client" 9 | ) 10 | 11 | func UpdateSystemResource() { 12 | var m runtime.MemStats 13 | ticker := time.NewTicker(5 * time.Second) 14 | for { 15 | <-ticker.C 16 | runtime.ReadMemStats(&m) 17 | client.BroadCastMessage("MemoryUsage", strconv.Itoa(int(m.Sys))) 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/-lang--zh_cn--bug-反馈.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "[Lang: zh_CN] BUG 反馈" 3 | about: Describe this issue template's purpose here. 4 | title: "[BUG] BUG 标题" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | BUG 简短描述 11 | 12 | # 环境 13 | 操作系统 & Docker 版本 (`lsb_release -a` && `docker version`): 14 | 15 | 使用的镜像版本 (`docker ps | grep als`): 16 | 17 | # 现象 18 | 预期返回: 19 | ``` 20 | 这样这样这样 21 | ``` 22 | 实际返回: 23 | ``` 24 | 那样那样那样 25 | ``` 26 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | **/speedtest-static/* 2 | **/ip.mmdb 3 | **/.DS_Store 4 | **/node_modules 5 | **/package-lock.json 6 | ui/public/speedtest_worker.js 7 | backend/app/webspaces/speedtest-static/* 8 | modules/speedtest/* 9 | !modules/speedtest/speedtest_worker.js 10 | backend/app/webspaces/speedtest_worker.js 11 | backend/app/webspaces/assets 12 | backend/app/webspaces/index.html 13 | backend/app/webspaces/favicon.ico 14 | backend/app/utilities/speedtest 15 | backend/go.work -------------------------------------------------------------------------------- /ui/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | 17 | /cypress/videos/ 18 | /cypress/screenshots/ 19 | 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/extensions.json 23 | .idea 24 | *.suo 25 | *.ntvs* 26 | *.njsproj 27 | *.sln 28 | *.sw? 29 | 30 | *.tsbuildinfo 31 | -------------------------------------------------------------------------------- /backend/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | 6 | "github.com/samlm0/als/v2/als" 7 | "github.com/samlm0/als/v2/config" 8 | "github.com/samlm0/als/v2/fakeshell" 9 | ) 10 | 11 | var shell = flag.Bool("shell", false, "Start as fake shell") 12 | 13 | func main() { 14 | flag.Parse() 15 | if *shell { 16 | config.IsInternalCall = true 17 | config.Load() 18 | fakeshell.HandleConsole() 19 | return 20 | } 21 | 22 | config.LoadWebConfig() 23 | 24 | als.Init() 25 | } 26 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gitsubmodule" 9 | # Workflow files stored in the 10 | # default location of `.github/workflows` 11 | directory: "/" 12 | schedule: 13 | interval: "daily" 14 | -------------------------------------------------------------------------------- /backend/http/init.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | ) 6 | 7 | type Server struct { 8 | engine *gin.Engine 9 | listen string 10 | } 11 | 12 | func CreateServer() *Server { 13 | gin.SetMode(gin.ReleaseMode) 14 | e := &Server{ 15 | engine: gin.Default(), 16 | listen: ":8080", 17 | } 18 | return e 19 | } 20 | 21 | func (e *Server) GetEngine() *gin.Engine { 22 | return e.engine 23 | } 24 | 25 | func (e *Server) SetListen(listen string) { 26 | e.listen = listen 27 | } 28 | 29 | func (e *Server) Start() { 30 | e.engine.Run(e.listen) 31 | } 32 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | pkg 23 | 24 | tmp -------------------------------------------------------------------------------- /backend/als/controller/cache/interface.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/samlm0/als/v2/als/client" 8 | "github.com/samlm0/als/v2/als/timer" 9 | ) 10 | 11 | func UpdateInterfaceCache(c *gin.Context) { 12 | v, _ := c.Get("clientSession") 13 | clientSession := v.(*client.ClientSession) 14 | 15 | interfaceCacheJson, _ := json.Marshal(timer.InterfaceCaches) 16 | clientSession.Channel <- &client.Message{ 17 | Name: "InterfaceCache", 18 | Content: string(interfaceCacheJson), 19 | } 20 | 21 | c.JSON(200, &gin.H{ 22 | "success": true, 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /backend/go.work.sum: -------------------------------------------------------------------------------- 1 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 2 | github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= 3 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 4 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 5 | go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 6 | golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 7 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 8 | -------------------------------------------------------------------------------- /backend/fakeshell/main.go: -------------------------------------------------------------------------------- 1 | package fakeshell 2 | 3 | import ( 4 | "io" 5 | "os" 6 | 7 | "github.com/reeflective/console" 8 | ) 9 | 10 | func exitCtrlD(c *console.Console) { 11 | os.Exit(0) 12 | } 13 | 14 | func HandleConsole() { 15 | app := console.New("example") 16 | app.NewlineBefore = true 17 | app.NewlineAfter = true 18 | 19 | menu := app.ActiveMenu() 20 | setupPrompt(menu) 21 | 22 | // go func() { 23 | // sig := make(chan os.Signal) 24 | // signal.Notify(sig) 25 | // for s := range sig { 26 | // fmt.Println(s) 27 | // } 28 | // }() 29 | 30 | menu.AddInterrupt(io.EOF, exitCtrlD) 31 | menu.SetCommands(defineMenuCommands(app)) 32 | app.Start() 33 | } 34 | -------------------------------------------------------------------------------- /ui/src/helper/unit.js: -------------------------------------------------------------------------------- 1 | export const formatBytes = (bytes, decimals = 2, bandwidth = false) => { 2 | if (bytes === 0) return '0 Bytes' 3 | 4 | let k = 1024 5 | const dm = decimals < 0 ? 0 : decimals 6 | const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] 7 | const bandwidthSizes = ['Bps', 'Kbps', 'Mbps', 'Gbps', 'Tbps', 'Pbs', 'Ebps', 'Zbps', 'Ybps'] 8 | 9 | const i = Math.floor(Math.log(bytes) / Math.log(k)) 10 | 11 | if (bandwidth) { 12 | let k = 1000 13 | bytes = bytes * 8 14 | return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + bandwidthSizes[i] 15 | } 16 | return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i] 17 | } 18 | -------------------------------------------------------------------------------- /ui/src/locales/zh-CN.json: -------------------------------------------------------------------------------- 1 | { 2 | "server_info": "服务器信息", 3 | "server_location": "服务器位置", 4 | "server_speedtest": "服务器网络测速", 5 | "file_speedtest": "文件下载测试", 6 | "file_ipv4_speedtest": "IPv4 下载测试", 7 | "file_ipv6_speedtest": "IPv6 下载测试", 8 | "librespeed_begin": "开始测试", 9 | "librespeed_stop": "停止测试", 10 | "librespeed_upload": "下行", 11 | "librespeed_download": "上行", 12 | "network_tools": "网络工具", 13 | "server_bandwidth_graph": "服务器流量图", 14 | "server_bandwidth_graph_receive": "已接收", 15 | "server_bandwidth_graph_sended": "已发送", 16 | "my_address": "您当前的 IP 地址", 17 | "ipv4_address": "IPv4 地址", 18 | "ipv6_address": "IPv6 地址", 19 | "sponsor_message": "节点赞助商消息", 20 | "memory_usage": "内存用量" 21 | } 22 | -------------------------------------------------------------------------------- /backend/als/als.go: -------------------------------------------------------------------------------- 1 | package als 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/samlm0/als/v2/als/client" 7 | "github.com/samlm0/als/v2/als/timer" 8 | "github.com/samlm0/als/v2/config" 9 | alsHttp "github.com/samlm0/als/v2/http" 10 | ) 11 | 12 | func Init() { 13 | aHttp := alsHttp.CreateServer() 14 | 15 | log.Default().Println("Listen on: " + config.Config.ListenHost + ":" + config.Config.ListenPort) 16 | aHttp.SetListen(config.Config.ListenHost + ":" + config.Config.ListenPort) 17 | 18 | SetupHttpRoute(aHttp.GetEngine()) 19 | 20 | if config.Config.FeatureIfaceTraffic { 21 | go timer.SetupInterfaceBroadcast() 22 | } 23 | go timer.UpdateSystemResource() 24 | go client.HandleQueue() 25 | aHttp.Start() 26 | } 27 | -------------------------------------------------------------------------------- /ui/src/components/Speedtest.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 16 | -------------------------------------------------------------------------------- /backend/fakeshell/commands/map.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func AddExecureableAsCommand(cmd *cobra.Command, command string, argFilter func(args []string) ([]string, error)) { 11 | 12 | cmdDefine := &cobra.Command{ 13 | Use: command, 14 | Run: func(cmd *cobra.Command, args []string) { 15 | args, err := argFilter(args) 16 | if err != nil { 17 | cmd.Println(err) 18 | return 19 | } 20 | c := exec.Command(command, args...) 21 | c.Env = os.Environ() 22 | c.Env = append(c.Env, "TERM=xterm-256color") 23 | c.Stdin = cmd.InOrStdin() 24 | c.Stdout = cmd.OutOrStdout() 25 | c.Stderr = cmd.OutOrStderr() 26 | 27 | c.Run() 28 | c.Wait() 29 | }, 30 | DisableFlagParsing: true, 31 | } 32 | 33 | cmd.AddCommand(cmdDefine) 34 | } 35 | -------------------------------------------------------------------------------- /ui/README.md: -------------------------------------------------------------------------------- 1 | # als 2 | 3 | This template should help get you started developing with Vue 3 in Vite. 4 | 5 | ## Recommended IDE Setup 6 | 7 | [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin). 8 | 9 | ## Customize configuration 10 | 11 | See [Vite Configuration Reference](https://vitejs.dev/config/). 12 | 13 | ## Project Setup 14 | 15 | ```sh 16 | pnpm install 17 | ``` 18 | 19 | ### Compile and Hot-Reload for Development 20 | 21 | ```sh 22 | pnpm dev 23 | ``` 24 | 25 | ### Compile and Minify for Production 26 | 27 | ```sh 28 | pnpm build 29 | ``` 30 | 31 | ### Lint with [ESLint](https://eslint.org/) 32 | 33 | ```sh 34 | pnpm lint 35 | ``` 36 | -------------------------------------------------------------------------------- /backend/config/location.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "log" 8 | "net/http" 9 | ) 10 | 11 | func updateLocation() { 12 | log.Default().Println("Updating server location from internet...") 13 | 14 | resp, err := http.Get("https://ipapi.co/json/") 15 | if err != nil { 16 | return 17 | } 18 | body, err := io.ReadAll(resp.Body) 19 | if err != nil { 20 | return 21 | } 22 | var data map[string]interface{} 23 | json.Unmarshal(body, &data) 24 | if _, ok := data["country_name"]; !ok { 25 | return 26 | } 27 | 28 | if _, ok := data["city"]; !ok { 29 | return 30 | } 31 | 32 | Config.Location = fmt.Sprintf("%s, %s", data["city"], data["country_name"]) 33 | log.Default().Println("Server location: " + Config.Location) 34 | log.Default().Println("Updating server location from internet successed, from ipapi.co") 35 | } 36 | -------------------------------------------------------------------------------- /ui/src/locales/en-US.json: -------------------------------------------------------------------------------- 1 | { 2 | "server_info": "Server Information", 3 | "server_location": "Server location", 4 | "server_speedtest": "Server Network Speed Test", 5 | "file_speedtest": "File Download Test", 6 | "file_ipv4_speedtest": "IPv4 Download Test", 7 | "file_ipv6_speedtest": "IPv6 Download Test", 8 | "librespeed_begin": "Begin test", 9 | "librespeed_stop": "Stop test", 10 | "librespeed_upload": "Upload", 11 | "librespeed_download": "Download", 12 | "network_tools": "Network tools", 13 | "server_bandwidth_graph": "Server bandwidth graph", 14 | "server_bandwidth_graph_receive": "Received", 15 | "server_bandwidth_graph_sended": "Sended", 16 | "ipv4_address": "IPv4 Address", 17 | "ipv6_address": "IPv6 Address", 18 | "my_address": "Your current IP address", 19 | "sponsor_message": "Sponsor message", 20 | "memory_usage": "Memory usage" 21 | } 22 | -------------------------------------------------------------------------------- /scripts/install-software.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | fix_arch(){ 4 | ARCH=`uname -m` 5 | if [ "$ARCH" == "aarch64" ];then 6 | echo "arm64" 7 | return 0 8 | fi; 9 | 10 | if [ "$ARCH" == "x86_64" ];then 11 | echo "amd64" 12 | return 0 13 | fi; 14 | echo $ARCH 15 | } 16 | 17 | install_from_github(){ 18 | OWNER=$1 19 | PROJECT=$2 20 | SAVE_AS=$3 21 | ARCH=$4 22 | if [ -z "$ARCH" ];then 23 | ARCH=`uname -m` 24 | fi; 25 | URL=$(wget -qO - https://api.github.com/repos/$1/$2/releases/latest | grep download_url | grep linux | grep $ARCH | awk -F'": "' '{print $2}' | tr '"' ' ') 26 | 27 | echo "Download $URL to $SAVE_AS" 28 | wget -O $SAVE_AS $URL 29 | } 30 | 31 | install_from_github "nxtrace" "Ntrace-V1" "/usr/local/bin/nexttrace" `fix_arch` 32 | chmod +x "/usr/local/bin/nexttrace" 33 | 34 | sh install-speedtest.sh -------------------------------------------------------------------------------- /backend/als/controller/middleware.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/samlm0/als/v2/als/client" 6 | ) 7 | 8 | func MiddlewareSessionOnHeader() gin.HandlerFunc { 9 | return func(c *gin.Context) { 10 | sessionId := c.GetHeader("session") 11 | client, ok := client.Clients[sessionId] 12 | if !ok { 13 | c.JSON(400, &gin.H{ 14 | "success": false, 15 | "error": "Invaild session", 16 | }) 17 | c.Abort() 18 | return 19 | } 20 | c.Set("clientSession", client) 21 | c.Next() 22 | } 23 | } 24 | 25 | func MiddlewareSessionOnUrl() gin.HandlerFunc { 26 | return func(c *gin.Context) { 27 | sessionId := c.Param("session") 28 | client, ok := client.Clients[sessionId] 29 | if !ok { 30 | c.JSON(400, &gin.H{ 31 | "success": false, 32 | "error": "Invaild session", 33 | }) 34 | c.Abort() 35 | return 36 | } 37 | c.Set("clientSession", client) 38 | c.Next() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /backend/als/client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | var Clients = make(map[string]*ClientSession) 8 | 9 | type Message struct { 10 | Name string 11 | Content string 12 | } 13 | 14 | type ClientSession struct { 15 | Channel chan *Message 16 | ctx context.Context 17 | } 18 | 19 | func (c *ClientSession) SetContext(ctx context.Context) { 20 | c.ctx = ctx 21 | } 22 | 23 | func (c *ClientSession) GetContext(requestCtx context.Context) context.Context { 24 | ctx, cancel := context.WithCancel(context.Background()) 25 | 26 | go func() { 27 | select { 28 | case <-c.ctx.Done(): 29 | cancel() 30 | break 31 | case <-requestCtx.Done(): 32 | cancel() 33 | break 34 | } 35 | }() 36 | 37 | return ctx 38 | } 39 | 40 | func BroadCastMessage(name string, content string) { 41 | for _, client := range Clients { 42 | client.Channel <- &Message{ 43 | Name: name, 44 | Content: content, 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-alpine as builderNodeJSCache 2 | ADD ui/package.json /app/package.json 3 | WORKDIR /app 4 | RUN npm i 5 | 6 | FROM node:lts-alpine as builderNodeJS 7 | ADD ui /app 8 | WORKDIR /app 9 | COPY --from=builderNodeJSCache /app/node_modules /app/node_modules 10 | RUN npm run build \ 11 | && chmod -R 650 /app/dist 12 | 13 | 14 | FROM alpine:3 as builderGolang 15 | ADD backend /app 16 | WORKDIR /app 17 | COPY --from=builderNodeJS /app/dist /app/embed/ui 18 | RUN apk add --no-cache go 19 | 20 | RUN go build -o als && \ 21 | chmod +x als 22 | 23 | FROM alpine:3 as builderEnv 24 | WORKDIR /app 25 | ADD scripts /app 26 | RUN sh /app/install-software.sh 27 | RUN apk add --no-cache \ 28 | iperf iperf3 \ 29 | mtr \ 30 | traceroute \ 31 | iputils 32 | RUN rm -rf /app 33 | 34 | FROM alpine:3 35 | LABEL maintainer="samlm0 " 36 | COPY --from=builderEnv / / 37 | COPY --from=builderGolang --chmod=777 /app/als/als /bin/als 38 | 39 | CMD /bin/als -------------------------------------------------------------------------------- /.github/workflows/docker-image.yml: -------------------------------------------------------------------------------- 1 | name: 'docker image build' 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 */7 * *" 6 | workflow_dispatch: 7 | push: 8 | tags: 9 | - '*' 10 | 11 | 12 | jobs: 13 | docker: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - 17 | name: Checkout 18 | uses: actions/checkout@v3 19 | - name: Checkout submodules 20 | run: git submodule update --init --recursive 21 | - 22 | name: Set up QEMU 23 | uses: docker/setup-qemu-action@v2 24 | - 25 | name: Set up Docker Buildx 26 | uses: docker/setup-buildx-action@v2 27 | - 28 | name: Login to DockerHub 29 | uses: docker/login-action@v2 30 | with: 31 | username: ${{ secrets.DOCKERHUB_USERNAME }} 32 | password: ${{ secrets.DOCKERHUB_TOKEN }} 33 | - 34 | name: Build and push 35 | uses: docker/build-push-action@v3 36 | with: 37 | context: . 38 | platforms: linux/amd64,linux/arm64/v8 39 | push: true 40 | tags: wikihostinc/looking-glass-server:latest 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 wikihost-opensource 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /backend/als/controller/ping/ping.go: -------------------------------------------------------------------------------- 1 | package ping 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/samlm0/als/v2/als/client" 8 | "github.com/samlm0/go-ping" 9 | ) 10 | 11 | func Handle(c *gin.Context) { 12 | ip, ok := c.GetQuery("ip") 13 | v, _ := c.Get("clientSession") 14 | clientSession := v.(*client.ClientSession) 15 | channel := clientSession.Channel 16 | if !ok { 17 | c.JSON(400, &gin.H{ 18 | "success": false, 19 | "error": "Invaild IP Address", 20 | }) 21 | return 22 | } 23 | 24 | p, err := ping.New(ip) 25 | if err != nil { 26 | c.JSON(400, &gin.H{ 27 | "success": false, 28 | "error": "Invaild IP Address", 29 | }) 30 | return 31 | } 32 | 33 | p.Count = 10 34 | p.OnEvent = func(event *ping.PacketEvent, _ error) { 35 | content, err := json.Marshal(event) 36 | if err != nil { 37 | return 38 | } 39 | msg := &client.Message{ 40 | Name: "Ping", 41 | Content: string(content), 42 | } 43 | channel <- msg 44 | } 45 | ctx := clientSession.GetContext(c.Request.Context()) 46 | p.Start(ctx) 47 | 48 | c.JSON(200, &gin.H{ 49 | "success": true, 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /backend/als/controller/speedtest/librespeed.go: -------------------------------------------------------------------------------- 1 | package speedtest 2 | 3 | import ( 4 | "crypto/rand" 5 | "io" 6 | "net/http" 7 | "strconv" 8 | 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | func HandleDownload(c *gin.Context) { 13 | c.Writer.WriteHeader(http.StatusOK) 14 | chunks := 4 15 | if ckSize, ok := c.GetQuery("ckSize"); ok { 16 | if ckSizeInt, err := strconv.Atoi(ckSize); err == nil && ckSizeInt > 0 { 17 | chunks = ckSizeInt 18 | if chunks > 1024 { 19 | chunks = 1024 20 | } 21 | } 22 | } 23 | 24 | data := make([]byte, 1048576) 25 | rand.Read(data) 26 | 27 | for i := 0; i < chunks; i++ { 28 | c.Writer.Write(data) 29 | } 30 | c.Writer.CloseNotify() 31 | } 32 | 33 | func HandleUpload(c *gin.Context) { 34 | c.Header("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0, s-maxage=0, post-check=0, pre-check=0") 35 | c.Header("Pragma", "no-cache") 36 | c.Header("Connection", "keep-alive") 37 | _, err := io.Copy(io.Discard, c.Request.Body) 38 | if err != nil { 39 | c.Status(http.StatusBadRequest) 40 | return 41 | } 42 | _ = c.Request.Body.Close() 43 | 44 | c.Header("Connection", "keep-alive") 45 | c.Status(http.StatusOK) 46 | } 47 | -------------------------------------------------------------------------------- /Dockerfile.cn: -------------------------------------------------------------------------------- 1 | FROM node:lts-alpine as builderNodeJSCache 2 | ADD ui/package.json /app/package.json 3 | WORKDIR /app 4 | RUN npm i 5 | 6 | FROM node:lts-alpine as builderNodeJS 7 | ADD ui /app 8 | WORKDIR /app 9 | COPY --from=builderNodeJSCache /app/node_modules /app/node_modules 10 | RUN npm run build \ 11 | && chmod -R 650 /app/dist 12 | 13 | 14 | FROM alpine:3 as builderGolang 15 | ADD backend /app 16 | WORKDIR /app 17 | COPY --from=builderNodeJS /app/dist /app/embed/ui 18 | RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories && \ 19 | apk add --no-cache go && \ 20 | go env -w GO111MODULE=on && \ 21 | go env -w GOPROXY=https://goproxy.cn,direct 22 | 23 | RUN go build -o als && \ 24 | chmod +x als 25 | 26 | FROM alpine:3 as builderEnv 27 | WORKDIR /app 28 | ADD scripts /app 29 | RUN sh /app/install-software.sh 30 | RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories && \ 31 | apk add --no-cache \ 32 | iperf iperf3 \ 33 | mtr \ 34 | traceroute \ 35 | iputils 36 | RUN rm -rf /app 37 | 38 | FROM alpine:3 39 | LABEL maintainer="samlm0 " 40 | COPY --from=builderEnv / / 41 | COPY --from=builderGolang --chmod=777 /app/als/als /bin/als 42 | 43 | CMD /bin/als -------------------------------------------------------------------------------- /ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "als", 3 | "version": "0.0.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore", 11 | "format": "prettier --write src/" 12 | }, 13 | "dependencies": { 14 | "pinia": "^2.1.7", 15 | "vue": "^3.3.11" 16 | }, 17 | "devDependencies": { 18 | "@rushstack/eslint-patch": "^1.3.3", 19 | "@vicons/carbon": "^0.12.0", 20 | "@vitejs/plugin-vue": "^4.5.2", 21 | "@vitejs/plugin-vue-jsx": "^3.1.0", 22 | "@vue/eslint-config-prettier": "^8.0.0", 23 | "@xterm/addon-attach": "0.10.0-beta.1", 24 | "@xterm/addon-fit": "0.9.0-beta.1", 25 | "@xterm/addon-serialize": "0.12.0-beta.1", 26 | "apexcharts": "^3.45.1", 27 | "axios": "^1.6.5", 28 | "eslint": "^8.49.0", 29 | "eslint-plugin-vue": "^9.17.0", 30 | "naive-ui": "^2.37.3", 31 | "prettier": "^3.0.3", 32 | "unplugin-auto-import": "^0.17.3", 33 | "unplugin-vue-components": "^0.26.0", 34 | "v-clipboard": "3.0.0-next.1", 35 | "vite": "^5.0.10", 36 | "vue-i18n": "9", 37 | "vue3-apexcharts": "^1.4.4", 38 | "vue3-markdown-it": "^1.0.10", 39 | "xterm": "^5.3.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /backend/als/client/queue.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | ) 7 | 8 | var queueLine = make(map[context.Context]context.CancelFunc, 0) 9 | var queueLock = sync.Mutex{} 10 | var queueNotify = make(map[context.Context]func(), 0) 11 | var queueWakeup = make(chan struct{}) 12 | 13 | func WaitQueue(ctx context.Context, cb func()) { 14 | queueCtx, cancel := context.WithCancel(ctx) 15 | queueLine[ctx] = cancel 16 | 17 | select { 18 | case queueWakeup <- struct{}{}: 19 | default: 20 | } 21 | 22 | queueLock.Lock() 23 | if cb != nil { 24 | queueNotify[ctx] = cb 25 | } 26 | queueLock.Unlock() 27 | 28 | select { 29 | case <-queueCtx.Done(): 30 | case <-ctx.Done(): 31 | } 32 | } 33 | 34 | func GetQueuePostitionByCtx(ctx context.Context) (int, int) { 35 | total := len(queueLine) 36 | 37 | found := false 38 | count := 0 39 | queueLock.Lock() 40 | for v, _ := range queueLine { 41 | count++ 42 | if v == ctx { 43 | found = true 44 | break 45 | } 46 | } 47 | queueLock.Unlock() 48 | 49 | if !found { 50 | return 0, 0 51 | } 52 | 53 | return count, total 54 | } 55 | 56 | func HandleQueue() { 57 | for { 58 | <-queueWakeup 59 | for ctx, notify := range queueLine { 60 | notify() 61 | <-ctx.Done() 62 | delete(queueLine, ctx) 63 | delete(queueNotify, ctx) 64 | 65 | for _, callNotify := range queueNotify { 66 | callNotify() 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /ui/src/components/Information.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 40 | 41 | 47 | -------------------------------------------------------------------------------- /ui/src/components/Copy.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 56 | -------------------------------------------------------------------------------- /backend/als/controller/session/session.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/google/uuid" 9 | "github.com/samlm0/als/v2/als/client" 10 | "github.com/samlm0/als/v2/als/timer" 11 | "github.com/samlm0/als/v2/config" 12 | ) 13 | 14 | type sessionConfig struct { 15 | config.ALSConfig 16 | ClientIP string `json:"my_ip"` 17 | } 18 | 19 | func Handle(c *gin.Context) { 20 | uuid := uuid.New().String() 21 | // uuid := "1" 22 | channel := make(chan *client.Message) 23 | clientSession := &client.ClientSession{Channel: channel} 24 | client.Clients[uuid] = clientSession 25 | ctx, cancel := context.WithCancel(c.Request.Context()) 26 | defer cancel() 27 | clientSession.SetContext(ctx) 28 | 29 | c.Writer.Header().Set("Content-Type", "text/event-stream") 30 | c.Writer.Header().Set("Cache-Control", "no-cache") 31 | c.Writer.Header().Set("Connection", "keep-alive") 32 | c.Writer.Header().Set("Access-Control-Allow-Origin", "*") 33 | c.SSEvent("SessionId", uuid) 34 | _config := &sessionConfig{ 35 | ALSConfig: *config.Config, 36 | ClientIP: c.ClientIP(), 37 | } 38 | 39 | configJson, _ := json.Marshal(_config) 40 | c.SSEvent("Config", string(configJson)) 41 | c.Writer.Flush() 42 | interfaceCacheJson, _ := json.Marshal(timer.InterfaceCaches) 43 | c.SSEvent("InterfaceCache", string(interfaceCacheJson)) 44 | c.Writer.Flush() 45 | 46 | for { 47 | select { 48 | case <-ctx.Done(): 49 | goto FINISH 50 | case msg, ok := <-channel: 51 | if !ok { 52 | break 53 | } 54 | c.SSEvent(msg.Name, msg.Content) 55 | c.Writer.Flush() 56 | } 57 | } 58 | 59 | FINISH: 60 | close(channel) 61 | delete(client.Clients, uuid) 62 | } 63 | -------------------------------------------------------------------------------- /backend/als/controller/iperf3/iperf3.go: -------------------------------------------------------------------------------- 1 | package iperf3 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "math/rand" 8 | "os/exec" 9 | "strconv" 10 | "time" 11 | 12 | "github.com/gin-gonic/gin" 13 | "github.com/samlm0/als/v2/als/client" 14 | "github.com/samlm0/als/v2/config" 15 | ) 16 | 17 | func random(min, max int) int { 18 | rand.Seed(time.Now().UnixNano()) 19 | return rand.Intn(max-min+1) + min 20 | } 21 | 22 | func Handle(c *gin.Context) { 23 | v, _ := c.Get("clientSession") 24 | clientSession := v.(*client.ClientSession) 25 | 26 | timeout := time.Second * 60 27 | port := random(config.Config.Iperf3StartPort, config.Config.Iperf3EndPort) 28 | 29 | ctx, cancel := context.WithTimeout(clientSession.GetContext(c.Request.Context()), timeout) 30 | defer cancel() 31 | 32 | cmd := exec.CommandContext(ctx, "iperf3", "-s", "--forceflush", "-p", fmt.Sprintf("%d", port)) 33 | clientSession.Channel <- &client.Message{ 34 | Name: "Iperf3", 35 | Content: strconv.Itoa(port), 36 | } 37 | 38 | writer := func(pipe io.ReadCloser, err error) { 39 | if err != nil { 40 | return 41 | } 42 | for { 43 | buf := make([]byte, 1024) 44 | n, err := pipe.Read(buf) 45 | if err != nil { 46 | return 47 | } 48 | msg := &client.Message{ 49 | Name: "Iperf3Stream", 50 | Content: string(buf[:n]), 51 | } 52 | clientSession.Channel <- msg 53 | } 54 | } 55 | 56 | go writer(cmd.StdoutPipe()) 57 | go writer(cmd.StderrPipe()) 58 | 59 | err := cmd.Start() 60 | if err != nil { 61 | // 处理错误 62 | // fmt.Println("Error starting command:", err) 63 | c.JSON(400, &gin.H{ 64 | "success": false, 65 | }) 66 | return 67 | } 68 | 69 | cmd.Wait() 70 | 71 | c.JSON(200, &gin.H{ 72 | "success": true, 73 | }) 74 | } 75 | -------------------------------------------------------------------------------- /backend/config/load_from_env.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | func LoadFromEnv() { 11 | envVarsString := map[string]*string{ 12 | "LISTEN_IP": &Config.ListenHost, 13 | "HTTP_PORT": &Config.ListenPort, 14 | "LOCATION": &Config.Location, 15 | "PUBLIC_IPV4": &Config.PublicIPv4, 16 | "PUBLIC_IPV6": &Config.PublicIPv6, 17 | "SPONSOR_MESSAGE": &Config.SponsorMessage, 18 | } 19 | 20 | envVarsInt := map[string]*int{ 21 | "UTILITIES_IPERF3_PORT_MIN": &Config.Iperf3StartPort, 22 | "UTILITIES_IPERF3_PORT_MAX": &Config.Iperf3EndPort, 23 | } 24 | 25 | envVarsBool := map[string]*bool{ 26 | "DISPLAY_TRAFFIC": &Config.FeatureIfaceTraffic, 27 | "ENABLE_SPEEDTEST": &Config.FeatureLibrespeed, 28 | "UTILITIES_SPEEDTESTDOTNET": &Config.FeatureSpeedtestDotNet, 29 | "UTILITIES_PING": &Config.FeaturePing, 30 | "UTILITIES_FAKESHELL": &Config.FeatureShell, 31 | "UTILITIES_IPERF3": &Config.FeatureIperf3, 32 | "UTILITIES_MTR": &Config.FeatureMTR, 33 | } 34 | 35 | for envVar, configField := range envVarsString { 36 | if v := os.Getenv(envVar); len(v) != 0 { 37 | *configField = v 38 | } 39 | } 40 | 41 | for envVar, configField := range envVarsInt { 42 | if v := os.Getenv(envVar); len(v) != 0 { 43 | v, err := strconv.Atoi(v) 44 | if err != nil { 45 | continue 46 | } 47 | *configField = v 48 | } 49 | } 50 | 51 | for envVar, configField := range envVarsBool { 52 | if v := os.Getenv(envVar); len(v) != 0 { 53 | *configField = v == "true" 54 | } 55 | } 56 | 57 | if v := os.Getenv("SPEEDTEST_FILE_LIST"); len(v) != 0 { 58 | fileLists := strings.Split(v, " ") 59 | Config.SpeedtestFileList = fileLists 60 | } 61 | 62 | if !IsInternalCall { 63 | log.Default().Println("Loading config from environment variables...") 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /ui/vite.config.js: -------------------------------------------------------------------------------- 1 | // vite.config.ts 2 | import { fileURLToPath, URL } from 'node:url' 3 | import { defineConfig } from 'vite' 4 | import vue from '@vitejs/plugin-vue' 5 | import AutoImport from 'unplugin-auto-import/vite' 6 | import Components from 'unplugin-vue-components/vite' 7 | import { NaiveUiResolver } from 'unplugin-vue-components/resolvers' 8 | import fs from 'node:fs' 9 | import path from 'node:path' 10 | 11 | // https://vitejs.dev/config/ 12 | export default defineConfig(({ command }) => { 13 | return { 14 | base: './', 15 | server: { 16 | proxy: { 17 | '/session': { 18 | target: 'http://127.0.0.1:8080', 19 | ws: true 20 | }, 21 | '/method': { 22 | target: 'http://127.0.0.1:8080', 23 | ws: true 24 | } 25 | } 26 | }, 27 | resolve: { 28 | alias: { 29 | '@': fileURLToPath(new URL('./src', import.meta.url)) 30 | } 31 | }, 32 | plugins: [ 33 | vue(), 34 | AutoImport({ 35 | imports: [ 36 | 'vue', 37 | { 38 | 'naive-ui': ['useDialog', 'useMessage', 'useNotification', 'useLoadingBar'] 39 | } 40 | ] 41 | }), 42 | { 43 | name: 'build-script', 44 | buildStart(options) { 45 | if (command === 'build') { 46 | const dirPath = path.join(__dirname, 'public'); 47 | const fileBuildRequired = { 48 | "speedtest_worker.js": "../speedtest/speedtest_worker.js" 49 | }; 50 | 51 | for (var dest in fileBuildRequired) { 52 | const source = fileBuildRequired[dest] 53 | if (fs.existsSync(dirPath + "/" + dest)) { 54 | fs.unlinkSync(dirPath + "/" + dest) 55 | } 56 | fs.copyFileSync(dirPath + "/" + source, dirPath + "/" + dest) 57 | } 58 | } 59 | }, 60 | }, 61 | Components({ 62 | resolvers: [NaiveUiResolver()] 63 | }) 64 | ] 65 | } 66 | }) 67 | -------------------------------------------------------------------------------- /ui/src/config/lang.js: -------------------------------------------------------------------------------- 1 | import { zhCN, dateZhCN, enUS, dateEnUS } from 'naive-ui' 2 | import { nextTick } from 'vue' 3 | import { createI18n } from 'vue-i18n' 4 | 5 | export const list = [ 6 | { 7 | label: '简体中文', 8 | value: 'zh-CN', 9 | autoChangeMap: ['zh-CN', 'zh'], 10 | uiLang: () => zhCN, 11 | dateLang: () => dateZhCN 12 | }, 13 | { 14 | label: 'English', 15 | value: 'en-US', 16 | autoChangeMap: ['en-US', 'en'], 17 | uiLang: () => enUS, 18 | dateLang: () => dateEnUS 19 | } 20 | ] 21 | 22 | const locales = list.map((x) => x.value) 23 | const i18n = createI18n({ 24 | locale: locales[0], 25 | legacy: false 26 | }) 27 | 28 | // copy from https://vue-i18n.intlify.dev/guide/advanced/lazy.html 29 | export function setupI18n() { 30 | loadLocaleMessages(locales[0]) 31 | setI18nLanguage(locales[0]) 32 | 33 | return i18n 34 | } 35 | 36 | export function setI18nLanguage(locale) { 37 | if (i18n.mode === 'legacy') { 38 | i18n.global.locale = locale 39 | } else { 40 | i18n.global.locale.value = locale 41 | } 42 | /** 43 | * NOTE: 44 | * If you need to specify the language setting for headers, such as the `fetch` API, set it here. 45 | * The following is an example for axios. 46 | * 47 | * axios.defaults.headers.common['Accept-Language'] = locale 48 | */ 49 | document.querySelector('html').setAttribute('lang', locale) 50 | } 51 | 52 | export async function loadLocaleMessages(locale) { 53 | // load locale messages with dynamic import 54 | const messages = await import(`../locales/${locale}.json`) 55 | 56 | console.log(messages.default) 57 | // set locale and locale message 58 | i18n.global.setLocaleMessage(locale, messages.default) 59 | 60 | return nextTick() 61 | } 62 | 63 | export async function autoLang() { 64 | for (var index in list) { 65 | const lang = list[index] 66 | if (lang.autoChangeMap.indexOf(navigator.language) != -1) { 67 | await loadLocaleMessages(lang.value) 68 | setI18nLanguage(lang.value) 69 | return lang.value 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /backend/fakeshell/menu.go: -------------------------------------------------------------------------------- 1 | package fakeshell 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os/exec" 7 | "regexp" 8 | 9 | "github.com/reeflective/console" 10 | "github.com/samlm0/als/v2/config" 11 | "github.com/samlm0/als/v2/fakeshell/commands" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | func defineMenuCommands(a *console.Console) console.Commands { 16 | showedIsFirstTime := false 17 | return func() *cobra.Command { 18 | rootCmd := &cobra.Command{} 19 | 20 | rootCmd.InitDefaultHelpCmd() 21 | rootCmd.CompletionOptions.DisableDefaultCmd = true 22 | rootCmd.DisableFlagsInUseLine = true 23 | 24 | features := map[string]bool{ 25 | "ping": config.Config.FeaturePing, 26 | "traceroute": config.Config.FeatureTraceroute, 27 | "nexttrace": config.Config.FeatureTraceroute, 28 | "speedtest": config.Config.FeatureSpeedtestDotNet, 29 | "mtr": config.Config.FeatureMTR, 30 | } 31 | 32 | argsFilter := map[string]func([]string) ([]string, error){ 33 | "ping": func(args []string) ([]string, error) { 34 | var re = regexp.MustCompile(`(?m)^-?f$|^-\S+f\S*$`) 35 | for _, str := range args { 36 | if len(re.FindAllString(str, -1)) != 0 { 37 | return []string{}, errors.New("dangerous flag detected, stop running") 38 | } 39 | } 40 | return args, nil 41 | }, 42 | } 43 | 44 | hasNotFound := false 45 | 46 | argsPassthough := func(args []string) ([]string, error) { 47 | return args, nil 48 | } 49 | 50 | for command, feature := range features { 51 | if feature { 52 | _, err := exec.LookPath(command) 53 | if err != nil { 54 | if !showedIsFirstTime { 55 | fmt.Println("Error: " + command + " is not install") 56 | } 57 | hasNotFound = true 58 | continue 59 | } 60 | filter, ok := argsFilter[command] 61 | if !ok { 62 | filter = argsPassthough 63 | } 64 | commands.AddExecureableAsCommand(rootCmd, command, filter) 65 | } 66 | } 67 | 68 | if hasNotFound { 69 | showedIsFirstTime = true 70 | } 71 | 72 | rootCmd.SetHelpCommand(&cobra.Command{ 73 | Use: "no-help", 74 | Hidden: true, 75 | }) 76 | 77 | return rootCmd 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /backend/als/controller/shell/shell.go: -------------------------------------------------------------------------------- 1 | package shell 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "os" 8 | "os/exec" 9 | "strconv" 10 | "strings" 11 | 12 | "github.com/creack/pty" 13 | "github.com/gin-gonic/gin" 14 | "github.com/gorilla/websocket" 15 | "github.com/samlm0/als/v2/als/client" 16 | ) 17 | 18 | var upgrader = websocket.Upgrader{ 19 | ReadBufferSize: 4096, 20 | WriteBufferSize: 4096, 21 | } 22 | 23 | func HandleNewShell(c *gin.Context) { 24 | upgrader.CheckOrigin = func(r *http.Request) bool { return true } 25 | conn, err := upgrader.Upgrade(c.Writer, c.Request, nil) 26 | if err != nil { 27 | fmt.Println(err) 28 | return 29 | } 30 | defer conn.Close() 31 | v, _ := c.Get("clientSession") 32 | clientSession := v.(*client.ClientSession) 33 | handleNewConnection(conn, clientSession, c) 34 | } 35 | 36 | func handleNewConnection(conn *websocket.Conn, session *client.ClientSession, ginC *gin.Context) { 37 | ctx, cancel := context.WithCancel(session.GetContext(ginC.Request.Context())) 38 | defer cancel() 39 | 40 | ex, _ := os.Executable() 41 | c := exec.Command(ex, "--shell") 42 | ptmx, err := pty.Start(c) 43 | if err != nil { 44 | return 45 | } 46 | defer ptmx.Close() 47 | 48 | // context aware 49 | go func() { 50 | <-ctx.Done() 51 | if c.Process != nil { 52 | c.Process.Kill() 53 | } 54 | }() 55 | 56 | // cmd -> websocket 57 | go func() { 58 | defer cancel() 59 | buf := make([]byte, 4096) 60 | for { 61 | n, err := ptmx.Read(buf) 62 | if err != nil { 63 | break 64 | } 65 | conn.WriteMessage(websocket.BinaryMessage, buf[:n]) 66 | } 67 | }() 68 | 69 | // websocket -> cmd 70 | go func() { 71 | defer cancel() 72 | for { 73 | _, buf, err := conn.ReadMessage() 74 | if err != nil { 75 | break 76 | } 77 | index := string(buf[:1]) 78 | switch index { 79 | case "1": 80 | // normal input 81 | ptmx.Write(buf[1:]) 82 | case "2": 83 | // win resize 84 | args := strings.Split(string(buf[1:]), ";") 85 | h, _ := strconv.Atoi(args[0]) 86 | w, _ := strconv.Atoi(args[1]) 87 | pty.Setsize(ptmx, &pty.Winsize{ 88 | Rows: uint16(h), 89 | Cols: uint16(w), 90 | }) 91 | } 92 | } 93 | }() 94 | c.Wait() 95 | } 96 | -------------------------------------------------------------------------------- /backend/als/controller/speedtest/fakefile.go: -------------------------------------------------------------------------------- 1 | package speedtest 2 | 3 | import ( 4 | "crypto/rand" 5 | "fmt" 6 | "io" 7 | "regexp" 8 | "strconv" 9 | "strings" 10 | 11 | "github.com/gin-gonic/gin" 12 | "github.com/samlm0/als/v2/als/client" 13 | "github.com/samlm0/als/v2/config" 14 | ) 15 | 16 | func contains(slice []string, item string) bool { 17 | for _, a := range slice { 18 | if a == item { 19 | return true 20 | } 21 | } 22 | return false 23 | } 24 | 25 | func sizeToBytes(size string) (int64, error) { 26 | re := regexp.MustCompile(`^(\d+)(KB|MB|GB|TB)$`) 27 | matches := re.FindStringSubmatch(size) 28 | 29 | if matches == nil { 30 | return 0, fmt.Errorf("invalid size format") 31 | } 32 | 33 | num, err := strconv.ParseInt(matches[1], 10, 64) 34 | if err != nil { 35 | return 0, err 36 | } 37 | 38 | switch strings.ToUpper(matches[2]) { 39 | case "KB": 40 | num *= 1024 41 | case "MB": 42 | num *= 1024 * 1024 43 | case "GB": 44 | num *= 1024 * 1024 * 1024 45 | case "TB": 46 | num *= 1024 * 1024 * 1024 * 1024 47 | } 48 | 49 | return num, nil 50 | } 51 | 52 | func HandleFakeFile(c *gin.Context) { 53 | filename := c.Param("filename") 54 | var re = regexp.MustCompile(`^(\d+)(KB|MB|GB|TB)\.test$`) 55 | 56 | pos := re.FindStringIndex(filename) 57 | if pos == nil { 58 | c.String(404, "404 file not found") 59 | return 60 | } 61 | 62 | client.WaitQueue(c.Request.Context(), nil) 63 | 64 | filename = filename[0 : len(filename)-5] 65 | if !contains(config.Config.SpeedtestFileList, filename) { 66 | c.String(404, "404 file not found") 67 | return 68 | } 69 | 70 | size, ok := sizeToBytes(filename) 71 | if ok != nil { 72 | c.String(404, "Invaild file size") 73 | return 74 | } 75 | c.Header("Content-Type", "application/octet-stream") 76 | c.Header("Content-Length", strconv.FormatInt(size, 10)) 77 | c.Stream(func(w io.Writer) bool { 78 | buf := make([]byte, 1024*1024) 79 | rand.Read(buf) 80 | 81 | for size > 0 { 82 | // 如果剩余的大小小于缓冲区的大小,只写入剩余的大小 83 | if size < int64(len(buf)) { 84 | buf = buf[:size] 85 | } 86 | 87 | // 将缓冲区写入响应 88 | w.Write(buf) 89 | 90 | // 更新剩余的大小 91 | size -= int64(len(buf)) 92 | } 93 | 94 | // 返回false表示我们已经完成了写入 95 | return false 96 | }) 97 | // c.Data() 98 | } 99 | -------------------------------------------------------------------------------- /backend/als/timer/interface_traffic.go: -------------------------------------------------------------------------------- 1 | package timer 2 | 3 | import ( 4 | "net" 5 | "strconv" 6 | "strings" 7 | "time" 8 | 9 | "github.com/samlm0/als/v2/als/client" 10 | "github.com/vishvananda/netlink" 11 | ) 12 | 13 | type InterfaceTrafficCache struct { 14 | InterfaceName string 15 | LastCacheTime time.Time 16 | Caches [][3]uint64 17 | LastRx uint64 `json:"-"` 18 | LastTx uint64 `json:"-"` 19 | } 20 | 21 | var InterfaceCaches = make(map[int]*InterfaceTrafficCache) 22 | 23 | func SetupInterfaceBroadcast() { 24 | ticker := time.NewTicker(1 * time.Second) 25 | for { 26 | <-ticker.C 27 | interfaces, err := net.Interfaces() 28 | if err != nil { 29 | continue 30 | } 31 | 32 | for _, iface := range interfaces { 33 | // skip down interface 34 | if iface.Flags&net.FlagUp == 0 { 35 | continue 36 | } 37 | 38 | // skip docker 39 | if strings.Index(iface.Name, "docker") == 0 { 40 | continue 41 | } 42 | 43 | // skip lo 44 | if iface.Name == "lo" { 45 | continue 46 | } 47 | 48 | // skip wireguard 49 | if strings.Index(iface.Name, "wt") == 0 { 50 | continue 51 | } 52 | 53 | // skip veth 54 | if strings.Index(iface.Name, "veth") == 0 { 55 | continue 56 | } 57 | 58 | link, err := netlink.LinkByIndex(iface.Index) 59 | if err != nil { 60 | continue 61 | } 62 | now := time.Now() 63 | cache, ok := InterfaceCaches[iface.Index] 64 | if !ok { 65 | InterfaceCaches[iface.Index] = &InterfaceTrafficCache{ 66 | InterfaceName: iface.Name, 67 | LastCacheTime: now, 68 | Caches: make([][3]uint64, 0), 69 | LastRx: 0, 70 | LastTx: 0, 71 | } 72 | cache = InterfaceCaches[iface.Index] 73 | } 74 | 75 | cache.LastRx = link.Attrs().Statistics.RxBytes 76 | cache.LastTx = link.Attrs().Statistics.TxBytes 77 | 78 | cache.Caches = append(cache.Caches, [3]uint64{uint64(now.Unix()), cache.LastRx, cache.LastTx}) 79 | if len(cache.Caches) > 30 { 80 | cache.Caches = cache.Caches[len(cache.Caches)-30:] 81 | } 82 | cache.LastCacheTime = now 83 | client.BroadCastMessage("InterfaceTraffic", iface.Name+","+strconv.Itoa(int(now.Unix()))+","+strconv.Itoa(int(cache.LastRx))+","+strconv.Itoa(int(cache.LastTx))) 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /ui/src/components/Utilities/Shell.vue: -------------------------------------------------------------------------------- 1 | 74 | 75 | 78 | 79 | 84 | -------------------------------------------------------------------------------- /backend/als/controller/speedtest/speedtest_cli.go: -------------------------------------------------------------------------------- 1 | package speedtest 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "os/exec" 9 | "sync" 10 | "time" 11 | 12 | "github.com/gin-gonic/gin" 13 | "github.com/samlm0/als/v2/als/client" 14 | ) 15 | 16 | var count = 1 17 | var lock = sync.Mutex{} 18 | 19 | func fakeQueue() { 20 | go func() { 21 | lock.Lock() 22 | count++ 23 | lock.Unlock() 24 | ctx, cancel := context.WithCancel(context.TODO()) 25 | client.WaitQueue(ctx, nil) 26 | fmt.Println(count) 27 | time.Sleep(time.Duration(count) * time.Second) 28 | cancel() 29 | }() 30 | } 31 | 32 | func HandleSpeedtestDotNet(c *gin.Context) { 33 | nodeId, ok := c.GetQuery("node_id") 34 | v, _ := c.Get("clientSession") 35 | clientSession := v.(*client.ClientSession) 36 | if !ok { 37 | nodeId = "" 38 | } 39 | closed := false 40 | timeout := time.Second * 60 41 | count = 1 42 | ctx, cancel := context.WithTimeout(clientSession.GetContext(c.Request.Context()), timeout) 43 | defer func() { 44 | cancel() 45 | closed = true 46 | }() 47 | go func() { 48 | <-ctx.Done() 49 | closed = true 50 | }() 51 | client.WaitQueue(ctx, func() { 52 | pos, totalPos := client.GetQueuePostitionByCtx(ctx) 53 | msg, _ := json.Marshal(gin.H{"type": "queue", "pos": pos, "totalPos": totalPos}) 54 | if !closed { 55 | clientSession.Channel <- &client.Message{ 56 | Name: "SpeedtestStream", 57 | Content: string(msg), 58 | } 59 | } 60 | }) 61 | args := []string{"--accept-license", "-f", "jsonl"} 62 | if nodeId != "" { 63 | args = append(args, "-s", nodeId) 64 | } 65 | cmd := exec.Command("speedtest", args...) 66 | 67 | go func() { 68 | <-ctx.Done() 69 | if cmd.Process != nil { 70 | cmd.Process.Kill() 71 | } 72 | }() 73 | 74 | writer := func(pipe io.ReadCloser, err error) { 75 | if err != nil { 76 | fmt.Println("Pipe closed", err) 77 | return 78 | } 79 | for { 80 | buf := make([]byte, 1024) 81 | n, err := pipe.Read(buf) 82 | if err != nil { 83 | return 84 | } 85 | if !closed { 86 | clientSession.Channel <- &client.Message{ 87 | Name: "SpeedtestStream", 88 | Content: string(buf[:n]), 89 | } 90 | } 91 | } 92 | } 93 | 94 | go writer(cmd.StdoutPipe()) 95 | go writer(cmd.StderrPipe()) 96 | 97 | cmd.Run() 98 | fmt.Println("speedtest-cli quit") 99 | c.JSON(200, &gin.H{ 100 | "success": true, 101 | }) 102 | } 103 | -------------------------------------------------------------------------------- /ui/src/components/Utilities/Ping.vue: -------------------------------------------------------------------------------- 1 | 52 | 53 | 88 | -------------------------------------------------------------------------------- /backend/als/route.go: -------------------------------------------------------------------------------- 1 | package als 2 | 3 | import ( 4 | "io/fs" 5 | "net/http" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/samlm0/als/v2/als/controller" 9 | "github.com/samlm0/als/v2/als/controller/cache" 10 | "github.com/samlm0/als/v2/als/controller/iperf3" 11 | "github.com/samlm0/als/v2/als/controller/ping" 12 | "github.com/samlm0/als/v2/als/controller/session" 13 | "github.com/samlm0/als/v2/als/controller/shell" 14 | "github.com/samlm0/als/v2/als/controller/speedtest" 15 | "github.com/samlm0/als/v2/config" 16 | iEmbed "github.com/samlm0/als/v2/embed" 17 | ) 18 | 19 | func SetupHttpRoute(e *gin.Engine) { 20 | e.GET("/session", session.Handle) 21 | v1 := e.Group("/method", controller.MiddlewareSessionOnHeader()) 22 | { 23 | if config.Config.FeatureIperf3 { 24 | v1.GET("/iperf3/server", iperf3.Handle) 25 | } 26 | 27 | if config.Config.FeaturePing { 28 | v1.GET("/ping", ping.Handle) 29 | } 30 | 31 | if config.Config.FeatureSpeedtestDotNet { 32 | v1.GET("/speedtest_dot_net", speedtest.HandleSpeedtestDotNet) 33 | } 34 | 35 | if config.Config.FeatureIfaceTraffic { 36 | v1.GET("/cache/interfaces", cache.UpdateInterfaceCache) 37 | } 38 | } 39 | 40 | session := e.Group("/session/:session", controller.MiddlewareSessionOnUrl()) 41 | { 42 | if config.Config.FeatureShell { 43 | session.GET("/shell", shell.HandleNewShell) 44 | } 45 | } 46 | 47 | speedtestRoute := session.Group("/speedtest", controller.MiddlewareSessionOnUrl()) 48 | { 49 | if config.Config.FeatureFileSpeedtest { 50 | speedtestRoute.GET("/file/:filename", speedtest.HandleFakeFile) 51 | } 52 | 53 | if config.Config.FeatureLibrespeed { 54 | speedtestRoute.GET("/download", speedtest.HandleDownload) 55 | speedtestRoute.POST("/upload", speedtest.HandleUpload) 56 | } 57 | } 58 | 59 | e.Any("/assets/:filename", func(c *gin.Context) { 60 | filePath := c.Request.RequestURI 61 | filePath = filePath[1:] 62 | handleStatisFile(filePath, c) 63 | }) 64 | 65 | e.GET("/", func(c *gin.Context) { 66 | filePath := "/index.html" 67 | filePath = filePath[1:] 68 | handleStatisFile(filePath, c) 69 | }) 70 | 71 | e.GET("/speedtest_worker.js", func(c *gin.Context) { 72 | handleStatisFile("speedtest_worker.js", c) 73 | }) 74 | 75 | e.GET("/favicon.ico", func(c *gin.Context) { 76 | handleStatisFile("favicon.ico", c) 77 | }) 78 | } 79 | 80 | func handleStatisFile(filePath string, c *gin.Context) { 81 | uiFs := iEmbed.UIStaticFiles 82 | subFs, _ := fs.Sub(uiFs, "ui") 83 | httpFs := http.FileServer(http.FS(subFs)) 84 | _, err := fs.ReadFile(subFs, filePath) 85 | if err != nil { 86 | c.String(404, "Not found") 87 | c.Abort() 88 | return 89 | } 90 | httpFs.ServeHTTP(c.Writer, c.Request) 91 | } 92 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: 'Build with release' 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | build-ui: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | - run: git submodule update --init --recursive 14 | - uses: actions/setup-node@v3 15 | with: 16 | node-version: 'lts/Hydrogen' 17 | cache: 'npm' 18 | cache-dependency-path: 'ui' 19 | - run: npm i 20 | working-directory: ./ui/ 21 | - run: npm run build 22 | working-directory: ./ui/ 23 | - uses: actions/upload-artifact@master 24 | with: 25 | name: ui 26 | path: ./ui/dist 27 | build-linux: 28 | runs-on: ubuntu-latest 29 | needs: build-ui 30 | strategy: 31 | matrix: 32 | goos: [linux] 33 | goarch: [amd64, arm64] 34 | steps: 35 | - uses: actions/checkout@v3 36 | - uses: actions/setup-go@v4 37 | with: 38 | go-version-file: 'backend/go.mod' 39 | cache-dependency-path: 'backend/go.sum' 40 | - uses: actions/download-artifact@master 41 | with: 42 | name: ui 43 | path: backend/embed/ui 44 | - name: Build for Linux 45 | working-directory: ./backend 46 | run: | 47 | env GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} go build -v -o als-${{ matrix.goos }}-${{ matrix.goarch }} 48 | - uses: svenstaro/upload-release-action@v2 49 | with: 50 | repo_token: ${{ secrets.GITHUB_TOKEN }} 51 | file: backend/als-${{ matrix.goos }}-${{ matrix.goarch }} 52 | asset_name: als-${{ matrix.goos }}-${{ matrix.goarch }} 53 | tag: ${{ github.ref }} 54 | build-macos: 55 | runs-on: ubuntu-latest 56 | needs: build-ui 57 | strategy: 58 | matrix: 59 | goos: [darwin] 60 | goarch: [amd64, arm64] 61 | steps: 62 | - uses: actions/checkout@v3 63 | - uses: actions/setup-go@v4 64 | with: 65 | go-version-file: 'backend/go.mod' 66 | cache-dependency-path: 'backend/go.sum' 67 | - uses: actions/download-artifact@master 68 | with: 69 | name: ui 70 | path: backend/embed/ui 71 | - name: Build for macOS 72 | working-directory: ./backend 73 | run: | 74 | env GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} go build -v -o als-${{ matrix.goos }}-${{ matrix.goarch }} 75 | - uses: svenstaro/upload-release-action@v2 76 | with: 77 | repo_token: ${{ secrets.GITHUB_TOKEN }} 78 | file: backend/als-${{ matrix.goos }}-${{ matrix.goarch }} 79 | asset_name: als-${{ matrix.goos }}-${{ matrix.goarch }} 80 | tag: ${{ github.ref }} -------------------------------------------------------------------------------- /backend/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/samlm0/als/v2 2 | 3 | go 1.21.4 4 | 5 | require ( 6 | github.com/creack/pty v1.1.21 7 | github.com/gin-gonic/gin v1.9.1 8 | github.com/google/uuid v1.5.0 9 | github.com/gorilla/websocket v1.5.1 10 | github.com/miekg/dns v1.1.57 11 | github.com/reeflective/console v0.1.15 12 | github.com/samlm0/go-ping v0.1.0 13 | github.com/spf13/cobra v1.8.0 14 | github.com/vishvananda/netlink v1.1.0 15 | ) 16 | 17 | require ( 18 | github.com/bytedance/sonic v1.10.2 // indirect 19 | github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect 20 | github.com/chenzhuoyu/iasm v0.9.1 // indirect 21 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 22 | github.com/frankban/quicktest v1.14.6 // indirect 23 | github.com/gabriel-vasile/mimetype v1.4.3 // indirect 24 | github.com/gin-contrib/sse v0.1.0 // indirect 25 | github.com/go-playground/locales v0.14.1 // indirect 26 | github.com/go-playground/universal-translator v0.18.1 // indirect 27 | github.com/go-playground/validator/v10 v10.16.0 // indirect 28 | github.com/goccy/go-json v0.10.2 // indirect 29 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 30 | github.com/json-iterator/go v1.1.12 // indirect 31 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect 32 | github.com/klauspost/cpuid/v2 v2.2.6 // indirect 33 | github.com/leodido/go-urn v1.2.4 // indirect 34 | github.com/mattn/go-isatty v0.0.20 // indirect 35 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 36 | github.com/modern-go/reflect2 v1.0.2 // indirect 37 | github.com/pelletier/go-toml/v2 v2.1.1 // indirect 38 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 39 | github.com/reeflective/readline v1.0.13 // indirect 40 | github.com/rivo/uniseg v0.4.4 // indirect 41 | github.com/rsteube/carapace v0.46.3-0.20231214181515-27e49f3c3b69 // indirect 42 | github.com/rsteube/carapace-shlex v0.1.1 // indirect 43 | github.com/spf13/pflag v1.0.5 // indirect 44 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 45 | github.com/ugorji/go/codec v1.2.12 // indirect 46 | github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df // indirect 47 | golang.org/x/arch v0.6.0 // indirect 48 | golang.org/x/crypto v0.17.0 // indirect 49 | golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 // indirect 50 | golang.org/x/mod v0.14.0 // indirect 51 | golang.org/x/net v0.19.0 // indirect 52 | golang.org/x/sys v0.16.0 // indirect 53 | golang.org/x/term v0.16.0 // indirect 54 | golang.org/x/text v0.14.0 // indirect 55 | golang.org/x/tools v0.16.0 // indirect 56 | google.golang.org/protobuf v1.31.0 // indirect 57 | gopkg.in/yaml.v3 v3.0.1 // indirect 58 | mvdan.cc/sh/v3 v3.7.0 // indirect 59 | ) 60 | -------------------------------------------------------------------------------- /ui/src/components/Speedtest/FileSpeedtest.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 92 | -------------------------------------------------------------------------------- /backend/config/ip.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "log" 8 | "net" 9 | "net/http" 10 | 11 | "github.com/miekg/dns" 12 | ) 13 | 14 | func updatePublicIP() { 15 | log.Default().Println("Updating IP address from internet...") 16 | // get ipv4 17 | go func() { 18 | addr, err := getPublicIPv4ViaDNS() 19 | if err == nil { 20 | Config.PublicIPv4 = addr 21 | log.Printf("Public IPv4 address: %s\n", addr) 22 | // fmt.Println(Config) 23 | return 24 | } 25 | 26 | addr, err = getPublicIPv4ViaHttp() 27 | if err == nil { 28 | Config.PublicIPv4 = addr 29 | log.Printf("Public IPv4 address: %s\n", addr) 30 | return 31 | } 32 | }() 33 | 34 | // get ipv6 35 | go func() { 36 | addr, err := getPublicIPv6ViaDNS() 37 | if err == nil { 38 | Config.PublicIPv6 = addr 39 | log.Printf("Public IPv6 address: %s\n", addr) 40 | return 41 | } 42 | }() 43 | } 44 | 45 | func getPublicIPv4ViaDNS() (string, error) { 46 | m := new(dns.Msg) 47 | m.SetQuestion("myip.opendns.com.", dns.TypeA) 48 | 49 | in, err := dns.Exchange(m, "resolver1.opendns.com:53") 50 | if err != nil { 51 | return "", err 52 | } 53 | 54 | if len(in.Answer) < 1 { 55 | return "", fmt.Errorf("no answer") 56 | } 57 | 58 | record, ok := in.Answer[0].(*dns.A) 59 | if !ok { 60 | return "", fmt.Errorf("not A record") 61 | } 62 | return record.A.String(), nil 63 | } 64 | 65 | func getPublicIPv6ViaDNS() (string, error) { 66 | m := new(dns.Msg) 67 | m.SetQuestion("myip.opendns.com.", dns.TypeAAAA) 68 | 69 | in, err := dns.Exchange(m, "resolver1.opendns.com:53") 70 | if err != nil { 71 | return "", err 72 | } 73 | 74 | if len(in.Answer) < 1 { 75 | return "", fmt.Errorf("no answer") 76 | } 77 | 78 | record, ok := in.Answer[0].(*dns.AAAA) 79 | if !ok { 80 | return "", fmt.Errorf("not A record") 81 | } 82 | 83 | return record.AAAA.String(), nil 84 | } 85 | 86 | func getPublicIPViaHttp(client *http.Client) (string, error) { 87 | lists := []string{ 88 | "https://myexternalip.com/raw", 89 | "https://ifconfig.co/ip", 90 | } 91 | 92 | for _, url := range lists { 93 | resp, err := client.Get(url) 94 | if err != nil { 95 | continue 96 | } 97 | 98 | body, err := io.ReadAll(resp.Body) 99 | if err != nil { 100 | return "", err 101 | } 102 | 103 | addr := net.ParseIP(string(body)) 104 | if addr != nil { 105 | return addr.String(), nil 106 | } 107 | } 108 | 109 | return "", fmt.Errorf("no answer") 110 | } 111 | 112 | func getPublicIPv4ViaHttp() (string, error) { 113 | client := &http.Client{ 114 | Transport: &http.Transport{ 115 | DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { 116 | var dialer net.Dialer 117 | return dialer.DialContext(ctx, "tcp4", addr) 118 | }, 119 | }, 120 | } 121 | return getPublicIPViaHttp(client) 122 | } 123 | -------------------------------------------------------------------------------- /ui/src/stores/app.js: -------------------------------------------------------------------------------- 1 | import { ref, computed } from 'vue' 2 | import { defineStore } from 'pinia' 3 | import axios from 'axios' 4 | import { formatBytes } from '@/helper/unit' 5 | export const useAppStore = defineStore('app', () => { 6 | const source = ref() 7 | const sessionId = ref() 8 | const connecting = ref(true) 9 | const config = ref() 10 | const drawerWidth = ref() 11 | const memoryUsage = ref() 12 | let timer = '' 13 | 14 | const handleResize = () => { 15 | let width = window.innerWidth 16 | if (width > 800) { 17 | drawerWidth.value = 800 18 | } else { 19 | drawerWidth.value = width 20 | } 21 | } 22 | window.addEventListener('resize', handleResize) 23 | handleResize() 24 | 25 | const reconnectEventSource = () => { 26 | clearTimeout(timer) 27 | setTimeout(() => { 28 | setupEventSource() 29 | }, 1000) 30 | } 31 | 32 | const setupEventSource = () => { 33 | connecting.value = true 34 | const eventSource = new EventSource('./session') 35 | eventSource.addEventListener('SessionId', (e) => { 36 | sessionId.value = e.data 37 | console.log('session', e.data) 38 | }) 39 | 40 | eventSource.addEventListener('Config', (e) => { 41 | config.value = JSON.parse(e.data) 42 | 43 | connecting.value = false 44 | }) 45 | eventSource.addEventListener('MemoryUsage', (e) => { 46 | memoryUsage.value = formatBytes(e.data) 47 | }) 48 | 49 | eventSource.onerror = function (e) { 50 | eventSource.close() 51 | connecting.value = true 52 | console.log('SSE disconnected') 53 | reconnectEventSource() 54 | } 55 | source.value = eventSource 56 | } 57 | 58 | setupEventSource() 59 | 60 | const requestMethod = (method, data = {}, signal = null) => { 61 | let axiosConfig = { 62 | timeout: 1000 * 120, // 请求超时时间 63 | headers: { 64 | session: sessionId.value 65 | } 66 | } 67 | 68 | if (signal != null) { 69 | axiosConfig.signal = signal 70 | } 71 | 72 | const _axios = axios.create(axiosConfig) 73 | 74 | return new Promise((resolve, reject) => { 75 | _axios 76 | .get('./method/' + method, { params: data }) 77 | .then((response) => { 78 | if (response.data.success) { 79 | resolve(response.data) 80 | return 81 | } 82 | reject(response) 83 | }) 84 | .catch((error) => { 85 | if (error.code == 'ERR_CANCELED') { 86 | reject(error) 87 | return 88 | } 89 | console.error(error) 90 | reject(error) 91 | }) 92 | }) 93 | } 94 | 95 | return { 96 | //vars 97 | source, 98 | sessionId, 99 | connecting, 100 | config, 101 | drawerWidth, 102 | memoryUsage, 103 | 104 | //methods 105 | requestMethod 106 | } 107 | }) 108 | -------------------------------------------------------------------------------- /ui/src/App.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 97 | -------------------------------------------------------------------------------- /ui/src/components/Utilities.vue: -------------------------------------------------------------------------------- 1 | 94 | 95 | 115 | -------------------------------------------------------------------------------- /ui/src/components/Utilities/IPerf3.vue: -------------------------------------------------------------------------------- 1 | 64 | 65 | 102 | -------------------------------------------------------------------------------- /backend/config/init.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "net/http" 7 | "os" 8 | "os/exec" 9 | ) 10 | 11 | var Config *ALSConfig 12 | var IsInternalCall bool 13 | 14 | type ALSConfig struct { 15 | ListenHost string `json:"-"` 16 | ListenPort string `json:"-"` 17 | 18 | Location string `json:"location"` 19 | 20 | PublicIPv4 string `json:"public_ipv4"` 21 | PublicIPv6 string `json:"public_ipv6"` 22 | 23 | Iperf3StartPort int `json:"-"` 24 | Iperf3EndPort int `json:"-"` 25 | 26 | SpeedtestFileList []string `json:"speedtest_files"` 27 | 28 | SponsorMessage string `json:"sponsor_message"` 29 | 30 | FeaturePing bool `json:"feature_ping"` 31 | FeatureShell bool `json:"feature_shell"` 32 | FeatureLibrespeed bool `json:"feature_librespeed"` 33 | FeatureFileSpeedtest bool `json:"feature_filespeedtest"` 34 | FeatureSpeedtestDotNet bool `json:"feature_speedtest_dot_net"` 35 | FeatureIperf3 bool `json:"feature_iperf3"` 36 | FeatureMTR bool `json:"feature_mtr"` 37 | FeatureTraceroute bool `json:"feature_traceroute"` 38 | FeatureIfaceTraffic bool `json:"feature_iface_traffic"` 39 | } 40 | 41 | func GetDefaultConfig() *ALSConfig { 42 | defaultConfig := &ALSConfig{ 43 | ListenHost: "0.0.0.0", 44 | ListenPort: "80", 45 | Location: "", 46 | Iperf3StartPort: 30000, 47 | Iperf3EndPort: 31000, 48 | 49 | SpeedtestFileList: []string{"1MB", "10MB", "100MB", "1GB", "100GB"}, 50 | PublicIPv4: "", 51 | PublicIPv6: "", 52 | 53 | FeaturePing: true, 54 | FeatureShell: true, 55 | FeatureLibrespeed: true, 56 | FeatureFileSpeedtest: true, 57 | FeatureSpeedtestDotNet: true, 58 | FeatureIperf3: true, 59 | FeatureMTR: true, 60 | FeatureTraceroute: true, 61 | FeatureIfaceTraffic: true, 62 | } 63 | 64 | return defaultConfig 65 | } 66 | 67 | func Load() { 68 | // default config 69 | Config = GetDefaultConfig() 70 | LoadFromEnv() 71 | } 72 | 73 | func LoadWebConfig() { 74 | Load() 75 | LoadSponsorMessage() 76 | log.Default().Println("Loading config for web services...") 77 | 78 | _, err := exec.LookPath("iperf3") 79 | if err != nil { 80 | log.Default().Println("WARN: Disable iperf3 due to not found") 81 | Config.FeatureIperf3 = false 82 | } 83 | 84 | if Config.PublicIPv4 == "" && Config.PublicIPv6 == "" { 85 | go func() { 86 | updatePublicIP() 87 | if Config.Location == "" { 88 | updateLocation() 89 | } 90 | }() 91 | } 92 | 93 | } 94 | 95 | func LoadSponsorMessage() { 96 | if Config.SponsorMessage == "" { 97 | return 98 | } 99 | 100 | log.Default().Println("Loading sponser message...") 101 | 102 | if _, err := os.Stat(Config.SponsorMessage); err == nil { 103 | content, err := os.ReadFile(Config.SponsorMessage) 104 | if err == nil { 105 | Config.SponsorMessage = string(content) 106 | return 107 | } 108 | } 109 | 110 | resp, err := http.Get(Config.SponsorMessage) 111 | if err == nil { 112 | content, err := io.ReadAll(resp.Body) 113 | if err == nil { 114 | log.Default().Println("Loaded sponser message from url.") 115 | Config.SponsorMessage = string(content) 116 | return 117 | } 118 | } 119 | 120 | log.Default().Println("ERROR: Failed to load sponsor message.") 121 | } 122 | -------------------------------------------------------------------------------- /README_zh_CN.md: -------------------------------------------------------------------------------- 1 | [![docker image build](https://github.com/wikihost-opensource/als/actions/workflows/docker-image.yml/badge.svg)](https://github.com/wikihost-opensource/als/actions/workflows/docker-image.yml) 2 | 3 | 4 | 语言: [English](README.md) | 简体中文 5 | 6 | # ALS - 另一个 Looking-glass 服务器 7 | 8 | ## 快速开始 (Docker 环境) 9 | ``` 10 | docker run -d --name looking-glass --restart always --network host wikihostinc/looking-glass-server 11 | ``` 12 | 13 | [DEMO](http://lg.hk1-bgp.hkg.50network.com/) 14 | 15 | 如果不想使用 Docker , 您可以使用编译好的[服务器端](https://github.com/wikihost-opensource/als/releases) 16 | 17 | ## 配置要求 18 | - 内存: 32MB 或更好 19 | 20 | ## 如何修改配置 21 | ``` 22 | # 你需要在 docker 命令中传递环境变量设置参数: -e KEY=VALUE 23 | # 你可以在 环境变量表 中找到 KEY 24 | # 例如,将监听端口改为 8080 25 | docker run -d \ 26 | --name looking-glass \ 27 | -e HTTP_PORT=8080 \ 28 | --restart always \ 29 | --network host \ 30 | wikihostinc/looking-glass-server 31 | ``` 32 | 33 | ## 环境变量表 34 | | Key | 示例 | 默认 | 描述 | 35 | | ------------------------- | ---------------------------------------------------------------------- | ---------------------------------------------------------- | --------------------------------------------------------------------------------------- | 36 | | LISTEN_IP | 127.0.0.1 | (全部 IP) | 监听在哪一个 IP 上 | 37 | | HTTP_PORT | 80 | 80 | 监听在哪一个端口上 | 38 | | SPEEDTEST_FILE_LIST | 100MB 1GB | 1MB 10MB 100MB 1GB | 静态文件大小列表, 使用空格隔开 | 39 | | LOCATION | "this is location" | (请求 ipapi.co 获取) | 服务器位置的文本 | 40 | | PUBLIC_IPV4 | 1.1.1.1 | (从在线获取) | 服务器的 IPv4 地址 | 41 | | PUBLIC_IPV6 | fe80::1 | (从在线获取) | 服务器的 IPv6 地址 | 42 | | DISPLAY_TRAFFIC | true | true | 实时流量开关 | 43 | | ENABLE_SPEEDTEST | true | true | 测速功能开关 | 44 | | UTILITIES_PING | true | true | Ping 功能开关 | 45 | | UTILITIES_SPEEDTESTDOTNET | true | true | Speedtest.net 功能开关 | 46 | | UTILITIES_FAKESHELL | true | true | Shell 功能开关 | 47 | | UTILITIES_IPERF3 | true | true | iPerf3 服务器功能开关 | 48 | | UTILITIES_IPERF3_PORT_MIN | 30000 | 30000 | iPerf3 服务器端口范围 - 开始 | 49 | | UTILITIES_IPERF3_PORT_MAX | 31000 | 31000 | iPerf3 服务器端口范围 - 结束 | 50 | | SPONSOR_MESSAGE | "Test message" or "/tmp/als_readme.md" or "http://some_host/114514.md" | '' | 显示节点赞助商信息 (支持 Markdown, 支持 URL/文字/文件 (文件需要映射到容器中, 使用映射后的路径) | 51 | 52 | 53 | ## 功能 54 | - [x] HTML 5 速度测试 55 | - [x] Ping - IPv4 / IPv6 56 | - [x] iPerf3 服务器控制 57 | - [x] 实时网卡流量显示 58 | - [x] Speedtest.net 客户端 59 | - [x] 在线 shell 盒子 (限制命令) 60 | - [x] [NextTrace](https://github.com/nxtrace/NTrace-core) 支持 61 | ## Thanks to 62 | https://github.com/librespeed/speedtest 63 | 64 | https://www.jetbrains.com/ 65 | 66 | ## License 67 | 68 | Code is licensed under MIT Public License. 69 | 70 | * If you wish to support my efforts, keep the "Powered by WIKIHOST Opensource - ALS" link intact. 71 | 72 | ## Star History 73 | 74 | [![Star History Chart](https://api.star-history.com/svg?repos=wikihost-opensource/als&type=Date)](https://star-history.com/#wikihost-opensource/als&Date) 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![docker image build](https://github.com/wikihost-opensource/als/actions/workflows/docker-image.yml/badge.svg)](https://github.com/wikihost-opensource/als/actions/workflows/docker-image.yml) 2 | 3 | 4 | Language: English | [简体中文](README_zh_CN.md) 5 | 6 | # ALS - Another Looking-glass Server 7 | 8 | ## Quick start 9 | ``` 10 | docker run -d --name looking-glass --restart always --network host wikihostinc/looking-glass-server 11 | ``` 12 | 13 | [DEMO](http://lg.hk1-bgp.hkg.50network.com/) 14 | 15 | If you don't want to use Docker , you can use the [compiled server](https://github.com/wikihost-opensource/als/releases) 16 | 17 | ## Host Requirements 18 | - RAM: 32MB or more 19 | 20 | ## How to change config 21 | ``` 22 | # you need pass -e KEY=VALUE to docker command 23 | # you can find the KEY below the [Image Environment Variables] 24 | # for example, change the listen port to 8080 25 | docker run -d \ 26 | --name looking-glass \ 27 | -e HTTP_PORT=8080 \ 28 | --restart always \ 29 | --network host \ 30 | wikihostinc/looking-glass-server 31 | ``` 32 | 33 | ## Environment variable table 34 | | Key | Example | Default | Description | 35 | | ------------------------- | ---------------------------------------------------------------------- | ---------------------------------------------------------- | --------------------------------------------------------------------------------------- | 36 | | LISTEN_IP | 127.0.0.1 | (all ip) | which IP address will be listen use | 37 | | HTTP_PORT | 80 | 80 | which HTTP port should use | 38 | | SPEEDTEST_FILE_LIST | 100MB 1GB | 1MB 10MB 100MB 1GB | size of static test files, separate with space | 39 | | LOCATION | "this is location" | (request from http://ipapi.co) | location string | 40 | | PUBLIC_IPV4 | 1.1.1.1 | (fetch from http://ifconfig.co) | The IPv4 address of the server | 41 | | PUBLIC_IPV6 | fe80::1 | (fetch from http://ifconfig.co) | The IPv6 address of the server | 42 | | DISPLAY_TRAFFIC | true | true | Toggle the streaming traffic graph | 43 | | ENABLE_SPEEDTEST | true | true | Toggle the speedtest feature | 44 | | UTILITIES_PING | true | true | Toggle the ping feature | 45 | | UTILITIES_SPEEDTESTDOTNET | true | true | Toggle the speedtest.net feature | 46 | | UTILITIES_FAKESHELL | true | true | Toggle the HTML Shell feature | 47 | | UTILITIES_IPERF3 | true | true | Toggle the iperf3 feature | 48 | | UTILITIES_IPERF3_PORT_MIN | 30000 | 30000 | iperf3 listen port range - from | 49 | | UTILITIES_IPERF3_PORT_MAX | 31000 | 31000 | iperf3 listen port range - to | 50 | | SPONSOR_MESSAGE | "Test message" or "/tmp/als_readme.md" or "http://some_host/114514.md" | '' | Show server sponsor message (support markdown file, required mapping file to container) | 51 | 52 | 53 | ## Features 54 | - [x] HTML 5 Speed Test 55 | - [x] Ping - IPv4 / IPv6 56 | - [x] iPerf3 server 57 | - [x] Streaming traffic graph 58 | - [x] Speedtest.net Client 59 | - [x] Online shell box (limited commands) 60 | - [x] [NextTrace](https://github.com/nxtrace/NTrace-core) Support 61 | ## Thanks to 62 | https://github.com/librespeed/speedtest 63 | 64 | https://www.jetbrains.com/ 65 | 66 | ## License 67 | 68 | Code is licensed under MIT Public License. 69 | 70 | * If you wish to support my efforts, keep the "Powered by WIKIHOST Opensource - ALS" link intact. 71 | 72 | ## Star History 73 | 74 | [![Star History Chart](https://api.star-history.com/svg?repos=wikihost-opensource/als&type=Date)](https://star-history.com/#wikihost-opensource/als&Date) 75 | -------------------------------------------------------------------------------- /ui/src/components/Speedtest/Librespeed.vue: -------------------------------------------------------------------------------- 1 | 170 | 171 | 211 | -------------------------------------------------------------------------------- /ui/src/components/TrafficDisplay.vue: -------------------------------------------------------------------------------- 1 | 208 | 209 | 239 | 240 | 250 | 251 | 257 | -------------------------------------------------------------------------------- /ui/src/components/Utilities/SpeedtestNet.vue: -------------------------------------------------------------------------------- 1 | 130 | 131 | 235 | -------------------------------------------------------------------------------- /backend/go.sum: -------------------------------------------------------------------------------- 1 | github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= 2 | github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= 3 | github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE= 4 | github.com/bytedance/sonic v1.10.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= 5 | github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= 6 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= 7 | github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0= 8 | github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA= 9 | github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= 10 | github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0= 11 | github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= 12 | github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 13 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 14 | github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= 15 | github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 16 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 17 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 18 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 19 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 20 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 21 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 22 | github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= 23 | github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= 24 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 25 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 26 | github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= 27 | github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= 28 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 29 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 30 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 31 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 32 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 33 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 34 | github.com/go-playground/validator/v10 v10.16.0 h1:x+plE831WK4vaKHO/jpgUGsvLKIqRRkz6M78GuJAfGE= 35 | github.com/go-playground/validator/v10 v10.16.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= 36 | github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= 37 | github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 38 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 39 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 40 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 41 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 42 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 43 | github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= 44 | github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 45 | github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= 46 | github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= 47 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 48 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 49 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 50 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 51 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= 52 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= 53 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 54 | github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= 55 | github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 56 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 57 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 58 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 59 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 60 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 61 | github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= 62 | github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= 63 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 64 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 65 | github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= 66 | github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= 67 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 68 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 69 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 70 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 71 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 72 | github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= 73 | github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= 74 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 75 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 76 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 77 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 78 | github.com/reeflective/console v0.1.15 h1:r4M1a19s882znSO5Zkj7memsLSDTLT/0fZwdNzhIldE= 79 | github.com/reeflective/console v0.1.15/go.mod h1:U2i+gzsZ5mT9LZHLzoeuOJ7BtcyXy7l+psRZSu4zmQU= 80 | github.com/reeflective/readline v1.0.13 h1:TeJmYw9B7VRPZWfNExr9QHxL1m0iSicyqBSQIRn39Ss= 81 | github.com/reeflective/readline v1.0.13/go.mod h1:3iOe/qyb2jEy0KqLrNlb/CojBVqxga9ACqz/VU22H6A= 82 | github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= 83 | github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 84 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 85 | github.com/rogpeppe/go-internal v1.10.1-0.20230524175051-ec119421bb97 h1:3RPlVWzZ/PDqmVuf/FKHARG5EMid/tl7cv54Sw/QRVY= 86 | github.com/rogpeppe/go-internal v1.10.1-0.20230524175051-ec119421bb97/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= 87 | github.com/rsteube/carapace v0.46.3-0.20231214181515-27e49f3c3b69 h1:ctOUuKn5PO6VtwtaS7unNrm6u20YXESPtnKEie/u304= 88 | github.com/rsteube/carapace v0.46.3-0.20231214181515-27e49f3c3b69/go.mod h1:4ZC5bulItu9t9sZ5yPcHgPREd8rPf274Q732n+wfl/o= 89 | github.com/rsteube/carapace-shlex v0.1.1 h1:fRQEBBKyYKm4TXUabm4tzH904iFWSmXJl3UZhMfQNYU= 90 | github.com/rsteube/carapace-shlex v0.1.1/go.mod h1:zPw1dOFwvLPKStUy9g2BYKanI6bsQMATzDMYQQybo3o= 91 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 92 | github.com/samlm0/go-ping v0.1.0 h1:ajEqnaEtP2HY9vldc38J2Y2Qve8j+E3jkgwKK+LIoWM= 93 | github.com/samlm0/go-ping v0.1.0/go.mod h1:3cg9EBJvzQ1vZZmTu0E/AQtagyHE7TEs4zXslwFPXLc= 94 | github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= 95 | github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= 96 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 97 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 98 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 99 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 100 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 101 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 102 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 103 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 104 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 105 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 106 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 107 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 108 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 109 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 110 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 111 | github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= 112 | github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 113 | github.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0= 114 | github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= 115 | github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df h1:OviZH7qLw/7ZovXvuNyL3XQl8UFofeikI1NW1Gypu7k= 116 | github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= 117 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 118 | golang.org/x/arch v0.6.0 h1:S0JTfE48HbRj80+4tbvZDYsJ3tGv6BUU3XxyZ7CirAc= 119 | golang.org/x/arch v0.6.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= 120 | golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= 121 | golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= 122 | golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 h1:+iq7lrkxmFNBM7xx+Rae2W6uyPfhPeDWD+n+JgppptE= 123 | golang.org/x/exp v0.0.0-20231219180239-dc181d75b848/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= 124 | golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= 125 | golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 126 | golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= 127 | golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= 128 | golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= 129 | golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 130 | golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 131 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 132 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 133 | golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= 134 | golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 135 | golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= 136 | golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= 137 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 138 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 139 | golang.org/x/tools v0.16.0 h1:GO788SKMRunPIBCXiQyo2AaexLstOrVhuAL5YwsckQM= 140 | golang.org/x/tools v0.16.0/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= 141 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 142 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 143 | google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= 144 | google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 145 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 146 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 147 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 148 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 149 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 150 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 151 | mvdan.cc/sh/v3 v3.7.0 h1:lSTjdP/1xsddtaKfGg7Myu7DnlHItd3/M2tomOcNNBg= 152 | mvdan.cc/sh/v3 v3.7.0/go.mod h1:K2gwkaesF/D7av7Kxl0HbF5kGOd2ArupNTX3X44+8l8= 153 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= 154 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 155 | --------------------------------------------------------------------------------