├── 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?"
':"")+'
"+l+'
'+n.content+"
"+c+"
",!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 |
56 |
57 |

NGINX配置格式化

58 |

支持NGINX的配置格式化

59 |
60 |
61 |
62 |
63 | 在线 65 | 66 | 离线 68 | 69 | 格式化 71 |
72 |
73 |

缩进值:

74 | 78 |

缩进方式:

79 | 82 | 84 | 87 |
88 | 89 |
90 | 91 | 92 |
93 |
94 |
95 | 104 |
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 |
81 |

Xshell / Xftp 激活下载

82 |

支持Xshell、Xftp、Xshell Plus、Xmanager Power Suite各版本激活下载

83 |
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 |
128 |
129 | 130 | 135 | 136 | 144 | 145 | 148 | 150 | 点击获取秘钥 151 | 152 |
153 |
154 | 点击获取下载链接 157 |
158 |
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 | // 兼容不支持 元素的老式浏览器 202 | progressBar.textContent = progressBar.value; 203 | } 204 | }; 205 | 206 | const method = config.method.toUpperCase(); 207 | // 如果是"简单"请求,则把data参数组装在url上 208 | if ((method === 'GET' || method === 'DELETE') && config.data) { 209 | let paramsStr; 210 | if ((config.data).constructor === Object) { 211 | let paramsArr = []; 212 | Object.keys(config.data).forEach(key => { 213 | paramsArr.push(`${encodeURIComponent(key)}=${encodeURIComponent(config.data[key])}`); 214 | }); 215 | paramsStr = paramsArr.join('&'); 216 | } else if ((config.data).constructor === String) { 217 | paramsStr = config.data; 218 | } else if ((config.data).constructor === Array) { 219 | paramsStr = config.data.join("&"); 220 | } else { 221 | throw new TypeError("ajax请求:数据类型错误!"); 222 | } 223 | config.url += (config.url.indexOf('?') !== -1) ? paramsStr : '?' + paramsStr; 224 | } 225 | 226 | // 初始化请求 227 | xhr.open(config.method, config.url, config.async); 228 | // 设置请求头,必须要放到open()后面 229 | for (const key of Object.keys(config.headers)) { 230 | xhr.setRequestHeader(key, config.headers[key]); 231 | } 232 | // 设置超时时间 233 | if (config.timeout) { 234 | xhr.timeout = config.timeout; 235 | } 236 | // 设置预期服务器返回数据类型,并进行本地解析 237 | xhr.responseType = config.responseType; 238 | 239 | // 请求参数类型需要和请求头Content-Type对应'PUT','POST','PATCH' 240 | if ((method === 'PUT' || method === 'POST' || method === 'PATCH') && config.data) { 241 | const ct = config.headers["Content-Type"].split(";")[0].toLocaleLowerCase(); 242 | if (ct === "application/x-www-form-urlencoded") { 243 | if ((config.data).constructor !== Object) { 244 | throw new TypeError("ajax请求:application/x-www-form-urlencoded数据类型错误!"); 245 | } 246 | const paramsArr = []; 247 | Object.keys(config.data).forEach(key => { 248 | paramsArr.push(`${encodeURIComponent(key)}=${encodeURIComponent(config.data[key])}`); 249 | }); 250 | xhr.send(paramsArr.join('&')); 251 | } else if (ct === "multipart/form-data") { 252 | if ((config.data).constructor !== Object) { 253 | throw new TypeError("ajax请求:multipart/form-data数据类型错误!"); 254 | } 255 | const formData = new FormData(); 256 | Object.keys(config.data).forEach(key => { 257 | formData.append(key, config.data[key]); 258 | }); 259 | xhr.send(formData); 260 | } else if (ct === "text/plain") { 261 | if ((config.data).constructor !== String) { 262 | throw new TypeError("ajax请求:text/plain数据类型错误!"); 263 | } 264 | xhr.send(config.data); 265 | } else if (ct === "application/json") { 266 | if ((config.data).constructor === String) { 267 | try { 268 | JSON.parse(config.data); 269 | xhr.send(config.data); 270 | } catch (e) { 271 | throw new TypeError("ajax请求:application/json数据类型错误!"); 272 | } 273 | } else if ((config.data).constructor === Array || (config.data).constructor === Object) { 274 | xhr.send(JSON.stringify(config.data)); 275 | } else { 276 | throw new TypeError("ajax请求:application/json数据类型错误!"); 277 | } 278 | } else { 279 | throw new TypeError("ajax请求:数据类型错误!"); 280 | } 281 | } else { 282 | xhr.send(); 283 | } 284 | } 285 | 286 | /** 287 | * export default 服从 ES6 的规范,补充:default 其实是别名 288 | * module.exports 服从 CommonJS 规范 https://javascript.ruanyifeng.com/nodejs/module.html 289 | * 一般导出一个属性或者对象用 export default 290 | * 一般导出模块或者说文件使用 module.exports 291 | * 292 | * import from 服从ES6规范,在编译器生效 293 | * require 服从ES5 规范,在运行期生效 294 | * 目前 vue 编译都是依赖label 插件,最终都转化为ES5 295 | * 296 | * @return 将方法、变量暴露出去 297 | */ 298 | export default { 299 | METHOD, CONTENT_TYPE, RESPONSE_TYPE, ajax 300 | } --------------------------------------------------------------------------------