diff --git a/.gitignore b/.gitignore index c2b41ec..2978a2f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ log/ # Ignore docker database volume db-data/ data/ +data-dev/ # Ignore binary file and only file safeu-backend diff --git a/ERRORS.md b/ERRORS.md new file mode 100644 index 0000000..238876e --- /dev/null +++ b/ERRORS.md @@ -0,0 +1,63 @@ +# Error Code Documentation + +## System Errors + +1xxxx 为系统级错误。 + +| Error Code | Error Message (en) | Error Message (zh) | +| :----: | :----: | :----: | +| 10001 | System error | 系统错误 | +| 10002 | Service unavailable | 服务暂停 | +| 10003 | Parameter error | 参数错误 | +| 10004 | Parameter value invalid | 参数非法 | +| 10005 | Missing required parameter | 缺少参数 | +| 10006 | Resource unavailable | 资源不存在 | +| 10007 | CSRF token mismatch | CSRF 认证失败 | +| 10008 | This service is undergoing maintenance | 服务处于维护模式,无法提供服务 | + +## Application Errors + +2xxxx 为应用级错误。 + +### General Errors + +| Error Code | Error Message (en) | Error Message (zh) | +| :----: | :----: | :----: | +| 20000 | General error | 通用应用错误(以返回错误信息为准) | + +### Upload Errors + +201xx 为上传相关错误。 + +### Update Errors + +202xx 为更新相关错误。 + +| Error Code | Error Message (en) | Error Message (zh) | +| :----: | :----: | :----: | +| 20201 | Can't find user token | 无法找到 Token | + +### Download Errors + +203xx 为下载相关错误。 + +| Error Code | Error Message (en) | Error Message (zh) | +| :----: | :----: | :----: | +| 20301 | Missing token in header | 请求头缺少 Token | +| 20302 | Token used | Token 已被使用 | +| 20303 | Token expired | Token 已过期 | +| 20304 | Token revoked | Token 不合法 | +| 20305 | Can't get the download link | 无法获取下载链接 | +| 20306 | The retrieve code mismatch auth | 提取码无法对应auth | +| 20307 | The retrieve code repeat | 提取码重复 | +### Delete Errors + +204xx 为删除相关错误。 + +### Validate Errors + +205xx 为认证相关错误。 + +| Error Code | Error Message (en) | Error Message (zh) | +| :----: | :----: | :----: | +| 20501 | Incorrect password | 密码错误 | diff --git a/README.md b/README.md index 1091b0f..93f552a 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,10 @@ - [MariaDB](#mariadb) - [Redis](#redis) - [Cloud Services Config](#cloud-services-config) + - [Security](#security) + - [CORS](#cors) + - [CSRF](#csrf) + - [Error Codes](#error-codes) - [Release Mode](#release-mode) - [Deploy (Development Environment)](#deploy-development-environment) - [Build from Source](#build-from-source) @@ -81,6 +85,30 @@ CREATE DATABASE safeu; 在 `conf/` 下新建并填写 `cloud.yml` 文件,具体请参照 `conf/cloud.exmaple.yml` 。 +## Security + +安全设计。 + +### CORS + +Cross-origin Resource Sharing 跨源资源共享。 + +- 可共享域名由 [CORS_ALLOW_ORIGINS](common/const.go#L65) 决定; +- 可返回的响应头由 [CORS_ALLOW_HEADERS](common/const.go#L72) 决定; +- 允许的 HTTP 方法由 [CORS_ALLOW_METHODS](common/const.go#L80) 决定。 + +### CSRF + +Cross-site Request Forgery 跨域请求伪造防护设计。 + +先请求 `/csrf` 接口获得 CSRF 口令。当发送 POST 请求时,必须在请求头中加入 `X-CSRF-TOKEN` CSRF 认证头,值为获得到的 CSRF 口令,否则会得到 [10007](ERRORS.md#L15) 错误。 + +## Error Codes + +返回错误码。 + +错误码对照文档:[ERRORS](ERRORS.md) + ## Release Mode 若用于生产环境部署,则建议使用 `RELEASE` 模式: @@ -123,7 +151,7 @@ $ ./run-dev-docker-containers.sh ```bash $ cd scripts/ -$ ./docker-compose-up-development.sh +$ ./dev-docker-compose.sh up ``` > 需要安装 `docker` 和 `docker-Publicompose` ,可用 `scripts/install-docker-family-on-ubuntuPublic1804.sh` 在 Ubuntu 18.04 中安装 Docker 和 DPubliccker Compose。该脚本为中国网络环境额外定制,Public证安装速度。 @@ -167,7 +195,7 @@ $ ./run-production-docker-containers.sh ```bash $ cd scripts/ -$ ./docker-compose-up-production.sh +$ ./prod-docker-compose.sh up ``` > 需要安装 `docker` 和 `docker-compose` ,可用 `scripts/install-docker-family-on-ubuntu-1804.sh` 在 Ubuntu 18.04 中安装 Docker 和 Docker Compose。该脚本为中国网络环境额外定制,保证安装速度。 diff --git a/api/openapi.yaml b/api/openapi.yaml index c21a0dd..129e946 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -22,7 +22,7 @@ info: license: name: Apache 2.0 url: http://www.apache.org/licenses/LICENSE-2.0.html - version: "1.0.0-alpha" + version: "1.0.0-beta" externalDocs: description: Find out more about A2OS SafeU backend url: https://api.safeu.a2os.club @@ -37,7 +37,125 @@ tags: description: Download item(s) operations - name: validation description: Validation operations + - name: miscellaneous + description: Miscellaneous operations paths: + /ping: + get: + tags: + - miscellaneous + summary: PING-PONG + operationId: ping + responses: + "200": + description: 节点正常 + content: + "application/json": + schema: + type: object + properties: + message: + type: string + description: 返回信息 + example: pong + + /csrf: + get: + tags: + - miscellaneous + summary: CSRF 口令获得接口 + operationId: getCSRFToken + responses: + "200": + description: 获得 CSRF 口令 + headers: + X-CSRF-TOKEN: + description: CSRF 口令 + schema: + type: string + content: + "text/plain": + schema: + type: string + description: 返回信息 + example: IN HEADER + + /v1/info/{retrieveCode}: + post: + tags: + - miscellaneous + summary: 文件(组)信息获取接口 + operationId: getFileInfo + parameters: + - name: retrieveCode + in: path + description: 提取码 + required: true + schema: + type: string + - name: X-CSRF-TOKEN + in: header + description: CSRF 口令 + required: true + schema: + type: string + requestBody: + content: + "application/json": + schema: + type: object + properties: + user_token: + type: string + description: 文件所有者信息 + responses: + "200": + description: 获得文件信息 + content: + "application/json": + schema: + type: object + properties: + down_count: + type: integer + format: int64 + description: 可下载次数 + expired_at: + type: string + format: date-time + description: 文件到期时间 + is_public: + type: boolean + description: 文件是否加密 + "400": + description: 无法获得请求参数 + content: + "application/json": + schema: + type: object + properties: + err_code: + type: integer + format: int64 + description: 错误码 + message: + type: string + description: 错误原因 + "401": + description: 认证失败 + content: + "application/json": + schema: + type: object + properties: + err_code: + type: integer + format: int64 + description: 错误码 + message: + type: string + description: 错误原因 + /v1/upload/policy: get: tags: @@ -89,11 +207,19 @@ paths: uuid: type: string description: 文件uuid + /v1/upload/finish: post: tags: - upload summary: 结束所有上传文件的上传操作 + parameters: + - name: X-CSRF-TOKEN + in: header + description: CSRF 口令 + required: true + schema: + type: string requestBody: content: "application/json": @@ -134,6 +260,12 @@ paths: required: true schema: type: string + - name: X-CSRF-TOKEN + in: header + description: CSRF 口令 + required: true + schema: + type: string requestBody: content: "application/json": @@ -166,17 +298,27 @@ paths: schema: type: object properties: + err_code: + type: integer + format: int64 + description: 错误码 message: type: string - example: Can`t Find User Token - description: 文件所有者信息不存在 + description: 错误原因 "500": description: 服务器错误 content: - "text/plain": + "application/json": schema: - type: string - description: 未知错误 + type: object + properties: + err_code: + type: integer + format: int64 + description: 错误码 + message: + type: string + description: 错误原因 /v1/recode/{retrieveCode}: post: @@ -185,6 +327,12 @@ paths: summary: 修改文件提取码 operationId: changeFileRetrieveCode parameters: + - name: X-CSRF-TOKEN + in: header + description: CSRF 口令 + required: true + schema: + type: string - name: retrieveCode in: path description: 提取码 @@ -225,10 +373,13 @@ paths: schema: type: object properties: + err_code: + type: integer + format: int64 + description: 错误码 message: type: string - example: reCode Repeat/require Auth - description: 提取码重复/需要新的哈希密码 + description: 错误原因 "401": description: 文件所有者信息不存在 content: @@ -236,17 +387,27 @@ paths: schema: type: object properties: + err_code: + type: integer + format: int64 + description: 错误码 message: type: string - example: Can`t Find User Token - description: 文件所有者信息不存在 + description: 错误原因 "500": description: 服务器错误 content: - "text/plain": + "application/json": schema: - type: string - description: 未知错误 + type: object + properties: + err_code: + type: integer + format: int64 + description: 错误码 + message: + type: string + description: 错误原因 /v1/expireTime/{retrieveCode}: post: @@ -255,6 +416,12 @@ paths: summary: 修改文件过期时间 operationId: changeFileExpireTime parameters: + - name: X-CSRF-TOKEN + in: header + description: CSRF 口令 + required: true + schema: + type: string - name: retrieveCode in: path description: 提取码 @@ -293,11 +460,13 @@ paths: schema: type: object properties: + err_code: + type: integer + format: int64 + description: 错误码 message: type: string - example: The length of the time is not within the right range - description: 新的文件过期时间不正确 - + description: 错误原因 "401": description: 文件所有者信息不存在 content: @@ -305,17 +474,27 @@ paths: schema: type: object properties: + err_code: + type: integer + format: int64 + description: 错误码 message: type: string - example: Can`t Find User Token - description: 文件所有者信息不存在 + description: 错误原因 "500": description: 服务器错误 content: - "text/plain": + "application/json": schema: - type: string - description: 未知错误 + type: object + properties: + err_code: + type: integer + format: int64 + description: 错误码 + message: + type: string + description: 错误原因 /v1/delete/{retrieveCode}: post: @@ -324,6 +503,12 @@ paths: summary: 手动删除文件接口 operationId: deleteFileManual parameters: + - name: X-CSRF-TOKEN + in: header + description: CSRF 口令 + required: true + schema: + type: string - name: retrieveCode in: path description: 提取码 @@ -358,17 +543,27 @@ paths: schema: type: object properties: + err_code: + type: integer + format: int64 + description: 错误码 message: type: string - example: Can`t Find User Token - description: 文件所有者信息不存在 + description: 错误原因 "500": description: 服务器错误 content: - "text/plain": + "application/json": schema: - type: string - description: 未知错误 + type: object + properties: + err_code: + type: integer + format: int64 + description: 错误码 + message: + type: string + description: 错误原因 /v1/downCount/{retrieveCode}: post: @@ -377,6 +572,12 @@ paths: summary: 修改文件下载次数 operationId: changeFileDownCount parameters: + - name: X-CSRF-TOKEN + in: header + description: CSRF 口令 + required: true + schema: + type: string - name: retrieveCode in: path description: 提取码 @@ -414,41 +615,59 @@ paths: schema: type: object properties: + err_code: + type: integer + format: int64 + description: 错误码 message: type: string - example: Can`t Find User Token - description: 文件所有者信息不存在 + description: 错误原因 "500": description: 服务器错误 content: - "text/plain": + "application/json": schema: - type: string - description: 未知错误 - get: + type: object + properties: + err_code: + type: integer + format: int64 + description: 错误码 + message: + type: string + description: 错误原因 + + /v1/minusDownCount/{retrieveCode}: + post: tags: - download summary: 下载感知接口 operationId: downloadAwareness parameters: + - name: X-CSRF-TOKEN + in: header + description: CSRF 口令 + required: true + schema: + type: string - name: retrieveCode in: path description: 提取码 required: true schema: type: string - - name: bucket - in: query - description: 文件所在 OSS 桶名 - required: true - schema: - type: string - - name: path - in: query - description: 文件所在 OSS 桶中的绝对路径 - required: true - schema: - type: string + requestBody: + content: + "application/json": + schema: + type: object + properties: + bucket: + description: 文件所在 OSS 桶名 + type: string + path: + description: 文件所在 OSS 桶中的绝对路径 + type: string responses: "200": description: 成功接收下载信息 @@ -464,7 +683,11 @@ paths: schema: type: object properties: - error: + err_code: + type: integer + format: int64 + description: 错误码 + message: type: string description: 错误原因 "500": @@ -482,6 +705,12 @@ paths: summary: 获取下载/打包文件连接 operationId: downloadItem parameters: + - name: X-CSRF-TOKEN + in: header + description: CSRF 口令 + required: true + schema: + type: string - name: Token in: header description: 临时认证口令 @@ -518,7 +747,11 @@ paths: schema: type: object properties: - error: + err_code: + type: integer + format: int64 + description: 错误码 + message: type: string description: 错误原因 "401": @@ -528,7 +761,11 @@ paths: schema: type: object properties: - error: + err_code: + type: integer + format: int64 + description: 错误码 + message: type: string description: 错误原因 "404": @@ -538,7 +775,11 @@ paths: schema: type: object properties: - error: + err_code: + type: integer + format: int64 + description: 错误码 + message: type: string description: 错误原因 "410": @@ -548,7 +789,25 @@ paths: schema: type: object properties: - error: + err_code: + type: integer + format: int64 + description: 错误码 + message: + type: string + description: 错误原因 + "500": + description: 无法获取签名下载链接 + content: + "application/json": + schema: + type: object + properties: + err_code: + type: integer + format: int64 + description: 错误码 + message: type: string description: 错误原因 "503": @@ -558,7 +817,11 @@ paths: schema: type: object properties: - error: + err_code: + type: integer + format: int64 + description: 错误码 + message: type: string description: 错误原因 @@ -569,6 +832,12 @@ paths: summary: 提取码认证接口 operationId: validateRC parameters: + - name: X-CSRF-TOKEN + in: header + description: CSRF 口令 + required: true + schema: + type: string - name: retrieveCode in: path description: 提取码 @@ -596,7 +865,7 @@ paths: type: string description: 临时认证口令 items: - $ref: "#/components/schemas/ItemGroup" + $ref: "#/components/schemas/ResponseItemGroup" "401": description: 需要密码 / 密码认证失败 content: @@ -604,7 +873,11 @@ paths: schema: type: object properties: - error: + err_code: + type: integer + format: int64 + description: 错误码 + message: type: string description: 错误原因 "404": @@ -614,7 +887,11 @@ paths: schema: type: object properties: - error: + err_code: + type: integer + format: int64 + description: 错误码 + message: type: string description: 错误原因 "500": @@ -624,38 +901,26 @@ paths: schema: type: object properties: - error: + err_code: + type: integer + format: int64 + description: 错误码 + message: type: string description: 错误原因 components: schemas: - Item: + ResponseItem: type: object description: 文件 properties: - id: - type: integer - format: int64 - status: - type: integer - format: int32 - description: 文件状态(正常,异常,已删除) name: type: string description: 存储单位 UUID 值 original_name: type: string description: 文件原名 - host: - type: string - description: 存储后端的 bucket 记录 - re_code: - type: string - description: 提取码 - password: - type: string - description: 私有文件[组]的密码 down_count: type: integer format: int32 @@ -663,13 +928,6 @@ components: type: type: string description: 文件类型 - is_public: - type: boolean - description: 是否公有 - archive_type: - type: integer - format: int32 - description: 压缩包类型(NULL,FULL,CUSTOM) protocol: type: string description: OSS 下载协议 @@ -682,22 +940,14 @@ components: path: type: string description: 文件所在 OSS 桶中的绝对路径 - created_at: - type: string - format: date-time - description: 最初生成时间 - updated_at: - type: string - format: date-time - description: 最后更新时间 - deleted_at: + expired_at: type: string format: date-time - description: 记录删除时间 - ItemGroup: + description: 文件到期时间 + ResponseItemGroup: type: array items: - $ref: "#/components/schemas/Item" + $ref: "#/components/schemas/ResponseItem" description: 文件组 ZipItem: type: object diff --git a/build/package/safeu-backend-dev/Dockerfile b/build/package/safeu-backend-dev/Dockerfile index 451a284..d0c72f5 100644 --- a/build/package/safeu-backend-dev/Dockerfile +++ b/build/package/safeu-backend-dev/Dockerfile @@ -5,12 +5,17 @@ MAINTAINER TripleZ "me@triplez.cn" WORKDIR $GOPATH/src/a2os/safeu-backend ADD . $GOPATH/src/a2os/safeu-backend/ -# Add timezone package -RUN apt-get update ;\ - apt-get install -y tzdata +# Solution for Chinese special network enviornment +# RUN mkdir -p $GOPATH/src/golang.org/x/ && \ +# git clone https://github.com/golang/sys.git $GOPATH/src/golang.org/x/sys/ +RUN mkdir -p $GOPATH/src/golang.org/x/ && \ + git clone https://gitee.com/Triple-Z/golang-sys.git $GOPATH/src/golang.org/x/sys/ # Add dependencies -RUN go get -u github.com/kardianos/govendor && go get github.com/pilu/fresh && govendor sync +# RUN go get -u github.com/kardianos/govendor && go get github.com/pilu/fresh && govendor sync + +# Add fresh package +RUN go get github.com/pilu/fresh # Build package RUN go build . diff --git a/build/package/safeu-backend-dev/Dockerfile-compose b/build/package/safeu-backend-dev/Dockerfile-compose index 7bbf451..f21544e 100644 --- a/build/package/safeu-backend-dev/Dockerfile-compose +++ b/build/package/safeu-backend-dev/Dockerfile-compose @@ -5,19 +5,21 @@ MAINTAINER TripleZ "me@triplez.cn" WORKDIR $GOPATH/src/a2os/safeu-backend ADD . $GOPATH/src/a2os/safeu-backend/ -COPY conf/db.example.json $GOPATH/src/a2os/safeu-backend/conf/db.json +# COPY conf/db.example.json $GOPATH/src/a2os/safeu-backend/conf/db.json -# Add timezone package -RUN apt-get update ;\ - apt-get install -y tzdata +# Solution for Chinese special network enviornment +# RUN mkdir -p $GOPATH/src/golang.org/x/ && \ +# git clone https://github.com/golang/sys.git $GOPATH/src/golang.org/x/sys/ +RUN mkdir -p $GOPATH/src/golang.org/x/ && \ + git clone https://gitee.com/Triple-Z/golang-sys.git $GOPATH/src/golang.org/x/sys/ # Add dependencies -RUN go get -u github.com/kardianos/govendor && go get github.com/pilu/fresh && govendor sync +RUN go get github.com/pilu/fresh # Build package -RUN go build . +RUN go build -o safeu-backend-dev . EXPOSE 8080 ENTRYPOINT ["fresh"] -#ENTRYPOINT ["./safeu-backend-dev"] +# ENTRYPOINT ["./safeu-backend-dev"] diff --git a/build/package/safeu-backend/Dockerfile b/build/package/safeu-backend/Dockerfile index 8e727b8..8955432 100644 --- a/build/package/safeu-backend/Dockerfile +++ b/build/package/safeu-backend/Dockerfile @@ -6,7 +6,7 @@ WORKDIR $GOPATH/src/a2os/safeu-backend ADD . $GOPATH/src/a2os/safeu-backend/ # Add dependencies -RUN go get -u github.com/kardianos/govendor && go get github.com/pilu/fresh && govendor sync +# RUN go get -u github.com/kardianos/govendor && govendor sync # Build package RUN go build . diff --git a/build/package/safeu-backend/Dockerfile-compose b/build/package/safeu-backend/Dockerfile-compose index c2583a5..4c3a9d9 100644 --- a/build/package/safeu-backend/Dockerfile-compose +++ b/build/package/safeu-backend/Dockerfile-compose @@ -5,14 +5,14 @@ MAINTAINER TripleZ "me@triplez.cn" WORKDIR $GOPATH/src/a2os/safeu-backend ADD . $GOPATH/src/a2os/safeu-backend/ -COPY conf/db.example.json $GOPATH/src/a2os/safeu-backend/conf/db.json +# COPY conf/db.example.json $GOPATH/src/a2os/safeu-backend/conf/db.json # Add dependencies -RUN go get -u github.com/kardianos/govendor && govendor sync +# RUN go get -u github.com/kardianos/govendor && govendor sync # Build package RUN go build . -EXPOSE 8080 +# EXPOSE 8080 ENTRYPOINT ["./safeu-backend"] diff --git a/common/const.go b/common/const.go index e418c3f..6c6d5c2 100644 --- a/common/const.go +++ b/common/const.go @@ -3,8 +3,9 @@ package common var CloudConfig *CloudConfiguration const ( - DEBUG bool = false - PORT string = "8080" + DEBUG = true + MAINTENANCE = false + PORT = "8080" ) const ( @@ -15,6 +16,7 @@ const ( ReCodeLength = 4 UserTokenLength = 32 MYSQLTIMEZONE = "Asia%2FShanghai" + SHADOWKEYPREFIX = "shadowKey:" ) // 文件状态码 @@ -53,6 +55,9 @@ const ( TOKEN_VALID_MINUTES int32 = 15 // Token 有效时长 ) +// 数据库连接失败重试间隔 +const DB_CONNECT_FAIL_RETRY_INTERVAL = 20 + // RedisDB const ( USER_TOKEN = iota // 0 @@ -68,11 +73,18 @@ var CORS_ALLOW_ORIGINS = []string{ "http://test.safeu.a2os.club", } +var CORS_ALLOW_DEBUG_ORIGINS = []string{ + "http://*", + "https://*", +} + var CORS_ALLOW_HEADERS = []string{ "Origin", "Content-Length", "Content-Type", "Token", + "X-CSRF-TOKEN", + "withCredentials", } var CORS_ALLOW_METHODS = []string{ @@ -83,3 +95,18 @@ var CORS_ALLOW_METHODS = []string{ "DELETE", "HEAD", } + +var CORS_EXPOSE_HEADERS = []string{ + "X-CSRF-TOKEN", + "Token", +} + +var CSRF_COOKIE_SECRET = []byte("csrf-secret") + +const ( + CSRF_SESSION_NAME string = "safeu-session" + CSRF_SECRET string = "safeu-secret" +) + +// OSS 下载请求所包含的 Content-Type 值,用于 URL 签名 +var OSS_DOWNLOAD_CONTENT_TYPE = "" diff --git a/common/database.go b/common/database.go index 241378d..eb8885e 100644 --- a/common/database.go +++ b/common/database.go @@ -5,6 +5,7 @@ import ( "fmt" "io/ioutil" "log" + "time" "github.com/go-redis/redis" "github.com/jinzhu/gorm" @@ -61,11 +62,18 @@ func InitDB() *gorm.DB { fmt.Println("Get DBConfig From File Err:", err) } DbConfig = DBConf - db, err := gorm.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&collation=utf8mb4_bin&parseTime=True&loc=%s", DBConf.Master.User, DBConf.Master.Pass, DBConf.Master.Host, DBConf.Master.Port, DBConf.Master.Database, MYSQLTIMEZONE)) - if err != nil { - //fmt.Println("Gorm Open DB Err: ", err) - log.Fatalln("Gorm Open DB Err: ", err) + var ( + db *gorm.DB + e error + ) + connectString := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&collation=utf8mb4_bin&parseTime=True&loc=%s", DBConf.Master.User, DBConf.Master.Pass, DBConf.Master.Host, DBConf.Master.Port, DBConf.Master.Database, MYSQLTIMEZONE) + // 重试连接 + for db, e = gorm.Open("mysql", connectString); e != nil; { + fmt.Println("Gorm Open DB Err: ", e) + log.Println(fmt.Sprintf("GORM cannot connect to database, retry in %d seconds...", DB_CONNECT_FAIL_RETRY_INTERVAL)) + time.Sleep(DB_CONNECT_FAIL_RETRY_INTERVAL * time.Second) } + log.Println("Connected to database ", DBConf.Master.User, " ", DBConf.Master.Pass, " ", DBConf.Master.Host, ":", DBConf.Master.Port, " ", DBConf.Master.Database) db.DB().SetMaxIdleConns(DBConf.Master.MaxIdleConns) DB = db diff --git a/common/error.go b/common/error.go new file mode 100644 index 0000000..4317a42 --- /dev/null +++ b/common/error.go @@ -0,0 +1,166 @@ +package common + +import ( + "fmt" + "log" + "net/http" + + "github.com/gin-gonic/gin" +) + +// FuncHandler 统一错误处理 +// i 传入error,bool,int +// judge 触发正确值 非error环境下有效 +// 如果触发了错误 return True +// Example: +// 1. common.FuncHandler(c, c.BindJSON(&x), nil, http.StatusBadRequest, 20301) +// == if(c.BindJSON(&x) != nil){ +// c.JSON(http.StatusBadRequest, gin.H{ +// "err_code": 20301, +// "message": common.Errors[20301], +// }) +// } +// 2. common.FuncHandler(c, c.BindJSON(&x), nil, http.StatusBadRequest, 20301,fmt.Sprintf("BindJson fail with %v",x)) +// == if(c.BindJSON(&x) != nil){ +// log.Println(fmt.Sprintf("BindJson fail with %v",x)) +// c.JSON(http.StatusBadRequest, gin.H{ +// "err_code": 20301, +// "message": common.Errors[20301], +// }) +// } +// 3. common.FuncHandler(c, isOdd(2), true, fmt.Sprintf("%d is even",2)) +// == if(isOdd(2) != true){ +// log.Println(fmt.Sprintf("%d is even",2)) +// } +func FuncHandler(c *gin.Context, i interface{}, judge interface{}, option ...interface{}) bool { + generalReturn := buildErrorMeta(option) + errType := gin.ErrorTypePrivate + // http返回码和错误码齐全则为公开错误 + if generalReturn.HTTPStatus != 0 || generalReturn.AppErrJSON.ErrCode != 0 { + errType = gin.ErrorTypePublic + } + switch i.(type) { + case nil: + return false + case error: + c.Error(i.(error)).SetMeta(generalReturn).SetType(errType) + return true + case bool: + if i.(bool) == judge.(bool) { + return false + } + if generalReturn.CustomMessage != "" { + c.Error(fmt.Errorf(generalReturn.CustomMessage)).SetMeta(generalReturn).SetType(errType) + } else if generalReturn.AppErrJSON.Message != "" { + c.Error(fmt.Errorf(generalReturn.AppErrJSON.Message)).SetMeta(generalReturn).SetType(errType) + } else { + c.Error(fmt.Errorf("no err")).SetMeta(generalReturn).SetType(errType) + } + return true + } + return true +} +func buildErrorMeta(option []interface{}) GeneralReturn { + var generalReturn GeneralReturn + for _, v := range option { + switch v.(type) { + case int: + // RFC 2616 HTTP Status Code 是3位数字代码 + if v.(int) >= 1000 { + generalReturn.AppErrJSON.ErrCode = v.(int) + generalReturn.AppErrJSON.Message = Errors[v.(int)] + } else { + generalReturn.HTTPStatus = v.(int) + } + break + case string: + generalReturn.CustomMessage = v.(string) + break + } + } + return generalReturn +} + +// GeneralReturn 通用码 +type GeneralReturn struct { + CustomMessage string + HTTPStatus int + AppErrJSON appErrJSON +} +type appErrJSON struct { + ErrCode int `json:"err_code"` + Message string `json:"message"` +} + +// ErrorHandling 错误处理中间件 +func ErrorHandling() gin.HandlerFunc { + return func(c *gin.Context) { + c.Next() + err := c.Errors.Last() + if err == nil { + return + } + // 转义 + var metaData GeneralReturn + switch err.Meta.(type) { + case GeneralReturn: + metaData = err.Meta.(GeneralReturn) + default: + return + } + switch err.Type { + case gin.ErrorTypePublic: + // 公开错误 返回对应Http状态码和错误码 + // 如果有自定义消息 写入日志 + if metaData.CustomMessage != "" { + log.Println(metaData.CustomMessage) + } + c.JSON(metaData.HTTPStatus, metaData.AppErrJSON) + return + case gin.ErrorTypePrivate: + // 如果有自定义消息 写入日志 + if metaData.CustomMessage != "" { + log.Println(metaData.CustomMessage) + } + break + default: + c.JSON(http.StatusInternalServerError, gin.H{ + "err_code": 10001, + "message": Errors[10001], + }) + return + } + + } +} + +// Errors 错误码 +var Errors = map[int]string{ + + 0: "OK", + + // 系统级错误 + 10001: "System error", + 10002: "Service unavailable", + 10003: "Parameter error", + 10004: "Parameter value invalid", + 10005: "Missing required parameter", + 10006: "Resource unavailable", + 10007: "CSRF token mismatch", + + // 应用级错误 + 20000: "Application error", + + 20201: "Can't find user token", + + 20301: "Missing token in header", + 20302: "Token used", + 20303: "Token expired", + 20304: "Token revoked", + 20305: "Can't get the download link", + + 20306: "The retrieve code mismatch auth", + 20307: "The retrieve code repeat", + + 20501: "Incorrect password", +} diff --git a/common/maintenance.go b/common/maintenance.go new file mode 100644 index 0000000..edb4bc3 --- /dev/null +++ b/common/maintenance.go @@ -0,0 +1,23 @@ +package common + +import ( + "log" + "net/http" + + "github.com/gin-gonic/gin" +) + +func MaintenanceHandling() gin.HandlerFunc { + return func(c *gin.Context) { + if MAINTENANCE { + c.JSON(http.StatusServiceUnavailable, gin.H{ + "err_code": 10008, + "message": Errors[10008], + }) + log.Println(c.ClientIP(), "Maintenance mode is open") + c.Abort() + } + + c.Next() + } +} diff --git a/common/tool.go b/common/tool.go index 000c01f..2e00660 100644 --- a/common/tool.go +++ b/common/tool.go @@ -56,3 +56,17 @@ func KeyISExistInRedis(str string, client *redis.Client) bool { } return true } + +func SetShadowKeyInRedis(key string, value interface{}, expiration time.Duration, client *redis.Client) error { + err := client.Set(key, value, 0).Err() + if err != nil { + log.Println(fmt.Sprintf("WriteShadowKeyInRedis key %s value %v fail in realKey Set", key, value)) + return err + } + err = client.Set(SHADOWKEYPREFIX+key, "", expiration).Err() + if err != nil { + log.Println(fmt.Sprintf("WriteShadowKeyInRedis key %s value %v fail in shadowKey Set", key, value)) + return err + } + return nil +} diff --git a/conf/db.example.json b/conf/db.example.json index ee75cc0..fca6f41 100644 --- a/conf/db.example.json +++ b/conf/db.example.json @@ -1,8 +1,8 @@ { "Master": { - "User": "root", - "Pass": "safeu", - "Host": "db", + "User": "your_db_username", + "Pass": "your_db_password", + "Host": "your_db_ip_address", "Port": "3306", "Database": "safeu", "MaxIdleConns": 30, @@ -10,7 +10,7 @@ "Debug": false }, "Redis": { - "Host": "redis", + "Host": "safeu-redis", "Port": "6379", "Pass": "" } diff --git a/conf/mariadb/collation.cnf b/conf/mariadb/collation.cnf new file mode 100644 index 0000000..3178256 --- /dev/null +++ b/conf/mariadb/collation.cnf @@ -0,0 +1,14 @@ +[client] +default-character-set=utf8mb4 + +[mysql] +default-character-set=utf8mb4 + +[mysqld] +character-set-server = utf8mb4 +character-set-client-handshake = utf8mb4 + +init-connect='SET NAMES utf8mb4 +init-connect='SET collation_connection = utf8mb4_bin' + +collation-server = utf8mb4_bin diff --git a/conf/nginx/api.safeu.a2os.club b/conf/nginx/api.safeu.a2os.club new file mode 100644 index 0000000..ea97a33 --- /dev/null +++ b/conf/nginx/api.safeu.a2os.club @@ -0,0 +1,31 @@ +# HTTP server +server { + listen 80 default_server; + listen [::]:80 default_server; + server_name api.safeu.a2os.club; + # server_name _; + # location / { + # proxy_pass http://app_servers; + # proxy_redirect default; + # } + return 301 https://$host$request_uri; +} +# HTTPS server +server { + # SSL configuration + listen 443 ssl default_server; + listen [::]:443 ssl default_server; + + server_name api.safeu.a2os.club; + ssl on; + ssl_certificate /opt/nginx/api.safeu.a2os.club/fullchain.pem; + ssl_certificate_key /opt/nginx/api.safeu.a2os.club/privkey.pem; + + location / { + # First attempt to serve request as file, then + # as directory, then fall back to displaying a 404. + #try_files $uri $uri/ =404; + proxy_pass http://app_servers; + proxy_redirect default; + } +} \ No newline at end of file diff --git a/conf/nginx/nginx.dev.conf b/conf/nginx/nginx.dev.conf new file mode 100644 index 0000000..bee36f9 --- /dev/null +++ b/conf/nginx/nginx.dev.conf @@ -0,0 +1,70 @@ +# user www-data; +worker_processes auto; +pid /run/nginx.pid; +include /etc/nginx/modules-enabled/*.conf; + +events { + worker_connections 768; + # multi_accept on; +} + +http { + + # SafeU backend server LB settings + upstream app_servers { + least_conn; + server web:8080 max_fails=10 fail_timeout=10s; + } + + ## + # Basic Settings + ## + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + # server_tokens off; + + # server_names_hash_bucket_size 64; + # server_name_in_redirect off; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + ## + # SSL Settings + ## + + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE + ssl_prefer_server_ciphers on; + + ## + # Logging Settings + ## + + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; + + ## + # Gzip Settings + ## + + gzip on; + + # gzip_vary on; + # gzip_proxied any; + # gzip_comp_level 6; + # gzip_buffers 16 8k; + # gzip_http_version 1.1; + # gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; + + ## + # Virtual Host Configs + ## + + # include /etc/nginx/conf.d/*.conf; + include /etc/nginx/sites-enabled/*; + +} diff --git a/conf/nginx/nginx.prod.conf b/conf/nginx/nginx.prod.conf new file mode 100644 index 0000000..7dec746 --- /dev/null +++ b/conf/nginx/nginx.prod.conf @@ -0,0 +1,72 @@ +user www-data; +worker_processes auto; +pid /run/nginx.pid; +include /etc/nginx/modules-enabled/*.conf; + +events { + worker_connections 4096; + # multi_accept on; +} + +http { + + # SafeU backend server LB settings + upstream app_servers { + least_conn; + server web1:8080 max_fails=3 fail_timeout=10s; + server web2:8080 max_fails=3 fail_timeout=10s; + } + + + ## + # Basic Settings + ## + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + # server_tokens off; + + # server_names_hash_bucket_size 64; + # server_name_in_redirect off; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + ## + # SSL Settings + ## + + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE + ssl_prefer_server_ciphers on; + + ## + # Logging Settings + ## + + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; + + ## + # Gzip Settings + ## + + gzip on; + + # gzip_vary on; + # gzip_proxied any; + # gzip_comp_level 6; + # gzip_buffers 16 8k; + # gzip_http_version 1.1; + # gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; + + ## + # Virtual Host Configs + ## + + # include /etc/nginx/conf.d/*.conf; + include /etc/nginx/sites-enabled/*; + +} diff --git a/conf/nginx/testapi.safeu.a2os.club b/conf/nginx/testapi.safeu.a2os.club new file mode 100644 index 0000000..be52833 --- /dev/null +++ b/conf/nginx/testapi.safeu.a2os.club @@ -0,0 +1,53 @@ +# HTTP server +server { + listen 80 default_server; + listen [::]:80 default_server; + server_name _; + # server_name testapi.safeu.a2os.club; + # Test without redirection + # return 301 https://$host$request_uri; + + location / { + proxy_pass http://app_servers; + proxy_redirect default; + } +} +# HTTPS server +# server { +# #listen 80 default_server; +# #listen [::]:80 default_server; + +# # SSL configuration +# # +# listen 443 ssl default_server; +# listen [::]:443 ssl default_server; +# # +# # Note: You should disable gzip for SSL traffic. +# # See: https://bugs.debian.org/773332 +# # +# # Read up on ssl_ciphers to ensure a secure configuration. +# # See: https://bugs.debian.org/765782 +# # +# # Self signed certs generated by the ssl-cert package +# # Don't use them in a production server! +# # +# # include snippets/snakeoil.conf; + +# # root /var/www/html; + +# # Add index.php to the list if you are using PHP +# # index index.html index.htm index.nginx-debian.html; + +# server_name testapi.safeu.a2os.club; +# ssl on; +# ssl_certificate /opt/nginx/testapi.safeu.a2os.club/fullchain.pem; +# ssl_certificate_key /opt/nginx/testapi.safeu.a2os.club/privkey.pem; + +# location / { +# # First attempt to serve request as file, then +# # as directory, then fall back to displaying a 404. +# #try_files $uri $uri/ =404; +# proxy_pass http://app_servers; +# proxy_redirect default; +# } +# } \ No newline at end of file diff --git a/conf/redis/redis.conf b/conf/redis/redis.conf new file mode 100644 index 0000000..97734a2 --- /dev/null +++ b/conf/redis/redis.conf @@ -0,0 +1,16 @@ +notify-keyspace-events "Ex" +# requirepass "safeu" +save 60 100 +save 900 1 +save 300 10 +stop-writes-on-bgsave-error no +rdbcompression yes +dbfilename dump.rdb + +appendonly no +appendfsync everysec +no-appendfsync-on-rewrite no +auto-aof-rewrite-percentage 100 +auto-aof-rewrite-min-size 64mb + +dir /data diff --git a/deployments/dev-safeu/docker-compose.yml b/deployments/dev-safeu/docker-compose.yml new file mode 100644 index 0000000..e460387 --- /dev/null +++ b/deployments/dev-safeu/docker-compose.yml @@ -0,0 +1,67 @@ +version: '3' +services: +# nginx: +# image: nginx +# volumes: +# - ../../conf/nginx/nginx.dev.conf:/etc/nginx/nginx.conf +# - ../../conf/nginx/testapi.safeu.a2os.club:/etc/nginx/sites-enabled/testapi.safeu.a2os.club +# - /etc/letsencrypt/live/testapi.safeu.a2os.club:/opt/nginx/testapi.safeu.a2os.club/ +# environment:res +# - TZ=Asia/Shanghai +# ports: +# - "80:80" +# networks: +# - webnet +# depends_on: +# - web +# restart: on-failure + safeu: + build: + context: ../.. + dockerfile: ././build/package/safeu-backend-dev/Dockerfile-compose + volumes: + - ../../log/web:/go/src/a2os/safeu-backend/log/ + # - ../..:/go/src/a2os/safeu-backend/ + environment: + - TZ=Asia/Shanghai + ports: + - "9000:8080" + networks: + - safeu-app-net + depends_on: +# - db + - safeu-redis + restart: always + +# db: +# environment: +# - TZ=Asia/Shanghai +# - MYSQL_ROOT_PASSWORD=safeu +# - MYSQL_DATABASE=safeu +## image: mysql:5.7.23 +# image: mariadb:10.3 +# volumes: +# - ../../data-dev/mariadb:/var/lib/mysql +# - ../../conf/mariadb:/etc/mysql/conf.d +## ports: +## - "3306:3306" +# networks: +# - safeu-app-net +# restart: on-failure + + safeu-redis: + image: redis:5.0.3-alpine + environment: + - TZ=Asia/Shanghai + volumes: + - ../../conf/redis/redis.conf:/usr/local/etc/redis/redis.conf + - ../../data-dev/redis:/data # for redis persistent storage + entrypoint: redis-server /usr/local/etc/redis/redis.conf + ports: + - "6379:6379" + networks: + - safeu-app-net + restart: on-failure + +networks: + safeu-app-net: diff --git a/deployments/development/docker-compose.yml b/deployments/development/docker-compose.yml deleted file mode 100644 index 072a422..0000000 --- a/deployments/development/docker-compose.yml +++ /dev/null @@ -1,42 +0,0 @@ -version: '3' -services: - web: - build: - context: ../.. - dockerfile: ././build/package/safeu-backend-dev/Dockerfile-compose - volumes: - - ../..:$GOPATH/src/a2os/safeu-backend/ - - ../../log/web:/$GOPATH/src/a2os/safeu-backend/log/ - environment: - - TZ=Asia/Shanghai - ports: - - "8080:8080" - networks: - - webnet - depends_on: - - db - - redis - restart: always - db: - environment: - - TZ=Asia/Shanghai - - MYSQL_ROOT_PASSWORD=safeu - - MYSQL_DATABASE=safeu - image: mariadb:10.3 - volumes: - - ../../db-data:/var/lib/mysql - ports: - - "3306:3306" - networks: - - webnet - redis: - image: redis:5.0.3-alpine - environment: - - TZ=Asia/Shanghai - ports: - - "6379:6379" - networks: - - webnet - -networks: - webnet: diff --git a/deployments/prod-safeu/docker-compose.yml b/deployments/prod-safeu/docker-compose.yml new file mode 100644 index 0000000..4f4a70c --- /dev/null +++ b/deployments/prod-safeu/docker-compose.yml @@ -0,0 +1,100 @@ +version: '3' +services: +# nginx: +# image: nginx +# volumes: +# - ../../conf/nginx/nginx.prod.conf:/etc/nginx/nginx.conf +# - ../../conf/nginx/api.safeu.a2os.club:/etc/nginx/sites-enabled/api.safeu.a2os.club +# - /etc/letsencrypt/live/api.safeu.a2os.club:/opt/nginx/api.safeu.a2os.club/ +# environment: +# - TZ=Asia/Shanghai +# ports: +# - "80:80" +# networks: +# - lbnet +# depends_on: +# - web1 +# - web2 +# restart: on-failure + + safeu1: + build: + context: ../.. + dockerfile: ././build/package/safeu-backend/Dockerfile-compose + volumes: + - ../../log/web1:/go/src/a2os/safeu-backend/log/ + # - ../..:/go/src/a2os/safeu-backend/ + environment: + - TZ=Asia/Shanghai + ports: + - "8080:8080" + networks: + - safeu-app-net + depends_on: + - safeu-redis + restart: on-failure + + safeu2: + build: + context: ../.. + dockerfile: ././build/package/safeu-backend/Dockerfile-compose + volumes: + - ../../log/web2:/go/src/a2os/safeu-backend/log/ + # - ../..:/go/src/a2os/safeu-backend/ + environment: + - TZ=Asia/Shanghai + ports: + - "8081:8080" + networks: + - safeu-app-net + depends_on: + - safeu-redis + restart: on-failure + + safeu3: + build: + context: ../.. + dockerfile: ././build/package/safeu-backend/Dockerfile-compose + volumes: + - ../../log/web2:/go/src/a2os/safeu-backend/log/ + # - ../..:/go/src/a2os/safeu-backend/ + environment: + - TZ=Asia/Shanghai + ports: + - "8082:8080" + networks: + - safeu-app-net + depends_on: + - safeu-redis + restart: on-failure + +# db: +# environment: +# - TZ=Asia/Shanghai +# - MYSQL_ROOT_PASSWORD=safeu +# - MYSQL_DATABASE=safeu +# image: mariadb:10.3 +# volumes: +# - ../../data/mariadb:/var/lib/mysql +# - ../../conf/mariadb:/etc/mysql/conf.d +# ports: +# - "3306:3306" +# networks: +# - dbnet +# restart: on-failure + safeu-redis: + image: redis:5.0.3-alpine + environment: + - TZ=Asia/Shanghai + volumes: + - ../../conf/redis/redis.conf:/usr/local/etc/redis/redis.conf + - ../../data/redis:/data # for redis persistent storage + entrypoint: redis-server /usr/local/etc/redis/redis.conf + ports: + - "6379:6379" + networks: + - safeu-app-net + restart: on-failure + +networks: + safeu-app-net: diff --git a/deployments/production/docker-compose.yml b/deployments/production/docker-compose.yml deleted file mode 100644 index 4107799..0000000 --- a/deployments/production/docker-compose.yml +++ /dev/null @@ -1,64 +0,0 @@ -version: '3' -services: - web1: - build: - context: ../.. - dockerfile: ././build/package/safeu-backend/Dockerfile-compose - volumes: - - ../..:$GOPATH/src/a2os/safeu-backend/ - - ../../log/web1:/$GOPATH/src/a2os/safeu-backend/log/ - environment: - - TZ=Asia/Shanghai - ports: - - "8080:8080" - networks: - - lbnet - - dbnet - depends_on: - - db - - redis - restart: always - web2: - build: - context: ../.. - dockerfile: ././build/package/safeu-backend/Dockerfile-compose - volumes: - - ../..:$GOPATH/src/a2os/safeu-backend/ - - ../../log/web2:/$GOPATH/src/a2os/safeu-backend/log/ - environment: - - TZ=Asia/Shanghai - ports: - - "8081:8080" - networks: - - lbnet - - dbnet - depends_on: - - db - - redis - restart: always - db: - environment: - - TZ=Asia/Shanghai - - MYSQL_ROOT_PASSWORD=safeu - - MYSQL_DATABASE=safeu - image: mariadb:10.3 - volumes: - - ../../data/maraidb:/var/lib/mysql - ports: - - "3306:3306" - networks: - - dbnet - restart: on-failure - redis: - image: redis:5.0.3-alpine - environment: - - TZ=Asia/Shanghai - ports: - - "6379:6379" - networks: - - dbnet - restart: always - -networks: - lbnet: - dbnet: diff --git a/faas/zip-items.py b/faas/zip-items.py index 21b648a..ac84249 100644 --- a/faas/zip-items.py +++ b/faas/zip-items.py @@ -1,3 +1,4 @@ +# -*- coding: UTF-8 -*- # Code for Python 2.7 import os @@ -26,7 +27,7 @@ def handler(environ, start_response): except(ValueError): request_body_size = 0 request_body = environ['wsgi.input'].read(request_body_size) - request_body = urllib.unquote(request_body).decode('utf8') + request_body = urllib.unquote(request_body).decode('utf8') # get request method request_method = environ['REQUEST_METHOD'] @@ -50,6 +51,7 @@ def handler(environ, start_response): # zip name re_code = request_body_json.get("re_code") is_full = request_body_json.get("full") + uuid = request_body_json.get("uuid") tmpdir = '/tmp/download/' os.system("rm -rf /tmp/*") @@ -78,9 +80,9 @@ def handler(environ, start_response): part_size = oss2.determine_part_size(total_size, preferred_size = 128 * 1024) if is_full: - zip_path = 'full-archive/' + re_code + '.zip' + zip_path = 'full-archive/' + re_code + '/' + uuid + '.zip' else: - zip_path = 'custom-archive/' + re_code + '.zip' + zip_path = 'custom-archive/' + re_code + '/' + uuid + '.zip' # use the last bucket to upload zip package upload_id = bucket.init_multipart_upload(zip_path).upload_id @@ -124,5 +126,6 @@ def make_zip(source_dir, output_filename): for filename in filenames: pathfile = os.path.join(parent, filename) arcname = pathfile[pre_len:].strip(os.path.sep) - zipf.write(pathfile, arcname) + zipf.write(pathfile, arcname.encode("utf-8")) + # zipf.write(pathfile, arcname.encode("gbk")) # for Windows(Chinese characters) zipf.close() diff --git a/item/delete.go b/item/delete.go index 2a2d2fb..d294b11 100644 --- a/item/delete.go +++ b/item/delete.go @@ -7,23 +7,46 @@ import ( "sync" "a2os/safeu-backend/common" - "github.com/gin-gonic/gin" "github.com/aliyun/aliyun-oss-go-sdk/oss" + "github.com/gin-gonic/gin" + "github.com/go-redis/redis" ) -type DelateItemBody struct { +type DeleteItemBody struct { UserToken string `json:"user_token"` } +// 过期文件主动删除 +func ActiveDelete(client *redis.Client) { + log.Println("ActiveDelete is Running........") + pubsub := client.Subscribe(fmt.Sprintf("__keyevent@%d__:expired", common.RECODE)) + _, err := pubsub.Receive() + if err != nil { + panic(err) + } + db := common.GetDB() + ch := pubsub.Channel() + for msg := range ch { + reCode := msg.Payload[len(common.SHADOWKEYPREFIX):] + var itemList []Item + db.Where("re_code = ? ", reCode).Find(&itemList) + for _, item := range itemList { + db.Delete(item) + } + go DeleteItems(itemList) + common.DeleteRedisRecodeFromRecode(reCode) + } +} func DeleteManual(c *gin.Context) { retrieveCode := c.Param("retrieveCode") - var deleteItemBody DelateItemBody + var deleteItemBody DeleteItemBody err := c.BindJSON(&deleteItemBody) if err != nil { log.Println(err) - c.JSON(http.StatusInternalServerError, gin.H{ - "message": err, + c.JSON(http.StatusBadRequest, gin.H{ + "err_code": 10003, + "message": common.Errors[10003], }) return } @@ -31,7 +54,8 @@ func DeleteManual(c *gin.Context) { reCodeRedisClient := common.GetReCodeRedisClient() if deleteItemBody.UserToken != reCodeRedisClient.Get(retrieveCode).Val() { c.JSON(http.StatusUnauthorized, gin.H{ - "message": "this reCode mismatch auth", + "err_code": 20306, + "message": common.Errors[20306], }) return } diff --git a/item/download.go b/item/download.go index 1243cc6..a9db20f 100644 --- a/item/download.go +++ b/item/download.go @@ -12,7 +12,7 @@ import ( "github.com/aliyun/aliyun-oss-go-sdk/oss" "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/json" - "github.com/satori/go.uuid" + uuid "github.com/satori/go.uuid" ) type PackRequest struct { @@ -41,7 +41,8 @@ func DownloadItems(c *gin.Context) { clientToken := c.Request.Header.Get("Token") if len(clientToken) == 0 { // if not get the token c.JSON(http.StatusUnauthorized, gin.H{ - "error": "Cannot get the token", + "err_code": 20301, + "message": common.Errors[20301], }) log.Println(c.ClientIP(), " Cannot get the client token from header") return @@ -53,7 +54,8 @@ func DownloadItems(c *gin.Context) { if db.Where("token = ?", clientToken).First(&tokenRecord).RecordNotFound() { // 无法找到该 token c.JSON(http.StatusNotFound, gin.H{ - "error": "Token invalid", + "err_code": 20304, + "message": common.Errors[20304], }) log.Println(c.ClientIP(), " Invalid token ", clientToken) return @@ -62,7 +64,8 @@ func DownloadItems(c *gin.Context) { // 检查 token 是否失效 if !tokenRecord.Valid { c.JSON(http.StatusUnauthorized, gin.H{ - "error": "Token invalid", + "err_code": 20302, + "message": common.Errors[20302], }) log.Println(c.ClientIP(), " Expired token ", clientToken) return @@ -74,7 +77,8 @@ func DownloadItems(c *gin.Context) { // Token 已过期,更新数据库并拒绝请求 db.Model(&tokenRecord).Update("valid", false) c.JSON(http.StatusUnauthorized, gin.H{ - "error": "Token invalid", + "err_code": 20303, + "message": common.Errors[20303], }) log.Println(c.ClientIP(), " Expired token ", clientToken) return @@ -85,7 +89,8 @@ func DownloadItems(c *gin.Context) { if tokenRetrieveCode != retrieveCode { // 提取码不正确 c.JSON(http.StatusUnauthorized, gin.H{ - "error": "Invalid token for this item", + "err_code": 20304, + "message": common.Errors[20304], }) log.Println(c.ClientIP(), " Invalid token ", clientToken, " for resource ", retrieveCode) return @@ -99,7 +104,8 @@ func DownloadItems(c *gin.Context) { if len(itemList) == 0 { c.JSON(http.StatusNotFound, gin.H{ - "error": "Cannot find the resource by retrieve code " + retrieveCode, + "err_code": 10006, + "message": common.Errors[10006], }) log.Println(c.ClientIP(), " resource ", retrieveCode, " not found") return @@ -116,6 +122,7 @@ func DownloadItems(c *gin.Context) { err := DeleteItem(singleItem.Bucket, singleItem.Path) if err != nil { log.Println("Cannot delete item in bucket ", singleItem.Bucket, ", path ", singleItem.Path) + // TODO: 返回 500 } // 删除数据库记录 @@ -123,7 +130,8 @@ func DownloadItems(c *gin.Context) { common.DeleteRedisRecodeFromRecode(singleItem.ReCode) // 返回 410 Gone c.JSON(http.StatusGone, gin.H{ - "error": "Over the expired time.", + "err_code": 10006, + "message": common.Errors[10006], }) log.Println(c.ClientIP(), " The retrieve code \"", retrieveCode, "\" resouce cannot be download due to the file duaration expired") return @@ -136,20 +144,28 @@ func DownloadItems(c *gin.Context) { err := DeleteItem(singleItem.Bucket, singleItem.Path) if err != nil { log.Println("Cannot delete item in bucket ", singleItem.Bucket, ", path ", singleItem.Path) + // TODO: 返回 500 } // 删除数据库记录 db.Delete(&singleItem) common.DeleteRedisRecodeFromRecode(singleItem.ReCode) c.JSON(http.StatusGone, gin.H{ - "error": "Out of downloadable count.", + "err_code": 10006, + "message": common.Errors[10006], }) log.Println(c.ClientIP(), " The retrieve code \"", retrieveCode, "\" resouce cannot be download due to downloadable counter = 0") return } // 剩余时间与下载次数合法,获取文件 - url := singleItem.Host + // 获取临时下载链接 + url, err := GetSignURL(singleItem.Bucket, singleItem.Path, common.GetAliyunOSSClient()) + if err != nil { + log.Println("Cannot get the signed downloadable link for item \"", singleItem.Bucket, singleItem.Path, "\"") + // TODO: 返回 500 + } + log.Println(c.ClientIP(), " Get the zip file signed url: ", url) c.JSON(http.StatusOK, gin.H{ "url": url, }) @@ -163,7 +179,27 @@ func DownloadItems(c *gin.Context) { // 缺少 ItemGroup log.Println(c.ClientIP(), " Cannot get the ItemGroup") c.JSON(http.StatusBadRequest, gin.H{ - "error": "Cannot get the items.", + "err_code": 10005, + "message": common.Errors[10005], + }) + return + } + + // 若为文件组中的单文件下载请求,直接返回签名链接 + if len(packRequest.ZipItems) == 1 { + singleItem := packRequest.ZipItems[0] + url, err := GetSignURL(singleItem.Bucket, singleItem.Path, common.GetAliyunOSSClient()) + if err != nil { + log.Println("Cannot get the signed downloadable link for item \"", singleItem.Bucket, singleItem.Path, "\"") + c.JSON(http.StatusInternalServerError, gin.H{ + "err_code": 20305, + "message": common.Errors[20305], + }) + return + } + log.Println(c.ClientIP(), " Get the single file signed url: ", url) + c.JSON(http.StatusOK, gin.H{ + "url": url, }) return } @@ -173,7 +209,8 @@ func DownloadItems(c *gin.Context) { if err != nil { log.Println(err) c.JSON(http.StatusServiceUnavailable, gin.H{ - "error": "Service Unavailable, please contact the maintainer.", + "err_code": 10002, + "message": common.Errors[10002], }) log.Println("[ERROR] Cannot get the proper FaaS zip config from cloud config") return @@ -203,26 +240,37 @@ func DownloadItems(c *gin.Context) { zipPack.Type = resJson["type"] zipPack.ArchiveType = common.ARCHIVE_FULL zipPack.DownCount = common.INFINITE_DOWNLOAD + zipPack.ExpiredAt = itemList[0].ExpiredAt // 过期时间跟随生成压缩包的文件有效时间 db.Create(&zipPack) log.Println("Generated the full files zip package for retrieve code \"", retrieveCode, "\"") - downloadLink := resJson["host"] - // 返回压缩包下载链接 + // 对压缩包签名 + url, err := GetSignURL(zipPack.Bucket, zipPack.Path, common.GetAliyunOSSClient()) + if err != nil { + log.Println("Cannot get the signed downloadable link for item \"", zipPack.Bucket, zipPack.Path, "\"") + // TODO: 返回 500 + } + log.Println(c.ClientIP(), " Get the zip file signed url: ", url) c.JSON(http.StatusOK, gin.H{ - "url": downloadLink, + "url": url, }) - log.Println(c.ClientIP(), " Get the zip file url: ", downloadLink) return } // 有全量打包,则直接发送打包文件 + // 对压缩包签名 + url, err := GetSignURL(zipPack.Bucket, zipPack.Path, common.GetAliyunOSSClient()) + if err != nil { + log.Println("Cannot get the signed downloadable link for item \"", zipPack.Bucket, zipPack.Path, "\"") + // TODO: 返回 500 + } c.JSON(http.StatusOK, gin.H{ - "url": zipPack.Host, + "url": url, }) - log.Println(c.ClientIP(), " Full zip pack has generated before, get the zip file url: ", zipPack.Host) + log.Println(c.ClientIP(), " Full zip pack has generated before, get the zip file signed url: ", url) return } @@ -246,23 +294,29 @@ func DownloadItems(c *gin.Context) { Bucket: resJson["bucket"], Endpoint: resJson["endpoint"], Path: resJson["path"], + ExpiredAt: itemList[0].ExpiredAt, // 过期时间跟随生成压缩包的文件有效时间 } // 先清除数据库之前同提取码的自定义压缩包记录 - var deleteZipPacks []Item - db.Where("re_code = ? AND (status = ? OR status = ?) AND archive_type = ?", retrieveCode, common.UPLOAD_FINISHED, common.FILE_ACTIVE, common.ARCHIVE_CUSTOM).Find(&deleteZipPacks) - for _, deleteZipPack := range deleteZipPacks { - db.Delete(&deleteZipPack) - } + // [4.5.2019] 不需要,不同压缩包可以共存 + //var deleteZipPacks []Item + //db.Where("re_code = ? AND (status = ? OR status = ?) AND archive_type = ?", retrieveCode, common.UPLOAD_FINISHED, common.FILE_ACTIVE, common.ARCHIVE_CUSTOM).Find(&deleteZipPacks) + //for _, deleteZipPack := range deleteZipPacks { + // db.Delete(&deleteZipPack) + //} db.Create(&zipPack) log.Println("Generated the custom files zip package for retrieve code \"", retrieveCode, "\"") - downloadLink := resJson["host"] - log.Println(c.ClientIP(), " Get the zip file url: ", downloadLink) - // 返回压缩包路径 + // 对自定义压缩包签名 + url, err := GetSignURL(zipPack.Bucket, zipPack.Path, common.GetAliyunOSSClient()) + if err != nil { + log.Println("Cannot get the signed downloadable link for item \"", zipPack.Bucket, zipPack.Path, "\"") + // TODO: 返回 500 + } + log.Println(c.ClientIP(), " Get the zip file signed url: ", url) c.JSON(http.StatusOK, gin.H{ - "url": downloadLink, + "url": url, }) return @@ -271,6 +325,7 @@ func DownloadItems(c *gin.Context) { func ZipItemsFaaS(zipItems []ZipItem, retrieveCode string, isFull bool, endpoint string) map[string]string { reqJson := map[string]interface{}{ "re_code": retrieveCode, + "uuid": uuid.Must(uuid.NewV4()).String(), "items": zipItems, "full": isFull, } @@ -284,6 +339,7 @@ func ZipItemsFaaS(zipItems []ZipItem, retrieveCode string, isFull bool, endpoint // 请求函数计算 res, err := http.Post(endpoint, "application/json", bytes.NewBuffer(bytesRepresentation)) if err != nil { + // TODO: 加入重试机制 log.Println(err) } @@ -311,15 +367,23 @@ func GetZipEndpoint() (string, error) { // 获取签名URL func GetSignURL(itemBucket string, itemPath string, client *oss.Client) (string, error) { + // TODO: 阿里云重试机制 bucket, err := client.Bucket(itemBucket) if err != nil { log.Println(fmt.Sprintf("Func: GetSignURL Get Client %v Bucket %s Failed %s", client, itemBucket, err.Error())) return "", err } - signedURL, err := bucket.SignURL(itemPath, oss.HTTPGet, common.FILE_DOWNLOAD_SIGNURL_TIME) + + // 请求头信息进行签名 + options := []oss.Option{ + oss.ContentType(common.OSS_DOWNLOAD_CONTENT_TYPE), + } + + signedURL, err := bucket.SignURL(itemPath, oss.HTTPGet, common.FILE_DOWNLOAD_SIGNURL_TIME, options...) if err != nil { log.Println(fmt.Sprintf("Func: GetSignURL Get Bucket %s Object %s Failed %s", itemBucket, itemPath, err.Error())) return "", err } + log.Println("signed url: ", signedURL) return signedURL, nil } diff --git a/item/downloadCount.go b/item/downloadCount.go index c8c3c4e..1e61086 100644 --- a/item/downloadCount.go +++ b/item/downloadCount.go @@ -10,11 +10,26 @@ import ( "github.com/gin-gonic/gin" ) -func DownloadCount(c *gin.Context) { +type DownloadRequest struct { + Bucket string `json:"bucket"` + Path string `json:"path"` +} + +func MinusDownloadCount(c *gin.Context) { retrieveCode := c.Param("retrieveCode") // 为文件组生命周期准备 - bucket := c.Query("bucket") - path := c.Query("path") + + var downloadRequest DownloadRequest + if err := c.ShouldBindJSON(&downloadRequest); err != nil { + log.Println(c.ClientIP(), " Cannot get the download request json string") + c.JSON(http.StatusBadRequest, gin.H{ + "err_code": 10005, + "message": common.Errors[10005], + }) + return + } + bucket := downloadRequest.Bucket + path := downloadRequest.Path db := common.GetDB() @@ -24,7 +39,8 @@ func DownloadCount(c *gin.Context) { // 提取码错误 if len(itemList) == 0 { c.JSON(http.StatusNotFound, gin.H{ - "error": "Cannot find the resource.", + "err_code": 10006, + "message": common.Errors[10006], }) log.Println(c.ClientIP(), " resource ", retrieveCode, " not found, cannot change download count") return diff --git a/item/info.go b/item/info.go new file mode 100644 index 0000000..d91ff25 --- /dev/null +++ b/item/info.go @@ -0,0 +1,37 @@ +package item + +import ( + "net/http" + + "a2os/safeu-backend/common" + + "github.com/gin-gonic/gin" +) + +type getItemInfoBody struct { + UserToken string `json:"user_token"` +} + +// GetItemInfo 获取文件信息 +func GetItemInfo(c *gin.Context) { + retrieveCode := c.Param("retrieveCode") + var getItemInfoBody getItemInfoBody + if common.FuncHandler(c, c.BindJSON(&getItemInfoBody), nil, http.StatusBadRequest, 20301) { + return + } + tokenRedisClient := common.GetUserTokenRedisClient() + if common.FuncHandler(c, KeyISExistInRedis(getItemInfoBody.UserToken, tokenRedisClient), true, http.StatusUnauthorized, 20201) { + return + } + db := common.GetDB() + var item Item + if common.FuncHandler(c, db.Where("re_code = ?", retrieveCode).First(&item).RecordNotFound(), false, http.StatusUnauthorized, 20306) { + return + } + c.JSON(http.StatusOK, gin.H{ + "down_count ": item.DownCount, + "expired_at": item.ExpiredAt, + "is_public": item.IsPublic, + }) + return +} diff --git a/item/update.go b/item/update.go index b4b3bca..9d370a9 100644 --- a/item/update.go +++ b/item/update.go @@ -42,15 +42,17 @@ func ChangeExpireTime(c *gin.Context) { err := c.BindJSON(&changeExpireTimeBody) if err != nil { log.Println(err) - c.JSON(http.StatusInternalServerError, gin.H{ - "message": err, + c.JSON(http.StatusBadRequest, gin.H{ + "err_code": 10003, + "message": common.Errors[10003], }) return } // 时间长度检查 if changeExpireTimeBody.NewExpireTime > common.FILE_MAX_EXIST_TIME || changeExpireTimeBody.NewExpireTime <= 0 { c.JSON(http.StatusBadRequest, gin.H{ - "message": "The length of the time is not within the right range", + "err_code": 10004, + "message": common.Errors[10004], }) return } @@ -58,7 +60,8 @@ func ChangeExpireTime(c *gin.Context) { files, err := tokenRedisClient.SMembers(changeExpireTimeBody.UserToken).Result() if len(files) == 0 { c.JSON(http.StatusUnauthorized, gin.H{ - "message": "Can`t Find User Token", + "err_code": 20201, + "message": common.Errors[20201], }) return } @@ -84,18 +87,19 @@ func ChangePassword(c *gin.Context) { err := c.BindJSON(&changePassBody) if err != nil { log.Println(err) - c.JSON(http.StatusInternalServerError, gin.H{ - "message": err, + c.JSON(http.StatusBadRequest, gin.H{ + "err_code": 10003, + "message": common.Errors[10003], }) return } - tokenRedisClient := common.GetUserTokenRedisClient() files, err := tokenRedisClient.SMembers(changePassBody.UserToken).Result() // 无文件则未从redis成功读取用户Token 鉴权失败 if len(files) == 0 { c.JSON(http.StatusUnauthorized, gin.H{ - "message": "Can`t Find User Token", + "err_code": 20201, + "message": common.Errors[20201], }) return } @@ -133,8 +137,9 @@ func ChangeRecode(c *gin.Context) { err := c.BindJSON(&changeRecodeBody) if err != nil { log.Println(err) - c.JSON(http.StatusInternalServerError, gin.H{ - "message": err, + c.JSON(http.StatusBadRequest, gin.H{ + "err_code": 10003, + "message": common.Errors[10003], }) return } @@ -145,7 +150,8 @@ func ChangeRecode(c *gin.Context) { if len(files) == 0 { log.Println("Can`t Find User Token In Redis", changeRecodeBody.UserToken) c.JSON(http.StatusUnauthorized, gin.H{ - "message": "Can`t Find User Token", + "err_code": 20201, + "message": common.Errors[20201], }) return } @@ -165,7 +171,8 @@ func ChangeRecode(c *gin.Context) { if CheckReCodeRepeatInDB(changeRecodeBody.NewReCode, db) { log.Println("Find reCode Repeat In DB", changeRecodeBody.NewReCode) c.JSON(http.StatusBadRequest, gin.H{ - "message": "reCode Repeat", + "err_code": 20307, + "message": common.Errors[20307], }) return } @@ -195,7 +202,8 @@ func ChangeRecode(c *gin.Context) { if changeRecodeBody.Auth == "" { log.Println("Item had password,but not Auth give Previous Recode:", retrieveCode) c.JSON(http.StatusBadRequest, gin.H{ - "message": "require Auth", + "err_code": 10005, + "message": common.Errors[10005], }) return } @@ -225,8 +233,9 @@ func ChangeDownCount(c *gin.Context) { err := c.BindJSON(&changeDownCount) if err != nil { log.Println(err) - c.JSON(http.StatusInternalServerError, gin.H{ - "message": err, + c.JSON(http.StatusBadRequest, gin.H{ + "err_code": 10003, + "message": common.Errors[10003], }) return } @@ -234,7 +243,8 @@ func ChangeDownCount(c *gin.Context) { files, err := tokenRedisClient.SMembers(changeDownCount.UserToken).Result() if len(files) == 0 { c.JSON(http.StatusUnauthorized, gin.H{ - "message": "Can`t Find User Token", + "err_code": 20201, + "message": common.Errors[20201], }) return } diff --git a/item/upload.go b/item/upload.go index bdbc7cc..0d8cdb9 100644 --- a/item/upload.go +++ b/item/upload.go @@ -24,7 +24,7 @@ import ( "a2os/safeu-backend/common" "github.com/gin-gonic/gin" - "github.com/satori/go.uuid" + uuid "github.com/satori/go.uuid" ) // 用户上传文件时指定的前缀。 @@ -68,7 +68,7 @@ func get_policy_token() string { expire_end := now + expire_time var tokenExpire = get_gmt_iso8601(expire_end) //upload_dir - upload_dir := "items/" + time.Now().Format("2006-01-02 15:04:05.00") + "/" + upload_dir := "items/" + uuid.Must(uuid.NewV4()).String() + "/" //create post policy json var config ConfigStruct config.Expiration = tokenExpire @@ -428,12 +428,6 @@ type FinishedFiles struct { Files []uuid.UUID `json:"files"` } -func UploadRegister(router *gin.RouterGroup) { - router.GET("/policy", GetPolicyToken) //鉴权 - router.POST("/callback", UploadCallBack) //回调 - router.POST("/finish", FinishUpload) //结束 -} - func GetPolicyToken(c *gin.Context) { response := get_policy_token() c.String(http.StatusOK, response) @@ -446,14 +440,16 @@ func FinishUpload(c *gin.Context) { err := c.BindJSON(&finishedFiles) if err != nil { log.Println(err) - c.JSON(http.StatusInternalServerError, gin.H{ - "message": err, + c.JSON(http.StatusBadRequest, gin.H{ + "err_code": 10003, + "message": common.Errors[10003], }) return } if finishedFiles.Files == nil { c.JSON(http.StatusBadRequest, gin.H{ - "message": "Parameter error", + "err_code": 10004, + "message": common.Errors[10004], }) return } @@ -473,7 +469,6 @@ func FinishUpload(c *gin.Context) { reCode := common.RandStringBytesMaskImprSrc(common.ReCodeLength) var files []string for _, value := range finishedFiles.Files { - fmt.Println(value) files = append(files, value.String()) db.Model(&Item{}).Where("name = ? AND status = ?", value, common.UPLOAD_BEGIN).Update(map[string]interface{}{"re_code": reCode, "status": common.UPLOAD_FINISHED}) } @@ -482,7 +477,15 @@ func FinishUpload(c *gin.Context) { // 将提取码推入Redis reCodeRedisClient := common.GetReCodeRedisClient() redisExpireTime, _ := time.ParseDuration(common.FILE_DEFAULT_EXIST_TIME) - reCodeRedisClient.Set(reCode, owner, redisExpireTime) + //reCodeRedisClient.Set(reCode, owner, redisExpireTime) + err = common.SetShadowKeyInRedis(reCode, owner, redisExpireTime, reCodeRedisClient) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "err_code": 10001, + "message": common.Errors[10001], + }) + return + } // 将用户识别码推入Redis tokenRedisClient := common.GetUserTokenRedisClient() tokenRedisClient.SAdd(owner, files) @@ -512,7 +515,8 @@ func UploadCallBack(c *gin.Context) { bytePublicKey, err := getPublicKey(r) if err != nil { c.JSON(http.StatusBadRequest, gin.H{ - "message": err, + "err_code": 20000, + "message": err, }) return } @@ -520,7 +524,8 @@ func UploadCallBack(c *gin.Context) { byteAuthorization, err := getAuthorization(r) if err != nil { c.JSON(http.StatusBadRequest, gin.H{ - "message": err, + "err_code": 20000, + "message": err, }) return } @@ -529,7 +534,8 @@ func UploadCallBack(c *gin.Context) { byteMD5, err := getMD5FromNewAuthString(r) if err != nil { c.JSON(http.StatusBadRequest, gin.H{ - "message": err, + "err_code": 20000, + "message": err, }) return } @@ -546,7 +552,8 @@ func UploadCallBack(c *gin.Context) { } else { log.Println("Fail") c.JSON(http.StatusBadRequest, gin.H{ - "message": err, + "err_code": 20000, + "message": err, }) return } @@ -559,8 +566,8 @@ func BuildItemFromCallBack(info FileInfo) *Item { h, _ := time.ParseDuration(common.FILE_DEFAULT_EXIST_TIME) item.Status = common.UPLOAD_BEGIN item.Name = uuid.Must(uuid.NewV4()).String() - // 目前的目录格式为: items/2019-02-23 09:43:27.63/your_file_name,因此第 29 个字符开始才是文件原名 - item.OriginalName = info.Object[29:] // 截取时间戳后的文件名称 + // 目前的目录格式为: items/uuid/your_file_name,因此第 43 个字符开始才是文件原名 + item.OriginalName = info.Object[43:] // 截取时间戳后的文件名称 item.Host = host item.DownCount = common.FILE_DEFAULT_DOWNCOUNT item.Type = info.MimeType diff --git a/item/validation.go b/item/validation.go index 39297ba..0b3688b 100644 --- a/item/validation.go +++ b/item/validation.go @@ -17,6 +17,18 @@ import ( "github.com/jinzhu/gorm" ) +type ResponseItem struct { + Name string `json:"name"` + OriginalName string `json:"original_name"` + DownCount int `json:"down_count"` + Type string `json:"type"` + Protocol string `json:"protocol"` + Bucket string `json:"bucket"` + Endpoint string `json:"endpoint"` + Path string `json:"path"` + ExpiredAt time.Time `json:"expired_at"` +} + type ValiPass struct { Password string `form:"password" json:"password" binding:"required"` } @@ -42,7 +54,8 @@ func Validation(c *gin.Context) { var curItem Item if db.Where("re_code = ? AND (status = ? OR status = ?) AND archive_type = ?", retrieveCode, common.UPLOAD_FINISHED, common.FILE_ACTIVE, common.ARCHIVE_NULL).First(&curItem).RecordNotFound() { c.JSON(http.StatusNotFound, gin.H{ - "error": "Cannot find resource via this retrieve code.", + "err_code": 10006, + "message": common.Errors[10006], }) log.Println(c.ClientIP(), " Cannot find resource via the retrieve code ", retrieveCode) return @@ -56,7 +69,8 @@ func Validation(c *gin.Context) { if err != nil { log.Println(c.ClientIP(), " check down count and expired time failed") c.JSON(http.StatusInternalServerError, gin.H{ - "error": "There is a problem for this resource, please contact the maintainer.", + "err_code": 10001, + "message": common.Errors[10001], }) return } @@ -64,7 +78,8 @@ func Validation(c *gin.Context) { // 文件过期/无下载次数被清空,返回 404 if len(itemList) == 0 { c.JSON(http.StatusNotFound, gin.H{ - "error": "Cannot find resource via this retrieve code.", + "err_code": 10006, + "message": common.Errors[10006], }) log.Println(c.ClientIP(), " Cannot find resource via the retrieve code ", retrieveCode) return @@ -77,9 +92,11 @@ func Validation(c *gin.Context) { db.Create(&tokenRecord) log.Println(c.ClientIP(), " Generated token ", token, " for retrieve code ", retrieveCode) + // 加工 itemList + responseItemList := GetResponseItemList(itemList) c.JSON(http.StatusOK, gin.H{ "token": token, - "items": itemList, + "items": responseItemList, }) return } @@ -90,7 +107,8 @@ func Validation(c *gin.Context) { var valiPass ValiPass if err := c.ShouldBindJSON(&valiPass); err != nil { c.JSON(http.StatusUnauthorized, gin.H{ - "error": "Cannot get the password", + "err_code": 10005, + "message": common.Errors[10005], }) log.Println(c.ClientIP(), " Cannot get the password from client, return 401 Unauthorized") return @@ -109,7 +127,8 @@ func Validation(c *gin.Context) { // 密码不正确/密码缺失返回 401 if hasherSum != refPassword { c.JSON(http.StatusUnauthorized, gin.H{ - "error": "The password is not correct", + "err_code": 20501, + "message": common.Errors[20501], }) log.Println(c.ClientIP(), " The password is not correct, return 401 Unauthorized") return @@ -120,7 +139,8 @@ func Validation(c *gin.Context) { if err != nil { log.Println(c.ClientIP(), " check down count and expired time failed") c.JSON(http.StatusInternalServerError, gin.H{ - "error": "There is a problem for this resource, please contact the maintainer.", + "err_code": 10001, + "message": common.Errors[10001], }) return } @@ -128,7 +148,8 @@ func Validation(c *gin.Context) { // 文件过期/无下载次数被清空,返回 404 if len(itemList) == 0 { c.JSON(http.StatusNotFound, gin.H{ - "error": "Cannot find resource via this retrieve code.", + "err_code": 10006, + "message": common.Errors[10006], }) log.Println(c.ClientIP(), " Cannot find resource via the retrieve code ", retrieveCode) return @@ -147,9 +168,11 @@ func Validation(c *gin.Context) { db.Create(&tokenRecord) log.Println(c.ClientIP(), " Generated token ", token, " for retrieve code ", retrieveCode) + // 加工 itemList + responseItemList := GetResponseItemList(itemList) c.JSON(http.StatusOK, gin.H{ "token": token, - "items": itemList, + "items": responseItemList, }) return } @@ -225,3 +248,23 @@ func CheckDownCountAndExpiredTime(db *gorm.DB, retrieveCode string) ([]Item, err return itemList, nil } + +func GetResponseItemList(itemList []Item) []ResponseItem { + var responseItemList []ResponseItem + for _, item := range itemList { + responseItem := ResponseItem{ + Protocol: item.Protocol, + Bucket: item.Bucket, + Endpoint: item.Endpoint, + Path: item.Path, + OriginalName: item.OriginalName, + Name: item.Name, + DownCount: item.DownCount, + Type: item.Type, + ExpiredAt: item.ExpiredAt, + } + responseItemList = append(responseItemList, responseItem) + } + + return responseItemList +} diff --git a/main.go b/main.go index 0da892d..b9a0bb5 100644 --- a/main.go +++ b/main.go @@ -18,14 +18,18 @@ import ( "fmt" "io" "log" + "net/http" "time" "a2os/safeu-backend/common" "a2os/safeu-backend/item" "github.com/gin-contrib/cors" + "github.com/gin-contrib/sessions" + "github.com/gin-contrib/sessions/cookie" "github.com/gin-gonic/gin" "github.com/jinzhu/gorm" + "github.com/utrack/gin-csrf" ) func Migrate(db *gorm.DB) { @@ -33,6 +37,12 @@ func Migrate(db *gorm.DB) { db.Set("gorm:table_options", "ENGINE=InnoDB CHARSET=utf8mb4 COLLATE=utf8mb4_bin auto_increment=1").AutoMigrate(&common.Config{}) db.Set("gorm:table_options", "ENGINE=InnoDB CHARSET=utf8mb4 COLLATE=utf8mb4_bin auto_increment=1").AutoMigrate(&item.Token{}) } + +// 系统启动后的任务 +func Tasks() { + // 主动删除 + go item.ActiveDelete(common.GetReCodeRedisClient()) +} func init() { // Logger init @@ -58,6 +68,8 @@ func init() { // 初始化阿里云对象存储客户端对象 common.InitAliyunOSSClient() + // 系统启动后的任务 + Tasks() } @@ -76,14 +88,23 @@ func main() { } r := gin.Default() + // 错误处理 + r.Use(common.ErrorHandling()) + r.Use(common.MaintenanceHandling()) // After init router + // CORS if common.DEBUG { r.Use(cors.New(cors.Config{ - AllowAllOrigins: true, + // The value of the 'Access-Control-Allow-Origin' header in the + // response must not be the wildcard '*' when the request's + // credentials mode is 'include'. + AllowOrigins: common.CORS_ALLOW_DEBUG_ORIGINS, AllowMethods: common.CORS_ALLOW_METHODS, AllowHeaders: common.CORS_ALLOW_HEADERS, + ExposeHeaders: common.CORS_EXPOSE_HEADERS, AllowCredentials: true, + AllowWildcard: true, MaxAge: 12 * time.Hour, })) //r.Use(CORS()) @@ -93,28 +114,60 @@ func main() { AllowOrigins: common.CORS_ALLOW_ORIGINS, AllowMethods: common.CORS_ALLOW_METHODS, AllowHeaders: common.CORS_ALLOW_HEADERS, + ExposeHeaders: common.CORS_EXPOSE_HEADERS, AllowCredentials: true, MaxAge: 12 * time.Hour, })) } + // CSRF + store := cookie.NewStore(common.CSRF_COOKIE_SECRET) + r.Use(sessions.Sessions(common.CSRF_SESSION_NAME, store)) + CSRF := csrf.Middleware(csrf.Options{ + Secret: common.CSRF_SECRET, + ErrorFunc: func(c *gin.Context) { + //c.String(http.StatusBadRequest, "CSRF token mismatch") + c.JSON(http.StatusBadRequest, gin.H{ + "err_code": 10007, + "message": common.Errors[10007], + }) + log.Println(c.ClientIP(), "CSRF token mismatch") + c.Abort() + }, + }) + r.GET("/ping", func(c *gin.Context) { - c.JSON(200, gin.H{ + c.JSON(http.StatusOK, gin.H{ "message": "pong", }) }) + r.GET("/csrf", CSRF, func(c *gin.Context) { + c.Header("X-CSRF-TOKEN", csrf.GetToken(c)) + c.String(http.StatusOK, "IN HEADER") + log.Println(c.ClientIP(), "response CSRF token", csrf.GetToken(c)) + }) + + // the API without CSRF middleware v1 := r.Group("/v1") { - item.UploadRegister(v1.Group("/upload")) - v1.POST("/password/:retrieveCode", item.ChangePassword) - v1.POST("/recode/:retrieveCode", item.ChangeRecode) - v1.POST("/delete/:retrieveCode", item.DeleteManual) - v1.GET("/downCount/:retrieveCode", item.DownloadCount) - v1.POST("/downCount/:retrieveCode", item.ChangeDownCount) - v1.POST("/expireTime/:retrieveCode", item.ChangeExpireTime) - v1.POST("/item/:retrieveCode", item.DownloadItems) - v1.POST("/validation/:retrieveCode", item.Validation) + v1.POST("/upload/callback", item.UploadCallBack) //回调 + } + + // the API with CSRF middleware + v1_csrf := r.Group("/v1", CSRF) + { + v1_csrf.GET("/upload/policy", item.GetPolicyToken) //鉴权 + v1_csrf.POST("/upload/finish", item.FinishUpload) //结束 + v1_csrf.POST("/password/:retrieveCode", item.ChangePassword) + v1_csrf.POST("/recode/:retrieveCode", item.ChangeRecode) + v1_csrf.POST("/delete/:retrieveCode", item.DeleteManual) + v1_csrf.POST("/info/:retrieveCode", item.GetItemInfo) + v1_csrf.POST("/minusDownCount/:retrieveCode", item.MinusDownloadCount) + v1_csrf.POST("/downCount/:retrieveCode", item.ChangeDownCount) + v1_csrf.POST("/expireTime/:retrieveCode", item.ChangeExpireTime) + v1_csrf.POST("/item/:retrieveCode", item.DownloadItems) + v1_csrf.POST("/validation/:retrieveCode", item.Validation) } r.Run(":" + common.PORT) // listen and serve on 0.0.0.0:PORT diff --git a/runner.conf b/runner.conf index 1d6850c..4c60a9a 100644 --- a/runner.conf +++ b/runner.conf @@ -4,7 +4,7 @@ build_name: runner-build build_log: runner-build-errors.log valid_ext: .go, .tpl, .tmpl, .html no_rebuild_ext: .tpl, .tmpl, .html -ignored: assets, tmp, vendor, db-data, data, log, api, build, deployments +ignored: assets, tmp, vendor, db-data, data, data-dev, log, api, build, deployments build_delay: 600 colors: 1 log_color_main: cyan diff --git a/scripts/dev-docker-compose.sh b/scripts/dev-docker-compose.sh new file mode 100755 index 0000000..90eac1c --- /dev/null +++ b/scripts/dev-docker-compose.sh @@ -0,0 +1,59 @@ +#!/bin/bash +# Author: TripleZ +# Date: 2019-03-11 + +echo -e "\n Build, up, down, restart, pull, check logs for SafeU development docker clusters.\n" + +if [ "$1" == "up" ] +then + mkdir -p ../data-dev + echo -e " Running dockers on daemon mode? (y/n, default: n): \c" + read isD + + if [ "$isD" = "y" ]||[ "$isD" = "Y" ] + then + sudo docker-compose -f ../deployments/dev-safeu/docker-compose.yml up -d + else + sudo docker-compose -f ../deployments/dev-safeu/docker-compose.yml up + fi +elif [ "$1" == "down" ] +then + sudo docker-compose -f ../deployments/dev-safeu/docker-compose.yml down +elif [ "$1" == "build" ] +then + sudo docker-compose -f ../deployments/dev-safeu/docker-compose.yml build --force-rm +elif [ "$1" == "pull" ] +then + sudo docker-compose -f ../deployments/dev-safeu/docker-compose.yml pull +elif [ "$1" == "restart" ] +then + sudo docker-compose -f ../deployments/dev-safeu/docker-compose.yml restart -t 10 +elif [ "$1" == "logs" ] +then + echo -e " Follow log output? (y/n, default: n): \c" + read isF + echo "" + if [ "$isF" == "y" ] || [ "$isF" == "Y" ] + then + sudo docker-compose -f ../deployments/dev-safeu/docker-compose.yml logs -f + else + sudo docker-compose -f ../deployments/dev-safeu/docker-compose.yml logs + fi +elif [ "$1" == "help" ] || [ "$1" == "-h" ] || [ "$1" == "--help" ] +then + echo -e " Usage: + ./dev-docker-compose.sh [COMMAND] + ./dev-docker-compose.sh -h|--help + + Commands: + build Build SafeU dev container images + down Down SafeU dev containers + help Show this help message + logs View output from dev containers + pull Pull SafeU dev container images + restart Restart SafeU dev containers + up Up SafeU dev containers + " +else + echo -e " Cannot match the command \"$1\", please type \"help\" command for help." +fi diff --git a/scripts/docker-compose-up-development.sh b/scripts/docker-compose-up-development.sh deleted file mode 100755 index 938e5c1..0000000 --- a/scripts/docker-compose-up-development.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -mkdir -p ../db-data - -echo "on daemon ?(y/n) " -read isD - -if [ "$isD" = "y" ]||[ "$isD" = "Y" ] -then - sudo docker-compose -f ../deployments/development/docker-compose.yml up --force-recreate --build -d -else - sudo docker-compose -f ../deployments/development/docker-compose.yml up --force-recreate --build -fi diff --git a/scripts/docker-compose-up-production.sh b/scripts/docker-compose-up-production.sh deleted file mode 100755 index 310664e..0000000 --- a/scripts/docker-compose-up-production.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -mkdir -p ../data/mariadb - -sudo docker-compose -f ../deployments/production/docker-compose.yml up --force-recreate --build -d diff --git a/scripts/getcert.sh b/scripts/getcert.sh new file mode 100755 index 0000000..4146277 --- /dev/null +++ b/scripts/getcert.sh @@ -0,0 +1,37 @@ +#! /bin/bash +# Author: Jinjin Feng, Zhenzhen Zhao +# Date: 2019-04-14 + +if [ "$1" == "-h" ] || [ "$1" == "--help" ] +then + echo -e " Usage: + ./getcert.sh [DOMAIN] Generate domain HTTPS certification + ./getcert.sh -h|--help Get this help message + " +elif [ "$1" != "" ] +then + domain=$1 + read -s -p "Authorization password: " password + echo + + mkdir ~/.secrets/ + mkdir ~/.secrets/certbot/ + chmod 700 ~/.secrets/ + + header="authorization: Basic " + auth="a2os:"$password + auth=$(echo -n $auth | base64) + header+="$auth" + + curl -o ~/.secrets/certbot/cloudflare.ini https://api.vvzero.com/certbot/cloudflare.ini -H "$header" + chmod 400 ~/.secrets/certbot/cloudflare.ini + + # just for Ubuntu/Debian + apt update + apt install python-pip -y + pip install certbot-dns-cloudflare + + certbot certonly --dns-cloudflare --dns-cloudflare-credentials ~/.secrets/certbot/cloudflare.ini -d $domain +else + echo "Cannot get the domain value, please use \"-h\" or \"--help\" to get help." +fi diff --git a/scripts/prod-docker-compose.sh b/scripts/prod-docker-compose.sh new file mode 100755 index 0000000..456f33a --- /dev/null +++ b/scripts/prod-docker-compose.sh @@ -0,0 +1,58 @@ +#!/bin/bash +# Author: TripleZ +# Date: 2019-03-11 + +echo -e "\n Build, up, down, restart, pull, check logs for SafeU production docker clusters.\n" + +if [ "$1" == "up" ] +then + mkdir -p ../data + sudo docker-compose -f ../deployments/prod-safeu/docker-compose.yml up -d + +elif [ "$1" == "down" ] +then + sudo docker-compose -f ../deployments/prod-safeu/docker-compose.yml down + +elif [ "$1" == "build" ] +then + sudo docker-compose -f ../deployments/prod-safeu/docker-compose.yml build --force-rm + +elif [ "$1" == "restart" ] +then + sudo docker-compose -f ../deployments/prod-safeu/docker-compose.yml restart -t 10 + +elif [ "$1" == "pull" ] +then + sudo docker-compose -f ../deployments/prod-safeu/docker-compose.yml pull + +elif [ "$1" == "logs" ] +then + echo -e " Follow log output? (y/n, default: n): \c" + read isF + echo "" + if [ "$isF" == "y" ] || [ "$isF" == "Y" ] + then + sudo docker-compose -f ../deployments/prod-safeu/docker-compose.yml logs -f + else + sudo docker-compose -f ../deployments/prod-safeu/docker-compose.yml logs + fi + +elif [ "$1" == "help" ] || [ "$1" == "-h" ] || [ "$1" == "--help" ] +then + echo -e " Usage: + ./prod-docker-compose.sh [COMMAND] + ./prod-docker-compose.sh -h|--help + + Commands: + build Build SafeU prod container images + down Down SafeU prod containers + help Show this help message + logs View output from prod containers + pull Pull SafeU prod container images + restart Restart SafeU prod containers + up Up SafeU prod containers + " + +else + echo -e " Cannot match the command \"$1\", please type \"help\" command for help." +fi diff --git a/scripts/remove-database-files.sh b/scripts/remove-database-files.sh new file mode 100755 index 0000000..988f957 --- /dev/null +++ b/scripts/remove-database-files.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +echo -e "\n Are you sure to remove all the database files? (y/n, default: n): \c" +read isSure + +if [ "$isSure" == "y" ] || [ "$isSure" == "Y" ] +then + echo -e "\n Removing the development database data..." + sudo rm -rf ../data-dev + + echo -e "\n Are you SURE to remove the production database data??? + ALL DATA WILL BE LOST!!!\n (y/n, default: n): \c" + read isProdSure + if [ "$isProdSure" == "y" ] || [ "$isProdSure" == "Y" ] + then + echo -e "\n Removing the production database data..." + sudo rm -rf ../data + fi +else + echo -e "\n Be careful!" +fi + +echo -e "\n Bye~\n" diff --git a/vendor/github.com/dchest/uniuri/README.md b/vendor/github.com/dchest/uniuri/README.md new file mode 100644 index 0000000..b321a5f --- /dev/null +++ b/vendor/github.com/dchest/uniuri/README.md @@ -0,0 +1,97 @@ +Package uniuri +===================== + +[![Build Status](https://travis-ci.org/dchest/uniuri.svg)](https://travis-ci.org/dchest/uniuri) + +```go +import "github.com/dchest/uniuri" +``` + +Package uniuri generates random strings good for use in URIs to identify +unique objects. + +Example usage: + +```go +s := uniuri.New() // s is now "apHCJBl7L1OmC57n" +``` + +A standard string created by New() is 16 bytes in length and consists of +Latin upper and lowercase letters, and numbers (from the set of 62 allowed +characters), which means that it has ~95 bits of entropy. To get more +entropy, you can use NewLen(UUIDLen), which returns 20-byte string, giving +~119 bits of entropy, or any other desired length. + +Functions read from crypto/rand random source, and panic if they fail to +read from it. + + +Constants +--------- + +```go +const ( + // StdLen is a standard length of uniuri string to achive ~95 bits of entropy. + StdLen = 16 + // UUIDLen is a length of uniuri string to achive ~119 bits of entropy, closest + // to what can be losslessly converted to UUIDv4 (122 bits). + UUIDLen = 20 +) + +``` + + + +Variables +--------- + +```go +var StdChars = []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") +``` + + +StdChars is a set of standard characters allowed in uniuri string. + + +Functions +--------- + +### func New + +```go +func New() string +``` + +New returns a new random string of the standard length, consisting of +standard characters. + +### func NewLen + +```go +func NewLen(length int) string +``` + +NewLen returns a new random string of the provided length, consisting of +standard characters. + +### func NewLenChars + +```go +func NewLenChars(length int, chars []byte) string +``` + +NewLenChars returns a new random string of the provided length, consisting +of the provided byte slice of allowed characters (maximum 256). + + + +Public domain dedication +------------------------ + +Written in 2011-2014 by Dmitry Chestnykh + +The author(s) have dedicated all copyright and related and +neighboring rights to this software to the public domain +worldwide. Distributed without any warranty. +http://creativecommons.org/publicdomain/zero/1.0/ + diff --git a/vendor/github.com/dchest/uniuri/uniuri.go b/vendor/github.com/dchest/uniuri/uniuri.go new file mode 100644 index 0000000..6393446 --- /dev/null +++ b/vendor/github.com/dchest/uniuri/uniuri.go @@ -0,0 +1,81 @@ +// Written in 2011-2014 by Dmitry Chestnykh +// +// The author(s) have dedicated all copyright and related and +// neighboring rights to this software to the public domain +// worldwide. Distributed without any warranty. +// http://creativecommons.org/publicdomain/zero/1.0/ + +// Package uniuri generates random strings good for use in URIs to identify +// unique objects. +// +// Example usage: +// +// s := uniuri.New() // s is now "apHCJBl7L1OmC57n" +// +// A standard string created by New() is 16 bytes in length and consists of +// Latin upper and lowercase letters, and numbers (from the set of 62 allowed +// characters), which means that it has ~95 bits of entropy. To get more +// entropy, you can use NewLen(UUIDLen), which returns 20-byte string, giving +// ~119 bits of entropy, or any other desired length. +// +// Functions read from crypto/rand random source, and panic if they fail to +// read from it. +package uniuri + +import "crypto/rand" + +const ( + // StdLen is a standard length of uniuri string to achive ~95 bits of entropy. + StdLen = 16 + // UUIDLen is a length of uniuri string to achive ~119 bits of entropy, closest + // to what can be losslessly converted to UUIDv4 (122 bits). + UUIDLen = 20 +) + +// StdChars is a set of standard characters allowed in uniuri string. +var StdChars = []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") + +// New returns a new random string of the standard length, consisting of +// standard characters. +func New() string { + return NewLenChars(StdLen, StdChars) +} + +// NewLen returns a new random string of the provided length, consisting of +// standard characters. +func NewLen(length int) string { + return NewLenChars(length, StdChars) +} + +// NewLenChars returns a new random string of the provided length, consisting +// of the provided byte slice of allowed characters (maximum 256). +func NewLenChars(length int, chars []byte) string { + if length == 0 { + return "" + } + clen := len(chars) + if clen < 2 || clen > 256 { + panic("uniuri: wrong charset length for NewLenChars") + } + maxrb := 255 - (256 % clen) + b := make([]byte, length) + r := make([]byte, length+(length/4)) // storage for random bytes. + i := 0 + for { + if _, err := rand.Read(r); err != nil { + panic("uniuri: error reading random bytes: " + err.Error()) + } + for _, rb := range r { + c := int(rb) + if c > maxrb { + // Skip this number to avoid modulo bias. + continue + } + b[i] = chars[c%clen] + i++ + if i == length { + return string(b) + } + } + } +} diff --git a/vendor/github.com/gin-contrib/sessions/LICENSE b/vendor/github.com/gin-contrib/sessions/LICENSE new file mode 100644 index 0000000..4e2cfb0 --- /dev/null +++ b/vendor/github.com/gin-contrib/sessions/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016 Gin-Gonic + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/gin-contrib/sessions/README.md b/vendor/github.com/gin-contrib/sessions/README.md new file mode 100644 index 0000000..855e248 --- /dev/null +++ b/vendor/github.com/gin-contrib/sessions/README.md @@ -0,0 +1,247 @@ +# sessions + +[![Build Status](https://travis-ci.org/gin-contrib/sessions.svg)](https://travis-ci.org/gin-contrib/sessions) +[![codecov](https://codecov.io/gh/gin-contrib/sessions/branch/master/graph/badge.svg)](https://codecov.io/gh/gin-contrib/sessions) +[![Go Report Card](https://goreportcard.com/badge/github.com/gin-contrib/sessions)](https://goreportcard.com/report/github.com/gin-contrib/sessions) +[![GoDoc](https://godoc.org/github.com/gin-contrib/sessions?status.svg)](https://godoc.org/github.com/gin-contrib/sessions) +[![Join the chat at https://gitter.im/gin-gonic/gin](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/gin-gonic/gin) + +Gin middleware for session management with multi-backend support (currently cookie, Redis, Memcached, MongoDB, memstore). + +## Usage + +### Start using it + +Download and install it: + +```bash +$ go get github.com/gin-contrib/sessions +``` + +Import it in your code: + +```go +import "github.com/gin-contrib/sessions" +``` + +## Examples + +#### cookie-based + +[embedmd]:# (example/cookie/main.go go) +```go +package main + +import ( + "github.com/gin-contrib/sessions" + "github.com/gin-contrib/sessions/cookie" + "github.com/gin-gonic/gin" +) + +func main() { + r := gin.Default() + store := cookie.NewStore([]byte("secret")) + r.Use(sessions.Sessions("mysession", store)) + + r.GET("/incr", func(c *gin.Context) { + session := sessions.Default(c) + var count int + v := session.Get("count") + if v == nil { + count = 0 + } else { + count = v.(int) + count++ + } + session.Set("count", count) + session.Save() + c.JSON(200, gin.H{"count": count}) + }) + r.Run(":8000") +} +``` + +#### Redis + +[embedmd]:# (example/redis/main.go go) +```go +package main + +import ( + "github.com/gin-contrib/sessions" + "github.com/gin-contrib/sessions/redis" + "github.com/gin-gonic/gin" +) + +func main() { + r := gin.Default() + store, _ := redis.NewStore(10, "tcp", "localhost:6379", "", []byte("secret")) + r.Use(sessions.Sessions("mysession", store)) + + r.GET("/incr", func(c *gin.Context) { + session := sessions.Default(c) + var count int + v := session.Get("count") + if v == nil { + count = 0 + } else { + count = v.(int) + count++ + } + session.Set("count", count) + session.Save() + c.JSON(200, gin.H{"count": count}) + }) + r.Run(":8000") +} +``` + +#### Memcached (ASCII protocol) + +[embedmd]:# (example/memcached/ascii.go go) +```go +package main + +import ( + "github.com/bradfitz/gomemcache/memcache" + "github.com/gin-contrib/sessions" + "github.com/gin-contrib/sessions/memcached" + "github.com/gin-gonic/gin" +) + +func main() { + r := gin.Default() + store := memcached.NewStore(memcache.New("localhost:11211"), "", []byte("secret")) + r.Use(sessions.Sessions("mysession", store)) + + r.GET("/incr", func(c *gin.Context) { + session := sessions.Default(c) + var count int + v := session.Get("count") + if v == nil { + count = 0 + } else { + count = v.(int) + count++ + } + session.Set("count", count) + session.Save() + c.JSON(200, gin.H{"count": count}) + }) + r.Run(":8000") +} +``` + +#### Memcached (binary protocol with optional SASL authentication) + +[embedmd]:# (example/memcached/binary.go go) +```go +package main + +import ( + "github.com/gin-contrib/sessions" + "github.com/gin-contrib/sessions/memcached" + "github.com/gin-gonic/gin" + "github.com/memcachier/mc" +) + +func main() { + r := gin.Default() + client := mc.NewMC("localhost:11211", "username", "password") + store := memcached.NewMemcacheStore(client, "", []byte("secret")) + r.Use(sessions.Sessions("mysession", store)) + + r.GET("/incr", func(c *gin.Context) { + session := sessions.Default(c) + var count int + v := session.Get("count") + if v == nil { + count = 0 + } else { + count = v.(int) + count++ + } + session.Set("count", count) + session.Save() + c.JSON(200, gin.H{"count": count}) + }) + r.Run(":8000") +} +``` + +#### MongoDB + +[embedmd]:# (example/mongo/main.go go) +```go +package main + +import ( + "github.com/gin-contrib/sessions" + "github.com/gin-contrib/sessions/mongo" + "github.com/gin-gonic/gin" + "github.com/globalsign/mgo" +) + +func main() { + r := gin.Default() + session, err := mgo.Dial("localhost:27017/test") + if err != nil { + // handle err + } + + c := session.DB("").C("sessions") + store := mongo.NewStore(c, 3600, true, []byte("secret")) + r.Use(sessions.Sessions("mysession", store)) + + r.GET("/incr", func(c *gin.Context) { + session := sessions.Default(c) + var count int + v := session.Get("count") + if v == nil { + count = 0 + } else { + count = v.(int) + count++ + } + session.Set("count", count) + session.Save() + c.JSON(200, gin.H{"count": count}) + }) + r.Run(":8000") +} +``` + +#### memstore + +[embedmd]:# (example/memstore/main.go go) +```go +package main + +import ( + "github.com/gin-contrib/sessions" + "github.com/gin-contrib/sessions/memstore" + "github.com/gin-gonic/gin" +) + +func main() { + r := gin.Default() + store := memstore.NewStore([]byte("secret")) + r.Use(sessions.Sessions("mysession", store)) + + r.GET("/incr", func(c *gin.Context) { + session := sessions.Default(c) + var count int + v := session.Get("count") + if v == nil { + count = 0 + } else { + count = v.(int) + count++ + } + session.Set("count", count) + session.Save() + c.JSON(200, gin.H{"count": count}) + }) + r.Run(":8000") +} +``` diff --git a/vendor/github.com/gin-contrib/sessions/cookie/cookie.go b/vendor/github.com/gin-contrib/sessions/cookie/cookie.go new file mode 100644 index 0000000..9434b8f --- /dev/null +++ b/vendor/github.com/gin-contrib/sessions/cookie/cookie.go @@ -0,0 +1,37 @@ +package cookie + +import ( + "github.com/gin-contrib/sessions" + gsessions "github.com/gorilla/sessions" +) + +type Store interface { + sessions.Store +} + +// Keys are defined in pairs to allow key rotation, but the common case is to set a single +// authentication key and optionally an encryption key. +// +// The first key in a pair is used for authentication and the second for encryption. The +// encryption key can be set to nil or omitted in the last pair, but the authentication key +// is required in all pairs. +// +// It is recommended to use an authentication key with 32 or 64 bytes. The encryption key, +// if set, must be either 16, 24, or 32 bytes to select AES-128, AES-192, or AES-256 modes. +func NewStore(keyPairs ...[]byte) Store { + return &store{gsessions.NewCookieStore(keyPairs...)} +} + +type store struct { + *gsessions.CookieStore +} + +func (c *store) Options(options sessions.Options) { + c.CookieStore.Options = &gsessions.Options{ + Path: options.Path, + Domain: options.Domain, + MaxAge: options.MaxAge, + Secure: options.Secure, + HttpOnly: options.HttpOnly, + } +} diff --git a/vendor/github.com/gin-contrib/sessions/go.mod b/vendor/github.com/gin-contrib/sessions/go.mod new file mode 100644 index 0000000..9241030 --- /dev/null +++ b/vendor/github.com/gin-contrib/sessions/go.mod @@ -0,0 +1,33 @@ +module github.com/gin-contrib/sessions + +require ( + github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff + github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737 + github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20181103040241-659414f458e1 + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 // indirect + github.com/gin-gonic/gin v1.3.0 + github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8 + github.com/golang/protobuf v1.2.0 // indirect + github.com/gomodule/redigo v2.0.0+incompatible + github.com/gorilla/context v1.1.1 + github.com/gorilla/sessions v1.1.3 + github.com/json-iterator/go v1.1.5 // indirect + github.com/kidstuff/mongostore v0.0.0-20181113001930-e650cd85ee4b + github.com/kr/pretty v0.1.0 // indirect + github.com/mattn/go-isatty v0.0.4 // indirect + github.com/memcachier/mc v2.0.1+incompatible + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/quasoft/memstore v0.0.0-20180925164028-84a050167438 + github.com/stretchr/testify v1.2.2 // indirect + github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2 // indirect + golang.org/x/net v0.0.0-20181220203305-927f97764cc3 // indirect + golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 // indirect + golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb // indirect + gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect + gopkg.in/go-playground/assert.v1 v1.2.1 // indirect + gopkg.in/go-playground/validator.v8 v8.18.2 // indirect + gopkg.in/yaml.v2 v2.2.2 // indirect +) diff --git a/vendor/github.com/gin-contrib/sessions/go.sum b/vendor/github.com/gin-contrib/sessions/go.sum new file mode 100644 index 0000000..f9d50f2 --- /dev/null +++ b/vendor/github.com/gin-contrib/sessions/go.sum @@ -0,0 +1,65 @@ +github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff h1:RmdPFa+slIr4SCBg4st/l/vZWVe9QJKMXGO60Bxbe04= +github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw= +github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737 h1:rRISKWyXfVxvoa702s91Zl5oREZTrR3yv+tXrrX7G/g= +github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60= +github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20181103040241-659414f458e1 h1:4QHxgr7hM4gVD8uOwrk8T1fjkKRLwaLjmTkU0ibhZKU= +github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20181103040241-659414f458e1/go.mod h1:dkChI7Tbtx7H1Tj7TqGSZMOeGpMP5gLHtjroHd4agiI= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 h1:AzN37oI0cOS+cougNAV9szl6CVoj2RYwzS3DpUQNtlY= +github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= +github.com/gin-gonic/gin v1.3.0 h1:kCmZyPklC0gVdL728E6Aj20uYBJV93nj/TkwBTKhFbs= +github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y= +github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8 h1:DujepqpGd1hyOd7aW59XpK7Qymp8iy83xq74fLr21is= +github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0= +github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= +github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= +github.com/gorilla/sessions v1.1.3 h1:uXoZdcdA5XdXF3QzuSlheVRUvjl+1rKY7zBXL68L9RU= +github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= +github.com/json-iterator/go v1.1.5 h1:gL2yXlmiIo4+t+y32d4WGwOjKGYcGOuyrg46vadswDE= +github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/kidstuff/mongostore v0.0.0-20181113001930-e650cd85ee4b h1:TLCm7HR+P9HM2NXaAJaIiHerOUMedtFJeAfaYwZ8YhY= +github.com/kidstuff/mongostore v0.0.0-20181113001930-e650cd85ee4b/go.mod h1:g2nVr8KZVXJSS97Jo8pJ0jgq29P6H7dG0oplUA86MQw= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/memcachier/mc v2.0.1+incompatible h1:s8EDz0xrJLP8goitwZOoq1vA/sm0fPS4X3KAF0nyhWQ= +github.com/memcachier/mc v2.0.1+incompatible/go.mod h1:7bkvFE61leUBvXz+yxsOnGBQSZpBSPIMUQSmmSHvuXc= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quasoft/memstore v0.0.0-20180925164028-84a050167438 h1:jnz/4VenymvySjE+Ez511s0pqVzkUOmr1fwCVytNNWk= +github.com/quasoft/memstore v0.0.0-20180925164028-84a050167438/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg= +github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2 h1:EICbibRW4JNKMcY+LsWmuwob+CRS1BmdRdjphAm9mH4= +github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3 h1:eH6Eip3UpmR+yM/qI9Ijluzb1bNv/cAU/n+6l8tRSis= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb h1:pf3XwC90UUdNPYWZdFjhGBE7DUFuK3Ct1zWmZ65QN30= +golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= +gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= +gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ= +gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/vendor/github.com/gin-contrib/sessions/sessions.go b/vendor/github.com/gin-contrib/sessions/sessions.go new file mode 100644 index 0000000..d9f86c4 --- /dev/null +++ b/vendor/github.com/gin-contrib/sessions/sessions.go @@ -0,0 +1,147 @@ +package sessions + +import ( + "log" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/gorilla/context" + "github.com/gorilla/sessions" +) + +const ( + DefaultKey = "github.com/gin-contrib/sessions" + errorFormat = "[sessions] ERROR! %s\n" +) + +type Store interface { + sessions.Store + Options(Options) +} + +// Options stores configuration for a session or session store. +// Fields are a subset of http.Cookie fields. +type Options struct { + Path string + Domain string + // MaxAge=0 means no 'Max-Age' attribute specified. + // MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0'. + // MaxAge>0 means Max-Age attribute present and given in seconds. + MaxAge int + Secure bool + HttpOnly bool +} + +// Wraps thinly gorilla-session methods. +// Session stores the values and optional configuration for a session. +type Session interface { + // Get returns the session value associated to the given key. + Get(key interface{}) interface{} + // Set sets the session value associated to the given key. + Set(key interface{}, val interface{}) + // Delete removes the session value associated to the given key. + Delete(key interface{}) + // Clear deletes all values in the session. + Clear() + // AddFlash adds a flash message to the session. + // A single variadic argument is accepted, and it is optional: it defines the flash key. + // If not defined "_flash" is used by default. + AddFlash(value interface{}, vars ...string) + // Flashes returns a slice of flash messages from the session. + // A single variadic argument is accepted, and it is optional: it defines the flash key. + // If not defined "_flash" is used by default. + Flashes(vars ...string) []interface{} + // Options sets confuguration for a session. + Options(Options) + // Save saves all sessions used during the current request. + Save() error +} + +func Sessions(name string, store Store) gin.HandlerFunc { + return func(c *gin.Context) { + s := &session{name, c.Request, store, nil, false, c.Writer} + c.Set(DefaultKey, s) + defer context.Clear(c.Request) + c.Next() + } +} + +type session struct { + name string + request *http.Request + store Store + session *sessions.Session + written bool + writer http.ResponseWriter +} + +func (s *session) Get(key interface{}) interface{} { + return s.Session().Values[key] +} + +func (s *session) Set(key interface{}, val interface{}) { + s.Session().Values[key] = val + s.written = true +} + +func (s *session) Delete(key interface{}) { + delete(s.Session().Values, key) + s.written = true +} + +func (s *session) Clear() { + for key := range s.Session().Values { + s.Delete(key) + } +} + +func (s *session) AddFlash(value interface{}, vars ...string) { + s.Session().AddFlash(value, vars...) + s.written = true +} + +func (s *session) Flashes(vars ...string) []interface{} { + s.written = true + return s.Session().Flashes(vars...) +} + +func (s *session) Options(options Options) { + s.Session().Options = &sessions.Options{ + Path: options.Path, + Domain: options.Domain, + MaxAge: options.MaxAge, + Secure: options.Secure, + HttpOnly: options.HttpOnly, + } +} + +func (s *session) Save() error { + if s.Written() { + e := s.Session().Save(s.request, s.writer) + if e == nil { + s.written = false + } + return e + } + return nil +} + +func (s *session) Session() *sessions.Session { + if s.session == nil { + var err error + s.session, err = s.store.Get(s.request, s.name) + if err != nil { + log.Printf(errorFormat, err) + } + } + return s.session +} + +func (s *session) Written() bool { + return s.written +} + +// shortcut to get session +func Default(c *gin.Context) Session { + return c.MustGet(DefaultKey).(Session) +} diff --git a/vendor/github.com/gorilla/context/LICENSE b/vendor/github.com/gorilla/context/LICENSE new file mode 100644 index 0000000..0e5fb87 --- /dev/null +++ b/vendor/github.com/gorilla/context/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2012 Rodrigo Moraes. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/gorilla/context/README.md b/vendor/github.com/gorilla/context/README.md new file mode 100644 index 0000000..d31f2ba --- /dev/null +++ b/vendor/github.com/gorilla/context/README.md @@ -0,0 +1,10 @@ +context +======= +[![Build Status](https://travis-ci.org/gorilla/context.png?branch=master)](https://travis-ci.org/gorilla/context) + +gorilla/context is a general purpose registry for global request variables. + +> Note: gorilla/context, having been born well before `context.Context` existed, does not play well +> with the shallow copying of the request that [`http.Request.WithContext`](https://golang.org/pkg/net/http/#Request.WithContext) (added to net/http Go 1.7 onwards) performs. You should either use *just* gorilla/context, or moving forward, the new `http.Request.Context()`. + +Read the full documentation here: https://www.gorillatoolkit.org/pkg/context diff --git a/vendor/github.com/gorilla/context/context.go b/vendor/github.com/gorilla/context/context.go new file mode 100644 index 0000000..81cb128 --- /dev/null +++ b/vendor/github.com/gorilla/context/context.go @@ -0,0 +1,143 @@ +// Copyright 2012 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package context + +import ( + "net/http" + "sync" + "time" +) + +var ( + mutex sync.RWMutex + data = make(map[*http.Request]map[interface{}]interface{}) + datat = make(map[*http.Request]int64) +) + +// Set stores a value for a given key in a given request. +func Set(r *http.Request, key, val interface{}) { + mutex.Lock() + if data[r] == nil { + data[r] = make(map[interface{}]interface{}) + datat[r] = time.Now().Unix() + } + data[r][key] = val + mutex.Unlock() +} + +// Get returns a value stored for a given key in a given request. +func Get(r *http.Request, key interface{}) interface{} { + mutex.RLock() + if ctx := data[r]; ctx != nil { + value := ctx[key] + mutex.RUnlock() + return value + } + mutex.RUnlock() + return nil +} + +// GetOk returns stored value and presence state like multi-value return of map access. +func GetOk(r *http.Request, key interface{}) (interface{}, bool) { + mutex.RLock() + if _, ok := data[r]; ok { + value, ok := data[r][key] + mutex.RUnlock() + return value, ok + } + mutex.RUnlock() + return nil, false +} + +// GetAll returns all stored values for the request as a map. Nil is returned for invalid requests. +func GetAll(r *http.Request) map[interface{}]interface{} { + mutex.RLock() + if context, ok := data[r]; ok { + result := make(map[interface{}]interface{}, len(context)) + for k, v := range context { + result[k] = v + } + mutex.RUnlock() + return result + } + mutex.RUnlock() + return nil +} + +// GetAllOk returns all stored values for the request as a map and a boolean value that indicates if +// the request was registered. +func GetAllOk(r *http.Request) (map[interface{}]interface{}, bool) { + mutex.RLock() + context, ok := data[r] + result := make(map[interface{}]interface{}, len(context)) + for k, v := range context { + result[k] = v + } + mutex.RUnlock() + return result, ok +} + +// Delete removes a value stored for a given key in a given request. +func Delete(r *http.Request, key interface{}) { + mutex.Lock() + if data[r] != nil { + delete(data[r], key) + } + mutex.Unlock() +} + +// Clear removes all values stored for a given request. +// +// This is usually called by a handler wrapper to clean up request +// variables at the end of a request lifetime. See ClearHandler(). +func Clear(r *http.Request) { + mutex.Lock() + clear(r) + mutex.Unlock() +} + +// clear is Clear without the lock. +func clear(r *http.Request) { + delete(data, r) + delete(datat, r) +} + +// Purge removes request data stored for longer than maxAge, in seconds. +// It returns the amount of requests removed. +// +// If maxAge <= 0, all request data is removed. +// +// This is only used for sanity check: in case context cleaning was not +// properly set some request data can be kept forever, consuming an increasing +// amount of memory. In case this is detected, Purge() must be called +// periodically until the problem is fixed. +func Purge(maxAge int) int { + mutex.Lock() + count := 0 + if maxAge <= 0 { + count = len(data) + data = make(map[*http.Request]map[interface{}]interface{}) + datat = make(map[*http.Request]int64) + } else { + min := time.Now().Unix() - int64(maxAge) + for r := range data { + if datat[r] < min { + clear(r) + count++ + } + } + } + mutex.Unlock() + return count +} + +// ClearHandler wraps an http.Handler and clears request values at the end +// of a request lifetime. +func ClearHandler(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer Clear(r) + h.ServeHTTP(w, r) + }) +} diff --git a/vendor/github.com/gorilla/context/doc.go b/vendor/github.com/gorilla/context/doc.go new file mode 100644 index 0000000..448d1bf --- /dev/null +++ b/vendor/github.com/gorilla/context/doc.go @@ -0,0 +1,88 @@ +// Copyright 2012 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +/* +Package context stores values shared during a request lifetime. + +Note: gorilla/context, having been born well before `context.Context` existed, +does not play well > with the shallow copying of the request that +[`http.Request.WithContext`](https://golang.org/pkg/net/http/#Request.WithContext) +(added to net/http Go 1.7 onwards) performs. You should either use *just* +gorilla/context, or moving forward, the new `http.Request.Context()`. + +For example, a router can set variables extracted from the URL and later +application handlers can access those values, or it can be used to store +sessions values to be saved at the end of a request. There are several +others common uses. + +The idea was posted by Brad Fitzpatrick to the go-nuts mailing list: + + http://groups.google.com/group/golang-nuts/msg/e2d679d303aa5d53 + +Here's the basic usage: first define the keys that you will need. The key +type is interface{} so a key can be of any type that supports equality. +Here we define a key using a custom int type to avoid name collisions: + + package foo + + import ( + "github.com/gorilla/context" + ) + + type key int + + const MyKey key = 0 + +Then set a variable. Variables are bound to an http.Request object, so you +need a request instance to set a value: + + context.Set(r, MyKey, "bar") + +The application can later access the variable using the same key you provided: + + func MyHandler(w http.ResponseWriter, r *http.Request) { + // val is "bar". + val := context.Get(r, foo.MyKey) + + // returns ("bar", true) + val, ok := context.GetOk(r, foo.MyKey) + // ... + } + +And that's all about the basic usage. We discuss some other ideas below. + +Any type can be stored in the context. To enforce a given type, make the key +private and wrap Get() and Set() to accept and return values of a specific +type: + + type key int + + const mykey key = 0 + + // GetMyKey returns a value for this package from the request values. + func GetMyKey(r *http.Request) SomeType { + if rv := context.Get(r, mykey); rv != nil { + return rv.(SomeType) + } + return nil + } + + // SetMyKey sets a value for this package in the request values. + func SetMyKey(r *http.Request, val SomeType) { + context.Set(r, mykey, val) + } + +Variables must be cleared at the end of a request, to remove all values +that were stored. This can be done in an http.Handler, after a request was +served. Just call Clear() passing the request: + + context.Clear(r) + +...or use ClearHandler(), which conveniently wraps an http.Handler to clear +variables at the end of a request lifetime. + +The Routers from the packages gorilla/mux and gorilla/pat call Clear() +so if you are using either of them you don't need to clear the context manually. +*/ +package context diff --git a/vendor/github.com/gorilla/securecookie/AUTHORS b/vendor/github.com/gorilla/securecookie/AUTHORS new file mode 100644 index 0000000..a4d447d --- /dev/null +++ b/vendor/github.com/gorilla/securecookie/AUTHORS @@ -0,0 +1,19 @@ +# This is the official list of gorilla/securecookie authors for copyright purposes. +# Please keep the list sorted. + +0x434D53 +Abdülhamit Yilmaz +Annonomus-Penguin +Craig Peterson +Cyril David +Dmitry Chestnykh +Dominik Honnef +Google LLC (https://opensource.google.com/) +John Downey +Kamil Kisiel +Keunwoo Lee +Mahmud Ridwan +Matt Silverlock +rodrigo moraes +s7v7nislands +Wesley Bitter diff --git a/vendor/github.com/gorilla/securecookie/LICENSE b/vendor/github.com/gorilla/securecookie/LICENSE new file mode 100644 index 0000000..6903df6 --- /dev/null +++ b/vendor/github.com/gorilla/securecookie/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2012-2018 The Gorilla Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/gorilla/securecookie/README.md b/vendor/github.com/gorilla/securecookie/README.md new file mode 100644 index 0000000..a914d4a --- /dev/null +++ b/vendor/github.com/gorilla/securecookie/README.md @@ -0,0 +1,82 @@ +# securecookie + +[![GoDoc](https://godoc.org/github.com/gorilla/securecookie?status.svg)](https://godoc.org/github.com/gorilla/securecookie) [![Build Status](https://travis-ci.org/gorilla/securecookie.png?branch=master)](https://travis-ci.org/gorilla/securecookie) +[![Sourcegraph](https://sourcegraph.com/github.com/gorilla/securecookie/-/badge.svg)](https://sourcegraph.com/github.com/gorilla/securecookie?badge) + +securecookie encodes and decodes authenticated and optionally encrypted +cookie values. + +Secure cookies can't be forged, because their values are validated using HMAC. +When encrypted, the content is also inaccessible to malicious eyes. It is still +recommended that sensitive data not be stored in cookies, and that HTTPS be used +to prevent cookie [replay attacks](https://en.wikipedia.org/wiki/Replay_attack). + +## Examples + +To use it, first create a new SecureCookie instance: + +```go +// Hash keys should be at least 32 bytes long +var hashKey = []byte("very-secret") +// Block keys should be 16 bytes (AES-128) or 32 bytes (AES-256) long. +// Shorter keys may weaken the encryption used. +var blockKey = []byte("a-lot-secret") +var s = securecookie.New(hashKey, blockKey) +``` + +The hashKey is required, used to authenticate the cookie value using HMAC. +It is recommended to use a key with 32 or 64 bytes. + +The blockKey is optional, used to encrypt the cookie value -- set it to nil +to not use encryption. If set, the length must correspond to the block size +of the encryption algorithm. For AES, used by default, valid lengths are +16, 24, or 32 bytes to select AES-128, AES-192, or AES-256. + +Strong keys can be created using the convenience function +`GenerateRandomKey()`. Note that keys created using `GenerateRandomKey()` are not +automatically persisted. New keys will be created when the application is +restarted, and previously issued cookies will not be able to be decoded. + +Once a SecureCookie instance is set, use it to encode a cookie value: + +```go +func SetCookieHandler(w http.ResponseWriter, r *http.Request) { + value := map[string]string{ + "foo": "bar", + } + if encoded, err := s.Encode("cookie-name", value); err == nil { + cookie := &http.Cookie{ + Name: "cookie-name", + Value: encoded, + Path: "/", + Secure: true, + HttpOnly: true, + } + http.SetCookie(w, cookie) + } +} +``` + +Later, use the same SecureCookie instance to decode and validate a cookie +value: + +```go +func ReadCookieHandler(w http.ResponseWriter, r *http.Request) { + if cookie, err := r.Cookie("cookie-name"); err == nil { + value := make(map[string]string) + if err = s2.Decode("cookie-name", cookie.Value, &value); err == nil { + fmt.Fprintf(w, "The value of foo is %q", value["foo"]) + } + } +} +``` + +We stored a map[string]string, but secure cookies can hold any value that +can be encoded using `encoding/gob`. To store custom types, they must be +registered first using gob.Register(). For basic types this is not needed; +it works out of the box. An optional JSON encoder that uses `encoding/json` is +available for types compatible with JSON. + +## License + +BSD licensed. See the LICENSE file for details. diff --git a/vendor/github.com/gorilla/securecookie/doc.go b/vendor/github.com/gorilla/securecookie/doc.go new file mode 100644 index 0000000..ae89408 --- /dev/null +++ b/vendor/github.com/gorilla/securecookie/doc.go @@ -0,0 +1,61 @@ +// Copyright 2012 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +/* +Package securecookie encodes and decodes authenticated and optionally +encrypted cookie values. + +Secure cookies can't be forged, because their values are validated using HMAC. +When encrypted, the content is also inaccessible to malicious eyes. + +To use it, first create a new SecureCookie instance: + + var hashKey = []byte("very-secret") + var blockKey = []byte("a-lot-secret") + var s = securecookie.New(hashKey, blockKey) + +The hashKey is required, used to authenticate the cookie value using HMAC. +It is recommended to use a key with 32 or 64 bytes. + +The blockKey is optional, used to encrypt the cookie value -- set it to nil +to not use encryption. If set, the length must correspond to the block size +of the encryption algorithm. For AES, used by default, valid lengths are +16, 24, or 32 bytes to select AES-128, AES-192, or AES-256. + +Strong keys can be created using the convenience function GenerateRandomKey(). + +Once a SecureCookie instance is set, use it to encode a cookie value: + + func SetCookieHandler(w http.ResponseWriter, r *http.Request) { + value := map[string]string{ + "foo": "bar", + } + if encoded, err := s.Encode("cookie-name", value); err == nil { + cookie := &http.Cookie{ + Name: "cookie-name", + Value: encoded, + Path: "/", + } + http.SetCookie(w, cookie) + } + } + +Later, use the same SecureCookie instance to decode and validate a cookie +value: + + func ReadCookieHandler(w http.ResponseWriter, r *http.Request) { + if cookie, err := r.Cookie("cookie-name"); err == nil { + value := make(map[string]string) + if err = s2.Decode("cookie-name", cookie.Value, &value); err == nil { + fmt.Fprintf(w, "The value of foo is %q", value["foo"]) + } + } + } + +We stored a map[string]string, but secure cookies can hold any value that +can be encoded using encoding/gob. To store custom types, they must be +registered first using gob.Register(). For basic types this is not needed; +it works out of the box. +*/ +package securecookie diff --git a/vendor/github.com/gorilla/securecookie/fuzz.go b/vendor/github.com/gorilla/securecookie/fuzz.go new file mode 100644 index 0000000..e4d0534 --- /dev/null +++ b/vendor/github.com/gorilla/securecookie/fuzz.go @@ -0,0 +1,25 @@ +// +build gofuzz + +package securecookie + +var hashKey = []byte("very-secret12345") +var blockKey = []byte("a-lot-secret1234") +var s = New(hashKey, blockKey) + +type Cookie struct { + B bool + I int + S string +} + +func Fuzz(data []byte) int { + datas := string(data) + var c Cookie + if err := s.Decode("fuzz", datas, &c); err != nil { + return 0 + } + if _, err := s.Encode("fuzz", c); err != nil { + panic(err) + } + return 1 +} diff --git a/vendor/github.com/gorilla/securecookie/go.mod b/vendor/github.com/gorilla/securecookie/go.mod new file mode 100644 index 0000000..db69e44 --- /dev/null +++ b/vendor/github.com/gorilla/securecookie/go.mod @@ -0,0 +1 @@ +module github.com/gorilla/securecookie diff --git a/vendor/github.com/gorilla/securecookie/securecookie.go b/vendor/github.com/gorilla/securecookie/securecookie.go new file mode 100644 index 0000000..a34f851 --- /dev/null +++ b/vendor/github.com/gorilla/securecookie/securecookie.go @@ -0,0 +1,650 @@ +// Copyright 2012 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package securecookie + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "crypto/subtle" + "encoding/base64" + "encoding/gob" + "encoding/json" + "fmt" + "hash" + "io" + "strconv" + "strings" + "time" +) + +// Error is the interface of all errors returned by functions in this library. +type Error interface { + error + + // IsUsage returns true for errors indicating the client code probably + // uses this library incorrectly. For example, the client may have + // failed to provide a valid hash key, or may have failed to configure + // the Serializer adequately for encoding value. + IsUsage() bool + + // IsDecode returns true for errors indicating that a cookie could not + // be decoded and validated. Since cookies are usually untrusted + // user-provided input, errors of this type should be expected. + // Usually, the proper action is simply to reject the request. + IsDecode() bool + + // IsInternal returns true for unexpected errors occurring in the + // securecookie implementation. + IsInternal() bool + + // Cause, if it returns a non-nil value, indicates that this error was + // propagated from some underlying library. If this method returns nil, + // this error was raised directly by this library. + // + // Cause is provided principally for debugging/logging purposes; it is + // rare that application logic should perform meaningfully different + // logic based on Cause. See, for example, the caveats described on + // (MultiError).Cause(). + Cause() error +} + +// errorType is a bitmask giving the error type(s) of an cookieError value. +type errorType int + +const ( + usageError = errorType(1 << iota) + decodeError + internalError +) + +type cookieError struct { + typ errorType + msg string + cause error +} + +func (e cookieError) IsUsage() bool { return (e.typ & usageError) != 0 } +func (e cookieError) IsDecode() bool { return (e.typ & decodeError) != 0 } +func (e cookieError) IsInternal() bool { return (e.typ & internalError) != 0 } + +func (e cookieError) Cause() error { return e.cause } + +func (e cookieError) Error() string { + parts := []string{"securecookie: "} + if e.msg == "" { + parts = append(parts, "error") + } else { + parts = append(parts, e.msg) + } + if c := e.Cause(); c != nil { + parts = append(parts, " - caused by: ", c.Error()) + } + return strings.Join(parts, "") +} + +var ( + errGeneratingIV = cookieError{typ: internalError, msg: "failed to generate random iv"} + + errNoCodecs = cookieError{typ: usageError, msg: "no codecs provided"} + errHashKeyNotSet = cookieError{typ: usageError, msg: "hash key is not set"} + errBlockKeyNotSet = cookieError{typ: usageError, msg: "block key is not set"} + errEncodedValueTooLong = cookieError{typ: usageError, msg: "the value is too long"} + + errValueToDecodeTooLong = cookieError{typ: decodeError, msg: "the value is too long"} + errTimestampInvalid = cookieError{typ: decodeError, msg: "invalid timestamp"} + errTimestampTooNew = cookieError{typ: decodeError, msg: "timestamp is too new"} + errTimestampExpired = cookieError{typ: decodeError, msg: "expired timestamp"} + errDecryptionFailed = cookieError{typ: decodeError, msg: "the value could not be decrypted"} + errValueNotByte = cookieError{typ: decodeError, msg: "value not a []byte."} + errValueNotBytePtr = cookieError{typ: decodeError, msg: "value not a pointer to []byte."} + + // ErrMacInvalid indicates that cookie decoding failed because the HMAC + // could not be extracted and verified. Direct use of this error + // variable is deprecated; it is public only for legacy compatibility, + // and may be privatized in the future, as it is rarely useful to + // distinguish between this error and other Error implementations. + ErrMacInvalid = cookieError{typ: decodeError, msg: "the value is not valid"} +) + +// Codec defines an interface to encode and decode cookie values. +type Codec interface { + Encode(name string, value interface{}) (string, error) + Decode(name, value string, dst interface{}) error +} + +// New returns a new SecureCookie. +// +// hashKey is required, used to authenticate values using HMAC. Create it using +// GenerateRandomKey(). It is recommended to use a key with 32 or 64 bytes. +// +// blockKey is optional, used to encrypt values. Create it using +// GenerateRandomKey(). The key length must correspond to the block size +// of the encryption algorithm. For AES, used by default, valid lengths are +// 16, 24, or 32 bytes to select AES-128, AES-192, or AES-256. +// The default encoder used for cookie serialization is encoding/gob. +// +// Note that keys created using GenerateRandomKey() are not automatically +// persisted. New keys will be created when the application is restarted, and +// previously issued cookies will not be able to be decoded. +func New(hashKey, blockKey []byte) *SecureCookie { + s := &SecureCookie{ + hashKey: hashKey, + blockKey: blockKey, + hashFunc: sha256.New, + maxAge: 86400 * 30, + maxLength: 4096, + sz: GobEncoder{}, + } + if len(hashKey) == 0 { + s.err = errHashKeyNotSet + } + if blockKey != nil { + s.BlockFunc(aes.NewCipher) + } + return s +} + +// SecureCookie encodes and decodes authenticated and optionally encrypted +// cookie values. +type SecureCookie struct { + hashKey []byte + hashFunc func() hash.Hash + blockKey []byte + block cipher.Block + maxLength int + maxAge int64 + minAge int64 + err error + sz Serializer + // For testing purposes, the function that returns the current timestamp. + // If not set, it will use time.Now().UTC().Unix(). + timeFunc func() int64 +} + +// Serializer provides an interface for providing custom serializers for cookie +// values. +type Serializer interface { + Serialize(src interface{}) ([]byte, error) + Deserialize(src []byte, dst interface{}) error +} + +// GobEncoder encodes cookie values using encoding/gob. This is the simplest +// encoder and can handle complex types via gob.Register. +type GobEncoder struct{} + +// JSONEncoder encodes cookie values using encoding/json. Users who wish to +// encode complex types need to satisfy the json.Marshaller and +// json.Unmarshaller interfaces. +type JSONEncoder struct{} + +// NopEncoder does not encode cookie values, and instead simply accepts a []byte +// (as an interface{}) and returns a []byte. This is particularly useful when +// you encoding an object upstream and do not wish to re-encode it. +type NopEncoder struct{} + +// MaxLength restricts the maximum length, in bytes, for the cookie value. +// +// Default is 4096, which is the maximum value accepted by Internet Explorer. +func (s *SecureCookie) MaxLength(value int) *SecureCookie { + s.maxLength = value + return s +} + +// MaxAge restricts the maximum age, in seconds, for the cookie value. +// +// Default is 86400 * 30. Set it to 0 for no restriction. +func (s *SecureCookie) MaxAge(value int) *SecureCookie { + s.maxAge = int64(value) + return s +} + +// MinAge restricts the minimum age, in seconds, for the cookie value. +// +// Default is 0 (no restriction). +func (s *SecureCookie) MinAge(value int) *SecureCookie { + s.minAge = int64(value) + return s +} + +// HashFunc sets the hash function used to create HMAC. +// +// Default is crypto/sha256.New. +func (s *SecureCookie) HashFunc(f func() hash.Hash) *SecureCookie { + s.hashFunc = f + return s +} + +// BlockFunc sets the encryption function used to create a cipher.Block. +// +// Default is crypto/aes.New. +func (s *SecureCookie) BlockFunc(f func([]byte) (cipher.Block, error)) *SecureCookie { + if s.blockKey == nil { + s.err = errBlockKeyNotSet + } else if block, err := f(s.blockKey); err == nil { + s.block = block + } else { + s.err = cookieError{cause: err, typ: usageError} + } + return s +} + +// Encoding sets the encoding/serialization method for cookies. +// +// Default is encoding/gob. To encode special structures using encoding/gob, +// they must be registered first using gob.Register(). +func (s *SecureCookie) SetSerializer(sz Serializer) *SecureCookie { + s.sz = sz + + return s +} + +// Encode encodes a cookie value. +// +// It serializes, optionally encrypts, signs with a message authentication code, +// and finally encodes the value. +// +// The name argument is the cookie name. It is stored with the encoded value. +// The value argument is the value to be encoded. It can be any value that can +// be encoded using the currently selected serializer; see SetSerializer(). +// +// It is the client's responsibility to ensure that value, when encoded using +// the current serialization/encryption settings on s and then base64-encoded, +// is shorter than the maximum permissible length. +func (s *SecureCookie) Encode(name string, value interface{}) (string, error) { + if s.err != nil { + return "", s.err + } + if s.hashKey == nil { + s.err = errHashKeyNotSet + return "", s.err + } + var err error + var b []byte + // 1. Serialize. + if b, err = s.sz.Serialize(value); err != nil { + return "", cookieError{cause: err, typ: usageError} + } + // 2. Encrypt (optional). + if s.block != nil { + if b, err = encrypt(s.block, b); err != nil { + return "", cookieError{cause: err, typ: usageError} + } + } + b = encode(b) + // 3. Create MAC for "name|date|value". Extra pipe to be used later. + b = []byte(fmt.Sprintf("%s|%d|%s|", name, s.timestamp(), b)) + mac := createMac(hmac.New(s.hashFunc, s.hashKey), b[:len(b)-1]) + // Append mac, remove name. + b = append(b, mac...)[len(name)+1:] + // 4. Encode to base64. + b = encode(b) + // 5. Check length. + if s.maxLength != 0 && len(b) > s.maxLength { + return "", errEncodedValueTooLong + } + // Done. + return string(b), nil +} + +// Decode decodes a cookie value. +// +// It decodes, verifies a message authentication code, optionally decrypts and +// finally deserializes the value. +// +// The name argument is the cookie name. It must be the same name used when +// it was stored. The value argument is the encoded cookie value. The dst +// argument is where the cookie will be decoded. It must be a pointer. +func (s *SecureCookie) Decode(name, value string, dst interface{}) error { + if s.err != nil { + return s.err + } + if s.hashKey == nil { + s.err = errHashKeyNotSet + return s.err + } + // 1. Check length. + if s.maxLength != 0 && len(value) > s.maxLength { + return errValueToDecodeTooLong + } + // 2. Decode from base64. + b, err := decode([]byte(value)) + if err != nil { + return err + } + // 3. Verify MAC. Value is "date|value|mac". + parts := bytes.SplitN(b, []byte("|"), 3) + if len(parts) != 3 { + return ErrMacInvalid + } + h := hmac.New(s.hashFunc, s.hashKey) + b = append([]byte(name+"|"), b[:len(b)-len(parts[2])-1]...) + if err = verifyMac(h, b, parts[2]); err != nil { + return err + } + // 4. Verify date ranges. + var t1 int64 + if t1, err = strconv.ParseInt(string(parts[0]), 10, 64); err != nil { + return errTimestampInvalid + } + t2 := s.timestamp() + if s.minAge != 0 && t1 > t2-s.minAge { + return errTimestampTooNew + } + if s.maxAge != 0 && t1 < t2-s.maxAge { + return errTimestampExpired + } + // 5. Decrypt (optional). + b, err = decode(parts[1]) + if err != nil { + return err + } + if s.block != nil { + if b, err = decrypt(s.block, b); err != nil { + return err + } + } + // 6. Deserialize. + if err = s.sz.Deserialize(b, dst); err != nil { + return cookieError{cause: err, typ: decodeError} + } + // Done. + return nil +} + +// timestamp returns the current timestamp, in seconds. +// +// For testing purposes, the function that generates the timestamp can be +// overridden. If not set, it will return time.Now().UTC().Unix(). +func (s *SecureCookie) timestamp() int64 { + if s.timeFunc == nil { + return time.Now().UTC().Unix() + } + return s.timeFunc() +} + +// Authentication ------------------------------------------------------------- + +// createMac creates a message authentication code (MAC). +func createMac(h hash.Hash, value []byte) []byte { + h.Write(value) + return h.Sum(nil) +} + +// verifyMac verifies that a message authentication code (MAC) is valid. +func verifyMac(h hash.Hash, value []byte, mac []byte) error { + mac2 := createMac(h, value) + // Check that both MACs are of equal length, as subtle.ConstantTimeCompare + // does not do this prior to Go 1.4. + if len(mac) == len(mac2) && subtle.ConstantTimeCompare(mac, mac2) == 1 { + return nil + } + return ErrMacInvalid +} + +// Encryption ----------------------------------------------------------------- + +// encrypt encrypts a value using the given block in counter mode. +// +// A random initialization vector (http://goo.gl/zF67k) with the length of the +// block size is prepended to the resulting ciphertext. +func encrypt(block cipher.Block, value []byte) ([]byte, error) { + iv := GenerateRandomKey(block.BlockSize()) + if iv == nil { + return nil, errGeneratingIV + } + // Encrypt it. + stream := cipher.NewCTR(block, iv) + stream.XORKeyStream(value, value) + // Return iv + ciphertext. + return append(iv, value...), nil +} + +// decrypt decrypts a value using the given block in counter mode. +// +// The value to be decrypted must be prepended by a initialization vector +// (http://goo.gl/zF67k) with the length of the block size. +func decrypt(block cipher.Block, value []byte) ([]byte, error) { + size := block.BlockSize() + if len(value) > size { + // Extract iv. + iv := value[:size] + // Extract ciphertext. + value = value[size:] + // Decrypt it. + stream := cipher.NewCTR(block, iv) + stream.XORKeyStream(value, value) + return value, nil + } + return nil, errDecryptionFailed +} + +// Serialization -------------------------------------------------------------- + +// Serialize encodes a value using gob. +func (e GobEncoder) Serialize(src interface{}) ([]byte, error) { + buf := new(bytes.Buffer) + enc := gob.NewEncoder(buf) + if err := enc.Encode(src); err != nil { + return nil, cookieError{cause: err, typ: usageError} + } + return buf.Bytes(), nil +} + +// Deserialize decodes a value using gob. +func (e GobEncoder) Deserialize(src []byte, dst interface{}) error { + dec := gob.NewDecoder(bytes.NewBuffer(src)) + if err := dec.Decode(dst); err != nil { + return cookieError{cause: err, typ: decodeError} + } + return nil +} + +// Serialize encodes a value using encoding/json. +func (e JSONEncoder) Serialize(src interface{}) ([]byte, error) { + buf := new(bytes.Buffer) + enc := json.NewEncoder(buf) + if err := enc.Encode(src); err != nil { + return nil, cookieError{cause: err, typ: usageError} + } + return buf.Bytes(), nil +} + +// Deserialize decodes a value using encoding/json. +func (e JSONEncoder) Deserialize(src []byte, dst interface{}) error { + dec := json.NewDecoder(bytes.NewReader(src)) + if err := dec.Decode(dst); err != nil { + return cookieError{cause: err, typ: decodeError} + } + return nil +} + +// Serialize passes a []byte through as-is. +func (e NopEncoder) Serialize(src interface{}) ([]byte, error) { + if b, ok := src.([]byte); ok { + return b, nil + } + + return nil, errValueNotByte +} + +// Deserialize passes a []byte through as-is. +func (e NopEncoder) Deserialize(src []byte, dst interface{}) error { + if dat, ok := dst.(*[]byte); ok { + *dat = src + return nil + } + return errValueNotBytePtr +} + +// Encoding ------------------------------------------------------------------- + +// encode encodes a value using base64. +func encode(value []byte) []byte { + encoded := make([]byte, base64.URLEncoding.EncodedLen(len(value))) + base64.URLEncoding.Encode(encoded, value) + return encoded +} + +// decode decodes a cookie using base64. +func decode(value []byte) ([]byte, error) { + decoded := make([]byte, base64.URLEncoding.DecodedLen(len(value))) + b, err := base64.URLEncoding.Decode(decoded, value) + if err != nil { + return nil, cookieError{cause: err, typ: decodeError, msg: "base64 decode failed"} + } + return decoded[:b], nil +} + +// Helpers -------------------------------------------------------------------- + +// GenerateRandomKey creates a random key with the given length in bytes. +// On failure, returns nil. +// +// Note that keys created using `GenerateRandomKey()` are not automatically +// persisted. New keys will be created when the application is restarted, and +// previously issued cookies will not be able to be decoded. +// +// Callers should explicitly check for the possibility of a nil return, treat +// it as a failure of the system random number generator, and not continue. +func GenerateRandomKey(length int) []byte { + k := make([]byte, length) + if _, err := io.ReadFull(rand.Reader, k); err != nil { + return nil + } + return k +} + +// CodecsFromPairs returns a slice of SecureCookie instances. +// +// It is a convenience function to create a list of codecs for key rotation. Note +// that the generated Codecs will have the default options applied: callers +// should iterate over each Codec and type-assert the underlying *SecureCookie to +// change these. +// +// Example: +// +// codecs := securecookie.CodecsFromPairs( +// []byte("new-hash-key"), +// []byte("new-block-key"), +// []byte("old-hash-key"), +// []byte("old-block-key"), +// ) +// +// // Modify each instance. +// for _, s := range codecs { +// if cookie, ok := s.(*securecookie.SecureCookie); ok { +// cookie.MaxAge(86400 * 7) +// cookie.SetSerializer(securecookie.JSONEncoder{}) +// cookie.HashFunc(sha512.New512_256) +// } +// } +// +func CodecsFromPairs(keyPairs ...[]byte) []Codec { + codecs := make([]Codec, len(keyPairs)/2+len(keyPairs)%2) + for i := 0; i < len(keyPairs); i += 2 { + var blockKey []byte + if i+1 < len(keyPairs) { + blockKey = keyPairs[i+1] + } + codecs[i/2] = New(keyPairs[i], blockKey) + } + return codecs +} + +// EncodeMulti encodes a cookie value using a group of codecs. +// +// The codecs are tried in order. Multiple codecs are accepted to allow +// key rotation. +// +// On error, may return a MultiError. +func EncodeMulti(name string, value interface{}, codecs ...Codec) (string, error) { + if len(codecs) == 0 { + return "", errNoCodecs + } + + var errors MultiError + for _, codec := range codecs { + encoded, err := codec.Encode(name, value) + if err == nil { + return encoded, nil + } + errors = append(errors, err) + } + return "", errors +} + +// DecodeMulti decodes a cookie value using a group of codecs. +// +// The codecs are tried in order. Multiple codecs are accepted to allow +// key rotation. +// +// On error, may return a MultiError. +func DecodeMulti(name string, value string, dst interface{}, codecs ...Codec) error { + if len(codecs) == 0 { + return errNoCodecs + } + + var errors MultiError + for _, codec := range codecs { + err := codec.Decode(name, value, dst) + if err == nil { + return nil + } + errors = append(errors, err) + } + return errors +} + +// MultiError groups multiple errors. +type MultiError []error + +func (m MultiError) IsUsage() bool { return m.any(func(e Error) bool { return e.IsUsage() }) } +func (m MultiError) IsDecode() bool { return m.any(func(e Error) bool { return e.IsDecode() }) } +func (m MultiError) IsInternal() bool { return m.any(func(e Error) bool { return e.IsInternal() }) } + +// Cause returns nil for MultiError; there is no unique underlying cause in the +// general case. +// +// Note: we could conceivably return a non-nil Cause only when there is exactly +// one child error with a Cause. However, it would be brittle for client code +// to rely on the arity of causes inside a MultiError, so we have opted not to +// provide this functionality. Clients which really wish to access the Causes +// of the underlying errors are free to iterate through the errors themselves. +func (m MultiError) Cause() error { return nil } + +func (m MultiError) Error() string { + s, n := "", 0 + for _, e := range m { + if e != nil { + if n == 0 { + s = e.Error() + } + n++ + } + } + switch n { + case 0: + return "(0 errors)" + case 1: + return s + case 2: + return s + " (and 1 other error)" + } + return fmt.Sprintf("%s (and %d other errors)", s, n-1) +} + +// any returns true if any element of m is an Error for which pred returns true. +func (m MultiError) any(pred func(Error) bool) bool { + for _, e := range m { + if ourErr, ok := e.(Error); ok && pred(ourErr) { + return true + } + } + return false +} diff --git a/vendor/github.com/gorilla/sessions/AUTHORS b/vendor/github.com/gorilla/sessions/AUTHORS new file mode 100644 index 0000000..1e3e7ac --- /dev/null +++ b/vendor/github.com/gorilla/sessions/AUTHORS @@ -0,0 +1,43 @@ +# This is the official list of gorilla/sessions authors for copyright purposes. +# +# Please keep the list sorted. + +Ahmadreza Zibaei +Anton Lindström +Brian Jones +Collin Stedman +Deniz Eren +Dmitry Chestnykh +Dustin Oprea +Egon Elbre +enumappstore +Geofrey Ernest +Google LLC (https://opensource.google.com/) +Jerry Saravia +Jonathan Gillham +Justin Clift +Justin Hellings +Kamil Kisiel +Keiji Yoshida +kliron +Kshitij Saraogi +Lauris BH +Lukas Rist +Mark Dain +Matt Ho +Matt Silverlock +Mattias Wadman +Michael Schuett +Michael Stapelberg +Mirco Zeiss +moraes +nvcnvn +pappz +Pontus Leitzler +QuaSoft +rcadena +rodrigo moraes +Shawn Smith +Taylor Hurt +Tortuoise +Vitor De Mario diff --git a/vendor/github.com/gorilla/sessions/LICENSE b/vendor/github.com/gorilla/sessions/LICENSE new file mode 100644 index 0000000..6903df6 --- /dev/null +++ b/vendor/github.com/gorilla/sessions/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2012-2018 The Gorilla Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/gorilla/sessions/README.md b/vendor/github.com/gorilla/sessions/README.md new file mode 100644 index 0000000..98c993d --- /dev/null +++ b/vendor/github.com/gorilla/sessions/README.md @@ -0,0 +1,83 @@ +# sessions + +[![GoDoc](https://godoc.org/github.com/gorilla/sessions?status.svg)](https://godoc.org/github.com/gorilla/sessions) [![Build Status](https://travis-ci.org/gorilla/sessions.svg?branch=master)](https://travis-ci.org/gorilla/sessions) +[![Sourcegraph](https://sourcegraph.com/github.com/gorilla/sessions/-/badge.svg)](https://sourcegraph.com/github.com/gorilla/sessions?badge) + +gorilla/sessions provides cookie and filesystem sessions and infrastructure for +custom session backends. + +The key features are: + +- Simple API: use it as an easy way to set signed (and optionally + encrypted) cookies. +- Built-in backends to store sessions in cookies or the filesystem. +- Flash messages: session values that last until read. +- Convenient way to switch session persistency (aka "remember me") and set + other attributes. +- Mechanism to rotate authentication and encryption keys. +- Multiple sessions per request, even using different backends. +- Interfaces and infrastructure for custom session backends: sessions from + different stores can be retrieved and batch-saved using a common API. + +Let's start with an example that shows the sessions API in a nutshell: + +```go + import ( + "net/http" + "github.com/gorilla/sessions" + ) + + // Note: Don't store your key in your source code. Pass it via an + // environmental variable, or flag (or both), and don't accidentally commit it + // alongside your code. Ensure your key is sufficiently random - i.e. use Go's + // crypto/rand or securecookie.GenerateRandomKey(32) and persist the result. + var store = sessions.NewCookieStore([]byte(os.Getenv("SESSION_KEY"))) + + func MyHandler(w http.ResponseWriter, r *http.Request) { + // Get a session. We're ignoring the error resulted from decoding an + // existing session: Get() always returns a session, even if empty. + session, _ := store.Get(r, "session-name") + // Set some session values. + session.Values["foo"] = "bar" + session.Values[42] = 43 + // Save it before we write to the response/return from the handler. + session.Save(r, w) + } +``` + +First we initialize a session store calling `NewCookieStore()` and passing a +secret key used to authenticate the session. Inside the handler, we call +`store.Get()` to retrieve an existing session or create a new one. Then we set +some session values in session.Values, which is a `map[interface{}]interface{}`. +And finally we call `session.Save()` to save the session in the response. + +More examples are available [on the Gorilla +website](https://www.gorillatoolkit.org/pkg/sessions). + +## Store Implementations + +Other implementations of the `sessions.Store` interface: + +- [github.com/starJammer/gorilla-sessions-arangodb](https://github.com/starJammer/gorilla-sessions-arangodb) - ArangoDB +- [github.com/yosssi/boltstore](https://github.com/yosssi/boltstore) - Bolt +- [github.com/srinathgs/couchbasestore](https://github.com/srinathgs/couchbasestore) - Couchbase +- [github.com/denizeren/dynamostore](https://github.com/denizeren/dynamostore) - Dynamodb on AWS +- [github.com/savaki/dynastore](https://github.com/savaki/dynastore) - DynamoDB on AWS (Official AWS library) +- [github.com/bradleypeabody/gorilla-sessions-memcache](https://github.com/bradleypeabody/gorilla-sessions-memcache) - Memcache +- [github.com/dsoprea/go-appengine-sessioncascade](https://github.com/dsoprea/go-appengine-sessioncascade) - Memcache/Datastore/Context in AppEngine +- [github.com/kidstuff/mongostore](https://github.com/kidstuff/mongostore) - MongoDB +- [github.com/srinathgs/mysqlstore](https://github.com/srinathgs/mysqlstore) - MySQL +- [github.com/EnumApps/clustersqlstore](https://github.com/EnumApps/clustersqlstore) - MySQL Cluster +- [github.com/antonlindstrom/pgstore](https://github.com/antonlindstrom/pgstore) - PostgreSQL +- [github.com/boj/redistore](https://github.com/boj/redistore) - Redis +- [github.com/boj/rethinkstore](https://github.com/boj/rethinkstore) - RethinkDB +- [github.com/boj/riakstore](https://github.com/boj/riakstore) - Riak +- [github.com/michaeljs1990/sqlitestore](https://github.com/michaeljs1990/sqlitestore) - SQLite +- [github.com/wader/gormstore](https://github.com/wader/gormstore) - GORM (MySQL, PostgreSQL, SQLite) +- [github.com/gernest/qlstore](https://github.com/gernest/qlstore) - ql +- [github.com/quasoft/memstore](https://github.com/quasoft/memstore) - In-memory implementation for use in unit tests +- [github.com/lafriks/xormstore](https://github.com/lafriks/xormstore) - XORM (MySQL, PostgreSQL, SQLite, Microsoft SQL Server, TiDB) + +## License + +BSD licensed. See the LICENSE file for details. diff --git a/vendor/github.com/gorilla/sessions/cookie.go b/vendor/github.com/gorilla/sessions/cookie.go new file mode 100644 index 0000000..1928b04 --- /dev/null +++ b/vendor/github.com/gorilla/sessions/cookie.go @@ -0,0 +1,19 @@ +// +build !go1.11 + +package sessions + +import "net/http" + +// newCookieFromOptions returns an http.Cookie with the options set. +func newCookieFromOptions(name, value string, options *Options) *http.Cookie { + return &http.Cookie{ + Name: name, + Value: value, + Path: options.Path, + Domain: options.Domain, + MaxAge: options.MaxAge, + Secure: options.Secure, + HttpOnly: options.HttpOnly, + } + +} diff --git a/vendor/github.com/gorilla/sessions/cookie_go111.go b/vendor/github.com/gorilla/sessions/cookie_go111.go new file mode 100644 index 0000000..173d1a3 --- /dev/null +++ b/vendor/github.com/gorilla/sessions/cookie_go111.go @@ -0,0 +1,20 @@ +// +build go1.11 + +package sessions + +import "net/http" + +// newCookieFromOptions returns an http.Cookie with the options set. +func newCookieFromOptions(name, value string, options *Options) *http.Cookie { + return &http.Cookie{ + Name: name, + Value: value, + Path: options.Path, + Domain: options.Domain, + MaxAge: options.MaxAge, + Secure: options.Secure, + HttpOnly: options.HttpOnly, + SameSite: options.SameSite, + } + +} diff --git a/vendor/github.com/gorilla/sessions/doc.go b/vendor/github.com/gorilla/sessions/doc.go new file mode 100644 index 0000000..64f858c --- /dev/null +++ b/vendor/github.com/gorilla/sessions/doc.go @@ -0,0 +1,194 @@ +// Copyright 2012 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +/* +Package sessions provides cookie and filesystem sessions and +infrastructure for custom session backends. + +The key features are: + + * Simple API: use it as an easy way to set signed (and optionally + encrypted) cookies. + * Built-in backends to store sessions in cookies or the filesystem. + * Flash messages: session values that last until read. + * Convenient way to switch session persistency (aka "remember me") and set + other attributes. + * Mechanism to rotate authentication and encryption keys. + * Multiple sessions per request, even using different backends. + * Interfaces and infrastructure for custom session backends: sessions from + different stores can be retrieved and batch-saved using a common API. + +Let's start with an example that shows the sessions API in a nutshell: + + import ( + "net/http" + "github.com/gorilla/sessions" + ) + + // Note: Don't store your key in your source code. Pass it via an + // environmental variable, or flag (or both), and don't accidentally commit it + // alongside your code. Ensure your key is sufficiently random - i.e. use Go's + // crypto/rand or securecookie.GenerateRandomKey(32) and persist the result. + var store = sessions.NewCookieStore(os.Getenv("SESSION_KEY")) + + func MyHandler(w http.ResponseWriter, r *http.Request) { + // Get a session. Get() always returns a session, even if empty. + session, err := store.Get(r, "session-name") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Set some session values. + session.Values["foo"] = "bar" + session.Values[42] = 43 + // Save it before we write to the response/return from the handler. + session.Save(r, w) + } + +First we initialize a session store calling NewCookieStore() and passing a +secret key used to authenticate the session. Inside the handler, we call +store.Get() to retrieve an existing session or a new one. Then we set some +session values in session.Values, which is a map[interface{}]interface{}. +And finally we call session.Save() to save the session in the response. + +Note that in production code, we should check for errors when calling +session.Save(r, w), and either display an error message or otherwise handle it. + +Save must be called before writing to the response, otherwise the session +cookie will not be sent to the client. + +That's all you need to know for the basic usage. Let's take a look at other +options, starting with flash messages. + +Flash messages are session values that last until read. The term appeared with +Ruby On Rails a few years back. When we request a flash message, it is removed +from the session. To add a flash, call session.AddFlash(), and to get all +flashes, call session.Flashes(). Here is an example: + + func MyHandler(w http.ResponseWriter, r *http.Request) { + // Get a session. + session, err := store.Get(r, "session-name") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Get the previous flashes, if any. + if flashes := session.Flashes(); len(flashes) > 0 { + // Use the flash values. + } else { + // Set a new flash. + session.AddFlash("Hello, flash messages world!") + } + session.Save(r, w) + } + +Flash messages are useful to set information to be read after a redirection, +like after form submissions. + +There may also be cases where you want to store a complex datatype within a +session, such as a struct. Sessions are serialised using the encoding/gob package, +so it is easy to register new datatypes for storage in sessions: + + import( + "encoding/gob" + "github.com/gorilla/sessions" + ) + + type Person struct { + FirstName string + LastName string + Email string + Age int + } + + type M map[string]interface{} + + func init() { + + gob.Register(&Person{}) + gob.Register(&M{}) + } + +As it's not possible to pass a raw type as a parameter to a function, gob.Register() +relies on us passing it a value of the desired type. In the example above we've passed +it a pointer to a struct and a pointer to a custom type representing a +map[string]interface. (We could have passed non-pointer values if we wished.) This will +then allow us to serialise/deserialise values of those types to and from our sessions. + +Note that because session values are stored in a map[string]interface{}, there's +a need to type-assert data when retrieving it. We'll use the Person struct we registered above: + + func MyHandler(w http.ResponseWriter, r *http.Request) { + session, err := store.Get(r, "session-name") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Retrieve our struct and type-assert it + val := session.Values["person"] + var person = &Person{} + if person, ok := val.(*Person); !ok { + // Handle the case that it's not an expected type + } + + // Now we can use our person object + } + +By default, session cookies last for a month. This is probably too long for +some cases, but it is easy to change this and other attributes during +runtime. Sessions can be configured individually or the store can be +configured and then all sessions saved using it will use that configuration. +We access session.Options or store.Options to set a new configuration. The +fields are basically a subset of http.Cookie fields. Let's change the +maximum age of a session to one week: + + session.Options = &sessions.Options{ + Path: "/", + MaxAge: 86400 * 7, + HttpOnly: true, + } + +Sometimes we may want to change authentication and/or encryption keys without +breaking existing sessions. The CookieStore supports key rotation, and to use +it you just need to set multiple authentication and encryption keys, in pairs, +to be tested in order: + + var store = sessions.NewCookieStore( + []byte("new-authentication-key"), + []byte("new-encryption-key"), + []byte("old-authentication-key"), + []byte("old-encryption-key"), + ) + +New sessions will be saved using the first pair. Old sessions can still be +read because the first pair will fail, and the second will be tested. This +makes it easy to "rotate" secret keys and still be able to validate existing +sessions. Note: for all pairs the encryption key is optional; set it to nil +or omit it and and encryption won't be used. + +Multiple sessions can be used in the same request, even with different +session backends. When this happens, calling Save() on each session +individually would be cumbersome, so we have a way to save all sessions +at once: it's sessions.Save(). Here's an example: + + var store = sessions.NewCookieStore([]byte("something-very-secret")) + + func MyHandler(w http.ResponseWriter, r *http.Request) { + // Get a session and set a value. + session1, _ := store.Get(r, "session-one") + session1.Values["foo"] = "bar" + // Get another session and set another value. + session2, _ := store.Get(r, "session-two") + session2.Values[42] = 43 + // Save all sessions. + sessions.Save(r, w) + } + +This is possible because when we call Get() from a session store, it adds the +session to a common registry. Save() uses it to save all registered sessions. +*/ +package sessions diff --git a/vendor/github.com/gorilla/sessions/go.mod b/vendor/github.com/gorilla/sessions/go.mod new file mode 100644 index 0000000..ea28ffe --- /dev/null +++ b/vendor/github.com/gorilla/sessions/go.mod @@ -0,0 +1,6 @@ +module github.com/gorilla/sessions + +require ( + github.com/gorilla/context v1.1.1 + github.com/gorilla/securecookie v1.1.1 +) diff --git a/vendor/github.com/gorilla/sessions/lex.go b/vendor/github.com/gorilla/sessions/lex.go new file mode 100644 index 0000000..4bbbe10 --- /dev/null +++ b/vendor/github.com/gorilla/sessions/lex.go @@ -0,0 +1,102 @@ +// This file contains code adapted from the Go standard library +// https://github.com/golang/go/blob/39ad0fd0789872f9469167be7fe9578625ff246e/src/net/http/lex.go + +package sessions + +import "strings" + +var isTokenTable = [127]bool{ + '!': true, + '#': true, + '$': true, + '%': true, + '&': true, + '\'': true, + '*': true, + '+': true, + '-': true, + '.': true, + '0': true, + '1': true, + '2': true, + '3': true, + '4': true, + '5': true, + '6': true, + '7': true, + '8': true, + '9': true, + 'A': true, + 'B': true, + 'C': true, + 'D': true, + 'E': true, + 'F': true, + 'G': true, + 'H': true, + 'I': true, + 'J': true, + 'K': true, + 'L': true, + 'M': true, + 'N': true, + 'O': true, + 'P': true, + 'Q': true, + 'R': true, + 'S': true, + 'T': true, + 'U': true, + 'W': true, + 'V': true, + 'X': true, + 'Y': true, + 'Z': true, + '^': true, + '_': true, + '`': true, + 'a': true, + 'b': true, + 'c': true, + 'd': true, + 'e': true, + 'f': true, + 'g': true, + 'h': true, + 'i': true, + 'j': true, + 'k': true, + 'l': true, + 'm': true, + 'n': true, + 'o': true, + 'p': true, + 'q': true, + 'r': true, + 's': true, + 't': true, + 'u': true, + 'v': true, + 'w': true, + 'x': true, + 'y': true, + 'z': true, + '|': true, + '~': true, +} + +func isToken(r rune) bool { + i := int(r) + return i < len(isTokenTable) && isTokenTable[i] +} + +func isNotToken(r rune) bool { + return !isToken(r) +} + +func isCookieNameValid(raw string) bool { + if raw == "" { + return false + } + return strings.IndexFunc(raw, isNotToken) < 0 +} diff --git a/vendor/github.com/gorilla/sessions/options.go b/vendor/github.com/gorilla/sessions/options.go new file mode 100644 index 0000000..38ba72f --- /dev/null +++ b/vendor/github.com/gorilla/sessions/options.go @@ -0,0 +1,18 @@ +// +build !go1.11 + +package sessions + +// Options stores configuration for a session or session store. +// +// Fields are a subset of http.Cookie fields. +type Options struct { + Path string + Domain string + // MaxAge=0 means no Max-Age attribute specified and the cookie will be + // deleted after the browser session ends. + // MaxAge<0 means delete cookie immediately. + // MaxAge>0 means Max-Age attribute present and given in seconds. + MaxAge int + Secure bool + HttpOnly bool +} diff --git a/vendor/github.com/gorilla/sessions/options_go111.go b/vendor/github.com/gorilla/sessions/options_go111.go new file mode 100644 index 0000000..388112a --- /dev/null +++ b/vendor/github.com/gorilla/sessions/options_go111.go @@ -0,0 +1,22 @@ +// +build go1.11 + +package sessions + +import "net/http" + +// Options stores configuration for a session or session store. +// +// Fields are a subset of http.Cookie fields. +type Options struct { + Path string + Domain string + // MaxAge=0 means no Max-Age attribute specified and the cookie will be + // deleted after the browser session ends. + // MaxAge<0 means delete cookie immediately. + // MaxAge>0 means Max-Age attribute present and given in seconds. + MaxAge int + Secure bool + HttpOnly bool + // Defaults to http.SameSiteDefaultMode + SameSite http.SameSite +} diff --git a/vendor/github.com/gorilla/sessions/sessions.go b/vendor/github.com/gorilla/sessions/sessions.go new file mode 100644 index 0000000..c052b28 --- /dev/null +++ b/vendor/github.com/gorilla/sessions/sessions.go @@ -0,0 +1,218 @@ +// Copyright 2012 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package sessions + +import ( + "context" + "encoding/gob" + "fmt" + "net/http" + "time" +) + +// Default flashes key. +const flashesKey = "_flash" + +// Session -------------------------------------------------------------------- + +// NewSession is called by session stores to create a new session instance. +func NewSession(store Store, name string) *Session { + return &Session{ + Values: make(map[interface{}]interface{}), + store: store, + name: name, + Options: new(Options), + } +} + +// Session stores the values and optional configuration for a session. +type Session struct { + // The ID of the session, generated by stores. It should not be used for + // user data. + ID string + // Values contains the user-data for the session. + Values map[interface{}]interface{} + Options *Options + IsNew bool + store Store + name string +} + +// Flashes returns a slice of flash messages from the session. +// +// A single variadic argument is accepted, and it is optional: it defines +// the flash key. If not defined "_flash" is used by default. +func (s *Session) Flashes(vars ...string) []interface{} { + var flashes []interface{} + key := flashesKey + if len(vars) > 0 { + key = vars[0] + } + if v, ok := s.Values[key]; ok { + // Drop the flashes and return it. + delete(s.Values, key) + flashes = v.([]interface{}) + } + return flashes +} + +// AddFlash adds a flash message to the session. +// +// A single variadic argument is accepted, and it is optional: it defines +// the flash key. If not defined "_flash" is used by default. +func (s *Session) AddFlash(value interface{}, vars ...string) { + key := flashesKey + if len(vars) > 0 { + key = vars[0] + } + var flashes []interface{} + if v, ok := s.Values[key]; ok { + flashes = v.([]interface{}) + } + s.Values[key] = append(flashes, value) +} + +// Save is a convenience method to save this session. It is the same as calling +// store.Save(request, response, session). You should call Save before writing to +// the response or returning from the handler. +func (s *Session) Save(r *http.Request, w http.ResponseWriter) error { + return s.store.Save(r, w, s) +} + +// Name returns the name used to register the session. +func (s *Session) Name() string { + return s.name +} + +// Store returns the session store used to register the session. +func (s *Session) Store() Store { + return s.store +} + +// Registry ------------------------------------------------------------------- + +// sessionInfo stores a session tracked by the registry. +type sessionInfo struct { + s *Session + e error +} + +// contextKey is the type used to store the registry in the context. +type contextKey int + +// registryKey is the key used to store the registry in the context. +const registryKey contextKey = 0 + +// GetRegistry returns a registry instance for the current request. +func GetRegistry(r *http.Request) *Registry { + var ctx = r.Context() + registry := ctx.Value(registryKey) + if registry != nil { + return registry.(*Registry) + } + newRegistry := &Registry{ + request: r, + sessions: make(map[string]sessionInfo), + } + *r = *r.WithContext(context.WithValue(ctx, registryKey, newRegistry)) + return newRegistry +} + +// Registry stores sessions used during a request. +type Registry struct { + request *http.Request + sessions map[string]sessionInfo +} + +// Get registers and returns a session for the given name and session store. +// +// It returns a new session if there are no sessions registered for the name. +func (s *Registry) Get(store Store, name string) (session *Session, err error) { + if !isCookieNameValid(name) { + return nil, fmt.Errorf("sessions: invalid character in cookie name: %s", name) + } + if info, ok := s.sessions[name]; ok { + session, err = info.s, info.e + } else { + session, err = store.New(s.request, name) + session.name = name + s.sessions[name] = sessionInfo{s: session, e: err} + } + session.store = store + return +} + +// Save saves all sessions registered for the current request. +func (s *Registry) Save(w http.ResponseWriter) error { + var errMulti MultiError + for name, info := range s.sessions { + session := info.s + if session.store == nil { + errMulti = append(errMulti, fmt.Errorf( + "sessions: missing store for session %q", name)) + } else if err := session.store.Save(s.request, w, session); err != nil { + errMulti = append(errMulti, fmt.Errorf( + "sessions: error saving session %q -- %v", name, err)) + } + } + if errMulti != nil { + return errMulti + } + return nil +} + +// Helpers -------------------------------------------------------------------- + +func init() { + gob.Register([]interface{}{}) +} + +// Save saves all sessions used during the current request. +func Save(r *http.Request, w http.ResponseWriter) error { + return GetRegistry(r).Save(w) +} + +// NewCookie returns an http.Cookie with the options set. It also sets +// the Expires field calculated based on the MaxAge value, for Internet +// Explorer compatibility. +func NewCookie(name, value string, options *Options) *http.Cookie { + cookie := newCookieFromOptions(name, value, options) + if options.MaxAge > 0 { + d := time.Duration(options.MaxAge) * time.Second + cookie.Expires = time.Now().Add(d) + } else if options.MaxAge < 0 { + // Set it to the past to expire now. + cookie.Expires = time.Unix(1, 0) + } + return cookie +} + +// Error ---------------------------------------------------------------------- + +// MultiError stores multiple errors. +// +// Borrowed from the App Engine SDK. +type MultiError []error + +func (m MultiError) Error() string { + s, n := "", 0 + for _, e := range m { + if e != nil { + if n == 0 { + s = e.Error() + } + n++ + } + } + switch n { + case 0: + return "(0 errors)" + case 1: + return s + case 2: + return s + " (and 1 other error)" + } + return fmt.Sprintf("%s (and %d other errors)", s, n-1) +} diff --git a/vendor/github.com/gorilla/sessions/store.go b/vendor/github.com/gorilla/sessions/store.go new file mode 100644 index 0000000..bb7f964 --- /dev/null +++ b/vendor/github.com/gorilla/sessions/store.go @@ -0,0 +1,292 @@ +// Copyright 2012 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package sessions + +import ( + "encoding/base32" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "strings" + "sync" + + "github.com/gorilla/securecookie" +) + +// Store is an interface for custom session stores. +// +// See CookieStore and FilesystemStore for examples. +type Store interface { + // Get should return a cached session. + Get(r *http.Request, name string) (*Session, error) + + // New should create and return a new session. + // + // Note that New should never return a nil session, even in the case of + // an error if using the Registry infrastructure to cache the session. + New(r *http.Request, name string) (*Session, error) + + // Save should persist session to the underlying store implementation. + Save(r *http.Request, w http.ResponseWriter, s *Session) error +} + +// CookieStore ---------------------------------------------------------------- + +// NewCookieStore returns a new CookieStore. +// +// Keys are defined in pairs to allow key rotation, but the common case is +// to set a single authentication key and optionally an encryption key. +// +// The first key in a pair is used for authentication and the second for +// encryption. The encryption key can be set to nil or omitted in the last +// pair, but the authentication key is required in all pairs. +// +// It is recommended to use an authentication key with 32 or 64 bytes. +// The encryption key, if set, must be either 16, 24, or 32 bytes to select +// AES-128, AES-192, or AES-256 modes. +func NewCookieStore(keyPairs ...[]byte) *CookieStore { + cs := &CookieStore{ + Codecs: securecookie.CodecsFromPairs(keyPairs...), + Options: &Options{ + Path: "/", + MaxAge: 86400 * 30, + }, + } + + cs.MaxAge(cs.Options.MaxAge) + return cs +} + +// CookieStore stores sessions using secure cookies. +type CookieStore struct { + Codecs []securecookie.Codec + Options *Options // default configuration +} + +// Get returns a session for the given name after adding it to the registry. +// +// It returns a new session if the sessions doesn't exist. Access IsNew on +// the session to check if it is an existing session or a new one. +// +// It returns a new session and an error if the session exists but could +// not be decoded. +func (s *CookieStore) Get(r *http.Request, name string) (*Session, error) { + return GetRegistry(r).Get(s, name) +} + +// New returns a session for the given name without adding it to the registry. +// +// The difference between New() and Get() is that calling New() twice will +// decode the session data twice, while Get() registers and reuses the same +// decoded session after the first call. +func (s *CookieStore) New(r *http.Request, name string) (*Session, error) { + session := NewSession(s, name) + opts := *s.Options + session.Options = &opts + session.IsNew = true + var err error + if c, errCookie := r.Cookie(name); errCookie == nil { + err = securecookie.DecodeMulti(name, c.Value, &session.Values, + s.Codecs...) + if err == nil { + session.IsNew = false + } + } + return session, err +} + +// Save adds a single session to the response. +func (s *CookieStore) Save(r *http.Request, w http.ResponseWriter, + session *Session) error { + encoded, err := securecookie.EncodeMulti(session.Name(), session.Values, + s.Codecs...) + if err != nil { + return err + } + http.SetCookie(w, NewCookie(session.Name(), encoded, session.Options)) + return nil +} + +// MaxAge sets the maximum age for the store and the underlying cookie +// implementation. Individual sessions can be deleted by setting Options.MaxAge +// = -1 for that session. +func (s *CookieStore) MaxAge(age int) { + s.Options.MaxAge = age + + // Set the maxAge for each securecookie instance. + for _, codec := range s.Codecs { + if sc, ok := codec.(*securecookie.SecureCookie); ok { + sc.MaxAge(age) + } + } +} + +// FilesystemStore ------------------------------------------------------------ + +var fileMutex sync.RWMutex + +// NewFilesystemStore returns a new FilesystemStore. +// +// The path argument is the directory where sessions will be saved. If empty +// it will use os.TempDir(). +// +// See NewCookieStore() for a description of the other parameters. +func NewFilesystemStore(path string, keyPairs ...[]byte) *FilesystemStore { + if path == "" { + path = os.TempDir() + } + fs := &FilesystemStore{ + Codecs: securecookie.CodecsFromPairs(keyPairs...), + Options: &Options{ + Path: "/", + MaxAge: 86400 * 30, + }, + path: path, + } + + fs.MaxAge(fs.Options.MaxAge) + return fs +} + +// FilesystemStore stores sessions in the filesystem. +// +// It also serves as a reference for custom stores. +// +// This store is still experimental and not well tested. Feedback is welcome. +type FilesystemStore struct { + Codecs []securecookie.Codec + Options *Options // default configuration + path string +} + +// MaxLength restricts the maximum length of new sessions to l. +// If l is 0 there is no limit to the size of a session, use with caution. +// The default for a new FilesystemStore is 4096. +func (s *FilesystemStore) MaxLength(l int) { + for _, c := range s.Codecs { + if codec, ok := c.(*securecookie.SecureCookie); ok { + codec.MaxLength(l) + } + } +} + +// Get returns a session for the given name after adding it to the registry. +// +// See CookieStore.Get(). +func (s *FilesystemStore) Get(r *http.Request, name string) (*Session, error) { + return GetRegistry(r).Get(s, name) +} + +// New returns a session for the given name without adding it to the registry. +// +// See CookieStore.New(). +func (s *FilesystemStore) New(r *http.Request, name string) (*Session, error) { + session := NewSession(s, name) + opts := *s.Options + session.Options = &opts + session.IsNew = true + var err error + if c, errCookie := r.Cookie(name); errCookie == nil { + err = securecookie.DecodeMulti(name, c.Value, &session.ID, s.Codecs...) + if err == nil { + err = s.load(session) + if err == nil { + session.IsNew = false + } + } + } + return session, err +} + +// Save adds a single session to the response. +// +// If the Options.MaxAge of the session is <= 0 then the session file will be +// deleted from the store path. With this process it enforces the properly +// session cookie handling so no need to trust in the cookie management in the +// web browser. +func (s *FilesystemStore) Save(r *http.Request, w http.ResponseWriter, + session *Session) error { + // Delete if max-age is <= 0 + if session.Options.MaxAge <= 0 { + if err := s.erase(session); err != nil { + return err + } + http.SetCookie(w, NewCookie(session.Name(), "", session.Options)) + return nil + } + + if session.ID == "" { + // Because the ID is used in the filename, encode it to + // use alphanumeric characters only. + session.ID = strings.TrimRight( + base32.StdEncoding.EncodeToString( + securecookie.GenerateRandomKey(32)), "=") + } + if err := s.save(session); err != nil { + return err + } + encoded, err := securecookie.EncodeMulti(session.Name(), session.ID, + s.Codecs...) + if err != nil { + return err + } + http.SetCookie(w, NewCookie(session.Name(), encoded, session.Options)) + return nil +} + +// MaxAge sets the maximum age for the store and the underlying cookie +// implementation. Individual sessions can be deleted by setting Options.MaxAge +// = -1 for that session. +func (s *FilesystemStore) MaxAge(age int) { + s.Options.MaxAge = age + + // Set the maxAge for each securecookie instance. + for _, codec := range s.Codecs { + if sc, ok := codec.(*securecookie.SecureCookie); ok { + sc.MaxAge(age) + } + } +} + +// save writes encoded session.Values to a file. +func (s *FilesystemStore) save(session *Session) error { + encoded, err := securecookie.EncodeMulti(session.Name(), session.Values, + s.Codecs...) + if err != nil { + return err + } + filename := filepath.Join(s.path, "session_"+session.ID) + fileMutex.Lock() + defer fileMutex.Unlock() + return ioutil.WriteFile(filename, []byte(encoded), 0600) +} + +// load reads a file and decodes its content into session.Values. +func (s *FilesystemStore) load(session *Session) error { + filename := filepath.Join(s.path, "session_"+session.ID) + fileMutex.RLock() + defer fileMutex.RUnlock() + fdata, err := ioutil.ReadFile(filename) + if err != nil { + return err + } + if err = securecookie.DecodeMulti(session.Name(), string(fdata), + &session.Values, s.Codecs...); err != nil { + return err + } + return nil +} + +// delete session file +func (s *FilesystemStore) erase(session *Session) error { + filename := filepath.Join(s.path, "session_"+session.ID) + + fileMutex.RLock() + defer fileMutex.RUnlock() + + err := os.Remove(filename) + return err +} diff --git a/vendor/github.com/utrack/gin-csrf/LICENSE b/vendor/github.com/utrack/gin-csrf/LICENSE new file mode 100644 index 0000000..fea5200 --- /dev/null +++ b/vendor/github.com/utrack/gin-csrf/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Nikita Koptelov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/utrack/gin-csrf/README.md b/vendor/github.com/utrack/gin-csrf/README.md new file mode 100644 index 0000000..77feaea --- /dev/null +++ b/vendor/github.com/utrack/gin-csrf/README.md @@ -0,0 +1,47 @@ +# gin-csrf [![Build Status](https://travis-ci.org/utrack/gin-csrf.svg?branch=master)](https://travis-ci.org/utrack/gin-csrf) + +CSRF protection middleware for [Gin]. This middleware has to be used with [gin-contrib/sessions](https://github.com/gin-contrib/sessions). + +Original credit to [tommy351](https://github.com/tommy351/gin-csrf), this fork makes it work with gin-gonic contrib sessions. + +## Installation + +``` bash +$ go get github.com/utrack/gin-csrf +``` + +## Usage + +``` go +import ( + "errors" + + "github.com/gin-gonic/gin" + "github.com/gin-contrib/sessions" + "github.com/utrack/gin-csrf" +) + +func main(){ + r := gin.Default() + store := sessions.NewCookieStore([]byte("secret")) + r.Use(sessions.Sessions("mysession", store)) + r.Use(csrf.Middleware(csrf.Options{ + Secret: "secret123", + ErrorFunc: func(c *gin.Context){ + c.String(400, "CSRF token mismatch") + c.Abort() + }, + })) + + r.GET("/protected", func(c *gin.Context){ + c.String(200, csrf.GetToken(c)) + }) + + r.POST("/protected", func(c *gin.Context){ + c.String(200, "CSRF token is valid") + }) +} +``` + +[Gin]: http://gin-gonic.github.io/gin/ +[gin-sessions]: https://github.com/utrack/gin-sessions diff --git a/vendor/github.com/utrack/gin-csrf/csrf.go b/vendor/github.com/utrack/gin-csrf/csrf.go new file mode 100644 index 0000000..4215494 --- /dev/null +++ b/vendor/github.com/utrack/gin-csrf/csrf.go @@ -0,0 +1,135 @@ +package csrf + +import ( + "crypto/sha1" + "encoding/base64" + "errors" + "io" + + "github.com/dchest/uniuri" + "github.com/gin-contrib/sessions" + "github.com/gin-gonic/gin" +) + +const ( + csrfSecret = "csrfSecret" + csrfSalt = "csrfSalt" + csrfToken = "csrfToken" +) + +var defaultIgnoreMethods = []string{"GET", "HEAD", "OPTIONS"} + +var defaultErrorFunc = func(c *gin.Context) { + panic(errors.New("CSRF token mismatch")) +} + +var defaultTokenGetter = func(c *gin.Context) string { + r := c.Request + + if t := r.FormValue("_csrf"); len(t) > 0 { + return t + } else if t := r.URL.Query().Get("_csrf"); len(t) > 0 { + return t + } else if t := r.Header.Get("X-CSRF-TOKEN"); len(t) > 0 { + return t + } else if t := r.Header.Get("X-XSRF-TOKEN"); len(t) > 0 { + return t + } + + return "" +} + +// Options stores configurations for a CSRF middleware. +type Options struct { + Secret string + IgnoreMethods []string + ErrorFunc gin.HandlerFunc + TokenGetter func(c *gin.Context) string +} + +func tokenize(secret, salt string) string { + h := sha1.New() + io.WriteString(h, salt+"-"+secret) + hash := base64.URLEncoding.EncodeToString(h.Sum(nil)) + + return hash +} + +func inArray(arr []string, value string) bool { + inarr := false + + for _, v := range arr { + if v == value { + inarr = true + break + } + } + + return inarr +} + +// Middleware validates CSRF token. +func Middleware(options Options) gin.HandlerFunc { + ignoreMethods := options.IgnoreMethods + errorFunc := options.ErrorFunc + tokenGetter := options.TokenGetter + + if ignoreMethods == nil { + ignoreMethods = defaultIgnoreMethods + } + + if errorFunc == nil { + errorFunc = defaultErrorFunc + } + + if tokenGetter == nil { + tokenGetter = defaultTokenGetter + } + + return func(c *gin.Context) { + session := sessions.Default(c) + c.Set(csrfSecret, options.Secret) + + if inArray(ignoreMethods, c.Request.Method) { + c.Next() + return + } + + salt, ok := session.Get(csrfSalt).(string) + + if !ok || len(salt) == 0 { + errorFunc(c) + return + } + + token := tokenGetter(c) + + if tokenize(options.Secret, salt) != token { + errorFunc(c) + return + } + + c.Next() + } +} + +// GetToken returns a CSRF token. +func GetToken(c *gin.Context) string { + session := sessions.Default(c) + secret := c.MustGet(csrfSecret).(string) + + if t, ok := c.Get(csrfToken); ok { + return t.(string) + } + + salt, ok := session.Get(csrfSalt).(string) + if !ok { + salt = uniuri.New() + session.Set(csrfSalt, salt) + session.Save() + } + token := tokenize(secret, salt) + c.Set(csrfToken, token) + + return token +} diff --git a/vendor/github.com/utrack/gin-csrf/go.mod b/vendor/github.com/utrack/gin-csrf/go.mod new file mode 100644 index 0000000..cd02e51 --- /dev/null +++ b/vendor/github.com/utrack/gin-csrf/go.mod @@ -0,0 +1,7 @@ +module github.com/utrack/gin-csrf + +require ( + github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9 + github.com/gin-contrib/sessions v0.0.0-20190101140330-dc5246754963 + github.com/gin-gonic/gin v1.3.0 +) diff --git a/vendor/github.com/utrack/gin-csrf/go.sum b/vendor/github.com/utrack/gin-csrf/go.sum new file mode 100644 index 0000000..01b26f4 --- /dev/null +++ b/vendor/github.com/utrack/gin-csrf/go.sum @@ -0,0 +1,50 @@ +github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw= +github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60= +github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20181103040241-659414f458e1/go.mod h1:dkChI7Tbtx7H1Tj7TqGSZMOeGpMP5gLHtjroHd4agiI= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9 h1:74lLNRzvsdIlkTgfDSMuaPjBr4cf6k7pwQQANm/yLKU= +github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4= +github.com/garyburd/redigo v1.6.0/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= +github.com/gin-contrib/sessions v0.0.0-20190101140330-dc5246754963 h1:ldKXSIxdVtXVCP4JW0p4ErvnPhISjbb5QSIqMnBa3ak= +github.com/gin-contrib/sessions v0.0.0-20190101140330-dc5246754963/go.mod h1:4lkInX8nHSR62NSmhXM3xtPeMSyfiR58NaEz+om1lHM= +github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 h1:AzN37oI0cOS+cougNAV9szl6CVoj2RYwzS3DpUQNtlY= +github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= +github.com/gin-gonic/gin v1.3.0 h1:kCmZyPklC0gVdL728E6Aj20uYBJV93nj/TkwBTKhFbs= +github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y= +github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= +github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= +github.com/gorilla/sessions v1.1.3 h1:uXoZdcdA5XdXF3QzuSlheVRUvjl+1rKY7zBXL68L9RU= +github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= +github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/kidstuff/mongostore v0.0.0-20181113001930-e650cd85ee4b/go.mod h1:g2nVr8KZVXJSS97Jo8pJ0jgq29P6H7dG0oplUA86MQw= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/memcachier/mc v2.0.1+incompatible/go.mod h1:7bkvFE61leUBvXz+yxsOnGBQSZpBSPIMUQSmmSHvuXc= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quasoft/memstore v0.0.0-20180925164028-84a050167438/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2 h1:EICbibRW4JNKMcY+LsWmuwob+CRS1BmdRdjphAm9mH4= +github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= +gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ= +gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= +gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/vendor/vendor.json b/vendor/vendor.json index 045d9e6..5bf2fa8 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -8,12 +8,32 @@ "revision": "2b29687e15f2cc71fb3a50f28be1d6d30c40c86a", "revisionTime": "2019-01-25T09:51:13Z" }, + { + "checksumSHA1": "pXA0VXOMBMY18/4WFJjqnsT51jo=", + "path": "github.com/dchest/uniuri", + "revision": "8902c56451e9b58ff940bbe5fec35d5f9c04584a", + "revisionTime": "2016-02-12T16:43:26Z" + }, { "checksumSHA1": "XNyrvOw+twqjPJvZjr13wzae4X4=", "path": "github.com/gin-contrib/cors", "revision": "5e7acb10687f94a88d0d8e96297818fff2da8f88", "revisionTime": "2019-01-01T12:32:55Z" }, + { + "checksumSHA1": "DKLBHtyjnaoUW/0w0amXnl6kTRE=", + "path": "github.com/gin-contrib/sessions", + "revision": "1532893d996f0fedbac1162ea1311ebfb57a3da5", + "revisionTime": "2019-02-26T02:30:29Z", + "version": "master", + "versionExact": "master" + }, + { + "checksumSHA1": "eyvExzi+ADNi3cMXeld05hHhWkA=", + "path": "github.com/gin-contrib/sessions/cookie", + "revision": "1532893d996f0fedbac1162ea1311ebfb57a3da5", + "revisionTime": "2019-02-26T02:30:29Z" + }, { "checksumSHA1": "90S9sFqcIo/uuwQzz1t/wYs7GEs=", "path": "github.com/gin-gonic/gin", @@ -76,6 +96,24 @@ "revision": "347cf4a86c1cb8d262994d8ef5924d4576c5b331", "revisionTime": "2019-01-09T07:22:47Z" }, + { + "checksumSHA1": "oHwfghigjTz8Xs+ssxwdl/atb44=", + "path": "github.com/gorilla/context", + "revision": "51ce91d2eaddeca0ef29a71d766bb3634dadf729", + "revisionTime": "2018-10-12T15:35:48Z" + }, + { + "checksumSHA1": "NScdwrIOpSH9hoP7i3a6JGmE5rw=", + "path": "github.com/gorilla/securecookie", + "revision": "e65cf8c5df817c89aeb47ecb46064e802e2de943", + "revisionTime": "2018-10-10T17:46:47Z" + }, + { + "checksumSHA1": "csomktSHfKVMLgh9B6nikPxd6sk=", + "path": "github.com/gorilla/sessions", + "revision": "12bd4761fc66ac946e16fcc2a32b1e0b066f6177", + "revisionTime": "2018-12-08T21:45:19Z" + }, { "checksumSHA1": "ZxzYc1JwJ3U6kZbw/KGuPko5lSY=", "path": "github.com/howeyc/fsnotify", @@ -130,6 +168,12 @@ "revision": "b2ce2384e17bbe0c6d34077efa39dbab3e09123b", "revisionTime": "2018-10-28T12:50:25Z" }, + { + "checksumSHA1": "DBMXq3Zu/x/Gu8lV4D/bj2wtpEE=", + "path": "github.com/utrack/gin-csrf", + "revision": "1689c46c9740944a8e0bfb13c34e190e7266ec76", + "revisionTime": "2019-02-13T16:04:13Z" + }, { "checksumSHA1": "P/k5ZGf0lEBgpKgkwy++F7K1PSg=", "path": "gopkg.in/go-playground/validator.v8",