├── .dockerignore ├── .github └── workflows │ └── docker-build.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── README_zh.md ├── app.json ├── cmd └── ipasd │ ├── doc.go │ ├── ipasd.go │ └── service │ ├── appinfo.go │ ├── middleware.go │ ├── plist.go │ ├── plist_test.go │ ├── service.go │ └── transport.go ├── docker-compose.yml ├── docker-entrypoint.sh ├── go.mod ├── go.sum ├── heroku.yml ├── pkg ├── apk │ ├── apk.go │ ├── package_info.go │ └── package_info_test.go ├── common │ └── common.go ├── http_basic_auth │ └── http_basic_auth.go ├── httpfs │ ├── afero.go │ └── httpfs.go ├── ipa │ ├── ipa.go │ ├── ipa_test.go │ ├── package_info.go │ └── test_data │ │ └── ipa.ipa ├── multipart │ └── multipart.go ├── plist │ └── plist.go ├── seekbuf │ ├── seekbuf.go │ └── seekbuf_test.go ├── storager │ ├── afero.go │ ├── alioss.go │ ├── alioss_test.go │ ├── basepath.go │ ├── helper │ │ └── helper.go │ ├── qiniu.go │ ├── qiniu_test.go │ ├── s3.go │ ├── s3_test.go │ ├── storager.go │ └── test.go ├── uuid │ └── uuid.go └── websocketfile │ └── websocketfile.go ├── public ├── app │ └── index.html ├── css │ └── core.css ├── img │ ├── android.svg │ ├── default.png │ └── ios.svg ├── index.html ├── js │ ├── core.js │ ├── dayjs.min.js │ ├── dayjs.relativeTime.min.js │ ├── dayjs.zh-cn.min.js │ ├── layzr.min.js │ └── qrcode.min.js └── public.go └── snapshot ├── en ├── 1.jpg └── 2.jpg └── zh-cn ├── 1.jpg └── 2.jpg /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | snapshot/ 3 | upload/ 4 | *.md 5 | .gitignore 6 | 7 | /ipasd 8 | /ipas -------------------------------------------------------------------------------- /.github/workflows/docker-build.yml: -------------------------------------------------------------------------------- 1 | name: Publish Docker image 2 | on: 3 | push: 4 | branches: 5 | - main 6 | release: 7 | types: [published] 8 | 9 | jobs: 10 | push_to_registry: 11 | name: Push Docker image to GitHub Packages 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v2 16 | 17 | - name: Login to DockerHub 18 | uses: docker/login-action@v1 19 | with: 20 | username: ${{ secrets.DOCKERHUB_USERNAME }} 21 | password: ${{ secrets.DOCKERHUB_TOKEN }} 22 | 23 | - if: github.event_name == 'release' 24 | name: Build and push release 25 | uses: docker/build-push-action@v2 26 | with: 27 | context: . 28 | push: true 29 | tags: ineva/ipa-server:latest,ineva/ipa-server:${{ github.event.release.tag_name }} 30 | 31 | - if: github.ref == 'refs/heads/main' 32 | name: Build and push dev 33 | uses: docker/build-push-action@v2 34 | with: 35 | context: . 36 | push: true 37 | tags: ineva/ipa-server:dev 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .DS_Store 3 | node_modules/ 4 | 5 | upload/ 6 | .idea/ 7 | /ipasd 8 | /ipas 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # builder 2 | FROM golang:1.22.6 AS builder 3 | WORKDIR /src/ 4 | COPY go.mod /src/ 5 | COPY go.sum /src/ 6 | RUN --mount=type=cache,id=gomod,target=/go/pkg/mod \ 7 | --mount=type=cache,id=gobuild,target=/root/.cache/go-build \ 8 | go mod download && \ 9 | go mod tidy 10 | COPY . /src/ 11 | RUN --mount=type=cache,id=gomod,target=/go/pkg/mod \ 12 | --mount=type=cache,id=gobuild,target=/root/.cache/go-build \ 13 | CGO_ENABLED=1 go build -ldflags '-linkmode "external" --extldflags "-static"' cmd/ipasd/ipasd.go 14 | 15 | # runtime 16 | FROM ineva/alpine:3.10.3 17 | LABEL maintainer="Steven " 18 | WORKDIR /app 19 | COPY --from=builder /src/ipasd /app 20 | COPY docker-entrypoint.sh /docker-entrypoint.sh 21 | RUN chmod +x /docker-entrypoint.sh 22 | ENTRYPOINT /docker-entrypoint.sh 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Steven 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION := 2.5.5 2 | DOCKER_IMAGE := ineva/ipa-server 3 | DOCKER_TARGET := $(DOCKER_IMAGE):$(VERSION) 4 | 5 | all:: web 6 | 7 | web:: 8 | go run cmd/ipasd/ipasd.go -del 9 | 10 | debug:: 11 | go run cmd/ipasd/ipasd.go -d -del 12 | 13 | build:: 14 | go build cmd/ipasd/ipasd.go 15 | 16 | test:: 17 | go test ./... 18 | 19 | image:: 20 | docker build --platform linux/amd64 -t $(DOCKER_TARGET) . 21 | 22 | push:: 23 | docker push $(DOCKER_TARGET) 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ipa-server 2 | 3 | ipa-server is updated to v2, to [older version v1](https://github.com/iineva/ipa-server/tree/v1) 4 | 5 | Upload and install Apple `.ipa` and Android `.apk` in web. 6 | 7 | # Demo 8 | 9 | 10 | 11 | ## Key features 12 | 13 | - Automatic parse packet information 14 | - Automatically generate icons 15 | - Parse icons from `Assets.car` 16 | - Out of the box 17 | - Free depoly, use `Heroku` as runtime and `Ali OSS` as storage, Both of them provide HTTPS access for free 18 | - The generated files are completely stored in external storage. Currently, it supports `S3` `Qiniu` `Alibaba Cloud OSS` 19 | - A single binary build-in all you need 20 | 21 | - [中文文档](README_zh.md) 22 | 23 | | Home | Detail | 24 | | ---------------------- | ---------------------- | 25 | | ![](snapshot/en/1.jpg) | ![](snapshot/en/2.jpg) | 26 | 27 | # Install for local trial 28 | 29 | ```shell 30 | # clone 31 | git clone https://github.com/iineva/ipa-server 32 | # build and start 33 | cd ipa-server 34 | docker-compose up -d 35 | # than open http://localhost:9008 in your browser. 36 | ``` 37 | 38 | # Heroku Deploy 39 | 40 | ### config 41 | 42 | - PUBLIC_URL: public URL for this server, empty to use `$DOMAIN` 43 | - REMOTE: remote storager config, `s3://ENDPOINT:AK:SK:BUCKET` `alioss://ENDPOINT:AK:SK:BUCKET` `qiniu://[ZONE]:AK:SK:BUCKET` 44 | - REMOTE_URL: remote storager public url, https://cdn.example.com 45 | - DELETE_ENABLED: delete app enabled, `true` `false` 46 | 47 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/iineva/ipa-server) 48 | 49 | # Docker Deploy 50 | 51 | - This server is not included SSL certificate. It must run behide the reverse proxy with HTTPS. 52 | 53 | - After deployed, you can access _https://\_ in your browser. 54 | 55 | - There is a simple way to setup a HTTPS with replace `docker-compose.yml` file: 56 | 57 | ``` 58 | 59 | # ***** Replace ALL to you really domain ***** 60 | 61 | version: "3" 62 | services: 63 | web: 64 | image: ineva/ipa-server:latest 65 | container_name: ipa-server 66 | restart: unless-stopped 67 | environment: 68 | # server public url 69 | - PUBLIC_URL=https:// 70 | # option, remote storager config, s3://ENDPOINT:AK:SK:BUCKET, alioss://ENDPOINT:AK:SK:BUCKET, qiniu://[ZONE]:AK:SK:BUCKET 71 | - REMOTE= 72 | # option, remote storager public url, https://cdn.example.com 73 | - REMOTE_URL= 74 | # option, metadata storage path, use random secret path to keep your metadata safer in case of remote storage 75 | - META_PATH=appList.json 76 | # delete app enabled, true/false 77 | - DELETE_ENABLED="false" 78 | # upload app disabled, true/false 79 | - UPLOAD_DISABLED="true" 80 | # meta data filter, string list, comma separated 81 | - META_DATA_FILTER="key1,key2" 82 | # If set, login user for upload and delete Apps. 83 | - LOGIN_USER= 84 | - LOGIN_PASS= 85 | volumes: 86 | - "/docker/data/ipa-server:/app/upload" 87 | caddy: 88 | image: ineva/caddy:2.4.1 89 | restart: unless-stopped 90 | ports: 91 | - 80:80 92 | - 443:443 93 | entrypoint: | 94 | sh -c 'echo "$$CADDY_CONFIG" > /srv/Caddyfile && /usr/bin/caddy run --config /srv/Caddyfile' 95 | environment: 96 | CADDY_CONFIG: | 97 | { 98 | reverse_proxy web:8080 99 | } 100 | ``` 101 | 102 | # Build or run from source code 103 | 104 | ```shell 105 | # install golang v1.16 first 106 | git clone https://github.com/iineva/ipa-server 107 | # build and start 108 | cd ipa-server 109 | # build binary 110 | make build 111 | # run local server 112 | make 113 | ``` 114 | -------------------------------------------------------------------------------- /README_zh.md: -------------------------------------------------------------------------------- 1 | # ipa-server 2 | 3 | ipa-server 已经更新到 v2, 使用 golang 重构, [老版本 v1](https://github.com/iineva/ipa-server/tree/v1) 4 | 5 | 使用浏览器上传和部署 苹果 `.ipa` 和 安卓 `.apk` 文件 6 | 7 | # Demo 8 | 9 | 10 | 11 | ## 关键特性 12 | 13 | - 自动识别包内信息 14 | - 自动读取图标 15 | - 支持解析`ipa`文件`Assets.car`内图标 16 | - 开箱即用 17 | - 可完全免费一键部署,使用`Heroku`作为 runtime,`阿里OSS`做存储器,他们都提供免费的 HTTPS 访问 18 | - 支持生成文件完全存储在外部存储,目前支持 `S3` `七牛对象存储` `阿里云OSS` 19 | - 单二进制文件包含所有运行所需 20 | 21 | | Home | Detail | 22 | | ------------------------- | ------------------------- | 23 | | ![](snapshot/zh-cn/1.jpg) | ![](snapshot/zh-cn/2.jpg) | 24 | 25 | # 安装本地试用 26 | 27 | ```shell 28 | # clone 29 | git clone https://github.com/iineva/ipa-server 30 | # build and start 31 | cd ipa-server 32 | docker-compose up -d 33 | # 启动后在浏览器打开 http://localhost:9008 34 | ``` 35 | 36 | # Heroku 部署 37 | 38 | ### 配置 39 | 40 | - PUBLIC_URL: 本服务的公网 URL, 如果为空试用 Heroku 默认的 `$DOMAIN` 41 | - REMOTE: option, 远程存储配置, `s3://ENDPOINT:AK:SK:BUCKET` `alioss://ENDPOINT:AK:SK:BUCKET` `qiniu://[ZONE]:AK:SK:BUCKET` 42 | - REMOTE_URL: option, 远程存储访问 URL, 注意需要开启 HTTPS 支持 iOS 才能正常安装!例子:https://cdn.example.com 43 | - DELETE_ENABLED: 是否开启删除 APP 功能 `true` `false` 44 | 45 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/iineva/ipa-server) 46 | 47 | # 正式部署 48 | 49 | - 本仓库代码不包含 SSL 证书部分,由于苹果在线安装必须具备 HTTPS,所以本程序必须运行在 HTTPS 反向代理后端。 50 | 51 | - 部署后,你可以使用浏览器访问 _https://\_ 52 | 53 | - 最简单的办法开启完整服务,使用下面的配置替换 `docker-compose.yml` 文件: 54 | 55 | ``` 56 | 57 | # ***** 更换所有 成你的真实域名 ***** 58 | 59 | version: "3" 60 | services: 61 | web: 62 | image: ineva/ipa-server:latest 63 | container_name: ipa-server 64 | restart: unless-stopped 65 | environment: 66 | # 本服务公网IP 67 | - PUBLIC_URL=https:// 68 | # option, 远程存储配置, s3://ENDPOINT:AK:SK:BUCKET, alioss://ENDPOINT:AK:SK:BUCKET, qiniu://[ZONE]:AK:SK:BUCKET 69 | - REMOTE= 70 | # option, 远程存储访问URL, https://cdn.example.com 71 | - REMOTE_URL= 72 | # option, 元数据存储路径, 使用一个随机路径来保护元数据,因为在使用远程存储的时候,没有更好的方法防止外部直接访问元数据文件 73 | - META_PATH=appList.json 74 | # 是否开启删除APP功能, true/false 75 | - DELETE_ENABLED="false" 76 | # 是否关闭APP上传功能, true/false 77 | - UPLOAD_DISABLED="true" 78 | # meta data 过滤显示, string list, 使用逗号分隔 79 | - META_DATA_FILTER="key1,key2" 80 | # 如果设置了,使用此用户名密码来上传和删除App 81 | - LOGIN_USER= 82 | - LOGIN_PASS= 83 | volumes: 84 | - "/docker/data/ipa-server:/app/upload" 85 | caddy: 86 | image: ineva/caddy:2.4.1 87 | restart: unless-stopped 88 | ports: 89 | - 80:80 90 | - 443:443 91 | entrypoint: | 92 | sh -c 'echo "$$CADDY_CONFIG" > /srv/Caddyfile && /usr/bin/caddy run --config /srv/Caddyfile' 93 | environment: 94 | CADDY_CONFIG: | 95 | { 96 | reverse_proxy web:8080 97 | } 98 | ``` 99 | 100 | # 源码编译 101 | 102 | ```shell 103 | # install golang v1.16 first 104 | git clone https://github.com/iineva/ipa-server 105 | # build and start 106 | cd ipa-server 107 | # build binary 108 | make build 109 | # run local server 110 | make 111 | ``` 112 | 113 | # TODO 114 | 115 | - [ ] 设计全新的鉴权方式,初步考虑试用 GitHub 登录鉴权 116 | - [x] 支持七牛存储 117 | - [x] 支持阿里云 OSS 存储 118 | - [x] 支持 S3 存储 119 | - [x] 兼容 v1 产生数据,无缝升级 120 | - [ ] 支持命令行生成静态文件部署 121 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ipa-server", 3 | "description": "Deploy ipa-server on Heroku.", 4 | "keywords": [ 5 | "ipa-server" 6 | ], 7 | "env": { 8 | "PUBLIC_URL": { 9 | "description": "public URL for this server, empty to use $DOMAIN", 10 | "required": false, 11 | "value": "" 12 | }, 13 | "REMOTE": { 14 | "description": "remote storager config, s3://ENDPOINT:AK:SK:BUCKET, alioss://ENDPOINT:AK:SK:BUCKET, qiniu://[ZONE]:AK:SK:BUCKET", 15 | "required": false, 16 | "value": "" 17 | }, 18 | "REMOTE_URL": { 19 | "description": "remote storager public url, https://cdn.example.com", 20 | "required": false, 21 | "value": "" 22 | }, 23 | "DELETE_ENABLED": { 24 | "description": "delete app enabled, true/false", 25 | "required": false, 26 | "value": "false" 27 | }, 28 | "META_PATH": { 29 | "description": "option, metadata storage path, use random secret path to keep your metadata safer in case of remote storage", 30 | "required": false, 31 | "value": "" 32 | } 33 | }, 34 | "website": "https://github.com/iineva/ipa-server", 35 | "repository": "https://github.com/iineva/ipa-server", 36 | "stack": "container" 37 | } -------------------------------------------------------------------------------- /cmd/ipasd/doc.go: -------------------------------------------------------------------------------- 1 | // Package ipa-server's web servie 2 | package main 3 | -------------------------------------------------------------------------------- /cmd/ipasd/ipasd.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "net/http" 7 | "os" 8 | "strings" 9 | "time" 10 | 11 | "github.com/go-kit/kit/log" 12 | "github.com/spf13/afero" 13 | 14 | httptransport "github.com/go-kit/kit/transport/http" 15 | "github.com/iineva/ipa-server/cmd/ipasd/service" 16 | "github.com/iineva/ipa-server/pkg/common" 17 | "github.com/iineva/ipa-server/pkg/http_basic_auth" 18 | "github.com/iineva/ipa-server/pkg/httpfs" 19 | "github.com/iineva/ipa-server/pkg/storager" 20 | "github.com/iineva/ipa-server/pkg/uuid" 21 | "github.com/iineva/ipa-server/pkg/websocketfile" 22 | "github.com/iineva/ipa-server/public" 23 | ) 24 | 25 | func redirect(m map[string]string, next http.Handler) http.Handler { 26 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 27 | p, ok := m[r.URL.Path] 28 | if ok { 29 | r.URL.Path = p 30 | } 31 | next.ServeHTTP(w, r) 32 | }) 33 | } 34 | 35 | func main() { 36 | 37 | addr := flag.String("addr", "0.0.0.0", "bind addr") 38 | port := flag.String("port", "8080", "bind port") 39 | debug := flag.Bool("d", false, "enable debug logging") 40 | user := flag.String("user", "", "basic auth username") 41 | pass := flag.String("pass", "", "basic auth password") 42 | storageDir := flag.String("dir", "upload", "upload data storage dir") 43 | publicURL := flag.String("public-url", "", "server public url") 44 | metadataPath := flag.String("meta-path", "appList.json", "metadata storage path, use random secret path to keep your metadata safer") 45 | deleteEnabled := flag.Bool("del", false, "delete app enabled") 46 | uploadDisabled := flag.Bool("upload-disabled", false, "upload app enabled") 47 | remoteCfg := flag.String("remote", "", "remote storager config, s3://ENDPOINT:AK:SK:BUCKET, alioss://ENDPOINT:AK:SK:BUCKET, qiniu://[ZONE]:AK:SK:BUCKET") 48 | remoteURL := flag.String("remote-url", "", "remote storager public url, https://cdn.example.com") 49 | realm := "My Realm" 50 | 51 | flag.Usage = usage 52 | flag.Parse() 53 | 54 | serve := http.NewServeMux() 55 | 56 | logger := log.NewLogfmtLogger(os.Stderr) 57 | logger = log.With(logger, "ts", log.TimestampFormat(time.Now, "2006-01-02 15:04:05.000"), "caller", log.DefaultCaller) 58 | 59 | var store storager.Storager 60 | if *remoteCfg != "" && *remoteURL != "" { 61 | r := strings.Split(*remoteCfg, "://") 62 | if len(r) != 2 { 63 | usage() 64 | os.Exit(0) 65 | } 66 | args := strings.Split(r[1], ":") 67 | if len(args) != 4 { 68 | usage() 69 | os.Exit(0) 70 | } 71 | 72 | switch r[0] { 73 | case "s3": 74 | logger.Log("msg", "used s3 storager") 75 | s, err := storager.NewS3Storager(args[0], args[1], args[2], args[3], *remoteURL) 76 | if err != nil { 77 | panic(err) 78 | } 79 | store = s 80 | case "alioss": 81 | logger.Log("msg", "used alioss storager") 82 | s, err := storager.NewAliOssStorager(args[0], args[1], args[2], args[3], *remoteURL) 83 | if err != nil { 84 | panic(err) 85 | } 86 | store = s 87 | case "qiniu": 88 | logger.Log("msg", "used qiniu storager") 89 | s, err := storager.NewQiniuStorager(args[0], args[1], args[2], args[3], *remoteURL) 90 | if err != nil { 91 | panic(err) 92 | } 93 | store = s 94 | } 95 | } else { 96 | logger.Log("msg", "used os file storager") 97 | store = storager.NewOsFileStorager(*storageDir) 98 | } 99 | 100 | srv := service.New(store, *publicURL, *metadataPath) 101 | basicAuth := service.BasicAuthMiddleware(*user, *pass, realm) 102 | listHandler := httptransport.NewServer( 103 | basicAuth(service.LoggingMiddleware(logger, "/api/list", *debug)(service.MakeListEndpoint(srv, !*uploadDisabled))), 104 | service.DecodeListRequest, 105 | service.EncodeJsonResponse, 106 | httptransport.ServerBefore(httptransport.PopulateRequestContext), 107 | ) 108 | findHandler := httptransport.NewServer( 109 | service.LoggingMiddleware(logger, "/api/info", *debug)(service.MakeFindEndpoint(srv)), 110 | service.DecodeFindRequest, 111 | service.EncodeJsonResponse, 112 | ) 113 | addHandler := httptransport.NewServer( 114 | basicAuth(service.LoggingMiddleware(logger, "/api/upload", *debug)(service.MakeAddEndpoint(srv, !*uploadDisabled))), 115 | service.DecodeAddRequest, 116 | service.EncodeJsonResponse, 117 | httptransport.ServerBefore(httptransport.PopulateRequestContext), 118 | ) 119 | deleteHandler := httptransport.NewServer( 120 | basicAuth(service.LoggingMiddleware(logger, "/api/delete", *debug)(service.MakeDeleteEndpoint(srv, *deleteEnabled))), 121 | service.DecodeDeleteRequest, 122 | service.EncodeJsonResponse, 123 | httptransport.ServerBefore(httptransport.PopulateRequestContext), 124 | ) 125 | deleteGetHandler := httptransport.NewServer( 126 | service.LoggingMiddleware(logger, "/api/delete/get", *debug)(service.MakeGetDeleteEndpoint(srv, *deleteEnabled)), 127 | service.DecodeDeleteRequest, 128 | service.EncodeJsonResponse, 129 | httptransport.ServerBefore(httptransport.PopulateRequestContext), 130 | ) 131 | plistHandler := httptransport.NewServer( 132 | service.LoggingMiddleware(logger, "/plist", *debug)(service.MakePlistEndpoint(srv)), 133 | service.DecodePlistRequest, 134 | service.EncodePlistResponse, 135 | ) 136 | 137 | // parser API 138 | serve.Handle("/api/list", listHandler) 139 | serve.Handle("/api/info/", findHandler) 140 | serve.Handle("/api/upload", addHandler) 141 | serve.Handle("/api/delete", deleteHandler) 142 | serve.Handle("/api/delete/get", deleteGetHandler) 143 | serve.Handle("/plist/", plistHandler) 144 | // upload file over Websocket 145 | serve.Handle("/api/upload/ws", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 146 | 147 | if *user != "" { 148 | if err := http_basic_auth.HandleBasicAuth(*user, *pass, realm, r); err != nil { 149 | logger.Log("msg", fmt.Sprintf("err: %v", err)) 150 | w.WriteHeader(http.StatusUnauthorized) 151 | return 152 | } 153 | } 154 | 155 | if *uploadDisabled { 156 | logger.Log("msg", "err: upload was disabled") 157 | w.WriteHeader(http.StatusUnauthorized) 158 | return 159 | } 160 | 161 | f, err := websocketfile.NewWebsocketFile(w, r) 162 | if err != nil { 163 | logger.Log("msg", fmt.Sprintf("err: %v", err)) 164 | return 165 | } 166 | 167 | size, err := f.Size() 168 | if err != nil { 169 | logger.Log("msg", fmt.Sprintf("err: %v", err)) 170 | return 171 | } 172 | 173 | name, err := f.Name() 174 | if err != nil { 175 | logger.Log("msg", fmt.Sprintf("err: %v", err)) 176 | return 177 | } 178 | t := service.FileType(name) 179 | 180 | logger.Log("name:", name, " size:", size) 181 | 182 | info, err := srv.Add(f, size, t) 183 | if err != nil { 184 | logger.Log("msg", fmt.Sprintf("err: %v", err)) 185 | return 186 | } 187 | 188 | err = f.Done(common.ToMap(info)) 189 | if err != nil { 190 | logger.Log("msg", fmt.Sprintf("err: %v", err)) 191 | return 192 | } 193 | 194 | })) 195 | 196 | // static files 197 | uploadFS := afero.NewBasePathFs(afero.NewOsFs(), *storageDir) 198 | staticFS := httpfs.New( 199 | http.FS(public.FS), 200 | httpfs.NewAferoFS(uploadFS), 201 | ) 202 | serve.Handle("/", redirect(map[string]string{ 203 | // random path to block local metadata 204 | fmt.Sprintf("/%s", *metadataPath): fmt.Sprintf("/%s", uuid.NewString()), 205 | }, http.FileServer(staticFS))) 206 | 207 | host := fmt.Sprintf("%s:%s", *addr, *port) 208 | logger.Log("msg", fmt.Sprintf("SERVER LISTEN ON: http://%v", host)) 209 | logger.Log("msg", http.ListenAndServe(host, serve)) 210 | } 211 | 212 | func usage() { 213 | fmt.Fprintf(os.Stderr, `Usage: ipa-server [options] 214 | Options: 215 | `) 216 | flag.PrintDefaults() 217 | } 218 | -------------------------------------------------------------------------------- /cmd/ipasd/service/appinfo.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "image" 5 | "path" 6 | "path/filepath" 7 | "strings" 8 | "time" 9 | 10 | "github.com/iineva/ipa-server/pkg/uuid" 11 | ) 12 | 13 | type AppInfoType int 14 | type AppInfo struct { 15 | ID string `json:"id"` 16 | Name string `json:"name"` 17 | Version string `json:"version"` 18 | Identifier string `json:"identifier"` 19 | Build string `json:"build"` 20 | Channel string `json:"channel"` 21 | Date time.Time `json:"date"` 22 | Size int64 `json:"size"` 23 | NoneIcon bool `json:"noneIcon"` 24 | Type AppInfoType `json:"type"` 25 | // Metadata 26 | MetaData map[string]interface{} `json:"metaData"` 27 | } 28 | 29 | const ( 30 | AppInfoTypeIpa = AppInfoType(0) 31 | AppInfoTypeApk = AppInfoType(1) 32 | AppInfoTypeUnknown = AppInfoType(-1) 33 | ) 34 | 35 | func (t AppInfoType) StorageName() string { 36 | switch t { 37 | case AppInfoTypeIpa: 38 | return ".ipa" 39 | case AppInfoTypeApk: 40 | return ".apk" 41 | default: 42 | return "unknown" 43 | } 44 | } 45 | 46 | func FileType(n string) AppInfoType { 47 | ext := strings.ToLower(path.Ext(n)) 48 | switch ext { 49 | case ".ipa": 50 | return AppInfoTypeIpa 51 | case ".apk": 52 | return AppInfoTypeApk 53 | default: 54 | return AppInfoTypeUnknown 55 | } 56 | } 57 | 58 | type AppList []*AppInfo 59 | 60 | func (a AppList) Len() int { return len(a) } 61 | func (a AppList) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 62 | func (a AppList) Less(i, j int) bool { return a[i].Date.After(a[j].Date) } 63 | 64 | type Package interface { 65 | Name() string 66 | Version() string 67 | Identifier() string 68 | Build() string 69 | Channel() string 70 | MetaData() map[string]interface{} 71 | Icon() image.Image 72 | Size() int64 73 | } 74 | 75 | func NewAppInfo(i Package, t AppInfoType) *AppInfo { 76 | return &AppInfo{ 77 | ID: uuid.NewString(), 78 | Name: i.Name(), 79 | Version: i.Version(), 80 | Identifier: i.Identifier(), 81 | Build: i.Build(), 82 | Channel: i.Channel(), 83 | MetaData: i.MetaData(), 84 | Date: time.Now(), 85 | Size: i.Size(), 86 | Type: t, 87 | NoneIcon: i.Icon() == nil, 88 | } 89 | } 90 | 91 | func (a *AppInfo) IconStorageName() string { 92 | if a.NoneIcon { 93 | return "" 94 | } 95 | return filepath.Join(a.Identifier, a.ID+".png") 96 | } 97 | 98 | func (a *AppInfo) PackageStorageName() string { 99 | return filepath.Join(a.Identifier, a.ID+a.Type.StorageName()) 100 | } 101 | -------------------------------------------------------------------------------- /cmd/ipasd/service/middleware.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | stdLog "log" 7 | "time" 8 | 9 | basic "github.com/go-kit/kit/auth/basic" 10 | "github.com/go-kit/kit/endpoint" 11 | "github.com/go-kit/kit/log" 12 | "github.com/go-kit/kit/log/level" 13 | ) 14 | 15 | func LoggingMiddleware(logger log.Logger, name string, debug bool) endpoint.Middleware { 16 | return func(next endpoint.Endpoint) endpoint.Endpoint { 17 | var logging log.Logger 18 | if debug { 19 | logging = level.NewFilter(logger, level.AllowDebug()) 20 | } else { 21 | logging = level.NewFilter(logger, level.AllowInfo()) 22 | } 23 | return func(ctx context.Context, request interface{}) (response interface{}, err error) { 24 | level.Info(logging).Log( 25 | "modle", name, 26 | "status", "start", 27 | "request", fmt.Sprintf("%+v", request), 28 | ) 29 | defer func(begin time.Time) { 30 | level.Debug(logging).Log( 31 | "modle", name, 32 | "status", "done", 33 | "request", fmt.Sprintf("%+v", request), 34 | "response", fmt.Sprintf("%+v", response), 35 | ) 36 | if err != nil { 37 | level.Info(logging).Log( 38 | "err", err, 39 | "modle", name, 40 | "status", "done", 41 | "took", time.Since(begin), 42 | ) 43 | } else { 44 | level.Info(logging).Log( 45 | "modle", name, 46 | "status", "done", 47 | "took", time.Since(begin), 48 | ) 49 | } 50 | }(time.Now()) 51 | response, err = next(ctx, request) 52 | return 53 | } 54 | } 55 | } 56 | 57 | func BasicAuthMiddleware(user, pass, realm string) endpoint.Middleware { 58 | if user == "" { 59 | return func(e endpoint.Endpoint) endpoint.Endpoint { 60 | return e 61 | } 62 | } 63 | stdLog.Println(user, pass, realm) 64 | return basic.AuthMiddleware(user, pass, realm) 65 | } 66 | -------------------------------------------------------------------------------- /cmd/ipasd/service/plist.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "text/template" 7 | ) 8 | 9 | const plistTpl = ` 10 | 11 | 12 | items 13 | 14 | 15 | assets 16 | 17 | 18 | kind 19 | software-package 20 | url 21 | {{ .Pkg }} 22 | 23 | 24 | kind 25 | display-image 26 | needs-shine 27 | 28 | url 29 | {{ .Icon }} 30 | 31 | 32 | metadata 33 | 34 | bundle-identifier 35 | {{ .Identifier }} 36 | bundle-version 37 | {{ .Version }} 38 | kind 39 | software 40 | title 41 | {{ .Name }} 42 | 43 | 44 | 45 | 46 | ` 47 | 48 | var defaultTemplate, _ = template.New("install-plist").Parse(plistTpl) 49 | 50 | func NewInstallPlist(app *Item) ([]byte, error) { 51 | buf := bytes.NewBufferString("") 52 | err := defaultTemplate.Execute(buf, app) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | d, err := ioutil.ReadAll(buf) 58 | if err != nil { 59 | return nil, err 60 | } 61 | return d, nil 62 | } 63 | -------------------------------------------------------------------------------- /cmd/ipasd/service/plist_test.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | ) 7 | 8 | const targetData = ` 9 | 10 | 11 | items 12 | 13 | 14 | assets 15 | 16 | 17 | kind 18 | software-package 19 | url 20 | https://file.example.com/ipa.ipa 21 | 22 | 23 | kind 24 | display-image 25 | needs-shine 26 | 27 | url 28 | https://file.example.com/icon.png 29 | 30 | 31 | metadata 32 | 33 | bundle-identifier 34 | com.ineva.test 35 | bundle-version 36 | 1.0 37 | kind 38 | software 39 | title 40 | Test 41 | 42 | 43 | 44 | 45 | ` 46 | 47 | func TestNewInstallPlist(t *testing.T) { 48 | app := &Item{} 49 | app.Name = "Test" 50 | app.Version = "1.0" 51 | app.Identifier = "com.ineva.test" 52 | app.Icon = "https://file.example.com/icon.png" 53 | app.Pkg = "https://file.example.com/ipa.ipa" 54 | 55 | d, err := NewInstallPlist(app) 56 | if err != nil { 57 | t.Fatal(err) 58 | } 59 | 60 | if string(d) != targetData { 61 | t.Fatal(errors.New("created install plist not match")) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /cmd/ipasd/service/service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "image/png" 9 | "io" 10 | "io/ioutil" 11 | "net/url" 12 | "os" 13 | "path/filepath" 14 | "sort" 15 | "strings" 16 | "sync" 17 | "time" 18 | 19 | "github.com/iineva/ipa-server/pkg/apk" 20 | "github.com/iineva/ipa-server/pkg/ipa" 21 | "github.com/iineva/ipa-server/pkg/storager" 22 | "github.com/iineva/ipa-server/pkg/uuid" 23 | ) 24 | 25 | var ( 26 | ErrIdNotFound = errors.New("id not found") 27 | ) 28 | 29 | const ( 30 | tempDir = ".ipa_parser_temp" 31 | ) 32 | 33 | // Item to use on web interface 34 | type Item struct { 35 | // from AppInfo 36 | ID string `json:"id"` 37 | Name string `json:"name"` 38 | Date time.Time `json:"date"` 39 | Size int64 `json:"size"` 40 | Channel string `json:"channel"` 41 | Build string `json:"build"` 42 | Version string `json:"version"` 43 | Identifier string `json:"identifier"` 44 | 45 | MetaData map[string]interface{} `json:"metaData"` 46 | MetaDataFilter []string `json:"metaDataFilter"` 47 | 48 | // package download link 49 | Pkg string `json:"pkg"` 50 | // Icon to display on iOS desktop 51 | Icon string `json:"icon"` 52 | // Plist to install ipa 53 | Plist string `json:"plist,omitempty"` 54 | // WebIcon to display on web 55 | WebIcon string `json:"webIcon"` 56 | // Type 0:ios 1:android 57 | Type AppInfoType `json:"type"` 58 | 59 | Current bool `json:"current"` 60 | History []*Item `json:"history,omitempty"` 61 | } 62 | 63 | func (i *Item) String() string { 64 | return fmt.Sprintf("%+v", *i) 65 | } 66 | 67 | type Service interface { 68 | List(publicURL string, uploadDisabled bool) (map[string]interface{}, error) 69 | Find(id string, publicURL string) (*Item, error) 70 | History(id string, publicURL string) ([]*Item, error) 71 | Delete(id string) error 72 | Add(r Reader, size int64, t AppInfoType) (*AppInfo, error) 73 | Plist(id, publicURL string) ([]byte, error) 74 | } 75 | 76 | type Reader interface { 77 | io.Reader 78 | io.ReaderAt 79 | } 80 | 81 | type service struct { 82 | list AppList 83 | lock sync.RWMutex 84 | store storager.Storager 85 | publicURL string 86 | metadataName string 87 | } 88 | 89 | func New(store storager.Storager, publicURL, metadataName string) Service { 90 | s := &service{ 91 | store: store, 92 | list: AppList{}, 93 | publicURL: publicURL, // use set public url 94 | metadataName: metadataName, 95 | } 96 | if err := s.tryMigrateOldData(); err != nil { 97 | // NOTE: ignore error 98 | } 99 | return s 100 | } 101 | 102 | func (s *service) List(publicURL string, uploadDisabled bool) (map[string]interface{}, error) { 103 | s.lock.RLock() 104 | defer s.lock.RUnlock() 105 | list := []*Item{} 106 | for _, row := range s.list { 107 | has := false 108 | for _, i := range list { 109 | if i.Identifier == row.Identifier { 110 | has = true 111 | break 112 | } 113 | } 114 | if has { 115 | continue 116 | } 117 | item := s.itemInfo(row, publicURL) 118 | item.History = s.history(row, publicURL) 119 | list = append(list, item) 120 | } 121 | return map[string]interface{}{ 122 | "list": list, 123 | "uploadDisabled": uploadDisabled, 124 | }, nil 125 | } 126 | 127 | func (s *service) Find(id string, publicURL string) (*Item, error) { 128 | s.lock.RLock() 129 | defer s.lock.RUnlock() 130 | app, err := s.find(id) 131 | if err != nil { 132 | return nil, err 133 | } 134 | 135 | item := s.itemInfo(app, publicURL) 136 | item.History = s.history(app, publicURL) 137 | return item, nil 138 | } 139 | 140 | func (s *service) History(id string, publicURL string) ([]*Item, error) { 141 | s.lock.RLock() 142 | defer s.lock.RUnlock() 143 | app, err := s.find(id) 144 | if err != nil { 145 | return nil, err 146 | } 147 | return s.history(app, publicURL), nil 148 | } 149 | 150 | func (s *service) Delete(id string) error { 151 | s.lock.Lock() 152 | var app *AppInfo 153 | for i, a := range s.list { 154 | if a.ID == id { 155 | app = a 156 | s.list = append(s.list[:i], s.list[i+1:]...) 157 | break 158 | } 159 | } 160 | s.lock.Unlock() 161 | 162 | if app == nil { 163 | return ErrIdNotFound 164 | } 165 | 166 | if err := s.saveMetadata(); err != nil { 167 | return err 168 | } 169 | 170 | if err := s.store.Delete(app.PackageStorageName()); err != nil { 171 | return err 172 | } 173 | if !app.NoneIcon { 174 | if err := s.store.Delete(app.IconStorageName()); err != nil { 175 | return err 176 | } 177 | } 178 | return nil 179 | } 180 | 181 | func (s *service) Add(r Reader, size int64, t AppInfoType) (*AppInfo, error) { 182 | 183 | app, err := s.addPackage(r, size, t) 184 | if err != nil { 185 | return nil, err 186 | } 187 | 188 | // update list 189 | s.lock.Lock() 190 | s.list = append([]*AppInfo{app}, s.list...) 191 | s.lock.Unlock() 192 | 193 | return app, s.saveMetadata() 194 | } 195 | 196 | func (s *service) addPackage(r Reader, size int64, t AppInfoType) (*AppInfo, error) { 197 | // save ipa file to temp 198 | pkgTempFileName := filepath.Join(tempDir, uuid.NewString()) 199 | if err := s.store.Save(pkgTempFileName, r); err != nil { 200 | return nil, err 201 | } 202 | 203 | // parse package 204 | var pkg Package 205 | var err error 206 | switch t { 207 | case AppInfoTypeIpa: 208 | pkg, err = ipa.Parse(r, size) 209 | case AppInfoTypeApk: 210 | pkg, err = apk.Parse(r, size) 211 | } 212 | if err != nil { 213 | return nil, err 214 | } 215 | 216 | // new AppInfo 217 | app := NewAppInfo(pkg, t) 218 | // move temp package file to target location 219 | err = s.store.Move(pkgTempFileName, app.PackageStorageName()) 220 | if err != nil { 221 | return nil, err 222 | } 223 | 224 | // try save icon file 225 | if pkg.Icon() != nil { 226 | buf := &bytes.Buffer{} 227 | err = png.Encode(buf, pkg.Icon()) 228 | if err == nil { 229 | if err := s.store.Save(app.IconStorageName(), buf); err != nil { 230 | // NOTE: ignore error 231 | } 232 | } 233 | } 234 | 235 | return app, nil 236 | } 237 | 238 | // save metadata 239 | func (s *service) saveMetadata() error { 240 | s.lock.Lock() 241 | d, err := json.Marshal(s.list) 242 | s.lock.Unlock() 243 | 244 | if err != nil { 245 | return err 246 | } 247 | 248 | b := bytes.NewBuffer(d) 249 | return s.store.Save(s.metadataName, b) 250 | } 251 | 252 | func (s *service) tryMigrateOldData() error { 253 | f, err := s.store.OpenMetadata(s.metadataName) 254 | if err != nil { 255 | return err 256 | } 257 | defer f.Close() 258 | b, err := ioutil.ReadAll(f) 259 | if err != nil { 260 | return err 261 | } 262 | 263 | list := AppList{} 264 | if err := json.Unmarshal(b, &list); err != nil { 265 | return err 266 | } 267 | 268 | s.lock.Lock() 269 | s.list = append(list, s.list...) 270 | sort.Sort(s.list) 271 | s.lock.Unlock() 272 | 273 | return nil 274 | } 275 | 276 | func (s *service) Plist(id, publicURL string) ([]byte, error) { 277 | app, err := s.Find(id, publicURL) 278 | if err != nil { 279 | return nil, err 280 | } 281 | return NewInstallPlist(app) 282 | } 283 | 284 | func (s *service) find(id string) (*AppInfo, error) { 285 | for _, row := range s.list { 286 | if row.ID == id { 287 | return row, nil 288 | } 289 | } 290 | return nil, ErrIdNotFound 291 | } 292 | 293 | // get public url 294 | func (s *service) storagerPublicURL(publicURL, name string) string { 295 | if s.publicURL != "" { 296 | publicURL = s.publicURL 297 | } 298 | u, err := s.store.PublicURL(publicURL, name) 299 | if err != nil { 300 | // TODO: handle err 301 | return "" 302 | } 303 | return u 304 | } 305 | 306 | func (s *service) servicePublicURL(publicURL, name string) string { 307 | if s.publicURL != "" { 308 | publicURL = s.publicURL 309 | } 310 | u, err := url.Parse(publicURL) 311 | if err != nil { 312 | // TODO: handle err 313 | return "" 314 | } 315 | u.Path = filepath.Join(u.Path, name) 316 | return u.String() 317 | } 318 | 319 | func (s *service) itemInfo(row *AppInfo, publicURL string) *Item { 320 | 321 | plist := "" 322 | switch row.Type { 323 | case AppInfoTypeIpa: 324 | plist = s.servicePublicURL(publicURL, fmt.Sprintf("plist/%v.plist", row.ID)) 325 | } 326 | 327 | metaDataFilter := []string{} 328 | for _, v := range strings.Split(os.Getenv("META_DATA_FILTER"), ",") { 329 | key := strings.TrimSpace(v) 330 | if len(key) > 0 { 331 | metaDataFilter = append(metaDataFilter, key) 332 | } 333 | } 334 | 335 | return &Item{ 336 | // from AppInfo 337 | ID: row.ID, 338 | Name: row.Name, 339 | Date: row.Date, 340 | Size: row.Size, 341 | Build: row.Build, 342 | Identifier: row.Identifier, 343 | Version: row.Version, 344 | Channel: row.Channel, 345 | Type: row.Type, 346 | 347 | MetaData: row.MetaData, 348 | MetaDataFilter: metaDataFilter, 349 | 350 | Pkg: s.storagerPublicURL(publicURL, row.PackageStorageName()), 351 | Plist: plist, 352 | Icon: s.iconPublicURL(publicURL, row), 353 | WebIcon: s.iconPublicURL(publicURL, row), 354 | } 355 | } 356 | 357 | func (s *service) history(row *AppInfo, publicURL string) []*Item { 358 | list := []*Item{} 359 | for _, i := range s.list { 360 | if i.Identifier == row.Identifier { 361 | item := s.itemInfo(i, publicURL) 362 | item.Current = i.ID == row.ID 363 | list = append(list, item) 364 | } 365 | } 366 | return list 367 | } 368 | 369 | func (s *service) iconPublicURL(publicURL string, app *AppInfo) string { 370 | name := app.IconStorageName() 371 | if name == "" { 372 | name = "img/default.png" 373 | return s.servicePublicURL(publicURL, name) 374 | } 375 | return s.storagerPublicURL(publicURL, name) 376 | } 377 | 378 | type ServiceMiddleware func(Service) Service 379 | -------------------------------------------------------------------------------- /cmd/ipasd/service/transport.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "net/http" 11 | "net/url" 12 | "path" 13 | "path/filepath" 14 | "regexp" 15 | "strings" 16 | 17 | "github.com/go-kit/kit/endpoint" 18 | "github.com/iineva/ipa-server/pkg/common" 19 | pkgMultipart "github.com/iineva/ipa-server/pkg/multipart" 20 | "github.com/iineva/ipa-server/pkg/seekbuf" 21 | ) 22 | 23 | type param struct { 24 | publicURL string 25 | id string 26 | } 27 | 28 | type delParam struct { 29 | publicURL string 30 | id string 31 | get bool // get if delete enabled 32 | } 33 | 34 | type addParam struct { 35 | file *pkgMultipart.FormFile 36 | } 37 | 38 | type data interface{} 39 | type response struct { 40 | data 41 | Err string `json:"err"` 42 | } 43 | 44 | var ( 45 | ErrIdInvalid = errors.New("id invalid") 46 | ) 47 | 48 | func MakeListEndpoint(srv Service, uploadDisabled bool) endpoint.Endpoint { 49 | return func(ctx context.Context, request interface{}) (interface{}, error) { 50 | p := request.(param) 51 | return srv.List(p.publicURL, uploadDisabled) 52 | } 53 | } 54 | 55 | func MakeFindEndpoint(srv Service) endpoint.Endpoint { 56 | return func(ctx context.Context, request interface{}) (interface{}, error) { 57 | p := request.(param) 58 | return srv.Find(p.id, p.publicURL) 59 | } 60 | } 61 | 62 | func MakeAddEndpoint(srv Service, uploadDisabled bool) endpoint.Endpoint { 63 | return func(ctx context.Context, request interface{}) (interface{}, error) { 64 | if !uploadDisabled { 65 | return nil, errors.New("upload was disabled") 66 | } 67 | 68 | p := request.(addParam) 69 | buf, err := seekbuf.Open(p.file, seekbuf.FileMode) 70 | if err != nil { 71 | return nil, err 72 | } 73 | defer buf.Close() 74 | 75 | t := FileType(p.file.FileName()) 76 | if t == AppInfoTypeUnknown { 77 | return nil, fmt.Errorf("do not support %s file", path.Ext(p.file.FileName())) 78 | } 79 | 80 | app, err := srv.Add(buf, p.file.Size(), t) 81 | if err != nil { 82 | return nil, err 83 | } 84 | return map[string]interface{}{"msg": "ok", "data": app}, nil 85 | } 86 | } 87 | 88 | func MakeDeleteEndpoint(srv Service, enabledDelete bool) endpoint.Endpoint { 89 | return func(ctx context.Context, request interface{}) (interface{}, error) { 90 | if !enabledDelete { 91 | return nil, errors.New("no permission to delete") 92 | } 93 | 94 | p := request.(delParam) 95 | err := srv.Delete(p.id) 96 | if err != nil { 97 | return nil, err 98 | } 99 | return map[string]string{"msg": "ok"}, nil 100 | } 101 | } 102 | 103 | func MakeGetDeleteEndpoint(srv Service, enabledDelete bool) endpoint.Endpoint { 104 | return func(ctx context.Context, request interface{}) (interface{}, error) { 105 | // check is delete enabled 106 | return map[string]interface{}{"delete": enabledDelete}, nil 107 | } 108 | } 109 | 110 | func MakePlistEndpoint(srv Service) endpoint.Endpoint { 111 | return func(ctx context.Context, request interface{}) (interface{}, error) { 112 | p := request.(param) 113 | 114 | d, err := srv.Plist(p.id, p.publicURL) 115 | if err != nil { 116 | return nil, err 117 | } 118 | return d, nil 119 | } 120 | } 121 | 122 | func DecodeListRequest(_ context.Context, r *http.Request) (interface{}, error) { 123 | // http://localhost/api/list 124 | return param{publicURL: publicURL(r)}, nil 125 | } 126 | 127 | func DecodeFindRequest(_ context.Context, r *http.Request) (interface{}, error) { 128 | // http://localhost/api/info/{id} 129 | id := filepath.Base(r.URL.Path) 130 | if id == "" { 131 | return nil, ErrIdInvalid 132 | } 133 | 134 | if err := tryMatchID(id); err != nil { 135 | return nil, ErrIdInvalid 136 | } 137 | return param{publicURL: publicURL(r), id: id}, nil 138 | } 139 | 140 | func DecodeAddRequest(_ context.Context, r *http.Request) (interface{}, error) { 141 | // http://localhost/api/upload 142 | if r.Method != http.MethodPost { 143 | return nil, errors.New("404") 144 | } 145 | 146 | m := pkgMultipart.New(r) 147 | f, err := m.GetFormFile("file") 148 | if err != nil { 149 | return nil, err 150 | } 151 | 152 | return addParam{file: f}, nil 153 | } 154 | 155 | func DecodeDeleteRequest(_ context.Context, r *http.Request) (interface{}, error) { 156 | // http://localhost/api/delete 157 | 158 | if r.Method == http.MethodGet { 159 | return delParam{get: true}, nil 160 | } 161 | 162 | p := map[string]string{} 163 | if err := json.NewDecoder(r.Body).Decode(&p); err != nil { 164 | return nil, err 165 | } 166 | 167 | id := p["id"] 168 | if err := tryMatchID(id); err != nil { 169 | return nil, err 170 | } 171 | 172 | return delParam{id: id, get: false}, nil 173 | } 174 | 175 | func DecodePlistRequest(_ context.Context, r *http.Request) (interface{}, error) { 176 | // http://localhost/plist/{id}.plist 177 | id := strings.TrimSuffix(filepath.Base(r.URL.Path), ".plist") 178 | if err := tryMatchID(id); err != nil { 179 | return nil, ErrIdInvalid 180 | } 181 | 182 | return param{publicURL: publicURL(r), id: id}, nil 183 | } 184 | 185 | func EncodeJsonResponse(_ context.Context, w http.ResponseWriter, response interface{}) error { 186 | return json.NewEncoder(w).Encode(response) 187 | } 188 | 189 | func EncodePlistResponse(_ context.Context, w http.ResponseWriter, response interface{}) error { 190 | d := response.([]byte) 191 | n, err := io.Copy(w, bytes.NewBuffer(d)) 192 | if err != nil { 193 | return err 194 | } 195 | if int64(len(d)) != n { 196 | return errors.New("wirte body len not match") 197 | } 198 | return nil 199 | } 200 | 201 | // auto check public url from frontend 202 | func publicURL(ctx *http.Request) string { 203 | ref := ctx.Header.Get("referer") 204 | if ref != "" { 205 | u, _ := url.Parse(ref) 206 | return fmt.Sprintf("%v://%v", u.Scheme, u.Host) 207 | } 208 | 209 | xProto := ctx.Header.Get("x-forwarded-proto") 210 | host := ctx.Host 211 | return fmt.Sprintf("%v://%v", common.Def(xProto, "http"), host) 212 | } 213 | 214 | func tryMatchID(id string) error { 215 | const idRegexp = `^[0-9a-zA-Z]{16,32}$` 216 | match, err := regexp.MatchString(idRegexp, id) 217 | if err != nil { 218 | return err 219 | } 220 | if !match { 221 | return ErrIdInvalid 222 | } 223 | return nil 224 | } 225 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | services: 4 | web: 5 | build: . 6 | container_name: ipa-server 7 | restart: unless-stopped 8 | environment: 9 | # server public url 10 | - PUBLIC_URL= 11 | # option, remote storager config, s3://ENDPOINT:AK:SK:BUCKET, alioss://ENDPOINT:AK:SK:BUCKET, qiniu://[ZONE]:AK:SK:BUCKET 12 | - REMOTE= 13 | # option, remote storager public url, https://cdn.example.com 14 | - REMOTE_URL= 15 | # option, metadata storage path, use random secret path to keep your metadata safer in case of remote storage 16 | - META_PATH=appList.json 17 | # delete app enabled, true/false 18 | - DELETE_ENABLED=false 19 | ports: 20 | - "9008:8080" 21 | volumes: 22 | - "/docker/data/ipa-server:/app/upload" 23 | -------------------------------------------------------------------------------- /docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ipasd_args="" 4 | 5 | if [ -n "$PORT" ];then 6 | ipasd_args=$ipasd_args"-port $PORT " 7 | fi 8 | 9 | PUBLIC_URL=${PUBLIC_URL:-$DOMAIN} 10 | if [ -n "$PUBLIC_URL" ];then 11 | ipasd_args=$ipasd_args"-public-url $PUBLIC_URL " 12 | fi 13 | 14 | if [ -n "$REMOTE" ];then 15 | ipasd_args=$ipasd_args"-remote $REMOTE " 16 | fi 17 | 18 | if [ -n "$REMOTE_URL" ];then 19 | ipasd_args=$ipasd_args"-remote-url $REMOTE_URL " 20 | fi 21 | 22 | if [ "$DELETE_ENABLED" = "true" -o "$DELETE_ENABLED" = "1" ];then 23 | ipasd_args=$ipasd_args"-del " 24 | fi 25 | 26 | if [ "$UPLOAD_DISABLED" = "true" -o "$UPLOAD_DISABLED" = "1" ];then 27 | ipasd_args=$ipasd_args"-upload-disabled " 28 | fi 29 | 30 | if [ -n "$META_PATH" ];then 31 | ipasd_args=$ipasd_args"-meta-path $META_PATH " 32 | fi 33 | 34 | if [ -n "$LOGIN_USER" ];then 35 | ipasd_args=$ipasd_args"-user $LOGIN_USER " 36 | fi 37 | 38 | if [ -n "$LOGIN_PASS" ];then 39 | ipasd_args=$ipasd_args"-pass $LOGIN_PASS " 40 | fi 41 | 42 | /app/ipasd $ipasd_args 43 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/iineva/ipa-server 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/aliyun/aliyun-oss-go-sdk v2.1.8+incompatible 7 | github.com/aws/aws-sdk-go-v2 v1.6.0 8 | github.com/aws/aws-sdk-go-v2/config v1.3.0 9 | github.com/aws/aws-sdk-go-v2/credentials v1.2.1 10 | github.com/aws/aws-sdk-go-v2/service/s3 v1.9.0 11 | github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f // indirect 12 | github.com/go-kit/kit v0.10.0 13 | github.com/google/uuid v1.2.0 // indirect 14 | github.com/gorilla/websocket v1.4.1 15 | github.com/iineva/bom v0.0.0-20250226145112-6ae267192ee0 16 | github.com/lithammer/shortuuid v3.0.0+incompatible 17 | github.com/poolqa/CgbiPngFix v0.0.0-20211024081647-8ad4fb5c23e4 18 | github.com/qiniu/go-sdk/v7 v7.9.5 19 | github.com/satori/go.uuid v1.2.0 // indirect 20 | github.com/shogo82148/androidbinary v1.0.2 21 | github.com/spf13/afero v1.6.0 22 | howett.net/plist v0.0.0-20201203080718-1454fab16a06 23 | ) 24 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 4 | github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= 5 | github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= 6 | github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= 7 | github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= 8 | github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= 9 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 10 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 11 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 12 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 13 | github.com/aliyun/aliyun-oss-go-sdk v2.1.8+incompatible h1:hLUNPbx10wawWW7DeNExvTrlb90db3UnnNTFKHZEFhE= 14 | github.com/aliyun/aliyun-oss-go-sdk v2.1.8+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8= 15 | github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= 16 | github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= 17 | github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= 18 | github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= 19 | github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 20 | github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= 21 | github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= 22 | github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= 23 | github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= 24 | github.com/aws/aws-sdk-go-v2 v1.6.0 h1:r20hdhm8wZmKkClREfacXrKfX0Y7/s0aOoeraFbf/sY= 25 | github.com/aws/aws-sdk-go-v2 v1.6.0/go.mod h1:tI4KhsR5VkzlUa2DZAdwx7wCAYGwkZZ1H31PYrBFx1w= 26 | github.com/aws/aws-sdk-go-v2/config v1.3.0 h1:0JAnp0WcsgKilFLiZEScUTKIvTKa2LkicadZADza+u0= 27 | github.com/aws/aws-sdk-go-v2/config v1.3.0/go.mod h1:lOxzHWDt/k7MMidA/K8DgXL4+ynnZYsDq65Qhs/l3dg= 28 | github.com/aws/aws-sdk-go-v2/credentials v1.2.1 h1:AqQ8PzWll1wegNUOfIKcbp/JspTbJl54gNonrO6VUsY= 29 | github.com/aws/aws-sdk-go-v2/credentials v1.2.1/go.mod h1:Rfvim1eZTC9W5s8YJyYYtl1KMk6e8fHv+wMRQGO4Ru0= 30 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.1.1 h1:w1ocBIhQkLgupEB3d0uOuBddqVYl0xpubz7HSTzWG8A= 31 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.1.1/go.mod h1:GTXAhrxHQOj9N+J5tYVjwt+rpRyy/42qLjlgw9pz1a0= 32 | github.com/aws/aws-sdk-go-v2/internal/ini v1.0.0 h1:k7I9E6tyVWBo7H9ffpnxDWudtjau6Qt9rnOYgV+ciEQ= 33 | github.com/aws/aws-sdk-go-v2/internal/ini v1.0.0/go.mod h1:g3XMXuxvqSMUjnsXXp/960152w0wFS4CXVYgQaSVOHE= 34 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.1.0 h1:XwqxIO9LtNXznBbEMNGumtLN60k4nVqDpVwVWx3XU/o= 35 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.1.0/go.mod h1:zdjOOy0ojUn3iNELo6ycIHSMCp4xUbycSHfb8PnbbyM= 36 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.1.1 h1:l7pDLsmOGrnR8LT+3gIv8NlHpUhs7220E457KEC2UM0= 37 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.1.1/go.mod h1:2+ehJPkdIdl46VCj67Emz/EH2hpebHZtaLdzqg+sWOI= 38 | github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.3.1 h1:VH1Y4k+IZ5kcRVqSNw7eAkXyfS7k2/ibKjrNtbhYhV4= 39 | github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.3.1/go.mod h1:IpjxfORBAFfkMM0VEx5gPPnEy6WV4Hk0F/+zb/SUWyw= 40 | github.com/aws/aws-sdk-go-v2/service/s3 v1.9.0 h1:FZ5UL5aiybSJKiJglPT7YMMwc431IgOX5gvlFAzSjzs= 41 | github.com/aws/aws-sdk-go-v2/service/s3 v1.9.0/go.mod h1:zHCjYoODbYRLz/iFicYswq1gRoxBnHvpY5h2Vg3/tJ4= 42 | github.com/aws/aws-sdk-go-v2/service/sso v1.2.1 h1:alpXc5UG7al7QnttHe/9hfvUfitV8r3w0onPpPkGzi0= 43 | github.com/aws/aws-sdk-go-v2/service/sso v1.2.1/go.mod h1:VimPFPltQ/920i1X0Sb0VJBROLIHkDg2MNP10D46OGs= 44 | github.com/aws/aws-sdk-go-v2/service/sts v1.4.1 h1:9Z00tExoaLutWVDmY6LyvIAcKjHetkbdmpRt4JN/FN0= 45 | github.com/aws/aws-sdk-go-v2/service/sts v1.4.1/go.mod h1:G9osDWA52WQ38BDcj65VY1cNmcAQXAXTsE8IWH8j81w= 46 | github.com/aws/smithy-go v1.4.0 h1:3rsQpgRe+OoQgJhEwGNpIkosl0fJLdmQqF4gSFRjg+4= 47 | github.com/aws/smithy-go v1.4.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= 48 | github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f h1:ZNv7On9kyUzm7fvRZumSyy/IUiSC7AzL0I1jKKtwooA= 49 | github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc= 50 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 51 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 52 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 53 | github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= 54 | github.com/blacktop/lzfse-cgo v1.1.20 h1:xlfMq7g1aaMCyWr8/hHqsWzvfaF8QaliU1YxDCfMx68= 55 | github.com/blacktop/lzfse-cgo v1.1.20/go.mod h1:VoBC8Nle73KZg/4X9NW94jkFrQkNwQ18SMMu5ev8zSA= 56 | github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= 57 | github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= 58 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 59 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 60 | github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= 61 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 62 | github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= 63 | github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= 64 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 65 | github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 66 | github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 67 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 68 | github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= 69 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 70 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 71 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 72 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 73 | github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 74 | github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= 75 | github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= 76 | github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= 77 | github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= 78 | github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= 79 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 80 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 81 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 82 | github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= 83 | github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= 84 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 85 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 86 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 87 | github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 88 | github.com/go-kit/kit v0.10.0 h1:dXFJfIHVvUcpSgDOV+Ne6t7jXri8Tfv2uOLHUZ2XNuo= 89 | github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= 90 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 91 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 92 | github.com/go-logfmt/logfmt v0.5.0 h1:TrB8swr/68K7m9CcGut2g3UOihhbcbiMAYiuTXdEih4= 93 | github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= 94 | github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= 95 | github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= 96 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 97 | github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= 98 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 99 | github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 100 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 101 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 102 | github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 103 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 104 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 105 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 106 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 107 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 108 | github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 109 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 110 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 111 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 112 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 113 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 114 | github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= 115 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 116 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 117 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 118 | github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 119 | github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= 120 | github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 121 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 122 | github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= 123 | github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= 124 | github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= 125 | github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 126 | github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= 127 | github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 128 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 129 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 130 | github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 131 | github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= 132 | github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= 133 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 134 | github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 135 | github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= 136 | github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= 137 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= 138 | github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= 139 | github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= 140 | github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= 141 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 142 | github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 143 | github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 144 | github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= 145 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 146 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 147 | github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= 148 | github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= 149 | github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= 150 | github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= 151 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 152 | github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= 153 | github.com/iineva/bom v0.0.0-20250226145112-6ae267192ee0 h1:AlGSnig7js+V4p17nQGi/JhxofcBcIq4qILzMuP1oFQ= 154 | github.com/iineva/bom v0.0.0-20250226145112-6ae267192ee0/go.mod h1:0qvCvt6Y90KGuWQMQbMb5EuMAjEPfmZ6qfM8CaW+OOk= 155 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 156 | github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= 157 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 158 | github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= 159 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 160 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 161 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 162 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 163 | github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 164 | github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 165 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 166 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 167 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 168 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 169 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 170 | github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 171 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 172 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 173 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 174 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 175 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 176 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 177 | github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= 178 | github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= 179 | github.com/lithammer/shortuuid v3.0.0+incompatible h1:NcD0xWW/MZYXEHa6ITy6kaXN5nwm/V115vj2YXfhS0w= 180 | github.com/lithammer/shortuuid v3.0.0+incompatible/go.mod h1:FR74pbAuElzOUuenUHTK2Tciko1/vKuIKS9dSkDrA4w= 181 | github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= 182 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 183 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 184 | github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 185 | github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 186 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 187 | github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= 188 | github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= 189 | github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 190 | github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= 191 | github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= 192 | github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= 193 | github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 194 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 195 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 196 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 197 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 198 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 199 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 200 | github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= 201 | github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= 202 | github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= 203 | github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= 204 | github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= 205 | github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= 206 | github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= 207 | github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= 208 | github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= 209 | github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= 210 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 211 | github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 212 | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 213 | github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= 214 | github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= 215 | github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= 216 | github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= 217 | github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= 218 | github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= 219 | github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= 220 | github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= 221 | github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= 222 | github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= 223 | github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= 224 | github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= 225 | github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= 226 | github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= 227 | github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= 228 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 229 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 230 | github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= 231 | github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= 232 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 233 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 234 | github.com/poolqa/CgbiPngFix v0.0.0-20211024081647-8ad4fb5c23e4 h1:n3IkiWQgZS8Ref8BX/NWpYe7VClpQJ8pbzUSBZOqoKs= 235 | github.com/poolqa/CgbiPngFix v0.0.0-20211024081647-8ad4fb5c23e4/go.mod h1:DG0Kw4br1oA1IRv7O/QNp8fzh+/YBgknQCLnMeioG1U= 236 | github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= 237 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 238 | github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= 239 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 240 | github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= 241 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 242 | github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 243 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 244 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 245 | github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 246 | github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 247 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 248 | github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= 249 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 250 | github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 251 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 252 | github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= 253 | github.com/qiniu/go-sdk/v7 v7.9.5 h1:hxsdSmRAfN8hp77OUjjDgKHyxjHpFlW5fC2rHC6hMRQ= 254 | github.com/qiniu/go-sdk/v7 v7.9.5/go.mod h1:Eeqk1/Km3f1MuLUUkg2JCSg/dVkydKbBvEdJJqFgn9g= 255 | github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= 256 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 257 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 258 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 259 | github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= 260 | github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= 261 | github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= 262 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 263 | github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= 264 | github.com/shogo82148/androidbinary v1.0.2 h1:tjgNUJeZwlIEytmn0u6Gk6b6rinpZSYi9HqRpLX7Obs= 265 | github.com/shogo82148/androidbinary v1.0.2/go.mod h1:c3BBft4TLXkvqry+EEN8z9ZtyY09r7jLMvFB/L/KrfI= 266 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 267 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 268 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 269 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 270 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 271 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 272 | github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= 273 | github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY= 274 | github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= 275 | github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= 276 | github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 277 | github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= 278 | github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= 279 | github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= 280 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 281 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 282 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 283 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 284 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 285 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 286 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 287 | github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 288 | github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= 289 | github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 290 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 291 | go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 292 | go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= 293 | go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= 294 | go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= 295 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 296 | go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 297 | go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 298 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 299 | go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= 300 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 301 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 302 | go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= 303 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 304 | golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 305 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 306 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 307 | golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 308 | golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 309 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 310 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 311 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 312 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 313 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 314 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 315 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 316 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 317 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 318 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 319 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 320 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 321 | golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 322 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 323 | golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 324 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 325 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 326 | golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 327 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 328 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 329 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 330 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 331 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 332 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 333 | golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 334 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 335 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 336 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 337 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 338 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 339 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 340 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 341 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 342 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a h1:DcqTD9SDLc+1P/r1EmRBwnVsrOwW+kk2vWf9n+1sGhs= 343 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 344 | golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 345 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 346 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 347 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 348 | golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 349 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 350 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 351 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 352 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 353 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 354 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 355 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 356 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 357 | golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 358 | golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 359 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 360 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 361 | golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= 362 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 363 | golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 364 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= 365 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 366 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 367 | golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 368 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 369 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 370 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 371 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 372 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 373 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 374 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 375 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 376 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 377 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 378 | golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 379 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 380 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 381 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 382 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 383 | google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= 384 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 385 | google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 386 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 387 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 388 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 389 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 390 | google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= 391 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 392 | google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= 393 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 394 | google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= 395 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 396 | google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 397 | google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 398 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 399 | google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 400 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 401 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 402 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 403 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 404 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 405 | gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= 406 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 407 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 408 | gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= 409 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 410 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 411 | gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= 412 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 413 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 414 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 415 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 416 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 417 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 418 | honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 419 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 420 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 421 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 422 | howett.net/plist v0.0.0-20201203080718-1454fab16a06 h1:QDxUo/w2COstK1wIBYpzQlHX/NqaQTcf9jyz347nI58= 423 | howett.net/plist v0.0.0-20201203080718-1454fab16a06/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0= 424 | sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= 425 | sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= 426 | -------------------------------------------------------------------------------- /heroku.yml: -------------------------------------------------------------------------------- 1 | build: 2 | docker: 3 | web: Dockerfile 4 | -------------------------------------------------------------------------------- /pkg/apk/apk.go: -------------------------------------------------------------------------------- 1 | package apk 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/shogo82148/androidbinary" 7 | "github.com/shogo82148/androidbinary/apk" 8 | ) 9 | 10 | func Parse(readerAt io.ReaderAt, size int64) (*APK, error) { 11 | pkg, err := apk.OpenZipReader(readerAt, size) 12 | if err != nil { 13 | return nil, err 14 | } 15 | defer pkg.Close() 16 | 17 | icon, err := pkg.Icon(&androidbinary.ResTableConfig{ 18 | Density: 720, 19 | }) 20 | if err != nil { 21 | // NOTE: ignore error 22 | } 23 | 24 | return &APK{ 25 | icon: icon, 26 | manifest: pkg.Manifest(), 27 | size: size, 28 | }, nil 29 | } 30 | -------------------------------------------------------------------------------- /pkg/apk/package_info.go: -------------------------------------------------------------------------------- 1 | package apk 2 | 3 | import ( 4 | "fmt" 5 | "image" 6 | 7 | "github.com/shogo82148/androidbinary/apk" 8 | ) 9 | 10 | type APK struct { 11 | manifest apk.Manifest 12 | icon image.Image 13 | size int64 14 | } 15 | 16 | func (a *APK) Name() string { 17 | return a.manifest.App.Label.MustString() 18 | } 19 | 20 | func (a *APK) Version() string { 21 | return a.manifest.VersionName.MustString() 22 | } 23 | 24 | func (a *APK) Identifier() string { 25 | return a.manifest.Package.MustString() 26 | } 27 | 28 | func (a *APK) Build() string { 29 | return fmt.Sprintf("%v", a.manifest.VersionCode.MustInt32()) 30 | } 31 | 32 | func (a *APK) Channel() string { 33 | for _, r := range a.manifest.App.MetaData { 34 | n := r.Name.MustString() 35 | if n == "channel" { 36 | return r.Value.MustString() 37 | } 38 | } 39 | return "" 40 | } 41 | 42 | func (a *APK) MetaData() map[string]interface{} { 43 | d := map[string]interface{}{} 44 | for _, r := range a.manifest.App.MetaData { 45 | name, err := r.Name.String() 46 | if err != nil { 47 | continue 48 | } 49 | value, err := r.Value.String() 50 | if err != nil { 51 | continue 52 | } 53 | d[name] = value 54 | } 55 | return d 56 | } 57 | 58 | func (a *APK) Icon() image.Image { 59 | return a.icon 60 | } 61 | 62 | func (a *APK) Size() int64 { 63 | return a.size 64 | } 65 | -------------------------------------------------------------------------------- /pkg/apk/package_info_test.go: -------------------------------------------------------------------------------- 1 | package apk 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/shogo82148/androidbinary/apk" 8 | ) 9 | 10 | func TestApkInfo(t *testing.T) { 11 | 12 | a, err := apk.OpenFile("./test.apk") 13 | if err != nil { 14 | t.Fatal(err) 15 | } 16 | 17 | for _, m := range a.Manifest().App.MetaData { 18 | name, _ := m.Name.String() 19 | value, _ := m.Value.String() 20 | fmt.Printf("%s: %s\n", name, value) 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /pkg/common/common.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import "encoding/json" 4 | 5 | // get args until arg is not empty 6 | func Def(args ...string) string { 7 | for _, v := range args { 8 | if v != "" { 9 | return v 10 | } 11 | } 12 | return "" 13 | } 14 | 15 | // 结构体转 map 16 | func ToMap(v interface{}) map[string]interface{} { 17 | b, err := json.Marshal(v) 18 | m := map[string]interface{}{} 19 | if err != nil { 20 | return m 21 | } 22 | _ = json.Unmarshal(b, &m) 23 | return m 24 | } 25 | -------------------------------------------------------------------------------- /pkg/http_basic_auth/http_basic_auth.go: -------------------------------------------------------------------------------- 1 | package http_basic_auth 2 | 3 | import ( 4 | "bytes" 5 | "crypto/sha256" 6 | "crypto/subtle" 7 | "encoding/base64" 8 | "errors" 9 | "net/http" 10 | "strings" 11 | ) 12 | 13 | var ErrUnauthorized = errors.New("Unauthorized") 14 | 15 | // Returns a hash of a given slice. 16 | func toHashSlice(s []byte) []byte { 17 | hash := sha256.Sum256(s) 18 | return hash[:] 19 | } 20 | 21 | // parseBasicAuth parses an HTTP Basic Authentication string. 22 | // "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" returns ([]byte("Aladdin"), []byte("open sesame"), true). 23 | func parseBasicAuth(auth string) (username, password []byte, ok bool) { 24 | const prefix = "Basic " 25 | if !strings.HasPrefix(auth, prefix) { 26 | return 27 | } 28 | c, err := base64.StdEncoding.DecodeString(auth[len(prefix):]) 29 | if err != nil { 30 | return 31 | } 32 | 33 | s := bytes.IndexByte(c, ':') 34 | if s < 0 { 35 | return 36 | } 37 | return c[:s], c[s+1:], true 38 | } 39 | 40 | func HandleBasicAuth(requiredUser, requiredPassword, realm string, r *http.Request) error { 41 | requiredUserBytes := toHashSlice([]byte(requiredUser)) 42 | requiredPasswordBytes := toHashSlice([]byte(requiredPassword)) 43 | 44 | auth := r.Header.Get("Authorization") 45 | if auth == "" { 46 | return ErrUnauthorized 47 | } 48 | 49 | givenUser, givenPassword, ok := parseBasicAuth(auth) 50 | if !ok { 51 | return ErrUnauthorized 52 | } 53 | 54 | givenUserBytes := toHashSlice(givenUser) 55 | givenPasswordBytes := toHashSlice(givenPassword) 56 | 57 | if subtle.ConstantTimeCompare(givenUserBytes, requiredUserBytes) == 0 || 58 | subtle.ConstantTimeCompare(givenPasswordBytes, requiredPasswordBytes) == 0 { 59 | return ErrUnauthorized 60 | } 61 | 62 | return nil 63 | } 64 | -------------------------------------------------------------------------------- /pkg/httpfs/afero.go: -------------------------------------------------------------------------------- 1 | package httpfs 2 | 3 | import ( 4 | "io/fs" 5 | "net/http" 6 | 7 | "github.com/spf13/afero" 8 | ) 9 | 10 | type aferoFS struct { 11 | fs afero.Fs 12 | } 13 | 14 | type aferoFile struct { 15 | f afero.File 16 | } 17 | 18 | var _ http.FileSystem = (*aferoFS)(nil) 19 | var _ http.File = (*aferoFile)(nil) 20 | 21 | func NewAferoFS(a afero.Fs) http.FileSystem { 22 | return &aferoFS{fs: a} 23 | } 24 | 25 | func (a *aferoFS) Open(name string) (http.File, error) { 26 | f, err := a.fs.Open(name) 27 | if err != nil { 28 | return nil, err 29 | } 30 | return &aferoFile{f: f}, nil 31 | } 32 | 33 | func (a *aferoFile) Close() error { 34 | return a.f.Close() 35 | } 36 | 37 | func (a *aferoFile) Read(p []byte) (int, error) { 38 | return a.f.Read(p) 39 | } 40 | 41 | func (a *aferoFile) Seek(offset int64, whence int) (int64, error) { 42 | return a.f.Seek(offset, whence) 43 | } 44 | 45 | func (a *aferoFile) Readdir(count int) ([]fs.FileInfo, error) { 46 | return a.f.Readdir(count) 47 | } 48 | 49 | func (a *aferoFile) Stat() (fs.FileInfo, error) { 50 | return a.f.Stat() 51 | } 52 | -------------------------------------------------------------------------------- /pkg/httpfs/httpfs.go: -------------------------------------------------------------------------------- 1 | // Package httpfs Merge multiple http.FileSystem as one 2 | package httpfs 3 | 4 | import ( 5 | "net/http" 6 | ) 7 | 8 | type httpFS struct { 9 | fss []http.FileSystem 10 | } 11 | 12 | var _ http.FileSystem = (*httpFS)(nil) 13 | 14 | func New(fss ...http.FileSystem) http.FileSystem { 15 | return &httpFS{fss: fss} 16 | } 17 | 18 | func (h *httpFS) Open(name string) (f http.File, err error) { 19 | for _, cfs := range h.fss { 20 | f, err = cfs.Open(name) 21 | if err != nil { 22 | continue 23 | } 24 | return f, err 25 | } 26 | return nil, err 27 | } 28 | -------------------------------------------------------------------------------- /pkg/ipa/ipa.go: -------------------------------------------------------------------------------- 1 | package ipa 2 | 3 | import ( 4 | "archive/zip" 5 | "errors" 6 | "image" 7 | "image/png" 8 | "io" 9 | "path/filepath" 10 | "regexp" 11 | "strconv" 12 | "strings" 13 | 14 | "github.com/iineva/bom/pkg/asset" 15 | "github.com/iineva/ipa-server/pkg/plist" 16 | "github.com/iineva/ipa-server/pkg/seekbuf" 17 | "github.com/poolqa/CgbiPngFix/ipaPng" 18 | ) 19 | 20 | var ( 21 | ErrInfoPlistNotFound = errors.New("Info.plist not found") 22 | ) 23 | 24 | const ( 25 | // Payload/UnicornApp.app/AppIcon_TikTok76x76@2x~ipad.png 26 | // Payload/UnicornApp.app/AppIcon76x76.png 27 | newIconRegular = `^Payload\/.*\.app\/AppIcon-?_?\w*(\d+(\.\d+)?)x(\d+(\.\d+)?)(@\dx)?(~ipad)?\.png$` 28 | oldIconRegular = `^Payload\/.*\.app\/Icon-?_?\w*(\d+(\.\d+)?)?.png$` 29 | assetRegular = `^Payload\/.*\.app/Assets.car$` 30 | infoPlistRegular = `^Payload\/.*\.app/Info.plist$` 31 | ) 32 | 33 | // TODO: use InfoPlistIcon to parse icon files 34 | type InfoPlistIcon struct { 35 | CFBundlePrimaryIcon struct { 36 | CFBundleIconFiles []string `json:"CFBundleIconFiles,omitempty"` 37 | CFBundleIconName string `json:"CFBundleIconName,omitempty"` 38 | } `json:"CFBundlePrimaryIcon,omitempty"` 39 | } 40 | type InfoPlist struct { 41 | CFBundleDisplayName string `json:"CFBundleDisplayName,omitempty"` 42 | CFBundleExecutable string `json:"CFBundleExecutable,omitempty"` 43 | CFBundleIconName string `json:"CFBundleIconName,omitempty"` 44 | CFBundleIcons InfoPlistIcon `json:"CFBundleIcons,omitempty"` 45 | CFBundleIconsIpad InfoPlistIcon `json:"CFBundleIcons~ipad,omitempty"` 46 | CFBundleIdentifier string `json:"CFBundleIdentifier,omitempty"` 47 | CFBundleName string `json:"CFBundleName,omitempty"` 48 | CFBundleShortVersionString string `json:"CFBundleShortVersionString,omitempty"` 49 | CFBundleSupportedPlatforms []string `json:"CFBundleSupportedPlatforms,omitempty"` 50 | CFBundleVersion string `json:"CFBundleVersion,omitempty"` 51 | // not standard 52 | Channel string `json:"channel"` 53 | // not standard 54 | ISMetaData map[string]interface{} `json:"ISMetaData,omitempty"` 55 | } 56 | 57 | func Parse(readerAt io.ReaderAt, size int64) (*IPA, error) { 58 | 59 | r, err := zip.NewReader(readerAt, size) 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | // match files 65 | var plistFile *zip.File 66 | var iconFiles []*zip.File 67 | var assetFile *zip.File 68 | for _, f := range r.File { 69 | 70 | // parse Info.plist 71 | match, err := regexp.MatchString(infoPlistRegular, f.Name) 72 | { 73 | if err != nil { 74 | return nil, err 75 | } 76 | if match { 77 | plistFile = f 78 | } 79 | } 80 | 81 | // parse old icons 82 | if match, err = regexp.MatchString(oldIconRegular, f.Name); err != nil { 83 | return nil, err 84 | } else if match { 85 | iconFiles = append(iconFiles, f) 86 | } 87 | 88 | // parse new icons 89 | if match, err = regexp.MatchString(newIconRegular, f.Name); err != nil { 90 | return nil, err 91 | } else if match { 92 | iconFiles = append(iconFiles, f) 93 | } 94 | 95 | // parse Assets.car 96 | if match, err = regexp.MatchString(assetRegular, f.Name); err != nil { 97 | return nil, err 98 | } else if match { 99 | assetFile = f 100 | } 101 | 102 | } 103 | 104 | // parse Info.plist 105 | if plistFile == nil { 106 | return nil, ErrInfoPlistNotFound 107 | } 108 | var app *IPA 109 | { 110 | pf, err := plistFile.Open() 111 | defer pf.Close() 112 | if err != nil { 113 | return nil, err 114 | } 115 | info := &InfoPlist{} 116 | err = plist.Decode(pf, info) 117 | if err != nil { 118 | return nil, err 119 | } 120 | app = &IPA{ 121 | info: info, 122 | size: size, 123 | } 124 | } 125 | 126 | // select bigest icon file 127 | var iconFile *zip.File 128 | var maxSize = -1 129 | for _, f := range iconFiles { 130 | size, err := iconSize(f.Name) 131 | if err != nil { 132 | return nil, err 133 | } 134 | if size > maxSize { 135 | maxSize = size 136 | iconFile = f 137 | } 138 | } 139 | // parse icon 140 | img, err := parseIconImage(iconFile) 141 | if err == nil { 142 | app.icon = img 143 | } else if assetFile != nil { 144 | // try get icon from Assets.car 145 | img, _ := parseIconAssets(assetFile) 146 | app.icon = img 147 | } 148 | 149 | return app, nil 150 | } 151 | 152 | func iconSize(fileName string) (s int, err error) { 153 | size := float64(0) 154 | match, _ := regexp.MatchString(oldIconRegular, fileName) 155 | name := strings.TrimSuffix(filepath.Base(fileName), ".png") 156 | if match { 157 | arr := strings.Split(name, "-") 158 | if len(arr) == 2 { 159 | size, err = strconv.ParseFloat(arr[1], 32) 160 | } else { 161 | size = 160 162 | } 163 | } 164 | match, _ = regexp.MatchString(newIconRegular, fileName) 165 | if match { 166 | s := strings.Split(name, "@")[0] 167 | s = strings.Split(s, "x")[1] 168 | s = strings.Split(s, "~")[0] 169 | size, err = strconv.ParseFloat(s, 32) 170 | if strings.Index(name, "@2x") != -1 { 171 | size *= 2 172 | } else if strings.Index(name, "@3x") != -1 { 173 | size *= 3 174 | } 175 | } 176 | return int(size), err 177 | } 178 | 179 | func parseIconImage(iconFile *zip.File) (image.Image, error) { 180 | 181 | if iconFile == nil { 182 | return nil, errors.New("icon file is nil") 183 | } 184 | 185 | f, err := iconFile.Open() 186 | if err != nil { 187 | return nil, err 188 | } 189 | defer f.Close() 190 | buf, err := seekbuf.Open(f, seekbuf.MemoryMode) 191 | if err != nil { 192 | return nil, err 193 | } 194 | defer buf.Close() 195 | 196 | img, err := png.Decode(buf) 197 | if err != nil { 198 | // try fix to std png 199 | if _, err := buf.Seek(0, 0); err != nil { 200 | return nil, err 201 | } 202 | cgbi, err := ipaPng.Decode(buf) 203 | if err != nil { 204 | return nil, err 205 | } 206 | img = cgbi.Img 207 | } 208 | 209 | return img, nil 210 | } 211 | 212 | func parseIconAssets(assetFile *zip.File) (image.Image, error) { 213 | 214 | f, err := assetFile.Open() 215 | if err != nil { 216 | return nil, err 217 | } 218 | defer f.Close() 219 | 220 | buf, err := seekbuf.Open(f, seekbuf.MemoryMode) 221 | if err != nil { 222 | return nil, err 223 | } 224 | defer buf.Close() 225 | 226 | a, err := asset.NewWithReadSeeker(buf) 227 | if err != nil { 228 | return nil, err 229 | } 230 | return a.Image("AppIcon") 231 | } 232 | -------------------------------------------------------------------------------- /pkg/ipa/ipa_test.go: -------------------------------------------------------------------------------- 1 | package ipa 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "runtime" 8 | "testing" 9 | 10 | "github.com/iineva/ipa-server/pkg/seekbuf" 11 | ) 12 | 13 | func TestReadPlistInfo(t *testing.T) { 14 | 15 | printMemUsage() 16 | 17 | fileName := "test_data/ipa.ipa" 18 | // fileName := "/Users/steven/Downloads/TikTok (18.5.0) Unicorn v4.9.ipa" 19 | f, err := os.Open(fileName) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | defer f.Close() 24 | fi, err := f.Stat() 25 | if err != nil { 26 | t.Fatal(err) 27 | } 28 | 29 | buf, err := seekbuf.Open(f, seekbuf.MemoryMode) 30 | if err != nil { 31 | t.Fatal(err) 32 | } 33 | defer buf.Close() 34 | 35 | info, err := Parse(buf, fi.Size()) 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | if info == nil { 40 | t.Fatal(errors.New("parse error")) 41 | } 42 | printMemUsage() 43 | // log.Printf("%+v", info) 44 | } 45 | 46 | func TestIconSize(t *testing.T) { 47 | 48 | data := map[string]int{ 49 | "Payload/UnicornApp.app/AppIcon_TikTok29x29@3x.png": 87, 50 | "Payload/UnicornApp.app/AppIcon_TikTok40x40@2x.png": 80, 51 | "Payload/UnicornApp.app/AppIcon_TikTok60x60@3x.png": 180, 52 | "Payload/UnicornApp.app/AppIcon_TikTok60x60@2x.png": 120, 53 | "Payload/UnicornApp.app/AppIcon_TikTok40x40@3x.png": 120, 54 | "Payload/UnicornApp.app/AppIcon_TikTok29x29@2x.png": 58, 55 | "Payload/UnicornApp.app/AppIcon_TikTok83.5x83.5@2x~ipad.png": 167, 56 | "Payload/UnicornApp.app/AppIcon_TikTok20x20@3x.png": 60, 57 | "Payload/UnicornApp.app/AppIcon_TikTok76x76~ipad.png": 76, 58 | "Payload/UnicornApp.app/AppIcon_TikTok20x20@2x.png": 40, 59 | "Payload/UnicornApp.app/AppIcon_TikTok76x76@2x~ipad.png": 152, 60 | } 61 | for k, v := range data { 62 | size, err := iconSize(k) 63 | if err != nil { 64 | t.Fatal(err) 65 | } 66 | if size != v { 67 | t.Fatal(errors.New("size error")) 68 | } 69 | } 70 | } 71 | 72 | func printMemUsage() { 73 | var m runtime.MemStats 74 | runtime.ReadMemStats(&m) 75 | // For info on each, see: https://golang.org/pkg/runtime/#MemStats 76 | fmt.Printf("Alloc = %v MiB", bToMb(m.Alloc)) 77 | fmt.Printf("\tTotalAlloc = %v MiB", bToMb(m.TotalAlloc)) 78 | fmt.Printf("\tSys = %v MiB", bToMb(m.Sys)) 79 | fmt.Printf("\tNumGC = %v\n", m.NumGC) 80 | } 81 | 82 | func bToMb(b uint64) uint64 { 83 | return b / 1024 / 1024 84 | } 85 | -------------------------------------------------------------------------------- /pkg/ipa/package_info.go: -------------------------------------------------------------------------------- 1 | package ipa 2 | 3 | import ( 4 | "image" 5 | 6 | "github.com/iineva/ipa-server/pkg/common" 7 | ) 8 | 9 | type IPA struct { 10 | info *InfoPlist 11 | icon image.Image 12 | size int64 13 | } 14 | 15 | func (i *IPA) Name() string { 16 | return common.Def(i.info.CFBundleDisplayName, i.info.CFBundleName, i.info.CFBundleExecutable) 17 | } 18 | 19 | func (i *IPA) Version() string { 20 | return i.info.CFBundleShortVersionString 21 | } 22 | 23 | func (i *IPA) Identifier() string { 24 | return i.info.CFBundleIdentifier 25 | } 26 | 27 | func (i *IPA) Build() string { 28 | return i.info.CFBundleVersion 29 | } 30 | 31 | func (i *IPA) Channel() string { 32 | return i.info.Channel 33 | } 34 | 35 | func (i *IPA) MetaData() map[string]interface{} { 36 | return i.info.ISMetaData 37 | } 38 | 39 | func (i *IPA) Icon() image.Image { 40 | return i.icon 41 | } 42 | 43 | func (i *IPA) Size() int64 { 44 | return i.size 45 | } 46 | -------------------------------------------------------------------------------- /pkg/ipa/test_data/ipa.ipa: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iineva/ipa-server/6faaeed6f57bdb780f0a840a8456d99e3cc3afcf/pkg/ipa/test_data/ipa.ipa -------------------------------------------------------------------------------- /pkg/multipart/multipart.go: -------------------------------------------------------------------------------- 1 | // Package multipart to handle MultipartForm 2 | package multipart 3 | 4 | import ( 5 | "fmt" 6 | "io" 7 | "mime" 8 | "mime/multipart" 9 | "net/http" 10 | "strconv" 11 | ) 12 | 13 | type MultipartForm struct { 14 | r *http.Request 15 | } 16 | 17 | type FormFile struct { 18 | part *multipart.Part 19 | name string // form name 20 | filename string // file name 21 | size int64 // readed size 22 | } 23 | 24 | var _ io.Reader = (*FormFile)(nil) 25 | 26 | func New(r *http.Request) *MultipartForm { 27 | return &MultipartForm{r: r} 28 | } 29 | 30 | func (m *MultipartForm) GetFormFile(targetName string) (*FormFile, error) { 31 | mr, err := m.multipartReader(false) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | p, err := mr.NextPart() 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | name := p.FormName() 42 | if name != targetName { 43 | return nil, fmt.Errorf("want %s got %s", targetName, name) 44 | } 45 | filename := p.FileName() 46 | 47 | s := m.r.Header.Get("Content-Length") 48 | size, err := strconv.ParseInt(s, 10, 64) 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | return &FormFile{ 54 | part: p, 55 | name: name, 56 | filename: filename, 57 | size: size, 58 | }, nil 59 | } 60 | 61 | // code copy from http/request.go:447 62 | func (m *MultipartForm) multipartReader(allowMixed bool) (*multipart.Reader, error) { 63 | r := m.r 64 | v := r.Header.Get("Content-Type") 65 | if v == "" { 66 | return nil, http.ErrNotMultipart 67 | } 68 | d, params, err := mime.ParseMediaType(v) 69 | if err != nil || !(d == "multipart/form-data" || allowMixed && d == "multipart/mixed") { 70 | return nil, http.ErrNotMultipart 71 | } 72 | boundary, ok := params["boundary"] 73 | if !ok { 74 | return nil, http.ErrMissingBoundary 75 | } 76 | return multipart.NewReader(r.Body, boundary), nil 77 | } 78 | 79 | func (f *FormFile) Read(p []byte) (n int, err error) { 80 | return f.part.Read(p) 81 | } 82 | 83 | func (f *FormFile) FileName() string { 84 | return f.filename 85 | } 86 | 87 | func (f *FormFile) Name() string { 88 | return f.name 89 | } 90 | 91 | func (f *FormFile) Size() int64 { 92 | return f.size 93 | } -------------------------------------------------------------------------------- /pkg/plist/plist.go: -------------------------------------------------------------------------------- 1 | package plist 2 | 3 | import ( 4 | "io" 5 | 6 | "howett.net/plist" 7 | 8 | "github.com/iineva/ipa-server/pkg/seekbuf" 9 | ) 10 | 11 | func Decode(r io.Reader, d interface{}) error { 12 | buf, err := seekbuf.Open(r, seekbuf.MemoryMode) 13 | if err != nil { 14 | return err 15 | } 16 | if err := plist.NewDecoder(buf).Decode(d); err != nil { 17 | return err 18 | } 19 | return nil 20 | } 21 | -------------------------------------------------------------------------------- /pkg/seekbuf/seekbuf.go: -------------------------------------------------------------------------------- 1 | // file as a buffer 2 | package seekbuf 3 | 4 | import ( 5 | "bytes" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "os" 10 | "sync" 11 | ) 12 | 13 | // Buffer type 14 | type Buffer struct { 15 | reader io.Reader 16 | data []byte 17 | len int64 18 | pos int64 19 | mode Mode 20 | f *os.File 21 | lock sync.Mutex 22 | } 23 | 24 | var _ io.ReaderAt = (*Buffer)(nil) 25 | var _ io.ReadSeeker = (*Buffer)(nil) 26 | var _ io.Closer = (*Buffer)(nil) 27 | 28 | // Mode cache mode 29 | type Mode int 30 | 31 | const ( 32 | // FileMode cache reader's data to file 33 | FileMode = Mode(0) 34 | // MemoryMode cache reader's data to memory 35 | MemoryMode = Mode(1) 36 | ) 37 | 38 | var ( 39 | // ErrModeNotFound mode not found 40 | ErrModeNotFound = errors.New("mode not found") 41 | ) 42 | 43 | // Open buffer and use reader as data source 44 | func Open(r io.Reader, m Mode) (*Buffer, error) { 45 | switch m { 46 | case FileMode: 47 | f, err := os.CreateTemp("", "seekbuf-") 48 | if err != nil { 49 | return nil, err 50 | } 51 | return &Buffer{reader: r, mode: m, f: f}, nil 52 | case MemoryMode: 53 | return &Buffer{reader: r, mode: m}, nil 54 | } 55 | return nil, ErrModeNotFound 56 | } 57 | 58 | // Close and release 59 | func (b *Buffer) Close() error { 60 | b.lock.Lock() 61 | defer b.lock.Unlock() 62 | b.data = nil 63 | b.reader = nil 64 | if b.mode == FileMode { 65 | name := b.f.Name() 66 | b.f.Close() 67 | b.f = nil 68 | os.Remove(name) 69 | } 70 | return nil 71 | } 72 | 73 | func (s *Buffer) ReadAt(p []byte, off int64) (n int, err error) { 74 | 75 | s.lock.Lock() 76 | defer s.lock.Unlock() 77 | 78 | total := off + int64(len(p)) 79 | if total > s.len { 80 | more := total - s.len 81 | switch s.mode { 82 | case MemoryMode: 83 | buf := &bytes.Buffer{} 84 | rn, e := io.CopyN(buf, s.reader, more) 85 | err = e 86 | s.len += int64(rn) 87 | s.data = append(s.data, buf.Bytes()...) 88 | case FileMode: 89 | rn, e := io.CopyN(s.f, s.reader, more) 90 | err = e 91 | s.len += int64(rn) 92 | } 93 | } 94 | 95 | if s.mode == FileMode { 96 | return s.f.ReadAt(p, off) 97 | } 98 | return copy(p, s.data[off:]), err 99 | } 100 | 101 | func (b *Buffer) Read(p []byte) (n int, err error) { 102 | n, err = b.ReadAt(p, b.pos) 103 | b.lock.Lock() 104 | b.pos += int64(n) 105 | b.lock.Unlock() 106 | return n, err 107 | } 108 | 109 | // Seek sets the offset for the next Read or Write on the buffer to offset, interpreted according to whence: 110 | // 0 means relative to the origin of the buffer, 1 means relative to the current offset, and 2 means relative to the end. 111 | // It returns the new offset and an error, if any. 112 | func (b *Buffer) Seek(offset int64, whence int) (int64, error) { 113 | b.lock.Lock() 114 | defer b.lock.Unlock() 115 | o := offset 116 | switch whence { 117 | case io.SeekCurrent: 118 | // if o > 0 && b.pos+o >= int64(len(b.data)) { 119 | // return -1, fmt.Errorf("invalid offset %d", offset) 120 | // } 121 | b.pos += o 122 | case io.SeekStart: 123 | // if o > 0 && o >= int64(len(b.data)) { 124 | // return -1, fmt.Errorf("invalid offset %d", offset) 125 | // } 126 | b.pos = o 127 | case io.SeekEnd: 128 | // if int64(len(b.data))+o < 0 { 129 | // return -1, fmt.Errorf("invalid offset %d", offset) 130 | // } 131 | b.pos = int64(len(b.data)) + o 132 | default: 133 | return -1, fmt.Errorf("invalid whence %d", whence) 134 | } 135 | 136 | return int64(b.pos), nil 137 | } 138 | 139 | // return current buffer len 140 | func (b *Buffer) Size() int64 { 141 | b.lock.Lock() 142 | defer b.lock.Unlock() 143 | return b.len 144 | } 145 | -------------------------------------------------------------------------------- /pkg/seekbuf/seekbuf_test.go: -------------------------------------------------------------------------------- 1 | package seekbuf 2 | 3 | // TODO: test 4 | -------------------------------------------------------------------------------- /pkg/storager/afero.go: -------------------------------------------------------------------------------- 1 | package storager 2 | 3 | import ( 4 | "bufio" 5 | "io" 6 | "path/filepath" 7 | 8 | "github.com/iineva/ipa-server/pkg/storager/helper" 9 | "github.com/spf13/afero" 10 | ) 11 | 12 | type oferoStorager struct { 13 | fs afero.Fs 14 | } 15 | 16 | const ( 17 | oferoStoragerDirPerm = 0755 18 | WRITER_BUFFER_SIZE = 1024 * 1024 * 2 // 1M 19 | ) 20 | 21 | var _ Storager = (*oferoStorager)(nil) 22 | 23 | func NewAferoStorager(fs afero.Fs) Storager { 24 | return &oferoStorager{fs: fs} 25 | } 26 | 27 | func NewOsFileStorager(basepath string) Storager { 28 | return NewAferoStorager(afero.NewBasePathFs(afero.NewOsFs(), basepath)) 29 | } 30 | 31 | func NewMemStorager() Storager { 32 | return NewAferoStorager(afero.NewMemMapFs()) 33 | } 34 | 35 | func (f *oferoStorager) Save(name string, reader io.Reader) error { 36 | dir := filepath.Dir(name) 37 | if err := f.fs.MkdirAll(dir, oferoStoragerDirPerm); err != nil { 38 | return err 39 | } 40 | fi, err := f.fs.Create(name) 41 | defer func() { 42 | _ = fi.Close() 43 | }() 44 | if err != nil { 45 | return err 46 | } 47 | 48 | // write with buffer 49 | w := bufio.NewWriterSize(fi, WRITER_BUFFER_SIZE) 50 | _, err = io.Copy(w, reader) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | err = w.Flush() 56 | if err != nil { 57 | return err 58 | } 59 | 60 | return err 61 | } 62 | 63 | func (f *oferoStorager) OpenMetadata(name string) (io.ReadCloser, error) { 64 | return f.fs.Open(name) 65 | } 66 | 67 | func (f *oferoStorager) Delete(name string) error { 68 | err := f.fs.Remove(name) 69 | if err != nil { 70 | return err 71 | } 72 | // auto delete empty dir 73 | err = f.deleteEmptyDir(filepath.Dir(name)) 74 | if err != nil { 75 | // NOTE: ignore error 76 | } 77 | return nil 78 | } 79 | 80 | func (f *oferoStorager) deleteEmptyDir(name string) error { 81 | name = filepath.Clean(name) 82 | if name == "." { 83 | return nil 84 | } 85 | 86 | err := f.fs.Remove(name) 87 | if err != nil { 88 | return err 89 | } 90 | 91 | return f.deleteEmptyDir(filepath.Dir(name)) 92 | } 93 | 94 | func (f *oferoStorager) Move(src, dest string) error { 95 | err := f.fs.MkdirAll(filepath.Dir(dest), oferoStoragerDirPerm) 96 | if err != nil { 97 | return err 98 | } 99 | return f.fs.Rename(src, dest) 100 | } 101 | 102 | func (f *oferoStorager) PublicURL(publicURL, name string) (string, error) { 103 | return helper.UrlJoin(publicURL, name) 104 | } 105 | -------------------------------------------------------------------------------- /pkg/storager/alioss.go: -------------------------------------------------------------------------------- 1 | package storager 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | 7 | "github.com/aliyun/aliyun-oss-go-sdk/oss" 8 | "github.com/iineva/ipa-server/pkg/storager/helper" 9 | ) 10 | 11 | type aliossStorager struct { 12 | client *oss.Client 13 | bucket *oss.Bucket 14 | domain string 15 | } 16 | 17 | var _ Storager = (*aliossStorager)(nil) 18 | 19 | // endpoint: https://help.aliyun.com/document_detail/31837.htm 20 | func NewAliOssStorager(endpoint, accessKeyId, accessKeySecret, bucketName, domain string) (Storager, error) { 21 | client, err := oss.New(endpoint, accessKeyId, accessKeySecret, oss.Timeout(10, 120)) 22 | if err != nil { 23 | return nil, err 24 | } 25 | bucket, err := client.Bucket(bucketName) 26 | if err != nil { 27 | return nil, err 28 | } 29 | return &aliossStorager{client: client, bucket: bucket, domain: domain}, nil 30 | } 31 | 32 | func (a *aliossStorager) Save(name string, reader io.Reader) error { 33 | r := ioutil.NopCloser(reader) // avoid oss SDK to close reader 34 | return a.bucket.PutObject(name, r) 35 | } 36 | 37 | func (a *aliossStorager) OpenMetadata(name string) (io.ReadCloser, error) { 38 | return a.bucket.GetObject(name) 39 | } 40 | 41 | func (a *aliossStorager) Delete(name string) error { 42 | return a.bucket.DeleteObject(name) 43 | } 44 | 45 | func (a *aliossStorager) Move(src, dest string) error { 46 | _, err := a.bucket.CopyObject(src, dest) 47 | if err != nil { 48 | return err 49 | } 50 | return a.bucket.DeleteObject(src) 51 | } 52 | 53 | func (a *aliossStorager) PublicURL(publicURL, name string) (string, error) { 54 | return helper.UrlJoin(a.domain, name) 55 | } 56 | -------------------------------------------------------------------------------- /pkg/storager/alioss_test.go: -------------------------------------------------------------------------------- 1 | package storager 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestAliOss(t *testing.T) { 8 | 9 | endpoint := "oss-cn-shenzhen.aliyuncs.com" 10 | accessKeyId := "" 11 | accessKeySecret := "" 12 | bucketName := "" 13 | domain := "" 14 | 15 | a, err := NewAliOssStorager(endpoint, accessKeyId, accessKeySecret, bucketName, domain) 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | 20 | testStorager(a, t) 21 | } 22 | -------------------------------------------------------------------------------- /pkg/storager/basepath.go: -------------------------------------------------------------------------------- 1 | package storager 2 | 3 | import ( 4 | "io" 5 | "path/filepath" 6 | ) 7 | 8 | type basepathStorager struct { 9 | base string 10 | s Storager 11 | } 12 | 13 | var _ Storager = (*basepathStorager)(nil) 14 | 15 | func NewBasePathStorager(basepath string, store Storager) Storager { 16 | return &basepathStorager{base: basepath, s: store} 17 | } 18 | 19 | func (b *basepathStorager) Save(name string, reader io.Reader) error { 20 | return b.s.Save(filepath.Join(b.base, name), reader) 21 | } 22 | 23 | func (b *basepathStorager) OpenMetadata(name string) (io.ReadCloser, error) { 24 | return b.s.OpenMetadata(filepath.Join(b.base, name)) 25 | } 26 | 27 | func (b *basepathStorager) Delete(name string) error { 28 | return b.s.Delete(filepath.Join(b.base, name)) 29 | } 30 | 31 | func (b *basepathStorager) Move(src, dest string) error { 32 | return b.s.Move(filepath.Join(b.base, src), filepath.Join(b.base, dest)) 33 | } 34 | 35 | func (b *basepathStorager) PublicURL(publicURL, name string) (string, error) { 36 | return b.s.PublicURL(publicURL, filepath.Join(b.base, name)) 37 | } 38 | -------------------------------------------------------------------------------- /pkg/storager/helper/helper.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "io" 5 | "net/url" 6 | "path/filepath" 7 | ) 8 | 9 | type CallbackAfterReaderClose struct { 10 | cb func() error 11 | reader io.ReadCloser 12 | } 13 | 14 | func NewCallbackAfterReaderClose(reader io.ReadCloser, cb func() error) io.ReadCloser { 15 | return &CallbackAfterReaderClose{reader: reader, cb: cb} 16 | } 17 | 18 | func (d *CallbackAfterReaderClose) Close() error { 19 | if err := d.reader.Close(); err != nil { 20 | return err 21 | } 22 | return d.cb() 23 | } 24 | 25 | func (d *CallbackAfterReaderClose) Read(p []byte) (int, error) { 26 | return d.reader.Read(p) 27 | } 28 | 29 | func UrlJoin(u string, p string) (string, error) { 30 | d, err := url.Parse(u) 31 | if err != nil { 32 | return "", err 33 | } 34 | d.Path = filepath.Join(d.Path, p) 35 | return d.String(), nil 36 | } 37 | -------------------------------------------------------------------------------- /pkg/storager/qiniu.go: -------------------------------------------------------------------------------- 1 | package storager 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | 10 | "github.com/qiniu/go-sdk/v7/auth" 11 | "github.com/qiniu/go-sdk/v7/auth/qbox" 12 | "github.com/qiniu/go-sdk/v7/storage" 13 | 14 | "github.com/iineva/ipa-server/pkg/storager/helper" 15 | "github.com/iineva/ipa-server/pkg/uuid" 16 | ) 17 | 18 | type qiniuStorager struct { 19 | bucket string 20 | accessKey string 21 | secretKey string 22 | domain string 23 | config *storage.Config 24 | } 25 | 26 | var _ Storager = (*qiniuStorager)(nil) 27 | 28 | var ( 29 | ErrQiniuZoneCodeNotFound = errors.New("qiniu zone code not found") 30 | ) 31 | 32 | // zone option: huadong:z0 huabei:z1 huanan:z2 northAmerica:na0 singapore:as0 fogCnEast1:fog-cn-east-1 33 | // domain required: https://file.example.com 34 | func NewQiniuStorager(zone, accessKey, secretKey, bucket, domain string) (Storager, error) { 35 | config := &storage.Config{ 36 | UseHTTPS: true, 37 | UseCdnDomains: false, 38 | } 39 | // try get zone 40 | if zone != "" { 41 | z, ok := storage.GetRegionByID(storage.RegionID(zone)) 42 | if !ok { 43 | return nil, ErrQiniuZoneCodeNotFound 44 | } 45 | config.Zone = &z 46 | } 47 | 48 | return &qiniuStorager{ 49 | bucket: bucket, 50 | accessKey: accessKey, 51 | secretKey: secretKey, 52 | config: config, 53 | domain: domain, 54 | }, nil 55 | } 56 | 57 | func (q *qiniuStorager) newMac() *auth.Credentials { 58 | return qbox.NewMac(q.accessKey, q.secretKey) 59 | } 60 | 61 | func (q *qiniuStorager) newUploadToken(keyToOverwrite string) string { 62 | putPolicy := storage.PutPolicy{ 63 | Scope: fmt.Sprintf("%s:%s", q.bucket, keyToOverwrite), 64 | } 65 | return putPolicy.UploadToken(q.newMac()) 66 | } 67 | 68 | func (q *qiniuStorager) newBucketManager() *storage.BucketManager { 69 | return storage.NewBucketManager(q.newMac(), q.config) 70 | } 71 | 72 | func (q *qiniuStorager) upload(name string, reader io.Reader) (*storage.PutRet, error) { 73 | // use FormUploader to ensure that the front-end progress is consistent with the back-end progress 74 | uploader := storage.NewFormUploader(q.config) 75 | ret := &storage.PutRet{} 76 | putExtra := storage.PutExtra{} 77 | size := int64(-1) 78 | err := uploader.Put(context.Background(), ret, q.newUploadToken(name), name, reader, size, &putExtra) 79 | if err != nil { 80 | return nil, err 81 | } 82 | return ret, nil 83 | } 84 | 85 | func (q *qiniuStorager) delete(name string) error { 86 | return q.newBucketManager().Delete(q.bucket, name) 87 | } 88 | 89 | func (q *qiniuStorager) copy(src string, dest string) error { 90 | return q.newBucketManager().Copy(q.bucket, src, q.bucket, dest, true) 91 | } 92 | 93 | func (q *qiniuStorager) Save(name string, reader io.Reader) error { 94 | _, err := q.upload(name, reader) 95 | return err 96 | } 97 | 98 | func (q *qiniuStorager) OpenMetadata(name string) (io.ReadCloser, error) { 99 | 100 | // copy to random file name to fix CDN cache 101 | // don not use refresh API, because it has rate limit 102 | targetName := fmt.Sprintf("temp-%v.json", uuid.NewString()) 103 | err := q.copy(name, targetName) 104 | if err != nil { 105 | return nil, err 106 | } 107 | 108 | u := storage.MakePublicURL(q.domain, targetName) 109 | resp, err := http.Get(u) 110 | if err != nil { 111 | return nil, err 112 | } 113 | return helper.NewCallbackAfterReaderClose(resp.Body, func() error { 114 | return q.delete(targetName) 115 | }), err 116 | } 117 | 118 | func (q *qiniuStorager) Delete(name string) error { 119 | return q.delete(name) 120 | } 121 | 122 | func (q *qiniuStorager) Move(src, dest string) error { 123 | return q.newBucketManager().Move(q.bucket, src, q.bucket, dest, true) 124 | } 125 | 126 | func (q *qiniuStorager) PublicURL(_, name string) (string, error) { 127 | return helper.UrlJoin(q.domain, name) 128 | } 129 | -------------------------------------------------------------------------------- /pkg/storager/qiniu_test.go: -------------------------------------------------------------------------------- 1 | package storager 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestQiniuUpload(t *testing.T) { 8 | zone := "" 9 | accessKeyId := "" 10 | accessKeySecret := "" 11 | bucketName := "" 12 | domain := "" 13 | 14 | q, err := NewQiniuStorager(zone, accessKeyId, accessKeySecret, bucketName, domain) 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | 19 | testStorager(q, t) 20 | } 21 | -------------------------------------------------------------------------------- /pkg/storager/s3.go: -------------------------------------------------------------------------------- 1 | package storager 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "io/ioutil" 7 | "net/url" 8 | 9 | "github.com/aws/aws-sdk-go-v2/aws" 10 | v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4" 11 | "github.com/aws/aws-sdk-go-v2/config" 12 | "github.com/aws/aws-sdk-go-v2/credentials" 13 | "github.com/aws/aws-sdk-go-v2/service/s3" 14 | "github.com/iineva/ipa-server/pkg/storager/helper" 15 | ) 16 | 17 | type s3Storager struct { 18 | endpoint string 19 | ak string 20 | sk string 21 | bucket string 22 | domain string 23 | client *s3.Client 24 | } 25 | 26 | func NewS3Storager(endpoint, ak, sk, bucket, domain string) (Storager, error) { 27 | 28 | u, err := url.Parse(endpoint) 29 | if err != nil { 30 | return nil, err 31 | } 32 | if u.Scheme == "" { 33 | u.Scheme = "https" 34 | } 35 | 36 | customResolver := aws.EndpointResolverFunc(func(service, region string) (aws.Endpoint, error) { 37 | return aws.Endpoint{ 38 | URL: u.String(), 39 | }, nil 40 | }) 41 | 42 | cfg, err := config.LoadDefaultConfig( 43 | context.Background(), 44 | config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(ak, sk, "")), 45 | config.WithEndpointResolver(customResolver), 46 | ) 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | return &s3Storager{ 52 | endpoint: endpoint, 53 | ak: ak, 54 | sk: sk, 55 | bucket: bucket, 56 | domain: domain, 57 | client: s3.NewFromConfig(cfg), 58 | }, nil 59 | } 60 | 61 | func (s *s3Storager) Save(name string, reader io.Reader) error { 62 | r := ioutil.NopCloser(reader) // avoid oss SDK to close reader 63 | _, err := s.client.PutObject(context.Background(), &s3.PutObjectInput{ 64 | Bucket: aws.String(s.bucket), 65 | Key: aws.String(name), 66 | Body: r, 67 | }, s3.WithAPIOptions( 68 | v4.SwapComputePayloadSHA256ForUnsignedPayloadMiddleware, 69 | )) 70 | return err 71 | } 72 | 73 | func (s *s3Storager) OpenMetadata(name string) (io.ReadCloser, error) { 74 | out, err := s.client.GetObject(context.Background(), &s3.GetObjectInput{ 75 | Bucket: aws.String(s.bucket), 76 | Key: aws.String(name), 77 | }) 78 | return out.Body, err 79 | } 80 | 81 | func (s *s3Storager) Delete(name string) error { 82 | _, err := s.client.DeleteObject(context.Background(), &s3.DeleteObjectInput{ 83 | Bucket: aws.String(s.bucket), 84 | Key: aws.String(name), 85 | }) 86 | return err 87 | } 88 | 89 | func (s *s3Storager) Move(src, dest string) error { 90 | _, err := s.client.CopyObject(context.Background(), &s3.CopyObjectInput{ 91 | Bucket: aws.String(s.bucket), 92 | CopySource: aws.String(s.bucket + "/" + src), 93 | Key: aws.String(dest), 94 | }) 95 | if err != nil { 96 | return err 97 | } 98 | return s.Delete(src) 99 | } 100 | 101 | func (s *s3Storager) PublicURL(publicURL, name string) (string, error) { 102 | return helper.UrlJoin(s.domain, name) 103 | } 104 | -------------------------------------------------------------------------------- /pkg/storager/s3_test.go: -------------------------------------------------------------------------------- 1 | package storager 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestS3(t *testing.T) { 8 | 9 | endpoint := "oss-cn-shenzhen.aliyuncs.com" 10 | accessKeyId := "" 11 | accessKeySecret := "" 12 | bucketName := "" 13 | domain := "" 14 | 15 | a, err := NewS3Storager(endpoint, accessKeyId, accessKeySecret, bucketName, domain) 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | 20 | testStorager(a, t) 21 | } 22 | -------------------------------------------------------------------------------- /pkg/storager/storager.go: -------------------------------------------------------------------------------- 1 | package storager 2 | 3 | import ( 4 | "io" 5 | ) 6 | 7 | type Storager interface { 8 | Save(name string, reader io.Reader) error 9 | OpenMetadata(name string) (io.ReadCloser, error) 10 | Delete(name string) error 11 | Move(src, dest string) error 12 | PublicURL(publicURL, name string) (string, error) 13 | } 14 | -------------------------------------------------------------------------------- /pkg/storager/test.go: -------------------------------------------------------------------------------- 1 | package storager 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func testStorager(s Storager, t *testing.T) { 9 | fileName := "../../public/img/default.png" 10 | name := "test.png" 11 | f, err := os.Open(fileName) 12 | defer f.Close() 13 | if err != nil { 14 | t.Fatal(err) 15 | } 16 | // save first 17 | if err := s.Save(name, f); err != nil { 18 | t.Fatal(err) 19 | } 20 | // overwite 21 | f2, err := os.Open(fileName) 22 | defer f.Close() 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | if err := s.Save(name, f2); err != nil { 27 | t.Fatal(err) 28 | } 29 | // open metadata 30 | if reader, err := s.OpenMetadata(name); err != nil { 31 | reader.Close() 32 | t.Fatal(err) 33 | } 34 | // delete file 35 | if err := s.Delete(name); err != nil { 36 | t.Fatal(err) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /pkg/uuid/uuid.go: -------------------------------------------------------------------------------- 1 | package uuid 2 | 3 | import "github.com/lithammer/shortuuid" 4 | 5 | func NewString() string { 6 | return shortuuid.New() 7 | } 8 | -------------------------------------------------------------------------------- /pkg/websocketfile/websocketfile.go: -------------------------------------------------------------------------------- 1 | // Open client side file from server side over websocket 2 | package websocketfile 3 | 4 | import ( 5 | "encoding/base64" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "math/rand" 10 | "net/http" 11 | "sync" 12 | "time" 13 | 14 | "github.com/gorilla/websocket" 15 | ) 16 | 17 | var upgrader = websocket.Upgrader{ 18 | ReadBufferSize: 1024, 19 | WriteBufferSize: 1024, 20 | } 21 | 22 | var ErrResponse = errors.New("ErrResponse") 23 | var _ io.Reader = (*websocketFile)(nil) 24 | var _ io.ReaderAt = (*websocketFile)(nil) 25 | 26 | type websocketFile struct { 27 | sync.RWMutex 28 | conn *websocket.Conn 29 | rand *rand.Rand 30 | offset int64 31 | size int64 32 | } 33 | 34 | type CommandType int32 35 | 36 | const ( 37 | CommandTypeReadAt CommandType = 1 38 | CommandTypeSize CommandType = 2 39 | CommandTypeName CommandType = 3 40 | CommandTypeDone CommandType = 4 41 | ) 42 | 43 | type Command struct { 44 | Command CommandType `json:"command"` // command type 45 | Param map[string]interface{} `json:"param"` // command param 46 | RequestId string `json:"requestId"` // requestId for check response 47 | } 48 | 49 | type WebsocketFile interface { 50 | io.ReaderAt 51 | io.Reader 52 | Size() (int64, error) 53 | Name() (string, error) 54 | Done(p map[string]interface{}) error 55 | } 56 | 57 | func NewWebsocketFile(w http.ResponseWriter, r *http.Request) (WebsocketFile, error) { 58 | conn, err := upgrader.Upgrade(w, r, nil) 59 | if err != nil { 60 | return nil, err 61 | } 62 | return &websocketFile{conn: conn, rand: rand.New(rand.NewSource(time.Now().UnixNano())), size: -1}, nil 63 | } 64 | 65 | func (w *websocketFile) setOffset(off int64) { 66 | w.Lock() 67 | defer w.Unlock() 68 | w.offset = off 69 | } 70 | 71 | func (w *websocketFile) getOffset() int64 { 72 | w.RLock() 73 | defer w.RUnlock() 74 | return w.offset 75 | } 76 | 77 | func (w *websocketFile) ReadAt(p []byte, off int64) (n int, err error) { 78 | 79 | // EOF 80 | size, err := w.Size() 81 | if err != nil { 82 | return 0, err 83 | } 84 | if off >= size { 85 | return 0, io.EOF 86 | } 87 | 88 | resp, err := w.request(CommandTypeReadAt, map[string]interface{}{ 89 | "offset": off, 90 | "length": len(p), 91 | }) 92 | if err != nil { 93 | return 0, err 94 | } 95 | 96 | data, ok := resp.Param["data"] 97 | if !ok { 98 | return 0, nil 99 | } 100 | 101 | d, ok := data.(string) 102 | if !ok { 103 | return 0, nil 104 | } 105 | 106 | if d == "" { 107 | return 0, io.EOF 108 | } 109 | 110 | // log.Printf("d: %v", d) 111 | buf, err := base64.StdEncoding.DecodeString(d) 112 | if err != nil { 113 | return 0, err 114 | } 115 | 116 | n = copy(p, buf) 117 | w.setOffset(off + int64(n)) 118 | 119 | return n, nil 120 | } 121 | 122 | func (w *websocketFile) Read(p []byte) (n int, err error) { 123 | return w.ReadAt(p, w.getOffset()) 124 | } 125 | 126 | func (w *websocketFile) Size() (n int64, err error) { 127 | 128 | // read from cache 129 | if w.size != -1 { 130 | return w.size, nil 131 | } 132 | 133 | resp, err := w.request(CommandTypeSize, nil) 134 | if err != nil { 135 | return 0, err 136 | } 137 | 138 | data, ok := resp.Param["size"] 139 | if !ok { 140 | return 0, nil 141 | } 142 | size, ok := data.(float64) 143 | if !ok { 144 | return 0, nil 145 | } 146 | 147 | // cache size 148 | w.size = int64(size) 149 | 150 | return w.size, nil 151 | } 152 | 153 | func (w *websocketFile) Name() (n string, err error) { 154 | resp, err := w.request(CommandTypeName, nil) 155 | if err != nil { 156 | return "", err 157 | } 158 | 159 | data, ok := resp.Param["name"] 160 | if !ok { 161 | return "", nil 162 | } 163 | name, ok := data.(string) 164 | if !ok { 165 | return "", nil 166 | } 167 | 168 | return name, nil 169 | } 170 | 171 | func (w *websocketFile) Done(p map[string]interface{}) error { 172 | err := w.send(CommandTypeDone, p) 173 | if err != nil { 174 | return err 175 | } 176 | return nil 177 | } 178 | 179 | func (w *websocketFile) send(typ CommandType, p map[string]interface{}) error { 180 | requestId := fmt.Sprintf("%d", rand.Uint64()) 181 | err := w.conn.WriteJSON(&Command{ 182 | Command: typ, 183 | RequestId: requestId, 184 | Param: p, 185 | }) 186 | if err != nil { 187 | return err 188 | } 189 | return nil 190 | } 191 | 192 | func (w *websocketFile) request(typ CommandType, p map[string]interface{}) (*Command, error) { 193 | requestId := fmt.Sprintf("%d", rand.Uint64()) 194 | err := w.conn.WriteJSON(&Command{ 195 | Command: typ, 196 | RequestId: requestId, 197 | Param: p, 198 | }) 199 | if err != nil { 200 | return nil, err 201 | } 202 | 203 | resp := &Command{} 204 | err = w.conn.ReadJSON(resp) 205 | if err != nil { 206 | return nil, err 207 | } 208 | if resp.Command != typ || resp.RequestId != requestId { 209 | return nil, ErrResponse 210 | } 211 | 212 | return resp, nil 213 | } 214 | -------------------------------------------------------------------------------- /public/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | IPA Server 17 | 93 | 94 | 95 | 96 |
97 |
98 | 109 | 110 | 243 | 244 | 245 | -------------------------------------------------------------------------------- /public/css/core.css: -------------------------------------------------------------------------------- 1 | /* list */ 2 | 3 | html, 4 | body { 5 | margin: 0; 6 | padding: 0; 7 | height: 100%; 8 | display: flex; 9 | flex-direction: column; 10 | } 11 | 12 | #list { 13 | display: block; 14 | padding: 1em 0; 15 | flex: 1 0 auto; 16 | } 17 | 18 | #list .row { 19 | text-decoration: none; 20 | padding: 0.75em; 21 | flex: 1; 22 | border-bottom: 0.5px solid #eee; 23 | display: flex; 24 | flex-direction: row; 25 | font-weight: 100; 26 | } 27 | 28 | #list .row img { 29 | width: 4em; 30 | height: 4em; 31 | border-radius: 20%; 32 | box-shadow: 0 0 4px #eee; 33 | } 34 | 35 | #list .row .center { 36 | flex: 1; 37 | color: #999; 38 | display: flex; 39 | flex-direction: column; 40 | margin: 0 0.5em; 41 | } 42 | 43 | #list .row .center .name { 44 | color: #666; 45 | font-size: 1.2em; 46 | flex: 1; 47 | } 48 | 49 | #list .row .center .date, 50 | #list .row .center .version { 51 | font-size: 0.75em; 52 | } 53 | 54 | #list .row .right { 55 | border: 1px solid #1890ff; 56 | border-radius: 4px; 57 | color: #1890ff; 58 | padding: 4px 14px; 59 | align-self: center; 60 | } 61 | 62 | #list .row .right:active { 63 | color: #333; 64 | border-color: #333; 65 | } 66 | 67 | #list .row .name { 68 | display: flex; 69 | align-items: center; 70 | } 71 | 72 | .tag { 73 | padding: 0px 3px; 74 | font-size: 10px; 75 | color: #1890ff; 76 | border: 1px #1890ff solid; 77 | border-radius: 2px; 78 | margin-left: 6px; 79 | } 80 | 81 | #list .row .icon-tag { 82 | width: 14px; 83 | height: auto; 84 | box-shadow: none; 85 | margin: 0 0 0 4px; 86 | } 87 | 88 | #list .row .icon-tag.android { 89 | width: 17px; 90 | } 91 | 92 | footer { 93 | text-align: center; 94 | display: flex; 95 | justify-content: center; 96 | align-items: center; 97 | align-content: center; 98 | gap: 4px; 99 | color: #666; 100 | flex-shrink: 0; 101 | padding: 120px 12px 20px 12px; 102 | flex-direction: column; 103 | font-size: 12px; 104 | } 105 | footer * { 106 | color: #666; 107 | } 108 | footer img { 109 | height: 20px; 110 | } 111 | -------------------------------------------------------------------------------- /public/img/android.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/img/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iineva/ipa-server/6faaeed6f57bdb780f0a840a8456d99e3cc3afcf/public/img/default.png -------------------------------------------------------------------------------- /public/img/ios.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | IPA Server 10 | 11 | 12 | 13 | 14 | 15 | 16 | 43 | 44 | 45 | 46 |
47 | 48 |
Add
49 |
50 |
51 | 62 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /public/js/core.js: -------------------------------------------------------------------------------- 1 | (function(exports) { 2 | 3 | dayjs.extend(window.dayjs_plugin_relativeTime) 4 | var lan = window.navigator.language 5 | if (lan.startsWith("zh")) { 6 | dayjs.locale("zh-cn") 7 | } 8 | 9 | // fetch with progress 10 | function fetch(url, opts = {}, onProgress) { 11 | return new Promise((res, rej) => { 12 | try { 13 | var xhr = new XMLHttpRequest() 14 | xhr.open(opts.method || 'get', url) 15 | for (var k in opts.headers || {}) 16 | xhr.setRequestHeader(k, opts.headers[k]) 17 | xhr.onload = e => { 18 | try { 19 | res(JSON.parse(e.target.responseText)) 20 | } catch (e) { 21 | rej(e) 22 | } 23 | } 24 | xhr.onerror = rej 25 | if (xhr.upload && onProgress) 26 | xhr.upload.onprogress = onProgress 27 | xhr.send(opts.body) 28 | } catch (e) { 29 | rej(e) 30 | } 31 | }); 32 | } 33 | 34 | function newUpload(file, _onProgress) { 35 | var onProgress = function(m) { 36 | _onProgress && _onProgress({ 37 | loaded: m.loaded, 38 | total: m.total, 39 | }) 40 | } 41 | return new Promise((res, rej) => { 42 | const u = location.origin 43 | .replace("https://", "wss://") 44 | .replace("http://", "ws://"); 45 | var ws = new WebSocket(u + "/api/upload/ws"); 46 | var CommandTypeReadAt = 1; 47 | var CommandTypeSize = 2; 48 | var CommandTypeName = 3; 49 | var CommandTypeDone = 4; 50 | 51 | function sendRequest(command, requestId, param) { 52 | var obj = { 53 | command: command, 54 | requestId: requestId, 55 | param: param, 56 | }; 57 | ws.send(JSON.stringify(obj)); 58 | } 59 | 60 | ws.onopen = function () { 61 | // console.log("ws opened"); 62 | }; 63 | 64 | ws.onmessage = function (evt) { 65 | var received_msg = evt.data; 66 | if (!received_msg) return; 67 | var msg = null; 68 | try { 69 | msg = JSON.parse(received_msg); 70 | } catch (err) { 71 | rej(err) 72 | console.error(err) 73 | } 74 | if (!msg) return; 75 | // console.log("onmessage", msg); 76 | switch (msg.command) { 77 | case CommandTypeReadAt: { 78 | var start = msg.param.offset; 79 | var end = Math.min(msg.param.offset + msg.param.length, file.size); 80 | if (end - start <= 0) { 81 | sendRequest(msg.command, msg.requestId, {data: ""}); 82 | onProgress({ 83 | loaded: end, 84 | total: file.size, 85 | }); 86 | return; 87 | } 88 | var reader = new FileReader(); 89 | reader.onload = function() { 90 | var text = reader.result; 91 | var data = text.substr(text.indexOf(',') + 1); 92 | sendRequest(msg.command, msg.requestId, {data: data}); 93 | onProgress({ 94 | loaded: end, 95 | total: file.size, 96 | end: end, 97 | }); 98 | }; 99 | reader.readAsDataURL(file.slice(start, end)); 100 | break 101 | } 102 | case CommandTypeSize: { 103 | sendRequest(msg.command, msg.requestId, { 104 | size: file.size, 105 | }); 106 | break 107 | } 108 | case CommandTypeName: { 109 | sendRequest(msg.command, msg.requestId, { 110 | name: file.name, 111 | }) 112 | break 113 | } 114 | case CommandTypeDone: { 115 | onProgress({ 116 | loaded: file.size, 117 | total: file.size, 118 | }) 119 | res(msg.param) 120 | break 121 | } 122 | } 123 | }; 124 | 125 | ws.onerror = function(e) { 126 | console.error("onerror"); 127 | rej(e) 128 | } 129 | 130 | ws.onclose = function () { 131 | // websocket closed 132 | // console.log("onclose"); 133 | }; 134 | 135 | }) 136 | } 137 | 138 | function getApiUrl(path) { 139 | return path 140 | } 141 | 142 | // return true if is PC 143 | function isPC() { 144 | const Agents = ["Android", "iPhone", "SymbianOS", "Windows Phone", "iPad", "iPod"] 145 | for (let v = 0; v < Agents.length; v++) { 146 | if (window.navigator.userAgent.indexOf(Agents[v]) > 0) { 147 | return false 148 | } 149 | } 150 | return true 151 | } 152 | 153 | function language() { 154 | return (navigator.language || navigator.browserLanguage) 155 | } 156 | 157 | // set locale for server 158 | document.cookie = `locale=${language()};` 159 | 160 | // localization string 161 | function langString(key) { 162 | const localStr = { 163 | 'Download': { 164 | 'zh-cn': '下载' 165 | }, 166 | 'Upload Date: ': { 167 | 'zh-cn': '更新时间:' 168 | }, 169 | 'Add': { 170 | 'zh-cn': '添加' 171 | }, 172 | 'Upload Done!': { 173 | 'zh-cn': '上传成功!' 174 | }, 175 | 'Download and Install': { 176 | 'zh-cn': '下载安装' 177 | }, 178 | 'Beta': { 179 | 'zh-cn': '内测版' 180 | }, 181 | 'Current': { 182 | 'zh-cn': '当前' 183 | }, 184 | 'Channel': { 185 | 'zh-cn': '渠道' 186 | }, 187 | 'Delete': { 188 | 'zh-cn': '删除' 189 | }, 190 | 'Back to home?': { 191 | 'zh-cn': '是否返回首页?' 192 | }, 193 | 'Confirm to Delete?': { 194 | 'zh-cn': '确认删除?' 195 | }, 196 | 'Delete Success!': { 197 | 'zh-cn': '删除成功!' 198 | }, 199 | } 200 | const lang = (localStr[key] || key)[language().toLowerCase()] 201 | return lang ? lang : key 202 | } 203 | 204 | // bytes to Human-readable string 205 | function sizeStr(size) { 206 | const K = 1024, 207 | M = 1024 * K, 208 | G = 1024 * M 209 | if (size > G) { 210 | return `${(size/G).toFixed(2)} GB` 211 | } else if (size > M) { 212 | return `${(size / M).toFixed(2)} MB` 213 | } else { 214 | return `${(size / K).toFixed(2)} KB` 215 | } 216 | } 217 | 218 | window.ipaInstall = function(event, plist) { 219 | event && event.stopPropagation() 220 | window.location.href = 'itms-services://?action=download-manifest&url=' + plist 221 | } 222 | 223 | window.goToLink = function(event, link) { 224 | event && event.stopPropagation() 225 | if (!link) return 226 | window.location.href = link 227 | } 228 | 229 | onInstallClick = function(row) { 230 | var needGoAppPage = !!( 231 | row.type === 0 ? 232 | (row.history || []).find(r => r.type === 1) : 233 | (row.history || []).find(r => r.type === 0) 234 | ) 235 | // if (needGoAppPage) { 236 | if (false) { 237 | return `goToLink(null, '/app?id=${row.id}')` 238 | } 239 | 240 | if (row.type == 0) { 241 | return `ipaInstall(event, '${row.plist}')` 242 | } 243 | return `goToLink(event, '${row.pkg}')` 244 | } 245 | 246 | function createItem(row) { 247 | var icons = [row.type === 0 ? 'ios' : 'android']; 248 | (row.history || []).forEach(r => { 249 | if (r.type === 0 && icons.indexOf('ios') === -1) { 250 | icons.push('ios') 251 | } 252 | if (r.type === 1 && icons.indexOf('android') === -1) { 253 | icons.push('android') 254 | } 255 | }); 256 | icons.sort().reverse() 257 | return ` 258 | 259 | 260 |
261 |
262 | ${row.name} 263 | ${icons.map(t => ``).join('')} 264 | ${row.current ? `${langString('Current')}` : ''} 265 |
266 |
267 | ${row.version}(Build ${row.build}) 268 | ${row.channel && IPA.langString('Channel') + ': '+row.channel || ''} 269 |
270 |
${IPA.langString('Upload Date: ')}${dayjs(row.date).fromNow()}
271 |
272 |
${IPA.langString('Download')}
273 |
274 | ` 275 | } 276 | 277 | exports.IPA = { 278 | fetch: fetch, 279 | isPC: isPC(), 280 | langString: langString, 281 | sizeStr: sizeStr, 282 | createItem: createItem, 283 | getApiUrl: getApiUrl, 284 | newUpload: newUpload, 285 | } 286 | 287 | })(window) -------------------------------------------------------------------------------- /public/js/dayjs.min.js: -------------------------------------------------------------------------------- 1 | !function(t,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):t.dayjs=n()}(this,function(){"use strict";var t="millisecond",n="second",e="minute",r="hour",i="day",s="week",u="month",o="quarter",a="year",h=/^(\d{4})-?(\d{1,2})-?(\d{0,2})[^0-9]*(\d{1,2})?:?(\d{1,2})?:?(\d{1,2})?.?(\d{1,3})?$/,f=/\[([^\]]+)]|Y{2,4}|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSS/g,c=function(t,n,e){var r=String(t);return!r||r.length>=n?t:""+Array(n+1-r.length).join(e)+t},d={s:c,z:function(t){var n=-t.utcOffset(),e=Math.abs(n),r=Math.floor(e/60),i=e%60;return(n<=0?"+":"-")+c(r,2,"0")+":"+c(i,2,"0")},m:function(t,n){var e=12*(n.year()-t.year())+(n.month()-t.month()),r=t.clone().add(e,u),i=n-r<0,s=t.clone().add(e+(i?-1:1),u);return Number(-(e+(n-r)/(i?r-s:s-r))||0)},a:function(t){return t<0?Math.ceil(t)||0:Math.floor(t)},p:function(h){return{M:u,y:a,w:s,d:i,D:"date",h:r,m:e,s:n,ms:t,Q:o}[h]||String(h||"").toLowerCase().replace(/s$/,"")},u:function(t){return void 0===t}},$={name:"en",weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_")},l="en",m={};m[l]=$;var y=function(t){return t instanceof v},M=function(t,n,e){var r;if(!t)return l;if("string"==typeof t)m[t]&&(r=t),n&&(m[t]=n,r=t);else{var i=t.name;m[i]=t,r=i}return e||(l=r),r},g=function(t,n,e){if(y(t))return t.clone();var r=n?"string"==typeof n?{format:n,pl:e}:n:{};return r.date=t,new v(r)},D=d;D.l=M,D.i=y,D.w=function(t,n){return g(t,{locale:n.$L,utc:n.$u,$offset:n.$offset})};var v=function(){function c(t){this.$L=this.$L||M(t.locale,null,!0),this.parse(t)}var d=c.prototype;return d.parse=function(t){this.$d=function(t){var n=t.date,e=t.utc;if(null===n)return new Date(NaN);if(D.u(n))return new Date;if(n instanceof Date)return new Date(n);if("string"==typeof n&&!/Z$/i.test(n)){var r=n.match(h);if(r)return e?new Date(Date.UTC(r[1],r[2]-1,r[3]||1,r[4]||0,r[5]||0,r[6]||0,r[7]||0)):new Date(r[1],r[2]-1,r[3]||1,r[4]||0,r[5]||0,r[6]||0,r[7]||0)}return new Date(n)}(t),this.init()},d.init=function(){var t=this.$d;this.$y=t.getFullYear(),this.$M=t.getMonth(),this.$D=t.getDate(),this.$W=t.getDay(),this.$H=t.getHours(),this.$m=t.getMinutes(),this.$s=t.getSeconds(),this.$ms=t.getMilliseconds()},d.$utils=function(){return D},d.isValid=function(){return!("Invalid Date"===this.$d.toString())},d.isSame=function(t,n){var e=g(t);return this.startOf(n)<=e&&e<=this.endOf(n)},d.isAfter=function(t,n){return g(t)0,m<=h.r||!h.r){1===m&&l>0&&(h=f[l-1]);var c=a[h.l];i="string"==typeof c?c.replace("%d",m):c(m,t,h.l,u);break}}return t?i:(u?a.future:a.past).replace("%s",i)};n.to=function(r,t){return o(r,t,this,!0)},n.from=function(r,t){return o(r,t,this)};var d=function(r){return r.$u?e.utc():e()};n.toNow=function(r){return this.to(d(this),r)},n.fromNow=function(r){return this.from(d(this),r)}}}); 2 | -------------------------------------------------------------------------------- /public/js/dayjs.zh-cn.min.js: -------------------------------------------------------------------------------- 1 | !function(_,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e(require("dayjs")):"function"==typeof define&&define.amd?define(["dayjs"],e):_.dayjs_locale_zh_cn=e(_.dayjs)}(this,function(_){"use strict";_=_&&_.hasOwnProperty("default")?_.default:_;var e={name:"zh-cn",weekdays:"星期日_星期一_星期二_星期三_星期四_星期五_星期六".split("_"),weekdaysShort:"周日_周一_周二_周三_周四_周五_周六".split("_"),weekdaysMin:"日_一_二_三_四_五_六".split("_"),months:"一月_二月_三月_四月_五月_六月_七月_八月_九月_十月_十一月_十二月".split("_"),monthsShort:"1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月".split("_"),ordinal:function(_,e){switch(e){case"W":return _+"周";default:return _+"日"}},weekStart:1,yearStart:4,formats:{LT:"HH:mm",LTS:"HH:mm:ss",L:"YYYY/MM/DD",LL:"YYYY年M月D日",LLL:"YYYY年M月D日Ah点mm分",LLLL:"YYYY年M月D日ddddAh点mm分",l:"YYYY/M/D",ll:"YYYY年M月D日",lll:"YYYY年M月D日 HH:mm",llll:"YYYY年M月D日dddd HH:mm"},relativeTime:{future:"%s内",past:"%s前",s:"几秒",m:"1 分钟",mm:"%d 分钟",h:"1 小时",hh:"%d 小时",d:"1 天",dd:"%d 天",M:"1 个月",MM:"%d 个月",y:"1 年",yy:"%d 年"},meridiem:function(_,e){var t=100*_+e;return t<600?"凌晨":t<900?"早上":t<1130?"上午":t<1230?"中午":t<1800?"下午":"晚上"}};return _.locale(e,null,!0),e}); 2 | -------------------------------------------------------------------------------- /public/js/layzr.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * layzr 2.2.2 3 | * https://github.com/callmecavs/layzr.js 4 | */ 5 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.Layzr=e()}(this,function(){"use strict";var t=Object.assign||function(t){for(var e=1;e1&&void 0!==arguments[1]&&arguments[1];return e?c[t].splice(c[t].indexOf(e),1):delete c[t],this}function i(t){for(var e=this,n=arguments.length,i=Array(n>1?n-1:0),o=1;o0&&void 0!==arguments[0]?arguments[0]:{},c=Object.create(null);return t({},o,{on:e,once:n,off:r,emit:i})},n=function(){function t(){return window.scrollY||window.pageYOffset}function n(){l=t(),r()}function r(){d||(window.requestAnimationFrame(function(){return u()}),d=!0)}function i(t){return t.getBoundingClientRect().top+l}function o(t){var e=l,n=e+v,r=i(t),o=r+t.offsetHeight,c=m.threshold/100*v;return o>=e-c&&r<=n+c}function c(t){if(g.emit("src:before",t),p&&t.hasAttribute(m.srcset))t.setAttribute("srcset",t.getAttribute(m.srcset));else{var e=w>1&&t.getAttribute(m.retina);t.setAttribute("src",e||t.getAttribute(m.normal))}g.emit("src:after",t),[m.normal,m.retina,m.srcset].forEach(function(e){return t.removeAttribute(e)}),a()}function s(t){var e=t?"addEventListener":"removeEventListener";return["scroll","resize"].forEach(function(t){return window[e](t,n)}),this}function u(){return v=window.innerHeight,h.forEach(function(t){return o(t)&&c(t)}),d=!1,this}function a(){return h=Array.prototype.slice.call(document.querySelectorAll("["+m.normal+"]")),this}var f=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},l=t(),d=void 0,h=void 0,v=void 0,m={normal:f.normal||"data-normal",retina:f.retina||"data-retina",srcset:f.srcset||"data-srcset",threshold:f.threshold||0},p=document.body.classList.contains("srcset")||"srcset"in document.createElement("img"),w=window.devicePixelRatio||window.screen.deviceXDPI/window.screen.logicalXDPI,g=e({handlers:s,check:u,update:a});return g};return n}); -------------------------------------------------------------------------------- /public/js/qrcode.min.js: -------------------------------------------------------------------------------- 1 | var QRCode;!function(){function a(a){this.mode=c.MODE_8BIT_BYTE,this.data=a,this.parsedData=[];for(var b=[],d=0,e=this.data.length;e>d;d++){var f=this.data.charCodeAt(d);f>65536?(b[0]=240|(1835008&f)>>>18,b[1]=128|(258048&f)>>>12,b[2]=128|(4032&f)>>>6,b[3]=128|63&f):f>2048?(b[0]=224|(61440&f)>>>12,b[1]=128|(4032&f)>>>6,b[2]=128|63&f):f>128?(b[0]=192|(1984&f)>>>6,b[1]=128|63&f):b[0]=f,this.parsedData=this.parsedData.concat(b)}this.parsedData.length!=this.data.length&&(this.parsedData.unshift(191),this.parsedData.unshift(187),this.parsedData.unshift(239))}function b(a,b){this.typeNumber=a,this.errorCorrectLevel=b,this.modules=null,this.moduleCount=0,this.dataCache=null,this.dataList=[]}function i(a,b){if(void 0==a.length)throw new Error(a.length+"/"+b);for(var c=0;c=f;f++){var h=0;switch(b){case d.L:h=l[f][0];break;case d.M:h=l[f][1];break;case d.Q:h=l[f][2];break;case d.H:h=l[f][3]}if(h>=e)break;c++}if(c>l.length)throw new Error("Too long data");return c}function s(a){var b=encodeURI(a).toString().replace(/\%[0-9a-fA-F]{2}/g,"a");return b.length+(b.length!=a?3:0)}a.prototype={getLength:function(){return this.parsedData.length},write:function(a){for(var b=0,c=this.parsedData.length;c>b;b++)a.put(this.parsedData[b],8)}},b.prototype={addData:function(b){var c=new a(b);this.dataList.push(c),this.dataCache=null},isDark:function(a,b){if(0>a||this.moduleCount<=a||0>b||this.moduleCount<=b)throw new Error(a+","+b);return this.modules[a][b]},getModuleCount:function(){return this.moduleCount},make:function(){this.makeImpl(!1,this.getBestMaskPattern())},makeImpl:function(a,c){this.moduleCount=4*this.typeNumber+17,this.modules=new Array(this.moduleCount);for(var d=0;d=7&&this.setupTypeNumber(a),null==this.dataCache&&(this.dataCache=b.createData(this.typeNumber,this.errorCorrectLevel,this.dataList)),this.mapData(this.dataCache,c)},setupPositionProbePattern:function(a,b){for(var c=-1;7>=c;c++)if(!(-1>=a+c||this.moduleCount<=a+c))for(var d=-1;7>=d;d++)-1>=b+d||this.moduleCount<=b+d||(this.modules[a+c][b+d]=c>=0&&6>=c&&(0==d||6==d)||d>=0&&6>=d&&(0==c||6==c)||c>=2&&4>=c&&d>=2&&4>=d?!0:!1)},getBestMaskPattern:function(){for(var a=0,b=0,c=0;8>c;c++){this.makeImpl(!0,c);var d=f.getLostPoint(this);(0==c||a>d)&&(a=d,b=c)}return b},createMovieClip:function(a,b,c){var d=a.createEmptyMovieClip(b,c),e=1;this.make();for(var f=0;f=g;g++)for(var h=-2;2>=h;h++)this.modules[d+g][e+h]=-2==g||2==g||-2==h||2==h||0==g&&0==h?!0:!1}},setupTypeNumber:function(a){for(var b=f.getBCHTypeNumber(this.typeNumber),c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[Math.floor(c/3)][c%3+this.moduleCount-8-3]=d}for(var c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[c%3+this.moduleCount-8-3][Math.floor(c/3)]=d}},setupTypeInfo:function(a,b){for(var c=this.errorCorrectLevel<<3|b,d=f.getBCHTypeInfo(c),e=0;15>e;e++){var g=!a&&1==(1&d>>e);6>e?this.modules[e][8]=g:8>e?this.modules[e+1][8]=g:this.modules[this.moduleCount-15+e][8]=g}for(var e=0;15>e;e++){var g=!a&&1==(1&d>>e);8>e?this.modules[8][this.moduleCount-e-1]=g:9>e?this.modules[8][15-e-1+1]=g:this.modules[8][15-e-1]=g}this.modules[this.moduleCount-8][8]=!a},mapData:function(a,b){for(var c=-1,d=this.moduleCount-1,e=7,g=0,h=this.moduleCount-1;h>0;h-=2)for(6==h&&h--;;){for(var i=0;2>i;i++)if(null==this.modules[d][h-i]){var j=!1;g>>e));var k=f.getMask(b,d,h-i);k&&(j=!j),this.modules[d][h-i]=j,e--,-1==e&&(g++,e=7)}if(d+=c,0>d||this.moduleCount<=d){d-=c,c=-c;break}}}},b.PAD0=236,b.PAD1=17,b.createData=function(a,c,d){for(var e=j.getRSBlocks(a,c),g=new k,h=0;h8*l)throw new Error("code length overflow. ("+g.getLengthInBits()+">"+8*l+")");for(g.getLengthInBits()+4<=8*l&&g.put(0,4);0!=g.getLengthInBits()%8;)g.putBit(!1);for(;;){if(g.getLengthInBits()>=8*l)break;if(g.put(b.PAD0,8),g.getLengthInBits()>=8*l)break;g.put(b.PAD1,8)}return b.createBytes(g,e)},b.createBytes=function(a,b){for(var c=0,d=0,e=0,g=new Array(b.length),h=new Array(b.length),j=0;j=0?p.get(q):0}}for(var r=0,m=0;mm;m++)for(var j=0;jm;m++)for(var j=0;j=0;)b^=f.G15<=0;)b^=f.G18<>>=1;return b},getPatternPosition:function(a){return f.PATTERN_POSITION_TABLE[a-1]},getMask:function(a,b,c){switch(a){case e.PATTERN000:return 0==(b+c)%2;case e.PATTERN001:return 0==b%2;case e.PATTERN010:return 0==c%3;case e.PATTERN011:return 0==(b+c)%3;case e.PATTERN100:return 0==(Math.floor(b/2)+Math.floor(c/3))%2;case e.PATTERN101:return 0==b*c%2+b*c%3;case e.PATTERN110:return 0==(b*c%2+b*c%3)%2;case e.PATTERN111:return 0==(b*c%3+(b+c)%2)%2;default:throw new Error("bad maskPattern:"+a)}},getErrorCorrectPolynomial:function(a){for(var b=new i([1],0),c=0;a>c;c++)b=b.multiply(new i([1,g.gexp(c)],0));return b},getLengthInBits:function(a,b){if(b>=1&&10>b)switch(a){case c.MODE_NUMBER:return 10;case c.MODE_ALPHA_NUM:return 9;case c.MODE_8BIT_BYTE:return 8;case c.MODE_KANJI:return 8;default:throw new Error("mode:"+a)}else if(27>b)switch(a){case c.MODE_NUMBER:return 12;case c.MODE_ALPHA_NUM:return 11;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 10;default:throw new Error("mode:"+a)}else{if(!(41>b))throw new Error("type:"+b);switch(a){case c.MODE_NUMBER:return 14;case c.MODE_ALPHA_NUM:return 13;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 12;default:throw new Error("mode:"+a)}}},getLostPoint:function(a){for(var b=a.getModuleCount(),c=0,d=0;b>d;d++)for(var e=0;b>e;e++){for(var f=0,g=a.isDark(d,e),h=-1;1>=h;h++)if(!(0>d+h||d+h>=b))for(var i=-1;1>=i;i++)0>e+i||e+i>=b||(0!=h||0!=i)&&g==a.isDark(d+h,e+i)&&f++;f>5&&(c+=3+f-5)}for(var d=0;b-1>d;d++)for(var e=0;b-1>e;e++){var j=0;a.isDark(d,e)&&j++,a.isDark(d+1,e)&&j++,a.isDark(d,e+1)&&j++,a.isDark(d+1,e+1)&&j++,(0==j||4==j)&&(c+=3)}for(var d=0;b>d;d++)for(var e=0;b-6>e;e++)a.isDark(d,e)&&!a.isDark(d,e+1)&&a.isDark(d,e+2)&&a.isDark(d,e+3)&&a.isDark(d,e+4)&&!a.isDark(d,e+5)&&a.isDark(d,e+6)&&(c+=40);for(var e=0;b>e;e++)for(var d=0;b-6>d;d++)a.isDark(d,e)&&!a.isDark(d+1,e)&&a.isDark(d+2,e)&&a.isDark(d+3,e)&&a.isDark(d+4,e)&&!a.isDark(d+5,e)&&a.isDark(d+6,e)&&(c+=40);for(var k=0,e=0;b>e;e++)for(var d=0;b>d;d++)a.isDark(d,e)&&k++;var l=Math.abs(100*k/b/b-50)/5;return c+=10*l}},g={glog:function(a){if(1>a)throw new Error("glog("+a+")");return g.LOG_TABLE[a]},gexp:function(a){for(;0>a;)a+=255;for(;a>=256;)a-=255;return g.EXP_TABLE[a]},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)},h=0;8>h;h++)g.EXP_TABLE[h]=1<h;h++)g.EXP_TABLE[h]=g.EXP_TABLE[h-4]^g.EXP_TABLE[h-5]^g.EXP_TABLE[h-6]^g.EXP_TABLE[h-8];for(var h=0;255>h;h++)g.LOG_TABLE[g.EXP_TABLE[h]]=h;i.prototype={get:function(a){return this.num[a]},getLength:function(){return this.num.length},multiply:function(a){for(var b=new Array(this.getLength()+a.getLength()-1),c=0;cf;f++)for(var g=c[3*f+0],h=c[3*f+1],i=c[3*f+2],k=0;g>k;k++)e.push(new j(h,i));return e},j.getRsBlockTable=function(a,b){switch(b){case d.L:return j.RS_BLOCK_TABLE[4*(a-1)+0];case d.M:return j.RS_BLOCK_TABLE[4*(a-1)+1];case d.Q:return j.RS_BLOCK_TABLE[4*(a-1)+2];case d.H:return j.RS_BLOCK_TABLE[4*(a-1)+3];default:return void 0}},k.prototype={get:function(a){var b=Math.floor(a/8);return 1==(1&this.buffer[b]>>>7-a%8)},put:function(a,b){for(var c=0;b>c;c++)this.putBit(1==(1&a>>>b-c-1))},getLengthInBits:function(){return this.length},putBit:function(a){var b=Math.floor(this.length/8);this.buffer.length<=b&&this.buffer.push(0),a&&(this.buffer[b]|=128>>>this.length%8),this.length++}};var l=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]],o=function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){function g(a,b){var c=document.createElementNS("http://www.w3.org/2000/svg",a);for(var d in b)b.hasOwnProperty(d)&&c.setAttribute(d,b[d]);return c}var b=this._htOption,c=this._el,d=a.getModuleCount();Math.floor(b.width/d),Math.floor(b.height/d),this.clear();var h=g("svg",{viewBox:"0 0 "+String(d)+" "+String(d),width:"100%",height:"100%",fill:b.colorLight});h.setAttributeNS("http://www.w3.org/2000/xmlns/","xmlns:xlink","http://www.w3.org/1999/xlink"),c.appendChild(h),h.appendChild(g("rect",{fill:b.colorDark,width:"1",height:"1",id:"template"}));for(var i=0;d>i;i++)for(var j=0;d>j;j++)if(a.isDark(i,j)){var k=g("use",{x:String(i),y:String(j)});k.setAttributeNS("http://www.w3.org/1999/xlink","href","#template"),h.appendChild(k)}},a.prototype.clear=function(){for(;this._el.hasChildNodes();)this._el.removeChild(this._el.lastChild)},a}(),p="svg"===document.documentElement.tagName.toLowerCase(),q=p?o:m()?function(){function a(){this._elImage.src=this._elCanvas.toDataURL("image/png"),this._elImage.style.display="block",this._elCanvas.style.display="none"}function d(a,b){var c=this;if(c._fFail=b,c._fSuccess=a,null===c._bSupportDataURI){var d=document.createElement("img"),e=function(){c._bSupportDataURI=!1,c._fFail&&_fFail.call(c)},f=function(){c._bSupportDataURI=!0,c._fSuccess&&c._fSuccess.call(c)};return d.onabort=e,d.onerror=e,d.onload=f,d.src="data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==",void 0}c._bSupportDataURI===!0&&c._fSuccess?c._fSuccess.call(c):c._bSupportDataURI===!1&&c._fFail&&c._fFail.call(c)}if(this._android&&this._android<=2.1){var b=1/window.devicePixelRatio,c=CanvasRenderingContext2D.prototype.drawImage;CanvasRenderingContext2D.prototype.drawImage=function(a,d,e,f,g,h,i,j){if("nodeName"in a&&/img/i.test(a.nodeName))for(var l=arguments.length-1;l>=1;l--)arguments[l]=arguments[l]*b;else"undefined"==typeof j&&(arguments[1]*=b,arguments[2]*=b,arguments[3]*=b,arguments[4]*=b);c.apply(this,arguments)}}var e=function(a,b){this._bIsPainted=!1,this._android=n(),this._htOption=b,this._elCanvas=document.createElement("canvas"),this._elCanvas.width=b.width,this._elCanvas.height=b.height,a.appendChild(this._elCanvas),this._el=a,this._oContext=this._elCanvas.getContext("2d"),this._bIsPainted=!1,this._elImage=document.createElement("img"),this._elImage.style.display="none",this._el.appendChild(this._elImage),this._bSupportDataURI=null};return e.prototype.draw=function(a){var b=this._elImage,c=this._oContext,d=this._htOption,e=a.getModuleCount(),f=d.width/e,g=d.height/e,h=Math.round(f),i=Math.round(g);b.style.display="none",this.clear();for(var j=0;e>j;j++)for(var k=0;e>k;k++){var l=a.isDark(j,k),m=k*f,n=j*g;c.strokeStyle=l?d.colorDark:d.colorLight,c.lineWidth=1,c.fillStyle=l?d.colorDark:d.colorLight,c.fillRect(m,n,f,g),c.strokeRect(Math.floor(m)+.5,Math.floor(n)+.5,h,i),c.strokeRect(Math.ceil(m)-.5,Math.ceil(n)-.5,h,i)}this._bIsPainted=!0},e.prototype.makeImage=function(){this._bIsPainted&&d.call(this,a)},e.prototype.isPainted=function(){return this._bIsPainted},e.prototype.clear=function(){this._oContext.clearRect(0,0,this._elCanvas.width,this._elCanvas.height),this._bIsPainted=!1},e.prototype.round=function(a){return a?Math.floor(1e3*a)/1e3:a},e}():function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){for(var b=this._htOption,c=this._el,d=a.getModuleCount(),e=Math.floor(b.width/d),f=Math.floor(b.height/d),g=[''],h=0;d>h;h++){g.push("");for(var i=0;d>i;i++)g.push('');g.push("")}g.push("
"),c.innerHTML=g.join("");var j=c.childNodes[0],k=(b.width-j.offsetWidth)/2,l=(b.height-j.offsetHeight)/2;k>0&&l>0&&(j.style.margin=l+"px "+k+"px")},a.prototype.clear=function(){this._el.innerHTML=""},a}();QRCode=function(a,b){if(this._htOption={width:256,height:256,typeNumber:4,colorDark:"#000000",colorLight:"#ffffff",correctLevel:d.H},"string"==typeof b&&(b={text:b}),b)for(var c in b)this._htOption[c]=b[c];"string"==typeof a&&(a=document.getElementById(a)),this._android=n(),this._el=a,this._oQRCode=null,this._oDrawing=new q(this._el,this._htOption),this._htOption.text&&this.makeCode(this._htOption.text)},QRCode.prototype.makeCode=function(a){this._oQRCode=new b(r(a,this._htOption.correctLevel),this._htOption.correctLevel),this._oQRCode.addData(a),this._oQRCode.make(),this._el.title=a,this._oDrawing.draw(this._oQRCode),this.makeImage()},QRCode.prototype.makeImage=function(){"function"==typeof this._oDrawing.makeImage&&(!this._android||this._android>=3)&&this._oDrawing.makeImage()},QRCode.prototype.clear=function(){this._oDrawing.clear()},QRCode.CorrectLevel=d}(); -------------------------------------------------------------------------------- /public/public.go: -------------------------------------------------------------------------------- 1 | package public 2 | 3 | import "embed" 4 | 5 | //go:embed app 6 | //go:embed css 7 | //go:embed img 8 | //go:embed js 9 | //go:embed *.html 10 | var FS embed.FS 11 | -------------------------------------------------------------------------------- /snapshot/en/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iineva/ipa-server/6faaeed6f57bdb780f0a840a8456d99e3cc3afcf/snapshot/en/1.jpg -------------------------------------------------------------------------------- /snapshot/en/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iineva/ipa-server/6faaeed6f57bdb780f0a840a8456d99e3cc3afcf/snapshot/en/2.jpg -------------------------------------------------------------------------------- /snapshot/zh-cn/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iineva/ipa-server/6faaeed6f57bdb780f0a840a8456d99e3cc3afcf/snapshot/zh-cn/1.jpg -------------------------------------------------------------------------------- /snapshot/zh-cn/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iineva/ipa-server/6faaeed6f57bdb780f0a840a8456d99e3cc3afcf/snapshot/zh-cn/2.jpg --------------------------------------------------------------------------------