├── 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 |
2 |
3 |
4 |
5 |
6 |
7 |
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 |
9 |
10 | {{ $t('server_speedtest') }}
11 |
12 |
13 |
14 |
15 |
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 |
18 |
19 | {{ $t('server_info') }}
20 |
21 |
22 |
23 |
24 |
25 | {{ $t(index) }}
26 | {{ appStore.config[key] }}
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | {{ $t('sponsor_message') }}
35 |
38 |
39 |
40 |
41 |
47 |
--------------------------------------------------------------------------------
/ui/src/components/Copy.vue:
--------------------------------------------------------------------------------
1 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | 点击复制
50 | 内容已复制 !
51 |
52 |
53 |
54 |
55 |
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 |
76 |
77 |
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 |
54 |
55 |
56 |
62 |
63 | Stop
64 | Ping
65 |
66 |
67 |
68 |
69 |
70 |
71 | | # |
72 | Host |
73 | TTL |
74 | Latency |
75 |
76 |
77 |
78 |
79 | | {{ record.seq }} |
80 | {{ record.host }} |
81 | {{ record.ttl }} |
82 | {{ record.latency.toFixed(2) }} ms |
83 |
84 |
85 |
86 |
87 |
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 |
9 |
15 |
16 | {{ $t('file_speedtest') }}
17 |
18 | {{ fileSize }}
29 |
30 |
31 |
32 |
33 |
34 |
35 |
{{ $t('file_ipv4_speedtest') }}
36 |
37 | {{ fileSize }}
59 |
60 |
61 |
62 |
63 |
{{ $t('file_ipv6_speedtest') }}
64 |
65 | {{ fileSize }}
87 |
88 |
89 |
90 |
91 |
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 |
55 |
56 |
57 |
58 |
59 | Looking Glass Server
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | Powered by
71 |
77 | WIKIHOST Opensource - ALS (Github)
78 |
79 |
80 |
81 |
{{ $t('memory_usage') }}: {{ appStore.memoryUsage }}
82 |
83 |
84 |
85 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
--------------------------------------------------------------------------------
/ui/src/components/Utilities.vue:
--------------------------------------------------------------------------------
1 |
94 |
95 |
96 |
97 | {{ $t('network_tools') }}
98 |
99 |
100 | {{ tool.label }}
101 |
102 |
103 |
104 |
110 |
111 |
112 |
113 |
114 |
115 |
--------------------------------------------------------------------------------
/ui/src/components/Utilities/IPerf3.vue:
--------------------------------------------------------------------------------
1 |
64 |
65 |
66 |
67 |
73 | 启动 iPerf3 服务器
74 | 停止 iPerf3 服务器
75 |
76 |
83 |
84 |
85 |
86 | // IPv4
87 | iperf3 -c {{ appStore.config.public_ipv4 }} -p {{ port }}
90 |
91 |
92 | // IPv6
93 | iperf3 -c {{ appStore.config.public_ipv6 }} -p {{ port }}
96 |
97 |
98 |
99 |
100 |
101 |
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 | [](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 | [](https://star-history.com/#wikihost-opensource/als&Date)
75 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](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 | [](https://star-history.com/#wikihost-opensource/als&Date)
75 |
--------------------------------------------------------------------------------
/ui/src/components/Speedtest/Librespeed.vue:
--------------------------------------------------------------------------------
1 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
{{ $t('librespeed_download') }}
177 | {{ downloadText }} Mbps
178 |
179 |
186 |
187 |
188 |
189 |
{{ $t('librespeed_upload') }}
190 | {{ uploadText }} Mbps
191 |
192 |
199 |
200 |
201 |
202 |
203 |
204 |
205 | {{ $t('librespeed_stop') }}
206 |
207 | {{ $t('librespeed_begin') }}
208 |
209 |
210 |
211 |
--------------------------------------------------------------------------------
/ui/src/components/TrafficDisplay.vue:
--------------------------------------------------------------------------------
1 |
208 |
209 |
210 |
211 | {{ $t('server_bandwidth_graph') }}
212 |
213 |
214 |
215 |
216 |
217 | {{ $t('server_bandwidth_graph_receive') }}
218 |
219 | {{ formatBytes(interfaceData.traffic.receive, 2, true) }} /
220 | {{ formatBytes(interfaceData.receive) }}
221 |
222 |
223 |
224 | {{ $t('server_bandwidth_graph_sended') }}
225 |
226 | {{ formatBytes(interfaceData.traffic.send, 2, true) }} /
227 | {{ formatBytes(interfaceData.send) }}
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
250 |
251 |
257 |
--------------------------------------------------------------------------------
/ui/src/components/Utilities/SpeedtestNet.vue:
--------------------------------------------------------------------------------
1 |
130 |
131 |
132 |
133 |
134 |
141 | Run
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 | 测速请求正在排队中, 目前您在第 {{ queueStat.pos }} 位 (共 {{ queueStat.total }} 位)
151 |
152 |
153 |
154 |
155 |
156 | 测试很快开始...
157 |
158 |
159 |
160 |
161 |
165 |
166 |
167 |
168 |
169 |
170 |
171 | {{ action }} - 进度
172 | {{ progress.sub }}%
173 |
174 |
180 |
181 | 总进度 {{ progress.full }}%
182 |
183 |
189 |
190 |
191 |
192 |
193 |
194 |
195 | | 服务器 ID |
196 | {{ speedtestData.serverInfo.id }} |
197 |
198 |
199 | | 服务器位置 |
200 | {{ speedtestData.serverInfo.pos }} |
201 |
202 |
203 | | 服务器名称 |
204 | {{ speedtestData.serverInfo.name }} |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 | | 延迟 |
216 | 等待开始 |
217 | {{ speedtestData.ping }} ms |
218 |
219 |
220 | | 下载速度 |
221 | 等待开始 |
222 | {{ speedtestData.download }} |
223 |
224 |
225 | | 上传速度 |
226 | 等待开始 |
227 | {{ speedtestData.upload }} |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
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 |
--------------------------------------------------------------------------------