├── static
├── images
│ ├── nginx.ico
│ ├── nginx.jpg
│ ├── nginx.png
│ ├── favicon.ico
│ └── favicon.jpg
├── js
│ ├── layer
│ │ ├── theme
│ │ │ └── default
│ │ │ │ ├── icon.png
│ │ │ │ ├── icon-ext.png
│ │ │ │ ├── loading-0.gif
│ │ │ │ ├── loading-1.gif
│ │ │ │ └── loading-2.gif
│ │ └── mobile
│ │ │ ├── layer.js
│ │ │ └── need
│ │ │ └── layer.css
│ ├── utils
│ │ ├── string.js
│ │ ├── color.js
│ │ ├── log.js
│ │ ├── array.js
│ │ ├── time.js
│ │ ├── util.js
│ │ └── http.js
│ └── detection.js
├── html
│ ├── nginx-format.html
│ └── index.html
└── css
│ ├── common.css
│ └── grids-responsive-min.css
├── mailtm
├── endpoints.go
├── domain.go
├── helper.go
├── message.go
├── README.md
└── account.go
├── utils
├── array.go
├── object.go
├── map.go
├── exec.go
├── number.go
├── stringbuffer.go
├── utils_test.go
├── utils.go
├── encrypt.go
├── stringjoiner.go
├── file.go
├── time.go
└── bytes.go
├── pyutils
├── py_test.go
├── reg_workshop_keygen.py
├── moba_xterm_Keygen.py
└── nginxfmt.py
├── README.md
├── .github
└── workflows
│ └── upload-to-release.yml
├── go_pack_embed.sh
├── result.go
├── main.go
├── go.mod
├── temp.go
├── reptile
├── reptile_test.go
├── mail.go
├── mail_test.go
├── svp_mail_decode.go
└── chromedp.go
├── router.go
└── app.go
/static/images/nginx.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bajins/tool-gin/HEAD/static/images/nginx.ico
--------------------------------------------------------------------------------
/static/images/nginx.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bajins/tool-gin/HEAD/static/images/nginx.jpg
--------------------------------------------------------------------------------
/static/images/nginx.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bajins/tool-gin/HEAD/static/images/nginx.png
--------------------------------------------------------------------------------
/static/images/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bajins/tool-gin/HEAD/static/images/favicon.ico
--------------------------------------------------------------------------------
/static/images/favicon.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bajins/tool-gin/HEAD/static/images/favicon.jpg
--------------------------------------------------------------------------------
/static/js/layer/theme/default/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bajins/tool-gin/HEAD/static/js/layer/theme/default/icon.png
--------------------------------------------------------------------------------
/static/js/layer/theme/default/icon-ext.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bajins/tool-gin/HEAD/static/js/layer/theme/default/icon-ext.png
--------------------------------------------------------------------------------
/static/js/layer/theme/default/loading-0.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bajins/tool-gin/HEAD/static/js/layer/theme/default/loading-0.gif
--------------------------------------------------------------------------------
/static/js/layer/theme/default/loading-1.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bajins/tool-gin/HEAD/static/js/layer/theme/default/loading-1.gif
--------------------------------------------------------------------------------
/static/js/layer/theme/default/loading-2.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bajins/tool-gin/HEAD/static/js/layer/theme/default/loading-2.gif
--------------------------------------------------------------------------------
/mailtm/endpoints.go:
--------------------------------------------------------------------------------
1 | package mailtm
2 |
3 | const (
4 | URL = "https://api.mail.tm"
5 | URI_DOMAINS = "https://api.mail.tm/domains"
6 | URI_ACCOUNTS = "https://api.mail.tm/accounts"
7 | URI_ME = "https://api.mail.tm/me"
8 | URI_MESSAGES = "https://api.mail.tm/messages"
9 | URI_TOKEN = "https://api.mail.tm/token"
10 | )
11 |
--------------------------------------------------------------------------------
/utils/array.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "math/rand"
5 | "time"
6 | )
7 |
8 | // Shuffle 随机打乱数组顺序
9 | func Shuffle(slice []interface{}) {
10 | r := rand.New(rand.NewSource(time.Now().Unix()))
11 | for len(slice) > 0 {
12 | n := len(slice)
13 | randIndex := r.Intn(n)
14 | slice[n-1], slice[randIndex] = slice[randIndex], slice[n-1]
15 | slice = slice[:n-1]
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/pyutils/py_test.go:
--------------------------------------------------------------------------------
1 | package pyutils
2 |
3 | import (
4 | "runtime"
5 | "testing"
6 | "tool-gin/utils"
7 | )
8 |
9 | //test测试
10 | func TestCmdPython(t *testing.T) {
11 | //result, err := utils.ExecutePython("xshell_key.py", "Xshell Plus", "6")
12 | //result, err := utils.ExecutePython("moba_xterm_Keygen.py", utils.OsPath(),"11.1")
13 | result, err := utils.ExecutePython("reg_workshop_keygen.py", "10")
14 | if err != nil {
15 | t.Error(err)
16 | return
17 | }
18 | t.Log("转换成功", result)
19 | }
20 | func TestOS(t *testing.T) {
21 | t.Log("转换成功", runtime.GOOS)
22 | }
23 |
--------------------------------------------------------------------------------
/utils/object.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "fmt"
5 | "time"
6 | )
7 |
8 | // TypeJudgment 判断数据类型
9 | func TypeJudgment(f interface{}) {
10 | switch vv := f.(type) {
11 | case string:
12 | if f != nil {
13 | fmt.Println("is string ", vv)
14 | }
15 | case int:
16 | if f.(int) > 0 {
17 | fmt.Println("is int ", vv)
18 | }
19 | case int64:
20 | if f.(int64) > 0 {
21 | fmt.Println("is int ", vv)
22 | }
23 | case time.Time:
24 | if !f.(time.Time).IsZero() {
25 | fmt.Println("is time.Time ", vv)
26 | }
27 | case []interface{}:
28 | fmt.Println("is array:")
29 | for i, j := range vv {
30 | fmt.Println(i, j)
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/utils/map.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "encoding/json"
5 | "reflect"
6 | )
7 |
8 | // StructToMapByReflect 结构体转map
9 | func StructToMapByReflect(s interface{}) map[string]interface{} {
10 | elem := reflect.ValueOf(&s).Elem()
11 | type_ := elem.Type()
12 |
13 | map_ := map[string]interface{}{}
14 |
15 | for i := 0; i < type_.NumField(); i++ {
16 | map_[type_.Field(i).Name] = elem.Field(i).Interface()
17 | }
18 | return map_
19 | }
20 |
21 | // StructToMapByJson 结构体转map
22 | func StructToMapByJson(s interface{}) map[string]interface{} {
23 | m := make(map[string]interface{})
24 | // 对象转换为JSON
25 | j, _ := json.Marshal(&s)
26 | // JSON 转换回对象
27 | json.Unmarshal(j, &m)
28 | return m
29 | }
30 |
--------------------------------------------------------------------------------
/mailtm/domain.go:
--------------------------------------------------------------------------------
1 | package mailtm
2 |
3 | import (
4 | "encoding/json"
5 | "time"
6 | )
7 |
8 | type Domain struct {
9 | Id string `json:"id"`
10 | Domain string `json:"domain"`
11 | IsActive bool `json:"isActive"`
12 | IsPrivate bool `json:"isPrivate"`
13 | CreatedAt time.Time `json:"createdAt"`
14 | UpdatedAt time.Time `json:"updatedAt"`
15 | }
16 |
17 | func AvailableDomains() ([]Domain, error) {
18 | request := requestData{
19 | uri: URI_DOMAINS,
20 | method: "GET",
21 | }
22 | response, err := makeRequest(request)
23 | if err != nil {
24 | return nil, err
25 | }
26 | if response.code != 200 {
27 | return nil, err
28 | }
29 | data := map[string][]Domain{}
30 | json.Unmarshal(response.body, &data)
31 | return data["hydra:member"], nil
32 | }
33 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # tool-gin
2 |
3 | > 基于go-gin框架
4 |
5 | **此分支使用了`chromedp`**
6 |
7 | ~~**必须安装`Chrome`**~~
8 |
9 | > [CentOS安装Chrome](https://www.bajins.com/System/CentOS.html#chrome)
10 |
11 |
12 | **当前分支使用了go:embed内嵌资源文件实现打包到一个二进制中,旧的压缩打包方式请访问分支:[zi-pack](https://github.com/woytu/tool-gin/tree/zip-pack)**
13 |
14 |
15 |
16 | ## 功能
17 |
18 | - 生成激活key
19 | - [mobaXtermGenerater.js](https://github.com/inused/pages/blob/master/file/tool/js/mobaXtermGenerater.js)
20 | - 获取`xshell`、`xftp`、`xmanager`下载链接
21 | - 格式化NGNIX配置
22 | - 获取Navicat下载地址
23 |
24 |
25 | ## 使用
26 |
27 | ### 到[releases](https://github.com/woytu/tool-gin/releases)下载解压并运行
28 |
29 | ```bash
30 | # Windows
31 | # 双击tool-gin-windows.exe根据默认端口8000运行
32 | # 或者在cmd、power shell中
33 | tool-gin-windows.exe
34 |
35 |
36 | # Linux
37 | nohup ./tool-gin_linux_amd64 -p 5000 >/dev/null 2>&1 &
38 | ```
39 |
--------------------------------------------------------------------------------
/utils/exec.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "os/exec"
7 | )
8 |
9 | // ExecutePython 执行python脚本
10 | // script: 要执行的Python脚本,应该是完整路径
11 | // args: 参数
12 | func ExecutePython(script string, args ...string) (string, error) {
13 | if !IsFileExist(script) {
14 | return "", errors.New(fmt.Sprintf(script, "error:%s", "文件不存在"))
15 | }
16 | name := "python"
17 | // 判断是否同时装了python2.7和python3,优先使用python3
18 | _, err := Execute("python3", "-V")
19 | if err == nil {
20 | name = "python3"
21 | }
22 | // 把脚本和参数组合到一个字符串数组
23 | args = append([]string{script}, args...)
24 | return Execute(name, args...)
25 | }
26 |
27 | // Execute 执行dos或shell命令
28 | // program: 程序名称
29 | // args: 参数
30 | func Execute(program string, args ...string) (string, error) {
31 | // exit status 2 一般是文件没有找到
32 | // exit status 1 一般是命令执行错误
33 | out, err := exec.Command(program, args...).Output()
34 | return string(out), err
35 | }
36 |
--------------------------------------------------------------------------------
/utils/number.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "fmt"
5 | "math/rand"
6 | "reflect"
7 | "strconv"
8 | "time"
9 | )
10 |
11 | // FloatRound 截取小数位数
12 | func FloatRound(f float64, n int) float64 {
13 | format := "%." + strconv.Itoa(n) + "f"
14 | res, _ := strconv.ParseFloat(fmt.Sprintf(format, f), 64)
15 | return res
16 | }
17 |
18 | // ToInt64 将任何数值转换为int64
19 | func ToInt64(value interface{}) (d int64, err error) {
20 | val := reflect.ValueOf(value)
21 | switch value.(type) {
22 | case int, int8, int16, int32, int64:
23 | d = val.Int()
24 | case uint, uint8, uint16, uint32, uint64:
25 | d = int64(val.Uint())
26 | default:
27 | err = fmt.Errorf("ToInt64 need numeric not `%T`", value)
28 | }
29 |
30 | return
31 | }
32 |
33 | // RandIntn 生成随机数
34 | //
35 | // 首先要初始化随机种子,不然每次生成都是(指每次重新开始)相同的数
36 | // 系统每次都会先用Seed函数初始化系统资源,如果用户不提供seed参数,则默认用seed=1来初始化,这就是为什么每次都输出一样的值的原因
37 | func RandIntn(length int) int {
38 | // 用一个不确定数来初始化随机种子
39 | rand.Seed(time.Now().UnixNano())
40 | return rand.Intn(length)
41 | }
42 |
--------------------------------------------------------------------------------
/.github/workflows/upload-to-release.yml:
--------------------------------------------------------------------------------
1 | # .github/workflows/upload-to-release.yml
2 |
3 | name: Go Release
4 |
5 | on:
6 | release:
7 | #types: [published]
8 | types: [created]
9 |
10 |
11 | jobs:
12 |
13 | release-matrix:
14 | if: github.repository == 'bajins/tool-gin'
15 | name: Build with go on ubuntu-latest and upload
16 | runs-on: ubuntu-latest
17 |
18 | #strategy:
19 | #matrix:
20 | #goos: [linux, windows, darwin]
21 | #goarch: ["386", amd64]
22 |
23 | steps:
24 |
25 | - name: Install Go
26 | uses: actions/setup-go@v3
27 | with:
28 | go-version: '^1.20'
29 | id: go
30 |
31 | - name: Check out source code
32 | uses: actions/checkout@v3
33 |
34 | - name: Get dependencies
35 | run: |
36 | go get -v -t -d ./...
37 | if [ -f Gopkg.toml ]; then
38 | curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
39 | dep ensure
40 | fi
41 | - name: Build and compression
42 | run: |
43 | bash go_pack_embed.sh
44 | - name: Upload to release
45 | uses: xresloader/upload-to-github-release@v1
46 | env:
47 | GITHUB_TOKEN: ${{ secrets.PERSONAL_TOKEN }}
48 | with:
49 | file: "build/tool-gin*;*.tar.gz;*.zip"
50 | tags: true
51 | draft: false
52 | update_latest_release: true
53 |
--------------------------------------------------------------------------------
/mailtm/helper.go:
--------------------------------------------------------------------------------
1 | package mailtm
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "io"
7 | "math/rand"
8 | "net/http"
9 | "time"
10 | )
11 |
12 | const charset = "abcdefghijklmnopqrstuvwxyz0123456789"
13 |
14 | type requestData struct {
15 | uri string
16 | method string
17 | body map[string]string
18 | bearer string
19 | }
20 |
21 | type responseData struct {
22 | code int
23 | body []byte
24 | }
25 |
26 | func generateString(length int) string {
27 | var seededRand *rand.Rand = rand.New(
28 | rand.NewSource(time.Now().UnixNano()))
29 | b := make([]byte, length)
30 | for i := 0; i < len(b); i++ {
31 | b[i] = charset[seededRand.Intn(len(charset))]
32 | }
33 | return string(b)
34 | }
35 |
36 | func makeRequest(data requestData) (responseData, error) {
37 | var body []byte
38 | if data.body != nil {
39 | var err error
40 | body, err = json.Marshal(data.body)
41 | if err != nil {
42 | return responseData{}, err
43 | }
44 | }
45 | request, err := http.NewRequest(data.method, data.uri, bytes.NewBuffer(body))
46 | request.Header.Set("Content-Type", "application/json")
47 | if err != nil {
48 | return responseData{}, err
49 | }
50 | if data.bearer != "" {
51 | request.Header.Add("Authorization", "Bearer "+data.bearer)
52 | }
53 | client := new(http.Client)
54 | response, err := client.Do(request)
55 | if err != nil {
56 | return responseData{}, err
57 | }
58 | if response.StatusCode == 429 {
59 | time.Sleep(1 * time.Second)
60 | return makeRequest(data)
61 | }
62 | resBody, err := io.ReadAll(response.Body)
63 | defer response.Body.Close()
64 | if err != nil {
65 | return responseData{}, err
66 | }
67 | return responseData{
68 | code: response.StatusCode,
69 | body: resBody,
70 | }, nil
71 | }
72 |
--------------------------------------------------------------------------------
/static/js/utils/string.js:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * @Description:
4 | * @Author: claer
5 | * @File: string.js
6 | * @Version: 1.0.0
7 | * @Time: 2019/9/15 20:03
8 | * @Project: tool-gin
9 | * @Package:
10 | * @Software: GoLand
11 | */
12 |
13 |
14 | /**
15 | * 生成一个指定长度的随机字符串
16 | *
17 | * @param len 指定长度
18 | * @param str 指定字符串范围,默认小写字母、数字、下划线
19 | * @returns {string}
20 | */
21 | const randomString = (len, str) => {
22 | str = str || 'abcdefghijklmnopqrstuvwxyz0123456789_';
23 | let randomString = '';
24 | for (let i = 0; i < len; i++) {
25 | let randomPoz = Math.floor(Math.random() * str.length);
26 | randomString += str.substring(randomPoz, randomPoz + 1);
27 | }
28 | return randomString;
29 | }
30 |
31 |
32 | /**
33 | * 正则表达式去除空行
34 | *
35 | * @param oldStr
36 | * @returns {string}
37 | */
38 | const replaceBlank = (oldStr) => {
39 | if (typeof oldStr != "string") {
40 | throw new Error("正则表达式去除空行,传入的不为字符串!");
41 | }
42 | // 匹配空行
43 | let reg = /\n(\n)*( )*(\n)*\n/g;
44 | return oldStr.replace(reg, "\n");
45 | }
46 |
47 |
48 | /**
49 | * 格式化数字为字符串
50 | *
51 | * @param n
52 | * @returns {string}
53 | */
54 | const formatNumber = (n) => {
55 | n = n.toString();
56 | return n[1] ? n : '0' + n;
57 | }
58 |
59 |
60 | /**
61 | * export default 服从 ES6 的规范,补充:default 其实是别名
62 | * module.exports 服从 CommonJS 规范 https://javascript.ruanyifeng.com/nodejs/module.html
63 | * 一般导出一个属性或者对象用 export default
64 | * 一般导出模块或者说文件使用 module.exports
65 | *
66 | * import from 服从ES6规范,在编译器生效
67 | * require 服从ES5 规范,在运行期生效
68 | * 目前 vue 编译都是依赖label 插件,最终都转化为ES5
69 | *
70 | * @return 将方法、变量暴露出去
71 | */
72 | export default {
73 | randomString,
74 | replaceBlank,
75 | formatNumber
76 | }
--------------------------------------------------------------------------------
/go_pack_embed.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # 脚本当前目录绝对路径
4 | project_path=$(
5 | cd "$(dirname "$0")" || exit
6 | pwd
7 | )
8 | # 脚本当前目录名
9 | project_name="${project_path##*/}"
10 |
11 | # 获得所有受支持平台的列表
12 | #os_arch_array=($(go tool dist list))
13 | #for i in "${!os_arch_array[@]}"; do
14 | # item=${array[i]}
15 | os_arch_array=$(go tool dist list)
16 |
17 | # 保存默认分隔符
18 | OLD_IFS="$IFS"
19 | for item in ${os_arch_array}; do
20 | # array=(${item//\// }) # 替换\为空格(IFS的默认分隔符)
21 | # 指定分隔符
22 | IFS="/"
23 | array=(${item})
24 |
25 | os=${array[0]}
26 | arch=${array[1]}
27 | binary_file=${project_name}_${os}_${arch}
28 |
29 | # 忽略以下平台的编译
30 | if [[ "$os" == "android" ]] || [[ "$os" == "ios" ]] || [[ "$os" == "darwin" && "$arch" == *arm* ]]; then
31 | continue
32 | fi
33 | # if [ "$os" != "linux" ] || [ "$arch" != "amd64" ]; then
34 | # continue
35 | # fi
36 |
37 | # 设置变量
38 | export GOOS="$os" GOARCH="$arch"
39 |
40 | echo "环境变量设置成功:$GOOS------$GOARCH"
41 |
42 | # 指定编译的二进制文件名
43 | if [ "$os" == "windows" ]; then
44 | binary_file="$binary_file".exe
45 | fi
46 |
47 | # 编译文件不同的系统架构使用不同的命令参数
48 | if [ "$os" == "android" ]; then
49 | # 开启 CGO
50 | # export CGO_ENABLED=1
51 | # flags="-s -w -linkmode=external -extldflags=-pie"
52 | : # 占位
53 | elif [ "$os" == "darwin" ]; then
54 | # 开启 CGO
55 | # export CGO_ENABLED=1
56 | flags="-s -w"
57 | elif [ "$os" == "windows" ]; then
58 | # 交叉编译不支持 CGO 所以要禁用它
59 | # export CGO_ENABLED=0
60 | # flags="-s -w -H windowsgui"
61 | : # 占位
62 | else
63 | # 交叉编译不支持 CGO 所以要禁用它
64 | export CGO_ENABLED=0
65 | flags="-s -w"
66 | fi
67 | # 编译二进制文件并输出到build目录
68 | go build -ldflags=$flags -o "build/$binary_file" -buildvcs=false -trimpath
69 | done
70 |
71 | # 还原默认分隔符
72 | IFS="$OLD_IFS"
73 |
74 | # 编译完成清理缓存
75 | go clean -cache
76 |
--------------------------------------------------------------------------------
/utils/stringbuffer.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "fmt"
5 | "strconv"
6 | "strings"
7 | "sync"
8 | )
9 |
10 | // StringBuffer 是一个线程安全的、可变的字符串缓冲区。
11 | // 它通过在 strings.Builder 周围封装一个互斥锁来实现。
12 | type StringBuffer struct {
13 | builder strings.Builder
14 | mu sync.Mutex
15 | }
16 |
17 | // NewStringBuffer 创建并返回一个新的 StringBuffer 实例。
18 | func NewStringBuffer() *StringBuffer {
19 | return &StringBuffer{}
20 | }
21 |
22 | // Append 将任意类型的值转换为字符串并追加到缓冲区。
23 | // 它使用 type switch 为常见类型提供高性能转换。
24 | // 这是一个线程安全的操作,并支持链式调用。
25 | func (sb *StringBuffer) Append(val interface{}) *StringBuffer {
26 | sb.mu.Lock()
27 | defer sb.mu.Unlock()
28 |
29 | // 使用 Type Switch 为特定类型提供高效的字符串转换
30 | switch v := val.(type) {
31 | case string:
32 | sb.builder.WriteString(v)
33 | case int:
34 | sb.builder.WriteString(strconv.Itoa(v))
35 | case int64:
36 | sb.builder.WriteString(strconv.FormatInt(v, 10))
37 | case uint:
38 | sb.builder.WriteString(strconv.FormatUint(uint64(v), 10))
39 | case uint64:
40 | sb.builder.WriteString(strconv.FormatUint(v, 10))
41 | case float32:
42 | sb.builder.WriteString(strconv.FormatFloat(float64(v), 'f', -1, 32))
43 | case float64:
44 | sb.builder.WriteString(strconv.FormatFloat(v, 'f', -1, 64))
45 | case bool:
46 | sb.builder.WriteString(strconv.FormatBool(v))
47 | case []byte:
48 | sb.builder.Write(v)
49 | case rune:
50 | sb.builder.WriteRune(v)
51 | default:
52 | // 对于其他所有类型,回退到 fmt.Sprintf
53 | sb.builder.WriteString(fmt.Sprintf("%v", v))
54 | }
55 |
56 | return sb
57 | }
58 |
59 | // Len 返回缓冲区中当前存储的字节数。
60 | // 这是一个线程安全的操作。
61 | func (sb *StringBuffer) Len() int {
62 | sb.mu.Lock()
63 | defer sb.mu.Unlock()
64 | return sb.builder.Len()
65 | }
66 |
67 | // String 返回缓冲区内容的字符串表示。
68 | // 这是一个线程安全的操作。
69 | func (sb *StringBuffer) String() string {
70 | sb.mu.Lock()
71 | defer sb.mu.Unlock()
72 | return sb.builder.String()
73 | }
74 |
75 | // Reset 清空缓冲区,使其可以被重用。
76 | // 这是一个线程安全的操作。
77 | func (sb *StringBuffer) Reset() {
78 | sb.mu.Lock()
79 | defer sb.mu.Unlock()
80 | sb.builder.Reset()
81 | }
82 |
--------------------------------------------------------------------------------
/static/js/detection.js:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * @Description:
4 | * @Author: https://www.bajins.com
5 | * @File: detection.js
6 | * @Version: 1.0.0
7 | * @Time: 2019/11/26/026 15:49
8 | * @Project: tool-gin
9 | * @Package:
10 | * @Software: GoLand
11 | */
12 |
13 | $(function () {
14 | let area_width = "500px";
15 | if (IEVersion() != -1) {
16 | const html = `
17 | 不支持IE,请使用其他浏览器
18 |
`;
19 | //自定页
20 | layer.open({
21 | // 在默认状态下,layer是宽高都自适应的,但当你只想定义宽度时,你可以area: '500px',高度仍然是自适应的。
22 | // 当你宽高都要定义时,你可以area: ['500px', '300px']
23 | area: [area_width],
24 | type: 1,
25 | title: false,
26 | content: html,
27 | scrollbar: false,
28 | closeBtn: 0
29 | });
30 | }
31 | /**
32 | * 监听窗口变化
33 | */
34 | window.onresize = function () {
35 | if (window.innerWidth <= 600) {
36 | area_width = "80%";
37 | }
38 | }
39 | })
40 |
41 | /**
42 | * 判断IE以及Edge浏览器的版本
43 | *
44 | * @returns {string|number}
45 | * @constructor
46 | */
47 | function IEVersion() {
48 | // 取得浏览器的userAgent字符串
49 | var userAgent = navigator.userAgent;
50 | // 判断是否IE<11浏览器
51 | if (userAgent.indexOf("compatible") > -1 && userAgent.indexOf("MSIE") > -1) {
52 | var reIE = new RegExp("MSIE (\\d+\\.\\d+);");
53 | reIE.test(userAgent);
54 | var fIEVersion = parseFloat(RegExp["$1"]);
55 | if (fIEVersion == 7) {
56 | return 7;
57 | } else if (fIEVersion == 8) {
58 | return 8;
59 | } else if (fIEVersion == 9) {
60 | return 9;
61 | } else if (fIEVersion == 10) {
62 | return 10;
63 | }
64 | // IE版本<=7
65 | else {
66 | return 6;
67 | }
68 | }
69 | // 判断是否IE的Edge浏览器
70 | else if (userAgent.indexOf("Edge") > -1 && ("ActiveXObject" in window)) {
71 | return 'edge';//edge
72 | }
73 | // IE11
74 | else if (userAgent.indexOf('Trident') > -1 && userAgent.indexOf("rv:11.0") > -1) {
75 | return 11;
76 | }
77 | // 不是ie浏览器
78 | else {
79 | return -1;
80 | }
81 | }
--------------------------------------------------------------------------------
/result.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/gin-gonic/gin"
7 | )
8 |
9 | // 定义业务状态码常量 net/http/status.go
10 | const (
11 | SUCCESS = 0
12 | ERROR = http.StatusInternalServerError
13 |
14 | ErrInvalidParams = 1002
15 | )
16 |
17 | // MsgFlags 存储状态码对应的消息
18 | var MsgFlags = map[int]string{
19 | SUCCESS: "success",
20 | ERROR: "fail",
21 |
22 | ErrInvalidParams: "请求参数错误",
23 | }
24 |
25 | // GetMsg 根据状态码获取消息
26 | func GetMsg(code int) string {
27 | msg, ok := MsgFlags[code]
28 | if ok {
29 | return msg
30 | }
31 | // 如果未找到,返回通用的服务器错误消息
32 | return MsgFlags[ERROR]
33 | }
34 |
35 | // Response 是返回给前端的统一结构体
36 | type Response struct {
37 | Code int `json:"code"` // 业务状态码
38 | Message string `json:"message"` // 提示信息
39 | Data interface{} `json:"data"` // 数据
40 | }
41 |
42 | // Result 是一个辅助函数,用于发送统一格式的响应
43 | func Result(c *gin.Context, code int, msg string, data interface{}) {
44 | c.JSON(http.StatusOK, Response{
45 | Code: code,
46 | Message: msg,
47 | Data: data,
48 | })
49 | /*var d = &struct {
50 | code int
51 | message string
52 | data interface{}
53 | }{code:code,message:msg,data:""}*/
54 | //c.JSON(code, gin.H{"code": code, "message": msg, "data": ""})
55 | }
56 |
57 | // respond 是一个内部方法,用于发送 JSON 响应
58 | func (ctx *Context) respond(httpStatus int, code int, msg string, data interface{}) {
59 | ctx.C.JSON(httpStatus, Response{
60 | Code: code,
61 | Message: msg,
62 | Data: data,
63 | })
64 | }
65 |
66 | func (ctx *Context) SuccessJSON(msg string, data interface{}) {
67 | ctx.respond(http.StatusOK, http.StatusOK, msg, data)
68 | }
69 |
70 | func (ctx *Context) ErrorJSON(code int, msg string) {
71 | ctx.respond(http.StatusOK, code, msg, nil)
72 | }
73 |
74 | func (ctx *Context) SystemErrorJSON(code int, msg string) {
75 | ctx.respond(http.StatusInternalServerError, code, msg, nil)
76 | }
77 |
78 | // ErrorByCode 方法,根据预定义的错误码发送失败响应
79 | func (ctx *Context) ErrorByCode(code int) {
80 | msg := GetMsg(code)
81 | ctx.respond(http.StatusOK, code, msg, nil)
82 | }
83 |
84 | // BindAndValidate 方法,封装了参数绑定和验证逻辑
85 | // 如果出错,它会自动发送错误响应
86 | func (ctx *Context) BindAndValidate(obj interface{}) bool {
87 | if err := ctx.C.ShouldBind(obj); err != nil {
88 | // 如果绑定或验证失败,直接返回参数错误
89 | ctx.ErrorByCode(ErrInvalidParams)
90 | return false
91 | }
92 | return true
93 | }
94 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | // 导包
4 | import (
5 | "log"
6 | "os"
7 | "os/signal"
8 | "syscall"
9 | )
10 |
11 | // 初始化函数
12 | func init() {
13 | // --- 设置日志初始化参数 开启文件名和行号显示 ---
14 | // 使用 | (位或运算符) 来组合多个标志
15 | // log.LstdFlags 包含了日期和时间 (log.Ldate | log.Ltime)
16 | // log.Lshortfile 简要文件路径,log.Llongfile 完整文件路径
17 | log.SetFlags(log.Lshortfile | log.LstdFlags)
18 |
19 | // github.com/sirupsen/logrus
20 | // 报告调用者的信息
21 | //log.SetReportCaller(true)
22 | // 自定义格式化器
23 | /*log.SetFormatter(&log.TextFormatter{
24 | FullTimestamp: true,
25 | // 让文件和行号信息更突出
26 | CallerPrettyfier: func(f *log.Frame) (string, string) {
27 | return "",
28 | // f.Function, // 这里可以返回函数名
29 | // 格式化为 file:line
30 | fmt.Sprintf(" <%s:%d>", f.File, f.Line)
31 | },
32 | })*/
33 |
34 | // 设置项目为发布环境
35 | //gin.SetMode(gin.ReleaseMode)
36 | }
37 |
38 | // 运行主体函数
39 | func main() {
40 |
41 | go run()
42 |
43 | // 通过WaitGroup管理两个协程,主协程等待两个子协程退出
44 | /*noChan := make(chan int)
45 | waiter := &sync.WaitGroup{}
46 | waiter.Add(2) // WaitGroup 计数器+2
47 | go func(ch chan int, wt *sync.WaitGroup) {
48 | data := <-ch
49 | log.Println("receive data ", data)
50 | wt.Done() // goroutine 结束时,计数器-1
51 | }(noChan, waiter)
52 |
53 | go func(ch chan int, wt *sync.WaitGroup) {
54 | ch <- 5
55 | log.Println("send data ", 5)
56 | wt.Done() // goroutine 结束时,计数器-1
57 | }(noChan, waiter)
58 | waiter.Wait()*/
59 |
60 | // Go 通过向一个通道发送 `os.Signal` 值来进行信号通知。
61 | // 创建一个通道来接收这些通知(同时还创建一个用于在程序可以结束时进行通知的通道)。
62 | sigs := make(chan os.Signal, 1)
63 | done := make(chan bool, 1)
64 |
65 | // `signal.Notify` 注册这个给定的通道用于接收特定信号。
66 | signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
67 | //signal.Notify(sigs, os.Interrupt)
68 |
69 | // 启用Go协程执行一个阻塞的信号接收操作。
70 | go func() {
71 | /*for s := range sigs {
72 | switch s {
73 | case syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT:
74 |
75 | //os.Exit(1)
76 | default:
77 | log.Println("other", s)
78 | }
79 | }*/
80 | /*select {
81 | case sig := <-sigs:
82 | log.Printf("Got %s signal. Aborting...\n", sig)
83 | //os.Exit(1)
84 | }*/
85 |
86 | // 得到一个信号值
87 | sig := <-sigs
88 | log.Println("得到一个信号值:", sig)
89 |
90 | DestroyTempDir()
91 |
92 | // 通知程序可以退出
93 | done <- true
94 | }()
95 |
96 | // 程序将在这里进行等待,直到它得到了期望的信号
97 | // (也就是上面的 Go 协程发送的 `done` 值)然后退出。
98 | <-done
99 | }
100 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module tool-gin
2 |
3 | go 1.25.2
4 |
5 | require (
6 | github.com/Davincible/chromedp-undetected v1.3.8
7 | github.com/PuerkitoBio/goquery v1.10.3
8 | github.com/antchfx/htmlquery v1.3.4
9 | github.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394
10 | github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d
11 | github.com/chromedp/chromedp v0.14.2
12 | github.com/felixstrobel/mailtm v0.4.0
13 | github.com/gin-contrib/static v1.1.5
14 | github.com/gin-gonic/gin v1.11.0
15 | github.com/go-resty/resty/v2 v2.16.5
16 | golang.org/x/crypto v0.43.0
17 | golang.org/x/net v0.46.0
18 | golang.org/x/text v0.30.0
19 | )
20 |
21 | require (
22 | github.com/Xuanwo/go-locale v1.1.3 // indirect
23 | github.com/andybalholm/cascadia v1.3.3 // indirect
24 | github.com/antchfx/xpath v1.3.5 // indirect
25 | github.com/bytedance/gopkg v0.1.3 // indirect
26 | github.com/bytedance/sonic v1.14.1 // indirect
27 | github.com/bytedance/sonic/loader v0.3.0 // indirect
28 | github.com/chromedp/sysutil v1.1.0 // indirect
29 | github.com/cloudwego/base64x v0.1.6 // indirect
30 | github.com/gabriel-vasile/mimetype v1.4.10 // indirect
31 | github.com/gin-contrib/sse v1.1.0 // indirect
32 | github.com/go-json-experiment/json v0.0.0-20250910080747-cc2cfa0554c3 // indirect
33 | github.com/go-playground/locales v0.14.1 // indirect
34 | github.com/go-playground/universal-translator v0.18.1 // indirect
35 | github.com/go-playground/validator/v10 v10.28.0 // indirect
36 | github.com/gobwas/httphead v0.1.0 // indirect
37 | github.com/gobwas/pool v0.2.1 // indirect
38 | github.com/gobwas/ws v1.4.0 // indirect
39 | github.com/goccy/go-json v0.10.5 // indirect
40 | github.com/goccy/go-yaml v1.18.0 // indirect
41 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
42 | github.com/google/uuid v1.6.0 // indirect
43 | github.com/josharian/intern v1.0.0 // indirect
44 | github.com/json-iterator/go v1.1.12 // indirect
45 | github.com/klauspost/cpuid/v2 v2.3.0 // indirect
46 | github.com/leodido/go-urn v1.4.0 // indirect
47 | github.com/mailru/easyjson v0.9.1 // indirect
48 | github.com/mattn/go-isatty v0.0.20 // indirect
49 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
50 | github.com/modern-go/reflect2 v1.0.2 // indirect
51 | github.com/pelletier/go-toml/v2 v2.2.4 // indirect
52 | github.com/quic-go/qpack v0.5.1 // indirect
53 | github.com/quic-go/quic-go v0.55.0 // indirect
54 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
55 | github.com/ugorji/go/codec v1.3.0 // indirect
56 | go.uber.org/mock v0.6.0 // indirect
57 | golang.org/x/arch v0.22.0 // indirect
58 | golang.org/x/exp v0.0.0-20251009144603-d2f985daa21b // indirect
59 | golang.org/x/mod v0.29.0 // indirect
60 | golang.org/x/sync v0.17.0 // indirect
61 | golang.org/x/sys v0.37.0 // indirect
62 | golang.org/x/tools v0.38.0 // indirect
63 | google.golang.org/protobuf v1.36.10 // indirect
64 | )
65 |
--------------------------------------------------------------------------------
/utils/utils_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2017 chai Author. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package utils
16 |
17 | import (
18 | "fmt"
19 | "net/http"
20 | "testing"
21 | "time"
22 | )
23 |
24 | // 返回参数的类型
25 | func TestType(t *testing.T) {
26 | var i int
27 | var s string
28 |
29 | if Type(i) != "int" {
30 | t.Error("type get fail by int")
31 | }
32 |
33 | if Type(s) != "string" {
34 | t.Error("type get fail by string")
35 | }
36 | }
37 |
38 | // 判断是否在数组中
39 | func TestInArray(t *testing.T) {
40 | a := "key"
41 | aList := []string{"key", "key2", "key3"}
42 | aList2 := []string{"key2", "key3"}
43 |
44 | if InArray(a, aList) != true {
45 | t.Error("value is in array")
46 | }
47 |
48 | if InArray(a, aList2) != false {
49 | t.Error("value is not in array")
50 | }
51 |
52 | b := 2
53 | bList := []int{2, 3, 4, 5}
54 | if InArray(b, aList) != false {
55 | t.Error("value is not in array")
56 | }
57 |
58 | if InArray(b, bList) != true {
59 | t.Error("value is in array")
60 | }
61 |
62 | }
63 |
64 | // 通过scrypt生成密码
65 | func TestNewPass(t *testing.T) {
66 | p, err := NewPass("123456", "123")
67 | if err != nil {
68 | t.Error(err.Error())
69 | }
70 |
71 | if len(p) != 64 {
72 | t.Error("password hash fail")
73 | }
74 | }
75 |
76 | func TestHttp(t *testing.T) {
77 | params := map[string]string{"test": "1", "t": "22"}
78 | var param string
79 | for key, value := range params {
80 | param += key + "=" + value + "&"
81 | }
82 | param = param[0 : len(param)-1]
83 | t.Error(param)
84 | result, err := HttpReadBodyString(http.MethodPost, "test", "", map[string]string{"test": "1", "t": "22"}, nil)
85 | t.Log(result, err)
86 | httpClient := HttpClient{
87 | Method: http.MethodPost,
88 | UrlText: "test",
89 | ContentType: ContentTypeMFD,
90 | Params: nil,
91 | Header: nil,
92 | }
93 | t.Log(httpClient.HttpReadBodyJsonMap())
94 | }
95 |
96 | func TestSchedulerIntervalsTimer(t *testing.T) {
97 | SchedulerIntervalsTimer(fmtp, time.Second*5)
98 | }
99 |
100 | func TestSchedulerFixedTimer(t *testing.T) {
101 | SchedulerFixedTicker(fmtp, time.Second*5)
102 | }
103 |
104 | func fmtp() {
105 | fmt.Println(TimeToString(time.Now()))
106 | }
107 |
108 | func TestStringJoiner(t *testing.T) {
109 | joiner := NewStringJoiner(",").SetPrefix("[").SetSuffix("]")
110 | fmt.Println(joiner.Add("1").Add("2").Add("3").String())
111 | }
112 |
--------------------------------------------------------------------------------
/static/js/layer/mobile/layer.js:
--------------------------------------------------------------------------------
1 | /*! layer mobile-v2.0.0 Web弹层组件 MIT License http://layer.layui.com/mobile By 贤心 */
2 | ;!function(e){"use strict";var t=document,n="querySelectorAll",i="getElementsByClassName",a=function(e){return t[n](e)},s={type:0,shade:!0,shadeClose:!0,fixed:!0,anim:"scale"},l={extend:function(e){var t=JSON.parse(JSON.stringify(s));for(var n in e)t[n]=e[n];return t},timer:{},end:{}};l.touch=function(e,t){e.addEventListener("click",function(e){t.call(this,e)},!1)};var r=0,o=["layui-m-layer"],c=function(e){var t=this;t.config=l.extend(e),t.view()};c.prototype.view=function(){var e=this,n=e.config,s=t.createElement("div");e.id=s.id=o[0]+r,s.setAttribute("class",o[0]+" "+o[0]+(n.type||0)),s.setAttribute("index",r);var l=function(){var e="object"==typeof n.title;return n.title?''+(e?n.title[0]:n.title)+"
":""}(),c=function(){"string"==typeof n.btn&&(n.btn=[n.btn]);var e,t=(n.btn||[]).length;return 0!==t&&n.btn?(e=''+n.btn[0]+"",2===t&&(e=''+n.btn[1]+""+e),''+e+"
"):""}();if(n.fixed||(n.top=n.hasOwnProperty("top")?n.top:100,n.style=n.style||"",n.style+=" top:"+(t.body.scrollTop+n.top)+"px"),2===n.type&&(n.content=''+(n.content||"")+"
"),n.skin&&(n.anim="up"),"msg"===n.skin&&(n.shade=!1),s.innerHTML=(n.shade?"':"")+'",!n.type||2===n.type){var d=t[i](o[0]+n.type),y=d.length;y>=1&&layer.close(d[0].getAttribute("index"))}document.body.appendChild(s);var u=e.elem=a("#"+e.id)[0];n.success&&n.success(u),e.index=r++,e.action(n,u)},c.prototype.action=function(e,t){var n=this;e.time&&(l.timer[n.index]=setTimeout(function(){layer.close(n.index)},1e3*e.time));var a=function(){var t=this.getAttribute("type");0==t?(e.no&&e.no(),layer.close(n.index)):e.yes?e.yes(n.index):layer.close(n.index)};if(e.btn)for(var s=t[i]("layui-m-layerbtn")[0].children,r=s.length,o=0;o: invalid syntax")
45 | }
46 |
47 | // Type 返回参数的类型
48 | func Type(v interface{}) string {
49 | t := reflect.TypeOf(v)
50 | k := t.Kind()
51 | return k.String()
52 | }
53 |
54 | // InArray 判断是否在数组中
55 | func InArray(in interface{}, list interface{}) bool {
56 | ret := false
57 | if in == nil {
58 | in = ""
59 | }
60 |
61 | // 判断list是否slice
62 | l := reflect.TypeOf(list).String()
63 | t := Type(in)
64 | if "[]"+t != l {
65 | return false
66 | }
67 |
68 | switch t {
69 | case "string":
70 | tv := reflect.ValueOf(in).String()
71 | for _, l := range list.([]string) {
72 | v := reflect.ValueOf(l)
73 | if tv == v.String() {
74 | ret = true
75 | break
76 | }
77 | }
78 |
79 | case "int":
80 | tv := reflect.ValueOf(in).Int()
81 | for _, l := range list.([]int) {
82 | v := reflect.ValueOf(l)
83 | if tv == v.Int() {
84 | ret = true
85 | break
86 | }
87 | }
88 | }
89 |
90 | return ret
91 | }
92 |
93 | // GBK2UTF gbk转换utf-8
94 | func GBK2UTF(text string) string {
95 | enc := mahonia.NewDecoder("GB18030")
96 |
97 | text = enc.ConvertString(text)
98 |
99 | return strings.ReplaceAll(text, "聽", " ")
100 | }
101 |
102 | // Pagination 分页
103 | // page 页数
104 | // rows 取多少条数据
105 | // total 数据总条数
106 | // 返回 起始-结束
107 | func Pagination(page, rows, total int) (int, int) {
108 | offset := (page - 1) * rows
109 | limit := offset + rows
110 | if limit > total {
111 | return offset, total
112 | }
113 | return offset, limit
114 | }
115 |
116 | // LogWithLocation 是一个封装好的日志工具函数
117 | func LogWithLocation(message string) {
118 | // 调用栈:
119 | // 0: runtime.Caller
120 | // 1: LogWithLocation (当前函数)
121 | // 2: main (我们想知道的位置!)
122 | pc, file, line, ok := runtime.Caller(2)
123 | if !ok {
124 | // 处理错误
125 | log.Printf("无法获取调用信息: %s", message)
126 | return
127 | }
128 |
129 | funcName := runtime.FuncForPC(pc).Name()
130 |
131 | // 格式化输出
132 | log.Printf("%s:%d [%s] - %s", file, line, funcName, message)
133 | }
134 |
--------------------------------------------------------------------------------
/temp.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "io"
5 | "log"
6 | "os"
7 | "path/filepath"
8 | "strings"
9 | )
10 |
11 | var TempDirPath string
12 |
13 | // destroyTemp 用于删除指定路径下的所有文件和文件夹。
14 | // 该函数设计为仅在路径包含 "temp" 字符串时执行操作,以防止误删其他重要数据。
15 | // 参数:
16 | //
17 | // path - 要删除的文件或目录的路径。
18 | //
19 | // 返回值:
20 | //
21 | // error - 如果删除过程中发生错误,返回相应的错误;否则返回 nil。
22 | func destroyTemp(path string) error {
23 | // 只有当路径中包含 "temp" 字符串时才执行删除操作(目前此判断未实现具体逻辑)。
24 | if strings.Contains(path, "temp") {
25 |
26 | }
27 |
28 | // 使用 filepath.Walk 遍历指定路径下的所有文件和子目录。
29 | // 注意:也可以使用更高效的 filepath.WalkDir 函数,但此处选择使用 filepath.Walk。
30 | err := filepath.Walk(path, func(path string, fi os.FileInfo, err error) error {
31 | // 如果文件信息为 nil,说明无法获取该文件的信息,直接返回错误。
32 | if nil == fi {
33 | return err
34 | }
35 |
36 | // 如果当前项不是目录,则将其作为普通文件删除。
37 | if !fi.IsDir() {
38 | err := os.Remove(path)
39 | if err != nil {
40 | return err // 删除文件失败时返回错误。
41 | }
42 | return nil
43 | }
44 |
45 | // 如果当前项是目录,则递归删除整个目录及其内容。
46 | err = os.RemoveAll(path)
47 | if err != nil {
48 | return err // 删除目录失败时返回错误。
49 | }
50 | return nil
51 | })
52 |
53 | // 返回遍历和删除过程中的最终结果(nil 表示成功,非 nil 表示出错)。
54 | return err
55 | }
56 |
57 | // DestroyTempDir 删除临时目录及其所有内容。
58 | // 该函数旨在清理不再需要的临时文件,以释放系统资源。
59 | // 没有输入参数,也不返回任何值。
60 | // 当无法删除目录时,会记录一个错误日志。
61 | func DestroyTempDir() {
62 | err := os.RemoveAll(TempDirPath)
63 | if err != nil {
64 | log.Println("删除缓存目录错误:", err)
65 | }
66 | }
67 |
68 | // CreateTmpDir 创建一个临时目录用于too-gin框架。
69 | // 该函数不接受任何参数。
70 | // 返回值是一个字符串,表示新创建的临时目录的路径,和一个错误值,如果创建过程中发生错误。
71 | func CreateTmpDir() (string, error) {
72 | // 使用os.MkdirTemp在系统临时目录下创建一个以"too-gin"为前缀的临时目录。
73 | file, err := os.MkdirTemp(os.TempDir(), "too-gin")
74 | if err != nil {
75 | // 如果创建临时目录时发生错误,返回空字符串和错误详情。
76 | return file, err
77 | }
78 | // 将新创建的临时目录路径赋值给全局变量TempDirPath,以便后续使用。
79 | TempDirPath = file
80 | // 返回新创建的临时目录路径和nil错误,表示操作成功。
81 | return file, err
82 | }
83 |
84 | // CreateTmpFiles 创建临时文件。
85 | // 该函数根据给定的名称从一个目录中读取所有文件,并将它们复制到一个临时目录中。
86 | // 参数:
87 | //
88 | // name - 源目录的名称,从该目录中读取文件。
89 | func CreateTmpFiles(name string) {
90 | // 创建一个临时目录。
91 | tempDir, err := CreateTmpDir()
92 | if err != nil {
93 | return
94 | }
95 |
96 | // 读取源目录中的文件信息。
97 | dir, err := local.ReadDir(name)
98 | if err != nil {
99 | return
100 | }
101 |
102 | // 确保临时目录路径以路径分隔符结尾。
103 | tempDir = tempDir + string(filepath.Separator)
104 |
105 | // 遍历源目录中的所有文件信息。
106 | for _, fileInfo := range dir {
107 | fileName := fileInfo.Name()
108 |
109 | // 检查临时目录中是否已存在同名文件。
110 | _, err := os.Stat(tempDir + fileName)
111 | if err == nil || os.IsExist(err) { // 如果文件存在
112 | // 尝试删除源文件,如果失败则忽略错误。
113 | _ = os.Remove(name)
114 | }
115 |
116 | // 打开源文件。
117 | file, err := local.Open(name + "/" + fileName)
118 | if err != nil {
119 | continue
120 | }
121 |
122 | // 读取源文件的全部内容。
123 | bytes, err := io.ReadAll(file)
124 | if err != nil {
125 | continue
126 | }
127 |
128 | // 在临时目录中创建新文件。
129 | tempFile, err := os.Create(tempDir + fileName)
130 | if err == nil {
131 | // 将源文件内容写入新文件。
132 | _, err := tempFile.Write(bytes)
133 | if err != nil {
134 | return
135 | }
136 | }
137 |
138 | // 关闭新创建的文件。
139 | err = tempFile.Close()
140 | if err != nil {
141 | return
142 | }
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/utils/encrypt.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "bytes"
5 | "crypto/aes"
6 | "crypto/hmac"
7 | "crypto/md5"
8 | "crypto/sha1"
9 | "crypto/sha256"
10 | "encoding/base64"
11 | "encoding/hex"
12 | "errors"
13 |
14 | "golang.org/x/crypto/scrypt"
15 | )
16 |
17 | // MD5 生成32位MD5
18 | func MD5(text string) string {
19 | ctx := md5.New()
20 | ctx.Write([]byte(text))
21 | return hex.EncodeToString(ctx.Sum(nil))
22 | }
23 |
24 | // MD5Byte 带byte的MD5
25 | func MD5Byte(data []byte) string {
26 | _md5 := md5.New()
27 | _md5.Write(data)
28 | return hex.EncodeToString(_md5.Sum([]byte("")))
29 | }
30 |
31 | // NewPass 通过scrypt生成密码
32 | func NewPass(passwd, salt string) (string, error) {
33 | dk, err := scrypt.Key([]byte(passwd), []byte(salt), 16384, 8, 1, 32)
34 | if err != nil {
35 | return "", err
36 | }
37 |
38 | return hex.EncodeToString(dk), nil
39 | }
40 |
41 | // ComputeHash1 计算hash1
42 | func ComputeHash1(message string, secret string) string {
43 | h := hmac.New(sha1.New, []byte(secret))
44 | h.Write([]byte(message))
45 | // 转成十六进制
46 | return hex.EncodeToString(h.Sum(nil))
47 | }
48 |
49 | // ComputeHmacSha256 计算HmacSha256
50 | func ComputeHmacSha256(message string, secret string) string {
51 | key := []byte(secret)
52 | h := hmac.New(sha256.New, key)
53 | h.Write([]byte(message))
54 | // 转成十六进制
55 | return hex.EncodeToString(h.Sum(nil))
56 |
57 | }
58 |
59 | // EncodeBase64 编码Base64
60 | func EncodeBase64(str string) string {
61 | return base64.StdEncoding.EncodeToString([]byte(str))
62 | }
63 |
64 | // Pkcs7Pad 使用 PKCS#7 填充
65 | func Pkcs7Pad(data []byte, blockSize int) []byte {
66 | padding := blockSize - len(data)%blockSize
67 | padtext := bytes.Repeat([]byte{byte(padding)}, padding)
68 | return append(data, padtext...)
69 | }
70 |
71 | // EncryptAESECB 使用 AES ECB 模式加密
72 | func EncryptAESECB(plaintext, key []byte) ([]byte, error) {
73 | block, err := aes.NewCipher(key)
74 | if err != nil {
75 | return nil, err
76 | }
77 | paddedPlaintext := Pkcs7Pad(plaintext, aes.BlockSize)
78 | ciphertext := make([]byte, len(paddedPlaintext))
79 | for bs, be := 0, block.BlockSize(); bs < len(paddedPlaintext); bs, be = bs+block.BlockSize(), be+block.BlockSize() {
80 | block.Encrypt(ciphertext[bs:be], paddedPlaintext[bs:be])
81 | }
82 | return ciphertext, nil
83 | }
84 |
85 | // Pkcs7Unpad 移除 PKCS#7 填充
86 | func Pkcs7Unpad(data []byte) ([]byte, error) {
87 | length := len(data)
88 | if length == 0 {
89 | return nil, errors.New("pkcs7: data is empty")
90 | }
91 | // 最后一个字节是填充的长度
92 | padLength := int(data[length-1])
93 | // 填充长度不能大于总长度
94 | if padLength > length {
95 | return nil, errors.New("pkcs7: invalid padding size")
96 | }
97 | // 返回移除填充后的部分
98 | return data[:(length - padLength)], nil
99 | }
100 |
101 | // DecryptAESECB 使用 AES ECB 模式解密
102 | func DecryptAESECB(ciphertext, key []byte) ([]byte, error) {
103 | // 创建 AES 密码块
104 | block, err := aes.NewCipher(key)
105 | if err != nil {
106 | return nil, err
107 | }
108 |
109 | // 检查密文长度是否是块大小的整数倍
110 | if len(ciphertext)%aes.BlockSize != 0 {
111 | return nil, errors.New("ciphertext is not a multiple of the block size")
112 | }
113 |
114 | decrypted := make([]byte, len(ciphertext))
115 |
116 | // ECB 模式是逐块解密
117 | for bs, be := 0, block.BlockSize(); bs < len(ciphertext); bs, be = bs+block.BlockSize(), be+block.BlockSize() {
118 | block.Decrypt(decrypted[bs:be], ciphertext[bs:be])
119 | }
120 |
121 | return decrypted, nil
122 | }
123 |
--------------------------------------------------------------------------------
/pyutils/reg_workshop_keygen.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import random
3 |
4 |
5 | def RandomBytes(n: int, no_zero_byte: bool = False):
6 | return bytes((random.randint(1 if no_zero_byte else 0, 255) for i in range(n)))
7 |
8 |
9 | # from https://en.wikibooks.org/wiki/Algorithm_Implementation/Mathematics/Extended_Euclidean_algorithm
10 | # return (g, x, y) where g = gcd(a, b) and g == a * x + b * y
11 | def xgcd(b, a):
12 | x0, x1, y0, y1 = 1, 0, 0, 1
13 | while a != 0:
14 | q, b, a = b // a, a, b % a
15 | x0, x1 = x1, x0 - q * x1
16 | y0, y1 = y1, y0 - q * y1
17 | return b, x0, y0
18 |
19 |
20 | def PKCS1_Padding(b: bytes, is_private_key_op: bool, sizeof_n: int):
21 | if len(b) > sizeof_n - 11:
22 | raise OverflowError('Message is too long.')
23 |
24 | ret = b'\x00\x01' if is_private_key_op else b'\x00\x02'
25 | ret += b'\xff' * (sizeof_n - 3 - len(b)) if is_private_key_op else RandomBytes(sizeof_n - 3 - len(b), True)
26 | ret += b'\x00'
27 | ret += b
28 | return ret
29 |
30 |
31 | def PKCS1_Unpadding(b: bytes, sizeof_n: int):
32 | if len(b) != sizeof_n:
33 | raise ValueError('Message\'s length is not correct')
34 |
35 | if b.startswith(b'\x00\x01'):
36 | is_private_key_op = True
37 | elif b.startswith(b'\x00\x02'):
38 | is_private_key_op = False
39 | else:
40 | # I know it is also valid if b starts with b'\x00\x00',
41 | # but now I do not care about this situation.
42 | raise ValueError('It is not a PKCS1-padded message.')
43 |
44 | msg_start_ptr = 3
45 | while msg_start_ptr < len(b):
46 | if is_private_key_op and b[msg_start_ptr] == 0:
47 | break
48 | if is_private_key_op and b[msg_start_ptr] != 0xff:
49 | raise ValueError('It is not a PKCS1-padded message.')
50 | if not is_private_key_op and b[msg_start_ptr] == 0:
51 | break
52 | msg_start_ptr += 1
53 | msg_start_ptr += 1
54 |
55 | msg = b[msg_start_ptr:]
56 | if len(msg) > sizeof_n - 11:
57 | raise OverflowError('Message is too long.')
58 |
59 | return msg
60 |
61 |
62 | def RSA_Encrypt(m: bytes, e: int, n: int):
63 | m = int.from_bytes(m, 'big')
64 | if m >= n:
65 | raise ValueError('Message is too big.')
66 |
67 | c = pow(m, e, n)
68 |
69 | return c.to_bytes((n.bit_length() + 7) // 8, 'big')
70 |
71 |
72 | def RSA_Decrypt(c: bytes, d: int, n: int):
73 | c = int.from_bytes(c, 'big')
74 | if c >= n:
75 | raise ValueError('Ciphertext is too big.')
76 |
77 | m = pow(c, d, n)
78 |
79 | return m.to_bytes((n.bit_length() + 7) // 8, 'big')
80 |
81 |
82 | p = 0x3862bf704e31d0962c0f27303efe8f5ba8d1edc08530351884522d3c1ddf289f
83 | q = 0x3cd9629192d2a4b0645103b892b32901801770269e10b00e562ec34d817bd0fd
84 | n = p * q
85 | phi = (p - 1) * (q - 1)
86 | e = 65537
87 | d = xgcd(e, phi)[1]
88 | while d < 0:
89 | d += phi
90 |
91 |
92 | def GenLicenseCode(name: str, license_count: int):
93 | if license_count > 500 or license_count < 1:
94 | raise ValueError('Invalid license count.')
95 |
96 | info = '%s\n%d\n' % (name, license_count)
97 | msg = info.encode() + RandomBytes(4)
98 | padded_msg = PKCS1_Padding(msg, True, (n.bit_length() + 7) // 8)
99 | enc_msg = RSA_Encrypt(padded_msg, d, n)
100 | return enc_msg.hex()
101 |
102 |
103 | if __name__ == '__main__':
104 | msg = GenLicenseCode("woytu", int(sys.argv[1]))
105 | print(msg)
106 |
--------------------------------------------------------------------------------
/static/js/utils/color.js:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * @Description:
4 | * @Author: https://www.bajins.com
5 | * @File: color.js
6 | * @Version: 1.0.0
7 | * @Time: 2019/11/21 21:14
8 | * @Project: tool-gin
9 | * @Package:
10 | * @Software: GoLand
11 | */
12 |
13 | // 首字母配置颜色
14 | const colorArr = {
15 | 'a': 'rgb(17, 1, 65)',
16 | 'b': 'rgb(113, 1, 98)',
17 | 'c': 'rgb(161, 42, 94)',
18 | 'd': 'rgb(237, 3, 69)',
19 | 'e': 'rgb(239, 106, 50)',
20 | 'f': 'rgb(251, 191, 69)',
21 | 'g': 'rgb(170, 217, 98)',
22 | 'h': 'rgb(3, 195, 131)',
23 | 'i': 'rgb(1, 115, 81)',
24 | 'j': 'rgb(1, 84, 90)',
25 | 'k': 'rgb(38, 41, 74)',
26 | 'l': 'rgb(26, 19, 52)',
27 | 'm': 'rgb(0, 102, 119)',
28 | 'n': 'rgb(119, 153, 85)',
29 | 'o': 'rgb(255, 170, 102)',
30 | 'p': 'rgb(255, 119, 119)',
31 | 'q': 'rgb(199, 96, 101)',
32 | 'r': 'rgb(23, 103, 87)',
33 | 's': 'rgb(188, 173, 148)',
34 | 't': 'rgb(83, 109, 114)',
35 | 'u': 'rgb(102, 188, 41)',
36 | 'v': 'rgb(181, 231, 146)',
37 | 'w': 'rgb(232, 247, 221)',
38 | 'x': 'rgb(113, 39, 122)',
39 | 'y': 'rgb(213, 150, 221)',
40 | 'z': 'rgb(242, 224, 245)'
41 | }
42 |
43 | /**
44 | * 随机颜色rgb
45 | *
46 | * @returns {string}
47 | */
48 | const randomRGBColor = function () {
49 | let r = Math.floor(Math.random() * 256);
50 | let g = Math.floor(Math.random() * 256);
51 | let b = Math.floor(Math.random() * 256);
52 | return `rgb(${r},${g},${b})`;
53 | }
54 |
55 | /**
56 | * 随机颜色十六进制值
57 | *
58 | * @returns {string}
59 | */
60 | const randomColor = () => {
61 | let str = Math.ceil(Math.random() * 16777215).toString(16);
62 | if (str.length < 6) {
63 | str = `0${str}`;
64 | }
65 | // return `#${Math.floor(Math.random()*(2<<23)).toString(16)}`;
66 | return `#${str}`;
67 | }
68 |
69 | /**
70 | * 随机颜色hsl
71 | *
72 | * @returns {string}
73 | */
74 | const randomHSLColor = function () {
75 | // Hue(色调)。0(或360)表示红色,120表示绿色,
76 | // 240表示蓝色,也可取其他数值来指定颜色。
77 | let h = Math.round(Math.random() * 360);
78 | // Saturation(饱和度)。取值为:0.0% - 100.0%
79 | let s = Math.round(Math.random() * 100);
80 | // Lightness(亮度)。取值为:0.0% - 100.0%
81 | let l = Math.round(Math.random() * 80);
82 | return `hsl(${h},${s}%,${l}%)`;
83 | }
84 |
85 |
86 | /**
87 | * 是否为css合法颜色值
88 | *
89 | * @param value
90 | * @returns {boolean}
91 | */
92 | const isColor = function (value) {
93 | let colorReg = /^#([a-fA-F0-9]){3}(([a-fA-F0-9]){3})?$/;
94 | let rgbaReg = /^[rR][gG][bB][aA]\(\s*((25[0-5]|2[0-4]\d|1?\d{1,2})\s*,\s*){3}\s*(\.|\d+\.)?\d+\s*\)$/;
95 | let rgbReg = /^[rR][gG][bB]\(\s*((25[0-5]|2[0-4]\d|1?\d{1,2})\s*,\s*){2}(25[0-5]|2[0-4]\d|1?\d{1,2})\s*\)$/;
96 | let hslReg = /^[hH][sS][lL]\(([0-9]|[1-9][0-9]|[1-3][0-5][0-9]|360)\,(100|[1-9]\d|\d)(.\d{1,2})?%\,(100|[1-9]\d|\d)(.\d{1,2})?%\)$/;
97 |
98 | return colorReg.test(value) || rgbaReg.test(value) || rgbReg.test(value) || hslReg.test(value);
99 | }
100 |
101 |
102 | /**
103 | * export default 服从 ES6 的规范,补充:default 其实是别名
104 | * module.exports 服从 CommonJS 规范 https://javascript.ruanyifeng.com/nodejs/module.html
105 | * 一般导出一个属性或者对象用 export default
106 | * 一般导出模块或者说文件使用 module.exports
107 | *
108 | * import from 服从ES6规范,在编译器生效
109 | * require 服从ES5 规范,在运行期生效
110 | * 目前 vue 编译都是依赖label 插件,最终都转化为ES5
111 | *
112 | * @return 将方法、变量暴露出去
113 | */
114 | export default {
115 | colorArr,
116 | randomRGBColor,
117 | randomColor,
118 | randomHSLColor,
119 | isColor
120 | }
--------------------------------------------------------------------------------
/utils/stringjoiner.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "fmt"
5 | "strconv"
6 | "strings"
7 | )
8 |
9 | // StringJoiner 用于构建由分隔符分隔的字符串,并可选择添加前缀和后缀。
10 | type StringJoiner struct {
11 | builder strings.Builder
12 | delimiter string
13 | prefix string
14 | suffix string
15 | emptyValue string
16 | isFirst bool
17 | }
18 |
19 | // NewStringJoiner 创建一个新的 StringJoiner。
20 | func NewStringJoiner(delimiter string) *StringJoiner {
21 | return &StringJoiner{
22 | delimiter: delimiter,
23 | isFirst: true,
24 | }
25 | }
26 |
27 | // SetPrefix 设置前缀
28 | func (sj *StringJoiner) SetPrefix(prefix string) *StringJoiner {
29 | sj.prefix = prefix
30 | return sj
31 | }
32 |
33 | // SetSuffix 设置后缀
34 | func (sj *StringJoiner) SetSuffix(suffix string) *StringJoiner {
35 | sj.suffix = suffix
36 | return sj
37 | }
38 |
39 | // SetEmptyValue 设置当没有添加任何元素时的默认返回值。
40 | func (sj *StringJoiner) SetEmptyValue(emptyValue string) *StringJoiner {
41 | sj.emptyValue = emptyValue
42 | return sj
43 | }
44 |
45 | // Add 添加一个新的元素到 StringJoiner。
46 | // 它使用 type switch 为常见类型提供高性能转换。
47 | func (sj *StringJoiner) Add(val interface{}) *StringJoiner {
48 | if sj.isFirst {
49 | sj.builder.WriteString(sj.prefix)
50 | sj.isFirst = false
51 | } else {
52 | sj.builder.WriteString(sj.delimiter)
53 | }
54 |
55 | // 使用 Type Switch 为特定类型提供高效的字符串转换
56 | switch v := val.(type) {
57 | case string:
58 | sj.builder.WriteString(v)
59 | case int:
60 | sj.builder.WriteString(strconv.Itoa(v))
61 | case int64:
62 | sj.builder.WriteString(strconv.FormatInt(v, 10))
63 | case uint:
64 | sj.builder.WriteString(strconv.FormatUint(uint64(v), 10))
65 | case uint64:
66 | sj.builder.WriteString(strconv.FormatUint(v, 10))
67 | case float32:
68 | sj.builder.WriteString(strconv.FormatFloat(float64(v), 'f', -1, 32))
69 | case float64:
70 | sj.builder.WriteString(strconv.FormatFloat(v, 'f', -1, 64))
71 | case bool:
72 | sj.builder.WriteString(strconv.FormatBool(v))
73 | case []byte:
74 | sj.builder.Write(v)
75 | case rune:
76 | sj.builder.WriteRune(v)
77 | default:
78 | // 对于其他所有类型
79 | sj.builder.WriteString(fmt.Sprintf("%v", v))
80 | }
81 |
82 | return sj
83 | }
84 |
85 | // AddInt 添加一个新的整数元素到 StringJoiner。
86 | func (sj *StringJoiner) AddInt(value int) *StringJoiner {
87 | return sj.Add(fmt.Sprintf("%d", value))
88 | }
89 |
90 | // AddFloat 添加一个新的浮点数元素到 StringJoiner。
91 | func (sj *StringJoiner) AddFloat(value float64) *StringJoiner {
92 | return sj.Add(fmt.Sprintf("%f", value))
93 | }
94 |
95 | // AddBool 添加一个新的布尔元素到 StringJoiner。
96 | func (sj *StringJoiner) AddBool(value bool) *StringJoiner {
97 | return sj.Add(fmt.Sprintf("%t", value))
98 | }
99 |
100 | // Merge 合并另一个 StringJoiner
101 | func (sj *StringJoiner) Merge(other *StringJoiner) *StringJoiner {
102 | if other.builder.Len() > 0 {
103 | if sj.builder.Len() > 0 {
104 | sj.builder.WriteString(sj.delimiter)
105 | }
106 | sj.builder.WriteString(other.builder.String())
107 | }
108 | return sj
109 | }
110 |
111 | // Length 返回当前内容的长度
112 | func (sj *StringJoiner) Length() int {
113 | if sj.builder.Len() <= 0 {
114 | return len(sj.emptyValue)
115 | }
116 | return len(sj.prefix) + sj.builder.Len() + len(sj.suffix)
117 | }
118 |
119 | // Empty 检查是否为空
120 | func (sj *StringJoiner) Empty() bool {
121 | return sj.builder.Len() <= 0
122 | }
123 |
124 | // String 返回最终的字符串。
125 | func (sj *StringJoiner) String() string {
126 | if sj.builder.Len() == 0 && sj.emptyValue != "" {
127 | return sj.emptyValue
128 | }
129 | if sj.builder.Len() == 0 {
130 | return sj.prefix + sj.suffix
131 | }
132 | sj.builder.WriteString(sj.suffix)
133 | return sj.builder.String()
134 | }
135 |
--------------------------------------------------------------------------------
/static/js/utils/log.js:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * @Description:
4 | * @Author: https://www.bajins.com
5 | * @File: log.js
6 | * @Version: 1.0.0
7 | * @Time: 2019/9/15 20:26
8 | * @Project: tool-gin
9 | * @Package:
10 | * @Software: GoLand
11 | */
12 |
13 | import time from "./time.js";
14 |
15 | // const isDebugEnabled = "production";
16 | const isDebugEnabled = "dev";
17 | const isInfoEnabled = true;
18 | const isErrorEnabled = true;
19 | const isWarnEnabled = true;
20 | const isTraceEnabled = true;
21 |
22 | /**
23 | * 自定义颜色打印日志
24 | *
25 | * @param title
26 | * @param content
27 | * @param backgroundColor 颜色
28 | */
29 | const log = (title, content, backgroundColor = "#1475b2") => {
30 | let i = [
31 | `%c ${title} %c ${content} `,
32 | "padding: 1px; border-radius: 3px 0 0 3px; color: #fff; background: ".concat("#606060", ";"),
33 | `padding: 1px; border-radius: 0 3px 3px 0; color: #fff; background: ${backgroundColor};`
34 | ];
35 | return function () {
36 | let t;
37 | window.console && "function" === typeof window.console.log && (t = console).log.apply(t, arguments);
38 | }.apply(void 0, i);
39 | }
40 |
41 | log("isDebugEnabled", isDebugEnabled, "#42c02e");
42 | log("isInfoEnabled", isInfoEnabled, "#42c02e");
43 | log("isErrorEnabled", isErrorEnabled, "#42c02e");
44 | log("isWarnEnabled", isWarnEnabled, "#42c02e");
45 | log("isTraceEnabled", isTraceEnabled, "#42c02e");
46 |
47 | /**
48 | * 箭头函数是匿名函数,不能作为构造函数,不能使用new
49 | *
50 | * 对日志参数解析
51 | * 格式为:
52 | * logger.info("页面{},点击第{}行", "App.vue", index);
53 | *
54 | * @param log 箭头函数不能绑定arguments,取而代之用rest参数
55 | * @returns {string}
56 | */
57 | const getParam = (...log) => {
58 | if (log.length == 0) {
59 | return "";
60 | }
61 | let params = log[0];
62 | let parentString = params[0].toString();
63 | // 正则表达式,如须匹配大小写则去掉i
64 | let re = eval("/{}/ig");
65 | // 匹配正则
66 | let ps = parentString.match(re);
67 |
68 | // 参数个数大于1,并且匹配的个数大于0
69 | if (params.length > 1 && ps != null) {
70 | // 移除第一个元素并返回该元素
71 | params.shift();
72 | for (let i = 0; i < ps.length; i++) {
73 | parentString = parentString.replace("{}", params[i]);
74 | }
75 | // 把替换后的字符串与参数未替换完的拼接起来
76 | parentString = parentString + params.slice(ps.length).toString();
77 | return parentString;
78 | }
79 | return JSON.stringify(params);
80 | }
81 |
82 | const debug = (...log) => {
83 | if (isDebugEnabled) {
84 | console.log(
85 | `${time.dateFormat(new Date, "yyyy-MM-dd HH:mm:ss")} %c ${getParam(log)}`,
86 | 'color:red;'
87 | );
88 | }
89 | }
90 |
91 | const logConcat = (...log) => {
92 | return `${time.dateFormat(new Date, "yyyy-MM-dd HH:mm:ss")} ${getParam(log)}`;
93 | }
94 |
95 | const info = (...log) => {
96 | if (isInfoEnabled) {
97 | console.info(logConcat(log));
98 | }
99 | }
100 |
101 | const error = (...log) => {
102 | if (isErrorEnabled) {
103 | console.error(logConcat(log));
104 | }
105 | }
106 | const warn = (...log) => {
107 | if (isWarnEnabled) {
108 | console.warn(logConcat(log));
109 | }
110 | }
111 | const trace = (...log) => {
112 | if (isTraceEnabled) {
113 | console.trace(logConcat(log));
114 | }
115 | }
116 |
117 |
118 | /**
119 | * export default 服从 ES6 的规范,补充:default 其实是别名
120 | * module.exports 服从 CommonJS 规范 https://javascript.ruanyifeng.com/nodejs/module.html
121 | * 一般导出一个属性或者对象用 export default
122 | * 一般导出模块或者说文件使用 module.exports
123 | *
124 | * import from 服从ES6规范,在编译器生效
125 | * require 服从ES5 规范,在运行期生效
126 | * 目前 vue 编译都是依赖label 插件,最终都转化为ES5
127 | *
128 | * @return 将方法、变量暴露出去
129 | */
130 | export default {
131 | debug,
132 | info,
133 | error,
134 | warn,
135 | trace
136 | }
--------------------------------------------------------------------------------
/reptile/reptile_test.go:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * @Description:
4 | * @Author: https://www.bajins.com
5 | * @File: reptile_test.go
6 | * @Version: 1.0.0
7 | * @Time: 2019/9/19 11:13
8 | * @Project: tool-gin
9 | * @Package:
10 | * @Software: GoLand
11 | */
12 | package reptile
13 |
14 | import (
15 | "fmt"
16 | "log"
17 | "regexp"
18 | "runtime/debug"
19 | "testing"
20 | "time"
21 |
22 | "github.com/chromedp/cdproto/target"
23 | "github.com/chromedp/chromedp"
24 | )
25 |
26 | func TestApply(t *testing.T) {
27 | ctx, cancel := Apply(false)
28 | defer cancel()
29 | var res string
30 | err := chromedp.Run(ctx, AntiDetectionHeadless(), chromedp.Tasks{
31 | chromedp.Sleep(20 * time.Second),
32 | // 跳转页面
33 | //chromedp.Navigate("https://intoli.com/blog/not-possible-to-block-chrome-headless/chrome-headless-test.html"),
34 | chromedp.Navigate("https://www.pexels.com/zh-cn/new-photos?page=1"),
35 | // 读取HTML源码
36 | chromedp.InnerHTML("html", &res, chromedp.BySearch),
37 | })
38 | t.Log(err)
39 | t.Log(res)
40 | url := "https://www.pexels.com/zh-cn/photo/3584157/"
41 | // 新建浏览器标签页及上下文
42 | ctx, cancel = chromedp.NewContext(ctx, chromedp.WithTargetID(target.ID(target.CreateTarget(url).BrowserContextID)))
43 | defer cancel()
44 | err = chromedp.Run(ctx, AntiDetectionHeadless(), chromedp.Tasks{
45 | // 读取HTML源码
46 | //chromedp.InnerHTML("html", &res, chromedp.BySearch),
47 | })
48 | t.Log(err)
49 | t.Log(res)
50 | }
51 |
52 | func TestNetsarang(t *testing.T) {
53 | defer func() { // 捕获panic
54 | if r := recover(); r != nil {
55 | log.Println("Recovered from panic:", r)
56 | }
57 | }()
58 | }
59 |
60 | func TestGetSvp(t *testing.T) {
61 | defer func() { // 捕获panic
62 | if r := recover(); r != nil {
63 | // https://pkg.go.dev/runtime#Stack
64 | // https://pkg.go.dev/runtime/debug#PrintStack
65 | log.Println("panic:", string(debug.Stack()))
66 | log.Println("Recovered from panic:", r)
67 | }
68 | }()
69 | //fmt.Println(getSvpGit())
70 | //fmt.Println(getSvpDP())
71 | //fmt.Println(getSvpDP1())
72 | //fmt.Println(getSvpYse())
73 | //fmt.Println(len(strings.Split(getSvpGitAgg(), "\n")))
74 | fmt.Println(getSvpAll())
75 | }
76 |
77 | func TestGetSvpYes(t *testing.T) {
78 | // 密钥 (Base64)
79 | base64Key := "plr4EY25bk1HbC6a+W76TQ=="
80 |
81 | // 创建 channel 用于接收结果
82 | ch1 := make(chan string)
83 | ch2 := make(chan string)
84 | // 启动协程执行任务
85 | go func() {
86 | defer func() {
87 | if r := recover(); r != nil {
88 | log.Println("捕获 panic:", r, string(debug.Stack()))
89 | }
90 | }()
91 | url := "https://api.v2rayse.com/api/live"
92 | ch1 <- getSvpYse(url, base64Key)
93 | close(ch1)
94 | }()
95 | go func() {
96 | defer func() {
97 | if r := recover(); r != nil {
98 | log.Println("捕获 panic:", r, string(debug.Stack()))
99 | }
100 | }()
101 | url := "https://api.v2rayse.com/api/batch"
102 | ch2 <- getSvpYse(url, base64Key)
103 | close(ch2)
104 | }()
105 | // 等待并收集结果
106 | fmt.Println(<-ch1 + "\n" + <-ch2)
107 | }
108 |
109 | func TestUrlRegx(t *testing.T) {
110 | urls := []string{
111 | "http://www.example.com",
112 | "https://example.com/path?query=123",
113 | "www.example.com",
114 | "example.com",
115 | "example.com/path",
116 | "ftp://example.com",
117 | "192.168.1.1", // IP address
118 | "localhost",
119 | "localhost:8080",
120 | "subdomain.example.co.uk",
121 | "example.museum",
122 | "http://[::1]:8080", // IPv6
123 | "https://[2001:db8::1]/path", //IPv6
124 | "www.example-.com", // Invalid, but test edge cases
125 | "-example.com", // Invalid
126 | "ww-example.com", // Invalid
127 | "example", // Invalid , but test edge cases
128 | }
129 | // 不适用于有其他文本内容参杂的情况
130 | //urlRegex := regexp.MustCompile(`(https?://)?([\w.-]+)(:\d+)?(/[\w./?%&=-]*)?`)
131 | // 更宽松,兼容性更好的正则表达式:
132 | urlRegex := regexp.MustCompile(`(?:(?:https?|ftp)://)?(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,63}|\[(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,7}:|::|localhost|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(?::\d+)?(?:[/?#]\S*)?`)
133 |
134 | for _, url := range urls {
135 | log.Println(url, "|||||||||||", urlRegex.MatchString(url))
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/mailtm/message.go:
--------------------------------------------------------------------------------
1 | package mailtm
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "errors"
7 | "strconv"
8 | )
9 |
10 | type Message struct {
11 | ID string `json:"id"`
12 | From Adressee `json:"from"`
13 | To []Adressee `json:"to"`
14 | Subject string `json:"subject"`
15 | Intro string `json:"intro"`
16 | Seen bool `json:"seen"`
17 | IsDeleted bool `json:"isDeleted"`
18 | Size int `json:"size"`
19 | Text string `json:"text"`
20 | HTML []string `json:"html"`
21 | Attachments []Attachment `json:"attachments"`
22 | }
23 |
24 | type Adressee struct {
25 | Name string `json:"name"`
26 | Address string `json:"address"`
27 | }
28 |
29 | type Attachment struct {
30 | Id string `json:"id"`
31 | Filename string `json:"filename"`
32 | ContentType string `json:"contentType"`
33 | Disposition string `json:"disposition"`
34 | TransferEncoding string `json:"transferEncoding"`
35 | Related bool `json:"related"`
36 | Size int `json:"size"`
37 | DownloadURL string `json:"downloadUrl"`
38 | }
39 |
40 | func (account *Account) MessagesAt(page int) ([]Message, error) {
41 | var data map[string][]Message
42 | URI := URI_MESSAGES + "?page=" + strconv.Itoa(page)
43 | request := requestData{
44 | uri: URI,
45 | method: "GET",
46 | bearer: account.bearer,
47 | }
48 | response, err := makeRequest(request)
49 | if err != nil {
50 | return nil, err
51 | }
52 | if response.code != 200 {
53 | return nil, errors.New("failed to get messages")
54 | }
55 | json.Unmarshal(response.body, &data)
56 | messages := data["hydra:member"]
57 | for i, _ := range messages {
58 | msg, err := account.MessageById(messages[i].ID)
59 | if err != nil {
60 | return nil, err
61 | }
62 | messages[i] = msg
63 | }
64 | return messages, nil
65 | }
66 |
67 | func (account *Account) MessageById(id string) (Message, error) {
68 | var msg Message
69 | URI := URI_MESSAGES + "/" + id
70 | request := requestData{
71 | uri: URI,
72 | method: "GET",
73 | bearer: account.bearer,
74 | }
75 | response, err := makeRequest(request)
76 | if err != nil {
77 | return Message{}, err
78 | }
79 | if response.code != 200 {
80 | return Message{}, errors.New("failed to get message")
81 | }
82 | json.Unmarshal(response.body, &msg)
83 | return msg, nil
84 | }
85 |
86 | func (account *Account) MessagesChan(ctx context.Context) <-chan Message {
87 | msgChan := make(chan Message)
88 | go func() {
89 | lastMsg, _ := account.LastMessage()
90 | lastMsgId := lastMsg.ID
91 | loop:
92 | for {
93 | select {
94 | case <-ctx.Done():
95 | close(msgChan)
96 | break loop
97 | default:
98 | }
99 | msg, err := account.LastMessage()
100 | if err != nil {
101 | continue
102 | }
103 | if msg.ID != lastMsgId {
104 | msgChan <- msg
105 | lastMsgId = msg.ID
106 | }
107 | }
108 | }()
109 | return msgChan
110 | }
111 |
112 | func (account *Account) LastMessage() (Message, error) {
113 | msgs, err := account.MessagesAt(1)
114 | if err != nil {
115 | return Message{}, err
116 | }
117 | if len(msgs) == 0 {
118 | return Message{}, errors.New("no messages")
119 | }
120 | return msgs[0], nil
121 | }
122 |
123 | func (account *Account) DeleteMessage(id string) error {
124 | URI := URI_MESSAGES + "/" + id
125 | request := requestData{
126 | uri: URI,
127 | method: "DELETE",
128 | bearer: account.bearer,
129 | }
130 | response, err := makeRequest(request)
131 | if err != nil {
132 | return err
133 | }
134 | if response.code == 404 {
135 | return errors.New("message with id " + id + " was not found")
136 | }
137 | if response.code != 204 {
138 | return errors.New("failed to delete message")
139 | }
140 | return nil
141 | }
142 |
143 | func (account *Account) MarkMessage(id string) error {
144 | URI := URI_MESSAGES + "/" + id
145 | request := requestData{
146 | uri: URI,
147 | method: "PATCH",
148 | bearer: account.bearer,
149 | }
150 | response, err := makeRequest(request)
151 | if err != nil {
152 | return err
153 | }
154 | if response.code == 404 {
155 | return errors.New("message with id " + id + " was not found")
156 | }
157 | if response.code != 200 {
158 | return errors.New("failed to mark message")
159 | }
160 | return nil
161 | }
162 |
--------------------------------------------------------------------------------
/mailtm/README.md:
--------------------------------------------------------------------------------
1 | # MailTM Wrapper
2 | A convenience-oriented [mail.tm](https://mail.tm) API wrapper written in Golang
3 |
4 | Feel free to report bugs and suggest improvements!
5 |
6 | Copy by https://github.com/msuny-c/mailtm/tree/0c39880925d6ce6a0651720dca77fd72cb1e831b
7 |
8 | ## Installation
9 | ```
10 | go get github.com/msuny-c/mailtm
11 | ```
12 | ## Getting started
13 | ### Register
14 | You can create a new account with random credentials
15 | ```go
16 | import "github.com/msuny-c/mailtm"
17 |
18 | func main() {
19 | account, err := mailtm.NewAccount()
20 | if err != nil {
21 | panic(err)
22 | }
23 | }
24 | ```
25 | Or provide data directly
26 | ```go
27 | import "github.com/msuny-c/mailtm"
28 |
29 | func main() {
30 | opts := mailtm.Options {
31 | Domain: mailtm.AvailableDomains()[0].Domain,
32 | Username: "someusername",
33 | Password: "mypassword",
34 | }
35 | account, err := mailtm.NewAccountWithOptions(opts)
36 | if err != nil {
37 | panic(err)
38 | }
39 | }
40 | ```
41 | ### Login
42 | You can login to your existing account using your address and password
43 | ```go
44 | import "github.com/msuny-c/mailtm"
45 |
46 | func main() {
47 | account, err := mailtm.Login("username@mail.tm", "mypassword")
48 | if err != nil {
49 | panic(err)
50 | }
51 | }
52 | ```
53 | Or using Bearer token
54 | ```go
55 | import "github.com/msuny-c/mailtm"
56 |
57 | func main() {
58 | account, err := mailtm.LoginWithToken("bearertoken")
59 | if err != nil {
60 | panic(err)
61 | }
62 | }
63 | ```
64 | ### Working with messages
65 | To get a message you can use the `MessagesAt(id)` method, which returns a slice of messages with their contents on a specific page
66 | ```go
67 | import "github.com/msuny-c/mailtm"
68 |
69 | func main() {
70 | account, err := mailtm.NewAccount()
71 | if err != nil {
72 | panic(err)
73 | }
74 | msgs, err := account.MessagesAt(1)
75 | if err != nil {
76 | print("failed to get messages")
77 | }
78 | }
79 | ```
80 | You can get a message channel that will receive new messages from current moment
81 | ```go
82 | import (
83 | "github.com/msuny-c/mailtm"
84 | "context"
85 | )
86 |
87 | func main() {
88 | account, err := mailtm.NewAccount()
89 | if err != nil {
90 | panic(err)
91 | }
92 | ctx, cancel := context.WithCancel(context.Background())
93 | ch := account.MessagesChan(ctx)
94 | for {
95 | select {
96 | case msg, ok := <- ch:
97 | if ok {
98 | print(msg.HTML)
99 | cancel()
100 | }
101 | }
102 | }
103 | }
104 | ```
105 | Also you can get the last message or by it's id
106 | ```go
107 | import "github.com/msuny-c/mailtm"
108 |
109 | func main() {
110 | account, err := mailtm.NewAccount()
111 | if err != nil {
112 | panic(err)
113 | }
114 | msg1, err := account.MessageById("someid")
115 | msg2, err := account.LastMessage()
116 | if err != nil {
117 | print("failed to get messages")
118 | }
119 | }
120 | ```
121 | And of course you can delete message
122 | ```go
123 | import "github.com/msuny-c/mailtm"
124 |
125 | func main() {
126 | account, err := mailtm.NewAccount()
127 | if err != nil {
128 | panic(err)
129 | }
130 | msg, err := account.LastMessage()
131 | if err != nil {
132 | print("failed to get message")
133 | }
134 | account.DeleteMessage(msg.ID)
135 | }
136 | ```
137 | ### Account
138 | You can get account's properties (those that are returned in the response by [api.mail.tm](https://api.mail.tm))
139 | ```go
140 | import "github.com/msuny-c/mailtm"
141 |
142 | func main() {
143 | account, err := mailtm.NewAccount()
144 | if err != nil {
145 | panic(err)
146 | }
147 | print(account.Property("createdAt"))
148 | }
149 | ```
150 | Also get address, password and token fields
151 | ```go
152 | import "github.com/msuny-c/mailtm"
153 |
154 | func main() {
155 | account, err := mailtm.NewAccount()
156 | if err != nil {
157 | panic(err)
158 | }
159 | println(account.Address())
160 | println(account.Password())
161 | println(account.Bearer())
162 | }
163 | ```
164 | If you wish you can delete your account
165 | ```go
166 | import "github.com/msuny-c/mailtm"
167 |
168 | func main() {
169 | account, err := mailtm.NewAccount()
170 | if err != nil {
171 | panic(err)
172 | }
173 | account.Delete()
174 | }
175 | ```
176 |
--------------------------------------------------------------------------------
/utils/file.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "errors"
5 | "io"
6 | "io/fs"
7 | "mime"
8 | "net/http"
9 | "os"
10 | "path/filepath"
11 | "strings"
12 | )
13 |
14 | // GetDirList 目录下所有的文件夹
15 | func GetDirList(dirPath string) ([]string, error) {
16 | var dirList []string
17 | err := filepath.Walk(dirPath,
18 | func(path string, f os.FileInfo, err error) error {
19 | if f == nil {
20 | return err
21 | }
22 | if f.IsDir() {
23 | dirList = append(dirList, path)
24 | return nil
25 | }
26 |
27 | return nil
28 | })
29 | return dirList, err
30 | }
31 |
32 | // GetDirListAll 获取一个目录下所有文件信息,包含子目录
33 | func GetDirListAll(files []os.FileInfo, dirPath string) ([]os.FileInfo, error) {
34 | err := filepath.Walk(dirPath, func(dPath string, f os.FileInfo, err error) error {
35 | if !f.IsDir() {
36 | files = append(files, f)
37 | } else {
38 | _, err := GetDirListAll(files, strings.ReplaceAll(filepath.Join(dPath, f.Name()), "\\", "/"))
39 | if err != nil {
40 | return err
41 | }
42 | }
43 | return nil
44 | })
45 | return files, err
46 | }
47 |
48 | // GetFileList 获取当前路径下所有文件
49 | func GetFileList(path string) ([]fs.DirEntry, error) {
50 | readerInfos, err := os.ReadDir(path)
51 | if err != nil {
52 | return nil, err
53 | }
54 | if readerInfos == nil {
55 | return nil, nil
56 | }
57 | return readerInfos, nil
58 | }
59 |
60 | // IsExistDir 判断路径是否为目录
61 | func IsExistDir(dirPath string) bool {
62 | if IsStringEmpty(dirPath) {
63 | return false
64 | }
65 | info, err := os.Stat(dirPath)
66 | if err != nil || !os.IsExist(err) || !info.IsDir() {
67 | return false
68 | }
69 | return true
70 | }
71 |
72 | // IsFileExist 判断文件是否存在:存在,返回true,否则返回false
73 | func IsFileExist(filename string) bool {
74 | info, err := os.Stat(filename)
75 | if err != nil || os.IsNotExist(err) || info.IsDir() {
76 | return false
77 | }
78 | return true
79 | }
80 |
81 | // IsExists 判断所给路径文件/文件夹是否存在
82 | func IsExists(path string) bool {
83 | if IsStringEmpty(path) {
84 | return false
85 | }
86 | // os.Stat获取文件信息
87 | _, err := os.Stat(path)
88 | if err != nil {
89 | if os.IsExist(err) {
90 | return true
91 | }
92 | return false
93 | }
94 | return true
95 | }
96 |
97 | // IsNotExists 判断所给路径文件/文件夹是否不存在
98 | func IsNotExists(path string) bool {
99 | return !IsExists(path)
100 | }
101 |
102 | // OsPath 获取当前程序运行所在路径
103 | func OsPath() (string, error) {
104 | return filepath.Abs(filepath.Dir(os.Args[0]))
105 | }
106 |
107 | // GetSuffix 获取路径中的文件的后缀
108 | func GetSuffix(filePath string) string {
109 | ext := filepath.Ext(filePath)
110 | return ext
111 | }
112 |
113 | // GetDirFile 获取路径中的目录及文件名
114 | func GetDirFile(filePath string) (dir, file string) {
115 | paths, fileName := filepath.Split(filePath)
116 | return paths, fileName
117 | }
118 |
119 | // ParentDirectory 获取父级目录
120 | func ParentDirectory(dir string) string {
121 | return filepath.Join(dir, "..")
122 | }
123 |
124 | // PathSeparatorSlash 目录分隔符转换
125 | func PathSeparatorSlash(path string) string {
126 | return strings.ReplaceAll(path, "\\", "/")
127 | }
128 |
129 | // ContextPath 获取上下文路径,传入指定目录截取前一部分
130 | func ContextPath(root string) (path string, err error) {
131 | // 获取当前绝对路径
132 | dir, err := os.Getwd()
133 | if err != nil {
134 | return "", err
135 | }
136 | index := strings.LastIndex(dir, root)
137 | if len(dir) < len(root) || index <= 0 {
138 | return dir, errors.New("错误:路径不正确")
139 | }
140 | return dir[0 : index+len(root)], nil
141 | }
142 |
143 | // Mkdir 创建所有不存在的层级目录
144 | func Mkdir(dir string) error {
145 | if _, err := os.Stat(dir); err != nil {
146 | err = os.MkdirAll(dir, 0711)
147 | return err
148 | }
149 | return nil
150 | }
151 |
152 | // CreateFile 创建文件
153 | func CreateFile(filePath string) error {
154 | if _, err := os.Stat(filePath); err != nil {
155 | _, err = os.Create(filePath)
156 | return err
157 | }
158 | return nil
159 | }
160 |
161 | // GetContentType 获取文件MIME类型
162 | // 见函数http.ServeContent
163 | func GetContentType(filename string) (string, error) {
164 | f, err := os.Open(filename)
165 | if err != nil {
166 | return "", err
167 | }
168 | fi, err := f.Stat()
169 | if err != nil {
170 | return "", err
171 | }
172 | ctype := mime.TypeByExtension(filepath.Ext(fi.Name()))
173 | if ctype == "" {
174 | // read a chunk to decide between utf-8 text and binary
175 | var buf [512]byte
176 | n, _ := io.ReadFull(f, buf[:])
177 | // 根据前512个字节的数据判断MIME类型
178 | ctype = http.DetectContentType(buf[:n])
179 | _, err := f.Seek(0, io.SeekStart) // rewind to output whole file
180 | if err != nil {
181 | return "", err
182 | }
183 | }
184 | return ctype, nil
185 | }
186 |
--------------------------------------------------------------------------------
/pyutils/moba_xterm_Keygen.py:
--------------------------------------------------------------------------------
1 | import os, sys, zipfile
2 | from os import path
3 |
4 | VariantBase64Table = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='
5 | VariantBase64Dict = {i: VariantBase64Table[i] for i in range(len(VariantBase64Table))}
6 | VariantBase64ReverseDict = {VariantBase64Table[i]: i for i in range(len(VariantBase64Table))}
7 |
8 |
9 | def VariantBase64Encode(bs: bytes):
10 | result = b''
11 | blocks_count, left_bytes = divmod(len(bs), 3)
12 |
13 | for i in range(blocks_count):
14 | coding_int = int.from_bytes(bs[3 * i:3 * i + 3], 'little')
15 | block = VariantBase64Dict[coding_int & 0x3f]
16 | block += VariantBase64Dict[(coding_int >> 6) & 0x3f]
17 | block += VariantBase64Dict[(coding_int >> 12) & 0x3f]
18 | block += VariantBase64Dict[(coding_int >> 18) & 0x3f]
19 | result += block.encode()
20 |
21 | if left_bytes == 0:
22 | return result
23 | elif left_bytes == 1:
24 | coding_int = int.from_bytes(bs[3 * blocks_count:], 'little')
25 | block = VariantBase64Dict[coding_int & 0x3f]
26 | block += VariantBase64Dict[(coding_int >> 6) & 0x3f]
27 | result += block.encode()
28 | return result
29 | else:
30 | coding_int = int.from_bytes(bs[3 * blocks_count:], 'little')
31 | block = VariantBase64Dict[coding_int & 0x3f]
32 | block += VariantBase64Dict[(coding_int >> 6) & 0x3f]
33 | block += VariantBase64Dict[(coding_int >> 12) & 0x3f]
34 | result += block.encode()
35 | return result
36 |
37 |
38 | def VariantBase64Decode(s: str):
39 | result = b''
40 | blocks_count, left_bytes = divmod(len(s), 4)
41 |
42 | for i in range(blocks_count):
43 | block = VariantBase64ReverseDict[s[4 * i]]
44 | block += VariantBase64ReverseDict[s[4 * i + 1]] << 6
45 | block += VariantBase64ReverseDict[s[4 * i + 2]] << 12
46 | block += VariantBase64ReverseDict[s[4 * i + 3]] << 18
47 | result += block.to_bytes(3, 'little')
48 |
49 | if left_bytes == 0:
50 | return result
51 | elif left_bytes == 2:
52 | block = VariantBase64ReverseDict[s[4 * blocks_count]]
53 | block += VariantBase64ReverseDict[s[4 * blocks_count + 1]] << 6
54 | result += block.to_bytes(1, 'little')
55 | return result
56 | elif left_bytes == 3:
57 | block = VariantBase64ReverseDict[s[4 * blocks_count]]
58 | block += VariantBase64ReverseDict[s[4 * blocks_count + 1]] << 6
59 | block += VariantBase64ReverseDict[s[4 * blocks_count + 2]] << 12
60 | result += block.to_bytes(2, 'little')
61 | return result
62 | else:
63 | raise ValueError('Invalid encoding.')
64 |
65 |
66 | def EncryptBytes(key: int, bs: bytes):
67 | result = bytearray()
68 | for i in range(len(bs)):
69 | result.append(bs[i] ^ ((key >> 8) & 0xff))
70 | key = result[-1] & key | 0x482D
71 | return bytes(result)
72 |
73 |
74 | def DecryptBytes(key: int, bs: bytes):
75 | result = bytearray()
76 | for i in range(len(bs)):
77 | result.append(bs[i] ^ ((key >> 8) & 0xff))
78 | key = bs[i] & key | 0x482D
79 | return bytes(result)
80 |
81 |
82 | class LicenseType:
83 | Professional = 1
84 | Educational = 3
85 | Persional = 4
86 |
87 |
88 | def GenerateLicense(Path: str, Type: LicenseType, Count: int, UserName: str, MajorVersion: int, MinorVersion):
89 | assert (Count >= 0)
90 | LicenseString = '%d#%s|%d%d#%d#%d3%d6%d#%d#%d#%d#' % (Type,
91 | UserName, MajorVersion, MinorVersion,
92 | Count,
93 | MajorVersion, MinorVersion, MinorVersion,
94 | 0, # Unknown
95 | 0,
96 | # No Games flag. 0 means "NoGames = false". But it does not work.
97 | 0) # No Plugins flag. 0 means "NoPlugins = false". But it does not work.
98 | EncodedLicenseString = VariantBase64Encode(EncryptBytes(0x787, LicenseString.encode())).decode()
99 | with zipfile.ZipFile(Path + '/Custom.mxtpro', 'w') as f:
100 | f.writestr('Pro.key', data=EncodedLicenseString)
101 |
102 |
103 | if __name__ == '__main__':
104 | MajorVersion, MinorVersion = sys.argv[2].split('.')[0:2]
105 | GenerateLicense(sys.argv[1], LicenseType.Professional, 1, "woytu", int(MajorVersion), int(MinorVersion))
106 |
--------------------------------------------------------------------------------
/mailtm/account.go:
--------------------------------------------------------------------------------
1 | package mailtm
2 |
3 | import (
4 | "encoding/json"
5 | "errors"
6 | )
7 |
8 | type Properties map[string]any
9 |
10 | type Account struct {
11 | address string
12 | password string
13 | bearer string
14 | properties Properties
15 | }
16 |
17 | func (account *Account) Address() string {
18 | return account.address
19 | }
20 |
21 | func (account *Account) Password() string {
22 | return account.password
23 | }
24 |
25 | func (account *Account) Bearer() string {
26 | return account.bearer
27 | }
28 |
29 | func (account *Account) Property(name string) (p any, ok bool) {
30 | property, exists := account.properties[name]
31 | return property, exists
32 | }
33 |
34 | func (account *Account) Delete() error {
35 | URI := URI_ACCOUNTS + "/" + account.properties["id"].(string)
36 | request := requestData{
37 | uri: URI,
38 | method: "DELETE",
39 | bearer: account.bearer,
40 | }
41 | response, err := makeRequest(request)
42 | if err != nil {
43 | return err
44 | }
45 | if response.code != 204 {
46 | return errors.New("failed to delete account")
47 | }
48 | return nil
49 | }
50 |
51 | type Options struct {
52 | Domain string
53 | Username string
54 | Password string
55 | }
56 |
57 | func NewAccount() (*Account, error) {
58 | domains, err := AvailableDomains()
59 | if err != nil {
60 | return nil, err
61 | }
62 | if len(domains) == 0 {
63 | return nil, errors.New("no domains available")
64 | }
65 | return NewAccountWithOptions(Options{
66 | Domain: domains[0].Domain,
67 | Username: generateString(16),
68 | Password: generateString(16),
69 | })
70 | }
71 |
72 | func Login(address string, password string) (*Account, error) {
73 | id, token, err := GetIdAndToken(address, password)
74 | if err != nil {
75 | return nil, err
76 | }
77 | account, err := LoginWithIdAndToken(id, token)
78 | if err != nil {
79 | return nil, err
80 | }
81 | account.password = password
82 | if violations, ok := account.Property("violations"); ok {
83 | return nil, errors.New(violations.([]any)[0].(map[string]any)["message"].(string))
84 | }
85 | return account, nil
86 | }
87 |
88 | func LoginWithToken(token string) (*Account, error) {
89 | account := new(Account)
90 | request := requestData{
91 | uri: URI_ME,
92 | method: "GET",
93 | bearer: token,
94 | }
95 | response, err := makeRequest(request)
96 | if err != nil {
97 | return nil, err
98 | }
99 | if response.code != 200 {
100 | return nil, errors.New("failed to get account")
101 | }
102 | json.Unmarshal(response.body, &account.properties)
103 | account.address = account.properties["address"].(string)
104 | account.bearer = token
105 | return account, nil
106 | }
107 |
108 | func GetIdAndToken(address string, password string) (string, string, error) {
109 | data := map[string]string{
110 | "address": address,
111 | "password": password,
112 | }
113 | body := make(map[string]any)
114 | request := requestData{
115 | uri: URI_TOKEN,
116 | method: "POST",
117 | body: data,
118 | }
119 | response, err := makeRequest(request)
120 | if err != nil {
121 | return "", "", err
122 | }
123 | if response.code != 200 {
124 | return "", "", errors.New("failed to get id and token")
125 | }
126 | err = json.Unmarshal(response.body, &body)
127 | if err != nil {
128 | return "", "", err
129 | }
130 | return body["id"].(string), body["token"].(string), nil
131 | }
132 |
133 | func LoginWithIdAndToken(id string, token string) (*Account, error) {
134 | account := new(Account)
135 | uri := URI_ACCOUNTS + "/" + id
136 | request := requestData{
137 | uri: uri,
138 | method: "GET",
139 | bearer: token,
140 | }
141 | response, err := makeRequest(request)
142 | if err != nil {
143 | return nil, err
144 | }
145 | if response.code != 200 {
146 | return nil, errors.New("failed to get account")
147 | }
148 | json.Unmarshal(response.body, &account.properties)
149 | account.address = account.properties["address"].(string)
150 | account.bearer = token
151 | return account, nil
152 | }
153 |
154 | func NewAccountWithOptions(options Options) (*Account, error) {
155 | address := options.Username + "@" + options.Domain
156 | password := options.Password
157 | data := map[string]string{
158 | "address": address,
159 | "password": password,
160 | }
161 | request := requestData{
162 | uri: URI_ACCOUNTS,
163 | method: "POST",
164 | body: data,
165 | }
166 | response, err := makeRequest(request)
167 | if err != nil {
168 | return nil, err
169 | }
170 | if response.code != 201 {
171 | return nil, errors.New("failed to create an account")
172 | }
173 | account, err := Login(address, password)
174 | return account, err
175 | }
176 |
--------------------------------------------------------------------------------
/static/html/nginx-format.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | NGINX配置格式化
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
52 |
53 |
54 |
55 |
105 |
106 |
107 |
108 |
109 |
119 |
120 |
121 |
--------------------------------------------------------------------------------
/static/js/layer/mobile/need/layer.css:
--------------------------------------------------------------------------------
1 | .layui-m-layer{position:relative;z-index:19891014}.layui-m-layer *{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}.layui-m-layermain,.layui-m-layershade{position:fixed;left:0;top:0;width:100%;height:100%}.layui-m-layershade{background-color:rgba(0,0,0,.7);pointer-events:auto}.layui-m-layermain{display:table;font-family:Helvetica,arial,sans-serif;pointer-events:none}.layui-m-layermain .layui-m-layersection{display:table-cell;vertical-align:middle;text-align:center}.layui-m-layerchild{position:relative;display:inline-block;text-align:left;background-color:#fff;font-size:14px;border-radius:5px;box-shadow:0 0 8px rgba(0,0,0,.1);pointer-events:auto;-webkit-overflow-scrolling:touch;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-duration:.2s;animation-duration:.2s}@-webkit-keyframes layui-m-anim-scale{0%{opacity:0;-webkit-transform:scale(.5);transform:scale(.5)}100%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}}@keyframes layui-m-anim-scale{0%{opacity:0;-webkit-transform:scale(.5);transform:scale(.5)}100%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}}.layui-m-anim-scale{animation-name:layui-m-anim-scale;-webkit-animation-name:layui-m-anim-scale}@-webkit-keyframes layui-m-anim-up{0%{opacity:0;-webkit-transform:translateY(800px);transform:translateY(800px)}100%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}}@keyframes layui-m-anim-up{0%{opacity:0;-webkit-transform:translateY(800px);transform:translateY(800px)}100%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}}.layui-m-anim-up{-webkit-animation-name:layui-m-anim-up;animation-name:layui-m-anim-up}.layui-m-layer0 .layui-m-layerchild{width:90%;max-width:640px}.layui-m-layer1 .layui-m-layerchild{border:none;border-radius:0}.layui-m-layer2 .layui-m-layerchild{width:auto;max-width:260px;min-width:40px;border:none;background:0 0;box-shadow:none;color:#fff}.layui-m-layerchild h3{padding:0 10px;height:60px;line-height:60px;font-size:16px;font-weight:400;border-radius:5px 5px 0 0;text-align:center}.layui-m-layerbtn span,.layui-m-layerchild h3{text-overflow:ellipsis;overflow:hidden;white-space:nowrap}.layui-m-layercont{padding:50px 30px;line-height:22px;text-align:center}.layui-m-layer1 .layui-m-layercont{padding:0;text-align:left}.layui-m-layer2 .layui-m-layercont{text-align:center;padding:0;line-height:0}.layui-m-layer2 .layui-m-layercont i{width:25px;height:25px;margin-left:8px;display:inline-block;background-color:#fff;border-radius:100%;-webkit-animation:layui-m-anim-loading 1.4s infinite ease-in-out;animation:layui-m-anim-loading 1.4s infinite ease-in-out;-webkit-animation-fill-mode:both;animation-fill-mode:both}.layui-m-layerbtn,.layui-m-layerbtn span{position:relative;text-align:center;border-radius:0 0 5px 5px}.layui-m-layer2 .layui-m-layercont p{margin-top:20px}@-webkit-keyframes layui-m-anim-loading{0%,100%,80%{transform:scale(0);-webkit-transform:scale(0)}40%{transform:scale(1);-webkit-transform:scale(1)}}@keyframes layui-m-anim-loading{0%,100%,80%{transform:scale(0);-webkit-transform:scale(0)}40%{transform:scale(1);-webkit-transform:scale(1)}}.layui-m-layer2 .layui-m-layercont i:first-child{margin-left:0;-webkit-animation-delay:-.32s;animation-delay:-.32s}.layui-m-layer2 .layui-m-layercont i.layui-m-layerload{-webkit-animation-delay:-.16s;animation-delay:-.16s}.layui-m-layer2 .layui-m-layercont>div{line-height:22px;padding-top:7px;margin-bottom:20px;font-size:14px}.layui-m-layerbtn{display:box;display:-moz-box;display:-webkit-box;width:100%;height:50px;line-height:50px;font-size:0;border-top:1px solid #D0D0D0;background-color:#F2F2F2}.layui-m-layerbtn span{display:block;-moz-box-flex:1;box-flex:1;-webkit-box-flex:1;font-size:14px;cursor:pointer}.layui-m-layerbtn span[yes]{color:#40AFFE}.layui-m-layerbtn span[no]{border-right:1px solid #D0D0D0;border-radius:0 0 0 5px}.layui-m-layerbtn span:active{background-color:#F6F6F6}.layui-m-layerend{position:absolute;right:7px;top:10px;width:30px;height:30px;border:0;font-weight:400;background:0 0;cursor:pointer;-webkit-appearance:none;font-size:30px}.layui-m-layerend::after,.layui-m-layerend::before{position:absolute;left:5px;top:15px;content:'';width:18px;height:1px;background-color:#999;transform:rotate(45deg);-webkit-transform:rotate(45deg);border-radius:3px}.layui-m-layerend::after{transform:rotate(-45deg);-webkit-transform:rotate(-45deg)}body .layui-m-layer .layui-m-layer-footer{position:fixed;width:95%;max-width:100%;margin:0 auto;left:0;right:0;bottom:10px;background:0 0}.layui-m-layer-footer .layui-m-layercont{padding:20px;border-radius:5px 5px 0 0;background-color:rgba(255,255,255,.8)}.layui-m-layer-footer .layui-m-layerbtn{display:block;height:auto;background:0 0;border-top:none}.layui-m-layer-footer .layui-m-layerbtn span{background-color:rgba(255,255,255,.8)}.layui-m-layer-footer .layui-m-layerbtn span[no]{color:#FD482C;border-top:1px solid #c2c2c2;border-radius:0 0 5px 5px}.layui-m-layer-footer .layui-m-layerbtn span[yes]{margin-top:10px;border-radius:5px}body .layui-m-layer .layui-m-layer-msg{width:auto;max-width:90%;margin:0 auto;bottom:-150px;background-color:rgba(0,0,0,.7);color:#fff}.layui-m-layer-msg .layui-m-layercont{padding:10px 20px}
--------------------------------------------------------------------------------
/utils/time.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "time"
7 | )
8 |
9 | // IsTimeEmpty 判断时间是否为空
10 | func IsTimeEmpty(t time.Time) bool {
11 | if !t.IsZero() {
12 | return false
13 | }
14 | return true
15 | }
16 |
17 | // FormatDateString 转换为自定义格式
18 | func FormatDateString(timestamp uint32, format string) string {
19 | if timestamp <= 0 {
20 | return ""
21 | }
22 | tm := time.Unix(int64(timestamp), 0)
23 | if IsStringEmpty(format) {
24 | return tm.Format(time.DateOnly)
25 | }
26 | return tm.Format(format)
27 | }
28 |
29 | // FormatDateTimeString 获取时间,格式yyyy-MM-dd HH:mm:ss
30 | func FormatDateTimeString(timestamp uint32, format string) string {
31 | if timestamp <= 0 {
32 | return ""
33 | }
34 | tm := time.Unix(int64(timestamp), 0)
35 | if IsStringEmpty(format) {
36 | return tm.Format(time.DateTime)
37 | }
38 | return tm.Format(format)
39 | }
40 |
41 | // TimeToString 时间转字符串,格式yyyy-MM-dd HH:mm:ss
42 | func TimeToString(t time.Time) string {
43 | if IsTimeEmpty(t) {
44 | t = time.Now()
45 | }
46 | return t.Format(time.DateTime)
47 | }
48 |
49 | // StringToTime 字符串转时间
50 | func StringToTime(str string) (time.Time, error) {
51 | if IsStringEmpty(str) {
52 | return time.Now(), nil
53 | }
54 | local, err := time.LoadLocation("Local")
55 | if err != nil {
56 | return time.Time{}, err
57 | }
58 | t, err := time.ParseInLocation(time.DateTime, str, local)
59 | if err != nil {
60 | return time.Time{}, err
61 | }
62 | return t, nil
63 | }
64 |
65 | // ParseDate 解析字符串日期为系统格式
66 | func ParseDate(dates string) (time.Time, error) {
67 | if IsStringEmpty(dates) {
68 | return time.Time{}, errors.New("参数错误")
69 | }
70 | loc, err := time.LoadLocation("Local")
71 | if err != nil {
72 | return time.Time{}, err
73 | }
74 | parse, err := time.ParseInLocation(time.DateOnly, dates, loc)
75 | if err != nil {
76 | return time.Time{}, err
77 | }
78 | return parse, nil
79 | }
80 |
81 | // DateEqual 判断两个日期是否相等
82 | func DateEqual(date1, date2 time.Time) bool {
83 | y1, m1, d1 := date1.Date()
84 | y2, m2, d2 := date2.Date()
85 | return y1 == y2 && m1 == m2 && d1 == d2
86 | }
87 |
88 | // GetDateFormat 转换为自定义格式
89 | func GetDateFormat(timestamp uint32, format string) string {
90 | if timestamp <= 0 {
91 | return ""
92 | }
93 | tm := time.Unix(int64(timestamp), 0)
94 | return tm.Format(format)
95 | }
96 |
97 | // GetDate 获取时间,使用默认格式
98 | func GetDate(timestamp uint32) string {
99 | if timestamp <= 0 {
100 | return ""
101 | }
102 | tm := time.Unix(int64(timestamp), 0)
103 | return tm.Format(time.DateOnly)
104 | }
105 |
106 | // GetyyyyMMddHHmm 获取时间,格式yyyy-MM-dd HH:mm
107 | func GetyyyyMMddHHmm(timestamp uint32) string {
108 | if timestamp <= 0 {
109 | return ""
110 | }
111 | tm := time.Unix(int64(timestamp), 0)
112 | return tm.Format(time.DateOnly + " 15:04")
113 | }
114 |
115 | // GetTimeParse 解析字符串时间为系统格式
116 | func GetTimeParse(times string) int64 {
117 | if "" == times {
118 | return 0
119 | }
120 | loc, _ := time.LoadLocation("Local")
121 | parse, _ := time.ParseInLocation(time.DateOnly+" 15:04", times, loc)
122 | return parse.Unix()
123 | }
124 |
125 | // GetDateParse 解析字符串日期为系统格式
126 | func GetDateParse(dates string) int64 {
127 | if "" == dates {
128 | return 0
129 | }
130 | loc, _ := time.LoadLocation("Local")
131 | parse, _ := time.ParseInLocation(time.DateOnly, dates, loc)
132 | return parse.Unix()
133 | }
134 |
135 | // SchedulerIntervalsTimer 启动的时候执行一次,不固定某个时间,滚动间隔时间执行
136 | func SchedulerIntervalsTimer(f func(), duration time.Duration) {
137 | // 定时任务
138 | ticker := time.NewTicker(duration)
139 | for {
140 | go f()
141 | <-ticker.C
142 | }
143 | }
144 |
145 | // SchedulerIntervalsTimerContext
146 | // 创建一个可以被取消的 context
147 | // ctx, cancel := context.WithCancel(context.Background())
148 | func SchedulerIntervalsTimerContext(ctx context.Context, f func(), duration time.Duration) {
149 | ticker := time.NewTicker(duration)
150 | // 在函数退出时,一定要调用 Stop() 来释放资源
151 | defer ticker.Stop()
152 |
153 | for {
154 | select {
155 | case <-ticker.C:
156 | // 等待一个 tick 到达后,再执行任务
157 | // 这样就避免了立即执行,并且保证了任务不会堆积
158 | f()
159 | case <-ctx.Done():
160 | // 如果外部的 context 发出了取消信号,则退出循环
161 | return
162 | }
163 | }
164 | }
165 |
166 | // SchedulerFixedTicker 启动的时候执行一次,固定在每天的某个时间滚动执行
167 | // 首次执行:函数 f 会在 SchedulerFixedTicker 被调用的一瞬间就执行一次。
168 | // 第二次执行:会在下一个午夜0点左右执行。
169 | // 后续执行:从第二次执行开始,每次执行的间隔由传入的 duration 参数决定。
170 | // 如果 duration = 24 * time.Hour:那么它确实会近似于“每天执行一次”。但由于 timer.Reset 存在微小的漂移,长时间运行后,执行时间可能会偏离午夜0点。
171 | // 如果 duration = 1 * time.Hour:那么在第二次执行(午夜0点)之后,它会变成每小时执行一次。
172 | // 如果 duration 是其他值:它就会按该值的间隔执行。
173 | func SchedulerFixedTicker(f func(), duration time.Duration) {
174 | now := time.Now()
175 | // 计算下一个时间点
176 | next := now.Add(duration)
177 | // 设置目标时间为今天的指定时分秒
178 | next = time.Date(next.Year(), next.Month(), next.Day(), 0, 0, 0, 0, next.Location())
179 | // 如果目标时间已经过去,则设置为明天的这个时间
180 | if next.Sub(now) <= 0 {
181 | next = next.Add(time.Hour * 24)
182 | }
183 | // 计算第一次需要等待的时间
184 | timer := time.NewTimer(next.Sub(now))
185 | for {
186 | go f()
187 | // 等待定时器触发
188 | <-timer.C
189 | // Reset 使 ticker 重新开始计时,否则会导致通道堵塞,(本方法返回后再)等待时间段 d 过去后到期。
190 | // 如果调用时t还在等待中会返回真;如果 t已经到期或者被停止了会返回假
191 | timer.Reset(duration)
192 | }
193 | }
194 |
--------------------------------------------------------------------------------
/static/css/common.css:
--------------------------------------------------------------------------------
1 | @media screen and (min-width: 35.5em) {
2 | /* 弹窗的样式 */
3 | div[times][type="page"] {
4 | width: 90vw !important;
5 | max-height: 95vh !important;
6 | }
7 | }
8 |
9 | @media screen and (min-width: 48em) {
10 | /* 弹窗的样式 */
11 | div[times][type="page"] {
12 | width: 80vw !important;
13 | max-height: 95vh !important;
14 | }
15 | }
16 |
17 | @media screen and (min-width: 58em) {
18 | /* 弹窗的样式 */
19 | div[times][type="page"] {
20 | width: 70vw !important;
21 | max-height: 95vh !important;
22 | }
23 | }
24 |
25 | @media screen and (min-width: 75em) {
26 | /* 弹窗的样式 */
27 | div[times][type="page"] {
28 | width: 60vw !important;
29 | max-height: 95vh !important;
30 | }
31 | }
32 |
33 | @media screen and (max-width: 35.5em) {
34 | .legal-license, .legal-copyright {
35 | width: 100%;
36 | text-align: center;
37 | margin: 0;
38 | }
39 |
40 | /* 弹窗的样式 */
41 | div[times][type="page"] {
42 | width: 90vw !important;
43 | max-height: 95vh !important;
44 | }
45 | }
46 |
47 | * {
48 | margin: 0;
49 | padding: 0;
50 | }
51 |
52 | html, body {
53 | height: 100%;
54 | width: 100%;
55 | /* 处理文本溢出 */
56 | word-wrap: break-word !important;
57 | /* 允许再单词内换行 */
58 | word-break: break-all;
59 | color: #777;
60 | }
61 |
62 | h1, h2, h3, h4, h5, h6 {
63 | font-weight: bold;
64 | color: #4b4b4b
65 | }
66 |
67 | h3 {
68 | font-size: 1.25em
69 | }
70 |
71 | h4 {
72 | font-size: 1.125em
73 | }
74 |
75 | a {
76 | color: #3b8bba;
77 | text-decoration: none
78 | }
79 |
80 | a:visited {
81 | color: #265778
82 | }
83 |
84 | dt {
85 | font-weight: bold
86 | }
87 |
88 | dd {
89 | margin: 0 0 10px 0
90 | }
91 |
92 | aside {
93 | background: #1f8dd6;
94 | padding: .3em 1em;
95 | border-radius: 3px;
96 | color: #fff
97 | }
98 |
99 | aside a, aside a:visited {
100 | color: #a9e2ff
101 | }
102 |
103 | pre, code {
104 | font-family: Consolas, Courier, monospace;
105 | color: #333;
106 | background: #fafafa
107 | }
108 |
109 | code {
110 | padding: .2em .4em;
111 | white-space: nowrap;
112 | color: #50504c;
113 | background-color: rgba(27, 31, 35, .05);
114 | }
115 |
116 | .button-success, .button-error, .button-warning, .button-secondary {
117 | color: white;
118 | border-radius: 4px;
119 | text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
120 | }
121 |
122 | .button-success {
123 | background: rgb(28, 184, 65);
124 | }
125 |
126 | .button-error {
127 | background: rgb(202, 60, 60);
128 | }
129 |
130 | .button-warning {
131 | background: rgb(223, 117, 20);
132 | }
133 |
134 | .button-secondary {
135 | background: rgb(66, 184, 221);
136 | }
137 |
138 | .button-xsmall {
139 | font-size: 70%;
140 | }
141 |
142 | .button-small {
143 | font-size: 85%;
144 | }
145 |
146 | .button-large {
147 | font-size: 110%;
148 | }
149 |
150 | .button-xlarge {
151 | font-size: 125%;
152 | }
153 |
154 | .table {
155 | border-top: solid 1px #ddd;
156 | border-left: solid 1px #ddd;
157 | margin: 0 auto;
158 | text-indent: 0;
159 | text-align: center;
160 | }
161 |
162 | .table th {
163 | background-color: #1abc9c;
164 | color: #fff;
165 | }
166 |
167 | .table td, .table th {
168 | border-bottom: solid 1px #ddd;
169 | border-right: solid 1px #ddd;
170 | padding: 10px 15px;
171 | }
172 |
173 | .table td:hover {
174 | font-weight: 900;
175 | background-color: #c0e712;
176 | }
177 |
178 | .table td a:hover {
179 | color: rgba(255, 0, 0, 0.6);
180 | }
181 |
182 | .table tr {
183 | background-color: #fff;
184 | transition: 0.3s;
185 | }
186 |
187 | .table tr:hover {
188 | background-color: #f9f9f9;
189 | transition: 0.3s;
190 | }
191 |
192 | tbody tr:nth-child(odd) {
193 | background-color: #f9f9f9;
194 | }
195 |
196 | /* ------------------------------- */
197 |
198 | .main {
199 | min-height: 100%;
200 | display: flex;
201 | flex-direction: column;
202 | }
203 |
204 | .header {
205 | font-family: "Raleway", "Helvetica Neue", Helvetica, Arial, sans-serif;
206 | max-width: 768px;
207 | margin: 0 auto;
208 | padding: 1em;
209 | text-align: center;
210 | border-bottom: 1px solid #eee;
211 | background: #fff;
212 | letter-spacing: .05em;
213 | flex: 0 0 auto;
214 | }
215 |
216 | .header h1 {
217 | font-size: 300%;
218 | font-weight: 100;
219 | margin: 0
220 | }
221 |
222 | .header h2 {
223 | font-size: 125%;
224 | font-weight: 100;
225 | line-height: 1.5;
226 | margin: 0;
227 | color: #666;
228 | letter-spacing: -0.02em
229 | }
230 |
231 | .content {
232 | margin: 20px auto;
233 | padding-left: 1em;
234 | padding-right: 1em;
235 | max-width: 768px;
236 | margin-top: 10px;
237 | flex: 1;
238 | }
239 |
240 | .footer {
241 | border-top: 1px solid #eee;
242 | padding: 1.1429em;
243 | color: #777;
244 | background: #fafafa;
245 | text-align: center;
246 | line-height: 1.6;
247 | }
248 |
249 | .legal-license {
250 | text-align: left;
251 | }
252 |
253 | .legal-copyright {
254 | text-align: right;
255 | }
256 |
257 | .pure-button {
258 | font-family: inherit
259 | }
--------------------------------------------------------------------------------
/reptile/mail.go:
--------------------------------------------------------------------------------
1 | package reptile
2 |
3 | /**
4 | *
5 | * @Description:
6 | * @Author: https://www.bajins.com
7 | * @File: mail.go
8 | * @Version: 1.0.0
9 | * @Time: 2019/9/16 11:36
10 | * @Project: tool-gin
11 | * @Package:
12 | * @Software: GoLand
13 | */
14 |
15 | import (
16 | "encoding/base64"
17 | "errors"
18 | "math"
19 | "net/http"
20 | "net/mail"
21 | "strings"
22 | "time"
23 | "tool-gin/utils"
24 |
25 | "github.com/antchfx/htmlquery"
26 | "github.com/chromedp/chromedp"
27 | )
28 |
29 | // DecodeMail 解码邮件内容 https://github.com/alexcesaro/quotedprintable
30 | func DecodeMail(msg *mail.Message) ([]byte, error) {
31 | body := utils.BytesToStringByBuffer(msg.Body)
32 | if len(body) == 0 || body == "" {
33 | return nil, errors.New("邮件内容不正确")
34 | }
35 | encoding := msg.Header.Get("Content-Transfer-Encoding")
36 | // 解码,邮件协议Content-Transfer-Encoding指定了编码方式
37 | if encoding == "base64" {
38 | body, err := base64.StdEncoding.DecodeString(body)
39 | return body, err
40 | }
41 | return nil, errors.New("解码方式错误:" + encoding)
42 | }
43 |
44 | const LinShiYouXiang = "https://www.linshiyouxiang.net"
45 |
46 | // LinShiYouXiangSuffix 获取邮箱号后缀
47 | func LinShiYouXiangSuffix() (string, error) {
48 | var suffixArray []string
49 | response, err := utils.HttpRequest(http.MethodGet, LinShiYouXiang, "", nil, nil)
50 | if err != nil {
51 | return "", err
52 | }
53 | root, err := htmlquery.Parse(response.Body)
54 | if err != nil {
55 | return "", err
56 | }
57 | li := htmlquery.Find(root, "//*[@id='top']/div/div/div[2]/div/div[2]/ul/li")
58 | for _, row := range li {
59 | m := htmlquery.InnerText(row)
60 | suffixArray = append(suffixArray, m)
61 | }
62 | suffixArrayLen := len(suffixArray)
63 | if suffixArrayLen == 0 {
64 | return "", nil
65 | }
66 | return suffixArray[utils.RandIntn(len(suffixArray)-1)], nil
67 | }
68 |
69 | // LinShiYouXiangApply 获取邮箱号
70 | // prefix: 邮箱前缀
71 | func LinShiYouXiangApply(prefix string) (map[string]interface{}, error) {
72 | url := LinShiYouXiang + "/api/v1/mailbox/keepalive"
73 | param := map[string]string{
74 | "force_change": "1",
75 | "mailbox": prefix,
76 | "_ts": utils.ToString(math.Round(float64(time.Now().Unix() / 1000))),
77 | }
78 | r, e := utils.HttpReadBodyJsonMap(http.MethodGet, url, "", param, nil)
79 | return r, e
80 | }
81 |
82 | // LinShiYouXiangList 获取邮件列表
83 | // prefix: 邮箱前缀
84 | func LinShiYouXiangList(prefix string) ([]map[string]interface{}, error) {
85 | url := LinShiYouXiang + "/api/v1/mailbox/" + prefix
86 | return utils.HttpReadBodyJsonMapArray(http.MethodGet, url, "", nil, nil)
87 | }
88 |
89 | // LinShiYouXiangGetMail 获取邮件内容
90 | // prefix: 邮箱前缀
91 | // id: 邮件编号
92 | //
93 | // 获取到邮件需要做以下操作
94 | // 分割取内容
95 | // text := strings.Split(content, "AmazonSES")
96 | // 解密,邮件协议Content-Transfer-Encoding指定了base64
97 | // htmlText, err := base64.StdEncoding.DecodeString(text[1])
98 | // 解析HTML
99 | // doc, err := goquery.NewDocumentFromReader(bytes.NewReader(htmlText))
100 | func LinShiYouXiangGetMail(prefix, id string) (*mail.Message, error) {
101 | url := LinShiYouXiang + "/mailbox/" + prefix + "/" + id + "/source"
102 | content, err := utils.HttpReadBodyString(http.MethodGet, url, "", nil, nil)
103 | if err != nil {
104 | return nil, err
105 | }
106 | r := strings.NewReader(content)
107 | m, err := mail.ReadMessage(r) // 解析邮件
108 | return m, err
109 | }
110 |
111 | // LinShiYouXiangDelete 删除邮件
112 | // prefix: 邮箱前缀
113 | // id: 邮件编号
114 | func LinShiYouXiangDelete(prefix, id string) (map[string]interface{}, error) {
115 | url := LinShiYouXiang + "/api/v1/mailbox/" + prefix + "/" + id
116 | return utils.HttpReadBodyJsonMap(http.MethodDelete, url, "", nil, nil)
117 | }
118 |
119 | const Mail24 = "http://24mail.chacuo.net"
120 |
121 | func GetMail24MailName(res *string) chromedp.Tasks {
122 | return chromedp.Tasks{
123 | // 浏览器下载行为,注意设置顺序,如果不是第一个会失败
124 | //page.SetDownloadBehavior(page.SetDownloadBehaviorBehaviorDeny),
125 | //network.Enable(),
126 | //visitWeb(url),
127 | //doCrawler(&res),
128 | //Screenshot(),
129 | // 跳转页面
130 | chromedp.Navigate(Mail24),
131 | chromedp.Sleep(20 * time.Second),
132 | // 查找并等待可见
133 | chromedp.WaitVisible("mail_cur_name", chromedp.ByID),
134 | chromedp.WaitReady("mail_cur_name", chromedp.ByID),
135 | chromedp.Value("mail_cur_name", res, chromedp.ByID),
136 | // 点击元素
137 | //chromedp.Click(`input[value="开始试用"][type="submit"]`, chromedp.BySearch),
138 | // 读取HTML源码
139 | //chromedp.OuterHTML(`.fusion-text h1`, res, chromedp.BySearch),
140 | //chromedp.Text(`//*[@id="content"]/div/div/div[2]/div/div/div/div[1]/h1`, res, chromedp.BySearch),
141 | //chromedp.TextContent(`.fusion-text h1`, res, chromedp.BySearch),
142 | //chromedp.Title(res),
143 | }
144 | }
145 |
146 | // GetMail24List 获取邮件列表
147 | func GetMail24List(res *string) chromedp.Tasks {
148 | return chromedp.Tasks{
149 | // 浏览器下载行为,注意设置顺序,如果不是第一个会失败
150 | //page.SetDownloadBehavior(page.SetDownloadBehaviorBehaviorDeny),
151 | chromedp.Sleep(20 * time.Second),
152 | // 读取HTML源码
153 | chromedp.InnerHTML(`//*[@id="convertd"]`, res, chromedp.BySearch),
154 | }
155 | }
156 |
157 | // GetMail24LatestMail 获取最新邮件
158 | func GetMail24LatestMail(res *string) chromedp.Tasks {
159 | return chromedp.Tasks{
160 | // 浏览器下载行为,注意设置顺序,如果不是第一个会失败
161 | //page.SetDownloadBehavior(page.SetDownloadBehaviorBehaviorDeny),
162 | chromedp.WaitVisible(`//*[@id="convertd"]/tr[1]`, chromedp.BySearch),
163 | chromedp.Click(`//*[@id="convertd"]/tr[1]`, chromedp.BySearch),
164 | chromedp.Sleep(10 * time.Second),
165 | //chromedp.WaitVisible(`//*[@id="mailview_data"]`, chromedp.BySearch),
166 | chromedp.Click(`//*[@id="mailview"]/thead/tr[1]/td/a[1]`, chromedp.BySearch),
167 | chromedp.TextContent(`//*[@id="mailview_data"]`, res, chromedp.BySearch),
168 | }
169 | }
170 |
--------------------------------------------------------------------------------
/router.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "github.com/gin-gonic/gin"
4 |
5 | // Context 是一个自定义的请求上下文结构体,封装了 gin.Context
6 | type Context struct {
7 | C *gin.Context
8 | }
9 |
10 | // New 创建一个新的自定义 Context 实例
11 | func New(c *gin.Context) *Context {
12 | return &Context{C: c}
13 | }
14 |
15 | // HandlerFunc 定义 Handler 函数签名
16 | type HandlerFunc func(*Context)
17 |
18 | // Wrap 适配器函数
19 | // 它接收我们自定义的 HandlerFunc,并返回一个标准的 gin.HandlerFunc
20 | func Wrap(handler HandlerFunc) gin.HandlerFunc {
21 | return func(c *gin.Context) {
22 | // 在这里,我们执行了之前在每个 Handler 开头都要做的工作
23 | // 创建自定义上下文
24 | ctx := New(c)
25 |
26 | // 调用我们真正的业务逻辑 Handler
27 | handler(ctx)
28 | }
29 | }
30 |
31 | // wrapHandlers 是一个辅助函数,用于将我们自定义的 HandlerFunc 列表转换为 gin.HandlerFunc 列表
32 | func wrapHandlers(handlers []HandlerFunc) []gin.HandlerFunc {
33 | wrappedHandlers := make([]gin.HandlerFunc, len(handlers))
34 | for i, handler := range handlers {
35 | wrappedHandlers[i] = Wrap(handler)
36 | }
37 | return wrappedHandlers
38 | }
39 |
40 | // CustomRouterGroup 是我们自定义的路由组,它封装了 gin.RouterGroup
41 | type CustomRouterGroup struct {
42 | *gin.RouterGroup
43 | }
44 |
45 | // Group 重写了原生的 Group 方法,确保返回的是我们自己的 CustomRouterGroup
46 | // 这样可以支持无限层级的路由分组,且每一层都支持自定义 Handler
47 | func (g *CustomRouterGroup) Group(relativePath string, handlers ...HandlerFunc) *CustomRouterGroup {
48 | // 调用 wrapHandlers 转换中间件
49 | ginHandlers := wrapHandlers(handlers)
50 | // 调用原生的 Group 方法创建新的 gin.RouterGroup
51 | newGinGroup := g.RouterGroup.Group(relativePath, ginHandlers...)
52 | // 将新的 gin.RouterGroup 包装成我们自己的 CustomRouterGroup 并返回
53 | return &CustomRouterGroup{RouterGroup: newGinGroup}
54 | }
55 |
56 | // 以下是所有 HTTP 方法的重写
57 | // 它们都遵循相同的模式:
58 | // 1. 接收自定义的 HandlerFunc
59 | // 2. 调用 wrapHandlers 进行转换
60 | // 3. 调用 gin.RouterGroup 中对应的原生方法
61 | // 4. 返回 *CustomRouterGroup 以支持链式调用 (e.g., r.GET(...).Use(...))
62 | // 注意:这里的 Use 仍然是 gin.Use,接收 gin.HandlerFunc
63 |
64 | func (g *CustomRouterGroup) POST(relativePath string, handlers ...HandlerFunc) *CustomRouterGroup {
65 | g.RouterGroup.POST(relativePath, wrapHandlers(handlers)...)
66 | return g
67 | }
68 |
69 | func (g *CustomRouterGroup) GET(relativePath string, handlers ...HandlerFunc) *CustomRouterGroup {
70 | g.RouterGroup.GET(relativePath, wrapHandlers(handlers)...)
71 | return g
72 | }
73 |
74 | func (g *CustomRouterGroup) DELETE(relativePath string, handlers ...HandlerFunc) *CustomRouterGroup {
75 | g.RouterGroup.DELETE(relativePath, wrapHandlers(handlers)...)
76 | return g
77 | }
78 |
79 | func (g *CustomRouterGroup) PATCH(relativePath string, handlers ...HandlerFunc) *CustomRouterGroup {
80 | g.RouterGroup.PATCH(relativePath, wrapHandlers(handlers)...)
81 | return g
82 | }
83 |
84 | func (g *CustomRouterGroup) PUT(relativePath string, handlers ...HandlerFunc) *CustomRouterGroup {
85 | g.RouterGroup.PUT(relativePath, wrapHandlers(handlers)...)
86 | return g
87 | }
88 |
89 | func (g *CustomRouterGroup) OPTIONS(relativePath string, handlers ...HandlerFunc) *CustomRouterGroup {
90 | g.RouterGroup.OPTIONS(relativePath, wrapHandlers(handlers)...)
91 | return g
92 | }
93 |
94 | func (g *CustomRouterGroup) HEAD(relativePath string, handlers ...HandlerFunc) *CustomRouterGroup {
95 | g.RouterGroup.HEAD(relativePath, wrapHandlers(handlers)...)
96 | return g
97 | }
98 |
99 | func (g *CustomRouterGroup) Any(relativePath string, handlers ...HandlerFunc) *CustomRouterGroup {
100 | g.RouterGroup.Any(relativePath, wrapHandlers(handlers)...)
101 | return g
102 | }
103 |
104 | // Handle 方法也需要重写
105 | func (g *CustomRouterGroup) Handle(httpMethod, relativePath string, handlers ...HandlerFunc) *CustomRouterGroup {
106 | g.RouterGroup.Handle(httpMethod, relativePath, wrapHandlers(handlers)...)
107 | return g
108 | }
109 |
110 | // 注意: Static, StaticFile, StaticFS 等方法不需要重写,因为它们不接收 HandlerFunc
111 | // 通过内嵌 *gin.RouterGroup,这些方法被自动继承,可以直接使用。
112 |
113 | // Engine 是我们的自定义引擎,封装了 gin.Engine
114 | type Engine struct {
115 | *gin.Engine
116 | RouterGroup *CustomRouterGroup
117 | }
118 |
119 | // NewEngine 创建并返回一个我们自定义的 Engine
120 | func NewEngine() *Engine {
121 | e := gin.Default() // 或者 gin.New(),并按需添加中间件
122 | return &Engine{
123 | Engine: e,
124 | RouterGroup: &CustomRouterGroup{
125 | RouterGroup: &e.RouterGroup,
126 | },
127 | }
128 | }
129 |
130 | // 为了让 r.POST(...) 这种顶层调用生效,我们也需要在 Engine 上实现这些方法
131 | // 这些方法直接代理到其内部的 RouterGroup
132 |
133 | func (e *Engine) Group(relativePath string, handlers ...HandlerFunc) *CustomRouterGroup {
134 | return e.RouterGroup.Group(relativePath, handlers...)
135 | }
136 |
137 | func (e *Engine) POST(relativePath string, handlers ...HandlerFunc) *CustomRouterGroup {
138 | return e.RouterGroup.POST(relativePath, handlers...)
139 | }
140 |
141 | func (e *Engine) GET(relativePath string, handlers ...HandlerFunc) *CustomRouterGroup {
142 | return e.RouterGroup.GET(relativePath, handlers...)
143 | }
144 |
145 | func (e *Engine) DELETE(relativePath string, handlers ...HandlerFunc) *CustomRouterGroup {
146 | return e.RouterGroup.DELETE(relativePath, handlers...)
147 | }
148 |
149 | func (e *Engine) PATCH(relativePath string, handlers ...HandlerFunc) *CustomRouterGroup {
150 | return e.RouterGroup.PATCH(relativePath, handlers...)
151 | }
152 |
153 | func (e *Engine) PUT(relativePath string, handlers ...HandlerFunc) *CustomRouterGroup {
154 | return e.RouterGroup.PUT(relativePath, handlers...)
155 | }
156 |
157 | func (e *Engine) OPTIONS(relativePath string, handlers ...HandlerFunc) *CustomRouterGroup {
158 | return e.RouterGroup.OPTIONS(relativePath, handlers...)
159 | }
160 |
161 | func (e *Engine) HEAD(relativePath string, handlers ...HandlerFunc) *CustomRouterGroup {
162 | return e.RouterGroup.HEAD(relativePath, handlers...)
163 | }
164 |
165 | func (e *Engine) Any(relativePath string, handlers ...HandlerFunc) *CustomRouterGroup {
166 | return e.RouterGroup.Any(relativePath, handlers...)
167 | }
168 |
169 | func (e *Engine) Handle(httpMethod, relativePath string, handlers ...HandlerFunc) *CustomRouterGroup {
170 | return e.RouterGroup.Handle(httpMethod, relativePath, handlers...)
171 | }
172 |
--------------------------------------------------------------------------------
/static/js/utils/array.js:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * @Description:
4 | * @Author: claer
5 | * @File: array.js
6 | * @Version: 1.0.0
7 | * @Time: 2019/9/15 20:01
8 | * @Project: tool-gin
9 | * @Package:
10 | * @Software: GoLand
11 | */
12 |
13 |
14 | /**
15 | * splice方法删除数组中的空值
16 | *
17 | * @param array
18 | * @returns {*}
19 | */
20 | const trimSpace = (array) => {
21 | for (let i = 0; i < array.length; i++) {
22 | if (array[i] == " " || array[i] == null || typeof (array[i]) == "undefined") {
23 | array.splice(i, 1);
24 | i = i - 1;
25 | }
26 | }
27 | return array;
28 | }
29 |
30 | /**
31 | * filter 过滤方法删除数组中的空值
32 | *
33 | * @param array
34 | */
35 | const trimFilter = (array) => {
36 | array.filter(function (s) {
37 | return s && s.trim(); // 注:IE9(不包含IE9)以下的版本没有trim()方法
38 | });
39 | }
40 |
41 |
42 | /**
43 | * 过滤不在数组中的值
44 | *
45 | * @param arr 元数据数组
46 | * @param retentionArr 需要保留的值数组
47 | * @returns {[]} 去掉值后的新数组
48 | */
49 | const notInArrayKV = (arr, retentionArr) => {
50 | let newArr = [];
51 | arr.forEach(function (value) {
52 | // 判断文件名以什么开头、是否在指定数组中存在
53 | if (!value.startsWith(".") && !retentionArr.includes(value)) {
54 | newArr.push(value);
55 | }
56 | });
57 | return newArr;
58 | }
59 |
60 |
61 | /**
62 | * 过滤在数组中的值
63 | *
64 | * @param arr 元数据数组
65 | * @param ignoresArr 需要去除的值数组
66 | * @returns {[]} 去掉值后的新数组
67 | */
68 | const inArrayKV = (arr, ignoresArr) => {
69 | let newArr = [];
70 | arr.forEach(function (value) {
71 | // 判断文件名以什么开头、是否在指定数组中存在
72 | if (!value.startsWith(".") && ignoresArr.includes(value)) {
73 | newArr.push(value);
74 | }
75 | });
76 | return newArr;
77 | }
78 |
79 | /**
80 | * 插入去重的元素
81 | *
82 | * @param array
83 | * @param element
84 | * @returns {*}
85 | */
86 | const reinsertElement = (array, element) => {
87 | if (array.indexOf(element) === -1) {
88 | array.push(element);
89 | }
90 | return array;
91 | }
92 |
93 |
94 | /**
95 | * 自定义数组合并并去重函数
96 | *
97 | * @param arr1
98 | * @param arr2
99 | * @returns {*}
100 | */
101 | const mergeArray = (arr1, arr2) => {
102 | // let _arr = new Array();
103 | // for (let i = 0; i < arr1.length; i++) {
104 | // _arr.push(arr1[i]);
105 | // }
106 | // for (let i = 0; i < arr2.length; i++) {
107 | // let flag = true;
108 | // for (let j = 0; j < arr1.length; j++) {
109 | // if (arr2[i] == arr1[j]) {
110 | // flag = false;
111 | // break;
112 | // }
113 | // }
114 | // if (flag) {
115 | // _arr.push(arr2[i]);
116 | // }
117 | // }
118 |
119 | for (let i = 0; i < arr2.length; i++) {
120 | if (arr1.indexOf(arr2[i]) === -1) {
121 | arr1.push(arr2[i]);
122 | }
123 | }
124 | return arr1;
125 | }
126 |
127 |
128 | /**
129 | * 将数组平均分割
130 | *
131 | * @param arr 数组
132 | * @param len 分割成多少个
133 | * @returns {[]}
134 | */
135 | const splitArray = (arr, len) => {
136 | let arr_length = arr.length;
137 | let newArr = [];
138 | for (let i = 0; i < arr_length; i += len) {
139 | newArr.push(arr.slice(i, i + len));
140 | }
141 | return newArr;
142 | }
143 |
144 | /**
145 | * 判断数组中是否包含指定字符串
146 | *
147 | * @param arr
148 | * @param obj
149 | * @returns {boolean}
150 | */
151 | const isInArray = (arr, obj) => {
152 | let i = arr.length;
153 | while (i--) {
154 | if (obj.match(RegExp(`^.*${arr[i]}.*`))) {
155 | return true;
156 | }
157 | }
158 | return false;
159 | }
160 |
161 |
162 | /**
163 | * 类正态排序
164 | *
165 | * @param arr
166 | * @returns {[]}
167 | */
168 | const normalSort = function (arr) {
169 | let temp = [];
170 | //先将数组从小到大排列得到 [1, 1, 2, 2, 3, 3, 3, 4, 6]
171 | let sortArr = arr.sort(function (a, b) {
172 | return a - b
173 | });
174 | for (let i = 0, l = arr.length; i < l; i++) {
175 | if (i % 2 == 0) {
176 | // 下标为偶数的顺序放到前边
177 | temp[i / 2] = sortArr[i];
178 | } else {
179 | // 下标为奇数的从后往前放
180 | temp[l - (i + 1) / 2] = sortArr[i];
181 | }
182 | }
183 | return temp;
184 | }
185 |
186 | /**
187 | * 利用Box-Muller方法极坐标形式
188 | * 使用两个均匀分布产生一个正态分布
189 | *
190 | * @param mean
191 | * @param sigma
192 | * @returns {*}
193 | */
194 | const normalDistribution = function (mean, sigma) {
195 | let u = 0.0;
196 | let v = 0.0;
197 | let w = 0.0;
198 | let c;
199 | do {
200 | //获得两个(-1,1)的独立随机变量
201 | u = Math.random() * 2 - 1.0;
202 | v = Math.random() * 2 - 1.0;
203 | w = u * u + v * v;
204 | } while (w == 0.0 || w >= 1.0);
205 |
206 | c = Math.sqrt((-2 * Math.log(w)) / w);
207 |
208 | return mean + u * c * sigma;
209 | }
210 |
211 |
212 | /**
213 | * 随机拆分一个数
214 | *
215 | * @param total 总和
216 | * @param nums 个数
217 | * @param max 最大值
218 | * @returns {number[]}
219 | */
220 | const randomSplit = function (total, nums, max) {
221 | let rest = total;
222 | let result = Array.apply(null, {length: nums}).map((n, i) => nums - i).map(n => {
223 | const v = 1 + Math.floor(Math.random() * (max | rest / n * 2 - 1));
224 | rest -= v;
225 | return v;
226 | });
227 | result[nums - 1] += rest;
228 | return result;
229 | }
230 |
231 |
232 | /**
233 | * export default 服从 ES6 的规范,补充:default 其实是别名
234 | * module.exports 服从 CommonJS 规范 https://javascript.ruanyifeng.com/nodejs/module.html
235 | * 一般导出一个属性或者对象用 export default
236 | * 一般导出模块或者说文件使用 module.exports
237 | *
238 | * import from 服从ES6规范,在编译器生效
239 | * require 服从ES5 规范,在运行期生效
240 | * 目前 vue 编译都是依赖label 插件,最终都转化为ES5
241 | *
242 | * @return 将方法、变量暴露出去
243 | */
244 | export default {
245 | trimSpace,
246 | trimFilter,
247 | notInArrayKV,
248 | inArrayKV,
249 | reinsertElement,
250 | mergeArray,
251 | splitArray,
252 | isInArray,
253 | normalSort,
254 | normalDistribution,
255 | randomSplit
256 | }
--------------------------------------------------------------------------------
/static/js/utils/time.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 日期时间工具类
3 | *
4 | * @Description:
5 | * @Author: claer
6 | * @File: time.js
7 | * @Version: 1.0.0
8 | * @Time: 2019/9/15 20:11
9 | * @Project: tool-gin
10 | * @Package:
11 | * @Software: GoLand
12 | */
13 |
14 |
15 | /**
16 | * 对Date的扩展,将 Date 转化为指定格式的String
17 | * 月(M)、日(d)、12小时(h)、24小时(H)、分(m)、秒(s)、周(E)、季度(q)可以用 1-2 个占位符
18 | * 年(y)可以用 1-4 个占位符,毫秒(S)只能用 1 个占位符(是 1-3 位的数字)
19 | * eg:
20 | * (newDate()).pattern("yyyy-MM-dd hh:mm:ss.S")==> 2006-07-02 08:09:04.423
21 | * (new Date()).pattern("yyyy-MM-dd E HH:mm:ss") ==> 2009-03-10 二 20:09:04
22 | * (new Date()).pattern("yyyy-MM-dd EE hh:mm:ss") ==> 2009-03-10 周二 08:09:04
23 | * (new Date()).pattern("yyyy-MM-dd EEE hh:mm:ss") ==> 2009-03-10 星期二 08:09:04
24 | * (new Date()).pattern("yyyy-M-d h:m:s.S") ==> 2006-7-2 8:9:4.18
25 | *
26 | * @param fmt
27 | * @returns {void | string}
28 | */
29 | Date.prototype.pattern = function (fmt) {
30 | if (typeof fmt != "string") {
31 | throw TypeError("fmt不是字符串类型");
32 | }
33 | if (fmt == "" || fmt.length == 0) {
34 | throw new Error("fmt不能为空");
35 | }
36 | let opt = {
37 | // 年
38 | "y+": this.getFullYear(),
39 | // 月份
40 | "M+": this.getMonth() + 1,
41 | // 日
42 | "d+": this.getDate(),
43 | // 小时
44 | "h+": this.getHours() % 12 == 0 ? 12 : this.getHours() % 12,
45 | // 小时
46 | "H+": this.getHours(),
47 | // 分
48 | "m+": this.getMinutes(),
49 | // 秒
50 | "s+": this.getSeconds(),
51 | // 季度
52 | "q+": Math.floor((this.getMonth() + 3) / 3),
53 | // 毫秒
54 | "S": this.getMilliseconds()
55 | };
56 | let week = {
57 | "0": "/u65e5",
58 | "1": "/u4e00",
59 | "2": "/u4e8c",
60 | "3": "/u4e09",
61 | "4": "/u56db",
62 | "5": "/u4e94",
63 | "6": "/u516d"
64 | };
65 | if (/(E+)/.test(fmt)) {
66 | let wk = RegExp.$1.length > 2 ? "/u661f/u671f" : "/u5468";
67 | wk = RegExp.$1.length > 1 ? wk : "";
68 | wk = wk + week[this.getDay().toString()];
69 | fmt = fmt.replace(RegExp.$1, wk);
70 | }
71 | for (let k in opt) {
72 | if (new RegExp(`(${k})`).test(fmt)) {
73 | let type = opt[k].toString();
74 | let time = type.padStart(RegExp.$1.length, "0");
75 | fmt = fmt.replace(RegExp.$1, RegExp.$1.length == 1 ? type : time);
76 | }
77 | }
78 | return fmt;
79 | }
80 |
81 | /**
82 | * 对Date的扩展,将 Date 转化为指定格式的String
83 | * 月(M)、日(d)、小时(h)、分(m)、秒(s)、季度(q) 可以用 1-2 个占位符,
84 | * 年(y)可以用 1-4 个占位符,毫秒(S)只能用 1 个占位符(是 1-3 位的数字)
85 | * (new Date()).format("yyyy-MM-dd HH:mm:ss.S") ==> 2019-11-19 17:17:25.932
86 | * (new Date()).format("yyyy-M-d H:m:s.S") ==> 2019-11-19 17:17:13.643
87 | * (new Date()).pattern("yyyy-MM-dd hh:mm:ss") ==> 2019-11-19 05:16:59
88 | * (new Date()).pattern("yyyy-M-d h:m:s.S") ==> 2019-11-19 5:16:47.906
89 | *
90 | * @param fmt
91 | * @returns {void | string}
92 | */
93 | Date.prototype.format = function (fmt) {
94 | if (typeof fmt != "string") {
95 | throw TypeError("fmt不是字符串类型");
96 | }
97 | if (fmt == "" || fmt.length == 0) {
98 | throw new Error("fmt不能为空");
99 | }
100 | let opt = {
101 | // 年
102 | "y+": this.getFullYear(),
103 | // 月份
104 | "M+": this.getMonth() + 1,
105 | // 日
106 | "d+": this.getDate(),
107 | // 小时
108 | "H+": this.getHours(),
109 | "h+": this.getHours() % 12 == 0 ? 12 : this.getHours() % 12,
110 | // 分
111 | "m+": this.getMinutes(),
112 | // 秒
113 | "s+": this.getSeconds(),
114 | // 季度
115 | "q+": Math.floor((this.getMonth() + 3) / 3),
116 | // 毫秒
117 | "S": this.getMilliseconds()
118 | };
119 | for (let k in opt) {
120 | if (new RegExp(`(${k})`).test(fmt)) {
121 | let type = opt[k].toString();
122 | let time = type.padStart(RegExp.$1.length, "0");
123 | fmt = fmt.replace(RegExp.$1, RegExp.$1.length == 1 ? type : time);
124 | }
125 | }
126 | return fmt;
127 | }
128 |
129 |
130 | /**
131 | * Date格式化输出
132 | *
133 | * dateFormat(new Date(),"yyyy-MM-dd HH:mm:ss.S") ==> 2019-11-19 17:10:22.970
134 | * dateFormat(new Date(),"yyyy-MM-dd hh:mm:ss.S") ==> 2019-11-19 05:09:54.203
135 | * dateFormat(new Date(),"yyyy-M-d h:m:s.S") ==> 2019-11-19 5:19:5.44
136 | *
137 | * @param date
138 | * @param fmt
139 | * @returns {void | string}
140 | */
141 | const dateFormat = (date, fmt) => {
142 | if (!(date instanceof Date)) {
143 | throw TypeError("date不是Date类型");
144 | }
145 | if (typeof fmt != "string") {
146 | throw TypeError("fmt不是字符串类型");
147 | }
148 | if (fmt == "" || fmt.length == 0) {
149 | throw new Error("fmt不能为空");
150 | }
151 | let opt = {
152 | // 年
153 | "y+": date.getFullYear(),
154 | // 月
155 | "M+": date.getMonth() + 1,
156 | // 日
157 | "d+": date.getDate(),
158 | // 时
159 | "H+": date.getHours(),
160 | "h+": date.getHours() % 12 == 0 ? 12 : date.getHours() % 12,
161 | // 分
162 | "m+": date.getMinutes(),
163 | // 秒
164 | "s+": date.getSeconds(),
165 | // 季度
166 | "q+": Math.floor((date.getMonth() + 3) / 3),
167 | // 毫秒
168 | "S": date.getMilliseconds()
169 | };
170 | for (let k in opt) {
171 | if (new RegExp(`(${k})`).test(fmt)) {
172 | let type = opt[k].toString();
173 | let time = type.padStart(RegExp.$1.length, "0");
174 | fmt = fmt.replace(RegExp.$1, RegExp.$1.length == 1 ? type : time)
175 | }
176 | }
177 | return fmt;
178 | }
179 |
180 |
181 | /**
182 | * export default 服从 ES6 的规范,补充:default 其实是别名
183 | * module.exports 服从 CommonJS 规范 https://javascript.ruanyifeng.com/nodejs/module.html
184 | * 一般导出一个属性或者对象用 export default
185 | * 一般导出模块或者说文件使用 module.exports
186 | *
187 | * import from 服从ES6规范,在编译器生效
188 | * require 服从ES5 规范,在运行期生效
189 | * 目前 vue 编译都是依赖label 插件,最终都转化为ES5
190 | *
191 | * @return 将方法、变量暴露出去
192 | */
193 | export default {
194 | dateFormat
195 | }
--------------------------------------------------------------------------------
/utils/bytes.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "bytes"
5 | "encoding/binary"
6 | "errors"
7 | "fmt"
8 | "io"
9 | "strconv"
10 | "strings"
11 | "unicode"
12 | "unsafe"
13 | )
14 |
15 | // 包含辅助方法和常量,用于转换为人类可读的字节格式。
16 | //
17 | // bytefmt.ByteSize(100.5*bytefmt.MEGABYTE) // "100.5M"
18 | // bytefmt.ByteSize(uint64(1024)) // "1K"
19 | // https://github.com/cloudfoundry/bytefmt/blob/master/bytes.go
20 |
21 | const (
22 | BYTE = 1 << (10 * iota)
23 | KILOBYTE
24 | MEGABYTE
25 | GIGABYTE
26 | TERABYTE
27 | PETABYTE
28 | EXABYTE
29 | )
30 |
31 | var invalidByteQuantityError = errors.New("字节数量必须是一个正整数,其单位为测量单位 M, MB, MiB, G, GiB, or GB")
32 |
33 | // ByteSize 返回10M,12.5K等形式的人类可读字节串。以下单位可供选择:
34 | //
35 | // E: Exabyte
36 | // P: Petabyte
37 | // T: Terabyte
38 | // G: Gigabyte
39 | // M: Megabyte
40 | // K: Kilobyte
41 | // B: Byte
42 | //
43 | // 始终选择导致最小数量大于或等于1的单位。
44 | func ByteSize(bytes uint64) string {
45 | unit := ""
46 | value := float64(bytes)
47 |
48 | switch {
49 | case bytes >= EXABYTE:
50 | unit = "EB"
51 | value = value / EXABYTE
52 | case bytes >= PETABYTE:
53 | unit = "PB"
54 | value = value / PETABYTE
55 | case bytes >= TERABYTE:
56 | unit = "TB"
57 | value = value / TERABYTE
58 | case bytes >= GIGABYTE:
59 | unit = "GB"
60 | value = value / GIGABYTE
61 | case bytes >= MEGABYTE:
62 | unit = "MB"
63 | value = value / MEGABYTE
64 | case bytes >= KILOBYTE:
65 | unit = "KB"
66 | value = value / KILOBYTE
67 | case bytes >= BYTE:
68 | unit = "B"
69 | case bytes == 0:
70 | return "0"
71 | }
72 |
73 | result := strconv.FormatFloat(value, 'f', 1, 64)
74 | result = strings.TrimSuffix(result, ".0")
75 | return result + unit
76 | }
77 |
78 | // ToMegabytes 将ByteSize格式化的字符串解析为兆字节。
79 | func ToMegabytes(s string) (uint64, error) {
80 | bytes, err := ToBytes(s)
81 | if err != nil {
82 | return 0, err
83 | }
84 |
85 | return bytes / MEGABYTE, nil
86 | }
87 |
88 | // ToBytes 将ByteSize格式化的字符串解析为字节。注意二进制前缀和SI前缀单位均表示基数为2的单位
89 | // KB = K = KiB = 1024
90 | // MB = M = MiB = 1024 * K
91 | // GB = G = GiB = 1024 * M
92 | // TB = T = TiB = 1024 * G
93 | // PB = P = PiB = 1024 * T
94 | // EB = E = EiB = 1024 * P
95 | func ToBytes(s string) (uint64, error) {
96 | s = strings.TrimSpace(s)
97 | s = strings.ToUpper(s)
98 |
99 | i := strings.IndexFunc(s, unicode.IsLetter)
100 |
101 | if i == -1 {
102 | return 0, invalidByteQuantityError
103 | }
104 |
105 | bytesString, multiple := s[:i], s[i:]
106 | bytes, err := strconv.ParseFloat(bytesString, 64)
107 | if err != nil || bytes <= 0 {
108 | return 0, invalidByteQuantityError
109 | }
110 |
111 | switch multiple {
112 | case "E", "EB", "EIB":
113 | return uint64(bytes * EXABYTE), nil
114 | case "P", "PB", "PIB":
115 | return uint64(bytes * PETABYTE), nil
116 | case "T", "TB", "TIB":
117 | return uint64(bytes * TERABYTE), nil
118 | case "G", "GB", "GIB":
119 | return uint64(bytes * GIGABYTE), nil
120 | case "M", "MB", "MIB":
121 | return uint64(bytes * MEGABYTE), nil
122 | case "K", "KB", "KIB":
123 | return uint64(bytes * KILOBYTE), nil
124 | case "B":
125 | return uint64(bytes), nil
126 | default:
127 | return 0, invalidByteQuantityError
128 | }
129 | }
130 |
131 | // BytesToStringByBuffer io.ReadCloser类型转换为string
132 | func BytesToStringByBuffer(body io.Reader) string {
133 | buf := new(bytes.Buffer) // io.ReadCloser类型转换为string
134 | buf.ReadFrom(body)
135 | //b := *(*string)(unsafe.Pointer(&body))
136 | /*_, err := io.Copy(buf, body)
137 | b :=buf.String()*/
138 | return buf.String()
139 | }
140 |
141 | // BytesToStringByIo io.ReadCloser类型转换为string
142 | func BytesToStringByIo(body io.Reader) (string, error) {
143 | bd, err := io.ReadAll(body)
144 | if err != nil {
145 | return "", err
146 | }
147 | b := string(bd)
148 | //b :=fmt.Sprintf("%s", body)
149 | return b, err
150 | }
151 |
152 | // Int2Byte 把int的每个字节取出来放入byte数组中,存储采用Littledian
153 | func Int2Byte(data int) (ret []byte) {
154 | var len_ uintptr = unsafe.Sizeof(data)
155 | ret = make([]byte, len_)
156 | var tmp int = 0xff
157 | var index uint = 0
158 | for index = 0; index < uint(len_); index++ {
159 | ret[index] = byte((tmp << (index * 8) & data) >> (index * 8))
160 | }
161 | return ret
162 | }
163 |
164 | // Byte2Int 把byte Slice 中的每个字节取出来, 按Littledian端拼成一个int
165 | func Byte2Int(data []byte) int {
166 | var ret int = 0
167 | var _len int = len(data)
168 | var i uint = 0
169 | for i = 0; i < uint(_len); i++ {
170 | ret = ret | (int(data[i]) << (i * 8))
171 | }
172 | return ret
173 | }
174 |
175 | // BytesToIntU 字节数(大端)组转成int(无符号的)
176 | func BytesToIntU(b []byte) (int, error) {
177 | if len(b) == 3 {
178 | b = append([]byte{0}, b...)
179 | }
180 | bytesBuffer := bytes.NewBuffer(b)
181 | switch len(b) {
182 | case 1:
183 | var tmp uint8
184 | err := binary.Read(bytesBuffer, binary.BigEndian, &tmp)
185 | return int(tmp), err
186 | case 2:
187 | var tmp uint16
188 | err := binary.Read(bytesBuffer, binary.BigEndian, &tmp)
189 | return int(tmp), err
190 | case 4:
191 | var tmp uint32
192 | err := binary.Read(bytesBuffer, binary.BigEndian, &tmp)
193 | return int(tmp), err
194 | default:
195 | return 0, fmt.Errorf("%s", "BytesToInt bytes lenth is invaild!")
196 | }
197 | }
198 |
199 | // BytesToIntS 字节数(大端)组转成int(有符号)
200 | func BytesToIntS(b []byte) (int, error) {
201 | if len(b) == 3 {
202 | b = append([]byte{0}, b...)
203 | }
204 | bytesBuffer := bytes.NewBuffer(b)
205 | switch len(b) {
206 | case 1:
207 | var tmp int8
208 | err := binary.Read(bytesBuffer, binary.BigEndian, &tmp)
209 | return int(tmp), err
210 | case 2:
211 | var tmp int16
212 | err := binary.Read(bytesBuffer, binary.BigEndian, &tmp)
213 | return int(tmp), err
214 | case 4:
215 | var tmp int32
216 | err := binary.Read(bytesBuffer, binary.BigEndian, &tmp)
217 | return int(tmp), err
218 | default:
219 | return 0, fmt.Errorf("%s", "BytesToInt bytes lenth is invaild!")
220 | }
221 | }
222 |
223 | // IntToBytes 整形转换成字节
224 | func IntToBytes(n int, b byte) ([]byte, error) {
225 | switch b {
226 | case 1:
227 | tmp := int8(n)
228 | bytesBuffer := bytes.NewBuffer([]byte{})
229 | err := binary.Write(bytesBuffer, binary.BigEndian, &tmp)
230 | if err != nil {
231 | return nil, err
232 | }
233 | return bytesBuffer.Bytes(), nil
234 | case 2:
235 | tmp := int16(n)
236 | bytesBuffer := bytes.NewBuffer([]byte{})
237 | err := binary.Write(bytesBuffer, binary.BigEndian, &tmp)
238 | if err != nil {
239 | return nil, err
240 | }
241 | return bytesBuffer.Bytes(), nil
242 | case 3, 4:
243 | tmp := int32(n)
244 | bytesBuffer := bytes.NewBuffer([]byte{})
245 | err := binary.Write(bytesBuffer, binary.BigEndian, &tmp)
246 | if err != nil {
247 | return nil, err
248 | }
249 | return bytesBuffer.Bytes(), nil
250 | }
251 | return nil, fmt.Errorf("IntToBytes b param is invaild")
252 | }
253 |
--------------------------------------------------------------------------------
/app.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "embed"
5 | "flag"
6 | "html/template"
7 | "io/fs"
8 | "log"
9 | "net/http"
10 | "strings"
11 | "tool-gin/utils"
12 |
13 | "github.com/gin-contrib/static"
14 | "github.com/gin-gonic/gin"
15 | )
16 |
17 | // 常量
18 | const (
19 | // TokenSalt 可自定义盐值
20 | TokenSalt = "default_salt"
21 | )
22 |
23 | // 内嵌资源目录指令
24 | //
25 | //go:embed static pyutils/*[^.go]
26 | var local embed.FS
27 |
28 | // init函数用于初始化应用程序,将在程序启动时自动执行
29 | func init() {
30 | // 这里调用了CreateTmpFiles函数,目的是在程序运行前创建必要的临时文件目录
31 | // 参数"pyutils"指定了创建的临时文件目录的名称
32 | CreateTmpFiles("pyutils")
33 | }
34 |
35 | type embedFileSystem struct {
36 | http.FileSystem
37 | }
38 |
39 | // Exists 检查给定路径的文件或目录是否存在。
40 | // 该方法通过尝试打开文件来判断路径是否存在,如果能够成功打开,则认为路径存在。
41 | // 此方法适用于嵌入式文件系统,允许程序检查资源是否可用。
42 | // 参数:
43 | //
44 | // prefix: 资源的前缀,用于区分不同的文件系统或资源集。
45 | // path: 要检查的文件或目录的路径。
46 | //
47 | // 返回值:
48 | //
49 | // bool: 如果文件或目录存在,则返回true;否则返回false。
50 | //
51 | // 注意:
52 | // - 此方法依赖于e.Open方法来实际检查路径。
53 | // - 不存在的路径将返回false,而实际上可能是因为其他原因(如权限问题)导致的打开失败。
54 | func (e embedFileSystem) Exists(prefix, path string) bool {
55 | _, err := e.Open(path)
56 | if err != nil {
57 | return false
58 | }
59 | return true
60 | }
61 |
62 | // EmbedFolder embed.FS转换为http.FileSystem https://github.com/gin-contrib/static/issues/19
63 | func EmbedFolder(fsEmbed embed.FS, targetPath string) static.ServeFileSystem {
64 | //http.FS(os.DirFS(targetPath))
65 | fsys, err := fs.Sub(fsEmbed, targetPath) // 获取目录下的文件
66 | if err != nil {
67 | panic(err)
68 | }
69 | return embedFileSystem{
70 | FileSystem: http.FS(fsys),
71 | }
72 | }
73 |
74 | // EmbedDir 返回一个 ServeFileSystem 接口,用于提供目录服务。
75 | // 该函数通过调用 EmbedFolder 函数,将指定路径的目录嵌入到可执行文件中。
76 | // 参数:
77 | //
78 | // targetPath - 目标目录的路径,表示要嵌入的目录。
79 | //
80 | // 返回值:
81 | //
82 | // static.ServeFileSystem - 一个接口类型,提供了访问嵌入目录中文件和子目录的功能。
83 | func EmbedDir(targetPath string) static.ServeFileSystem {
84 | return EmbedFolder(local, targetPath)
85 | }
86 |
87 | // Authorize 认证拦截中间件
88 | func Authorize(c *gin.Context) {
89 | username := c.Query("username") // 用户名
90 | ts := c.Query("ts") // 时间戳
91 | token := c.Query("token") // 访问令牌
92 |
93 | if strings.ToLower(utils.MD5(username+ts+TokenSalt)) == strings.ToLower(token) {
94 | // 验证通过,会继续访问下一个中间件
95 | c.Next()
96 | } else {
97 | // 验证不通过,不再调用后续的函数处理
98 | c.Abort()
99 | c.JSON(http.StatusUnauthorized, gin.H{"message": "访问未授权"})
100 | // return可省略, 只要前面执行Abort()就可以让后面的handler函数不再执行
101 | return
102 | }
103 | }
104 |
105 | // FilterNoCache 禁止浏览器页面缓存
106 | func FilterNoCache(c *gin.Context) {
107 | c.Header("Cache-Control", "no-cache, no-store, must-revalidate")
108 | c.Header("Pragma", "no-cache")
109 | c.Header("Expires", "0")
110 | // 继续访问下一个中间件
111 | c.Next()
112 | }
113 |
114 | // Cors 处理跨域请求,支持options访问
115 | func Cors(c *gin.Context) {
116 |
117 | // 它指定允许进入来源的域名、ip+端口号 。 如果值是 ‘*’ ,表示接受任意的域名请求,这个方式不推荐,
118 | // 主要是因为其不安全,而且因为如果浏览器的请求携带了cookie信息,会发生错误
119 | c.Header("Access-Control-Allow-Origin", "*")
120 | // 设置服务器允许浏览器发送请求都携带cookie
121 | c.Header("Access-Control-Allow-Credentials", "true")
122 | // 允许的访问方法
123 | c.Header("Access-Control-Allow-Methods", "POST, GET, PUT, OPTIONS, DELETE, PATCH")
124 | // Access-Control-Max-Age 用于 CORS 相关配置的缓存
125 | c.Header("Access-Control-Max-Age", "3600")
126 | // 设置允许的请求头信息 DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization
127 | c.Header("Access-Control-Allow-Headers", "Token,Origin, X-Requested-With, Content-Type, Accept,mid,X-Token,AccessToken,X-CSRF-Token, Authorization")
128 |
129 | c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type")
130 |
131 | method := c.Request.Method
132 | // 放行所有OPTIONS方法
133 | if method == "OPTIONS" {
134 | c.AbortWithStatus(http.StatusNoContent)
135 | }
136 | // 继续访问下一个中间件
137 | c.Next()
138 | }
139 |
140 | // Port 获取传入参数的端口,如果没传默认值为8000
141 | func Port() (port string) {
142 | flag.StringVar(&port, "p", "8000", "默认端口:8000")
143 | flag.Parse()
144 | return ":" + port
145 |
146 | //if len(os.Args[1:]) == 0 {
147 | // return ":8000"
148 | //}
149 | //return ":" + os.Args[1]
150 | }
151 |
152 | // 配置gin(路由、中间件)并监听运行
153 | func run() {
154 |
155 | router := NewEngine()
156 |
157 | // 将全局中间件附加到路由器,使用中间件处理通用、横切性的关注点
158 | // Gin 自带的 panic 恢复中间件
159 | router.Use(gin.Recovery())
160 | router.Use(FilterNoCache)
161 | //router.Use(Cors())
162 | //router.Use(Authorize())
163 | // 设置可信代理的 IP 地址或 CIDR 范围
164 | //router.TrustedPlatform = "CF-Connecting-IP" // 信任特定平台,来自请求头部信息
165 | //router.ForwardedByClientIP = true // 启用基于客户端 IP 的转发功能
166 | //err := router.SetTrustedProxies([]string{"127.0.0.1"})
167 | err := router.SetTrustedProxies(nil) // 禁用代理信任,直接使用 Request.RemoteAddr 作为客户端 IP,忽略所有代理头部
168 | if err != nil {
169 | log.Println(err)
170 | }
171 |
172 | // 在go:embed下必须指定模板路径
173 | t, _ := template.ParseFS(local, "static/html/*.html")
174 | router.SetHTMLTemplate(t)
175 | // 注册接口
176 | router.Any("/", WebRoot)
177 | router.POST("/getKey", GetKey)
178 | router.POST("/SystemInfo", SystemInfo)
179 | router.POST("/getXshellUrl", GetNetSarangDownloadUrl)
180 | router.Any("/nginx-format", NginxFormatIndex)
181 | router.POST("/nginx-format-py", NginxFormatPython)
182 | router.Any("/navicat", GetNavicatDownloadUrl)
183 | router.Any("/svp", GetSvp)
184 |
185 | /*
186 | //router.POST("/upload", UnifiedUpload)
187 | //router.POST("/download", UnifiedDownload)
188 |
189 | // 为 multipart forms 设置文件大小限制, 默认是32MB
190 | // 此处为左移位运算符, << 20 表示1MiB,8 << 20就是8MiB
191 | router.MaxMultipartMemory = 8 << 20
192 | router.POST("/upload", func(c *gin.Context) {
193 | // 单文件
194 | file, _ := c.FormFile("file")
195 | log.Println(file.Filename)
196 |
197 | // 上传文件至指定的完整文件路径
198 | dst := "/home/test" + file.Filename
199 | err := c.SaveUploadedFile(file, dst)
200 | if err != nil {
201 | log.Println(err)
202 | }
203 | c.String(http.StatusOK, fmt.Sprintf("'%s' uploaded!", file.Filename))
204 | })
205 | */
206 |
207 | // 注册一个目录,gin 会把该目录当成一个静态的资源目录
208 | // 如 static 目录下有图片路径 index/logo.png , 你可以通过 GET /static/index/logo.png 访问到
209 | //router.Static("/static", "./static")
210 |
211 | router.StaticFS("/static", EmbedFolder(local, "static"))
212 |
213 | //router.Use(static.Serve("/", EmbedFolder(local, "static")))
214 | /*router.NoRoute(func (c *gin.Context) {
215 | log.Printf("%s doesn't exists, redirect on /", c.Request.URL.Path)
216 | c.Redirect(http.StatusMovedPermanently, "/")
217 | })*/
218 |
219 | //router.LoadHTMLFiles("./static/html/index.html")
220 | // 注册一个路径,gin 加载模板的时候会从该目录查找
221 | // 参数是一个匹配字符,如 templates/*/* 的意思是 模板目录有两层
222 | // gin 在启动时会自动把该目录的文件编译一次缓存,不用担心效率问题
223 | //router.LoadHTMLGlob("static/html/*") // 在go:embed下无效
224 |
225 | // listen and serve on 0.0.0.0:8080
226 | err = router.Run(Port())
227 | if err != nil {
228 | log.Fatal(err)
229 | }
230 |
231 | /*listener, err := net.Listen("tcp", "0.0.0.0"+Port())
232 | if err != nil {
233 | panic(listener)
234 | }
235 | httpServer := &http.Server{
236 | Handler: router,
237 | }
238 | err = httpServer.Serve(listener)
239 | if err != nil {
240 | panic(err)
241 | }*/
242 | }
243 |
--------------------------------------------------------------------------------
/static/css/grids-responsive-min.css:
--------------------------------------------------------------------------------
1 | /*!
2 | Pure v2.0.5
3 | Copyright 2013 Yahoo!
4 | Licensed under the BSD License.
5 | https://github.com/pure-css/pure/blob/master/LICENSE
6 | */
7 | @media screen and (min-width:35.5em){.pure-u-sm-1,.pure-u-sm-1-1,.pure-u-sm-1-12,.pure-u-sm-1-2,.pure-u-sm-1-24,.pure-u-sm-1-3,.pure-u-sm-1-4,.pure-u-sm-1-5,.pure-u-sm-1-6,.pure-u-sm-1-8,.pure-u-sm-10-24,.pure-u-sm-11-12,.pure-u-sm-11-24,.pure-u-sm-12-24,.pure-u-sm-13-24,.pure-u-sm-14-24,.pure-u-sm-15-24,.pure-u-sm-16-24,.pure-u-sm-17-24,.pure-u-sm-18-24,.pure-u-sm-19-24,.pure-u-sm-2-24,.pure-u-sm-2-3,.pure-u-sm-2-5,.pure-u-sm-20-24,.pure-u-sm-21-24,.pure-u-sm-22-24,.pure-u-sm-23-24,.pure-u-sm-24-24,.pure-u-sm-3-24,.pure-u-sm-3-4,.pure-u-sm-3-5,.pure-u-sm-3-8,.pure-u-sm-4-24,.pure-u-sm-4-5,.pure-u-sm-5-12,.pure-u-sm-5-24,.pure-u-sm-5-5,.pure-u-sm-5-6,.pure-u-sm-5-8,.pure-u-sm-6-24,.pure-u-sm-7-12,.pure-u-sm-7-24,.pure-u-sm-7-8,.pure-u-sm-8-24,.pure-u-sm-9-24{display:inline-block;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-sm-1-24{width:4.1667%}.pure-u-sm-1-12,.pure-u-sm-2-24{width:8.3333%}.pure-u-sm-1-8,.pure-u-sm-3-24{width:12.5%}.pure-u-sm-1-6,.pure-u-sm-4-24{width:16.6667%}.pure-u-sm-1-5{width:20%}.pure-u-sm-5-24{width:20.8333%}.pure-u-sm-1-4,.pure-u-sm-6-24{width:25%}.pure-u-sm-7-24{width:29.1667%}.pure-u-sm-1-3,.pure-u-sm-8-24{width:33.3333%}.pure-u-sm-3-8,.pure-u-sm-9-24{width:37.5%}.pure-u-sm-2-5{width:40%}.pure-u-sm-10-24,.pure-u-sm-5-12{width:41.6667%}.pure-u-sm-11-24{width:45.8333%}.pure-u-sm-1-2,.pure-u-sm-12-24{width:50%}.pure-u-sm-13-24{width:54.1667%}.pure-u-sm-14-24,.pure-u-sm-7-12{width:58.3333%}.pure-u-sm-3-5{width:60%}.pure-u-sm-15-24,.pure-u-sm-5-8{width:62.5%}.pure-u-sm-16-24,.pure-u-sm-2-3{width:66.6667%}.pure-u-sm-17-24{width:70.8333%}.pure-u-sm-18-24,.pure-u-sm-3-4{width:75%}.pure-u-sm-19-24{width:79.1667%}.pure-u-sm-4-5{width:80%}.pure-u-sm-20-24,.pure-u-sm-5-6{width:83.3333%}.pure-u-sm-21-24,.pure-u-sm-7-8{width:87.5%}.pure-u-sm-11-12,.pure-u-sm-22-24{width:91.6667%}.pure-u-sm-23-24{width:95.8333%}.pure-u-sm-1,.pure-u-sm-1-1,.pure-u-sm-24-24,.pure-u-sm-5-5{width:100%}}@media screen and (min-width:48em){.pure-u-md-1,.pure-u-md-1-1,.pure-u-md-1-12,.pure-u-md-1-2,.pure-u-md-1-24,.pure-u-md-1-3,.pure-u-md-1-4,.pure-u-md-1-5,.pure-u-md-1-6,.pure-u-md-1-8,.pure-u-md-10-24,.pure-u-md-11-12,.pure-u-md-11-24,.pure-u-md-12-24,.pure-u-md-13-24,.pure-u-md-14-24,.pure-u-md-15-24,.pure-u-md-16-24,.pure-u-md-17-24,.pure-u-md-18-24,.pure-u-md-19-24,.pure-u-md-2-24,.pure-u-md-2-3,.pure-u-md-2-5,.pure-u-md-20-24,.pure-u-md-21-24,.pure-u-md-22-24,.pure-u-md-23-24,.pure-u-md-24-24,.pure-u-md-3-24,.pure-u-md-3-4,.pure-u-md-3-5,.pure-u-md-3-8,.pure-u-md-4-24,.pure-u-md-4-5,.pure-u-md-5-12,.pure-u-md-5-24,.pure-u-md-5-5,.pure-u-md-5-6,.pure-u-md-5-8,.pure-u-md-6-24,.pure-u-md-7-12,.pure-u-md-7-24,.pure-u-md-7-8,.pure-u-md-8-24,.pure-u-md-9-24{display:inline-block;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-md-1-24{width:4.1667%}.pure-u-md-1-12,.pure-u-md-2-24{width:8.3333%}.pure-u-md-1-8,.pure-u-md-3-24{width:12.5%}.pure-u-md-1-6,.pure-u-md-4-24{width:16.6667%}.pure-u-md-1-5{width:20%}.pure-u-md-5-24{width:20.8333%}.pure-u-md-1-4,.pure-u-md-6-24{width:25%}.pure-u-md-7-24{width:29.1667%}.pure-u-md-1-3,.pure-u-md-8-24{width:33.3333%}.pure-u-md-3-8,.pure-u-md-9-24{width:37.5%}.pure-u-md-2-5{width:40%}.pure-u-md-10-24,.pure-u-md-5-12{width:41.6667%}.pure-u-md-11-24{width:45.8333%}.pure-u-md-1-2,.pure-u-md-12-24{width:50%}.pure-u-md-13-24{width:54.1667%}.pure-u-md-14-24,.pure-u-md-7-12{width:58.3333%}.pure-u-md-3-5{width:60%}.pure-u-md-15-24,.pure-u-md-5-8{width:62.5%}.pure-u-md-16-24,.pure-u-md-2-3{width:66.6667%}.pure-u-md-17-24{width:70.8333%}.pure-u-md-18-24,.pure-u-md-3-4{width:75%}.pure-u-md-19-24{width:79.1667%}.pure-u-md-4-5{width:80%}.pure-u-md-20-24,.pure-u-md-5-6{width:83.3333%}.pure-u-md-21-24,.pure-u-md-7-8{width:87.5%}.pure-u-md-11-12,.pure-u-md-22-24{width:91.6667%}.pure-u-md-23-24{width:95.8333%}.pure-u-md-1,.pure-u-md-1-1,.pure-u-md-24-24,.pure-u-md-5-5{width:100%}}@media screen and (min-width:64em){.pure-u-lg-1,.pure-u-lg-1-1,.pure-u-lg-1-12,.pure-u-lg-1-2,.pure-u-lg-1-24,.pure-u-lg-1-3,.pure-u-lg-1-4,.pure-u-lg-1-5,.pure-u-lg-1-6,.pure-u-lg-1-8,.pure-u-lg-10-24,.pure-u-lg-11-12,.pure-u-lg-11-24,.pure-u-lg-12-24,.pure-u-lg-13-24,.pure-u-lg-14-24,.pure-u-lg-15-24,.pure-u-lg-16-24,.pure-u-lg-17-24,.pure-u-lg-18-24,.pure-u-lg-19-24,.pure-u-lg-2-24,.pure-u-lg-2-3,.pure-u-lg-2-5,.pure-u-lg-20-24,.pure-u-lg-21-24,.pure-u-lg-22-24,.pure-u-lg-23-24,.pure-u-lg-24-24,.pure-u-lg-3-24,.pure-u-lg-3-4,.pure-u-lg-3-5,.pure-u-lg-3-8,.pure-u-lg-4-24,.pure-u-lg-4-5,.pure-u-lg-5-12,.pure-u-lg-5-24,.pure-u-lg-5-5,.pure-u-lg-5-6,.pure-u-lg-5-8,.pure-u-lg-6-24,.pure-u-lg-7-12,.pure-u-lg-7-24,.pure-u-lg-7-8,.pure-u-lg-8-24,.pure-u-lg-9-24{display:inline-block;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-lg-1-24{width:4.1667%}.pure-u-lg-1-12,.pure-u-lg-2-24{width:8.3333%}.pure-u-lg-1-8,.pure-u-lg-3-24{width:12.5%}.pure-u-lg-1-6,.pure-u-lg-4-24{width:16.6667%}.pure-u-lg-1-5{width:20%}.pure-u-lg-5-24{width:20.8333%}.pure-u-lg-1-4,.pure-u-lg-6-24{width:25%}.pure-u-lg-7-24{width:29.1667%}.pure-u-lg-1-3,.pure-u-lg-8-24{width:33.3333%}.pure-u-lg-3-8,.pure-u-lg-9-24{width:37.5%}.pure-u-lg-2-5{width:40%}.pure-u-lg-10-24,.pure-u-lg-5-12{width:41.6667%}.pure-u-lg-11-24{width:45.8333%}.pure-u-lg-1-2,.pure-u-lg-12-24{width:50%}.pure-u-lg-13-24{width:54.1667%}.pure-u-lg-14-24,.pure-u-lg-7-12{width:58.3333%}.pure-u-lg-3-5{width:60%}.pure-u-lg-15-24,.pure-u-lg-5-8{width:62.5%}.pure-u-lg-16-24,.pure-u-lg-2-3{width:66.6667%}.pure-u-lg-17-24{width:70.8333%}.pure-u-lg-18-24,.pure-u-lg-3-4{width:75%}.pure-u-lg-19-24{width:79.1667%}.pure-u-lg-4-5{width:80%}.pure-u-lg-20-24,.pure-u-lg-5-6{width:83.3333%}.pure-u-lg-21-24,.pure-u-lg-7-8{width:87.5%}.pure-u-lg-11-12,.pure-u-lg-22-24{width:91.6667%}.pure-u-lg-23-24{width:95.8333%}.pure-u-lg-1,.pure-u-lg-1-1,.pure-u-lg-24-24,.pure-u-lg-5-5{width:100%}}@media screen and (min-width:80em){.pure-u-xl-1,.pure-u-xl-1-1,.pure-u-xl-1-12,.pure-u-xl-1-2,.pure-u-xl-1-24,.pure-u-xl-1-3,.pure-u-xl-1-4,.pure-u-xl-1-5,.pure-u-xl-1-6,.pure-u-xl-1-8,.pure-u-xl-10-24,.pure-u-xl-11-12,.pure-u-xl-11-24,.pure-u-xl-12-24,.pure-u-xl-13-24,.pure-u-xl-14-24,.pure-u-xl-15-24,.pure-u-xl-16-24,.pure-u-xl-17-24,.pure-u-xl-18-24,.pure-u-xl-19-24,.pure-u-xl-2-24,.pure-u-xl-2-3,.pure-u-xl-2-5,.pure-u-xl-20-24,.pure-u-xl-21-24,.pure-u-xl-22-24,.pure-u-xl-23-24,.pure-u-xl-24-24,.pure-u-xl-3-24,.pure-u-xl-3-4,.pure-u-xl-3-5,.pure-u-xl-3-8,.pure-u-xl-4-24,.pure-u-xl-4-5,.pure-u-xl-5-12,.pure-u-xl-5-24,.pure-u-xl-5-5,.pure-u-xl-5-6,.pure-u-xl-5-8,.pure-u-xl-6-24,.pure-u-xl-7-12,.pure-u-xl-7-24,.pure-u-xl-7-8,.pure-u-xl-8-24,.pure-u-xl-9-24{display:inline-block;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-xl-1-24{width:4.1667%}.pure-u-xl-1-12,.pure-u-xl-2-24{width:8.3333%}.pure-u-xl-1-8,.pure-u-xl-3-24{width:12.5%}.pure-u-xl-1-6,.pure-u-xl-4-24{width:16.6667%}.pure-u-xl-1-5{width:20%}.pure-u-xl-5-24{width:20.8333%}.pure-u-xl-1-4,.pure-u-xl-6-24{width:25%}.pure-u-xl-7-24{width:29.1667%}.pure-u-xl-1-3,.pure-u-xl-8-24{width:33.3333%}.pure-u-xl-3-8,.pure-u-xl-9-24{width:37.5%}.pure-u-xl-2-5{width:40%}.pure-u-xl-10-24,.pure-u-xl-5-12{width:41.6667%}.pure-u-xl-11-24{width:45.8333%}.pure-u-xl-1-2,.pure-u-xl-12-24{width:50%}.pure-u-xl-13-24{width:54.1667%}.pure-u-xl-14-24,.pure-u-xl-7-12{width:58.3333%}.pure-u-xl-3-5{width:60%}.pure-u-xl-15-24,.pure-u-xl-5-8{width:62.5%}.pure-u-xl-16-24,.pure-u-xl-2-3{width:66.6667%}.pure-u-xl-17-24{width:70.8333%}.pure-u-xl-18-24,.pure-u-xl-3-4{width:75%}.pure-u-xl-19-24{width:79.1667%}.pure-u-xl-4-5{width:80%}.pure-u-xl-20-24,.pure-u-xl-5-6{width:83.3333%}.pure-u-xl-21-24,.pure-u-xl-7-8{width:87.5%}.pure-u-xl-11-12,.pure-u-xl-22-24{width:91.6667%}.pure-u-xl-23-24{width:95.8333%}.pure-u-xl-1,.pure-u-xl-1-1,.pure-u-xl-24-24,.pure-u-xl-5-5{width:100%}}
--------------------------------------------------------------------------------
/static/html/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
10 | Shell激活
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
76 |
77 |
78 |
79 |
80 |
84 |
85 |
86 | nginx-format
87 | navicat
88 | HTML to PDF
89 |
90 |
91 |
92 | 官方网址:
93 |
94 |
95 | https://www.netsarang.com/zh/all-downloads
96 |
97 |
98 |
99 | https://www.xshell.com/zh/free-for-home-school
100 |
101 |
102 |
注意:
103 |
安装新版本之前请把老版本先卸载!!!
104 |
105 |
106 |
屏蔽版本升级方式一:
107 |
108 |
109 |
110 | 127.0.0.1 transact.netsarang.com
111 | 127.0.0.1 update.netsarang.com
112 | 127.0.0.1 www.netsarang.com
113 | 127.0.0.1 www.netsarang.co.kr
114 | 127.0.0.1 sales.netsarang.com
115 |
116 |
117 |
屏蔽版本升级方式二:
118 |
把以下几行保存到文件reg.ini,然后在cmd中执行REGINI reg.ini
119 |
120 |
121 | HKEY_CURRENT_USER\Software\NetSarang\Xshell\6\LiveUpdate [19]
122 | HKEY_CURRENT_USER\Software\NetSarang\Xftp\6\LiveUpdate [19]
123 | HKEY_CURRENT_USER\Software\NetSarang\Xmanager\6\LiveUpdate [19]
124 | HKEY_CURRENT_USER\Software\NetSarang\Xlpd\6\LiveUpdate [19]
125 |
126 |
127 |
159 |
160 |
169 |
170 |
171 |
172 |
173 |
174 |
184 |
185 |
186 |
187 |
--------------------------------------------------------------------------------
/reptile/mail_test.go:
--------------------------------------------------------------------------------
1 | package reptile
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "errors"
7 | "fmt"
8 | "log"
9 | "math/big"
10 | "net/http"
11 | "net/mail"
12 | "strings"
13 | "testing"
14 | "time"
15 | mailtmM "tool-gin/mailtm"
16 | "tool-gin/utils"
17 |
18 | "github.com/chromedp/chromedp"
19 | mailtmF "github.com/felixstrobel/mailtm"
20 | )
21 |
22 | // https://pkg.go.dev/net/mail#ReadMessage
23 | func TestMail(t *testing.T) {
24 | msg := "Received: from a27-154.smtp-out.us-west-2.amazonses.com ([54.240.27.154]) by temporary-mail.net\n for ; Wed, 21 Apr 2021 13:29:21 +0800 (CST)\nDKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/simple;\n\ts=n6yk34xlzntpmtevqgs5ghp2jksprvft; d=netsarang.com; t=1618982959;\n\th=Date:To:From:Reply-To:Subject:Message-ID:MIME-Version:Content-Type:Content-Transfer-Encoding;\n\tbh=OsRfLaUS97/+yRoJ/BSpIARvBe+S33pKrzp1it7xCyQ=;\n\tb=LTblU9qEhTeorpht/julhD6ar7a6MDmEI9zH3TBy28KI6ah7Q+E1J0fAML2Pbcd0\n\tqX/68C8+vtGD03BGEzVPjJTJDoztO0qoNz5l7C/DSGV1MEyxh8ccQtW7rw+7+kQXv/+\n\tBKztqxpwXMjNuzZb3E0W1GYju0kr4qT5nXkMXD5o=\nDKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/simple;\n\ts=7v7vs6w47njt4pimodk5mmttbegzsi6n; d=amazonses.com; t=1618982959;\n\th=Date:To:From:Reply-To:Subject:Message-ID:MIME-Version:Content-Type:Content-Transfer-Encoding:Feedback-ID;\n\tbh=OsRfLaUS97/+yRoJ/BSpIARvBe+S33pKrzp1it7xCyQ=;\n\tb=NhCoNeJCoIpySzLuSxUV5P0zlsh4pLKUz5bhIG3spGhLgW0Pzf/1ZHyJJOTL9T3C\n\tDi7ChUyfnWdRjWSp+7EiA4VNrqGtOzoOsMZKipURghknjlG8bYjpCpdGXrO1D5IBlIj\n\tCl8sZevNShTh++kQW27S4S83cuDhbwxGEgxhSy3k=\nDate: Wed, 21 Apr 2021 05:29:19 +0000\nTo: yikxcsuka@meantinc.com\nFrom: \"NetSarang, Inc.\" \nReply-To: no-reply@netsarang.com\nSubject: Xshell 7 download instruction\nMessage-ID: <01010178f2e77a87-c9bb10f6-ef08-4dad-ba57-4524546de5d2-000000@us-west-2.amazonses.com>\nX-Mailer: PHPMailer 5.2.10 (https://github.com/PHPMailer/PHPMailer/)\nMIME-Version: 1.0\nContent-Type: text/html; charset=utf-8\nContent-Transfer-Encoding: base64\nFeedback-ID: 1.us-west-2.l7ekw14vD6Jumpwas0GHbg0O54ld7FbCklw8tqJLu88=:AmazonSES\nX-SES-Outgoing: 2021.04.21-54.240.27.154\n\nPHNwYW4+RGVhciB1c2VyLDwvc3Bhbj4NCjxiciAvPjxiciAvPg0KPHNwYW4+VGhhbmsgeW91IGZv\nciB5b3VyIGludGVyZXN0IGluIFhzaGVsbCA3LiBXZSBoYXZlIHByZXBhcmVkIHlvdXIgZXZhbHVh\ndGlvbiBwYWNrYWdlLiBJZiB5b3UgZGlkIG5vdCByZXF1ZXN0IGFuIGV2YWx1YXRpb24gb2YgWHNo\nZWxsIDcsIHBsZWFzZSBjb250YWN0IG91ciBzdXBwb3J0IHRlYW0gYXQgc3VwcG9ydEBuZXRzYXJh\nbmcuY29tIHRvIGhhdmUgeW91ciBlbWFpbCBhZGRyZXNzIHJlbW92ZWQgZnJvbSBhbnkgZnV0dXJl\nIGVtYWlscyByZWxhdGVkIHRvIFhzaGVsbCA3Ljwvc3Bhbj4NCjxiciAvPjxiciAvPg0KPHNwYW4+\nUGxlYXNlIGdvIHRvIHRoZSBmb2xsb3dpbmcgVVJMIHRvIHN0YXJ0IGRvd25sb2FkaW5nIHlvdXIg\nZXZhbHVhdGlvbiBzb2Z0d2FyZTo8L3NwYW4+DQo8YnIgLz48YnIgLz4NCjxzcGFuPjxhIGhyZWY9\nImh0dHBzOi8vd3d3Lm5ldHNhcmFuZy5jb20vZW4vZG93bmxvYWRpbmcvP3Rva2VuPVZscDJUM1pN\nTVROblR6RmhXWGxFYnpWck1uSjNRVUE0U1VWWGMxcFlMVE5aY0hoRFQxcDJkV1JEU1RCQiIgdGFy\nZ2V0PSJfYmxhbmsiPmh0dHBzOi8vd3d3Lm5ldHNhcmFuZy5jb20vZW4vZG93bmxvYWRpbmcvP3Rv\na2VuPVZscDJUM1pNTVROblR6RmhXWGxFYnpWck1uSjNRVUE0U1VWWGMxcFlMVE5aY0hoRFQxcDJk\nV1JEU1RCQjwvYT48L3NwYW4+DQo8YnIgLz48YnIgLz4NCjxzcGFuPlRoaXMgbGluayB3aWxsIGV4\ncGlyZSBvbiBNYXkgMjEsIDIwMjE8L3NwYW4+IDxzcGFuPllvdSBjYW4gZXZhbHVhdGUgdGhlIHNv\nZnR3YXJlIGZvciAzMCBkYXlzIHNpbmNlIGluc3RhbGxhdGlvbi48L3NwYW4+DQo8YnIgLz48YnIg\nLz48YnIgLz4NCjxiPkRvIHlvdSBoYXZlIGFueSBxdWVzdGlvbnM/PC9iPg0KPGJyIC8+DQo8c3Bh\nbj5XZSBvZmZlciBmcmVlIHRlY2huaWNhbCBzdXBwb3J0IGR1cmluZyB0aGUgZXZhbHVhdGlvbiBw\nZXJpb2QuIElmIHlvdSBoYXZlIGFueSBxdWVzdGlvbnMsIHBsZWFzZSBzZW5kIHVzIGFuIGVtYWls\nIGF0IDxhIGhyZWY9Im1haWx0bzpzdXBwb3J0QG5ldHNhcmFuZy5jb20iPnN1cHBvcnRAbmV0c2Fy\nYW5nLmNvbTwvYT4uPC9zcGFuPg0KPGJyIC8+PGJyIC8+PGJyIC8+DQo8c3Bhbj5CZXN0IHJlZ2Fy\nZHMsPC9zcGFuPg0KPGJyIC8+PGJyIC8+PGJyIC8+DQo8dGFibGUgYm9yZGVyPSIwIiBjZWxscGFk\nZGluZz0iMCIgY2VsbHNwYWNpbmc9IjAiPg0KPHRib2R5Pg0KPHRyPjx0ZD49PT09PT09PT09PT09\nPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PC90\nZD48L3RyPg0KPHRyPjx0ZD5OZXRTYXJhbmcsIEluYy48L3RkPjwvdHI+DQo8dHI+PHRkPjQ3MDEg\nUGF0cmljayBIZW5yeSBEci4gQkxERyAyMiwgU3VpdGUgMTM3LCBTYW50YSBDbGFyYSwgQ0EgOTUw\nNTQsIFUuUy5BLjwvdGQ+PC90cj4NCjx0cj48dGQ+V2Vic2l0ZTogaHR0cDovL3d3dy5uZXRzYXJh\nbmcuY29tIHwgRW1haWw6IHN1cHBvcnRAbmV0c2FyYW5nLmNvbTwvdGQ+PC90cj4NCjx0cj48dGQ+\nUGhvbmU6ICg2NjkpIDIwNC0zMzAxPC90ZD48L3RyPg0KPC90Ym9keT4NCjwvdGFibGU+DQo=\n\n"
25 | r := strings.NewReader(msg)
26 | m, err := mail.ReadMessage(r)
27 | if err != nil {
28 | log.Fatal(err)
29 | }
30 | header := m.Header
31 | fmt.Println("Date:", header.Get("Date"))
32 | fmt.Println("From:", header.Get("From"))
33 | fmt.Println("To:", header.Get("To"))
34 | fmt.Println("Subject:", header.Get("Subject"))
35 | fmt.Println("Content-Transfer-Encoding:", header.Get("Content-Transfer-Encoding"))
36 |
37 | buf := new(bytes.Buffer) // io.ReadCloser类型转换为string
38 | buf.ReadFrom(m.Body)
39 | b := buf.String()
40 | fmt.Println("-------", b)
41 | }
42 |
43 | func TestLinShiYouXiangSuffix(t *testing.T) {
44 | LinShiYouXiangSuffix()
45 | }
46 |
47 | func TestLinShiYouXiangList(t *testing.T) {
48 | list, _ := LinShiYouXiangList("5wij52emu")
49 | t.Log(list)
50 | }
51 |
52 | func TestGetMail24(t *testing.T) {
53 | //GetMail24()
54 | var test string
55 | //ctx, cancel := Apply(true)
56 | //defer cancel()
57 | ctx, _ := Apply(true)
58 | err := chromedp.Run(ctx, GetMail24MailName(&test))
59 | t.Log(err)
60 | t.Log(test)
61 | err = chromedp.Run(ctx, GetMail24LatestMail(&test))
62 | t.Log(err)
63 | fmt.Println(test)
64 | }
65 |
66 | func TestGetSecmail(t *testing.T) {
67 | // 获取邮箱
68 | /*res, err := utils.HttpReadBodyString(http.MethodGet, secmail1+"?action=genRandomMailbox&count=1", "",
69 | nil, nil)
70 | var data []interface{}
71 | err = json.Unmarshal([]byte(res), &data)
72 | fmt.Println(res, err)
73 | r := strings.Split(data[0].(string), "@") // 获取用户名和域名*/
74 | //url := secmail1 + "?action=getMessages&login=" + r[0] + "&domain=" + r[1]
75 | url := "?action=getMessages&login=qw7dtxz8gu&domain=1secmail.org"
76 | // 获取邮件列表
77 | mailList, err := utils.HttpReadBodyJsonMapArray(http.MethodGet, url, "", nil, nil)
78 | fmt.Println(mailList, err)
79 | if len(mailList) == 0 {
80 | return
81 | }
82 | // 科学计数法转换string数字
83 | newNum := big.NewRat(1, 1)
84 | newNum.SetFloat64(mailList[0]["id"].(float64))
85 | id := newNum.FloatString(0)
86 | // 获取邮件内容
87 | m, err := utils.HttpReadBodyJsonMap(http.MethodGet, "?action=readMessage&login=qw7dtxz8gu&domain=1secmail.org&id="+id, "",
88 | nil, nil)
89 | fmt.Println(m, err)
90 | }
91 |
92 | func TestMailtmM(t *testing.T) {
93 | account, err := mailtmM.NewAccount()
94 | if err != nil {
95 | panic(err)
96 | }
97 | log.Println(account.Address())
98 | log.Println(account.Bearer())
99 | ctx, cancel := context.WithTimeout(context.Background(), 100*time.Second)
100 | defer cancel()
101 | ch := account.MessagesChan(ctx)
102 | //loop: // 定义标签
103 | //for { // for select 通常用于持续监听多个通道(channels)
104 | select { // select 语句允许 goroutine 等待多个通道操作中的一个完成
105 | case msg, ok := <-ch:
106 | if ok {
107 | log.Println(msg.Text)
108 | //break loop // 跳出标签为 loop 的 for 循环
109 | }
110 | case <-ctx.Done():
111 | if err := ctx.Err(); err != nil {
112 | if errors.Is(err, context.DeadlineExceeded) {
113 | log.Println("超时:", err)
114 | }
115 | if errors.Is(err, context.Canceled) {
116 | log.Println("主动取消:", err)
117 | }
118 | }
119 | //case <-time.After(30 * time.Second): // 总超时 N 秒
120 | // log.Println("总处理超时,强制退出")
121 | }
122 | //}
123 | defer func(account *mailtmM.Account) {
124 | err := account.Delete()
125 | if err != nil {
126 | log.Println(err)
127 | }
128 | }(account)
129 | log.Println("删除成功")
130 | }
131 |
132 | func TestMailtmF(t *testing.T) {
133 | client := mailtmF.New()
134 | ctx, cancel := context.WithCancel(context.Background())
135 | err := client.Authenticate(ctx, "xxxxxxxx", "xxxxxxxx")
136 | account, err := client.CreateAccount(ctx, "xxxxxxxx", "xxxxxxxx")
137 | fmt.Println(account.ID)
138 | if err != nil {
139 | fmt.Println(err)
140 | }
141 | cancel()
142 | }
143 |
--------------------------------------------------------------------------------
/reptile/svp_mail_decode.go:
--------------------------------------------------------------------------------
1 | package reptile
2 |
3 | import (
4 | "bytes"
5 | "encoding/base64"
6 | "errors"
7 | "fmt"
8 | "log"
9 | "regexp"
10 | "strconv"
11 | "strings"
12 | "time"
13 |
14 | "github.com/PuerkitoBio/goquery"
15 | "github.com/go-resty/resty/v2"
16 | "golang.org/x/net/html"
17 | )
18 |
19 | /*
20 | 这个 JavaScript 脚本是 Cloudflare 用来保护网页上的电子邮件地址不被爬虫轻易抓取的功能,被称为 "Email Address Obfuscation"。
21 | 它的核心原理是:
22 | 将原始电子邮件地址进行 XOR 加密,并编码为十六进制字符串。
23 | 在前端页面上,用一个特殊的 标签(带有 href="/cdn-cgi/l/email-protection#...")或者一个带有 class="__cf_email__" 和 data-cfemail="..." 属性的 标签来占位。
24 | 当浏览器加载页面时,此 JavaScript 脚本会执行,查找这些特殊标签。
25 | 脚本读取十六进制字符串,使用第一个字节作为密钥,对后续所有字节进行 XOR 解密,还原出原始的电子邮件地址。
26 | 最后,用解密后的邮件地址替换掉页面上的占位标签。
27 | */
28 | const (
29 | // Cloudflare's email protection path prefix
30 | cfEmailProtectionPath = "/cdn-cgi/l/email-protection#"
31 | // The class name for email-protected elements
32 | cfEmailClassName = "__cf_email__"
33 | // The data attribute holding the encoded email
34 | cfDataAttribute = "data-cfemail"
35 | )
36 |
37 | // decodeCfEmail decodes a Cloudflare-encoded hexadecimal string into an email address.
38 | // This is the core logic ported from the JS `n` function.
39 | func decodeCfEmail(encodedString string) (string, error) {
40 | // Must have at least 2 chars for the key and some for the content
41 | if len(encodedString) < 2 {
42 | return "", fmt.Errorf("encoded string is too short: %s", encodedString)
43 | }
44 |
45 | // The first two hex characters are the XOR key
46 | keyHex := encodedString[0:2]
47 | key, err := strconv.ParseInt(keyHex, 16, 64)
48 | if err != nil {
49 | return "", fmt.Errorf("failed to parse key '%s': %w", keyHex, err)
50 | }
51 |
52 | var decodedBytes []byte
53 | // Loop through the rest of the string, two characters at a time
54 | for i := 2; i < len(encodedString); i += 2 {
55 | // Get the next two hex characters
56 | charHex := encodedString[i : i+2]
57 | // Parse them as a hex number
58 | charCode, err := strconv.ParseInt(charHex, 16, 64)
59 | if err != nil {
60 | return "", fmt.Errorf("failed to parse char '%s': %w", charHex, err)
61 | }
62 | // XOR with the key to get the original character code
63 | decodedBytes = append(decodedBytes, byte(charCode^key))
64 | }
65 |
66 | return string(decodedBytes), nil
67 | }
68 |
69 | // processNode recursively traverses the HTML tree, finds protected emails, and decodes them.
70 | // This function replaces the JS functions `c`, `o`, `a`, and `i`.
71 | func processNode(n *html.Node) {
72 | if n.Type == html.ElementNode {
73 | // Case 1: Handle tags with protected href
74 | // Corresponds to JS function `c`
75 | if n.Data == "a" {
76 | for i, attr := range n.Attr {
77 | if attr.Key == "href" && strings.HasPrefix(attr.Val, cfEmailProtectionPath) {
78 | encodedString := strings.TrimPrefix(attr.Val, cfEmailProtectionPath)
79 | if decodedEmail, err := decodeCfEmail(encodedString); err == nil {
80 | // Replace the attribute value
81 | n.Attr[i].Val = "mailto:" + decodedEmail
82 | }
83 | }
84 | }
85 | }
86 |
87 | // Case 2: Handle elements with data-cfemail attribute
88 | // Corresponds to JS function `o`
89 | isCfEmailElement := false
90 | var encodedString string
91 | for _, attr := range n.Attr {
92 | if attr.Key == "class" && strings.Contains(attr.Val, cfEmailClassName) {
93 | isCfEmailElement = true
94 | }
95 | if attr.Key == cfDataAttribute {
96 | encodedString = attr.Val
97 | }
98 | }
99 |
100 | if isCfEmailElement && encodedString != "" {
101 | if decodedEmail, err := decodeCfEmail(encodedString); err == nil {
102 | // Create a new text node with the decoded email
103 | textNode := &html.Node{
104 | Type: html.TextNode,
105 | Data: decodedEmail,
106 | }
107 | // Replace the protected element with the new text node
108 | if n.Parent != nil {
109 | n.Parent.InsertBefore(textNode, n)
110 | n.Parent.RemoveChild(n)
111 | }
112 | }
113 | }
114 | }
115 |
116 | // Recurse for all children
117 | for c := n.FirstChild; c != nil; c = c.NextSibling {
118 | processNode(c)
119 | }
120 | }
121 |
122 | // DecodeCloudflareEmails takes an HTML string as input and returns a version
123 | // with all Cloudflare-protected emails decoded.
124 | func DecodeCloudflareEmails(htmlContent string) (string, error) {
125 | doc, err := html.Parse(strings.NewReader(htmlContent))
126 | if err != nil {
127 | return "", fmt.Errorf("failed to parse HTML: %w", err)
128 | }
129 |
130 | processNode(doc)
131 |
132 | var buf bytes.Buffer
133 | if err := html.Render(&buf, doc); err != nil {
134 | return "", fmt.Errorf("failed to render HTML: %w", err)
135 | }
136 |
137 | // html.Render adds ... wrappers,
138 | // which we might not want if the input was a fragment.
139 | // This is a simple way to strip them for cleaner output.
140 | output := buf.String()
141 | output = strings.TrimPrefix(output, "")
142 | output = strings.TrimSuffix(output, "")
143 |
144 | return output, nil
145 | }
146 |
147 | // processSvpLinks finds all Cloudflare-protected links in a block of text
148 | // and replaces them with the decoded content.
149 | // This version is much simpler as it uses regex for this specific text format.
150 | func processSvpLinks(text string) (string, error) {
151 | // This regex finds the entire tag but specifically captures
152 | // the content of the `data-cfemail` attribute.
153 | // ...
154 | re := regexp.MustCompile(`]*data-cfemail="([a-f0-9]+)"[^>]*>.*?`)
155 |
156 | // ReplaceAllStringFunc finds all matches and calls a function to get the replacement string.
157 | // This is very efficient for this kind of task.
158 | processedText := re.ReplaceAllStringFunc(text, func(match string) string {
159 | // `FindStringSubmatch` returns the full match and all captured groups.
160 | // submatches[0] is the full match (the whole tag)
161 | // submatches[1] is the first captured group (the hex string)
162 | submatches := re.FindStringSubmatch(match)
163 | if len(submatches) < 2 {
164 | // This should not happen if the regex matched, but it's a safe check.
165 | return match
166 | }
167 |
168 | encodedString := submatches[1]
169 | decoded, err := decodeCfEmail(encodedString)
170 | if err != nil {
171 | log.Printf("Warning: could not decode '%s', leaving original. Error: %v", encodedString, err)
172 | return match // On error, leave the original tag
173 | }
174 |
175 | // Return the decoded string as the replacement for the entire tag.
176 | return decoded
177 | })
178 |
179 | return processedText, nil
180 | }
181 |
182 | // ParseSvpHtml 解析HTML并解析SVP邮件后获取URL
183 | func ParseSvpHtml(htmlContent []byte) string {
184 | // 解析HTML
185 | doc, err := goquery.NewDocumentFromReader(bytes.NewReader(htmlContent))
186 | if err != nil {
187 | panic(err.Error())
188 | }
189 | // 找到最后一个pre
190 | pre := doc.Find(`pre`).Last()
191 | if pre.Length() > 0 {
192 | ret, err := pre.Html()
193 | if err != nil {
194 | log.Printf("An error occurred: %v\n", err)
195 | }
196 | // 解析HTML
197 | doc, err = goquery.NewDocumentFromReader(bytes.NewReader([]byte(ret)))
198 | if err == nil {
199 | ret = doc.Text()
200 | }
201 | if urlRegex.MatchString(ret) {
202 | client := resty.New()
203 | client.SetHeader("User-Agent", vnVersion.Load().(string))
204 | resp, err := client.R().Get(ret)
205 | if err != nil {
206 | panic(err.Error())
207 | }
208 | if resp.IsError() {
209 | panic(errors.New(fmt.Sprintf("请求错误,url:%v,响应状态: %v,响应内容:%v\n", ret, resp.Status(), string(resp.Body()))))
210 | }
211 | dbuf := make([]byte, base64.StdEncoding.DecodedLen(len(resp.Body())))
212 | n, err := base64.StdEncoding.Decode(dbuf, resp.Body())
213 | if err != nil {
214 | panic(err.Error())
215 | }
216 | return string(dbuf[:n])
217 | }
218 | decodedHTML, err := processSvpLinks(ret)
219 | if err != nil {
220 | log.Fatalf("An error occurred: %v", err)
221 | } else {
222 | // 解析HTML
223 | doc, err = goquery.NewDocumentFromReader(bytes.NewReader([]byte(decodedHTML)))
224 | if err != nil {
225 | panic(err.Error())
226 | }
227 | return doc.Text()
228 | }
229 | }
230 | return ""
231 | }
232 |
233 | // isLatestTime 判断是否是最新的时间
234 | func isLatestTime(tp int, text string) bool {
235 | re := regexp.MustCompile(`最后更新时间:\s*(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})`)
236 |
237 | matches := re.FindStringSubmatch(text)
238 | if len(matches) < 2 {
239 | return true
240 | }
241 |
242 | timeStr := matches[1] // 使用第一个捕获组
243 | log.Println("提取到的时间字符串:", timeStr)
244 |
245 | // 解析时间
246 | parsedTime, err := time.Parse(time.DateTime, timeStr)
247 | if err != nil {
248 | return true
249 | }
250 |
251 | value, ok := smTime.Load(tp)
252 | if ok {
253 | if parsedTime.After(value.(time.Time)) {
254 | smTime.Store(tp, parsedTime)
255 | } else {
256 | return false
257 | }
258 | } else {
259 | smTime.Store(tp, parsedTime)
260 | }
261 | return true
262 | }
263 |
--------------------------------------------------------------------------------
/pyutils/nginxfmt.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 |
4 | """This Python script formats nginx configuration files in consistent way.
5 |
6 | Originally published under https://github.com/1connect/nginx-config-formatter
7 | """
8 |
9 | import argparse
10 | import codecs
11 |
12 | import re
13 |
14 | import sys
15 |
16 | __author__ = "Michał Słomkowski"
17 | __license__ = "Apache 2.0"
18 | __version__ = "1.0.2"
19 |
20 | INDENTATION = ' ' * 4
21 |
22 | TEMPLATE_VARIABLE_OPENING_TAG = '___TEMPLATE_VARIABLE_OPENING_TAG___'
23 | TEMPLATE_VARIABLE_CLOSING_TAG = '___TEMPLATE_VARIABLE_CLOSING_TAG___'
24 |
25 | TEMPLATE_BRACKET_OPENING_TAG = '___TEMPLATE_BRACKET_OPENING_TAG___'
26 | TEMPLATE_BRACKET_CLOSING_TAG = '___TEMPLATE_BRACKET_CLOSING_TAG___'
27 |
28 |
29 | def strip_line(single_line):
30 | """Strips the line and replaces neighbouring whitespaces with single space (except when within quotation marks)."""
31 | single_line = single_line.strip()
32 | if single_line.startswith('#'):
33 | return single_line
34 |
35 | within_quotes = False
36 | parts = []
37 | for part in re.split('"', single_line):
38 | if within_quotes:
39 | parts.append(part)
40 | else:
41 | parts.append(re.sub(r'[\s]+', ' ', part))
42 | within_quotes = not within_quotes
43 | return '"'.join(parts)
44 |
45 |
46 | def count_multi_semicolon(single_line):
47 | """count multi_semicolon (except when within quotation marks)."""
48 | single_line = single_line.strip()
49 | if single_line.startswith('#'):
50 | return 0, 0
51 |
52 | within_quotes = False
53 | q = 0
54 | c = 0
55 | for part in re.split('"', single_line):
56 | if within_quotes:
57 | q = 1
58 | else:
59 | c += part.count(';')
60 | within_quotes = not within_quotes
61 | return q, c
62 |
63 |
64 | def multi_semicolon(single_line):
65 | """break multi_semicolon into multiline (except when within quotation marks)."""
66 | single_line = single_line.strip()
67 | if single_line.startswith('#'):
68 | return single_line
69 |
70 | within_quotes = False
71 | parts = []
72 | for part in re.split('"', single_line):
73 | if within_quotes:
74 | parts.append(part)
75 | else:
76 | parts.append(part.replace(";", ";\n"))
77 | within_quotes = not within_quotes
78 | return '"'.join(parts)
79 |
80 |
81 | def apply_variable_template_tags(line: str) -> str:
82 | """Replaces variable indicators ${ and } with tags, so subsequent formatting is easier."""
83 | return re.sub(r'\${\s*(\w+)\s*}',
84 | TEMPLATE_VARIABLE_OPENING_TAG + r"\1" + TEMPLATE_VARIABLE_CLOSING_TAG,
85 | line,
86 | flags=re.UNICODE)
87 |
88 |
89 | def strip_variable_template_tags(line: str) -> str:
90 | """Replaces tags back with ${ and } respectively."""
91 | return re.sub(TEMPLATE_VARIABLE_OPENING_TAG + r'\s*(\w+)\s*' + TEMPLATE_VARIABLE_CLOSING_TAG,
92 | r'${\1}',
93 | line,
94 | flags=re.UNICODE)
95 |
96 |
97 | def apply_bracket_template_tags(content: str) -> str:
98 | """ Replaces bracket { and } with tags, so subsequent formatting is easier."""
99 | result = ""
100 | in_quotes = False
101 | last_c = ""
102 |
103 | for c in content:
104 | if (c == "\'" or c == "\"") and last_c != "\\":
105 | in_quotes = reverse_in_quotes_status(in_quotes)
106 | if in_quotes:
107 | if c == "{":
108 | result += TEMPLATE_BRACKET_OPENING_TAG
109 | elif c == "}":
110 | result += TEMPLATE_BRACKET_CLOSING_TAG
111 | else:
112 | result += c
113 | else:
114 | result += c
115 | last_c = c
116 | return result
117 |
118 |
119 | def reverse_in_quotes_status(status: bool) -> bool:
120 | if status:
121 | return False
122 | return True
123 |
124 |
125 | def strip_bracket_template_tags(content: str) -> str:
126 | """ Replaces tags back with { and } respectively."""
127 | content = content.replace(TEMPLATE_BRACKET_OPENING_TAG, "{", -1)
128 | content = content.replace(TEMPLATE_BRACKET_CLOSING_TAG, "}", -1)
129 | return content
130 |
131 |
132 | def clean_lines(orig_lines) -> list:
133 | """Strips the lines and splits them if they contain curly brackets."""
134 | cleaned_lines = []
135 | for line in orig_lines:
136 | line = strip_line(line)
137 | line = apply_variable_template_tags(line)
138 | if line == "":
139 | cleaned_lines.append("")
140 | continue
141 | else:
142 | if line.startswith("#"):
143 | cleaned_lines.append(strip_variable_template_tags(line))
144 | else:
145 | q, c = count_multi_semicolon(line)
146 | if q == 1 and c > 1:
147 | ml = multi_semicolon(line)
148 | cleaned_lines.extend(clean_lines(ml.splitlines()))
149 | elif q != 1 and c > 1:
150 | newlines = line.split(";")
151 | cleaned_lines.extend(clean_lines(["".join([ln, ";"]) for ln in newlines if ln != ""]))
152 | else:
153 | if line.startswith("rewrite"):
154 | cleaned_lines.append(strip_variable_template_tags(line))
155 | else:
156 | cleaned_lines.extend(
157 | [strip_variable_template_tags(l).strip() for l in re.split(r"([{}])", line) if l != ""])
158 | return cleaned_lines
159 |
160 |
161 | def join_opening_bracket(lines):
162 | """When opening curly bracket is in it's own line (K&R convention), it's joined with precluding line (Java)."""
163 | modified_lines = []
164 | for i in range(len(lines)):
165 | if i > 0 and lines[i] == "{":
166 | modified_lines[-1] += " {"
167 | else:
168 | modified_lines.append(lines[i])
169 | return modified_lines
170 |
171 |
172 | def perform_indentation(lines):
173 | """Indents the lines according to their nesting level determined by curly brackets."""
174 | indented_lines = []
175 | current_indent = 0
176 | for line in lines:
177 | if not line.startswith("#") and line.endswith('}') and current_indent > 0:
178 | current_indent -= 1
179 |
180 | if line != "":
181 | indented_lines.append(current_indent * INDENTATION + line)
182 | else:
183 | indented_lines.append("")
184 |
185 | if not line.startswith("#") and line.endswith('{'):
186 | current_indent += 1
187 |
188 | return indented_lines
189 |
190 |
191 | def format_config_contents(contents):
192 | """Accepts the string containing nginx configuration and returns formatted one. Adds newline at the end."""
193 | contents = apply_bracket_template_tags(contents)
194 | lines = contents.splitlines()
195 | lines = clean_lines(lines)
196 | lines = join_opening_bracket(lines)
197 | lines = perform_indentation(lines)
198 |
199 | text = '\n'.join(lines)
200 | text = strip_bracket_template_tags(text)
201 |
202 | for pattern, substitute in ((r'\n{3,}', '\n\n\n'), (r'^\n', ''), (r'\n$', '')):
203 | text = re.sub(pattern, substitute, text, re.MULTILINE)
204 |
205 | return text + '\n'
206 |
207 |
208 | def format_config_file(file_path, original_backup_file_path=None, verbose=True):
209 | """
210 | Performs the formatting on the given file. The function tries to detect file encoding first.
211 | :param file_path: path to original nginx configuration file. This file will be overridden.
212 | :param original_backup_file_path: optional path, where original file will be backed up.
213 | :param verbose: show messages
214 | """
215 | encodings = ('utf-8', 'latin1')
216 |
217 | encoding_failures = []
218 | chosen_encoding = None
219 |
220 | for enc in encodings:
221 | try:
222 | with codecs.open(file_path, 'r', encoding=enc) as rfp:
223 | original_file_content = rfp.read()
224 | chosen_encoding = enc
225 | break
226 | except ValueError as e:
227 | encoding_failures.append(e)
228 |
229 | if chosen_encoding is None:
230 | raise Exception('none of encodings %s are valid for file %s. Errors: %s'
231 | % (encodings, file_path, [e.message for e in encoding_failures]))
232 |
233 | assert original_file_content is not None
234 |
235 | with codecs.open(file_path, 'w', encoding=chosen_encoding) as wfp:
236 | wfp.write(format_config_contents(original_file_content))
237 |
238 | if verbose:
239 | print("Formatted file '%s' (detected encoding %s)." % (file_path, chosen_encoding))
240 |
241 | if original_backup_file_path:
242 | with codecs.open(original_backup_file_path, 'w', encoding=chosen_encoding) as wfp:
243 | wfp.write(original_file_content)
244 | if verbose:
245 | print("Original saved to '%s'." % original_backup_file_path)
246 |
247 |
248 | if __name__ == "__main__":
249 | contents = format_config_contents(sys.argv[1])
250 | print(contents)
--------------------------------------------------------------------------------
/reptile/chromedp.go:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * @Description:
4 | * @Author: https://www.bajins.com
5 | * @File: chromedp.go
6 | * @Version: 1.0.0
7 | * @Time: 2019/9/19 9:31
8 | * @Project: tool-gin
9 | * @Package:
10 | * @Software: GoLand
11 | */
12 | package reptile
13 |
14 | import (
15 | "context"
16 | "fmt"
17 | "log"
18 | "os"
19 | "strings"
20 | "time"
21 | "tool-gin/utils"
22 |
23 | "github.com/chromedp/cdproto/cdp"
24 | "github.com/chromedp/cdproto/network"
25 | "github.com/chromedp/cdproto/page"
26 | "github.com/chromedp/cdproto/runtime"
27 | "github.com/chromedp/chromedp"
28 | )
29 |
30 | // Apply 启动,建议在主入口处调用一次即可
31 | //
32 | // context.Context部分不能抽离,否则会报 context canceled
33 | func Apply(debug bool) (context.Context, context.CancelFunc) {
34 | // 创建缓存目录
35 | //dir, err := os.MkdirTemp("", "chromedp-example")
36 | //if err != nil {
37 | // panic(err)
38 | //}
39 | //defer os.RemoveAll(dir)
40 |
41 | //dir, err := os.MkdirTemp("", "chromedp-example")
42 | //if err != nil {
43 | // panic(err)
44 | //}
45 | //defer os.RemoveAll(dir)
46 | opts := append(chromedp.DefaultExecAllocatorOptions[:],
47 | // 禁用GPU,不显示GUI
48 | chromedp.DisableGPU,
49 | // 取消沙盒模式
50 | chromedp.NoSandbox,
51 | // 指定浏览器分辨率
52 | //chromedp.WindowSize(1600, 900),
53 | // 设置UA,防止有些页面识别headless模式
54 | chromedp.UserAgent(utils.UserAgent),
55 | // 隐身模式启动
56 | chromedp.Flag("incognito", true),
57 | // 忽略证书错误
58 | chromedp.Flag("ignore-certificate-errors", true),
59 | // 窗口最大化
60 | chromedp.Flag("start-maximized", true),
61 | // 不加载图片, 提升速度
62 | chromedp.Flag("disable-images", true),
63 | chromedp.Flag("blink-settings", "imagesEnabled=false"),
64 | // 禁用扩展
65 | chromedp.Flag("disable-extensions", true),
66 | // 禁止加载所有插件
67 | chromedp.Flag("disable-plugins", true),
68 | // 禁用浏览器应用
69 | chromedp.Flag("disable-software-rasterizer", true),
70 | //chromedp.Flag("remote-debugging-port","9222"),
71 | //chromedp.Flag("debuggerAddress","127.0.0.1:9222"),
72 | chromedp.Flag("user-data-dir", "./.cache"),
73 | //chromedp.Flag("excludeSwitches", "enable-automation"),
74 | // 设置用户数据目录
75 | //chromedp.UserDataDir(dir),
76 | //chromedp.ExecPath("C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe"),
77 | //chromedp.Flag("disable-dev-shm-usage", true),
78 | chromedp.Flag("disable-application-cache", true), // 禁用应用缓存
79 | chromedp.Flag("disk-cache-dir", ""), // 禁用磁盘缓存,可能会导致加载缓慢
80 | chromedp.Flag("no-cache", true), // 禁用内存缓存,可能会导致加载缓慢
81 | //chromedp.Flag("disable-gpu", true),
82 | //chromedp.Flag("disable-gpu-compositing", true),
83 | //chromedp.Flag("disable-gpu-sandbox", true),
84 | //chromedp.Flag("disable-web-security", true),
85 | //chromedp.Flag("disable-webgl", true),
86 | //chromedp.Flag("disable-web-security-warnings", true),
87 | //chromedp.Flag("disable-webgl2", true),
88 | //chromedp.Flag("disable-web-security-csp", true),
89 | //chromedp.Flag("disable-web-security-x-frame-options", true),
90 | //chromedp.Flag("disable-web-security-x-frame-options-allow-from", true),
91 | )
92 | if debug {
93 | opts = append(opts, chromedp.Flag("headless", false), chromedp.Flag("hide-scrollbars", false))
94 | }
95 |
96 | ctx, cancel := chromedp.NewExecAllocator(context.Background(), opts...)
97 | // 自定义记录器
98 | ctx, cancel = chromedp.NewContext(ctx, chromedp.WithLogf(log.Printf))
99 | // 设置超时时间
100 | ctx, cancel = context.WithTimeout(ctx, 3*time.Minute)
101 | //ctx, cancel = context.WithCancel(ctx)
102 | //if close {
103 | // defer cancel()
104 | //}
105 | return ctx, cancel
106 | }
107 |
108 | // 监听
109 | func listenForNetworkEvent(ctx context.Context) {
110 | chromedp.ListenTarget(ctx, func(ev interface{}) {
111 | switch ev := ev.(type) {
112 | case *network.EventResponseReceived:
113 | resp := ev.Response
114 | if len(resp.Headers) != 0 {
115 | // log.Printf("received headers: %s", resp.Headers)
116 | if strings.Index(resp.URL, ".ts") != -1 {
117 | log.Printf("received headers: %s", resp.URL)
118 | }
119 | }
120 | case *network.WebSocketResponse:
121 | respH := ev.Headers
122 | log.Println("WebSocketResponse", respH)
123 | case *network.Cookie:
124 | domain := ev.Domain
125 | log.Println("Cookie", domain)
126 | case *network.Headers:
127 | log.Println("Headers", ev)
128 | }
129 | // other needed network Event
130 | })
131 | }
132 |
133 | // 任务 主要用来设置cookie ,获取登录账号后的页面
134 | func visitWeb(url string) chromedp.Tasks {
135 | return chromedp.Tasks{
136 | chromedp.ActionFunc(func(ctxt context.Context) error {
137 | expr := cdp.TimeSinceEpoch(time.Now().Add(180 * 24 * time.Hour))
138 | // 设置cookie
139 | err := network.SetCookie("ASP.NET_SessionId", "这里是值").
140 | WithExpires(&expr).
141 | // 访问网站主体
142 | WithDomain(url).
143 | WithHTTPOnly(true).
144 | Do(ctxt)
145 | if err != nil {
146 | return err
147 | }
148 | return nil
149 | }),
150 | // 页面跳转
151 | chromedp.Navigate(url),
152 | }
153 | }
154 |
155 | // 截图
156 | func Screenshot() chromedp.Tasks {
157 | var buf []byte
158 |
159 | task := chromedp.Tasks{
160 | chromedp.CaptureScreenshot(&buf),
161 | chromedp.ActionFunc(func(context.Context) error {
162 | return os.WriteFile("testimonials.png", buf, 0644)
163 | }),
164 | }
165 | /*if err := os.WriteFile("fullScreenshot.png", buf, 0644); err != nil {
166 | log.Fatal("生成图片错误:", err)
167 | }*/
168 | return task
169 | }
170 |
171 | // 任务 主要执行翻页功能和或者html
172 | func DoCrawler(url string, res *string) chromedp.Tasks {
173 | return chromedp.Tasks{
174 | // 浏览器下载行为,注意设置顺序,如果不是第一个会失败
175 | //page.SetDownloadBehavior(page.SetDownloadBehaviorBehaviorDeny),
176 | network.Enable(),
177 | //visitWeb(url),
178 | //doCrawler(&res),
179 | //Screenshot(),
180 | // 跳转页面
181 | chromedp.Navigate(url),
182 | // 查找并等待可见
183 | chromedp.WaitVisible(`body`, chromedp.ByQuery),
184 | // 等待1秒
185 | chromedp.Sleep(1 * time.Second),
186 | // 点击元素
187 | //chromedp.Click(`.pagination li:nth-last-child(4) a`, chromedp.BySearch),
188 | // 读取HTML源码
189 | chromedp.OuterHTML(`body`, res, chromedp.ByQuery),
190 | //chromedp.Text(`.fusion-text h1`, res, chromedp.BySearch),
191 | //chromedp.TextContent(`.fusion-text h1`, res, chromedp.BySearch),
192 | chromedp.Title(res),
193 | }
194 | }
195 |
196 | // 执行js
197 | // https://github.com/chromedp/chromedp/issues/256
198 | func EvalJS(js string) chromedp.Tasks {
199 | var res *runtime.RemoteObject
200 | return chromedp.Tasks{
201 | chromedp.EvaluateAsDevTools(js, &res),
202 | //chromedp.Evaluate(js, &res),
203 | chromedp.ActionFunc(func(ctx context.Context) error {
204 | b, err := res.Value.MarshalJSON()
205 | if err != nil {
206 | return err
207 | }
208 | fmt.Println("result: ", string(b))
209 | return nil
210 | }),
211 | }
212 | }
213 |
214 | // see: https://intoli.com/blog/not-possible-to-block-chrome-headless/
215 | const script = `(function(w, n, wn) {
216 | console.log(navigator.webdriver);
217 |
218 | // Pass the Webdriver Test.
219 | // chrome 为undefined,Firefox 为false
220 | //Object.defineProperty(n, 'webdriver', {
221 | // get: () => undefined,
222 | //});
223 | // 通过原型删除该属性
224 | delete navigator.__proto__.webdriver;
225 | console.log(navigator.webdriver);
226 |
227 | // Pass the Plugins Length Test.
228 | // Overwrite the plugins property to use a custom getter.
229 | Object.defineProperty(n, 'plugins', {
230 | // This just needs to have length > 0 for the current test,
231 | // but we could mock the plugins too if necessary.
232 | get: () =>[
233 | {filename:'internal-pdf-viewer'},
234 | {filename:'adsfkjlkjhalkh'},
235 | {filename:'internal-nacl-plugin'}
236 | ],
237 | });
238 |
239 | // Pass the Languages Test.
240 | // Overwrite the plugins property to use a custom getter.
241 | Object.defineProperty(n, 'languages', {
242 | get: () => ['zh-CN', 'en'],
243 | });
244 |
245 | // store the existing descriptor
246 | const elementDescriptor = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'offsetHeight');
247 |
248 | // redefine the property with a patched descriptor
249 | Object.defineProperty(HTMLDivElement.prototype, 'offsetHeight', {
250 | ...elementDescriptor,
251 | get: function() {
252 | if (this.id === 'modernizr') {
253 | return 1;
254 | }
255 | return elementDescriptor.get.apply(this);
256 | },
257 | });
258 |
259 | ['height', 'width'].forEach(property => {
260 | // store the existing descriptor
261 | const imageDescriptor = Object.getOwnPropertyDescriptor(HTMLImageElement.prototype, property);
262 |
263 | // redefine the property with a patched descriptor
264 | Object.defineProperty(HTMLImageElement.prototype, property, {
265 | ...imageDescriptor,
266 | get: function() {
267 | // return an arbitrary non-zero dimension if the image failed to load
268 | if (this.complete && this.naturalHeight == 0) {
269 | return 20;
270 | }
271 | // otherwise, return the actual dimension
272 | return imageDescriptor.get.apply(this);
273 | },
274 | });
275 | });
276 |
277 | // Pass the Chrome Test.
278 | // We can mock this in as much depth as we need for the test.
279 | w.chrome = {
280 | runtime: {},
281 | };
282 | window.navigator.chrome = {
283 | runtime: {},
284 | };
285 |
286 | // Pass the Permissions Test.
287 | const originalQuery = wn.permissions.query;
288 | return wn.permissions.query = (parameters) => (
289 | parameters.name === 'notifications' ?
290 | Promise.resolve({ state: Notification.permission }) :
291 | originalQuery(parameters)
292 | );
293 |
294 | })(window, navigator, window.navigator);`
295 |
296 | // 反检测Headless
297 | // https://github.com/chromedp/chromedp/issues/396
298 | func AntiDetectionHeadless() chromedp.Tasks {
299 | return chromedp.Tasks{
300 | chromedp.ActionFunc(func(ctx context.Context) error {
301 | identifier, err := page.AddScriptToEvaluateOnNewDocument(script).Do(ctx)
302 | if err != nil {
303 | return err
304 | }
305 | fmt.Println("identifier: ", identifier.String())
306 | return nil
307 | }),
308 | }
309 | }
310 |
--------------------------------------------------------------------------------
/static/js/utils/util.js:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * @Description:
4 | * @Author: claer
5 | * @File: util.js
6 | * @Version: 1.0.0
7 | * @Time: 2019/9/15 20:11
8 | * @Project: tool-gin
9 | * @Package:
10 | * @Software: GoLand
11 | */
12 |
13 |
14 | /**
15 | * 给String对象增加一个原型方法:
16 | * 判断一个字符串是以指定字符串结尾的
17 | *
18 | * @param str 需要判断的子字符串
19 | * @returns {boolean} 是否以该字符串结尾
20 | */
21 | String.prototype.endWith = function (str) {
22 | if (str == null || str == "" || this.length == 0 || str.length > this.length)
23 | return false;
24 | if (this.substring(this.length - str.length) != str) {
25 | return false;
26 | }
27 | return true;
28 | }
29 |
30 |
31 | /**
32 | * 给String对象增加一个原型方法:
33 | * 判断一个字符串是以指定字符串开头的
34 | *
35 | * @param str 需要判断的子字符串
36 | * @returns {boolean} 是否以该字符串开头
37 | */
38 | String.prototype.startWith = function (str) {
39 | if (str == null || str == "" || this.length == 0 || str.length > this.length)
40 | return false;
41 | if (this.substr(0, str.length) != str) {
42 | return false;
43 | }
44 | return true;
45 | }
46 |
47 | /**
48 | * 给String对象增加一个原型方法:
49 | * 判断一个字符串是以指定字符串结尾的
50 | *
51 | * @param str 需要判断的子字符串
52 | * @returns {boolean} 是否以该字符串结尾
53 | */
54 | String.prototype.endWithRegExp = function (str) {
55 | let reg = new RegExp(str + "$");
56 | return reg.test(this);
57 | }
58 | /**
59 | * 给String对象增加一个原型方法:
60 | * 判断一个字符串是以指定字符串开头的
61 | *
62 | * @param str 需要判断的子字符串
63 | * @returns {boolean} 是否以该字符串开头
64 | */
65 | String.prototype.startWithRegExp = function (str) {
66 | let reg = new RegExp("^" + str);
67 | return reg.test(this);
68 | }
69 |
70 |
71 | /**
72 | * 给String对象增加一个原型方法:
73 | * 替换全部字符串 - 无replaceAll的解决方案,自定义扩展js函数库
74 | * 原生js中并没有replaceAll方法,只有replace,如果要将字符串替换,一般使用replace
75 | *
76 | * @param FindText 要替换的字符串
77 | * @param RepText 新的字符串
78 | * @returns {string}
79 | */
80 | String.prototype.replaceAll = function (FindText, RepText) {
81 | // g表示执行全局匹配,m表示执行多次匹配
82 | let regExp = new RegExp(FindText, "gm");
83 | return this.replace(regExp, RepText);
84 | }
85 |
86 |
87 | if (!String.prototype.trim) {
88 | String.prototype.trim = function () {
89 | return this.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '');
90 | }
91 | }
92 |
93 | if (!String.prototype.startsWith) {
94 | String.prototype.startsWith = function (searchString, position) {
95 | position = position || 0;
96 | return this.substr(position, searchString.length) === searchString;
97 | }
98 | }
99 |
100 |
101 | if (!String.prototype.endsWith) {
102 | String.prototype.endsWith = function (searchString, position) {
103 | let subjectString = this.toString();
104 | if (typeof position !== 'number' || !isFinite(position) || Math.floor(position) !== position || position > subjectString.length) {
105 | position = subjectString.length;
106 | }
107 | position -= searchString.length;
108 | let lastIndex = subjectString.indexOf(searchString, position);
109 | return lastIndex !== -1 && lastIndex === position;
110 | }
111 | }
112 |
113 |
114 | if (!String.prototype.includes) {
115 | String.prototype.includes = function (search, start) {
116 | 'use strict';
117 | if (typeof start !== 'number') {
118 | start = 0;
119 | }
120 |
121 | if (start + search.length > this.length) {
122 | return false;
123 | } else {
124 | return this.indexOf(search, start) !== -1;
125 | }
126 | }
127 | }
128 |
129 | if (!String.prototype.repeat) {
130 | String.prototype.repeat = function (count) {
131 | if (this == null) {
132 | throw new TypeError('can\'t convert ' + this + ' to object');
133 | }
134 | let str = '' + this;
135 | count = +count;
136 | if (count != count) {
137 | count = 0;
138 | }
139 | if (count < 0) {
140 | throw new RangeError('repeat count must be non-negative');
141 | }
142 | if (count == Infinity) {
143 | throw new RangeError('repeat count must be less than infinity');
144 | }
145 | count = Math.floor(count);
146 | if (str.length == 0 || count == 0) {
147 | return '';
148 | }
149 | // Ensuring count is a 31-bit integer allows us to heavily optimize the
150 | // main part. But anyway, most current (August 2014) browsers can't handle
151 | // strings 1 << 28 chars or longer, so:
152 | if (str.length * count >= 1 << 28) {
153 | throw new RangeError('repeat count must not overflow maximum string size');
154 | }
155 | let rpt = '';
156 | for (; ;) {
157 | if ((count & 1) == 1) {
158 | rpt += str;
159 | }
160 | count >>>= 1;
161 | if (count == 0) {
162 | break;
163 | }
164 | str += str;
165 | }
166 | // Could we try:
167 | // return Array(count + 1).join(this);
168 | return rpt;
169 | }
170 | }
171 |
172 | //removes element from array
173 | if (!Array.prototype.remove) {
174 | Array.prototype.remove = function (index) {
175 | this.splice(index, 1);
176 | }
177 | }
178 |
179 |
180 | if (!String.prototype.contains) {
181 | String.prototype.contains = String.prototype.includes;
182 | }
183 |
184 | if (!Array.prototype.insert) {
185 | Array.prototype.insert = function (index, item) {
186 | this.splice(index, 0, item);
187 | }
188 | }
189 |
190 |
191 | // ====================================== 全局工具函数 =======================================
192 |
193 |
194 | // JS获取当前文件所在的文件夹全路径
195 | // let js = document.scripts;
196 | // js = js[js.length - 1].src.substring(0, js[js.length - 1].src.lastIndexOf("/") + 1);
197 |
198 | /**
199 | * 判断一个元素是否含有指定class
200 | * @param selector
201 | * @param cls
202 | * @returns {boolean}
203 | */
204 | const hasClass = (selector, cls) => {
205 | return (` ${document.querySelector(selector).className} `).indexOf(` ${cls} `) > -1;
206 | }
207 |
208 |
209 | /**
210 | * 获取当前路径
211 | *
212 | * @returns {string}
213 | */
214 | const getCurrAbsPath = () => {
215 | // FF,Chrome
216 | if (document.currentScript) {
217 | return document.currentScript.src;
218 | }
219 |
220 | // IE10
221 | let e = new Error('');
222 | let stack = e.stack || e.sourceURL || e.stacktrace || '';
223 | let rExtractUri = /((?:http|https|file):\/\/.*?\/[^:]+)(?::\d+)?:\d+/;
224 | // let rgx = /(?:http|https|file):\/\/.*?\/.+?.js/;
225 | let absPath = rExtractUri.exec(stack);
226 | if (absPath) {
227 | return absPath[1];
228 | }
229 |
230 | // IE5-9
231 | let doc = exports.document;
232 | let scripts = doc.scripts;
233 | let expose = +new Date();
234 | let isLtIE8 = ('' + doc.querySelector).indexOf('[native code]') === -1;
235 | for (let i = 0; i < scripts.length; i++) {
236 | let script = scripts[i];
237 | if (script.className != expose && script.readyState === 'interactive') {
238 | script.className = expose;
239 | // 如果小于ie 8,则必须通过getAttribute(src,4)获得abs路径
240 | return isLtIE8 ? script.getAttribute('src', 4) : script.src;
241 | }
242 | }
243 | }
244 |
245 | /**
246 | * 获取绝对路径
247 | *
248 | * @returns {string}
249 | */
250 | const getPath = () => {
251 | let jsPath = document.currentScript ? document.currentScript.src : function () {
252 | let js = document.scripts,
253 | last = js.length - 1,
254 | src;
255 | for (let i = last; i > 0; i--) {
256 | if (js[i].readyState === 'interactive') {
257 | src = js[i].src;
258 | break;
259 | }
260 | }
261 | return src || js[last].src;
262 | }();
263 | return jsPath.substring(jsPath.lastIndexOf('/') + 1, jsPath.length);
264 | }
265 |
266 |
267 | /**
268 | * 生成从最小数到最大数的随机数
269 | * minNum 最小数
270 | * maxNum 最大数
271 | */
272 | const randomNum = (minNum, maxNum) => {
273 | return parseInt(Math.random() * (maxNum - minNum + 1) + minNum, 10);
274 | }
275 |
276 |
277 | /**
278 | * 设置延时后再执行下一步操作
279 | *
280 | * @return
281 | */
282 | const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
283 |
284 |
285 | /**
286 | * 判断Array/Object/String是否为空
287 | *
288 | * @param obj
289 | * @return {boolean}
290 | */
291 | const isEmpty = (obj) => {
292 | let type = Object.prototype.toString.call(obj);
293 | if (obj == null || obj == undefined) {
294 | return true;
295 | }
296 | switch (type) {
297 | case "[object Undefined]", "[object Null]":
298 | return true;
299 | case "[object String]":
300 | obj = obj.replace(/\s*/g, "");
301 | if (obj === "" || obj.length == 0) {
302 | return true;
303 | }
304 | return false;
305 | case "[object Array]":
306 | if (!Array.isArray(obj) || obj.length == 0) {
307 | return true;
308 | }
309 | return false;
310 | case "[object Object]":
311 | // Object.keys() 返回一个由给定对象的自身可枚举属性组成的数组
312 | if (obj.length == 0 || Object.keys(obj).length == 0) {
313 | return true;
314 | }
315 | return false;
316 | default:
317 | throw TypeError("只能判断Array/Object/String,当前类型为:" + type);
318 | }
319 | }
320 |
321 |
322 | /**
323 | * export default 服从 ES6 的规范,补充:default 其实是别名
324 | * module.exports 服从 CommonJS 规范 https://javascript.ruanyifeng.com/nodejs/module.html
325 | * 一般导出一个属性或者对象用 export default
326 | * 一般导出模块或者说文件使用 module.exports
327 | *
328 | * import from 服从ES6规范,在编译器生效
329 | * require 服从ES5 规范,在运行期生效
330 | * 目前 vue 编译都是依赖label 插件,最终都转化为ES5
331 | *
332 | * @return 将方法、变量暴露出去
333 | */
334 | export default {
335 | hasClass,
336 | getCurrAbsPath,
337 | getPath,
338 | randomNum,
339 | delay,
340 | isEmpty
341 | }
--------------------------------------------------------------------------------
/static/js/utils/http.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @Description:
3 | * @Author: bajins www.bajins.com
4 | * @File: http.js
5 | * @Version: 1.0.0
6 | * @Time: 2019/9/12 11:29
7 | * @Project: tool-gin
8 | * @Package:
9 | * @Software: GoLand
10 | */
11 |
12 | /**
13 | * 请求方式(OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, PATCH)
14 | * https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Methods
15 | *
16 | * @type {{TRACE: string, HEAD: string, DELETE: string, POST: string, GET: string, PATCH: string, OPTIONS: string, PUT: string}}
17 | */
18 | const METHOD = {
19 | GET: "GET",
20 | HEAD: "HEAD",
21 | POST: "POST",
22 | PUT: "PUT",
23 | DELETE: "DELETE",
24 | CONNECT: "CONNECT",
25 | OPTIONS: "OPTIONS",
26 | TRACE: "TRACE",
27 | PATCH: "PATCH",
28 | }
29 |
30 | /**
31 | * Content-Type请求数据类型,告诉接收方,我发什么类型的数据。
32 | * https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Type
33 | * https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Basics_of_HTTP/MIME_types
34 | * https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Disposition
35 | *
36 | * application/x-www-form-urlencoded:数据被编码为名称/值对。这是标准的编码格式。默认使用此类型。
37 | * multipart/form-data:数据被编码为一条消息,页上的每个控件对应消息中的一个部分。
38 | * text/plain:数据以纯文本形式(text/json/xml/html)进行编码,其中不含任何控件或格式字符。postman软件里标的是RAW。
39 | *
40 | * @type {{FORM_DATA: string, URLENCODED: string, TEXT_PLAIN: string}}
41 | */
42 | const CONTENT_TYPE = {
43 | URLENCODED: "application/x-www-form-urlencoded",
44 | FORM_DATA: "multipart/form-data",
45 | TEXT_PLAIN: "text/plain",
46 | APP_JSON: "application/json",
47 | APP_OS: "application/octet-stream",
48 | }
49 |
50 | /**
51 | * XMLHttpRequest预期服务器返回数据类型,并根据此值进行本地解析
52 | * https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest/responseType
53 | *
54 | * @type {{ARRAY_BUFFER: string, BLOB: string, MS_STREAM: string, DOCUMENT: string, TEXT: string, JSON: string}}
55 | */
56 | const RESPONSE_TYPE = {
57 | TEXT: "text", ARRAY_BUFFER: "arraybuffer", BLOB: "blob", DOCUMENT: "document", JSON: "json", MS_STREAM: "ms-stream"
58 | }
59 |
60 |
61 | /**
62 | * js封装ajax请求 https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest
63 | * 使用new XMLHttpRequest 创建请求对象,所以不考虑低端IE浏览器(IE6及以下不支持XMLHttpRequest)
64 | * 注意:请求参数如果包含日期类型.是否能请求成功需要后台接口配合
65 | *
66 | * url: 请求路径
67 | * method: 请求方式(OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, PATCH)
68 | * data: 是作为请求主体被发送的数据,只适用于这些请求方法 'PUT','POST','PATCH'
69 | * contentType: 请求数据类型(application/x-www-form-urlencoded,multipart/form-data,text/plain)
70 | * responseType: 响应的数据类型(text,arraybuffer,blob,document,json,ms-stream)
71 | * timeout: 超时时间,0表示不设置超时
72 | *
73 | * @param settings
74 | */
75 | const ajax = (settings = {}) => {
76 | // 初始化请求参数
77 | const config = Object.assign({
78 | method: settings.type || settings.method || METHOD.GET, // string 期望的返回数据类型:'json' 'text' 'document' ...
79 | responseType: settings.dataType || settings.responseType || RESPONSE_TYPE.JSON,
80 | async: true, // boolean true:异步请求 false:同步请求 required
81 | data: null, // any 请求参数,data需要和请求头Content-Type对应
82 | headers: {},
83 | timeout: settings.timeout || 1000, // 超时时间:0表示不设置超时
84 | beforeSend: (xhr) => {
85 |
86 | },
87 | success: (result, status, xhr) => {
88 |
89 | },
90 | error: (xhr, status, error) => {
91 |
92 | },
93 | complete: (xhr, status) => {
94 |
95 | }
96 | }, settings);
97 |
98 | if (!config.headers["Content-Type"]) {
99 | // 服务器会根据此值解析参数,同时在返回时也指定此值
100 | config.headers["Content-Type"] = settings.contentType || config.headers["content-type"] || CONTENT_TYPE.URLENCODED;
101 | }
102 | if (!config.headers["Content-Type"]) { // 应对上传文件,会自动设置为multipart/form-data; boundary=----WebKitFormBoundary
103 | delete config.headers["Content-Type"];
104 | }
105 | // 参数验证
106 | if (!config.url) {
107 | throw new TypeError("ajax请求:url为空");
108 | }
109 | if (!config.method) {
110 | throw new TypeError("ajax请求:type或method参数不正确");
111 | }
112 | if (!config.responseType) {
113 | throw new TypeError("ajax请求:dataType或responseType参数不正确");
114 | }
115 | if (!config.headers || !config.headers["Content-Type"]) {
116 | throw new TypeError("ajax请求:Content-Type参数不正确");
117 | }
118 | // 创建XMLHttpRequest请求对象
119 | let xhr = new XMLHttpRequest();
120 |
121 | // 请求开始回调函数,对应xhr.loadstart
122 | xhr.addEventListener('loadstart', e => {
123 | config.beforeSend(xhr, e);
124 | });
125 | // 请求成功回调函数,对应xhr.onload
126 | xhr.addEventListener('load', e => {
127 | // https://blog.csdn.net/qq_43418737/article/details/121851847
128 | if ((xhr.status < 200 || xhr.status >= 300) && xhr.status !== 304) {
129 | config.error(xhr, xhr.status, e);
130 | return;
131 | }
132 | if (xhr.responseType === 'text') {
133 | config.success(xhr.responseText, xhr.status, xhr);
134 | } else if (xhr.responseType === 'document') {
135 | config.success(xhr.responseXML, xhr.status, xhr);
136 | } else if (Object.getPrototypeOf(xhr.response) === Blob.prototype) { // 二进制,用于下载文件
137 | const ct = xhr.getResponseHeader("content-type");
138 | if (xhr.response.type === CONTENT_TYPE.APP_OS && new RegExp(CONTENT_TYPE.APP_OS, "i").test(ct)) {
139 | // console.log(xhr.getAllResponseHeaders())
140 | // 后端response.setHeader("Content-Disposition", "attachment; filename=xxxx.xxx") 设置的文件名;
141 | const contentDisposition = xhr.getResponseHeader('Content-Disposition');
142 | const contentType = xhr.getResponseHeader('Content-Type') || 'application/octet-stream';
143 | // let contentLength = result.headers["Content-Length"] || result.headers["content-length"];
144 | let filename;
145 | // 如果从Content-Disposition中取到的文件名不为空
146 | if (contentDisposition) {
147 | // 取出文件名,这里正则注意顺序 (.*)在|前如果有;号那么永远都会是真 把分号以及后面的字符取到
148 | filename = new RegExp("(?<=filename=)((.*)(?=;|%3B)|(.*))").exec(contentDisposition)[1];
149 | //filename = contentDisposition.split(/filename=/i)[1].split(";")[0];
150 | } else {
151 | const urls = xhr.responseURL.split("/");
152 | filename = urls[urls.length - 1];
153 | }
154 | // 解决中文乱码,编码格式
155 | filename = decodeURIComponent(filename);// 需要后端进行转义序列
156 | // filename = unescape(filename.replace(/\\/g, "%"));
157 | // filename = btoa(filename);
158 | const ael = document.createElement('a');
159 | ael.style.display = 'none';
160 | // 创建下载的链接
161 | // downloadElement.href = URL.createObjectURL(new Blob([xhr.response], {type: contentType}));
162 | ael.href = URL.createObjectURL(xhr.response);
163 | // 下载后文件名
164 | ael.download = filename;
165 | // 点击下载
166 | ael.click();
167 | // 释放掉blob对象
168 | URL.revokeObjectURL(ael.href);
169 | ael.remove();
170 | } else if (xhr.response.type === CONTENT_TYPE.APP_JSON) { // 如果服务器返回JSON
171 | const reader = new FileReader();
172 | reader.readAsText(xhr.response, 'UTF-8');
173 | reader.onload = () => {
174 | config.success(JSON.parse(reader.result), xhr.status, xhr);
175 | }
176 | } else { // 失败返回信息
177 | config.error(xhr, xhr.status, e);
178 | }
179 | } else {
180 | config.success(xhr.response, xhr.status, xhr);
181 | }
182 | });
183 | // 请求结束,对应xhr.onloadend
184 | xhr.addEventListener('loadend', e => {
185 | config.complete(xhr, xhr.status, e);
186 | });
187 | // 请求出错,对应xhr.onerror
188 | xhr.addEventListener('error', e => {
189 | config.error(xhr, xhr.status, e);
190 | });
191 | // 请求超时,对应xhr.ontimeout
192 | xhr.addEventListener('timeout', e => {
193 | config.error(xhr, 408, e);
194 | });
195 |
196 | // 上传文件进度
197 | const progressBar = document.querySelector('progress');
198 | xhr.upload.onprogress = function (e) {
199 | if (e.lengthComputable) {
200 | progressBar.value = (e.loaded / e.total) * 100;
201 | // 兼容不支持