mirror of
https://github.com/elyby/chrly.git
synced 2025-05-31 14:11:51 +05:30
Compare commits
56 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cfe8fea3f7 | ||
|
|
27c7b79b32 | ||
|
|
ad31fdb709 | ||
|
|
fa62d45d00 | ||
|
|
cadb89f00a | ||
|
|
20ba78953b | ||
|
|
883a7bda3c | ||
|
|
d678f61df7 | ||
|
|
1543e98b87 | ||
|
|
0e0b41d6d7 | ||
|
|
3cd12acc1b | ||
|
|
dac4ed0ac6 | ||
|
|
11a779c670 | ||
|
|
6cb5e1eb42 | ||
|
|
8959e53270 | ||
|
|
3526570dd3 | ||
|
|
4b7f1346f5 | ||
|
|
e7721f9e5a | ||
|
|
26bfbd1517 | ||
|
|
ecbb06b83c | ||
|
|
6accffed45 | ||
|
|
980c920ceb | ||
|
|
32a9fee3e6 | ||
|
|
7cf5ae13be | ||
|
|
98d280240e | ||
|
|
26042037b6 | ||
|
|
1e3307dcbe | ||
|
|
d1d2c7ee6e | ||
|
|
2bc9f8eb57 | ||
|
|
6f148a8791 | ||
|
|
247499df6a | ||
|
|
3bf6872f3e | ||
|
|
60774b6b72 | ||
|
|
37cc8cda32 | ||
|
|
620bb95c74 | ||
|
|
fd05220299 | ||
|
|
dfe024756e | ||
|
|
66ef76ce6d | ||
|
|
aabf54e318 | ||
|
|
5dbe6af1d0 | ||
|
|
4c21fc5c90 | ||
|
|
2ea094bbf6 | ||
|
|
c4566a337b | ||
|
|
05c68c6ba6 | ||
|
|
8001eab9db | ||
|
|
33b286cba0 | ||
|
|
f997fdf9b0 | ||
|
|
be30c23823 | ||
|
|
f43c1a9a37 | ||
|
|
585318d307 | ||
|
|
b2e501af60 | ||
|
|
d8f6786c69 | ||
|
|
30c095525c | ||
|
|
436d98e1a0 | ||
|
|
1b9e943c0e | ||
|
|
29b6bc89b3 |
13
.fossa.yml
13
.fossa.yml
@@ -1,13 +0,0 @@
|
||||
version: 2
|
||||
cli:
|
||||
server: https://app.fossa.com
|
||||
fetcher: git
|
||||
project: github.com:elyby/chrly
|
||||
analyze:
|
||||
modules:
|
||||
- name: chrly
|
||||
type: go
|
||||
target: github.com/elyby/chrly
|
||||
path: .
|
||||
options:
|
||||
strategy: dep
|
||||
53
.github/workflows/build.yml
vendored
Normal file
53
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
tags:
|
||||
- '*.*.*'
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
services:
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
options: >-
|
||||
--health-cmd "redis-cli ping"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 6379:6379
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
cache-dependency-path: go.sum
|
||||
go-version-file: go.mod
|
||||
|
||||
- name: Install dependencies
|
||||
run: go get .
|
||||
|
||||
- name: Go Format
|
||||
run: gofmt -s -w . && git diff --exit-code
|
||||
|
||||
- name: Go Vet
|
||||
run: go vet ./...
|
||||
|
||||
- name: Go Test
|
||||
run: go test -v -race --tags redis -coverprofile=coverage.txt -covermode=atomic ./...
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v4-beta
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
- name: Build
|
||||
run: go build ./...
|
||||
66
.github/workflows/release.yml
vendored
Normal file
66
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows:
|
||||
- Build
|
||||
types:
|
||||
- completed
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
dockerhub:
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- id: meta
|
||||
name: Docker meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ github.repository }}
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
type=edge,branch=${{ github.event.repository.default_branch }}
|
||||
|
||||
- id: version
|
||||
name: Set up build version
|
||||
run: |
|
||||
if [[ $GITHUB_REF_TYPE == "tag" ]]; then
|
||||
VERSION=${GITHUB_REF#refs/tags/}
|
||||
else
|
||||
BRANCH_NAME=${GITHUB_REF#refs/heads/}
|
||||
SHORT_SHA=$(git rev-parse --short $GITHUB_SHA)
|
||||
VERSION="${BRANCH_NAME}-${SHORT_SHA}"
|
||||
fi
|
||||
echo "### Version: $VERSION" >> $GITHUB_STEP_SUMMARY
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and Push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
build-args: |
|
||||
VERSION=${{ steps.version.outputs.version }}
|
||||
COMMIT=${{ github.sha }}
|
||||
63
.travis.yml
63
.travis.yml
@@ -1,63 +0,0 @@
|
||||
os: linux
|
||||
dist: xenial
|
||||
language: go
|
||||
go:
|
||||
- "1.14"
|
||||
|
||||
stages:
|
||||
- Tests
|
||||
- name: Deploy
|
||||
if: env(TRAVIS_PULL_REQUEST) IS false AND (branch = master OR tag IS present) AND commit_message !~ /(\[skip deploy\])/
|
||||
|
||||
install:
|
||||
- go get -u github.com/golang/dep/cmd/dep
|
||||
- dep ensure
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $GOPATH/pkg/dep
|
||||
|
||||
jobs:
|
||||
include:
|
||||
# Tests stage
|
||||
- name: Unit tests
|
||||
stage: Tests
|
||||
services:
|
||||
- redis
|
||||
script:
|
||||
- go test -v -race --tags redis -coverprofile=coverage.txt -covermode=atomic ./...
|
||||
- bash <(curl -s https://codecov.io/bash)
|
||||
|
||||
- name: FOSSA
|
||||
stage: Tests
|
||||
if: branch = master
|
||||
before_script:
|
||||
- curl https://raw.githubusercontent.com/fossas/fossa-cli/master/install.sh | bash
|
||||
script:
|
||||
- fossa init
|
||||
- fossa analyze
|
||||
# Disable until https://github.com/fossas/fossa-cli/issues/596 will be resolved
|
||||
# - fossa test
|
||||
|
||||
# Deploy stage
|
||||
- name: Docker image
|
||||
stage: Deploy
|
||||
services:
|
||||
- docker
|
||||
script:
|
||||
- docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD"
|
||||
- export DOCKER_TAG="${TRAVIS_TAG:-dev}"
|
||||
- export APP_VERSION="${TRAVIS_TAG:-dev-${TRAVIS_COMMIT:0:7}}"
|
||||
- >
|
||||
env CGO_ENABLED=0 GOOS=linux GOARCH=amd64
|
||||
go build
|
||||
-o release/chrly
|
||||
-ldflags "-extldflags '-static' -X github.com/elyby/chrly/version.version=$APP_VERSION -X github.com/elyby/chrly/version.commit=$TRAVIS_COMMIT"
|
||||
main.go
|
||||
- docker build -t elyby/chrly:$DOCKER_TAG .
|
||||
- docker push elyby/chrly:$DOCKER_TAG
|
||||
- |
|
||||
if [ ! -z ${TRAVIS_TAG:+x} ]; then
|
||||
docker tag elyby/chrly:$DOCKER_TAG elyby/chrly:latest
|
||||
docker push elyby/chrly:latest
|
||||
fi
|
||||
64
CHANGELOG.md
64
CHANGELOG.md
@@ -5,6 +5,66 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased] - xxxx-xx-xx
|
||||
### Added
|
||||
- Allow to remove a skin without removing all user information
|
||||
- New StatsD metrics:
|
||||
- Counters:
|
||||
- `ely.skinsystem.{hostname}.app.profiles.request`
|
||||
|
||||
### Fixed
|
||||
- Adjusted Mojang usernames filter to be stickier according to their docs
|
||||
- `/profile/{username}` endpoint now returns the correct signature for the custom property as well.
|
||||
|
||||
### Changed
|
||||
- Bumped Go version to 1.21.
|
||||
|
||||
### Removed
|
||||
- Removed mentioning and processing of skin uploading as a file, as this functionality was never implemented and was not planned to be implemented
|
||||
- StatsD metrics:
|
||||
- Gauges:
|
||||
- `ely.skinsystem.{hostname}.app.redis.pool.available`
|
||||
|
||||
## [4.6.0] - 2021-03-04
|
||||
### Added
|
||||
- `/profile/{username}` endpoint, which returns a profile and its textures, equivalent of the Mojang's
|
||||
[UUID -> Profile + Skin/Cape endpoint](https://wiki.vg/Mojang_API#UUID_-.3E_Profile_.2B_Skin.2FCape).
|
||||
- `/signature-verification-key.der` and `/signature-verification-key.pem` endpoints, which returns the public key in
|
||||
`DER` or `PEM` formats for signature verification.
|
||||
|
||||
### Fixed
|
||||
- [#28](https://github.com/elyby/chrly/issues/28): Added handling of corrupted data from the Mojang's username to UUID
|
||||
cache.
|
||||
- [#29](https://github.com/elyby/chrly/issues/29): If a previously cached UUID no longer exists,
|
||||
it will be invalidated and re-requested.
|
||||
- Use correct status code for error about empty response from Mojang's API.
|
||||
|
||||
### Changed
|
||||
- **BREAKING**: `/cloaks/{username}` and `/textures/{username}` endpoints will no longer return a cape if there are no
|
||||
textures for the requested username.
|
||||
- All endpoints are now returns `500` status code when an error occurred during request processing.
|
||||
- Increased the response timeout for Mojang's API from 3 to 10 seconds.
|
||||
|
||||
## [4.5.0] - 2020-05-01
|
||||
### Added
|
||||
- [#24](https://github.com/elyby/chrly/issues/24): Implemented a new strategy for the queue in the batch provider of
|
||||
Mojang UUIDs: `full-bus`.
|
||||
- New configuration param `QUEUE_STRATEGY` with the default value `periodic`.
|
||||
- New configuration params: `MOJANG_API_BASE_URL` and `MOJANG_SESSION_SERVER_BASE_URL`, that allow you to spoof
|
||||
Mojang API base addresses.
|
||||
- New health checker, that ensures that response for textures provider from Mojang's API is valid.
|
||||
- `dev` Docker images now have the `--cpuprofile` flag, which allows you to run the program with CPU profiling.
|
||||
- New StatsD metrics:
|
||||
- Gauges:
|
||||
- `ely.skinsystem.{hostname}.app.redis.pool.available`
|
||||
|
||||
### Fixed
|
||||
- Handle the case when there is no textures property in Mojang's response.
|
||||
- Handle `SIGTERM` as a valid stop signal for a graceful shutdown since it's the default stop code for the Docker.
|
||||
- Default connections pool size for Redis.
|
||||
|
||||
### Changed
|
||||
- `ely.skinsystem.{hostname}.app.mojang_textures.usernames.round_time` timer will not be recorded if the iteration was
|
||||
empty.
|
||||
|
||||
## [4.4.1] - 2020-04-24
|
||||
### Added
|
||||
@@ -124,7 +184,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
from the textures link instead.
|
||||
- `hash` field from `POST /api/skins` endpoint.
|
||||
|
||||
[Unreleased]: https://github.com/elyby/chrly/compare/4.4.1...HEAD
|
||||
[Unreleased]: https://github.com/elyby/chrly/compare/4.6.0...HEAD
|
||||
[4.6.0]: https://github.com/elyby/chrly/compare/4.5.0...4.6.0
|
||||
[4.5.0]: https://github.com/elyby/chrly/compare/4.4.1...4.5.0
|
||||
[4.4.1]: https://github.com/elyby/chrly/compare/4.4.0...4.4.1
|
||||
[4.4.0]: https://github.com/elyby/chrly/compare/4.3.0...4.4.0
|
||||
[4.3.0]: https://github.com/elyby/chrly/compare/4.2.3...4.3.0
|
||||
|
||||
29
Dockerfile
29
Dockerfile
@@ -1,14 +1,29 @@
|
||||
FROM alpine:3.9.3
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
FROM golang:1.21-alpine AS builder
|
||||
|
||||
ARG VERSION=unversioned
|
||||
ARG COMMIT=unspecified
|
||||
|
||||
COPY . /build
|
||||
WORKDIR /build
|
||||
RUN go mod download
|
||||
|
||||
RUN CGO_ENABLED=0 \
|
||||
go build \
|
||||
-trimpath \
|
||||
-ldflags "-w -s -X github.com/elyby/chrly/version.version=$VERSION -X github.com/elyby/chrly/version.commit=$COMMIT" \
|
||||
-o chrly \
|
||||
main.go
|
||||
|
||||
FROM alpine:3.19
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
RUN apk add --no-cache ca-certificates
|
||||
|
||||
ENV STORAGE_REDIS_HOST=redis
|
||||
ENV STORAGE_FILESYSTEM_HOST=/data
|
||||
|
||||
COPY docker-entrypoint.sh /usr/local/bin/
|
||||
COPY release/chrly /usr/local/bin/
|
||||
COPY docker-entrypoint.sh /
|
||||
COPY --from=builder /build/chrly /usr/local/bin/chrly
|
||||
|
||||
ENTRYPOINT ["docker-entrypoint.sh"]
|
||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||
CMD ["serve"]
|
||||
|
||||
359
Gopkg.lock
generated
359
Gopkg.lock
generated
@@ -1,359 +0,0 @@
|
||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||
|
||||
|
||||
[[projects]]
|
||||
digest = "1:8855efc2aff3afd6319da41b22a8ca1cfd1698af05a24852c01636ba65b133f0"
|
||||
name = "github.com/SermoDigital/jose"
|
||||
packages = [
|
||||
".",
|
||||
"crypto",
|
||||
"jws",
|
||||
"jwt",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "f6df55f235c24f236d11dbcf665249a59ac2021f"
|
||||
version = "1.1"
|
||||
|
||||
[[projects]]
|
||||
branch = "publish_nil_values"
|
||||
digest = "1:d02c8323070a3d8d8ca039d0d180198ead0a75eac4fb1003af812435a2b391e8"
|
||||
name = "github.com/asaskevich/EventBus"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "33b3bc6a7ddca2f99683c5c3ee86b24f80a7a075"
|
||||
source = "https://github.com/erickskrauch/EventBus.git"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:c7b11da9bf0707e6920e1b361fbbbbe9b277ef3a198377baa4527f6e31049be0"
|
||||
name = "github.com/certifi/gocertifi"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "3fd9e1adb12b72d2f3f82191d49be9b93c69f67c"
|
||||
version = "2017.07.27"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:56c130d885a4aacae1dd9c7b71cfe39912c7ebc1ff7d2b46083c8812996dc43b"
|
||||
name = "github.com/davecgh/go-spew"
|
||||
packages = ["spew"]
|
||||
pruneopts = ""
|
||||
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:2e7c296138d042515eb2995fe58026eaef2c08f660a5f36584faecf34eea3cf0"
|
||||
name = "github.com/etherlabsio/healthcheck"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "dd3d2fd8c3f620a32b7f3cd9b4f0d2f7d0875ab1"
|
||||
version = "2.0.3"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:9f1e571696860f2b4f8a241b43ce91c6085e7aaed849ccca53f590a4dc7b95bd"
|
||||
name = "github.com/fsnotify/fsnotify"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "629574ca2a5df945712d3079857300b5e4da0236"
|
||||
version = "v1.4.2"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:904b0b847f705de43c15e6c8f3dd639044db5601dedfb2f3fdb3021a28491d15"
|
||||
name = "github.com/getsentry/raven-go"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "919484f041ea21e7e27be291cee1d6af7bc98864"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:8c9f13aac9e92f3754ea591b39ada87b9f89f1e75c4b90ccbd0b1084069c436f"
|
||||
name = "github.com/goava/di"
|
||||
packages = [
|
||||
".",
|
||||
"internal/reflection",
|
||||
"internal/stacktrace",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "1eb6eb721bf050edff0efbf15c31636def701b4b"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:65c7ed49d9f36dd4752e43013323fa9229db60b29aa4f5a75aaecda3130c74e2"
|
||||
name = "github.com/gorilla/mux"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "c5c6c98bc25355028a63748a498942a6398ccd22"
|
||||
version = "v1.7.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:5eeb4bfc6db411dbb34a6d9e5d49a9956b160d59fd004ee8f03fe53c9605c082"
|
||||
name = "github.com/h2non/gock"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "ba88c4862a27596539531ce469478a91bc5a0511"
|
||||
version = "v1.0.14"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:0f31ddb2589297fc1d716f45b34e34bff34e968de1aa239543274c87522e86f4"
|
||||
name = "github.com/h2non/parth"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "b4df798d65426f8c8ab5ca5f9987aec5575d26c9"
|
||||
version = "v2.0.1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:8017a99c7fa4dac90c7a34e08d5f890043fc27e91e561f3267a09f65595b158c"
|
||||
name = "github.com/hashicorp/hcl"
|
||||
packages = [
|
||||
".",
|
||||
"hcl/ast",
|
||||
"hcl/parser",
|
||||
"hcl/printer",
|
||||
"hcl/scanner",
|
||||
"hcl/strconv",
|
||||
"hcl/token",
|
||||
"json/parser",
|
||||
"json/scanner",
|
||||
"json/token",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "8f6b1344a92ff8877cf24a5de9177bf7d0a2a187"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:870d441fe217b8e689d7949fef6e43efbc787e50f200cb1e70dbca9204a1d6be"
|
||||
name = "github.com/inconshreveable/mousetrap"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75"
|
||||
version = "v1.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:1ce378ab2352c756c6d7a0172c22ecbd387659d32712a4ce3bc474273309a5dc"
|
||||
name = "github.com/magiconair/properties"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "be5ece7dd465ab0765a9682137865547526d1dfb"
|
||||
version = "v1.7.3"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:19a9f4143462f07553e9bf6ae0f1b8633a2c44763b1df90d4e9e49f51cd8423a"
|
||||
name = "github.com/mediocregopher/radix.v2"
|
||||
packages = [
|
||||
"cluster",
|
||||
"pool",
|
||||
"redis",
|
||||
"util",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "b67df6e626f993b64b3ca9f4b8630900e61002e3"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:c9ede10a9ded782d25d1f0be87c680e11409c23554828f19a19d691a95e76130"
|
||||
name = "github.com/mitchellh/mapstructure"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "d0303fe809921458f417bcf828397a65db30a7e4"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:c62e653e0a78bcf08fd56c764e5725e604693ffbd35b2b283b360f174d073a75"
|
||||
name = "github.com/mono83/slf"
|
||||
packages = [
|
||||
".",
|
||||
"filters",
|
||||
"params",
|
||||
"rays",
|
||||
"recievers",
|
||||
"recievers/sentry",
|
||||
"recievers/statsd",
|
||||
"recievers/writer",
|
||||
"wd",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "79153e9636db86e1c6b74d74dd04176f257a4f2d"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:270261c28f6e71a8a31b9d308ec9145147040fd80d21563307767a8e844bfabc"
|
||||
name = "github.com/mono83/udpwriter"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "a064bd7e3acfda563ea680b913b9ef24b7a73e15"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:049b5bee78dfdc9628ee0e557219c41f683e5b06c5a5f20eaba0105ccc586689"
|
||||
name = "github.com/pelletier/go-buffruneio"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "c37440a7cf42ac63b919c752ca73a85067e05992"
|
||||
version = "v0.2.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:6d5a9728ae27e477a07bb69f02ea0bade74eb8b0c7346d046337904bbf7af065"
|
||||
name = "github.com/pelletier/go-toml"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "5ccdfb18c776b740aecaf085c4d9a2779199c279"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:256484dbbcd271f9ecebc6795b2df8cad4c458dd0f5fd82a8c2fa0c29f233411"
|
||||
name = "github.com/pmezard/go-difflib"
|
||||
packages = ["difflib"]
|
||||
pruneopts = ""
|
||||
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:c189f11a84aa8b868a4b7cd4605653160424ab299cf7cfb1c5bd2740b949928f"
|
||||
name = "github.com/spf13/afero"
|
||||
packages = [
|
||||
".",
|
||||
"mem",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "ee1bd8ee15a1306d1f9201acc41ef39cd9f99a1b"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:6ff9b74bfea2625f805edec59395dc37e4a06458dd3c14e3372337e3d35a2ed6"
|
||||
name = "github.com/spf13/cast"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "acbeb36b902d72a7a4c18e8f3241075e7ab763e4"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:a1403cc8a94b8d7956ee5e9694badef0e7b051af289caad1cf668331e3ffa4f6"
|
||||
name = "github.com/spf13/cobra"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "ef82de70bb3f60c65fb8eebacbb2d122ef517385"
|
||||
version = "v0.0.3"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:5cb42b990db5dc48b8bc23b6ee77b260713ba3244ca495cd1ed89533dc482a49"
|
||||
name = "github.com/spf13/jwalterweatherman"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "12bd96e66386c1960ab0f74ced1362f66f552f7b"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:cbaf13cdbfef0e4734ed8a7504f57fe893d471d62a35b982bf6fb3f036449a66"
|
||||
name = "github.com/spf13/pflag"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "298182f68c66c05229eb03ac171abe6e309ee79a"
|
||||
version = "v1.0.3"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:90fe60ab6f827e308b0c8cc1e11dce8ff1e96a927c8b171271a3cb04dd517606"
|
||||
name = "github.com/spf13/viper"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "9e56dacc08fbbf8c9ee2dbc717553c758ce42bc9"
|
||||
version = "v1.3.2"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:711eebe744c0151a9d09af2315f0bb729b2ec7637ef4c410fa90a18ef74b65b6"
|
||||
name = "github.com/stretchr/objx"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "477a77ecc69700c7cdeb1fa9e129548e1c1c393c"
|
||||
version = "v0.1.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:381bcbeb112a51493d9d998bbba207a529c73dbb49b3fd789e48c63fac1f192c"
|
||||
name = "github.com/stretchr/testify"
|
||||
packages = [
|
||||
"assert",
|
||||
"mock",
|
||||
"require",
|
||||
"suite",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "ffdc059bfe9ce6a4e144ba849dbedead332c6053"
|
||||
version = "v1.3.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:86e6712cfd4070a2120c03fcec41cfcbbc51813504a74e28d74479edfaf669ee"
|
||||
name = "github.com/tevino/abool"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "9b9efcf221b50905aab9bbabd3daed56dc10f339"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:061754b9de261d8e1cf804970dff7b3e105d1cb4883ef446dbe911489ba8e9eb"
|
||||
name = "github.com/thedevsaddam/govalidator"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "0413a0eb80cac8ab2d666639130658ce49a0c967"
|
||||
version = "v1.9.6"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:19adc71218d62052cd18b83ebaab77961378876094081f4b1581ca28ef80395d"
|
||||
name = "golang.org/x/sys"
|
||||
packages = ["unix"]
|
||||
pruneopts = ""
|
||||
revision = "7ddbeae9ae08c6a06a59597f0c9edbc5ff2444ce"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:653926785eac385fd1d61dc16360a5194c68d4bf2541234363a9375d2e88a039"
|
||||
name = "golang.org/x/text"
|
||||
packages = [
|
||||
"internal/gen",
|
||||
"internal/triegen",
|
||||
"internal/ucd",
|
||||
"transform",
|
||||
"unicode/cldr",
|
||||
"unicode/norm",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "bd91bbf73e9a4a801adbfb97133c992678533126"
|
||||
|
||||
[[projects]]
|
||||
branch = "v2"
|
||||
digest = "1:81314a486195626940617e43740b4fa073f265b0715c9f54ce2027fee1cb5f61"
|
||||
name = "gopkg.in/yaml.v2"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "eb3733d160e74a9c7e442f435eb3bea458e1d19f"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
input-imports = [
|
||||
"github.com/SermoDigital/jose/crypto",
|
||||
"github.com/SermoDigital/jose/jws",
|
||||
"github.com/asaskevich/EventBus",
|
||||
"github.com/etherlabsio/healthcheck",
|
||||
"github.com/getsentry/raven-go",
|
||||
"github.com/goava/di",
|
||||
"github.com/gorilla/mux",
|
||||
"github.com/h2non/gock",
|
||||
"github.com/mediocregopher/radix.v2/pool",
|
||||
"github.com/mediocregopher/radix.v2/redis",
|
||||
"github.com/mediocregopher/radix.v2/util",
|
||||
"github.com/mono83/slf",
|
||||
"github.com/mono83/slf/params",
|
||||
"github.com/mono83/slf/rays",
|
||||
"github.com/mono83/slf/recievers/sentry",
|
||||
"github.com/mono83/slf/recievers/statsd",
|
||||
"github.com/mono83/slf/recievers/writer",
|
||||
"github.com/mono83/slf/wd",
|
||||
"github.com/spf13/cobra",
|
||||
"github.com/spf13/viper",
|
||||
"github.com/stretchr/testify/assert",
|
||||
"github.com/stretchr/testify/mock",
|
||||
"github.com/stretchr/testify/require",
|
||||
"github.com/stretchr/testify/suite",
|
||||
"github.com/tevino/abool",
|
||||
"github.com/thedevsaddam/govalidator",
|
||||
]
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
60
Gopkg.toml
60
Gopkg.toml
@@ -1,60 +0,0 @@
|
||||
ignored = ["github.com/elyby/chrly"]
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/gorilla/mux"
|
||||
version = "^1.6.1"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/mediocregopher/radix.v2"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/mono83/slf"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/spf13/cobra"
|
||||
version = "^0.0.3"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/spf13/viper"
|
||||
version = "^1.0.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/getsentry/raven-go"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/SermoDigital/jose"
|
||||
version = "~1.1.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/thedevsaddam/govalidator"
|
||||
version = "^1.9.6"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/tevino/abool"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/asaskevich/EventBus"
|
||||
source = "https://github.com/erickskrauch/EventBus.git"
|
||||
branch = "publish_nil_values"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/etherlabsio/healthcheck"
|
||||
version = "2.0.3"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/goava/di"
|
||||
branch = "master"
|
||||
|
||||
# Testing dependencies
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/stretchr/testify"
|
||||
version = "^1.3.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/h2non/gock"
|
||||
version = "^1.0.6"
|
||||
2
LICENSE
2
LICENSE
@@ -186,7 +186,7 @@
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2018 Ely.by (http://ely.by)
|
||||
Copyright 2023 Ely.by (https://ely.by)
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
145
README.md
145
README.md
@@ -5,7 +5,6 @@
|
||||
[![Coverage][ico-coverage]][link-coverage]
|
||||
[![Keep a Changelog][ico-changelog]](CHANGELOG.md)
|
||||
[![Software License][ico-license]](LICENSE)
|
||||
[![FOSSA Status][ico-fossa]][link-fossa]
|
||||
|
||||
Chrly is a lightweight implementation of Minecraft skins system server with ability to proxy requests to Mojang's
|
||||
skins system. It's packaged and distributed as a Docker image and can be downloaded from
|
||||
@@ -15,8 +14,9 @@ production ready.
|
||||
## Installation
|
||||
|
||||
You can easily install Chrly using [docker-compose](https://docs.docker.com/compose/). The configuration below (save
|
||||
it as `docker-compose.yml`) can be used to start a Chrly server. It relies on `CHRLY_SECRET` environment variable
|
||||
that you must set before running `docker-compose up -d`. Other possible variables are described below.
|
||||
it as `docker-compose.yml`) can be used to start a Chrly server. It relies on `CHRLY_SECRET` and `CHRLY_SIGNING_KEY`
|
||||
environment variables that you must set before running `docker-compose up -d`. Other possible variables are described
|
||||
below.
|
||||
|
||||
```yml
|
||||
version: '2'
|
||||
@@ -33,6 +33,7 @@ services:
|
||||
- "80:80"
|
||||
environment:
|
||||
CHRLY_SECRET: replace_this_value_in_production
|
||||
CHRLY_SIGNING_KEY: base64:LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlCT3dJQkFBSkJBTmJVcFZDWmtNS3BmdllaMDhXM2x1bWRBYVl4TEJubVVEbHpIQlFIM0RwWWVmNVdDTzMyClREVTZmZUlKNThBMGxBeXdndFo0d3dpMmRHSE96LzFoQXZjQ0F3RUFBUUpBSXRheFNIVGU2UEtieUVVLzlweGoKT05kaFlSWXdWTExvNTZnbk1ZaGt5b0VxYWFNc2ZvdjhoaG9lcGtZWkJNdlpGQjJiRE9zUTJTYUorRTJlaUJPNApBUUloQVBzc1MwK0JSOXcwYk9kbWpHcW1kRTlOck41VUpRY09XMTNzMjkrNlF6VUJBaUVBMnZXT2VwQTVBcGl1CnBFQTNwd29HZGtWQ3JOU25uS2pEUXpEWEJucGQzL2NDSUVGTmQ5c1k0cVVHNEZXZFhONlJubVhMN1NqMHVaZkgKRE13enU4ckVNNXNCQWlFQWh2ZG9ETnFMbWJNZHEzYytGc1BTT2VMMWQyMVpwL0pLOGtiUHRGbUhOZjhDSVFEVgo2RlNaRHd2V2Z1eGFNN0JzeWNRT05rakRCVFBOdStscWN0SkJHbkJ2M0E9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=
|
||||
|
||||
redis:
|
||||
image: redis:4.0-32bit
|
||||
@@ -41,6 +42,11 @@ services:
|
||||
- ./data/redis:/data
|
||||
```
|
||||
|
||||
**Tip**: to generate a value for the `CHRLY_SIGNING_KEY` use the command below and then join it with a `base64:` prefix.
|
||||
```sh
|
||||
openssl genrsa 4096 | base64 -w0
|
||||
```
|
||||
|
||||
Chrly uses some volumes to persist storage for capes and Redis database. The configuration above mounts them to
|
||||
the host machine to do not lose data on container recreations.
|
||||
|
||||
@@ -48,11 +54,10 @@ the host machine to do not lose data on container recreations.
|
||||
|
||||
Application's configuration is based on the environment variables. You can adjust config by modifying `environment` key
|
||||
inside your `docker-compose.yml` file. After value will have been changed, container should be stopped and recreated.
|
||||
If environment variables have been changed, Docker will automatically recreate the container, so you only need to `stop`
|
||||
and `up` it:
|
||||
If environment variables have been changed, Docker will automatically recreate the container, so you only need to `up`
|
||||
it again:
|
||||
|
||||
```sh
|
||||
docker-compose stop app
|
||||
docker-compose up -d app
|
||||
```
|
||||
|
||||
@@ -97,6 +102,14 @@ docker-compose up -d app
|
||||
<td>Sentry can be used to collect app errors</td>
|
||||
<td><code>https://public:private@your.sentry.io/1</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>QUEUE_STRATEGY</td>
|
||||
<td>
|
||||
Sets the strategy for the queue in the batch provider of Mojang UUIDs. Allowed values are <code>periodic</code>
|
||||
and <code>full-bus</code> (see <a href="https://github.com/elyby/chrly/issues/24">#24</a>).
|
||||
</td>
|
||||
<td><code>periodic</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>QUEUE_LOOP_DELAY</td>
|
||||
<td>
|
||||
@@ -137,6 +150,20 @@ docker-compose up -d app
|
||||
</td>
|
||||
<td><code>http://remote-provider.com/api/worker/mojang-uuid</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MOJANG_API_BASE_URL</td>
|
||||
<td>
|
||||
Allows you to spoof the Mojang's API server address.
|
||||
</td>
|
||||
<td><code>https://api.mojang.com</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MOJANG_SESSION_SERVER_BASE_URL</td>
|
||||
<td>
|
||||
Allows you to spoof the Mojang's Session server address.
|
||||
</td>
|
||||
<td><code>https://sessionserver.mojang.com</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>TEXTURES_EXTRA_PARAM_NAME</td>
|
||||
<td>
|
||||
@@ -160,7 +187,7 @@ If something goes wrong, you can always access logs by executing `docker-compose
|
||||
|
||||
## Endpoints
|
||||
|
||||
Each endpoint that accepts `username` as a part of an url takes it case insensitive. `.png` part can be omitted too.
|
||||
Each endpoint that accepts `username` as a part of an url takes it case-insensitive. The `.png` postfix can be omitted.
|
||||
|
||||
#### `GET /skins/{username}.png`
|
||||
|
||||
@@ -198,11 +225,76 @@ That request is handy in case when your server implements authentication for a g
|
||||
operation) and you have to respond with hasJoined request with an actual user textures. You have to simply send request
|
||||
to the Chrly server and put the result in your hasJoined response.
|
||||
|
||||
#### `GET /profile/{username}`
|
||||
|
||||
This endpoint behaves exactly like the
|
||||
[Mojang's UUID -> Profile + Skin/Cape endpoint](https://wiki.vg/Mojang_API#UUID_-.3E_Profile_.2B_Skin.2FCape), but using
|
||||
a username instead of the UUID. Just like in the Mojang's API, you can append `?unsigned=false` part to URL to sign
|
||||
the `textures` property. If the textures for the requested username aren't found, it'll request them through the
|
||||
Mojang's API, but the Mojang's signature will be discarded and the textures will be re-signed using the signature key
|
||||
for your Chrly instance.
|
||||
|
||||
Response example:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "0f657aa8bfbe415db7005750090d3af3",
|
||||
"name": "username",
|
||||
"properties": [
|
||||
{
|
||||
"name": "textures",
|
||||
"signature": "textures signature value",
|
||||
"value": "base64 encoded value"
|
||||
},
|
||||
{
|
||||
"name": "chrly",
|
||||
"signature": "custom property signature value",
|
||||
"value": "how do you tame a horse in Minecraft?"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The base64 `value` string for the `textures` property decoded:
|
||||
|
||||
```json
|
||||
{
|
||||
"timestamp": 1614387238630,
|
||||
"profileId": "0f657aa8bfbe415db7005750090d3af3",
|
||||
"profileName": "username",
|
||||
"textures": {
|
||||
"SKIN": {
|
||||
"url": "http://example.com/skin.png"
|
||||
},
|
||||
"CAPE": {
|
||||
"url": "http://example.com/cape.png"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If username can't be found locally and can't be obtained from the Mojang's API, empty response with `204` status code
|
||||
will be sent.
|
||||
|
||||
Note that this endpoint will try to use the UUID for the stored profile in the database. This is an edge case, related
|
||||
to the situation where the user is available in the database but has no textures, which caused them to be retrieved
|
||||
from the Mojang's API.
|
||||
|
||||
#### `GET /signature-verification-key.der`
|
||||
|
||||
This endpoint returns a public key that can be used to verify textures signatures. The key is provided in `DER` format,
|
||||
so it can be used directly in the Authlib, without modifying the signature checking algorithm.
|
||||
|
||||
#### `GET /signature-verification-key.pem`
|
||||
|
||||
The same endpoint as the previous one, except that it returns the key in `PEM` format.
|
||||
|
||||
#### `GET /textures/signed/{username}`
|
||||
|
||||
Actually, it's [Ely.by](http://ely.by) feature called [Server Skins System](http://ely.by/server-skins-system), but if
|
||||
you have your own source of Mojang's signatures, then you can pass it with textures and it'll be displayed in response
|
||||
of this endpoint. Received response should be directly sent to the client without any modification via game server API.
|
||||
Actually, this is the [Ely.by](https://ely.by)'s feature called
|
||||
[Server Skins System](https://ely.by/server-skins-system), but if you have your own source of Mojang's signatures,
|
||||
then you can pass it with textures and it'll be displayed in response of this endpoint. Received response should be
|
||||
directly sent to the client without any modification via game server API.
|
||||
|
||||
Response example:
|
||||
|
||||
@@ -253,10 +345,9 @@ You can obtain token by executing `docker-compose run --rm app token`.
|
||||
|
||||
#### `POST /api/skins`
|
||||
|
||||
> **Warning**: skin uploading via `skin` field is not implemented for now.
|
||||
Endpoint allows you to create or update skin record for a username.
|
||||
|
||||
Endpoint allows you to create or update skin record for a username. To upload skin, you have to send multipart
|
||||
form data. `form-urlencoded` also supported, but, as you may know, it doesn't support files uploading.
|
||||
The request body must be encoded as `application/x-www-form-urlencoded`.
|
||||
|
||||
**Request params:**
|
||||
|
||||
@@ -270,8 +361,9 @@ form data. `form-urlencoded` also supported, but, as you may know, it doesn't su
|
||||
| isSlim | bool | Does skin have slim arms (Alex model). |
|
||||
| mojangTextures | string | Mojang textures field. It must be a base64 encoded json string. Not required. |
|
||||
| mojangSignature | string | Signature for Mojang textures, which is required when `mojangTextures` passed. |
|
||||
| url | string | Actual url of the skin. You have to pass this parameter or `skin`. |
|
||||
| skin | file | Skin file. You have to pass this parameter or `url`. |
|
||||
| url | string | Actual url of the skin. |
|
||||
|
||||
**Important**: all parameters are always read at least as their default values. So, if you only want to update the username and not pass the skin data it will reset all skin information. If you want to keep the data, you should always pass the full set of parameters.
|
||||
|
||||
If successful you'll receive `201` status code. In the case of failure there will be `400` status code and errors list
|
||||
as json:
|
||||
@@ -362,18 +454,15 @@ If any of the checks fails, the server will return `503` status code with the fo
|
||||
First of all you should install the [latest stable version of Go](https://golang.org/doc/install) and set `GOPATH`
|
||||
environment variable.
|
||||
|
||||
This project uses [`dep`](https://github.com/golang/dep) for dependencies management, so it
|
||||
[should be installed](https://github.com/golang/dep#installation) too.
|
||||
|
||||
Then you must fork this repository. Now follow these steps:
|
||||
|
||||
```sh
|
||||
# Get the source code
|
||||
go get github.com/elyby/chrly
|
||||
git clone https://github.com/elyby/chrly.git
|
||||
# Switch to the project folder
|
||||
cd $GOPATH/src/github.com/elyby/chrly
|
||||
# Install dependencies (it can take a while)
|
||||
dep ensure
|
||||
cd chrly
|
||||
# Install dependencies
|
||||
go mod download
|
||||
# Add your fork link as a remote
|
||||
git remote add fork git@github.com:your-username/chrly.git
|
||||
# Create a new branch for your task
|
||||
@@ -401,18 +490,12 @@ If your Redis instance isn't located at the `localhost`, you can change host by
|
||||
After all of that `go run main.go serve` should successfully start the application.
|
||||
To run tests execute `go test ./...`.
|
||||
|
||||
## License
|
||||
[![FOSSA Status][ico-fossa-big]][link-fossa]
|
||||
|
||||
[ico-lang]: https://img.shields.io/badge/lang-go%201.14-blue.svg?style=flat-square
|
||||
[ico-build]: https://img.shields.io/travis/elyby/chrly.svg?style=flat-square
|
||||
[ico-lang]: https://img.shields.io/github/go-mod/go-version/elyby/chrly?style=flat-square
|
||||
[ico-build]: https://img.shields.io/github/actions/workflow/status/elyby/chrly/build.yml?style=flat-square
|
||||
[ico-coverage]: https://img.shields.io/codecov/c/github/elyby/chrly.svg?style=flat-square
|
||||
[ico-changelog]: https://img.shields.io/badge/keep%20a-changelog-orange.svg?style=flat-square
|
||||
[ico-license]: https://img.shields.io/github/license/elyby/chrly.svg?style=flat-square
|
||||
[ico-fossa]: https://app.fossa.io/api/projects/git%2Bgithub.com%2Felyby%2Fchrly.svg?type=shield
|
||||
[ico-fossa-big]: https://app.fossa.io/api/projects/git%2Bgithub.com%2Felyby%2Fchrly.svg?type=large
|
||||
|
||||
[link-go]: https://golang.org
|
||||
[link-build]: https://travis-ci.org/elyby/chrly
|
||||
[link-build]: https://github.com/elyby/chrly/actions
|
||||
[link-coverage]: https://codecov.io/gh/elyby/chrly
|
||||
[link-fossa]: https://app.fossa.io/projects/git%2Bgithub.com%2Felyby%2Fchrly
|
||||
|
||||
@@ -7,25 +7,29 @@ import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var HttpClient = &http.Client{
|
||||
Timeout: 3 * time.Second,
|
||||
Timeout: 10 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
MaxIdleConnsPerHost: 1024,
|
||||
},
|
||||
}
|
||||
|
||||
type SignedTexturesResponse struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Props []*Property `json:"properties"`
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Props []*Property `json:"properties"`
|
||||
|
||||
once sync.Once
|
||||
decodedTextures *TexturesProp
|
||||
decodedErr error
|
||||
}
|
||||
|
||||
func (t *SignedTexturesResponse) DecodeTextures() *TexturesProp {
|
||||
if t.decodedTextures == nil {
|
||||
func (t *SignedTexturesResponse) DecodeTextures() (*TexturesProp, error) {
|
||||
t.once.Do(func() {
|
||||
var texturesProp string
|
||||
for _, prop := range t.Props {
|
||||
if prop.Name == "textures" {
|
||||
@@ -35,14 +39,18 @@ func (t *SignedTexturesResponse) DecodeTextures() *TexturesProp {
|
||||
}
|
||||
|
||||
if texturesProp == "" {
|
||||
return nil
|
||||
return
|
||||
}
|
||||
|
||||
decodedTextures, _ := DecodeTextures(texturesProp)
|
||||
t.decodedTextures = decodedTextures
|
||||
}
|
||||
decodedTextures, err := DecodeTextures(texturesProp)
|
||||
if err != nil {
|
||||
t.decodedErr = err
|
||||
} else {
|
||||
t.decodedTextures = decodedTextures
|
||||
}
|
||||
})
|
||||
|
||||
return t.decodedTextures
|
||||
return t.decodedTextures, t.decodedErr
|
||||
}
|
||||
|
||||
type Property struct {
|
||||
@@ -58,11 +66,17 @@ type ProfileInfo struct {
|
||||
IsDemo bool `json:"demo,omitempty"`
|
||||
}
|
||||
|
||||
var ApiMojangDotComAddr = "https://api.mojang.com"
|
||||
var SessionServerMojangComAddr = "https://sessionserver.mojang.com"
|
||||
|
||||
// Exchanges usernames array to array of uuids
|
||||
// See https://wiki.vg/Mojang_API#Playernames_-.3E_UUIDs
|
||||
func UsernamesToUuids(usernames []string) ([]*ProfileInfo, error) {
|
||||
requestBody, _ := json.Marshal(usernames)
|
||||
request, _ := http.NewRequest("POST", "https://api.mojang.com/profiles/minecraft", bytes.NewBuffer(requestBody))
|
||||
request, err := http.NewRequest("POST", ApiMojangDotComAddr+"/profiles/minecraft", bytes.NewBuffer(requestBody))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
@@ -88,12 +102,15 @@ func UsernamesToUuids(usernames []string) ([]*ProfileInfo, error) {
|
||||
// See https://wiki.vg/Mojang_API#UUID_-.3E_Profile_.2B_Skin.2FCape
|
||||
func UuidToTextures(uuid string, signed bool) (*SignedTexturesResponse, error) {
|
||||
normalizedUuid := strings.ReplaceAll(uuid, "-", "")
|
||||
url := "https://sessionserver.mojang.com/session/minecraft/profile/" + normalizedUuid
|
||||
url := SessionServerMojangComAddr + "/session/minecraft/profile/" + normalizedUuid
|
||||
if signed {
|
||||
url += "?unsigned=false"
|
||||
}
|
||||
|
||||
request, _ := http.NewRequest("GET", url, nil)
|
||||
request, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response, err := HttpClient.Do(request)
|
||||
if err != nil {
|
||||
@@ -149,7 +166,7 @@ type EmptyResponse struct {
|
||||
}
|
||||
|
||||
func (*EmptyResponse) Error() string {
|
||||
return "200: Empty Response"
|
||||
return "204: Empty Response"
|
||||
}
|
||||
|
||||
func (*EmptyResponse) IsMojangError() bool {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/h2non/gock"
|
||||
|
||||
testify "github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -20,7 +21,8 @@ func TestSignedTexturesResponse(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
textures := obj.DecodeTextures()
|
||||
textures, err := obj.DecodeTextures()
|
||||
testify.Nil(t, err)
|
||||
testify.Equal(t, "3e3ee6c35afa48abb61e8cd8c42fc0d9", textures.ProfileID)
|
||||
})
|
||||
|
||||
@@ -30,7 +32,8 @@ func TestSignedTexturesResponse(t *testing.T) {
|
||||
Name: "mock",
|
||||
Props: []*Property{},
|
||||
}
|
||||
textures := obj.DecodeTextures()
|
||||
textures, err := obj.DecodeTextures()
|
||||
testify.Nil(t, err)
|
||||
testify.Nil(t, textures)
|
||||
})
|
||||
}
|
||||
@@ -258,7 +261,7 @@ func TestUuidToTextures(t *testing.T) {
|
||||
result, err := UuidToTextures("4566e69fc90748ee8d71d7ba5aa00d20", false)
|
||||
assert.Nil(result)
|
||||
assert.IsType(&EmptyResponse{}, err)
|
||||
assert.EqualError(err, "200: Empty Response")
|
||||
assert.EqualError(err, "204: Empty Response")
|
||||
assert.Implements((*ResponseError)(nil), err)
|
||||
})
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
. "github.com/goava/di"
|
||||
. "github.com/defval/di"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
|
||||
56
cmd/root_profiling.go
Normal file
56
cmd/root_profiling.go
Normal file
@@ -0,0 +1,56 @@
|
||||
//go:build profiling
|
||||
// +build profiling
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"runtime/pprof"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
var profilePath string
|
||||
RootCmd.PersistentFlags().StringVar(&profilePath, "cpuprofile", "", "enables pprof profiling and sets its output path")
|
||||
|
||||
pprofEnabled := false
|
||||
originalPersistentPreRunE := RootCmd.PersistentPreRunE
|
||||
RootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
|
||||
if profilePath == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
f, err := os.Create(profilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Println("enabling profiling")
|
||||
err = pprof.StartCPUProfile(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pprofEnabled = true
|
||||
|
||||
if originalPersistentPreRunE != nil {
|
||||
return originalPersistentPreRunE(cmd, args)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
originalPersistentPostRun := RootCmd.PersistentPreRun
|
||||
RootCmd.PersistentPostRun = func(cmd *cobra.Command, args []string) {
|
||||
if pprofEnabled {
|
||||
log.Println("shutting down profiling")
|
||||
pprof.StopCPUProfile()
|
||||
}
|
||||
|
||||
if originalPersistentPostRun != nil {
|
||||
originalPersistentPostRun(cmd, args)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,30 +3,30 @@ package redis
|
||||
import (
|
||||
"bytes"
|
||||
"compress/zlib"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mediocregopher/radix.v2/pool"
|
||||
"github.com/mediocregopher/radix.v2/redis"
|
||||
"github.com/mediocregopher/radix.v2/util"
|
||||
"github.com/mediocregopher/radix/v4"
|
||||
|
||||
"github.com/elyby/chrly/model"
|
||||
"github.com/elyby/chrly/mojangtextures"
|
||||
)
|
||||
|
||||
var now = time.Now
|
||||
|
||||
func New(addr string, poolSize int) (*Redis, error) {
|
||||
conn, err := pool.New("tcp", addr, poolSize)
|
||||
func New(ctx context.Context, addr string, poolSize int) (*Redis, error) {
|
||||
client, err := (radix.PoolConfig{Size: poolSize}).New(ctx, "tcp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Redis{
|
||||
pool: conn,
|
||||
client: client,
|
||||
context: ctx,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -34,27 +34,34 @@ const accountIdToUsernameKey = "hash:username-to-account-id" // TODO: this shoul
|
||||
const mojangUsernameToUuidKey = "hash:mojang-username-to-uuid"
|
||||
|
||||
type Redis struct {
|
||||
pool *pool.Pool
|
||||
client radix.Client
|
||||
context context.Context
|
||||
}
|
||||
|
||||
func (db *Redis) FindSkinByUsername(username string) (*model.Skin, error) {
|
||||
conn, err := db.pool.Get()
|
||||
var skin *model.Skin
|
||||
err := db.client.Do(db.context, radix.WithConn("", func(ctx context.Context, conn radix.Conn) error {
|
||||
var err error
|
||||
skin, err = findByUsername(ctx, conn, username)
|
||||
|
||||
return err
|
||||
}))
|
||||
|
||||
return skin, err
|
||||
}
|
||||
|
||||
func findByUsername(ctx context.Context, conn radix.Conn, username string) (*model.Skin, error) {
|
||||
redisKey := buildUsernameKey(username)
|
||||
var encodedResult []byte
|
||||
err := conn.Do(ctx, radix.Cmd(&encodedResult, "GET", redisKey))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer db.pool.Put(conn)
|
||||
|
||||
return findByUsername(username, conn)
|
||||
}
|
||||
|
||||
func findByUsername(username string, conn util.Cmder) (*model.Skin, error) {
|
||||
redisKey := buildUsernameKey(username)
|
||||
response := conn.Cmd("GET", redisKey)
|
||||
if response.IsType(redis.Nil) {
|
||||
if len(encodedResult) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
encodedResult, _ := response.Bytes()
|
||||
result, err := zlibDecode(encodedResult)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -66,62 +73,78 @@ func findByUsername(username string, conn util.Cmder) (*model.Skin, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Some old data causing issues in the production.
|
||||
// TODO: remove after investigation will be finished
|
||||
if skin.Uuid == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
skin.OldUsername = skin.Username
|
||||
|
||||
return skin, nil
|
||||
}
|
||||
|
||||
func (db *Redis) FindSkinByUserId(id int) (*model.Skin, error) {
|
||||
conn, err := db.pool.Get()
|
||||
var skin *model.Skin
|
||||
err := db.client.Do(db.context, radix.WithConn("", func(ctx context.Context, conn radix.Conn) error {
|
||||
var err error
|
||||
skin, err = findByUserId(ctx, conn, id)
|
||||
|
||||
return err
|
||||
}))
|
||||
|
||||
return skin, err
|
||||
}
|
||||
|
||||
func findByUserId(ctx context.Context, conn radix.Conn, id int) (*model.Skin, error) {
|
||||
var username string
|
||||
err := conn.Do(ctx, radix.FlatCmd(&username, "HGET", accountIdToUsernameKey, id))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer db.pool.Put(conn)
|
||||
|
||||
return findByUserId(id, conn)
|
||||
}
|
||||
|
||||
func findByUserId(id int, conn util.Cmder) (*model.Skin, error) {
|
||||
response := conn.Cmd("HGET", accountIdToUsernameKey, id)
|
||||
if response.IsType(redis.Nil) {
|
||||
if username == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
username, err := response.Str()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return findByUsername(username, conn)
|
||||
return findByUsername(ctx, conn, username)
|
||||
}
|
||||
|
||||
func (db *Redis) SaveSkin(skin *model.Skin) error {
|
||||
conn, err := db.pool.Get()
|
||||
return db.client.Do(db.context, radix.WithConn("", func(ctx context.Context, conn radix.Conn) error {
|
||||
return save(ctx, conn, skin)
|
||||
}))
|
||||
}
|
||||
|
||||
func save(ctx context.Context, conn radix.Conn, skin *model.Skin) error {
|
||||
err := conn.Do(ctx, radix.Cmd(nil, "MULTI"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer db.pool.Put(conn)
|
||||
|
||||
return save(skin, conn)
|
||||
}
|
||||
|
||||
func save(skin *model.Skin, conn util.Cmder) error {
|
||||
conn.Cmd("MULTI")
|
||||
|
||||
// If user has changed username, then we must delete his old username record
|
||||
if skin.OldUsername != "" && skin.OldUsername != skin.Username {
|
||||
conn.Cmd("DEL", buildUsernameKey(skin.OldUsername))
|
||||
err = conn.Do(ctx, radix.Cmd(nil, "DEL", buildUsernameKey(skin.OldUsername)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// If this is a new record or if the user has changed username, we set the value in the hash table
|
||||
if skin.OldUsername != "" || skin.OldUsername != skin.Username {
|
||||
conn.Cmd("HSET", accountIdToUsernameKey, skin.UserId, skin.Username)
|
||||
err = conn.Do(ctx, radix.FlatCmd(nil, "HSET", accountIdToUsernameKey, skin.UserId, skin.Username))
|
||||
}
|
||||
|
||||
str, _ := json.Marshal(skin)
|
||||
conn.Cmd("SET", buildUsernameKey(skin.Username), zlibEncode(str))
|
||||
err = conn.Do(ctx, radix.FlatCmd(nil, "SET", buildUsernameKey(skin.Username), zlibEncode(str)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
conn.Cmd("EXEC")
|
||||
err = conn.Do(ctx, radix.Cmd(nil, "EXEC"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
skin.OldUsername = skin.Username
|
||||
|
||||
@@ -129,45 +152,45 @@ func save(skin *model.Skin, conn util.Cmder) error {
|
||||
}
|
||||
|
||||
func (db *Redis) RemoveSkinByUserId(id int) error {
|
||||
conn, err := db.pool.Get()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer db.pool.Put(conn)
|
||||
|
||||
return removeByUserId(id, conn)
|
||||
return db.client.Do(db.context, radix.WithConn("", func(ctx context.Context, conn radix.Conn) error {
|
||||
return removeByUserId(ctx, conn, id)
|
||||
}))
|
||||
}
|
||||
|
||||
func removeByUserId(id int, conn util.Cmder) error {
|
||||
record, err := findByUserId(id, conn)
|
||||
func removeByUserId(ctx context.Context, conn radix.Conn, id int) error {
|
||||
record, err := findByUserId(ctx, conn, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
conn.Cmd("MULTI")
|
||||
|
||||
conn.Cmd("HDEL", accountIdToUsernameKey, id)
|
||||
if record != nil {
|
||||
conn.Cmd("DEL", buildUsernameKey(record.Username))
|
||||
err = conn.Do(ctx, radix.Cmd(nil, "MULTI"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
conn.Cmd("EXEC")
|
||||
err = conn.Do(ctx, radix.FlatCmd(nil, "HDEL", accountIdToUsernameKey, id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
if record != nil {
|
||||
err = conn.Do(ctx, radix.Cmd(nil, "DEL", buildUsernameKey(record.Username)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return conn.Do(ctx, radix.Cmd(nil, "EXEC"))
|
||||
}
|
||||
|
||||
func (db *Redis) RemoveSkinByUsername(username string) error {
|
||||
conn, err := db.pool.Get()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer db.pool.Put(conn)
|
||||
|
||||
return removeByUsername(username, conn)
|
||||
return db.client.Do(db.context, radix.WithConn("", func(ctx context.Context, conn radix.Conn) error {
|
||||
return removeByUsername(ctx, conn, username)
|
||||
}))
|
||||
}
|
||||
|
||||
func removeByUsername(username string, conn util.Cmder) error {
|
||||
record, err := findByUsername(username, conn)
|
||||
func removeByUsername(ctx context.Context, conn radix.Conn, username string) error {
|
||||
record, err := findByUsername(ctx, conn, username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -176,70 +199,92 @@ func removeByUsername(username string, conn util.Cmder) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
conn.Cmd("MULTI")
|
||||
|
||||
conn.Cmd("DEL", buildUsernameKey(record.Username))
|
||||
conn.Cmd("HDEL", accountIdToUsernameKey, record.UserId)
|
||||
|
||||
conn.Cmd("EXEC")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *Redis) GetUuid(username string) (string, error) {
|
||||
conn, err := db.pool.Get()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer db.pool.Put(conn)
|
||||
|
||||
return findMojangUuidByUsername(username, conn)
|
||||
}
|
||||
|
||||
func findMojangUuidByUsername(username string, conn util.Cmder) (string, error) {
|
||||
response := conn.Cmd("HGET", mojangUsernameToUuidKey, strings.ToLower(username))
|
||||
if response.IsType(redis.Nil) {
|
||||
return "", &mojangtextures.ValueNotFound{}
|
||||
}
|
||||
|
||||
data, _ := response.Str()
|
||||
parts := strings.Split(data, ":")
|
||||
timestamp, _ := strconv.ParseInt(parts[1], 10, 64)
|
||||
storedAt := time.Unix(timestamp, 0)
|
||||
if storedAt.Add(time.Hour * 24 * 30).Before(now()) {
|
||||
return "", &mojangtextures.ValueNotFound{}
|
||||
}
|
||||
|
||||
return parts[0], nil
|
||||
}
|
||||
|
||||
func (db *Redis) StoreUuid(username string, uuid string) error {
|
||||
conn, err := db.pool.Get()
|
||||
err = conn.Do(ctx, radix.Cmd(nil, "MULTI"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer db.pool.Put(conn)
|
||||
|
||||
return storeMojangUuid(username, uuid, conn)
|
||||
err = conn.Do(ctx, radix.Cmd(nil, "DEL", buildUsernameKey(record.Username)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = conn.Do(ctx, radix.FlatCmd(nil, "HDEL", accountIdToUsernameKey, record.UserId))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return conn.Do(ctx, radix.Cmd(nil, "EXEC"))
|
||||
}
|
||||
|
||||
func storeMojangUuid(username string, uuid string, conn util.Cmder) error {
|
||||
func (db *Redis) GetUuid(username string) (string, bool, error) {
|
||||
var uuid string
|
||||
var found bool
|
||||
err := db.client.Do(db.context, radix.WithConn("", func(ctx context.Context, conn radix.Conn) error {
|
||||
var err error
|
||||
uuid, found, err = findMojangUuidByUsername(ctx, conn, username)
|
||||
|
||||
return err
|
||||
}))
|
||||
|
||||
return uuid, found, err
|
||||
}
|
||||
|
||||
func findMojangUuidByUsername(ctx context.Context, conn radix.Conn, username string) (string, bool, error) {
|
||||
key := strings.ToLower(username)
|
||||
var result string
|
||||
err := conn.Do(ctx, radix.Cmd(&result, "HGET", mojangUsernameToUuidKey, key))
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
if result == "" {
|
||||
return "", false, nil
|
||||
}
|
||||
|
||||
parts := strings.Split(result, ":")
|
||||
// https://github.com/elyby/chrly/issues/28
|
||||
if len(parts) < 2 {
|
||||
err = conn.Do(ctx, radix.Cmd(nil, "HDEL", mojangUsernameToUuidKey, key))
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
return "", false, fmt.Errorf("got unexpected response from the mojangUsernameToUuid hash: \"%s\"", result)
|
||||
}
|
||||
|
||||
timestamp, _ := strconv.ParseInt(parts[1], 10, 64)
|
||||
storedAt := time.Unix(timestamp, 0)
|
||||
if storedAt.Add(time.Hour * 24 * 30).Before(now()) {
|
||||
err = conn.Do(ctx, radix.Cmd(nil, "HDEL", mojangUsernameToUuidKey, key))
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
return "", false, nil
|
||||
}
|
||||
|
||||
return parts[0], true, nil
|
||||
}
|
||||
|
||||
func (db *Redis) StoreUuid(username string, uuid string) error {
|
||||
return db.client.Do(db.context, radix.WithConn("", func(ctx context.Context, conn radix.Conn) error {
|
||||
return storeMojangUuid(ctx, conn, username, uuid)
|
||||
}))
|
||||
}
|
||||
|
||||
func storeMojangUuid(ctx context.Context, conn radix.Conn, username string, uuid string) error {
|
||||
value := uuid + ":" + strconv.FormatInt(now().Unix(), 10)
|
||||
res := conn.Cmd("HSET", mojangUsernameToUuidKey, strings.ToLower(username), value)
|
||||
if res.IsType(redis.Err) {
|
||||
return res.Err
|
||||
err := conn.Do(ctx, radix.Cmd(nil, "HSET", mojangUsernameToUuidKey, strings.ToLower(username), value))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *Redis) Ping() error {
|
||||
r := db.pool.Cmd("PING")
|
||||
if r.Err != nil {
|
||||
return r.Err
|
||||
}
|
||||
|
||||
return nil
|
||||
return db.client.Do(db.context, radix.Cmd(nil, "PING"))
|
||||
}
|
||||
|
||||
func buildUsernameKey(username string) string {
|
||||
|
||||
@@ -1,34 +1,47 @@
|
||||
// +build redis
|
||||
//go:build redis
|
||||
|
||||
package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"os"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mediocregopher/radix.v2/redis"
|
||||
"github.com/mediocregopher/radix/v4"
|
||||
assert "github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/elyby/chrly/model"
|
||||
"github.com/elyby/chrly/mojangtextures"
|
||||
)
|
||||
|
||||
const redisAddr = "localhost:6379"
|
||||
var redisAddr string
|
||||
|
||||
func init() {
|
||||
host := "localhost"
|
||||
port := 6379
|
||||
if os.Getenv("STORAGE_REDIS_HOST") != "" {
|
||||
host = os.Getenv("STORAGE_REDIS_HOST")
|
||||
}
|
||||
|
||||
if os.Getenv("STORAGE_REDIS_PORT") != "" {
|
||||
port, _ = strconv.Atoi(os.Getenv("STORAGE_REDIS_PORT"))
|
||||
}
|
||||
|
||||
redisAddr = fmt.Sprintf("%s:%d", host, port)
|
||||
}
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
t.Run("should connect", func(t *testing.T) {
|
||||
conn, err := New(redisAddr, 12)
|
||||
conn, err := New(context.Background(), redisAddr, 12)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, conn)
|
||||
internalPool := reflect.ValueOf(conn.pool).Elem().FieldByName("pool")
|
||||
assert.Equal(t, 12, internalPool.Cap())
|
||||
})
|
||||
|
||||
t.Run("should return error", func(t *testing.T) {
|
||||
conn, err := New("localhost:12345", 12) // Use localhost to avoid DNS resolution
|
||||
conn, err := New(context.Background(), "localhost:12345", 12) // Use localhost to avoid DNS resolution
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, conn)
|
||||
})
|
||||
@@ -39,21 +52,30 @@ type redisTestSuite struct {
|
||||
|
||||
Redis *Redis
|
||||
|
||||
cmd func(cmd string, args ...interface{}) *redis.Resp
|
||||
cmd func(cmd string, args ...interface{}) string
|
||||
}
|
||||
|
||||
func (suite *redisTestSuite) SetupSuite() {
|
||||
conn, err := New(redisAddr, 10)
|
||||
ctx := context.Background()
|
||||
conn, err := New(ctx, redisAddr, 10)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot establish connection to redis: %w", err))
|
||||
}
|
||||
|
||||
suite.Redis = conn
|
||||
suite.cmd = conn.pool.Cmd
|
||||
suite.cmd = func(cmd string, args ...interface{}) string {
|
||||
var result string
|
||||
err := suite.Redis.client.Do(ctx, radix.FlatCmd(&result, cmd, args...))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *redisTestSuite) SetupTest() {
|
||||
// Cleanup database before the each test
|
||||
// Cleanup database before each test
|
||||
suite.cmd("FLUSHALL")
|
||||
}
|
||||
|
||||
@@ -85,7 +107,7 @@ func TestRedis(t *testing.T) {
|
||||
* mojangSignature: "mock-mojang-signature"
|
||||
* }
|
||||
*/
|
||||
var skinRecord = []byte{
|
||||
var skinRecord = string([]byte{
|
||||
0x78, 0x9c, 0x5c, 0xce, 0x4b, 0x4a, 0x4, 0x41, 0xc, 0xc6, 0xf1, 0xbb, 0x7c, 0xeb, 0x1a, 0xdb, 0xd6, 0xb2,
|
||||
0x9c, 0xc9, 0xd, 0x5c, 0x88, 0x8b, 0xd1, 0xb5, 0x84, 0x4e, 0xa6, 0xa7, 0xec, 0x7a, 0xc, 0xf5, 0x0, 0x41,
|
||||
0xbc, 0xbb, 0xb4, 0xd2, 0xa, 0x2e, 0xf3, 0xe3, 0x9f, 0x90, 0xf, 0xf4, 0xaa, 0xe5, 0x41, 0x40, 0xa3, 0x41,
|
||||
@@ -96,7 +118,7 @@ var skinRecord = []byte{
|
||||
0xa0, 0x13, 0x87, 0xaa, 0x6, 0x31, 0xbf, 0x71, 0x9a, 0x9f, 0xf5, 0xbd, 0xf5, 0xa2, 0x15, 0x84, 0x98, 0xa7,
|
||||
0x65, 0xf7, 0xa3, 0xbb, 0xb6, 0xf1, 0xd6, 0x1d, 0xfd, 0x9c, 0x78, 0xa5, 0x7f, 0x61, 0xfd, 0x75, 0x83, 0xa7,
|
||||
0x20, 0x2f, 0x7f, 0xff, 0xe2, 0xf3, 0x2b, 0x0, 0x0, 0xff, 0xff, 0x6f, 0xdd, 0x51, 0x71,
|
||||
}
|
||||
})
|
||||
|
||||
func (suite *redisTestSuite) TestFindSkinByUsername() {
|
||||
suite.RunSubTest("exists record", func() {
|
||||
@@ -182,14 +204,11 @@ func (suite *redisTestSuite) TestSaveSkin() {
|
||||
suite.Require().Nil(err)
|
||||
|
||||
usernameResp := suite.cmd("GET", "username:mock")
|
||||
suite.Require().False(usernameResp.IsType(redis.Nil))
|
||||
bytes, _ := usernameResp.Bytes()
|
||||
suite.Require().Equal(skinRecord, bytes)
|
||||
suite.Require().NotEmpty(usernameResp)
|
||||
suite.Require().Equal(skinRecord, usernameResp)
|
||||
|
||||
idResp := suite.cmd("HGET", "hash:username-to-account-id", 1)
|
||||
suite.Require().False(usernameResp.IsType(redis.Nil))
|
||||
str, _ := idResp.Str()
|
||||
suite.Require().Equal("Mock", str)
|
||||
suite.Require().Equal("Mock", idResp)
|
||||
})
|
||||
|
||||
suite.RunSubTest("save exists record with changed username", func() {
|
||||
@@ -211,9 +230,8 @@ func (suite *redisTestSuite) TestSaveSkin() {
|
||||
suite.Require().Nil(err)
|
||||
|
||||
usernameResp := suite.cmd("GET", "username:newmock")
|
||||
suite.Require().False(usernameResp.IsType(redis.Nil))
|
||||
bytes, _ := usernameResp.Bytes()
|
||||
suite.Require().Equal([]byte{
|
||||
suite.Require().NotEmpty(usernameResp)
|
||||
suite.Require().Equal(string([]byte{
|
||||
0x78, 0x9c, 0x5c, 0x8e, 0xcb, 0x4e, 0xc3, 0x40, 0xc, 0x45, 0xff, 0xe5, 0xae, 0xa7, 0x84, 0x40, 0x18, 0x5a,
|
||||
0xff, 0x1, 0xb, 0x60, 0x51, 0x58, 0x23, 0x2b, 0x76, 0xd3, 0x21, 0xf3, 0xa8, 0xe6, 0x21, 0x90, 0x10, 0xff,
|
||||
0x8e, 0x52, 0x14, 0x90, 0xba, 0xf4, 0xd1, 0xf1, 0xd5, 0xf9, 0x42, 0x2b, 0x9a, 0x1f, 0x4, 0xd4, 0x1b, 0xb4,
|
||||
@@ -225,15 +243,14 @@ func (suite *redisTestSuite) TestSaveSkin() {
|
||||
0x42, 0x1a, 0xe7, 0xcd, 0x2f, 0xdd, 0xd4, 0x15, 0xaf, 0xde, 0xde, 0x4d, 0x91, 0x17, 0x74, 0x21, 0x96, 0x3f,
|
||||
0x6e, 0xf0, 0xec, 0xe5, 0xf5, 0x3f, 0xf9, 0xdc, 0xfb, 0xfd, 0x13, 0x0, 0x0, 0xff, 0xff, 0xca, 0xc3, 0x54,
|
||||
0x25,
|
||||
}, bytes)
|
||||
}), usernameResp)
|
||||
|
||||
oldUsernameResp := suite.cmd("GET", "username:mock")
|
||||
suite.Require().True(oldUsernameResp.IsType(redis.Nil))
|
||||
suite.Require().Empty(oldUsernameResp)
|
||||
|
||||
idResp := suite.cmd("HGET", "hash:username-to-account-id", 1)
|
||||
suite.Require().False(usernameResp.IsType(redis.Nil))
|
||||
str, _ := idResp.Str()
|
||||
suite.Require().Equal("NewMock", str)
|
||||
suite.Require().NotEmpty(usernameResp)
|
||||
suite.Require().Equal("NewMock", idResp)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -246,10 +263,10 @@ func (suite *redisTestSuite) TestRemoveSkinByUserId() {
|
||||
suite.Require().Nil(err)
|
||||
|
||||
usernameResp := suite.cmd("GET", "username:mock")
|
||||
suite.Require().True(usernameResp.IsType(redis.Nil))
|
||||
suite.Require().Empty(usernameResp)
|
||||
|
||||
idResp := suite.cmd("HGET", "hash:username-to-account-id", 1)
|
||||
suite.Require().True(idResp.IsType(redis.Nil))
|
||||
suite.Require().Empty(idResp)
|
||||
})
|
||||
|
||||
suite.RunSubTest("exists only id", func() {
|
||||
@@ -259,7 +276,7 @@ func (suite *redisTestSuite) TestRemoveSkinByUserId() {
|
||||
suite.Require().Nil(err)
|
||||
|
||||
idResp := suite.cmd("HGET", "hash:username-to-account-id", 1)
|
||||
suite.Require().True(idResp.IsType(redis.Nil))
|
||||
suite.Require().Empty(idResp)
|
||||
})
|
||||
|
||||
suite.RunSubTest("error when querying skin record", func() {
|
||||
@@ -280,10 +297,10 @@ func (suite *redisTestSuite) TestRemoveSkinByUsername() {
|
||||
suite.Require().Nil(err)
|
||||
|
||||
usernameResp := suite.cmd("GET", "username:mock")
|
||||
suite.Require().True(usernameResp.IsType(redis.Nil))
|
||||
suite.Require().Empty(usernameResp)
|
||||
|
||||
idResp := suite.cmd("HGET", "hash:username-to-account-id", 1)
|
||||
suite.Require().True(idResp.IsType(redis.Nil))
|
||||
suite.Require().Empty(idResp)
|
||||
})
|
||||
|
||||
suite.RunSubTest("exists only username", func() {
|
||||
@@ -293,7 +310,7 @@ func (suite *redisTestSuite) TestRemoveSkinByUsername() {
|
||||
suite.Require().Nil(err)
|
||||
|
||||
usernameResp := suite.cmd("GET", "username:mock")
|
||||
suite.Require().True(usernameResp.IsType(redis.Nil))
|
||||
suite.Require().Empty(usernameResp)
|
||||
})
|
||||
|
||||
suite.RunSubTest("no records", func() {
|
||||
@@ -317,15 +334,30 @@ func (suite *redisTestSuite) TestGetUuid() {
|
||||
fmt.Sprintf("%s:%d", "d3ca513eb3e14946b58047f2bd3530fd", time.Now().Unix()),
|
||||
)
|
||||
|
||||
uuid, err := suite.Redis.GetUuid("Mock")
|
||||
uuid, found, err := suite.Redis.GetUuid("Mock")
|
||||
suite.Require().Nil(err)
|
||||
suite.Require().True(found)
|
||||
suite.Require().Equal("d3ca513eb3e14946b58047f2bd3530fd", uuid)
|
||||
})
|
||||
|
||||
suite.RunSubTest("exists record with empty uuid value", func() {
|
||||
suite.cmd("HSET",
|
||||
"hash:mojang-username-to-uuid",
|
||||
"mock",
|
||||
fmt.Sprintf(":%d", time.Now().Unix()),
|
||||
)
|
||||
|
||||
uuid, found, err := suite.Redis.GetUuid("Mock")
|
||||
suite.Require().Nil(err)
|
||||
suite.Require().True(found)
|
||||
suite.Require().Empty("", uuid)
|
||||
})
|
||||
|
||||
suite.RunSubTest("not exists record", func() {
|
||||
uuid, err := suite.Redis.GetUuid("Mock")
|
||||
uuid, found, err := suite.Redis.GetUuid("Mock")
|
||||
suite.Require().Nil(err)
|
||||
suite.Require().False(found)
|
||||
suite.Require().Empty(uuid)
|
||||
suite.Require().IsType(new(mojangtextures.ValueNotFound), err)
|
||||
})
|
||||
|
||||
suite.RunSubTest("exists, but expired record", func() {
|
||||
@@ -335,24 +367,56 @@ func (suite *redisTestSuite) TestGetUuid() {
|
||||
fmt.Sprintf("%s:%d", "d3ca513eb3e14946b58047f2bd3530fd", time.Now().Add(-1*time.Hour*24*31).Unix()),
|
||||
)
|
||||
|
||||
uuid, err := suite.Redis.GetUuid("Mock")
|
||||
uuid, found, err := suite.Redis.GetUuid("Mock")
|
||||
suite.Require().Empty(uuid)
|
||||
suite.Require().IsType(new(mojangtextures.ValueNotFound), err)
|
||||
suite.Require().False(found)
|
||||
suite.Require().Nil(err)
|
||||
|
||||
resp := suite.cmd("HGET", "hash:mojang-username-to-uuid", "mock")
|
||||
suite.Require().Empty(resp, "should cleanup expired records")
|
||||
})
|
||||
|
||||
suite.RunSubTest("exists, but corrupted record", func() {
|
||||
suite.cmd("HSET",
|
||||
"hash:mojang-username-to-uuid",
|
||||
"mock",
|
||||
"corrupted value",
|
||||
)
|
||||
|
||||
uuid, found, err := suite.Redis.GetUuid("Mock")
|
||||
suite.Require().Empty(uuid)
|
||||
suite.Require().False(found)
|
||||
suite.Require().Error(err, "Got unexpected response from the mojangUsernameToUuid hash: \"corrupted value\"")
|
||||
|
||||
resp := suite.cmd("HGET", "hash:mojang-username-to-uuid", "mock")
|
||||
suite.Require().Empty(resp, "should cleanup expired records")
|
||||
})
|
||||
}
|
||||
|
||||
func (suite *redisTestSuite) TestStoreUuid() {
|
||||
now = func() time.Time {
|
||||
return time.Date(2020, 04, 21, 02, 10, 16, 0, time.UTC)
|
||||
}
|
||||
suite.RunSubTest("store uuid", func() {
|
||||
now = func() time.Time {
|
||||
return time.Date(2020, 04, 21, 02, 10, 16, 0, time.UTC)
|
||||
}
|
||||
|
||||
err := suite.Redis.StoreUuid("Mock", "d3ca513eb3e14946b58047f2bd3530fd")
|
||||
suite.Require().Nil(err)
|
||||
err := suite.Redis.StoreUuid("Mock", "d3ca513eb3e14946b58047f2bd3530fd")
|
||||
suite.Require().Nil(err)
|
||||
|
||||
resp := suite.cmd("HGET", "hash:mojang-username-to-uuid", "mock")
|
||||
suite.Require().False(resp.IsType(redis.Nil))
|
||||
str, _ := resp.Str()
|
||||
suite.Require().Equal(str, "d3ca513eb3e14946b58047f2bd3530fd:1587435016")
|
||||
resp := suite.cmd("HGET", "hash:mojang-username-to-uuid", "mock")
|
||||
suite.Require().Equal(resp, "d3ca513eb3e14946b58047f2bd3530fd:1587435016")
|
||||
})
|
||||
|
||||
suite.RunSubTest("store empty uuid", func() {
|
||||
now = func() time.Time {
|
||||
return time.Date(2020, 04, 21, 02, 10, 16, 0, time.UTC)
|
||||
}
|
||||
|
||||
err := suite.Redis.StoreUuid("Mock", "")
|
||||
suite.Require().Nil(err)
|
||||
|
||||
resp := suite.cmd("HGET", "hash:mojang-username-to-uuid", "mock")
|
||||
suite.Require().Equal(resp, ":1587435016")
|
||||
})
|
||||
}
|
||||
|
||||
func (suite *redisTestSuite) TestPing() {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package di
|
||||
|
||||
import (
|
||||
"github.com/goava/di"
|
||||
"github.com/defval/di"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
|
||||
13
di/db.go
13
di/db.go
@@ -1,10 +1,11 @@
|
||||
package di
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path"
|
||||
|
||||
"github.com/goava/di"
|
||||
"github.com/defval/di"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/elyby/chrly/db/fs"
|
||||
@@ -22,7 +23,7 @@ import (
|
||||
var db = di.Options(
|
||||
di.Provide(newRedis,
|
||||
di.As(new(http.SkinsRepository)),
|
||||
di.As(new(mojangtextures.UuidsStorage)),
|
||||
di.As(new(mojangtextures.UUIDsStorage)),
|
||||
),
|
||||
di.Provide(newFSFactory,
|
||||
di.As(new(http.CapesRepository)),
|
||||
@@ -33,9 +34,10 @@ var db = di.Options(
|
||||
func newRedis(container *di.Container, config *viper.Viper) (*redis.Redis, error) {
|
||||
config.SetDefault("storage.redis.host", "localhost")
|
||||
config.SetDefault("storage.redis.port", 6379)
|
||||
config.SetDefault("storage.redis.poll", 10)
|
||||
config.SetDefault("storage.redis.poolSize", 10)
|
||||
|
||||
conn, err := redis.New(
|
||||
context.Background(),
|
||||
fmt.Sprintf("%s:%d", config.GetString("storage.redis.host"), config.GetInt("storage.redis.port")),
|
||||
config.GetInt("storage.redis.poolSize"),
|
||||
)
|
||||
@@ -66,8 +68,5 @@ func newFSFactory(config *viper.Viper) (*fs.Filesystem, error) {
|
||||
}
|
||||
|
||||
func newMojangSignedTexturesStorage() mojangtextures.TexturesStorage {
|
||||
texturesStorage := mojangtextures.NewInMemoryTexturesStorage()
|
||||
texturesStorage.Start()
|
||||
|
||||
return texturesStorage
|
||||
return mojangtextures.NewInMemoryTexturesStorage()
|
||||
}
|
||||
|
||||
3
di/di.go
3
di/di.go
@@ -1,6 +1,6 @@
|
||||
package di
|
||||
|
||||
import "github.com/goava/di"
|
||||
import "github.com/defval/di"
|
||||
|
||||
func New() (*di.Container, error) {
|
||||
container, err := di.New(
|
||||
@@ -11,6 +11,7 @@ func New() (*di.Container, error) {
|
||||
mojangTextures,
|
||||
handlers,
|
||||
server,
|
||||
signer,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package di
|
||||
|
||||
import (
|
||||
"github.com/goava/di"
|
||||
"github.com/defval/di"
|
||||
"github.com/mono83/slf"
|
||||
|
||||
d "github.com/elyby/chrly/dispatcher"
|
||||
@@ -30,7 +30,7 @@ func enableEventsHandlers(
|
||||
logger slf.Logger,
|
||||
statsReporter slf.StatsReporter,
|
||||
) {
|
||||
// TODO: use idea from https://github.com/goava/di/issues/10#issuecomment-615869852
|
||||
// TODO: use idea from https://github.com/defval/di/issues/10#issuecomment-615869852
|
||||
(&eventsubscribers.Logger{Logger: logger}).ConfigureWithDispatcher(dispatcher)
|
||||
(&eventsubscribers.StatsReporter{StatsReporter: statsReporter}).ConfigureWithDispatcher(dispatcher)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package di
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/etherlabsio/healthcheck"
|
||||
"github.com/goava/di"
|
||||
"github.com/defval/di"
|
||||
"github.com/etherlabsio/healthcheck/v2"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
@@ -74,10 +75,15 @@ func newHandlerFactory(
|
||||
mount(router, "/api", apiRouter)
|
||||
}
|
||||
|
||||
err := container.Invoke(enableReporters)
|
||||
if err != nil && !errors.Is(err, di.ErrTypeNotExists) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Resolve health checkers last, because all the services required by the application
|
||||
// must first be initialized and each of them can publish its own checkers
|
||||
var healthCheckers []*namedHealthChecker
|
||||
if container.Has(&healthCheckers) {
|
||||
if has, _ := container.Has(&healthCheckers); has {
|
||||
if err := container.Resolve(&healthCheckers); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -99,23 +105,29 @@ func newSkinsystemHandler(
|
||||
skinsRepository SkinsRepository,
|
||||
capesRepository CapesRepository,
|
||||
mojangTexturesProvider MojangTexturesProvider,
|
||||
) *mux.Router {
|
||||
texturesSigner TexturesSigner,
|
||||
) (*mux.Router, error) {
|
||||
config.SetDefault("textures.extra_param_name", "chrly")
|
||||
config.SetDefault("textures.extra_param_value", "how do you tame a horse in Minecraft?")
|
||||
|
||||
return (&Skinsystem{
|
||||
Emitter: emitter,
|
||||
SkinsRepo: skinsRepository,
|
||||
CapesRepo: capesRepository,
|
||||
MojangTexturesProvider: mojangTexturesProvider,
|
||||
TexturesExtraParamName: config.GetString("textures.extra_param_name"),
|
||||
TexturesExtraParamValue: config.GetString("textures.extra_param_value"),
|
||||
}).Handler()
|
||||
app, err := NewSkinsystem(
|
||||
emitter,
|
||||
skinsRepository,
|
||||
capesRepository,
|
||||
mojangTexturesProvider,
|
||||
texturesSigner,
|
||||
config.GetString("textures.extra_param_name"),
|
||||
config.GetString("textures.extra_param_value"),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return app.Handler(), nil
|
||||
}
|
||||
|
||||
func newApiHandler(emitter Emitter, skinsRepository SkinsRepository) *mux.Router {
|
||||
func newApiHandler(skinsRepository SkinsRepository) *mux.Router {
|
||||
return (&Api{
|
||||
Emitter: emitter,
|
||||
SkinsRepo: skinsRepository,
|
||||
}).Handler()
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ package di
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/defval/di"
|
||||
"github.com/getsentry/raven-go"
|
||||
"github.com/goava/di"
|
||||
"github.com/mono83/slf"
|
||||
"github.com/mono83/slf/rays"
|
||||
"github.com/mono83/slf/recievers/sentry"
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/mono83/slf/wd"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/elyby/chrly/eventsubscribers"
|
||||
"github.com/elyby/chrly/version"
|
||||
)
|
||||
|
||||
@@ -95,3 +96,9 @@ func newStatsReporter(config *viper.Viper) (slf.StatsReporter, error) {
|
||||
|
||||
return wd.Custom("", "", dispatcher), nil
|
||||
}
|
||||
|
||||
func enableReporters(reporter slf.StatsReporter, factories []eventsubscribers.Reporter) {
|
||||
for _, factory := range factories {
|
||||
factory.Enable(reporter)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,58 @@
|
||||
package di
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/goava/di"
|
||||
"github.com/defval/di"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/elyby/chrly/api/mojang"
|
||||
es "github.com/elyby/chrly/eventsubscribers"
|
||||
"github.com/elyby/chrly/http"
|
||||
"github.com/elyby/chrly/mojangtextures"
|
||||
)
|
||||
|
||||
var mojangTextures = di.Options(
|
||||
di.Invoke(interceptMojangApiUrls),
|
||||
di.Provide(newMojangTexturesProviderFactory),
|
||||
di.Provide(newMojangTexturesProvider),
|
||||
di.Provide(newMojangTexturesUuidsProviderFactory),
|
||||
di.Provide(newMojangTexturesBatchUUIDsProvider),
|
||||
di.Provide(newMojangTexturesBatchUUIDsProviderStrategyFactory),
|
||||
di.Provide(newMojangTexturesBatchUUIDsProviderDelayedStrategy),
|
||||
di.Provide(newMojangTexturesBatchUUIDsProviderFullBusStrategy),
|
||||
di.Provide(newMojangTexturesRemoteUUIDsProvider),
|
||||
di.Provide(newMojangSignedTexturesProvider),
|
||||
di.Provide(newMojangTexturesStorageFactory),
|
||||
)
|
||||
|
||||
func interceptMojangApiUrls(config *viper.Viper) error {
|
||||
apiUrl := config.GetString("mojang.api_base_url")
|
||||
if apiUrl != "" {
|
||||
u, err := url.ParseRequestURI(apiUrl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mojang.ApiMojangDotComAddr = u.String()
|
||||
}
|
||||
|
||||
sessionServerUrl := config.GetString("mojang.session_server_base_url")
|
||||
if sessionServerUrl != "" {
|
||||
u, err := url.ParseRequestURI(apiUrl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mojang.SessionServerMojangComAddr = u.String()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func newMojangTexturesProviderFactory(
|
||||
container *di.Container,
|
||||
config *viper.Viper,
|
||||
@@ -75,7 +105,7 @@ func newMojangTexturesUuidsProviderFactory(
|
||||
|
||||
func newMojangTexturesBatchUUIDsProvider(
|
||||
container *di.Container,
|
||||
config *viper.Viper,
|
||||
strategy mojangtextures.BatchUuidsProviderStrategy,
|
||||
emitter mojangtextures.Emitter,
|
||||
) (*mojangtextures.BatchUuidsProvider, error) {
|
||||
if err := container.Provide(func(emitter es.Subscriber, config *viper.Viper) *namedHealthChecker {
|
||||
@@ -106,17 +136,60 @@ func newMojangTexturesBatchUUIDsProvider(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mojangtextures.NewBatchUuidsProvider(context.Background(), strategy, emitter), nil
|
||||
}
|
||||
|
||||
func newMojangTexturesBatchUUIDsProviderStrategyFactory(
|
||||
container *di.Container,
|
||||
config *viper.Viper,
|
||||
) (mojangtextures.BatchUuidsProviderStrategy, error) {
|
||||
config.SetDefault("queue.strategy", "periodic")
|
||||
|
||||
strategyName := config.GetString("queue.strategy")
|
||||
switch strategyName {
|
||||
case "periodic":
|
||||
var strategy *mojangtextures.PeriodicStrategy
|
||||
err := container.Resolve(&strategy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return strategy, nil
|
||||
case "full-bus":
|
||||
var strategy *mojangtextures.FullBusStrategy
|
||||
err := container.Resolve(&strategy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return strategy, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown queue strategy \"%s\"", strategyName)
|
||||
}
|
||||
}
|
||||
|
||||
func newMojangTexturesBatchUUIDsProviderDelayedStrategy(config *viper.Viper) *mojangtextures.PeriodicStrategy {
|
||||
config.SetDefault("queue.loop_delay", 2*time.Second+500*time.Millisecond)
|
||||
config.SetDefault("queue.batch_size", 10)
|
||||
|
||||
return &mojangtextures.BatchUuidsProvider{
|
||||
Emitter: emitter,
|
||||
IterationDelay: config.GetDuration("queue.loop_delay"),
|
||||
IterationSize: config.GetInt("queue.batch_size"),
|
||||
}, nil
|
||||
return mojangtextures.NewPeriodicStrategy(
|
||||
config.GetDuration("queue.loop_delay"),
|
||||
config.GetInt("queue.batch_size"),
|
||||
)
|
||||
}
|
||||
|
||||
func newMojangTexturesBatchUUIDsProviderFullBusStrategy(config *viper.Viper) *mojangtextures.FullBusStrategy {
|
||||
config.SetDefault("queue.loop_delay", 2*time.Second+500*time.Millisecond)
|
||||
config.SetDefault("queue.batch_size", 10)
|
||||
|
||||
return mojangtextures.NewFullBusStrategy(
|
||||
config.GetDuration("queue.loop_delay"),
|
||||
config.GetInt("queue.batch_size"),
|
||||
)
|
||||
}
|
||||
|
||||
func newMojangTexturesRemoteUUIDsProvider(
|
||||
container *di.Container,
|
||||
config *viper.Viper,
|
||||
emitter mojangtextures.Emitter,
|
||||
) (*mojangtextures.RemoteApiUuidsProvider, error) {
|
||||
@@ -125,6 +198,20 @@ func newMojangTexturesRemoteUUIDsProvider(
|
||||
return nil, fmt.Errorf("unable to parse remote url: %w", err)
|
||||
}
|
||||
|
||||
if err := container.Provide(func(emitter es.Subscriber, config *viper.Viper) *namedHealthChecker {
|
||||
config.SetDefault("healthcheck.mojang_api_textures_provider_cool_down_duration", time.Minute+10*time.Second)
|
||||
|
||||
return &namedHealthChecker{
|
||||
Name: "mojang-api-textures-provider-response-checker",
|
||||
Checker: es.MojangApiTexturesProviderResponseChecker(
|
||||
emitter,
|
||||
config.GetDuration("healthcheck.mojang_api_textures_provider_cool_down_duration"),
|
||||
),
|
||||
}
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &mojangtextures.RemoteApiUuidsProvider{
|
||||
Emitter: emitter,
|
||||
Url: *remoteUrl,
|
||||
@@ -138,11 +225,11 @@ func newMojangSignedTexturesProvider(emitter mojangtextures.Emitter) mojangtextu
|
||||
}
|
||||
|
||||
func newMojangTexturesStorageFactory(
|
||||
uuidsStorage mojangtextures.UuidsStorage,
|
||||
uuidsStorage mojangtextures.UUIDsStorage,
|
||||
texturesStorage mojangtextures.TexturesStorage,
|
||||
) mojangtextures.Storage {
|
||||
return &mojangtextures.SeparatedStorage{
|
||||
UuidsStorage: uuidsStorage,
|
||||
UUIDsStorage: uuidsStorage,
|
||||
TexturesStorage: texturesStorage,
|
||||
}
|
||||
}
|
||||
|
||||
20
di/server.go
20
di/server.go
@@ -4,10 +4,11 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"runtime/debug"
|
||||
"time"
|
||||
|
||||
"github.com/defval/di"
|
||||
"github.com/getsentry/raven-go"
|
||||
"github.com/goava/di"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
. "github.com/elyby/chrly/http"
|
||||
@@ -42,13 +43,26 @@ func newServer(params serverParams) *http.Server {
|
||||
params.Config.SetDefault("server.host", "")
|
||||
params.Config.SetDefault("server.port", 80)
|
||||
|
||||
handler := params.Handler
|
||||
var handler http.Handler
|
||||
if params.Sentry != nil {
|
||||
// raven.Recoverer uses DefaultClient and nothing can be done about it
|
||||
// To avoid code duplication, if the Sentry service is successfully initiated,
|
||||
// it will also replace DefaultClient, so raven.Recoverer will work with the instance
|
||||
// created in the application constructor
|
||||
handler = raven.Recoverer(handler)
|
||||
handler = raven.Recoverer(params.Handler)
|
||||
} else {
|
||||
// Raven's Recoverer is prints the stacktrace and sets the corresponding status itself.
|
||||
// But there is no magic and if you don't define a panic handler, Mux will just reset the connection
|
||||
handler = http.HandlerFunc(func(request http.ResponseWriter, response *http.Request) {
|
||||
defer func() {
|
||||
if recovered := recover(); recovered != nil {
|
||||
debug.PrintStack() // TODO: colorize output
|
||||
request.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}()
|
||||
|
||||
params.Handler.ServeHTTP(request, response)
|
||||
})
|
||||
}
|
||||
|
||||
address := fmt.Sprintf("%s:%d", params.Config.GetString("server.host"), params.Config.GetInt("server.port"))
|
||||
|
||||
48
di/signer.go
Normal file
48
di/signer.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package di
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"github.com/elyby/chrly/http"
|
||||
. "github.com/elyby/chrly/signer"
|
||||
"strings"
|
||||
|
||||
"github.com/defval/di"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var signer = di.Options(
|
||||
di.Provide(newTexturesSigner,
|
||||
di.As(new(http.TexturesSigner)),
|
||||
),
|
||||
)
|
||||
|
||||
func newTexturesSigner(config *viper.Viper) (*Signer, error) {
|
||||
keyStr := config.GetString("chrly.signing.key")
|
||||
if keyStr == "" {
|
||||
return nil, errors.New("chrly.signing.key must be set in order to sign textures")
|
||||
}
|
||||
|
||||
var keyBytes []byte
|
||||
if strings.HasPrefix(keyStr, "base64:") {
|
||||
base64Value := keyStr[7:]
|
||||
decodedKey, err := base64.URLEncoding.DecodeString(base64Value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
keyBytes = decodedKey
|
||||
} else {
|
||||
keyBytes = []byte(keyStr)
|
||||
}
|
||||
|
||||
rawPem, _ := pem.Decode(keyBytes)
|
||||
key, err := x509.ParsePKCS1PrivateKey(rawPem.Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Signer{Key: key}, nil
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/etherlabsio/healthcheck"
|
||||
"github.com/etherlabsio/healthcheck/v2"
|
||||
|
||||
"github.com/elyby/chrly/api/mojang"
|
||||
)
|
||||
@@ -32,33 +32,16 @@ func DatabaseChecker(connection Pingable) healthcheck.CheckerFunc {
|
||||
}
|
||||
|
||||
func MojangBatchUuidsProviderResponseChecker(dispatcher Subscriber, resetDuration time.Duration) healthcheck.CheckerFunc {
|
||||
var mutex sync.Mutex
|
||||
var lastCallErr error
|
||||
var expireTimer *time.Timer
|
||||
errHolder := &expiringErrHolder{D: resetDuration}
|
||||
dispatcher.Subscribe(
|
||||
"mojang_textures:batch_uuids_provider:result",
|
||||
func(usernames []string, profiles []*mojang.ProfileInfo, err error) {
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
|
||||
lastCallErr = err
|
||||
if expireTimer != nil {
|
||||
expireTimer.Stop()
|
||||
}
|
||||
|
||||
expireTimer = time.AfterFunc(resetDuration, func() {
|
||||
mutex.Lock()
|
||||
lastCallErr = nil
|
||||
mutex.Unlock()
|
||||
})
|
||||
errHolder.Set(err)
|
||||
},
|
||||
)
|
||||
|
||||
return func(ctx context.Context) error {
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
|
||||
return lastCallErr
|
||||
return errHolder.Get()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,3 +65,47 @@ func MojangBatchUuidsProviderQueueLengthChecker(dispatcher Subscriber, maxLength
|
||||
return errors.New("the maximum number of tasks in the queue has been exceeded")
|
||||
}
|
||||
}
|
||||
|
||||
func MojangApiTexturesProviderResponseChecker(dispatcher Subscriber, resetDuration time.Duration) healthcheck.CheckerFunc {
|
||||
errHolder := &expiringErrHolder{D: resetDuration}
|
||||
dispatcher.Subscribe(
|
||||
"mojang_textures:mojang_api_textures_provider:after_request",
|
||||
func(uuid string, profile *mojang.SignedTexturesResponse, err error) {
|
||||
errHolder.Set(err)
|
||||
},
|
||||
)
|
||||
|
||||
return func(ctx context.Context) error {
|
||||
return errHolder.Get()
|
||||
}
|
||||
}
|
||||
|
||||
type expiringErrHolder struct {
|
||||
D time.Duration
|
||||
err error
|
||||
l sync.Mutex
|
||||
t *time.Timer
|
||||
}
|
||||
|
||||
func (h *expiringErrHolder) Get() error {
|
||||
h.l.Lock()
|
||||
defer h.l.Unlock()
|
||||
|
||||
return h.err
|
||||
}
|
||||
|
||||
func (h *expiringErrHolder) Set(err error) {
|
||||
h.l.Lock()
|
||||
defer h.l.Unlock()
|
||||
if h.t != nil {
|
||||
h.t.Stop()
|
||||
h.t = nil
|
||||
}
|
||||
|
||||
h.err = err
|
||||
if err != nil {
|
||||
h.t = time.AfterFunc(h.D, func() {
|
||||
h.Set(nil)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,9 @@ func TestDatabaseChecker(t *testing.T) {
|
||||
waitChan := make(chan time.Time, 1)
|
||||
p.On("Ping").WaitUntil(waitChan).Return(nil)
|
||||
|
||||
ctx, _ := context.WithTimeout(context.Background(), 0)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 0)
|
||||
defer cancel()
|
||||
|
||||
checker := DatabaseChecker(p)
|
||||
assert.Errorf(t, checker(ctx), "check timeout")
|
||||
close(waitChan)
|
||||
@@ -56,7 +58,7 @@ func TestMojangBatchUuidsProviderChecker(t *testing.T) {
|
||||
checker := MojangBatchUuidsProviderResponseChecker(d, time.Millisecond)
|
||||
assert.Nil(t, checker(context.Background()))
|
||||
})
|
||||
//
|
||||
|
||||
t.Run("when no error occurred", func(t *testing.T) {
|
||||
d := dispatcher.New()
|
||||
checker := MojangBatchUuidsProviderResponseChecker(d, time.Millisecond)
|
||||
@@ -107,3 +109,40 @@ func TestMojangBatchUuidsProviderQueueLengthChecker(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMojangApiTexturesProviderResponseChecker(t *testing.T) {
|
||||
t.Run("empty state", func(t *testing.T) {
|
||||
d := dispatcher.New()
|
||||
checker := MojangApiTexturesProviderResponseChecker(d, time.Millisecond)
|
||||
assert.Nil(t, checker(context.Background()))
|
||||
})
|
||||
|
||||
t.Run("when no error occurred", func(t *testing.T) {
|
||||
d := dispatcher.New()
|
||||
checker := MojangApiTexturesProviderResponseChecker(d, time.Millisecond)
|
||||
d.Emit("mojang_textures:mojang_api_textures_provider:after_request",
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
&mojang.SignedTexturesResponse{},
|
||||
nil,
|
||||
)
|
||||
assert.Nil(t, checker(context.Background()))
|
||||
})
|
||||
|
||||
t.Run("when error occurred", func(t *testing.T) {
|
||||
d := dispatcher.New()
|
||||
checker := MojangApiTexturesProviderResponseChecker(d, time.Millisecond)
|
||||
err := errors.New("some error occurred")
|
||||
d.Emit("mojang_textures:mojang_api_textures_provider:after_request", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", nil, err)
|
||||
assert.Equal(t, err, checker(context.Background()))
|
||||
})
|
||||
|
||||
t.Run("should reset value after passed duration", func(t *testing.T) {
|
||||
d := dispatcher.New()
|
||||
checker := MojangApiTexturesProviderResponseChecker(d, 20*time.Millisecond)
|
||||
err := errors.New("some error occurred")
|
||||
d.Emit("mojang_textures:mojang_api_textures_provider:after_request", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", nil, err)
|
||||
assert.Equal(t, err, checker(context.Background()))
|
||||
time.Sleep(40 * time.Millisecond)
|
||||
assert.Nil(t, checker(context.Background()))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -19,6 +19,17 @@ type StatsReporter struct {
|
||||
timersMutex sync.Mutex
|
||||
}
|
||||
|
||||
type Reporter interface {
|
||||
Enable(reporter slf.StatsReporter)
|
||||
}
|
||||
|
||||
type ReporterFunc func(reporter slf.StatsReporter)
|
||||
|
||||
func (f ReporterFunc) Enable(reporter slf.StatsReporter) {
|
||||
f(reporter)
|
||||
}
|
||||
|
||||
// TODO: rework all reporters in the same style as it was there: https://github.com/elyby/chrly/blob/1543e98b/di/db.go#L48-L52
|
||||
func (s *StatsReporter) ConfigureWithDispatcher(d Subscriber) {
|
||||
s.timersMap = make(map[string]time.Time)
|
||||
|
||||
@@ -34,15 +45,15 @@ func (s *StatsReporter) ConfigureWithDispatcher(d Subscriber) {
|
||||
|
||||
// Mojang signed textures source events
|
||||
d.Subscribe("mojang_textures:call", s.incCounterHandler("mojang_textures.request"))
|
||||
d.Subscribe("mojang_textures:usernames:after_cache", func(username string, uuid string, err error) {
|
||||
if err != nil {
|
||||
d.Subscribe("mojang_textures:usernames:after_cache", func(username string, uuid string, found bool, err error) {
|
||||
if err != nil || !found {
|
||||
return
|
||||
}
|
||||
|
||||
if uuid == "" {
|
||||
s.IncCounter("mojang_textures:usernames:cache_hit_nil", 1)
|
||||
s.IncCounter("mojang_textures.usernames.cache_hit_nil", 1)
|
||||
} else {
|
||||
s.IncCounter("mojang_textures:usernames:cache_hit", 1)
|
||||
s.IncCounter("mojang_textures.usernames.cache_hit", 1)
|
||||
}
|
||||
})
|
||||
d.Subscribe("mojang_textures:textures:after_cache", func(uuid string, textures *mojang.SignedTexturesResponse, err error) {
|
||||
@@ -96,12 +107,12 @@ func (s *StatsReporter) ConfigureWithDispatcher(d Subscriber) {
|
||||
d.Subscribe("mojang_textures:batch_uuids_provider:round", func(usernames []string, queueSize int) {
|
||||
s.UpdateGauge("mojang_textures.usernames.iteration_size", int64(len(usernames)))
|
||||
s.UpdateGauge("mojang_textures.usernames.queue_size", int64(queueSize))
|
||||
if len(usernames) != 0 {
|
||||
s.startTimeRecording("batch_uuids_provider_round_time_" + strings.Join(usernames, "|"))
|
||||
}
|
||||
})
|
||||
d.Subscribe("mojang_textures:batch_uuids_provider:before_round", func() {
|
||||
s.startTimeRecording("batch_uuids_provider_round_time")
|
||||
})
|
||||
d.Subscribe("mojang_textures:batch_uuids_provider:after_round", func() {
|
||||
s.finalizeTimeRecording("batch_uuids_provider_round_time", "mojang_textures.usernames.round_time")
|
||||
d.Subscribe("mojang_textures:batch_uuids_provider:result", func(usernames []string, profiles []*mojang.ProfileInfo, err error) {
|
||||
s.finalizeTimeRecording("batch_uuids_provider_round_time_"+strings.Join(usernames, "|"), "mojang_textures.usernames.round_time")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -121,6 +132,8 @@ func (s *StatsReporter) handleBeforeRequest(req *http.Request) {
|
||||
key = "signed_textures.request"
|
||||
} else if strings.HasPrefix(p, "/textures/") {
|
||||
key = "textures.request"
|
||||
} else if strings.HasPrefix(p, "/profile/") {
|
||||
key = "profiles.request"
|
||||
} else if m == http.MethodPost && p == "/api/skins" {
|
||||
key = "api.skins.post.request"
|
||||
} else if m == http.MethodDelete && strings.HasPrefix(p, "/api/skins/") {
|
||||
|
||||
@@ -98,6 +98,14 @@ var statsReporterTestCases = []*StatsReporterTestCase{
|
||||
{"IncCounter", "signed_textures.request", int64(1)},
|
||||
},
|
||||
},
|
||||
{
|
||||
Events: [][]interface{}{
|
||||
{"skinsystem:before_request", httptest.NewRequest("GET", "http://localhost/profile/username", nil)},
|
||||
},
|
||||
ExpectedCalls: [][]interface{}{
|
||||
{"IncCounter", "profiles.request", int64(1)},
|
||||
},
|
||||
},
|
||||
{
|
||||
Events: [][]interface{}{
|
||||
{"skinsystem:before_request", httptest.NewRequest("POST", "http://localhost/api/skins", nil)},
|
||||
@@ -213,24 +221,30 @@ var statsReporterTestCases = []*StatsReporterTestCase{
|
||||
},
|
||||
{
|
||||
Events: [][]interface{}{
|
||||
{"mojang_textures:usernames:after_cache", "username", "", errors.New("error")},
|
||||
{"mojang_textures:usernames:after_cache", "username", "", false, errors.New("error")},
|
||||
},
|
||||
ExpectedCalls: [][]interface{}{},
|
||||
},
|
||||
{
|
||||
Events: [][]interface{}{
|
||||
{"mojang_textures:usernames:after_cache", "username", "", nil},
|
||||
{"mojang_textures:usernames:after_cache", "username", "", false, nil},
|
||||
},
|
||||
ExpectedCalls: [][]interface{}{},
|
||||
},
|
||||
{
|
||||
Events: [][]interface{}{
|
||||
{"mojang_textures:usernames:after_cache", "username", "", true, nil},
|
||||
},
|
||||
ExpectedCalls: [][]interface{}{
|
||||
{"IncCounter", "mojang_textures:usernames:cache_hit_nil", int64(1)},
|
||||
{"IncCounter", "mojang_textures.usernames.cache_hit_nil", int64(1)},
|
||||
},
|
||||
},
|
||||
{
|
||||
Events: [][]interface{}{
|
||||
{"mojang_textures:usernames:after_cache", "username", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", nil},
|
||||
{"mojang_textures:usernames:after_cache", "username", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", true, nil},
|
||||
},
|
||||
ExpectedCalls: [][]interface{}{
|
||||
{"IncCounter", "mojang_textures:usernames:cache_hit", int64(1)},
|
||||
{"IncCounter", "mojang_textures.usernames.cache_hit", int64(1)},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -337,19 +351,24 @@ var statsReporterTestCases = []*StatsReporterTestCase{
|
||||
{
|
||||
Events: [][]interface{}{
|
||||
{"mojang_textures:batch_uuids_provider:round", []string{"username1", "username2"}, 5},
|
||||
{"mojang_textures:batch_uuids_provider:result", []string{"username1", "username2"}, []*mojang.ProfileInfo{}, nil},
|
||||
},
|
||||
ExpectedCalls: [][]interface{}{
|
||||
{"UpdateGauge", "mojang_textures.usernames.iteration_size", int64(2)},
|
||||
{"UpdateGauge", "mojang_textures.usernames.queue_size", int64(5)},
|
||||
{"RecordTimer", "mojang_textures.usernames.round_time", mock.AnythingOfType("time.Duration")},
|
||||
},
|
||||
},
|
||||
{
|
||||
Events: [][]interface{}{
|
||||
{"mojang_textures:batch_uuids_provider:before_round"},
|
||||
{"mojang_textures:batch_uuids_provider:after_round"},
|
||||
{"mojang_textures:batch_uuids_provider:round", []string{}, 0},
|
||||
// This event will be not emitted, but we emit it to ensure, that RecordTimer will not be called
|
||||
{"mojang_textures:batch_uuids_provider:result", []string{}, []*mojang.ProfileInfo{}, nil},
|
||||
},
|
||||
ExpectedCalls: [][]interface{}{
|
||||
{"RecordTimer", "mojang_textures.usernames.round_time", mock.AnythingOfType("time.Duration")},
|
||||
{"UpdateGauge", "mojang_textures.usernames.iteration_size", int64(0)},
|
||||
{"UpdateGauge", "mojang_textures.usernames.queue_size", int64(0)},
|
||||
// Should not call RecordTimer
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
55
go.mod
Normal file
55
go.mod
Normal file
@@ -0,0 +1,55 @@
|
||||
module github.com/elyby/chrly
|
||||
|
||||
go 1.21
|
||||
|
||||
replace github.com/asaskevich/EventBus v0.0.0-20200330115301-33b3bc6a7ddc => github.com/erickskrauch/EventBus v0.0.0-20200330115301-33b3bc6a7ddc
|
||||
|
||||
// Main dependencies
|
||||
require (
|
||||
github.com/SermoDigital/jose v0.9.2-0.20161205224733-f6df55f235c2
|
||||
github.com/asaskevich/EventBus v0.0.0-20200330115301-33b3bc6a7ddc
|
||||
github.com/defval/di v1.12.0
|
||||
github.com/etherlabsio/healthcheck/v2 v2.0.0
|
||||
github.com/getsentry/raven-go v0.2.1-0.20190419175539-919484f041ea
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/mediocregopher/radix/v4 v4.1.4
|
||||
github.com/mono83/slf v0.0.0-20170919161409-79153e9636db
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/spf13/viper v1.18.1
|
||||
github.com/thedevsaddam/govalidator v1.9.10
|
||||
)
|
||||
|
||||
// Dev dependencies
|
||||
require (
|
||||
github.com/h2non/gock v1.2.0
|
||||
github.com/stretchr/testify v1.8.4
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/mono83/udpwriter v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/stretchr/objx v0.5.0 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/tilinna/clock v1.0.2 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb // indirect
|
||||
golang.org/x/sys v0.15.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
106
go.sum
Normal file
106
go.sum
Normal file
@@ -0,0 +1,106 @@
|
||||
github.com/SermoDigital/jose v0.9.2-0.20161205224733-f6df55f235c2 h1:koK7z0nSsRiRiBWwa+E714Puh+DO+ZRdIyAXiXzL+lg=
|
||||
github.com/SermoDigital/jose v0.9.2-0.20161205224733-f6df55f235c2/go.mod h1:ARgCUhI1MHQH+ONky/PAtmVHQrP5JlGY0F3poXOp/fA=
|
||||
github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d h1:S2NE3iHSwP0XV47EEXL8mWmRdEfGscSJ+7EgePNgt0s=
|
||||
github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/defval/di v1.12.0 h1:xXm7BMX2+Nr0Yyu55DeJl/rmfCA7CQX89f4AGE0zA6U=
|
||||
github.com/defval/di v1.12.0/go.mod h1:PhVbOxQOvU7oawTOJXXTvqOJp1Dvsjs5PuzMw9gGl0I=
|
||||
github.com/erickskrauch/EventBus v0.0.0-20200330115301-33b3bc6a7ddc h1:kz3f5uMA1LxfRvJjZmMYG7Zu2rddTfJy6QZofz2YoGQ=
|
||||
github.com/erickskrauch/EventBus v0.0.0-20200330115301-33b3bc6a7ddc/go.mod h1:RHSo3YFV/SbOGyFR36RKWaXPy3g9nKAmn6ebNLpbco4=
|
||||
github.com/etherlabsio/healthcheck/v2 v2.0.0 h1:oKq8cbpwM/yNGPXf2Sff6MIjVUjx/pGYFydWzeK2MpA=
|
||||
github.com/etherlabsio/healthcheck/v2 v2.0.0/go.mod h1:huNVOjKzu6FI1eaO1CGD3ZjhrmPWf5Obu/pzpI6/wog=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/getsentry/raven-go v0.2.1-0.20190419175539-919484f041ea h1:t6e33/eet/VyiHHHKs0cBytUISUWQ/hmQwOlqtFoGEo=
|
||||
github.com/getsentry/raven-go v0.2.1-0.20190419175539-919484f041ea/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE=
|
||||
github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk=
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mediocregopher/radix/v4 v4.1.4 h1:Uze6DEbEAvL+VHXUEu/EDBTkUk5CLct5h3nVSGpc6Ts=
|
||||
github.com/mediocregopher/radix/v4 v4.1.4/go.mod h1:ajchozX/6ELmydxWeWM6xCFHVpZ4+67LXHOTOVR0nCE=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mono83/slf v0.0.0-20170919161409-79153e9636db h1:tlz4fTklh5mttoq5M+0yEc5Lap8W/02A2HCXCJn5iz0=
|
||||
github.com/mono83/slf v0.0.0-20170919161409-79153e9636db/go.mod h1:MfF+zNMZz+5IGY9h8jpFaGLyGoJ2ZPri2FmUVftBoUU=
|
||||
github.com/mono83/udpwriter v1.0.2 h1:JiQ/N646oZoJA1G0FOMvn2teMt6SdL1KwNH2mszOlQs=
|
||||
github.com/mono83/udpwriter v1.0.2/go.mod h1:mTDiyLtA0tXoxckkV9T4NUkJTgSQIuO8pAUKx/dSRkQ=
|
||||
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4=
|
||||
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
|
||||
github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
|
||||
github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
||||
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
|
||||
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.18.1 h1:rmuU42rScKWlhhJDyXZRKJQHXFX02chSVW1IvkPGiVM=
|
||||
github.com/spf13/viper v1.18.1/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/thedevsaddam/govalidator v1.9.10 h1:m3dLRbSZ5Hts3VUWYe+vxLMG+FdyQuWOjzTeQRiMCvU=
|
||||
github.com/thedevsaddam/govalidator v1.9.10/go.mod h1:Ilx8u7cg5g3LXbSS943cx5kczyNuUn7LH/cK5MYuE90=
|
||||
github.com/tilinna/clock v1.0.2 h1:6BO2tyAC9JbPExKH/z9zl44FLu1lImh3nDNKA0kgrkI=
|
||||
github.com/tilinna/clock v1.0.2/go.mod h1:ZsP7BcY7sEEz7ktc0IVy8Us6boDrK8VradlKRUGfOao=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb h1:c0vyKkb6yr3KR7jEfJaOSv4lG7xPkbN6r52aJz1d8a8=
|
||||
golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
|
||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
68
http/api.go
68
http/api.go
@@ -13,20 +13,12 @@ import (
|
||||
"github.com/elyby/chrly/model"
|
||||
)
|
||||
|
||||
//noinspection GoSnakeCaseUsage
|
||||
// noinspection GoSnakeCaseUsage
|
||||
const UUID_ANY = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
|
||||
|
||||
var regexUuidAny = regexp.MustCompile(UUID_ANY)
|
||||
|
||||
func init() {
|
||||
govalidator.AddCustomRule("skinUploadingNotAvailable", func(field string, rule string, message string, value interface{}) error {
|
||||
if message == "" {
|
||||
message = "Skin uploading is temporary unavailable"
|
||||
}
|
||||
|
||||
return errors.New(message)
|
||||
})
|
||||
|
||||
// Add ability to validate any possible uuid form
|
||||
govalidator.AddCustomRule("uuid_any", func(field string, rule string, message string, value interface{}) error {
|
||||
str := value.(string)
|
||||
@@ -43,7 +35,6 @@ func init() {
|
||||
}
|
||||
|
||||
type Api struct {
|
||||
Emitter
|
||||
SkinsRepo SkinsRepository
|
||||
}
|
||||
|
||||
@@ -68,9 +59,7 @@ func (ctx *Api) postSkinHandler(resp http.ResponseWriter, req *http.Request) {
|
||||
|
||||
record, err := ctx.findIdentityOrCleanup(identityId, username)
|
||||
if err != nil {
|
||||
ctx.Emit("skinsystem:error", fmt.Errorf("error on requesting a skin from the repository: %w", err))
|
||||
apiServerError(resp)
|
||||
return
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if record == nil {
|
||||
@@ -94,9 +83,7 @@ func (ctx *Api) postSkinHandler(resp http.ResponseWriter, req *http.Request) {
|
||||
|
||||
err = ctx.SkinsRepo.SaveSkin(record)
|
||||
if err != nil {
|
||||
ctx.Emit("skinsystem:error", fmt.Errorf("unable to save record to the repository: %w", err))
|
||||
apiServerError(resp)
|
||||
return
|
||||
panic(err)
|
||||
}
|
||||
|
||||
resp.WriteHeader(http.StatusCreated)
|
||||
@@ -116,9 +103,7 @@ func (ctx *Api) deleteSkinByUsernameHandler(resp http.ResponseWriter, req *http.
|
||||
|
||||
func (ctx *Api) deleteSkin(skin *model.Skin, err error, resp http.ResponseWriter) {
|
||||
if err != nil {
|
||||
ctx.Emit("skinsystem:error", fmt.Errorf("unable to find skin info from the repository: %w", err))
|
||||
apiServerError(resp)
|
||||
return
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if skin == nil {
|
||||
@@ -128,9 +113,7 @@ func (ctx *Api) deleteSkin(skin *model.Skin, err error, resp http.ResponseWriter
|
||||
|
||||
err = ctx.SkinsRepo.RemoveSkinByUserId(skin.UserId)
|
||||
if err != nil {
|
||||
ctx.Emit("skinsystem:error", fmt.Errorf("cannot delete skin by error: %w", err))
|
||||
apiServerError(resp)
|
||||
return
|
||||
panic(err)
|
||||
}
|
||||
|
||||
resp.WriteHeader(http.StatusNoContent)
|
||||
@@ -172,50 +155,41 @@ func (ctx *Api) findIdentityOrCleanup(identityId int, username string) (*model.S
|
||||
}
|
||||
|
||||
func validatePostSkinRequest(request *http.Request) map[string][]string {
|
||||
const maxMultipartMemory int64 = 32 << 20
|
||||
const oneOfSkinOrUrlMessage = "One of url or skin should be provided, but not both"
|
||||
|
||||
_ = request.ParseMultipartForm(maxMultipartMemory)
|
||||
_ = request.ParseForm()
|
||||
|
||||
validationRules := govalidator.MapData{
|
||||
"identityId": {"required", "numeric", "min:1"},
|
||||
"username": {"required"},
|
||||
"uuid": {"required", "uuid_any"},
|
||||
"skinId": {"required", "numeric", "min:1"},
|
||||
"url": {"url"},
|
||||
"file:skin": {"ext:png", "size:24576", "mime:image/png"},
|
||||
"is1_8": {"bool"},
|
||||
"isSlim": {"bool"},
|
||||
"identityId": {"required", "numeric", "min:1"},
|
||||
"username": {"required"},
|
||||
"uuid": {"required", "uuid_any"},
|
||||
"skinId": {"required", "numeric"},
|
||||
"url": {},
|
||||
"is1_8": {"bool"},
|
||||
"isSlim": {"bool"},
|
||||
"mojangTextures": {},
|
||||
"mojangSignature": {},
|
||||
}
|
||||
|
||||
shouldAppendSkinRequiredError := false
|
||||
url := request.Form.Get("url")
|
||||
_, _, skinErr := request.FormFile("skin")
|
||||
if (url != "" && skinErr == nil) || (url == "" && skinErr != nil) {
|
||||
shouldAppendSkinRequiredError = true
|
||||
} else if skinErr == nil {
|
||||
validationRules["file:skin"] = append(validationRules["file:skin"], "skinUploadingNotAvailable")
|
||||
} else if url != "" {
|
||||
if url == "" {
|
||||
validationRules["skinId"] = append(validationRules["skinId"], "numeric_between:0,0")
|
||||
} else {
|
||||
validationRules["url"] = append(validationRules["url"], "url")
|
||||
validationRules["skinId"] = append(validationRules["skinId"], "numeric_between:1,")
|
||||
validationRules["is1_8"] = append(validationRules["is1_8"], "required")
|
||||
validationRules["isSlim"] = append(validationRules["isSlim"], "required")
|
||||
}
|
||||
|
||||
mojangTextures := request.Form.Get("mojangTextures")
|
||||
if mojangTextures != "" {
|
||||
validationRules["mojangSignature"] = []string{"required"}
|
||||
validationRules["mojangSignature"] = append(validationRules["mojangSignature"], "required")
|
||||
}
|
||||
|
||||
validator := govalidator.New(govalidator.Options{
|
||||
Request: request,
|
||||
Rules: validationRules,
|
||||
RequiredDefault: false,
|
||||
FormSize: maxMultipartMemory,
|
||||
})
|
||||
validationResults := validator.Validate()
|
||||
if shouldAppendSkinRequiredError {
|
||||
validationResults["url"] = append(validationResults["url"], oneOfSkinOrUrlMessage)
|
||||
validationResults["skin"] = append(validationResults["skin"], oneOfSkinOrUrlMessage)
|
||||
}
|
||||
|
||||
if len(validationResults) != 0 {
|
||||
return validationResults
|
||||
|
||||
150
http/api_test.go
150
http/api_test.go
@@ -6,7 +6,6 @@ import (
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
@@ -28,7 +27,6 @@ type apiTestSuite struct {
|
||||
App *Api
|
||||
|
||||
SkinsRepository *skinsRepositoryMock
|
||||
Emitter *emitterMock
|
||||
}
|
||||
|
||||
/********************
|
||||
@@ -37,17 +35,14 @@ type apiTestSuite struct {
|
||||
|
||||
func (suite *apiTestSuite) SetupTest() {
|
||||
suite.SkinsRepository = &skinsRepositoryMock{}
|
||||
suite.Emitter = &emitterMock{}
|
||||
|
||||
suite.App = &Api{
|
||||
SkinsRepo: suite.SkinsRepository,
|
||||
Emitter: suite.Emitter,
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *apiTestSuite) TearDownTest() {
|
||||
suite.SkinsRepository.AssertExpectations(suite.T())
|
||||
suite.Emitter.AssertExpectations(suite.T())
|
||||
}
|
||||
|
||||
func (suite *apiTestSuite) RunSubTest(name string, subTest func()) {
|
||||
@@ -72,6 +67,7 @@ type postSkinTestCase struct {
|
||||
Name string
|
||||
Form io.Reader
|
||||
BeforeTest func(suite *apiTestSuite)
|
||||
PanicErr string
|
||||
AfterTest func(suite *apiTestSuite, response *http.Response)
|
||||
}
|
||||
|
||||
@@ -139,6 +135,41 @@ var postSkinTestsCases = []*postSkinTestCase{
|
||||
suite.Empty(body)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Update exists identity by changing textures data to empty",
|
||||
Form: bytes.NewBufferString(url.Values{
|
||||
"identityId": {"1"},
|
||||
"username": {"mock_username"},
|
||||
"uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"},
|
||||
"skinId": {"0"},
|
||||
"is1_8": {"0"},
|
||||
"isSlim": {"0"},
|
||||
"url": {""},
|
||||
"mojangTextures": {""},
|
||||
"mojangSignature": {""},
|
||||
}.Encode()),
|
||||
BeforeTest: func(suite *apiTestSuite) {
|
||||
suite.SkinsRepository.On("FindSkinByUserId", 1).Return(createSkinModel("mock_username", false), nil)
|
||||
suite.SkinsRepository.On("SaveSkin", mock.MatchedBy(func(model *model.Skin) bool {
|
||||
suite.Equal(1, model.UserId)
|
||||
suite.Equal("mock_username", model.Username)
|
||||
suite.Equal("0f657aa8-bfbe-415d-b700-5750090d3af3", model.Uuid)
|
||||
suite.Equal(0, model.SkinId)
|
||||
suite.False(model.Is1_8)
|
||||
suite.False(model.IsSlim)
|
||||
suite.Equal("", model.Url)
|
||||
suite.Equal("", model.MojangTextures)
|
||||
suite.Equal("", model.MojangSignature)
|
||||
|
||||
return true
|
||||
})).Times(1).Return(nil)
|
||||
},
|
||||
AfterTest: func(suite *apiTestSuite, response *http.Response) {
|
||||
suite.Equal(201, response.StatusCode)
|
||||
body, _ := io.ReadAll(response.Body)
|
||||
suite.Equal("", string(body))
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Update exists identity by changing its identityId",
|
||||
Form: bytes.NewBufferString(url.Values{
|
||||
@@ -198,6 +229,22 @@ var postSkinTestsCases = []*postSkinTestCase{
|
||||
},
|
||||
{
|
||||
Name: "Handle an error when loading the data from the repository",
|
||||
Form: bytes.NewBufferString(url.Values{
|
||||
"identityId": {"1"},
|
||||
"username": {"changed_username"},
|
||||
"uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"},
|
||||
"skinId": {"5"},
|
||||
"is1_8": {"0"},
|
||||
"isSlim": {"0"},
|
||||
"url": {"http://example.com/skin.png"},
|
||||
}.Encode()),
|
||||
BeforeTest: func(suite *apiTestSuite) {
|
||||
suite.SkinsRepository.On("FindSkinByUserId", 1).Return(nil, errors.New("can't find skin by user id"))
|
||||
},
|
||||
PanicErr: "can't find skin by user id",
|
||||
},
|
||||
{
|
||||
Name: "Handle an error when saving the data into the repository",
|
||||
Form: bytes.NewBufferString(url.Values{
|
||||
"identityId": {"1"},
|
||||
"username": {"mock_username"},
|
||||
@@ -209,43 +256,9 @@ var postSkinTestsCases = []*postSkinTestCase{
|
||||
}.Encode()),
|
||||
BeforeTest: func(suite *apiTestSuite) {
|
||||
suite.SkinsRepository.On("FindSkinByUserId", 1).Return(createSkinModel("mock_username", false), nil)
|
||||
err := errors.New("mock error")
|
||||
suite.SkinsRepository.On("SaveSkin", mock.Anything).Return(err)
|
||||
suite.Emitter.On("Emit", "skinsystem:error", mock.MatchedBy(func(cErr error) bool {
|
||||
return cErr.Error() == "unable to save record to the repository: mock error" &&
|
||||
errors.Is(cErr, err)
|
||||
})).Once()
|
||||
},
|
||||
AfterTest: func(suite *apiTestSuite, response *http.Response) {
|
||||
suite.Equal(500, response.StatusCode)
|
||||
body, _ := ioutil.ReadAll(response.Body)
|
||||
suite.Empty(body)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Handle an error when saving the data into the repository",
|
||||
Form: bytes.NewBufferString(url.Values{
|
||||
"identityId": {"1"},
|
||||
"username": {"changed_username"},
|
||||
"uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"},
|
||||
"skinId": {"5"},
|
||||
"is1_8": {"0"},
|
||||
"isSlim": {"0"},
|
||||
"url": {"http://example.com/skin.png"},
|
||||
}.Encode()),
|
||||
BeforeTest: func(suite *apiTestSuite) {
|
||||
err := errors.New("mock error")
|
||||
suite.SkinsRepository.On("FindSkinByUserId", 1).Return(nil, err)
|
||||
suite.Emitter.On("Emit", "skinsystem:error", mock.MatchedBy(func(cErr error) bool {
|
||||
return cErr.Error() == "error on requesting a skin from the repository: mock error" &&
|
||||
errors.Is(cErr, err)
|
||||
})).Once()
|
||||
},
|
||||
AfterTest: func(suite *apiTestSuite, response *http.Response) {
|
||||
suite.Equal(500, response.StatusCode)
|
||||
body, _ := ioutil.ReadAll(response.Body)
|
||||
suite.Empty(body)
|
||||
suite.SkinsRepository.On("SaveSkin", mock.Anything).Return(errors.New("can't save textures"))
|
||||
},
|
||||
PanicErr: "can't save textures",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -258,9 +271,14 @@ func (suite *apiTestSuite) TestPostSkin() {
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
suite.App.Handler().ServeHTTP(w, req)
|
||||
|
||||
testCase.AfterTest(suite, w.Result())
|
||||
if testCase.PanicErr != "" {
|
||||
suite.PanicsWithError(testCase.PanicErr, func() {
|
||||
suite.App.Handler().ServeHTTP(w, req)
|
||||
})
|
||||
} else {
|
||||
suite.App.Handler().ServeHTTP(w, req)
|
||||
testCase.AfterTest(suite, w.Result())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -287,7 +305,7 @@ func (suite *apiTestSuite) TestPostSkin() {
|
||||
"skinId": [
|
||||
"The skinId field is required",
|
||||
"The skinId field must be numeric",
|
||||
"The skinId field must be minimum 1 char"
|
||||
"The skinId field must be numeric value between 0 and 0"
|
||||
],
|
||||
"username": [
|
||||
"The username field is required"
|
||||
@@ -296,54 +314,12 @@ func (suite *apiTestSuite) TestPostSkin() {
|
||||
"The uuid field is required",
|
||||
"The uuid field must contain valid UUID"
|
||||
],
|
||||
"url": [
|
||||
"One of url or skin should be provided, but not both"
|
||||
],
|
||||
"skin": [
|
||||
"One of url or skin should be provided, but not both"
|
||||
],
|
||||
"mojangSignature": [
|
||||
"The mojangSignature field is required"
|
||||
]
|
||||
}
|
||||
}`, string(body))
|
||||
})
|
||||
|
||||
suite.RunSubTest("Upload textures with skin as file", func() {
|
||||
inputBody := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(inputBody)
|
||||
|
||||
part, _ := writer.CreateFormFile("skin", "char.png")
|
||||
_, _ = part.Write(loadSkinFile())
|
||||
|
||||
_ = writer.WriteField("identityId", "1")
|
||||
_ = writer.WriteField("username", "mock_user")
|
||||
_ = writer.WriteField("uuid", "0f657aa8-bfbe-415d-b700-5750090d3af3")
|
||||
_ = writer.WriteField("skinId", "5")
|
||||
|
||||
err := writer.Close()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("POST", "http://chrly/skins", inputBody)
|
||||
req.Header.Add("Content-Type", writer.FormDataContentType())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
suite.App.Handler().ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
defer resp.Body.Close()
|
||||
suite.Equal(400, resp.StatusCode)
|
||||
responseBody, _ := ioutil.ReadAll(resp.Body)
|
||||
suite.JSONEq(`{
|
||||
"errors": {
|
||||
"skin": [
|
||||
"Skin uploading is temporary unavailable"
|
||||
]
|
||||
}
|
||||
}`, string(responseBody))
|
||||
})
|
||||
}
|
||||
|
||||
/**************************************
|
||||
|
||||
12
http/http.go
12
http/http.go
@@ -7,6 +7,7 @@ import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/mono83/slf"
|
||||
@@ -28,15 +29,14 @@ func StartServer(server *http.Server, logger slf.Logger) {
|
||||
logger.Info("Starting the server, HTTP on: :addr", wd.StringParam("addr", server.Addr))
|
||||
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
logger.Emergency("Error in main(): :err", wd.ErrParam(err))
|
||||
close(done)
|
||||
}
|
||||
|
||||
close(done)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
s := waitForExitSignal()
|
||||
logger.Info("Got signal: :signal, starting graceful shutdown", wd.StringParam("signal", s.String()))
|
||||
server.Shutdown(context.Background())
|
||||
_ = server.Shutdown(context.Background())
|
||||
logger.Info("Graceful shutdown succeed, exiting", wd.StringParam("signal", s.String()))
|
||||
close(done)
|
||||
}()
|
||||
@@ -46,7 +46,7 @@ func StartServer(server *http.Server, logger slf.Logger) {
|
||||
|
||||
func waitForExitSignal() os.Signal {
|
||||
ch := make(chan os.Signal, 1)
|
||||
signal.Notify(ch, os.Interrupt, os.Kill)
|
||||
signal.Notify(ch, os.Interrupt, syscall.SIGTERM, os.Kill)
|
||||
|
||||
return <-ch
|
||||
}
|
||||
@@ -135,7 +135,3 @@ func apiNotFound(resp http.ResponseWriter, reason string) {
|
||||
})
|
||||
_, _ = resp.Write(result)
|
||||
}
|
||||
|
||||
func apiServerError(resp http.ResponseWriter) {
|
||||
resp.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ func TestJwtAuth_Authenticate(t *testing.T) {
|
||||
emitter.On("Emit", "authentication:success")
|
||||
|
||||
req := httptest.NewRequest("POST", "http://localhost", nil)
|
||||
req.Header.Add("Authorization", "Bearer " + jwt)
|
||||
req.Header.Add("Authorization", "Bearer "+jwt)
|
||||
jwt := &JwtAuth{Key: []byte("secret"), Emitter: emitter}
|
||||
|
||||
err := jwt.Authenticate(req)
|
||||
@@ -99,7 +99,7 @@ func TestJwtAuth_Authenticate(t *testing.T) {
|
||||
}))
|
||||
|
||||
req := httptest.NewRequest("POST", "http://localhost", nil)
|
||||
req.Header.Add("Authorization", "Bearer " + jwt)
|
||||
req.Header.Add("Authorization", "Bearer "+jwt)
|
||||
jwt := &JwtAuth{Emitter: emitter}
|
||||
|
||||
err := jwt.Authenticate(req)
|
||||
@@ -116,7 +116,7 @@ func TestJwtAuth_Authenticate(t *testing.T) {
|
||||
}))
|
||||
|
||||
req := httptest.NewRequest("POST", "http://localhost", nil)
|
||||
req.Header.Add("Authorization", "Bearer " + jwt)
|
||||
req.Header.Add("Authorization", "Bearer "+jwt)
|
||||
jwt := &JwtAuth{Key: []byte("this is another secret"), Emitter: emitter}
|
||||
|
||||
err := jwt.Authenticate(req)
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/elyby/chrly/api/mojang"
|
||||
"github.com/elyby/chrly/model"
|
||||
"github.com/elyby/chrly/utils"
|
||||
)
|
||||
|
||||
var timeNow = time.Now
|
||||
|
||||
type SkinsRepository interface {
|
||||
FindSkinByUsername(username string) (*model.Skin, error)
|
||||
FindSkinByUserId(id int) (*model.Skin, error)
|
||||
@@ -29,13 +37,55 @@ type MojangTexturesProvider interface {
|
||||
GetForUsername(username string) (*mojang.SignedTexturesResponse, error)
|
||||
}
|
||||
|
||||
type TexturesSigner interface {
|
||||
SignTextures(textures string) (string, error)
|
||||
GetPublicKey() (*rsa.PublicKey, error)
|
||||
}
|
||||
|
||||
type Skinsystem struct {
|
||||
Emitter
|
||||
SkinsRepo SkinsRepository
|
||||
CapesRepo CapesRepository
|
||||
MojangTexturesProvider MojangTexturesProvider
|
||||
TexturesExtraParamName string
|
||||
TexturesExtraParamValue string
|
||||
SkinsRepo SkinsRepository
|
||||
CapesRepo CapesRepository
|
||||
MojangTexturesProvider MojangTexturesProvider
|
||||
TexturesSigner TexturesSigner
|
||||
TexturesExtraParamName string
|
||||
TexturesExtraParamValue string
|
||||
texturesExtraParamSignature string
|
||||
}
|
||||
|
||||
func NewSkinsystem(
|
||||
emitter Emitter,
|
||||
skinsRepo SkinsRepository,
|
||||
capesRepo CapesRepository,
|
||||
mojangTexturesProvider MojangTexturesProvider,
|
||||
texturesSigner TexturesSigner,
|
||||
texturesExtraParamName string,
|
||||
texturesExtraParamValue string,
|
||||
) (*Skinsystem, error) {
|
||||
texturesExtraParamSignature, err := texturesSigner.SignTextures(texturesExtraParamValue)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to generate signature for textures extra param: %w", err)
|
||||
}
|
||||
|
||||
return &Skinsystem{
|
||||
Emitter: emitter,
|
||||
SkinsRepo: skinsRepo,
|
||||
CapesRepo: capesRepo,
|
||||
MojangTexturesProvider: mojangTexturesProvider,
|
||||
TexturesSigner: texturesSigner,
|
||||
TexturesExtraParamName: texturesExtraParamName,
|
||||
TexturesExtraParamValue: texturesExtraParamValue,
|
||||
texturesExtraParamSignature: texturesExtraParamSignature,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type profile struct {
|
||||
Id string
|
||||
Username string
|
||||
Textures *mojang.TexturesResponse
|
||||
CapeFile io.Reader
|
||||
MojangTextures string
|
||||
MojangSignature string
|
||||
}
|
||||
|
||||
func (ctx *Skinsystem) Handler() *mux.Router {
|
||||
@@ -45,35 +95,29 @@ func (ctx *Skinsystem) Handler() *mux.Router {
|
||||
router.HandleFunc("/cloaks/{username}", ctx.capeHandler).Methods(http.MethodGet).Name("cloaks")
|
||||
router.HandleFunc("/textures/{username}", ctx.texturesHandler).Methods(http.MethodGet)
|
||||
router.HandleFunc("/textures/signed/{username}", ctx.signedTexturesHandler).Methods(http.MethodGet)
|
||||
router.HandleFunc("/profile/{username}", ctx.profileHandler).Methods(http.MethodGet)
|
||||
// Legacy
|
||||
router.HandleFunc("/skins", ctx.skinGetHandler).Methods(http.MethodGet)
|
||||
router.HandleFunc("/cloaks", ctx.capeGetHandler).Methods(http.MethodGet)
|
||||
// Utils
|
||||
router.HandleFunc("/signature-verification-key.der", ctx.signatureVerificationKeyHandler).Methods(http.MethodGet)
|
||||
router.HandleFunc("/signature-verification-key.pem", ctx.signatureVerificationKeyHandler).Methods(http.MethodGet)
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
func (ctx *Skinsystem) skinHandler(response http.ResponseWriter, request *http.Request) {
|
||||
username := parseUsername(mux.Vars(request)["username"])
|
||||
rec, err := ctx.SkinsRepo.FindSkinByUsername(username)
|
||||
if err == nil && rec != nil && rec.SkinId != 0 {
|
||||
http.Redirect(response, request, rec.Url, 301)
|
||||
return
|
||||
profile, err := ctx.getProfile(request, true)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
mojangTextures, err := ctx.MojangTexturesProvider.GetForUsername(username)
|
||||
if err != nil || mojangTextures == nil {
|
||||
if profile == nil || profile.Textures == nil || profile.Textures.Skin == nil {
|
||||
response.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
texturesProp := mojangTextures.DecodeTextures()
|
||||
skin := texturesProp.Textures.Skin
|
||||
if skin == nil {
|
||||
response.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(response, request, skin.Url, 301)
|
||||
http.Redirect(response, request, profile.Textures.Skin.Url, 301)
|
||||
}
|
||||
|
||||
func (ctx *Skinsystem) skinGetHandler(response http.ResponseWriter, request *http.Request) {
|
||||
@@ -84,34 +128,27 @@ func (ctx *Skinsystem) skinGetHandler(response http.ResponseWriter, request *htt
|
||||
}
|
||||
|
||||
mux.Vars(request)["username"] = username
|
||||
mux.Vars(request)["converted"] = "1"
|
||||
|
||||
ctx.skinHandler(response, request)
|
||||
}
|
||||
|
||||
func (ctx *Skinsystem) capeHandler(response http.ResponseWriter, request *http.Request) {
|
||||
username := parseUsername(mux.Vars(request)["username"])
|
||||
rec, err := ctx.CapesRepo.FindCapeByUsername(username)
|
||||
if err == nil && rec != nil {
|
||||
profile, err := ctx.getProfile(request, true)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if profile == nil || profile.Textures == nil || (profile.CapeFile == nil && profile.Textures.Cape == nil) {
|
||||
response.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if profile.CapeFile == nil {
|
||||
http.Redirect(response, request, profile.Textures.Cape.Url, 301)
|
||||
} else {
|
||||
request.Header.Set("Content-Type", "image/png")
|
||||
_, _ = io.Copy(response, rec.File)
|
||||
return
|
||||
_, _ = io.Copy(response, profile.CapeFile)
|
||||
}
|
||||
|
||||
mojangTextures, err := ctx.MojangTexturesProvider.GetForUsername(username)
|
||||
if err != nil || mojangTextures == nil {
|
||||
response.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
texturesProp := mojangTextures.DecodeTextures()
|
||||
cape := texturesProp.Textures.Cape
|
||||
if cape == nil {
|
||||
response.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(response, request, cape.Url, 301)
|
||||
}
|
||||
|
||||
func (ctx *Skinsystem) capeGetHandler(response http.ResponseWriter, request *http.Request) {
|
||||
@@ -122,105 +159,247 @@ func (ctx *Skinsystem) capeGetHandler(response http.ResponseWriter, request *htt
|
||||
}
|
||||
|
||||
mux.Vars(request)["username"] = username
|
||||
mux.Vars(request)["converted"] = "1"
|
||||
|
||||
ctx.capeHandler(response, request)
|
||||
}
|
||||
|
||||
func (ctx *Skinsystem) texturesHandler(response http.ResponseWriter, request *http.Request) {
|
||||
username := parseUsername(mux.Vars(request)["username"])
|
||||
|
||||
var textures *mojang.TexturesResponse
|
||||
skin, skinErr := ctx.SkinsRepo.FindSkinByUsername(username)
|
||||
cape, capeErr := ctx.CapesRepo.FindCapeByUsername(username)
|
||||
if (skinErr == nil && skin != nil && skin.SkinId != 0) || (capeErr == nil && cape != nil) {
|
||||
textures = &mojang.TexturesResponse{}
|
||||
if skinErr == nil && skin != nil && skin.SkinId != 0 {
|
||||
skinTextures := &mojang.SkinTexturesResponse{
|
||||
Url: skin.Url,
|
||||
}
|
||||
|
||||
if skin.IsSlim {
|
||||
skinTextures.Metadata = &mojang.SkinTexturesMetadata{
|
||||
Model: "slim",
|
||||
}
|
||||
}
|
||||
|
||||
textures.Skin = skinTextures
|
||||
}
|
||||
|
||||
if capeErr == nil && cape != nil {
|
||||
textures.Cape = &mojang.CapeTexturesResponse{
|
||||
// Use statically http since the application doesn't support TLS
|
||||
Url: "http://" + request.Host + "/cloaks/" + username,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
mojangTextures, err := ctx.MojangTexturesProvider.GetForUsername(username)
|
||||
if err != nil || mojangTextures == nil {
|
||||
response.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
texturesProp := mojangTextures.DecodeTextures()
|
||||
if texturesProp == nil {
|
||||
ctx.Emit("skinsystem:error", errors.New("unable to find textures property"))
|
||||
apiServerError(response)
|
||||
return
|
||||
}
|
||||
|
||||
textures = texturesProp.Textures
|
||||
if textures.Skin == nil && textures.Cape == nil {
|
||||
response.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
profile, err := ctx.getProfile(request, true)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
responseData, _ := json.Marshal(textures)
|
||||
if profile == nil || profile.Textures == nil || (profile.Textures.Skin == nil && profile.Textures.Cape == nil) {
|
||||
response.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
responseData, _ := json.Marshal(profile.Textures)
|
||||
response.Header().Set("Content-Type", "application/json")
|
||||
_, _ = response.Write(responseData)
|
||||
}
|
||||
|
||||
func (ctx *Skinsystem) signedTexturesHandler(response http.ResponseWriter, request *http.Request) {
|
||||
username := parseUsername(mux.Vars(request)["username"])
|
||||
|
||||
var responseData *mojang.SignedTexturesResponse
|
||||
|
||||
rec, err := ctx.SkinsRepo.FindSkinByUsername(username)
|
||||
if err == nil && rec != nil && rec.SkinId != 0 && rec.MojangTextures != "" {
|
||||
responseData = &mojang.SignedTexturesResponse{
|
||||
Id: strings.Replace(rec.Uuid, "-", "", -1),
|
||||
Name: rec.Username,
|
||||
Props: []*mojang.Property{
|
||||
{
|
||||
Name: "textures",
|
||||
Signature: rec.MojangSignature,
|
||||
Value: rec.MojangTextures,
|
||||
},
|
||||
},
|
||||
}
|
||||
} else if request.URL.Query().Get("proxy") != "" {
|
||||
mojangTextures, err := ctx.MojangTexturesProvider.GetForUsername(username)
|
||||
if err == nil && mojangTextures != nil {
|
||||
responseData = mojangTextures
|
||||
}
|
||||
profile, err := ctx.getProfile(request, request.URL.Query().Get("proxy") != "")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if responseData == nil {
|
||||
if profile == nil || profile.MojangTextures == "" {
|
||||
response.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
responseData.Props = append(responseData.Props, &mojang.Property{
|
||||
Name: ctx.TexturesExtraParamName,
|
||||
Value: ctx.TexturesExtraParamValue,
|
||||
})
|
||||
profileResponse := &mojang.SignedTexturesResponse{
|
||||
Id: profile.Id,
|
||||
Name: profile.Username,
|
||||
Props: []*mojang.Property{
|
||||
{
|
||||
Name: "textures",
|
||||
Signature: profile.MojangSignature,
|
||||
Value: profile.MojangTextures,
|
||||
},
|
||||
{
|
||||
Name: ctx.TexturesExtraParamName,
|
||||
Value: ctx.TexturesExtraParamValue,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
responseJson, _ := json.Marshal(responseData)
|
||||
responseJson, _ := json.Marshal(profileResponse)
|
||||
response.Header().Set("Content-Type", "application/json")
|
||||
_, _ = response.Write(responseJson)
|
||||
}
|
||||
|
||||
func (ctx *Skinsystem) profileHandler(response http.ResponseWriter, request *http.Request) {
|
||||
profile, err := ctx.getProfile(request, true)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if profile == nil {
|
||||
forceResponseWithUuid := request.URL.Query().Get("onUnknownProfileRespondWithUuid")
|
||||
if forceResponseWithUuid == "" {
|
||||
response.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
profile = createEmptyProfile()
|
||||
profile.Id = formatUuid(forceResponseWithUuid)
|
||||
profile.Username = parseUsername(mux.Vars(request)["username"])
|
||||
}
|
||||
|
||||
texturesPropContent := &mojang.TexturesProp{
|
||||
Timestamp: utils.UnixMillisecond(timeNow()),
|
||||
ProfileID: profile.Id,
|
||||
ProfileName: profile.Username,
|
||||
Textures: profile.Textures,
|
||||
}
|
||||
|
||||
texturesPropValueJson, _ := json.Marshal(texturesPropContent)
|
||||
texturesPropEncodedValue := base64.StdEncoding.EncodeToString(texturesPropValueJson)
|
||||
|
||||
texturesProp := &mojang.Property{
|
||||
Name: "textures",
|
||||
Value: texturesPropEncodedValue,
|
||||
}
|
||||
customProp := &mojang.Property{
|
||||
Name: ctx.TexturesExtraParamName,
|
||||
Value: ctx.TexturesExtraParamValue,
|
||||
}
|
||||
|
||||
if request.URL.Query().Get("unsigned") == "false" {
|
||||
customProp.Signature = ctx.texturesExtraParamSignature
|
||||
|
||||
texturesSignature, err := ctx.TexturesSigner.SignTextures(texturesProp.Value)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
texturesProp.Signature = texturesSignature
|
||||
}
|
||||
|
||||
profileResponse := &mojang.SignedTexturesResponse{
|
||||
Id: profile.Id,
|
||||
Name: profile.Username,
|
||||
Props: []*mojang.Property{
|
||||
texturesProp,
|
||||
customProp,
|
||||
},
|
||||
}
|
||||
|
||||
responseJson, _ := json.Marshal(profileResponse)
|
||||
response.Header().Set("Content-Type", "application/json")
|
||||
_, _ = response.Write(responseJson)
|
||||
}
|
||||
|
||||
func (ctx *Skinsystem) signatureVerificationKeyHandler(response http.ResponseWriter, request *http.Request) {
|
||||
publicKey, err := ctx.TexturesSigner.GetPublicKey()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
asn1Bytes, err := x509.MarshalPKIXPublicKey(publicKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if strings.HasSuffix(request.URL.Path, ".pem") {
|
||||
publicKeyBlock := pem.Block{
|
||||
Type: "PUBLIC KEY",
|
||||
Bytes: asn1Bytes,
|
||||
}
|
||||
|
||||
publicKeyPemBytes := pem.EncodeToMemory(&publicKeyBlock)
|
||||
|
||||
response.Header().Set("Content-Disposition", "attachment; filename=\"yggdrasil_session_pubkey.pem\"")
|
||||
_, _ = response.Write(publicKeyPemBytes)
|
||||
} else {
|
||||
response.Header().Set("Content-Type", "application/octet-stream")
|
||||
response.Header().Set("Content-Disposition", "attachment; filename=\"yggdrasil_session_pubkey.der\"")
|
||||
_, _ = response.Write(asn1Bytes)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: in v5 should be extracted into some ProfileProvider interface,
|
||||
//
|
||||
// which will encapsulate all logics, declared in this method
|
||||
func (ctx *Skinsystem) getProfile(request *http.Request, proxy bool) (*profile, error) {
|
||||
username := parseUsername(mux.Vars(request)["username"])
|
||||
|
||||
skin, err := ctx.SkinsRepo.FindSkinByUsername(username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
profile := createEmptyProfile()
|
||||
|
||||
if skin != nil {
|
||||
profile.Id = formatUuid(skin.Uuid)
|
||||
profile.Username = skin.Username
|
||||
}
|
||||
|
||||
if skin != nil && skin.Url != "" {
|
||||
profile.Textures.Skin = &mojang.SkinTexturesResponse{
|
||||
Url: skin.Url,
|
||||
}
|
||||
|
||||
if skin.IsSlim {
|
||||
profile.Textures.Skin.Metadata = &mojang.SkinTexturesMetadata{
|
||||
Model: "slim",
|
||||
}
|
||||
}
|
||||
|
||||
cape, _ := ctx.CapesRepo.FindCapeByUsername(username)
|
||||
if cape != nil {
|
||||
profile.CapeFile = cape.File
|
||||
profile.Textures.Cape = &mojang.CapeTexturesResponse{
|
||||
// Use statically http since the application doesn't support TLS
|
||||
Url: "http://" + request.Host + "/cloaks/" + username,
|
||||
}
|
||||
}
|
||||
|
||||
profile.MojangTextures = skin.MojangTextures
|
||||
profile.MojangSignature = skin.MojangSignature
|
||||
} else if proxy {
|
||||
mojangProfile, err := ctx.MojangTexturesProvider.GetForUsername(username)
|
||||
// If we at least know something about a user,
|
||||
// than we can ignore an error and return profile without textures
|
||||
if err != nil && profile.Id != "" {
|
||||
return profile, nil
|
||||
}
|
||||
|
||||
if err != nil || mojangProfile == nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
decodedTextures, err := mojangProfile.DecodeTextures()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// There might be no textures property
|
||||
if decodedTextures != nil {
|
||||
profile.Textures = decodedTextures.Textures
|
||||
}
|
||||
|
||||
var texturesProp *mojang.Property
|
||||
for _, prop := range mojangProfile.Props {
|
||||
if prop.Name == "textures" {
|
||||
texturesProp = prop
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if texturesProp != nil {
|
||||
profile.MojangTextures = texturesProp.Value
|
||||
profile.MojangSignature = texturesProp.Signature
|
||||
}
|
||||
|
||||
// If user id is unknown at this point, then use values from Mojang profile
|
||||
if profile.Id == "" {
|
||||
profile.Id = mojangProfile.Id
|
||||
profile.Username = mojangProfile.Name
|
||||
}
|
||||
} else if profile.Id != "" {
|
||||
return profile, nil
|
||||
} else {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return profile, nil
|
||||
}
|
||||
|
||||
func createEmptyProfile() *profile {
|
||||
return &profile{
|
||||
Textures: &mojang.TexturesResponse{}, // Field must be initialized to avoid "null" after json encoding
|
||||
}
|
||||
}
|
||||
|
||||
func formatUuid(uuid string) string {
|
||||
return strings.Replace(uuid, "-", "", -1)
|
||||
}
|
||||
|
||||
func parseUsername(username string) string {
|
||||
return strings.TrimSuffix(username, ".png")
|
||||
}
|
||||
|
||||
@@ -2,11 +2,17 @@ package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"image"
|
||||
"image/png"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -89,6 +95,25 @@ func (m *mojangTexturesProviderMock) GetForUsername(username string) (*mojang.Si
|
||||
return result, args.Error(1)
|
||||
}
|
||||
|
||||
type texturesSignerMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *texturesSignerMock) SignTextures(textures string) (string, error) {
|
||||
args := m.Called(textures)
|
||||
return args.String(0), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *texturesSignerMock) GetPublicKey() (*rsa.PublicKey, error) {
|
||||
args := m.Called()
|
||||
var publicKey *rsa.PublicKey
|
||||
if casted, ok := args.Get(0).(*rsa.PublicKey); ok {
|
||||
publicKey = casted
|
||||
}
|
||||
|
||||
return publicKey, args.Error(1)
|
||||
}
|
||||
|
||||
type skinsystemTestSuite struct {
|
||||
suite.Suite
|
||||
|
||||
@@ -97,6 +122,7 @@ type skinsystemTestSuite struct {
|
||||
SkinsRepository *skinsRepositoryMock
|
||||
CapesRepository *capesRepositoryMock
|
||||
MojangTexturesProvider *mojangTexturesProviderMock
|
||||
TexturesSigner *texturesSignerMock
|
||||
Emitter *emitterMock
|
||||
}
|
||||
|
||||
@@ -105,25 +131,35 @@ type skinsystemTestSuite struct {
|
||||
********************/
|
||||
|
||||
func (suite *skinsystemTestSuite) SetupTest() {
|
||||
timeNow = func() time.Time {
|
||||
CET, _ := time.LoadLocation("CET")
|
||||
return time.Date(2021, 02, 25, 01, 50, 23, 0, CET)
|
||||
}
|
||||
|
||||
suite.SkinsRepository = &skinsRepositoryMock{}
|
||||
suite.CapesRepository = &capesRepositoryMock{}
|
||||
suite.MojangTexturesProvider = &mojangTexturesProviderMock{}
|
||||
suite.TexturesSigner = &texturesSignerMock{}
|
||||
suite.Emitter = &emitterMock{}
|
||||
|
||||
suite.App = &Skinsystem{
|
||||
SkinsRepo: suite.SkinsRepository,
|
||||
CapesRepo: suite.CapesRepository,
|
||||
MojangTexturesProvider: suite.MojangTexturesProvider,
|
||||
Emitter: suite.Emitter,
|
||||
TexturesExtraParamName: "texturesParamName",
|
||||
TexturesExtraParamValue: "texturesParamValue",
|
||||
}
|
||||
suite.TexturesSigner.On("SignTextures", "texturesParamValue").Times(1).Return("texturesParamSignature", nil)
|
||||
|
||||
suite.App, _ = NewSkinsystem(
|
||||
suite.Emitter,
|
||||
suite.SkinsRepository,
|
||||
suite.CapesRepository,
|
||||
suite.MojangTexturesProvider,
|
||||
suite.TexturesSigner,
|
||||
"texturesParamName",
|
||||
"texturesParamValue",
|
||||
)
|
||||
}
|
||||
|
||||
func (suite *skinsystemTestSuite) TearDownTest() {
|
||||
suite.SkinsRepository.AssertExpectations(suite.T())
|
||||
suite.CapesRepository.AssertExpectations(suite.T())
|
||||
suite.MojangTexturesProvider.AssertExpectations(suite.T())
|
||||
suite.TexturesSigner.AssertExpectations(suite.T())
|
||||
suite.Emitter.AssertExpectations(suite.T())
|
||||
}
|
||||
|
||||
@@ -144,6 +180,7 @@ func TestSkinsystem(t *testing.T) {
|
||||
type skinsystemTestCase struct {
|
||||
Name string
|
||||
BeforeTest func(suite *skinsystemTestSuite)
|
||||
PanicErr string
|
||||
AfterTest func(suite *skinsystemTestSuite, response *http.Response)
|
||||
}
|
||||
|
||||
@@ -156,6 +193,7 @@ var skinsTestsCases = []*skinsystemTestCase{
|
||||
Name: "Username exists in the local storage",
|
||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil)
|
||||
suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil)
|
||||
},
|
||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||
suite.Equal(301, response.StatusCode)
|
||||
@@ -166,7 +204,7 @@ var skinsTestsCases = []*skinsystemTestCase{
|
||||
Name: "Username doesn't exists on the local storage, but exists on Mojang and has textures",
|
||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
|
||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createMojangResponse(true, false), nil)
|
||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createMojangResponseWithTextures(true, false), nil)
|
||||
},
|
||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||
suite.Equal(301, response.StatusCode)
|
||||
@@ -174,10 +212,20 @@ var skinsTestsCases = []*skinsystemTestCase{
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Username doesn't exists on the local storage, but exists on Mojang and has no textures",
|
||||
Name: "Username doesn't exists on the local storage, but exists on Mojang and has no skin texture",
|
||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
|
||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createMojangResponse(false, false), nil)
|
||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createMojangResponseWithTextures(false, false), nil)
|
||||
},
|
||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||
suite.Equal(404, response.StatusCode)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Username doesn't exists on the local storage, but exists on Mojang and has an empty properties",
|
||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
|
||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createEmptyMojangResponse(), nil)
|
||||
},
|
||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||
suite.Equal(404, response.StatusCode)
|
||||
@@ -193,6 +241,13 @@ var skinsTestsCases = []*skinsystemTestCase{
|
||||
suite.Equal(404, response.StatusCode)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Receive an error from the SkinsRepository",
|
||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, errors.New("skins repository error"))
|
||||
},
|
||||
PanicErr: "skins repository error",
|
||||
},
|
||||
}
|
||||
|
||||
func (suite *skinsystemTestSuite) TestSkin() {
|
||||
@@ -203,14 +258,20 @@ func (suite *skinsystemTestSuite) TestSkin() {
|
||||
req := httptest.NewRequest("GET", "http://chrly/skins/mock_username", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
suite.App.Handler().ServeHTTP(w, req)
|
||||
|
||||
testCase.AfterTest(suite, w.Result())
|
||||
if testCase.PanicErr != "" {
|
||||
suite.PanicsWithError(testCase.PanicErr, func() {
|
||||
suite.App.Handler().ServeHTTP(w, req)
|
||||
})
|
||||
} else {
|
||||
suite.App.Handler().ServeHTTP(w, req)
|
||||
testCase.AfterTest(suite, w.Result())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
suite.RunSubTest("Pass username with png extension", func() {
|
||||
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil)
|
||||
suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil)
|
||||
|
||||
req := httptest.NewRequest("GET", "http://chrly/skins/mock_username.png", nil)
|
||||
w := httptest.NewRecorder()
|
||||
@@ -231,14 +292,18 @@ func (suite *skinsystemTestSuite) TestSkinGET() {
|
||||
req := httptest.NewRequest("GET", "http://chrly/skins?name=mock_username", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
suite.App.Handler().ServeHTTP(w, req)
|
||||
|
||||
testCase.AfterTest(suite, w.Result())
|
||||
if testCase.PanicErr != "" {
|
||||
suite.PanicsWithError(testCase.PanicErr, func() {
|
||||
suite.App.Handler().ServeHTTP(w, req)
|
||||
})
|
||||
} else {
|
||||
suite.App.Handler().ServeHTTP(w, req)
|
||||
testCase.AfterTest(suite, w.Result())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
suite.RunSubTest("Do not pass name param", func() {
|
||||
|
||||
req := httptest.NewRequest("GET", "http://chrly/skins", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
@@ -257,6 +322,7 @@ var capesTestsCases = []*skinsystemTestCase{
|
||||
{
|
||||
Name: "Username exists in the local storage",
|
||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil)
|
||||
suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(createCapeModel(), nil)
|
||||
},
|
||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||
@@ -269,8 +335,8 @@ var capesTestsCases = []*skinsystemTestCase{
|
||||
{
|
||||
Name: "Username doesn't exists on the local storage, but exists on Mojang and has textures",
|
||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||
suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil)
|
||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createMojangResponse(true, true), nil)
|
||||
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
|
||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createMojangResponseWithTextures(true, true), nil)
|
||||
},
|
||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||
suite.Equal(301, response.StatusCode)
|
||||
@@ -278,10 +344,20 @@ var capesTestsCases = []*skinsystemTestCase{
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Username doesn't exists on the local storage, but exists on Mojang and has no textures",
|
||||
Name: "Username doesn't exists on the local storage, but exists on Mojang and has no cape texture",
|
||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||
suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil)
|
||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createMojangResponse(false, false), nil)
|
||||
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
|
||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createMojangResponseWithTextures(false, false), nil)
|
||||
},
|
||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||
suite.Equal(404, response.StatusCode)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Username doesn't exists on the local storage, but exists on Mojang and has an empty properties",
|
||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
|
||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createEmptyMojangResponse(), nil)
|
||||
},
|
||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||
suite.Equal(404, response.StatusCode)
|
||||
@@ -290,13 +366,20 @@ var capesTestsCases = []*skinsystemTestCase{
|
||||
{
|
||||
Name: "Username doesn't exists on the local storage and doesn't exists on Mojang",
|
||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||
suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil)
|
||||
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
|
||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(nil, nil)
|
||||
},
|
||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||
suite.Equal(404, response.StatusCode)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Receive an error from the SkinsRepository",
|
||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, errors.New("skins repository error"))
|
||||
},
|
||||
PanicErr: "skins repository error",
|
||||
},
|
||||
}
|
||||
|
||||
func (suite *skinsystemTestSuite) TestCape() {
|
||||
@@ -307,13 +390,19 @@ func (suite *skinsystemTestSuite) TestCape() {
|
||||
req := httptest.NewRequest("GET", "http://chrly/cloaks/mock_username", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
suite.App.Handler().ServeHTTP(w, req)
|
||||
|
||||
testCase.AfterTest(suite, w.Result())
|
||||
if testCase.PanicErr != "" {
|
||||
suite.PanicsWithError(testCase.PanicErr, func() {
|
||||
suite.App.Handler().ServeHTTP(w, req)
|
||||
})
|
||||
} else {
|
||||
suite.App.Handler().ServeHTTP(w, req)
|
||||
testCase.AfterTest(suite, w.Result())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
suite.RunSubTest("Pass username with png extension", func() {
|
||||
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil)
|
||||
suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(createCapeModel(), nil)
|
||||
|
||||
req := httptest.NewRequest("GET", "http://chrly/cloaks/mock_username.png", nil)
|
||||
@@ -337,14 +426,18 @@ func (suite *skinsystemTestSuite) TestCapeGET() {
|
||||
req := httptest.NewRequest("GET", "http://chrly/cloaks?name=mock_username", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
suite.App.Handler().ServeHTTP(w, req)
|
||||
|
||||
testCase.AfterTest(suite, w.Result())
|
||||
if testCase.PanicErr != "" {
|
||||
suite.PanicsWithError(testCase.PanicErr, func() {
|
||||
suite.App.Handler().ServeHTTP(w, req)
|
||||
})
|
||||
} else {
|
||||
suite.App.Handler().ServeHTTP(w, req)
|
||||
testCase.AfterTest(suite, w.Result())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
suite.RunSubTest("Do not pass name param", func() {
|
||||
|
||||
req := httptest.NewRequest("GET", "http://chrly/cloaks", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
@@ -397,23 +490,9 @@ var texturesTestsCases = []*skinsystemTestCase{
|
||||
}`, string(body))
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Username exists and has cape, no skin",
|
||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
|
||||
suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(createCapeModel(), nil)
|
||||
},
|
||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||
suite.Equal(200, response.StatusCode)
|
||||
suite.Equal("application/json", response.Header.Get("Content-Type"))
|
||||
body, _ := ioutil.ReadAll(response.Body)
|
||||
suite.JSONEq(`{
|
||||
"CAPE": {
|
||||
"url": "http://chrly/cloaks/mock_username"
|
||||
}
|
||||
}`, string(body))
|
||||
},
|
||||
},
|
||||
// There is no case when the user has cape, but has no skin.
|
||||
// In v5 we will rework textures repositories to be more generic about source of textures,
|
||||
// but right now it's not possible to return profile entity with a cape only.
|
||||
{
|
||||
Name: "Username exists and has both skin and cape",
|
||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||
@@ -438,8 +517,7 @@ var texturesTestsCases = []*skinsystemTestCase{
|
||||
Name: "Username not exists, but Mojang profile available",
|
||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
|
||||
suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil)
|
||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(createMojangResponse(true, true), nil)
|
||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(createMojangResponseWithTextures(true, true), nil)
|
||||
},
|
||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||
suite.Equal(200, response.StatusCode)
|
||||
@@ -456,11 +534,20 @@ var texturesTestsCases = []*skinsystemTestCase{
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Username not exists, but Mojang profile available, but there is no textures",
|
||||
Name: "Username not exists, but Mojang profile available, but there is an empty skin and cape textures",
|
||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
|
||||
suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil)
|
||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(createMojangResponse(false, false), nil)
|
||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(createMojangResponseWithTextures(false, false), nil)
|
||||
},
|
||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||
suite.Equal(204, response.StatusCode)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Username not exists, but Mojang profile available, but there is an empty properties",
|
||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
|
||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(createEmptyMojangResponse(), nil)
|
||||
},
|
||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||
suite.Equal(204, response.StatusCode)
|
||||
@@ -470,7 +557,6 @@ var texturesTestsCases = []*skinsystemTestCase{
|
||||
Name: "Username not exists and Mojang profile unavailable",
|
||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
|
||||
suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil)
|
||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(nil, nil)
|
||||
},
|
||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||
@@ -479,6 +565,13 @@ var texturesTestsCases = []*skinsystemTestCase{
|
||||
suite.Equal("", string(body))
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Receive an error from the SkinsRepository",
|
||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, errors.New("skins repository error"))
|
||||
},
|
||||
PanicErr: "skins repository error",
|
||||
},
|
||||
}
|
||||
|
||||
func (suite *skinsystemTestSuite) TestTextures() {
|
||||
@@ -489,9 +582,14 @@ func (suite *skinsystemTestSuite) TestTextures() {
|
||||
req := httptest.NewRequest("GET", "http://chrly/textures/mock_username", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
suite.App.Handler().ServeHTTP(w, req)
|
||||
|
||||
testCase.AfterTest(suite, w.Result())
|
||||
if testCase.PanicErr != "" {
|
||||
suite.PanicsWithError(testCase.PanicErr, func() {
|
||||
suite.App.Handler().ServeHTTP(w, req)
|
||||
})
|
||||
} else {
|
||||
suite.App.Handler().ServeHTTP(w, req)
|
||||
testCase.AfterTest(suite, w.Result())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -504,6 +602,7 @@ type signedTexturesTestCase struct {
|
||||
Name string
|
||||
AllowProxy bool
|
||||
BeforeTest func(suite *skinsystemTestSuite)
|
||||
PanicErr string
|
||||
AfterTest func(suite *skinsystemTestSuite, response *http.Response)
|
||||
}
|
||||
|
||||
@@ -513,6 +612,7 @@ var signedTexturesTestsCases = []*signedTexturesTestCase{
|
||||
AllowProxy: false,
|
||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(createSkinModel("mock_username", true), nil)
|
||||
suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil)
|
||||
},
|
||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||
suite.Equal(200, response.StatusCode)
|
||||
@@ -555,6 +655,7 @@ var signedTexturesTestsCases = []*signedTexturesTestCase{
|
||||
skinModel.MojangTextures = ""
|
||||
skinModel.MojangSignature = ""
|
||||
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(skinModel, nil)
|
||||
suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil)
|
||||
},
|
||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||
suite.Equal(204, response.StatusCode)
|
||||
@@ -567,19 +668,20 @@ var signedTexturesTestsCases = []*signedTexturesTestCase{
|
||||
AllowProxy: true,
|
||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
|
||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createMojangResponse(true, false), nil)
|
||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createMojangResponseWithTextures(true, false), nil)
|
||||
},
|
||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||
suite.Equal(200, response.StatusCode)
|
||||
suite.Equal("application/json", response.Header.Get("Content-Type"))
|
||||
body, _ := ioutil.ReadAll(response.Body)
|
||||
suite.JSONEq(`{
|
||||
"id": "00000000000000000000000000000000",
|
||||
"id": "292a1db7353d476ca99cab8f57mojang",
|
||||
"name": "mock_username",
|
||||
"properties": [
|
||||
{
|
||||
"name": "textures",
|
||||
"value": "eyJ0aW1lc3RhbXAiOjE1NTYzOTg1NzIsInByb2ZpbGVJZCI6IjAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwIiwicHJvZmlsZU5hbWUiOiJtb2NrX3VzZXJuYW1lIiwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vbW9qYW5nL3NraW4ucG5nIn19fQ=="
|
||||
"value": "eyJ0aW1lc3RhbXAiOjE1NTYzOTg1NzIwMDAsInByb2ZpbGVJZCI6IjI5MmExZGI3MzUzZDQ3NmNhOTljYWI4ZjU3bW9qYW5nIiwicHJvZmlsZU5hbWUiOiJtb2NrX3VzZXJuYW1lIiwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vbW9qYW5nL3NraW4ucG5nIn19fQ==",
|
||||
"signature": "mojang signature"
|
||||
},
|
||||
{
|
||||
"name": "texturesParamName",
|
||||
@@ -602,6 +704,13 @@ var signedTexturesTestsCases = []*signedTexturesTestCase{
|
||||
suite.Equal("", string(body))
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Receive an error from the SkinsRepository",
|
||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, errors.New("skins repository error"))
|
||||
},
|
||||
PanicErr: "skins repository error",
|
||||
},
|
||||
}
|
||||
|
||||
func (suite *skinsystemTestSuite) TestSignedTextures() {
|
||||
@@ -619,9 +728,480 @@ func (suite *skinsystemTestSuite) TestSignedTextures() {
|
||||
req := httptest.NewRequest("GET", target, nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
suite.App.Handler().ServeHTTP(w, req)
|
||||
if testCase.PanicErr != "" {
|
||||
suite.PanicsWithError(testCase.PanicErr, func() {
|
||||
suite.App.Handler().ServeHTTP(w, req)
|
||||
})
|
||||
} else {
|
||||
suite.App.Handler().ServeHTTP(w, req)
|
||||
testCase.AfterTest(suite, w.Result())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
testCase.AfterTest(suite, w.Result())
|
||||
/***************************
|
||||
* Get profile tests cases *
|
||||
***************************/
|
||||
|
||||
type profileTestCase struct {
|
||||
Name string
|
||||
Signed bool
|
||||
ForceResponse string
|
||||
BeforeTest func(suite *skinsystemTestSuite)
|
||||
PanicErr string
|
||||
AfterTest func(suite *skinsystemTestSuite, response *http.Response)
|
||||
}
|
||||
|
||||
var profileTestsCases = []*profileTestCase{
|
||||
{
|
||||
Name: "Username exists and has both skin and cape, don't sign",
|
||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil)
|
||||
suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(createCapeModel(), nil)
|
||||
},
|
||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||
suite.Equal(200, response.StatusCode)
|
||||
suite.Equal("application/json", response.Header.Get("Content-Type"))
|
||||
body, _ := ioutil.ReadAll(response.Body)
|
||||
suite.JSONEq(`{
|
||||
"id": "0f657aa8bfbe415db7005750090d3af3",
|
||||
"name": "mock_username",
|
||||
"properties": [
|
||||
{
|
||||
"name": "textures",
|
||||
"value": "eyJ0aW1lc3RhbXAiOjE2MTQyMTQyMjMwMDAsInByb2ZpbGVJZCI6IjBmNjU3YWE4YmZiZTQxNWRiNzAwNTc1MDA5MGQzYWYzIiwicHJvZmlsZU5hbWUiOiJtb2NrX3VzZXJuYW1lIiwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vY2hybHkvc2tpbi5wbmcifSwiQ0FQRSI6eyJ1cmwiOiJodHRwOi8vY2hybHkvY2xvYWtzL21vY2tfdXNlcm5hbWUifX19"
|
||||
},
|
||||
{
|
||||
"name": "texturesParamName",
|
||||
"value": "texturesParamValue"
|
||||
}
|
||||
]
|
||||
}`, string(body))
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Username exists and has both skin and cape",
|
||||
Signed: true,
|
||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil)
|
||||
suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(createCapeModel(), nil)
|
||||
suite.TexturesSigner.On("SignTextures", "eyJ0aW1lc3RhbXAiOjE2MTQyMTQyMjMwMDAsInByb2ZpbGVJZCI6IjBmNjU3YWE4YmZiZTQxNWRiNzAwNTc1MDA5MGQzYWYzIiwicHJvZmlsZU5hbWUiOiJtb2NrX3VzZXJuYW1lIiwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vY2hybHkvc2tpbi5wbmcifSwiQ0FQRSI6eyJ1cmwiOiJodHRwOi8vY2hybHkvY2xvYWtzL21vY2tfdXNlcm5hbWUifX19").Return("textures signature", nil)
|
||||
},
|
||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||
suite.Equal(200, response.StatusCode)
|
||||
suite.Equal("application/json", response.Header.Get("Content-Type"))
|
||||
body, _ := ioutil.ReadAll(response.Body)
|
||||
suite.JSONEq(`{
|
||||
"id": "0f657aa8bfbe415db7005750090d3af3",
|
||||
"name": "mock_username",
|
||||
"properties": [
|
||||
{
|
||||
"name": "textures",
|
||||
"signature": "textures signature",
|
||||
"value": "eyJ0aW1lc3RhbXAiOjE2MTQyMTQyMjMwMDAsInByb2ZpbGVJZCI6IjBmNjU3YWE4YmZiZTQxNWRiNzAwNTc1MDA5MGQzYWYzIiwicHJvZmlsZU5hbWUiOiJtb2NrX3VzZXJuYW1lIiwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vY2hybHkvc2tpbi5wbmcifSwiQ0FQRSI6eyJ1cmwiOiJodHRwOi8vY2hybHkvY2xvYWtzL21vY2tfdXNlcm5hbWUifX19"
|
||||
},
|
||||
{
|
||||
"name": "texturesParamName",
|
||||
"signature": "texturesParamSignature",
|
||||
"value": "texturesParamValue"
|
||||
}
|
||||
]
|
||||
}`, string(body))
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Username exists and has skin, no cape",
|
||||
Signed: true,
|
||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil)
|
||||
suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil)
|
||||
suite.TexturesSigner.On("SignTextures", mock.Anything).Return("textures signature", nil)
|
||||
},
|
||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||
suite.Equal(200, response.StatusCode)
|
||||
suite.Equal("application/json", response.Header.Get("Content-Type"))
|
||||
body, _ := ioutil.ReadAll(response.Body)
|
||||
suite.JSONEq(`{
|
||||
"id": "0f657aa8bfbe415db7005750090d3af3",
|
||||
"name": "mock_username",
|
||||
"properties": [
|
||||
{
|
||||
"name": "textures",
|
||||
"signature": "textures signature",
|
||||
"value": "eyJ0aW1lc3RhbXAiOjE2MTQyMTQyMjMwMDAsInByb2ZpbGVJZCI6IjBmNjU3YWE4YmZiZTQxNWRiNzAwNTc1MDA5MGQzYWYzIiwicHJvZmlsZU5hbWUiOiJtb2NrX3VzZXJuYW1lIiwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vY2hybHkvc2tpbi5wbmcifX19"
|
||||
},
|
||||
{
|
||||
"name": "texturesParamName",
|
||||
"signature": "texturesParamSignature",
|
||||
"value": "texturesParamValue"
|
||||
}
|
||||
]
|
||||
}`, string(body))
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Username exists and has slim skin, no cape",
|
||||
Signed: true,
|
||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(createSkinModel("mock_username", true), nil)
|
||||
suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil)
|
||||
suite.TexturesSigner.On("SignTextures", mock.Anything).Return("textures signature", nil)
|
||||
},
|
||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||
suite.Equal(200, response.StatusCode)
|
||||
suite.Equal("application/json", response.Header.Get("Content-Type"))
|
||||
body, _ := ioutil.ReadAll(response.Body)
|
||||
suite.JSONEq(`{
|
||||
"id": "0f657aa8bfbe415db7005750090d3af3",
|
||||
"name": "mock_username",
|
||||
"properties": [
|
||||
{
|
||||
"name": "textures",
|
||||
"signature": "textures signature",
|
||||
"value": "eyJ0aW1lc3RhbXAiOjE2MTQyMTQyMjMwMDAsInByb2ZpbGVJZCI6IjBmNjU3YWE4YmZiZTQxNWRiNzAwNTc1MDA5MGQzYWYzIiwicHJvZmlsZU5hbWUiOiJtb2NrX3VzZXJuYW1lIiwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vY2hybHkvc2tpbi5wbmciLCJtZXRhZGF0YSI6eyJtb2RlbCI6InNsaW0ifX19fQ=="
|
||||
},
|
||||
{
|
||||
"name": "texturesParamName",
|
||||
"signature": "texturesParamSignature",
|
||||
"value": "texturesParamValue"
|
||||
}
|
||||
]
|
||||
}`, string(body))
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Username exists, but has no skin and Mojang profile with textures available",
|
||||
Signed: true,
|
||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||
skin := createSkinModel("mock_username", false)
|
||||
skin.SkinId = 0
|
||||
skin.Url = ""
|
||||
|
||||
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(skin, nil)
|
||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(createMojangResponseWithTextures(true, true), nil)
|
||||
suite.TexturesSigner.On("SignTextures", mock.Anything).Return("chrly signature", nil)
|
||||
},
|
||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||
suite.Equal(200, response.StatusCode)
|
||||
suite.Equal("application/json", response.Header.Get("Content-Type"))
|
||||
body, _ := ioutil.ReadAll(response.Body)
|
||||
suite.JSONEq(`{
|
||||
"id": "0f657aa8bfbe415db7005750090d3af3",
|
||||
"name": "mock_username",
|
||||
"properties": [
|
||||
{
|
||||
"name": "textures",
|
||||
"signature": "chrly signature",
|
||||
"value": "eyJ0aW1lc3RhbXAiOjE2MTQyMTQyMjMwMDAsInByb2ZpbGVJZCI6IjBmNjU3YWE4YmZiZTQxNWRiNzAwNTc1MDA5MGQzYWYzIiwicHJvZmlsZU5hbWUiOiJtb2NrX3VzZXJuYW1lIiwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vbW9qYW5nL3NraW4ucG5nIn0sIkNBUEUiOnsidXJsIjoiaHR0cDovL21vamFuZy9jYXBlLnBuZyJ9fX0="
|
||||
},
|
||||
{
|
||||
"name": "texturesParamName",
|
||||
"signature": "texturesParamSignature",
|
||||
"value": "texturesParamValue"
|
||||
}
|
||||
]
|
||||
}`, string(body))
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Username exists, but has no skin and Mojang textures proxy returned an error",
|
||||
Signed: true,
|
||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||
skin := createSkinModel("mock_username", false)
|
||||
skin.SkinId = 0
|
||||
skin.Url = ""
|
||||
|
||||
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(skin, nil)
|
||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(nil, errors.New("shit happened"))
|
||||
suite.TexturesSigner.On("SignTextures", mock.Anything).Return("chrly signature", nil)
|
||||
},
|
||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||
suite.Equal(200, response.StatusCode)
|
||||
suite.Equal("application/json", response.Header.Get("Content-Type"))
|
||||
body, _ := ioutil.ReadAll(response.Body)
|
||||
suite.JSONEq(`{
|
||||
"id": "0f657aa8bfbe415db7005750090d3af3",
|
||||
"name": "mock_username",
|
||||
"properties": [
|
||||
{
|
||||
"name": "textures",
|
||||
"signature": "chrly signature",
|
||||
"value": "eyJ0aW1lc3RhbXAiOjE2MTQyMTQyMjMwMDAsInByb2ZpbGVJZCI6IjBmNjU3YWE4YmZiZTQxNWRiNzAwNTc1MDA5MGQzYWYzIiwicHJvZmlsZU5hbWUiOiJtb2NrX3VzZXJuYW1lIiwidGV4dHVyZXMiOnt9fQ=="
|
||||
},
|
||||
{
|
||||
"name": "texturesParamName",
|
||||
"signature": "texturesParamSignature",
|
||||
"value": "texturesParamValue"
|
||||
}
|
||||
]
|
||||
}`, string(body))
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Username not exists, but Mojang profile with textures available",
|
||||
Signed: true,
|
||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
|
||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(createMojangResponseWithTextures(true, true), nil)
|
||||
suite.TexturesSigner.On("SignTextures", mock.Anything).Return("chrly signature", nil)
|
||||
},
|
||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||
suite.Equal(200, response.StatusCode)
|
||||
suite.Equal("application/json", response.Header.Get("Content-Type"))
|
||||
body, _ := ioutil.ReadAll(response.Body)
|
||||
suite.JSONEq(`{
|
||||
"id": "292a1db7353d476ca99cab8f57mojang",
|
||||
"name": "mock_username",
|
||||
"properties": [
|
||||
{
|
||||
"name": "textures",
|
||||
"signature": "chrly signature",
|
||||
"value": "eyJ0aW1lc3RhbXAiOjE2MTQyMTQyMjMwMDAsInByb2ZpbGVJZCI6IjI5MmExZGI3MzUzZDQ3NmNhOTljYWI4ZjU3bW9qYW5nIiwicHJvZmlsZU5hbWUiOiJtb2NrX3VzZXJuYW1lIiwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vbW9qYW5nL3NraW4ucG5nIn0sIkNBUEUiOnsidXJsIjoiaHR0cDovL21vamFuZy9jYXBlLnBuZyJ9fX0="
|
||||
},
|
||||
{
|
||||
"name": "texturesParamName",
|
||||
"signature": "texturesParamSignature",
|
||||
"value": "texturesParamValue"
|
||||
}
|
||||
]
|
||||
}`, string(body))
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Username not exists, but Mojang profile available, but there is an empty skin and cape textures",
|
||||
Signed: true,
|
||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
|
||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(createMojangResponseWithTextures(false, false), nil)
|
||||
suite.TexturesSigner.On("SignTextures", mock.Anything).Return("chrly signature", nil)
|
||||
},
|
||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||
suite.Equal(200, response.StatusCode)
|
||||
suite.Equal("application/json", response.Header.Get("Content-Type"))
|
||||
body, _ := ioutil.ReadAll(response.Body)
|
||||
suite.JSONEq(`{
|
||||
"id": "292a1db7353d476ca99cab8f57mojang",
|
||||
"name": "mock_username",
|
||||
"properties": [
|
||||
{
|
||||
"name": "textures",
|
||||
"signature": "chrly signature",
|
||||
"value": "eyJ0aW1lc3RhbXAiOjE2MTQyMTQyMjMwMDAsInByb2ZpbGVJZCI6IjI5MmExZGI3MzUzZDQ3NmNhOTljYWI4ZjU3bW9qYW5nIiwicHJvZmlsZU5hbWUiOiJtb2NrX3VzZXJuYW1lIiwidGV4dHVyZXMiOnt9fQ=="
|
||||
},
|
||||
{
|
||||
"name": "texturesParamName",
|
||||
"signature": "texturesParamSignature",
|
||||
"value": "texturesParamValue"
|
||||
}
|
||||
]
|
||||
}`, string(body))
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Username not exists, but Mojang profile available, but there is an empty properties",
|
||||
Signed: true,
|
||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
|
||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(createEmptyMojangResponse(), nil)
|
||||
suite.TexturesSigner.On("SignTextures", mock.Anything).Return("chrly signature", nil)
|
||||
},
|
||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||
suite.Equal(200, response.StatusCode)
|
||||
suite.Equal("application/json", response.Header.Get("Content-Type"))
|
||||
body, _ := ioutil.ReadAll(response.Body)
|
||||
suite.JSONEq(`{
|
||||
"id": "292a1db7353d476ca99cab8f57mojang",
|
||||
"name": "mock_username",
|
||||
"properties": [
|
||||
{
|
||||
"name": "textures",
|
||||
"signature": "chrly signature",
|
||||
"value": "eyJ0aW1lc3RhbXAiOjE2MTQyMTQyMjMwMDAsInByb2ZpbGVJZCI6IjI5MmExZGI3MzUzZDQ3NmNhOTljYWI4ZjU3bW9qYW5nIiwicHJvZmlsZU5hbWUiOiJtb2NrX3VzZXJuYW1lIiwidGV4dHVyZXMiOnt9fQ=="
|
||||
},
|
||||
{
|
||||
"name": "texturesParamName",
|
||||
"signature": "texturesParamSignature",
|
||||
"value": "texturesParamValue"
|
||||
}
|
||||
]
|
||||
}`, string(body))
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Username not exists and Mojang profile unavailable",
|
||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
|
||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(nil, nil)
|
||||
},
|
||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||
suite.Equal(204, response.StatusCode)
|
||||
body, _ := ioutil.ReadAll(response.Body)
|
||||
suite.Equal("", string(body))
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Username not exists and Mojang profile unavailable, but there is a forceResponse param",
|
||||
ForceResponse: "a12e41a4-e8e5-4503-987e-0adacf72ab93",
|
||||
Signed: true,
|
||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
|
||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(nil, nil)
|
||||
suite.TexturesSigner.On("SignTextures", mock.Anything).Return("chrly signature", nil)
|
||||
},
|
||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||
suite.Equal(200, response.StatusCode)
|
||||
suite.Equal("application/json", response.Header.Get("Content-Type"))
|
||||
body, _ := ioutil.ReadAll(response.Body)
|
||||
suite.JSONEq(`{
|
||||
"id": "a12e41a4e8e54503987e0adacf72ab93",
|
||||
"name": "mock_username",
|
||||
"properties": [
|
||||
{
|
||||
"name": "textures",
|
||||
"signature": "chrly signature",
|
||||
"value": "eyJ0aW1lc3RhbXAiOjE2MTQyMTQyMjMwMDAsInByb2ZpbGVJZCI6ImExMmU0MWE0ZThlNTQ1MDM5ODdlMGFkYWNmNzJhYjkzIiwicHJvZmlsZU5hbWUiOiJtb2NrX3VzZXJuYW1lIiwidGV4dHVyZXMiOnt9fQ=="
|
||||
},
|
||||
{
|
||||
"name": "texturesParamName",
|
||||
"signature": "texturesParamSignature",
|
||||
"value": "texturesParamValue"
|
||||
}
|
||||
]
|
||||
}`, string(body))
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Username not exists and Mojang textures proxy returned an error",
|
||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
|
||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(nil, errors.New("mojang textures provider error"))
|
||||
},
|
||||
PanicErr: "mojang textures provider error",
|
||||
},
|
||||
{
|
||||
Name: "Receive an error from the SkinsRepository",
|
||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, errors.New("skins repository error"))
|
||||
},
|
||||
PanicErr: "skins repository error",
|
||||
},
|
||||
{
|
||||
Name: "Receive an error from the TexturesSigner",
|
||||
Signed: true,
|
||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil)
|
||||
suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil)
|
||||
suite.TexturesSigner.On("SignTextures", mock.Anything).Return("", errors.New("textures signer error"))
|
||||
},
|
||||
PanicErr: "textures signer error",
|
||||
},
|
||||
}
|
||||
|
||||
func (suite *skinsystemTestSuite) TestProfile() {
|
||||
for _, testCase := range profileTestsCases {
|
||||
suite.RunSubTest(testCase.Name, func() {
|
||||
testCase.BeforeTest(suite)
|
||||
|
||||
u, _ := url.Parse("http://chrly/profile/mock_username")
|
||||
q := make(url.Values)
|
||||
if testCase.Signed {
|
||||
q.Set("unsigned", "false")
|
||||
}
|
||||
|
||||
if testCase.ForceResponse != "" {
|
||||
q.Set("onUnknownProfileRespondWithUuid", testCase.ForceResponse)
|
||||
}
|
||||
|
||||
u.RawQuery = q.Encode()
|
||||
req := httptest.NewRequest("GET", u.String(), nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
if testCase.PanicErr != "" {
|
||||
suite.PanicsWithError(testCase.PanicErr, func() {
|
||||
suite.App.Handler().ServeHTTP(w, req)
|
||||
})
|
||||
} else {
|
||||
suite.App.Handler().ServeHTTP(w, req)
|
||||
testCase.AfterTest(suite, w.Result())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/***************************
|
||||
* Get profile tests cases *
|
||||
***************************/
|
||||
|
||||
type signingKeyTestCase struct {
|
||||
Name string
|
||||
KeyFormat string
|
||||
BeforeTest func(suite *skinsystemTestSuite)
|
||||
PanicErr string
|
||||
AfterTest func(suite *skinsystemTestSuite, response *http.Response)
|
||||
}
|
||||
|
||||
var signingKeyTestsCases = []*signingKeyTestCase{
|
||||
{
|
||||
Name: "Get public key in DER format",
|
||||
KeyFormat: "DER",
|
||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||
pubPem, _ := pem.Decode([]byte("-----BEGIN PUBLIC KEY-----\nMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANbUpVCZkMKpfvYZ08W3lumdAaYxLBnm\nUDlzHBQH3DpYef5WCO32TDU6feIJ58A0lAywgtZ4wwi2dGHOz/1hAvcCAwEAAQ==\n-----END PUBLIC KEY-----"))
|
||||
publicKey, _ := x509.ParsePKIXPublicKey(pubPem.Bytes)
|
||||
|
||||
suite.TexturesSigner.On("GetPublicKey").Return(publicKey, nil)
|
||||
},
|
||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||
suite.Equal(200, response.StatusCode)
|
||||
suite.Equal("application/octet-stream", response.Header.Get("Content-Type"))
|
||||
suite.Equal("attachment; filename=\"yggdrasil_session_pubkey.der\"", response.Header.Get("Content-Disposition"))
|
||||
body, _ := ioutil.ReadAll(response.Body)
|
||||
suite.Equal([]byte{48, 92, 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0, 3, 75, 0, 48, 72, 2, 65, 0, 214, 212, 165, 80, 153, 144, 194, 169, 126, 246, 25, 211, 197, 183, 150, 233, 157, 1, 166, 49, 44, 25, 230, 80, 57, 115, 28, 20, 7, 220, 58, 88, 121, 254, 86, 8, 237, 246, 76, 53, 58, 125, 226, 9, 231, 192, 52, 148, 12, 176, 130, 214, 120, 195, 8, 182, 116, 97, 206, 207, 253, 97, 2, 247, 2, 3, 1, 0, 1}, body)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Get public key in PEM format",
|
||||
KeyFormat: "PEM",
|
||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||
pubPem, _ := pem.Decode([]byte("-----BEGIN PUBLIC KEY-----\nMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANbUpVCZkMKpfvYZ08W3lumdAaYxLBnm\nUDlzHBQH3DpYef5WCO32TDU6feIJ58A0lAywgtZ4wwi2dGHOz/1hAvcCAwEAAQ==\n-----END PUBLIC KEY-----"))
|
||||
publicKey, _ := x509.ParsePKIXPublicKey(pubPem.Bytes)
|
||||
|
||||
suite.TexturesSigner.On("GetPublicKey").Return(publicKey, nil)
|
||||
},
|
||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||
suite.Equal(200, response.StatusCode)
|
||||
suite.Equal("text/plain; charset=utf-8", response.Header.Get("Content-Type"))
|
||||
suite.Equal("attachment; filename=\"yggdrasil_session_pubkey.pem\"", response.Header.Get("Content-Disposition"))
|
||||
body, _ := ioutil.ReadAll(response.Body)
|
||||
suite.Equal("-----BEGIN PUBLIC KEY-----\nMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANbUpVCZkMKpfvYZ08W3lumdAaYxLBnm\nUDlzHBQH3DpYef5WCO32TDU6feIJ58A0lAywgtZ4wwi2dGHOz/1hAvcCAwEAAQ==\n-----END PUBLIC KEY-----\n", string(body))
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Error while obtaining public key",
|
||||
KeyFormat: "DER",
|
||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||
suite.TexturesSigner.On("GetPublicKey").Return(nil, errors.New("textures signer error"))
|
||||
},
|
||||
PanicErr: "textures signer error",
|
||||
},
|
||||
}
|
||||
|
||||
func (suite *skinsystemTestSuite) TestSignatureVerificationKey() {
|
||||
for _, testCase := range signingKeyTestsCases {
|
||||
suite.RunSubTest(testCase.Name, func() {
|
||||
testCase.BeforeTest(suite)
|
||||
|
||||
req := httptest.NewRequest("GET", "http://chrly/signature-verification-key."+strings.ToLower(testCase.KeyFormat), nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
if testCase.PanicErr != "" {
|
||||
suite.PanicsWithError(testCase.PanicErr, func() {
|
||||
suite.App.Handler().ServeHTTP(w, req)
|
||||
})
|
||||
} else {
|
||||
suite.App.Handler().ServeHTTP(w, req)
|
||||
testCase.AfterTest(suite, w.Result())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -666,11 +1246,19 @@ func createCapeModel() *model.Cape {
|
||||
return &model.Cape{File: bytes.NewReader(createCape())}
|
||||
}
|
||||
|
||||
func createMojangResponse(includeSkin bool, includeCape bool) *mojang.SignedTexturesResponse {
|
||||
func createEmptyMojangResponse() *mojang.SignedTexturesResponse {
|
||||
return &mojang.SignedTexturesResponse{
|
||||
Id: "292a1db7353d476ca99cab8f57mojang",
|
||||
Name: "mock_username",
|
||||
Props: []*mojang.Property{},
|
||||
}
|
||||
}
|
||||
|
||||
func createMojangResponseWithTextures(includeSkin bool, includeCape bool) *mojang.SignedTexturesResponse {
|
||||
timeZone, _ := time.LoadLocation("Europe/Minsk")
|
||||
textures := &mojang.TexturesProp{
|
||||
Timestamp: time.Date(2019, 4, 27, 23, 56, 12, 0, timeZone).Unix(),
|
||||
ProfileID: "00000000000000000000000000000000",
|
||||
Timestamp: time.Date(2019, 4, 27, 23, 56, 12, 0, timeZone).UnixNano() / int64(time.Millisecond),
|
||||
ProfileID: "292a1db7353d476ca99cab8f57mojang",
|
||||
ProfileName: "mock_username",
|
||||
Textures: &mojang.TexturesResponse{},
|
||||
}
|
||||
@@ -687,16 +1275,12 @@ func createMojangResponse(includeSkin bool, includeCape bool) *mojang.SignedText
|
||||
}
|
||||
}
|
||||
|
||||
response := &mojang.SignedTexturesResponse{
|
||||
Id: "00000000000000000000000000000000",
|
||||
Name: "mock_username",
|
||||
Props: []*mojang.Property{
|
||||
{
|
||||
Name: "textures",
|
||||
Value: mojang.EncodeTextures(textures),
|
||||
},
|
||||
},
|
||||
}
|
||||
response := createEmptyMojangResponse()
|
||||
response.Props = append(response.Props, &mojang.Property{
|
||||
Name: "textures",
|
||||
Value: mojang.EncodeTextures(textures),
|
||||
Signature: "mojang signature",
|
||||
})
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ type Skin struct {
|
||||
UserId int `json:"userId"`
|
||||
Uuid string `json:"uuid"`
|
||||
Username string `json:"username"`
|
||||
SkinId int `json:"skinId"`
|
||||
SkinId int `json:"skinId"` // deprecated
|
||||
Url string `json:"url"`
|
||||
Is1_8 bool `json:"is1_8"`
|
||||
IsSlim bool `json:"isSlim"`
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package mojangtextures
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -9,131 +10,240 @@ import (
|
||||
)
|
||||
|
||||
type jobResult struct {
|
||||
profile *mojang.ProfileInfo
|
||||
error error
|
||||
Profile *mojang.ProfileInfo
|
||||
Error error
|
||||
}
|
||||
|
||||
type jobItem struct {
|
||||
username string
|
||||
respondChan chan *jobResult
|
||||
type job struct {
|
||||
Username string
|
||||
RespondChan chan *jobResult
|
||||
}
|
||||
|
||||
type jobsQueue struct {
|
||||
lock sync.Mutex
|
||||
items []*jobItem
|
||||
items []*job
|
||||
}
|
||||
|
||||
func (s *jobsQueue) New() *jobsQueue {
|
||||
s.items = []*jobItem{}
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *jobsQueue) Enqueue(t *jobItem) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
s.items = append(s.items, t)
|
||||
}
|
||||
|
||||
func (s *jobsQueue) Dequeue(n int) []*jobItem {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
if n > s.size() {
|
||||
n = s.size()
|
||||
func newJobsQueue() *jobsQueue {
|
||||
return &jobsQueue{
|
||||
items: []*job{},
|
||||
}
|
||||
|
||||
items := s.items[0:n]
|
||||
s.items = s.items[n:len(s.items)]
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
func (s *jobsQueue) Size() int {
|
||||
func (s *jobsQueue) Enqueue(job *job) int {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
return s.size()
|
||||
}
|
||||
s.items = append(s.items, job)
|
||||
|
||||
func (s *jobsQueue) size() int {
|
||||
return len(s.items)
|
||||
}
|
||||
|
||||
func (s *jobsQueue) Dequeue(n int) ([]*job, int) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
l := len(s.items)
|
||||
if n > l {
|
||||
n = l
|
||||
}
|
||||
|
||||
items := s.items[0:n]
|
||||
s.items = s.items[n:l]
|
||||
|
||||
return items, l - n
|
||||
}
|
||||
|
||||
var usernamesToUuids = mojang.UsernamesToUuids
|
||||
var forever = func() bool {
|
||||
return true
|
||||
|
||||
type JobsIteration struct {
|
||||
Jobs []*job
|
||||
Queue int
|
||||
c chan struct{}
|
||||
}
|
||||
|
||||
func (j *JobsIteration) Done() {
|
||||
if j.c != nil {
|
||||
close(j.c)
|
||||
}
|
||||
}
|
||||
|
||||
type BatchUuidsProviderStrategy interface {
|
||||
Queue(job *job)
|
||||
GetJobs(abort context.Context) <-chan *JobsIteration
|
||||
}
|
||||
|
||||
type PeriodicStrategy struct {
|
||||
Delay time.Duration
|
||||
Batch int
|
||||
queue *jobsQueue
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
func NewPeriodicStrategy(delay time.Duration, batch int) *PeriodicStrategy {
|
||||
return &PeriodicStrategy{
|
||||
Delay: delay,
|
||||
Batch: batch,
|
||||
queue: newJobsQueue(),
|
||||
}
|
||||
}
|
||||
|
||||
func (ctx *PeriodicStrategy) Queue(job *job) {
|
||||
ctx.queue.Enqueue(job)
|
||||
}
|
||||
|
||||
func (ctx *PeriodicStrategy) GetJobs(abort context.Context) <-chan *JobsIteration {
|
||||
ch := make(chan *JobsIteration)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-abort.Done():
|
||||
close(ch)
|
||||
return
|
||||
case <-time.After(ctx.Delay):
|
||||
jobs, queueLen := ctx.queue.Dequeue(ctx.Batch)
|
||||
jobDoneChan := make(chan struct{})
|
||||
ch <- &JobsIteration{jobs, queueLen, jobDoneChan}
|
||||
<-jobDoneChan
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return ch
|
||||
}
|
||||
|
||||
type FullBusStrategy struct {
|
||||
Delay time.Duration
|
||||
Batch int
|
||||
queue *jobsQueue
|
||||
busIsFull chan bool
|
||||
}
|
||||
|
||||
func NewFullBusStrategy(delay time.Duration, batch int) *FullBusStrategy {
|
||||
return &FullBusStrategy{
|
||||
Delay: delay,
|
||||
Batch: batch,
|
||||
queue: newJobsQueue(),
|
||||
busIsFull: make(chan bool),
|
||||
}
|
||||
}
|
||||
|
||||
func (ctx *FullBusStrategy) Queue(job *job) {
|
||||
n := ctx.queue.Enqueue(job)
|
||||
if n%ctx.Batch == 0 {
|
||||
ctx.busIsFull <- true
|
||||
}
|
||||
}
|
||||
|
||||
// Формально, это описание логики водителя маршрутки xD
|
||||
func (ctx *FullBusStrategy) GetJobs(abort context.Context) <-chan *JobsIteration {
|
||||
ch := make(chan *JobsIteration)
|
||||
go func() {
|
||||
for {
|
||||
t := time.NewTimer(ctx.Delay)
|
||||
select {
|
||||
case <-abort.Done():
|
||||
close(ch)
|
||||
return
|
||||
case <-t.C:
|
||||
ctx.sendJobs(ch)
|
||||
case <-ctx.busIsFull:
|
||||
t.Stop()
|
||||
ctx.sendJobs(ch)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return ch
|
||||
}
|
||||
|
||||
func (ctx *FullBusStrategy) sendJobs(ch chan *JobsIteration) {
|
||||
jobs, queueLen := ctx.queue.Dequeue(ctx.Batch)
|
||||
ch <- &JobsIteration{jobs, queueLen, nil}
|
||||
}
|
||||
|
||||
type BatchUuidsProvider struct {
|
||||
Emitter
|
||||
|
||||
IterationDelay time.Duration
|
||||
IterationSize int
|
||||
|
||||
context context.Context
|
||||
emitter Emitter
|
||||
strategy BatchUuidsProviderStrategy
|
||||
onFirstCall sync.Once
|
||||
queue jobsQueue
|
||||
}
|
||||
|
||||
func NewBatchUuidsProvider(
|
||||
context context.Context,
|
||||
strategy BatchUuidsProviderStrategy,
|
||||
emitter Emitter,
|
||||
) *BatchUuidsProvider {
|
||||
return &BatchUuidsProvider{
|
||||
context: context,
|
||||
emitter: emitter,
|
||||
strategy: strategy,
|
||||
}
|
||||
}
|
||||
|
||||
func (ctx *BatchUuidsProvider) GetUuid(username string) (*mojang.ProfileInfo, error) {
|
||||
ctx.onFirstCall.Do(func() {
|
||||
ctx.queue.New()
|
||||
ctx.startQueue()
|
||||
})
|
||||
ctx.onFirstCall.Do(ctx.startQueue)
|
||||
|
||||
resultChan := make(chan *jobResult)
|
||||
ctx.queue.Enqueue(&jobItem{username, resultChan})
|
||||
ctx.Emit("mojang_textures:batch_uuids_provider:queued", username)
|
||||
ctx.strategy.Queue(&job{username, resultChan})
|
||||
ctx.emitter.Emit("mojang_textures:batch_uuids_provider:queued", username)
|
||||
|
||||
result := <-resultChan
|
||||
|
||||
return result.profile, result.error
|
||||
return result.Profile, result.Error
|
||||
}
|
||||
|
||||
func (ctx *BatchUuidsProvider) startQueue() {
|
||||
// This synchronization chan is used to ensure that strategy's jobs provider
|
||||
// will be initialized before any job will be scheduled
|
||||
d := make(chan struct{})
|
||||
go func() {
|
||||
time.Sleep(ctx.IterationDelay)
|
||||
for forever() {
|
||||
ctx.Emit("mojang_textures:batch_uuids_provider:before_round")
|
||||
ctx.queueRound()
|
||||
ctx.Emit("mojang_textures:batch_uuids_provider:after_round")
|
||||
time.Sleep(ctx.IterationDelay)
|
||||
jobsChan := ctx.strategy.GetJobs(ctx.context)
|
||||
close(d)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.context.Done():
|
||||
return
|
||||
case iteration := <-jobsChan:
|
||||
go func() {
|
||||
ctx.performRequest(iteration)
|
||||
iteration.Done()
|
||||
}()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
<-d
|
||||
}
|
||||
|
||||
func (ctx *BatchUuidsProvider) queueRound() {
|
||||
queueSize := ctx.queue.Size()
|
||||
jobs := ctx.queue.Dequeue(ctx.IterationSize)
|
||||
|
||||
var usernames []string
|
||||
for _, job := range jobs {
|
||||
usernames = append(usernames, job.username)
|
||||
func (ctx *BatchUuidsProvider) performRequest(iteration *JobsIteration) {
|
||||
usernames := make([]string, len(iteration.Jobs))
|
||||
for i, job := range iteration.Jobs {
|
||||
usernames[i] = job.Username
|
||||
}
|
||||
|
||||
ctx.Emit("mojang_textures:batch_uuids_provider:round", usernames, queueSize-len(jobs))
|
||||
ctx.emitter.Emit("mojang_textures:batch_uuids_provider:round", usernames, iteration.Queue)
|
||||
if len(usernames) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
profiles, err := usernamesToUuids(usernames)
|
||||
ctx.Emit("mojang_textures:batch_uuids_provider:result", usernames, profiles, err)
|
||||
for _, job := range jobs {
|
||||
go func(job *jobItem) {
|
||||
response := &jobResult{}
|
||||
if err != nil {
|
||||
response.error = err
|
||||
} else {
|
||||
// The profiles in the response aren't ordered, so we must search each username over full array
|
||||
for _, profile := range profiles {
|
||||
if strings.EqualFold(job.username, profile.Name) {
|
||||
response.profile = profile
|
||||
break
|
||||
}
|
||||
ctx.emitter.Emit("mojang_textures:batch_uuids_provider:result", usernames, profiles, err)
|
||||
for _, job := range iteration.Jobs {
|
||||
response := &jobResult{}
|
||||
if err == nil {
|
||||
// The profiles in the response aren't ordered, so we must search each username over full array
|
||||
for _, profile := range profiles {
|
||||
if strings.EqualFold(job.Username, profile.Name) {
|
||||
response.Profile = profile
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
response.Error = err
|
||||
}
|
||||
|
||||
job.respondChan <- response
|
||||
}(job)
|
||||
job.RespondChan <- response
|
||||
close(job.RespondChan)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,64 +1,51 @@
|
||||
package mojangtextures
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"strings"
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
testify "github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/elyby/chrly/api/mojang"
|
||||
)
|
||||
|
||||
func TestJobsQueue(t *testing.T) {
|
||||
createQueue := func() *jobsQueue {
|
||||
queue := &jobsQueue{}
|
||||
queue.New()
|
||||
|
||||
return queue
|
||||
}
|
||||
|
||||
t.Run("Enqueue", func(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
s := createQueue()
|
||||
s.Enqueue(&jobItem{username: "username1"})
|
||||
s.Enqueue(&jobItem{username: "username2"})
|
||||
s.Enqueue(&jobItem{username: "username3"})
|
||||
|
||||
assert.Equal(3, s.Size())
|
||||
s := newJobsQueue()
|
||||
require.Equal(t, 1, s.Enqueue(&job{Username: "username1"}))
|
||||
require.Equal(t, 2, s.Enqueue(&job{Username: "username2"}))
|
||||
require.Equal(t, 3, s.Enqueue(&job{Username: "username3"}))
|
||||
})
|
||||
|
||||
t.Run("Dequeue", func(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
s := newJobsQueue()
|
||||
s.Enqueue(&job{Username: "username1"})
|
||||
s.Enqueue(&job{Username: "username2"})
|
||||
s.Enqueue(&job{Username: "username3"})
|
||||
s.Enqueue(&job{Username: "username4"})
|
||||
s.Enqueue(&job{Username: "username5"})
|
||||
|
||||
s := createQueue()
|
||||
s.Enqueue(&jobItem{username: "username1"})
|
||||
s.Enqueue(&jobItem{username: "username2"})
|
||||
s.Enqueue(&jobItem{username: "username3"})
|
||||
s.Enqueue(&jobItem{username: "username4"})
|
||||
items, queueLen := s.Dequeue(2)
|
||||
require.Len(t, items, 2)
|
||||
require.Equal(t, 3, queueLen)
|
||||
require.Equal(t, "username1", items[0].Username)
|
||||
require.Equal(t, "username2", items[1].Username)
|
||||
|
||||
items := s.Dequeue(2)
|
||||
assert.Len(items, 2)
|
||||
assert.Equal("username1", items[0].username)
|
||||
assert.Equal("username2", items[1].username)
|
||||
assert.Equal(2, s.Size())
|
||||
|
||||
items = s.Dequeue(40)
|
||||
assert.Len(items, 2)
|
||||
assert.Equal("username3", items[0].username)
|
||||
assert.Equal("username4", items[1].username)
|
||||
items, queueLen = s.Dequeue(40)
|
||||
require.Len(t, items, 3)
|
||||
require.Equal(t, 0, queueLen)
|
||||
require.Equal(t, "username3", items[0].Username)
|
||||
require.Equal(t, "username4", items[1].Username)
|
||||
require.Equal(t, "username5", items[2].Username)
|
||||
})
|
||||
}
|
||||
|
||||
// This is really stupid test just to get 100% coverage on this package :)
|
||||
func TestBatchUuidsProvider_forever(t *testing.T) {
|
||||
testify.True(t, forever())
|
||||
}
|
||||
|
||||
type mojangUsernamesToUuidsRequestMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
@@ -73,6 +60,37 @@ func (o *mojangUsernamesToUuidsRequestMock) UsernamesToUuids(usernames []string)
|
||||
return result, args.Error(1)
|
||||
}
|
||||
|
||||
type manualStrategy struct {
|
||||
ch chan *JobsIteration
|
||||
once sync.Once
|
||||
lock sync.Mutex
|
||||
jobs []*job
|
||||
}
|
||||
|
||||
func (m *manualStrategy) Queue(job *job) {
|
||||
m.lock.Lock()
|
||||
m.jobs = append(m.jobs, job)
|
||||
m.lock.Unlock()
|
||||
}
|
||||
|
||||
func (m *manualStrategy) GetJobs(_ context.Context) <-chan *JobsIteration {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
m.ch = make(chan *JobsIteration)
|
||||
|
||||
return m.ch
|
||||
}
|
||||
|
||||
func (m *manualStrategy) Iterate(countJobsToReturn int, countLeftJobsInQueue int) {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
|
||||
m.ch <- &JobsIteration{
|
||||
Jobs: m.jobs[0:countJobsToReturn],
|
||||
Queue: countLeftJobsInQueue,
|
||||
}
|
||||
}
|
||||
|
||||
type batchUuidsProviderGetUuidResult struct {
|
||||
Result *mojang.ProfileInfo
|
||||
Error error
|
||||
@@ -81,71 +99,54 @@ type batchUuidsProviderGetUuidResult struct {
|
||||
type batchUuidsProviderTestSuite struct {
|
||||
suite.Suite
|
||||
|
||||
Provider *BatchUuidsProvider
|
||||
GetUuidAsync func(username string) chan *batchUuidsProviderGetUuidResult
|
||||
Provider *BatchUuidsProvider
|
||||
|
||||
Emitter *mockEmitter
|
||||
Strategy *manualStrategy
|
||||
MojangApi *mojangUsernamesToUuidsRequestMock
|
||||
|
||||
Iterate func()
|
||||
done func()
|
||||
iterateChan chan bool
|
||||
stop context.CancelFunc
|
||||
}
|
||||
|
||||
func (suite *batchUuidsProviderTestSuite) GetUuidAsync(username string) <-chan *batchUuidsProviderGetUuidResult {
|
||||
s := make(chan struct{})
|
||||
// This dirty hack ensures, that the username will be queued before we return control to the caller.
|
||||
// It's needed to keep expected calls order and prevent cases when iteration happens before
|
||||
// all usernames will be queued.
|
||||
suite.Emitter.On("Emit",
|
||||
"mojang_textures:batch_uuids_provider:queued",
|
||||
username,
|
||||
).Once().Run(func(args mock.Arguments) {
|
||||
close(s)
|
||||
})
|
||||
|
||||
c := make(chan *batchUuidsProviderGetUuidResult)
|
||||
go func() {
|
||||
profile, err := suite.Provider.GetUuid(username)
|
||||
c <- &batchUuidsProviderGetUuidResult{
|
||||
Result: profile,
|
||||
Error: err,
|
||||
}
|
||||
}()
|
||||
|
||||
<-s
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func (suite *batchUuidsProviderTestSuite) SetupTest() {
|
||||
suite.Emitter = &mockEmitter{}
|
||||
|
||||
suite.Provider = &BatchUuidsProvider{
|
||||
Emitter: suite.Emitter,
|
||||
IterationDelay: 0,
|
||||
IterationSize: 10,
|
||||
}
|
||||
|
||||
suite.iterateChan = make(chan bool)
|
||||
forever = func() bool {
|
||||
return <-suite.iterateChan
|
||||
}
|
||||
|
||||
suite.Iterate = func() {
|
||||
suite.iterateChan <- true
|
||||
}
|
||||
|
||||
suite.done = func() {
|
||||
suite.iterateChan <- false
|
||||
}
|
||||
|
||||
suite.GetUuidAsync = func(username string) chan *batchUuidsProviderGetUuidResult {
|
||||
s := make(chan bool)
|
||||
// This dirty hack ensures, that the username will be queued before we return control to the caller.
|
||||
// It's needed to keep expected calls order and prevent cases when iteration happens before all usernames
|
||||
// will be queued.
|
||||
suite.Emitter.On("Emit",
|
||||
"mojang_textures:batch_uuids_provider:queued",
|
||||
username,
|
||||
).Once().Run(func(args mock.Arguments) {
|
||||
s <- true
|
||||
})
|
||||
|
||||
c := make(chan *batchUuidsProviderGetUuidResult)
|
||||
go func() {
|
||||
profile, err := suite.Provider.GetUuid(username)
|
||||
c <- &batchUuidsProviderGetUuidResult{
|
||||
Result: profile,
|
||||
Error: err,
|
||||
}
|
||||
}()
|
||||
|
||||
<-s
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
suite.Strategy = &manualStrategy{}
|
||||
ctx, stop := context.WithCancel(context.Background())
|
||||
suite.stop = stop
|
||||
suite.MojangApi = &mojangUsernamesToUuidsRequestMock{}
|
||||
usernamesToUuids = suite.MojangApi.UsernamesToUuids
|
||||
|
||||
suite.Provider = NewBatchUuidsProvider(ctx, suite.Strategy, suite.Emitter)
|
||||
}
|
||||
|
||||
func (suite *batchUuidsProviderTestSuite) TearDownTest() {
|
||||
suite.done()
|
||||
suite.stop()
|
||||
suite.Emitter.AssertExpectations(suite.T())
|
||||
suite.MojangApi.AssertExpectations(suite.T())
|
||||
}
|
||||
@@ -154,37 +155,14 @@ func TestBatchUuidsProvider(t *testing.T) {
|
||||
suite.Run(t, new(batchUuidsProviderTestSuite))
|
||||
}
|
||||
|
||||
func (suite *batchUuidsProviderTestSuite) TestGetUuidForOneUsername() {
|
||||
expectedUsernames := []string{"username"}
|
||||
expectedResult := &mojang.ProfileInfo{Id: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", Name: "username"}
|
||||
expectedResponse := []*mojang.ProfileInfo{expectedResult}
|
||||
|
||||
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:before_round").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:round", expectedUsernames, 0).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:result", expectedUsernames, expectedResponse, nil).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:after_round").Once()
|
||||
|
||||
suite.MojangApi.On("UsernamesToUuids", expectedUsernames).Once().Return([]*mojang.ProfileInfo{expectedResult}, nil)
|
||||
|
||||
resultChan := suite.GetUuidAsync("username")
|
||||
|
||||
suite.Iterate()
|
||||
|
||||
result := <-resultChan
|
||||
suite.Assert().Equal(expectedResult, result.Result)
|
||||
suite.Assert().Nil(result.Error)
|
||||
}
|
||||
|
||||
func (suite *batchUuidsProviderTestSuite) TestGetUuidForTwoUsernames() {
|
||||
func (suite *batchUuidsProviderTestSuite) TestGetUuidForFewUsernames() {
|
||||
expectedUsernames := []string{"username1", "username2"}
|
||||
expectedResult1 := &mojang.ProfileInfo{Id: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", Name: "username1"}
|
||||
expectedResult2 := &mojang.ProfileInfo{Id: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", Name: "username2"}
|
||||
expectedResponse := []*mojang.ProfileInfo{expectedResult1, expectedResult2}
|
||||
|
||||
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:before_round").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:round", expectedUsernames, 0).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:result", expectedUsernames, expectedResponse, nil).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:after_round").Once()
|
||||
|
||||
suite.MojangApi.On("UsernamesToUuids", expectedUsernames).Once().Return([]*mojang.ProfileInfo{
|
||||
expectedResult1,
|
||||
@@ -194,7 +172,7 @@ func (suite *batchUuidsProviderTestSuite) TestGetUuidForTwoUsernames() {
|
||||
resultChan1 := suite.GetUuidAsync("username1")
|
||||
resultChan2 := suite.GetUuidAsync("username2")
|
||||
|
||||
suite.Iterate()
|
||||
suite.Strategy.Iterate(2, 0)
|
||||
|
||||
result1 := <-resultChan1
|
||||
suite.Assert().Equal(expectedResult1, result1.Result)
|
||||
@@ -205,78 +183,41 @@ func (suite *batchUuidsProviderTestSuite) TestGetUuidForTwoUsernames() {
|
||||
suite.Assert().Nil(result2.Error)
|
||||
}
|
||||
|
||||
func (suite *batchUuidsProviderTestSuite) TestGetUuidForMoreThan10Usernames() {
|
||||
usernames := make([]string, 12)
|
||||
for i := 0; i < cap(usernames); i++ {
|
||||
usernames[i] = randStr(8)
|
||||
}
|
||||
func (suite *batchUuidsProviderTestSuite) TestShouldNotSendRequestWhenNoJobsAreReturned() {
|
||||
//noinspection GoPreferNilSlice
|
||||
emptyUsernames := []string{}
|
||||
done := make(chan struct{})
|
||||
suite.Emitter.On("Emit",
|
||||
"mojang_textures:batch_uuids_provider:round",
|
||||
emptyUsernames,
|
||||
1,
|
||||
).Once().Run(func(args mock.Arguments) {
|
||||
close(done)
|
||||
})
|
||||
|
||||
// In this test we're not testing response, so always return an empty resultset
|
||||
expectedResponse := []*mojang.ProfileInfo{}
|
||||
suite.GetUuidAsync("username") // Schedule one username to run the queue
|
||||
|
||||
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:before_round").Twice()
|
||||
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:round", usernames[0:10], 2).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:result", usernames[0:10], expectedResponse, nil).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:round", usernames[10:12], 0).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:result", usernames[10:12], expectedResponse, nil).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:after_round").Twice()
|
||||
suite.Strategy.Iterate(0, 1) // Return no jobs and indicate that there is one job in queue
|
||||
|
||||
suite.MojangApi.On("UsernamesToUuids", usernames[0:10]).Once().Return(expectedResponse, nil)
|
||||
suite.MojangApi.On("UsernamesToUuids", usernames[10:12]).Once().Return(expectedResponse, nil)
|
||||
|
||||
channels := make([]chan *batchUuidsProviderGetUuidResult, len(usernames))
|
||||
for i, username := range usernames {
|
||||
channels[i] = suite.GetUuidAsync(username)
|
||||
}
|
||||
|
||||
suite.Iterate()
|
||||
suite.Iterate()
|
||||
|
||||
for _, channel := range channels {
|
||||
<-channel
|
||||
}
|
||||
<-done
|
||||
}
|
||||
|
||||
func (suite *batchUuidsProviderTestSuite) TestDoNothingWhenNoTasks() {
|
||||
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:before_round").Times(3)
|
||||
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:round", []string{"username"}, 0).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:result", []string{"username"}, mock.Anything, nil).Once()
|
||||
var nilStringSlice []string
|
||||
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:round", nilStringSlice, 0).Twice()
|
||||
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:after_round").Times(3)
|
||||
|
||||
suite.MojangApi.On("UsernamesToUuids", []string{"username"}).Once().Return([]*mojang.ProfileInfo{}, nil)
|
||||
|
||||
// Perform first iteration and await it finishes
|
||||
resultChan := suite.GetUuidAsync("username")
|
||||
|
||||
suite.Iterate()
|
||||
|
||||
result := <-resultChan
|
||||
suite.Assert().Nil(result.Result)
|
||||
suite.Assert().Nil(result.Error)
|
||||
|
||||
// Let it to perform a few more iterations to ensure, that there are no calls to external APIs
|
||||
suite.Iterate()
|
||||
suite.Iterate()
|
||||
}
|
||||
|
||||
func (suite *batchUuidsProviderTestSuite) TestGetUuidForTwoUsernamesWithAnError() {
|
||||
// Test written for multiple usernames to ensure that the error
|
||||
// will be returned for each iteration group
|
||||
func (suite *batchUuidsProviderTestSuite) TestGetUuidForFewUsernamesWithAnError() {
|
||||
expectedUsernames := []string{"username1", "username2"}
|
||||
expectedError := &mojang.TooManyRequestsError{}
|
||||
var nilProfilesResponse []*mojang.ProfileInfo
|
||||
|
||||
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:before_round").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:round", expectedUsernames, 0).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:result", expectedUsernames, nilProfilesResponse, expectedError).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:batch_uuids_provider:after_round").Once()
|
||||
|
||||
suite.MojangApi.On("UsernamesToUuids", expectedUsernames).Once().Return(nil, expectedError)
|
||||
|
||||
resultChan1 := suite.GetUuidAsync("username1")
|
||||
resultChan2 := suite.GetUuidAsync("username2")
|
||||
|
||||
suite.Iterate()
|
||||
suite.Strategy.Iterate(2, 0)
|
||||
|
||||
result1 := <-resultChan1
|
||||
suite.Assert().Nil(result1.Result)
|
||||
@@ -287,14 +228,214 @@ func (suite *batchUuidsProviderTestSuite) TestGetUuidForTwoUsernamesWithAnError(
|
||||
suite.Assert().Equal(expectedError, result2.Error)
|
||||
}
|
||||
|
||||
var replacer = strings.NewReplacer("-", "_", "=", "")
|
||||
func TestPeriodicStrategy(t *testing.T) {
|
||||
t.Run("should return first job only after duration", func(t *testing.T) {
|
||||
d := 20 * time.Millisecond
|
||||
strategy := NewPeriodicStrategy(d, 10)
|
||||
j := &job{}
|
||||
strategy.Queue(j)
|
||||
|
||||
// https://stackoverflow.com/a/50581165
|
||||
func randStr(len int) string {
|
||||
buff := make([]byte, len)
|
||||
_, _ = rand.Read(buff)
|
||||
str := replacer.Replace(base64.URLEncoding.EncodeToString(buff))
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
startedAt := time.Now()
|
||||
ch := strategy.GetJobs(ctx)
|
||||
iteration := <-ch
|
||||
durationBeforeResult := time.Now().Sub(startedAt)
|
||||
require.True(t, durationBeforeResult >= d)
|
||||
require.True(t, durationBeforeResult < d*2)
|
||||
|
||||
// Base 64 can be longer than len
|
||||
return str[:len]
|
||||
require.Equal(t, []*job{j}, iteration.Jobs)
|
||||
require.Equal(t, 0, iteration.Queue)
|
||||
|
||||
cancel()
|
||||
})
|
||||
|
||||
t.Run("should return the configured batch size", func(t *testing.T) {
|
||||
strategy := NewPeriodicStrategy(0, 10)
|
||||
jobs := make([]*job, 15)
|
||||
for i := 0; i < 15; i++ {
|
||||
jobs[i] = &job{Username: strconv.Itoa(i)}
|
||||
strategy.Queue(jobs[i])
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ch := strategy.GetJobs(ctx)
|
||||
iteration := <-ch
|
||||
require.Len(t, iteration.Jobs, 10)
|
||||
require.Equal(t, jobs[0:10], iteration.Jobs)
|
||||
require.Equal(t, 5, iteration.Queue)
|
||||
|
||||
cancel()
|
||||
})
|
||||
|
||||
t.Run("should not return the next iteration until the previous one is finished", func(t *testing.T) {
|
||||
strategy := NewPeriodicStrategy(0, 10)
|
||||
strategy.Queue(&job{})
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ch := strategy.GetJobs(ctx)
|
||||
iteration := <-ch
|
||||
require.Len(t, iteration.Jobs, 1)
|
||||
require.Equal(t, 0, iteration.Queue)
|
||||
|
||||
time.Sleep(time.Millisecond) // Let strategy's internal loop to work (if the implementation is broken)
|
||||
|
||||
select {
|
||||
case <-ch:
|
||||
require.Fail(t, "the previous iteration isn't marked as done")
|
||||
default:
|
||||
// ok
|
||||
}
|
||||
|
||||
iteration.Done()
|
||||
|
||||
time.Sleep(time.Millisecond) // Let strategy's internal loop to work
|
||||
|
||||
select {
|
||||
case iteration = <-ch:
|
||||
// ok
|
||||
default:
|
||||
require.Fail(t, "iteration should be provided")
|
||||
}
|
||||
|
||||
require.Empty(t, iteration.Jobs)
|
||||
require.Equal(t, 0, iteration.Queue)
|
||||
iteration.Done()
|
||||
|
||||
cancel()
|
||||
})
|
||||
|
||||
t.Run("each iteration should be returned only after the configured duration", func(t *testing.T) {
|
||||
d := 5 * time.Millisecond
|
||||
strategy := NewPeriodicStrategy(d, 10)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ch := strategy.GetJobs(ctx)
|
||||
for i := 0; i < 3; i++ {
|
||||
startedAt := time.Now()
|
||||
iteration := <-ch
|
||||
durationBeforeResult := time.Now().Sub(startedAt)
|
||||
require.True(t, durationBeforeResult >= d)
|
||||
require.True(t, durationBeforeResult < d*2)
|
||||
|
||||
require.Empty(t, iteration.Jobs)
|
||||
require.Equal(t, 0, iteration.Queue)
|
||||
|
||||
// Sleep for at least doubled duration before calling Done() to check,
|
||||
// that this duration isn't included into the next iteration time
|
||||
time.Sleep(d * 2)
|
||||
iteration.Done()
|
||||
}
|
||||
|
||||
cancel()
|
||||
})
|
||||
}
|
||||
|
||||
func TestFullBusStrategy(t *testing.T) {
|
||||
t.Run("should provide iteration immediately when the batch size exceeded", func(t *testing.T) {
|
||||
jobs := make([]*job, 10)
|
||||
for i := 0; i < 10; i++ {
|
||||
jobs[i] = &job{}
|
||||
}
|
||||
|
||||
d := 20 * time.Millisecond
|
||||
strategy := NewFullBusStrategy(d, 10)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ch := strategy.GetJobs(ctx)
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
select {
|
||||
case iteration := <-ch:
|
||||
require.Len(t, iteration.Jobs, 10)
|
||||
require.Equal(t, 0, iteration.Queue)
|
||||
case <-time.After(d):
|
||||
require.Fail(t, "iteration should be provided immediately")
|
||||
}
|
||||
}()
|
||||
|
||||
for _, j := range jobs {
|
||||
strategy.Queue(j)
|
||||
}
|
||||
|
||||
<-done
|
||||
|
||||
cancel()
|
||||
})
|
||||
|
||||
t.Run("should provide iteration after duration if batch size isn't exceeded", func(t *testing.T) {
|
||||
jobs := make([]*job, 9)
|
||||
for i := 0; i < 9; i++ {
|
||||
jobs[i] = &job{}
|
||||
}
|
||||
|
||||
d := 20 * time.Millisecond
|
||||
strategy := NewFullBusStrategy(d, 10)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
startedAt := time.Now()
|
||||
ch := strategy.GetJobs(ctx)
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
iteration := <-ch
|
||||
duration := time.Now().Sub(startedAt)
|
||||
require.True(t, duration >= d, fmt.Sprintf("has %d, expected %d", duration, d))
|
||||
require.True(t, duration < d*2)
|
||||
require.Equal(t, jobs, iteration.Jobs)
|
||||
require.Equal(t, 0, iteration.Queue)
|
||||
}()
|
||||
|
||||
for _, j := range jobs {
|
||||
strategy.Queue(j)
|
||||
}
|
||||
|
||||
<-done
|
||||
|
||||
cancel()
|
||||
})
|
||||
|
||||
t.Run("should provide iteration as soon as the bus is full, without waiting for the previous iteration to finish", func(t *testing.T) {
|
||||
d := 20 * time.Millisecond
|
||||
strategy := NewFullBusStrategy(d, 10)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ch := strategy.GetJobs(ctx)
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
for i := 0; i < 3; i++ {
|
||||
time.Sleep(5 * time.Millisecond) // See comment below
|
||||
select {
|
||||
case iteration := <-ch:
|
||||
require.Len(t, iteration.Jobs, 10)
|
||||
// Don't assert iteration.Queue length since it might be unstable
|
||||
// Don't call iteration.Done()
|
||||
case <-time.After(d):
|
||||
t.Errorf("iteration should be provided as soon as the bus is full")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Scheduled 31 tasks. 3 iterations should be performed immediately
|
||||
// and should be executed only after timeout. The timeout above is used
|
||||
// to increase overall time to ensure, that timer resets on every iteration
|
||||
|
||||
startedAt := time.Now()
|
||||
iteration := <-ch
|
||||
duration := time.Now().Sub(startedAt)
|
||||
require.True(t, duration >= d)
|
||||
require.True(t, duration < d*2)
|
||||
require.Len(t, iteration.Jobs, 1)
|
||||
require.Equal(t, 0, iteration.Queue)
|
||||
}()
|
||||
|
||||
for i := 0; i < 31; i++ {
|
||||
strategy.Queue(&job{})
|
||||
}
|
||||
|
||||
<-done
|
||||
|
||||
cancel()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,12 +5,9 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/elyby/chrly/api/mojang"
|
||||
|
||||
"github.com/tevino/abool"
|
||||
"github.com/elyby/chrly/utils"
|
||||
)
|
||||
|
||||
var now = time.Now
|
||||
|
||||
type inMemoryItem struct {
|
||||
textures *mojang.SignedTexturesResponse
|
||||
timestamp int64
|
||||
@@ -20,9 +17,10 @@ type InMemoryTexturesStorage struct {
|
||||
GCPeriod time.Duration
|
||||
Duration time.Duration
|
||||
|
||||
lock sync.RWMutex
|
||||
data map[string]*inMemoryItem
|
||||
working *abool.AtomicBool
|
||||
once sync.Once
|
||||
lock sync.RWMutex
|
||||
data map[string]*inMemoryItem
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
func NewInMemoryTexturesStorage() *InMemoryTexturesStorage {
|
||||
@@ -35,30 +33,6 @@ func NewInMemoryTexturesStorage() *InMemoryTexturesStorage {
|
||||
return storage
|
||||
}
|
||||
|
||||
func (s *InMemoryTexturesStorage) Start() {
|
||||
if s.working == nil {
|
||||
s.working = abool.New()
|
||||
}
|
||||
|
||||
if !s.working.IsSet() {
|
||||
go func() {
|
||||
time.Sleep(s.GCPeriod)
|
||||
// TODO: this can be reimplemented in future with channels, but right now I have no idea how to make it right
|
||||
for s.working.IsSet() {
|
||||
start := time.Now()
|
||||
s.gc()
|
||||
time.Sleep(s.GCPeriod - time.Since(start))
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
s.working.Set()
|
||||
}
|
||||
|
||||
func (s *InMemoryTexturesStorage) Stop() {
|
||||
s.working.UnSet()
|
||||
}
|
||||
|
||||
func (s *InMemoryTexturesStorage) GetTextures(uuid string) (*mojang.SignedTexturesResponse, error) {
|
||||
s.lock.RLock()
|
||||
defer s.lock.RUnlock()
|
||||
@@ -66,34 +40,43 @@ func (s *InMemoryTexturesStorage) GetTextures(uuid string) (*mojang.SignedTextur
|
||||
item, exists := s.data[uuid]
|
||||
validRange := s.getMinimalNotExpiredTimestamp()
|
||||
if !exists || validRange > item.timestamp {
|
||||
return nil, &ValueNotFound{}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return item.textures, nil
|
||||
}
|
||||
|
||||
func (s *InMemoryTexturesStorage) StoreTextures(uuid string, textures *mojang.SignedTexturesResponse) {
|
||||
var timestamp int64
|
||||
if textures != nil {
|
||||
decoded := textures.DecodeTextures()
|
||||
if decoded == nil {
|
||||
panic("unable to decode textures")
|
||||
}
|
||||
|
||||
timestamp = decoded.Timestamp
|
||||
} else {
|
||||
timestamp = unixNanoToUnixMicro(now().UnixNano())
|
||||
}
|
||||
s.once.Do(s.start)
|
||||
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
s.data[uuid] = &inMemoryItem{
|
||||
textures: textures,
|
||||
timestamp: timestamp,
|
||||
timestamp: utils.UnixMillisecond(time.Now()),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *InMemoryTexturesStorage) start() {
|
||||
s.done = make(chan struct{})
|
||||
ticker := time.NewTicker(s.GCPeriod)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-s.done:
|
||||
return
|
||||
case <-ticker.C:
|
||||
s.gc()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *InMemoryTexturesStorage) Stop() {
|
||||
close(s.done)
|
||||
}
|
||||
|
||||
func (s *InMemoryTexturesStorage) gc() {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
@@ -107,9 +90,5 @@ func (s *InMemoryTexturesStorage) gc() {
|
||||
}
|
||||
|
||||
func (s *InMemoryTexturesStorage) getMinimalNotExpiredTimestamp() int64 {
|
||||
return unixNanoToUnixMicro(now().Add(s.Duration * time.Duration(-1)).UnixNano())
|
||||
}
|
||||
|
||||
func unixNanoToUnixMicro(unixNano int64) int64 {
|
||||
return unixNano / 10e5
|
||||
return utils.UnixMillisecond(time.Now().Add(s.Duration * time.Duration(-1)))
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package mojangtextures
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/elyby/chrly/api/mojang"
|
||||
assert "github.com/stretchr/testify/require"
|
||||
|
||||
testify "github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
"github.com/elyby/chrly/api/mojang"
|
||||
)
|
||||
|
||||
var texturesWithSkin = &mojang.SignedTexturesResponse{
|
||||
@@ -45,156 +45,120 @@ var texturesWithoutSkin = &mojang.SignedTexturesResponse{
|
||||
}
|
||||
|
||||
func TestInMemoryTexturesStorage_GetTextures(t *testing.T) {
|
||||
t.Run("get error when uuid is not exists", func(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
t.Run("should return nil, nil when textures are unavailable", func(t *testing.T) {
|
||||
storage := NewInMemoryTexturesStorage()
|
||||
result, err := storage.GetTextures("b5d58475007d4f9e9ddd1403e2497579")
|
||||
|
||||
assert.Nil(result)
|
||||
assert.Error(err, "value not found in the storage")
|
||||
assert.Nil(t, result)
|
||||
assert.Nil(t, err)
|
||||
})
|
||||
|
||||
t.Run("get textures object, when uuid is stored in the storage", func(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
storage := NewInMemoryTexturesStorage()
|
||||
storage.StoreTextures("dead24f9a4fa4877b7b04c8c6c72bb46", texturesWithSkin)
|
||||
result, err := storage.GetTextures("dead24f9a4fa4877b7b04c8c6c72bb46")
|
||||
|
||||
assert.Equal(texturesWithSkin, result)
|
||||
assert.Nil(err)
|
||||
assert.Equal(t, texturesWithSkin, result)
|
||||
assert.Nil(t, err)
|
||||
})
|
||||
|
||||
t.Run("get error when uuid is exists, but textures are expired", func(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
t.Run("should return nil, nil when textures are exists, but cache duration is expired", func(t *testing.T) {
|
||||
storage := NewInMemoryTexturesStorage()
|
||||
storage.Duration = 10 * time.Millisecond
|
||||
storage.GCPeriod = time.Minute
|
||||
storage.StoreTextures("dead24f9a4fa4877b7b04c8c6c72bb46", texturesWithSkin)
|
||||
|
||||
now = func() time.Time {
|
||||
return time.Now().Add(time.Minute * 2)
|
||||
}
|
||||
time.Sleep(storage.Duration * 2)
|
||||
|
||||
result, err := storage.GetTextures("dead24f9a4fa4877b7b04c8c6c72bb46")
|
||||
|
||||
assert.Nil(result)
|
||||
assert.Error(err, "value not found in the storage")
|
||||
|
||||
now = time.Now
|
||||
assert.Nil(t, result)
|
||||
assert.Nil(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestInMemoryTexturesStorage_StoreTextures(t *testing.T) {
|
||||
t.Run("store textures for previously not existed uuid", func(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
storage := NewInMemoryTexturesStorage()
|
||||
storage.StoreTextures("dead24f9a4fa4877b7b04c8c6c72bb46", texturesWithSkin)
|
||||
result, err := storage.GetTextures("dead24f9a4fa4877b7b04c8c6c72bb46")
|
||||
|
||||
assert.Equal(texturesWithSkin, result)
|
||||
assert.Nil(err)
|
||||
assert.Equal(t, texturesWithSkin, result)
|
||||
assert.Nil(t, err)
|
||||
})
|
||||
|
||||
t.Run("override already existed textures for uuid", func(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
storage := NewInMemoryTexturesStorage()
|
||||
storage.StoreTextures("dead24f9a4fa4877b7b04c8c6c72bb46", texturesWithoutSkin)
|
||||
storage.StoreTextures("dead24f9a4fa4877b7b04c8c6c72bb46", texturesWithSkin)
|
||||
result, err := storage.GetTextures("dead24f9a4fa4877b7b04c8c6c72bb46")
|
||||
|
||||
assert.NotEqual(texturesWithoutSkin, result)
|
||||
assert.Equal(texturesWithSkin, result)
|
||||
assert.Nil(err)
|
||||
assert.NotEqual(t, texturesWithoutSkin, result)
|
||||
assert.Equal(t, texturesWithSkin, result)
|
||||
assert.Nil(t, err)
|
||||
})
|
||||
|
||||
t.Run("store nil textures", func(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
storage := NewInMemoryTexturesStorage()
|
||||
storage.StoreTextures("dead24f9a4fa4877b7b04c8c6c72bb46", nil)
|
||||
result, err := storage.GetTextures("dead24f9a4fa4877b7b04c8c6c72bb46")
|
||||
|
||||
assert.Nil(result)
|
||||
assert.Nil(err)
|
||||
})
|
||||
|
||||
t.Run("should panic if textures prop is not decoded", func(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
toStore := &mojang.SignedTexturesResponse{
|
||||
Id: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||
t.Run("store textures with empty properties", func(t *testing.T) {
|
||||
texturesWithEmptyProps := &mojang.SignedTexturesResponse{
|
||||
Id: "dead24f9a4fa4877b7b04c8c6c72bb46",
|
||||
Name: "mock",
|
||||
Props: []*mojang.Property{},
|
||||
}
|
||||
|
||||
assert.PanicsWithValue("unable to decode textures", func() {
|
||||
storage := NewInMemoryTexturesStorage()
|
||||
storage.StoreTextures("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", toStore)
|
||||
})
|
||||
storage := NewInMemoryTexturesStorage()
|
||||
storage.StoreTextures("dead24f9a4fa4877b7b04c8c6c72bb46", texturesWithEmptyProps)
|
||||
result, err := storage.GetTextures("dead24f9a4fa4877b7b04c8c6c72bb46")
|
||||
|
||||
assert.Exactly(t, texturesWithEmptyProps, result)
|
||||
assert.Nil(t, err)
|
||||
})
|
||||
|
||||
t.Run("store nil textures", func(t *testing.T) {
|
||||
storage := NewInMemoryTexturesStorage()
|
||||
storage.StoreTextures("dead24f9a4fa4877b7b04c8c6c72bb46", nil)
|
||||
result, err := storage.GetTextures("dead24f9a4fa4877b7b04c8c6c72bb46")
|
||||
|
||||
assert.Nil(t, result)
|
||||
assert.Nil(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestInMemoryTexturesStorage_GarbageCollection(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
storage := NewInMemoryTexturesStorage()
|
||||
defer storage.Stop()
|
||||
storage.GCPeriod = 10 * time.Millisecond
|
||||
storage.Duration = 10 * time.Millisecond
|
||||
storage.Duration = 9 * time.Millisecond
|
||||
|
||||
textures1 := &mojang.SignedTexturesResponse{
|
||||
Id: "dead24f9a4fa4877b7b04c8c6c72bb46",
|
||||
Name: "mock1",
|
||||
Props: []*mojang.Property{
|
||||
{
|
||||
Name: "textures",
|
||||
Value: mojang.EncodeTextures(&mojang.TexturesProp{
|
||||
Timestamp: time.Now().Add(storage.GCPeriod-time.Millisecond*time.Duration(5)).UnixNano() / 10e5,
|
||||
ProfileID: "dead24f9a4fa4877b7b04c8c6c72bb46",
|
||||
ProfileName: "mock1",
|
||||
Textures: &mojang.TexturesResponse{},
|
||||
}),
|
||||
},
|
||||
},
|
||||
Id: "dead24f9a4fa4877b7b04c8c6c72bb46",
|
||||
Name: "mock1",
|
||||
Props: []*mojang.Property{},
|
||||
}
|
||||
textures2 := &mojang.SignedTexturesResponse{
|
||||
Id: "b5d58475007d4f9e9ddd1403e2497579",
|
||||
Name: "mock2",
|
||||
Props: []*mojang.Property{
|
||||
{
|
||||
Name: "textures",
|
||||
Value: mojang.EncodeTextures(&mojang.TexturesProp{
|
||||
Timestamp: time.Now().Add(storage.GCPeriod-time.Millisecond*time.Duration(15)).UnixNano() / 10e5,
|
||||
ProfileID: "b5d58475007d4f9e9ddd1403e2497579",
|
||||
ProfileName: "mock2",
|
||||
Textures: &mojang.TexturesResponse{},
|
||||
}),
|
||||
},
|
||||
},
|
||||
Id: "b5d58475007d4f9e9ddd1403e2497579",
|
||||
Name: "mock2",
|
||||
Props: []*mojang.Property{},
|
||||
}
|
||||
|
||||
storage.StoreTextures("dead24f9a4fa4877b7b04c8c6c72bb46", textures1)
|
||||
// Store another texture a bit later to avoid it removing by GC after the first iteration
|
||||
time.Sleep(2 * time.Millisecond)
|
||||
storage.StoreTextures("b5d58475007d4f9e9ddd1403e2497579", textures2)
|
||||
|
||||
storage.Start()
|
||||
storage.lock.RLock()
|
||||
assert.Len(t, storage.data, 2, "the GC period has not yet reached")
|
||||
storage.lock.RUnlock()
|
||||
|
||||
time.Sleep(storage.GCPeriod + time.Millisecond) // Let it start first iteration
|
||||
time.Sleep(storage.GCPeriod) // Let it perform the first GC iteration
|
||||
|
||||
_, textures1Err := storage.GetTextures("dead24f9a4fa4877b7b04c8c6c72bb46")
|
||||
_, textures2Err := storage.GetTextures("b5d58475007d4f9e9ddd1403e2497579")
|
||||
storage.lock.RLock()
|
||||
assert.Len(t, storage.data, 1, "the first texture should be cleaned by GC")
|
||||
assert.Contains(t, storage.data, "b5d58475007d4f9e9ddd1403e2497579")
|
||||
storage.lock.RUnlock()
|
||||
|
||||
assert.Nil(textures1Err)
|
||||
assert.Error(textures2Err)
|
||||
time.Sleep(storage.GCPeriod) // Let another iteration happen
|
||||
|
||||
time.Sleep(storage.GCPeriod + time.Millisecond) // Let another iteration happen
|
||||
|
||||
_, textures1Err = storage.GetTextures("dead24f9a4fa4877b7b04c8c6c72bb46")
|
||||
_, textures2Err = storage.GetTextures("b5d58475007d4f9e9ddd1403e2497579")
|
||||
|
||||
assert.Error(textures1Err)
|
||||
assert.Error(textures2Err)
|
||||
|
||||
storage.Stop()
|
||||
storage.lock.RLock()
|
||||
assert.Len(t, storage.data, 0)
|
||||
storage.lock.RUnlock()
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ type MojangApiTexturesProvider struct {
|
||||
func (ctx *MojangApiTexturesProvider) GetTextures(uuid string) (*mojang.SignedTexturesResponse, error) {
|
||||
ctx.Emit("mojang_textures:mojang_api_textures_provider:before_request", uuid)
|
||||
result, err := uuidToTextures(uuid, true)
|
||||
ctx.Emit("mojang_textures:mojang_api_textures_provider:after_request", result, err)
|
||||
ctx.Emit("mojang_textures:mojang_api_textures_provider:after_request", uuid, result, err)
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
@@ -64,6 +64,7 @@ func (suite *mojangApiTexturesProviderTestSuite) TestGetTextures() {
|
||||
).Once()
|
||||
suite.Emitter.On("Emit",
|
||||
"mojang_textures:mojang_api_textures_provider:after_request",
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
expectedResult,
|
||||
nil,
|
||||
).Once()
|
||||
@@ -85,6 +86,7 @@ func (suite *mojangApiTexturesProviderTestSuite) TestGetTexturesWithError() {
|
||||
).Once()
|
||||
suite.Emitter.On("Emit",
|
||||
"mojang_textures:mojang_api_textures_provider:after_request",
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
expectedResponse,
|
||||
expectedError,
|
||||
).Once()
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package mojangtextures
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -61,8 +60,8 @@ func (c *broadcaster) BroadcastAndRemove(username string, result *broadcastResul
|
||||
delete(c.listeners, username)
|
||||
}
|
||||
|
||||
// https://help.mojang.com/customer/portal/articles/928638
|
||||
var allowedUsernamesRegex = regexp.MustCompile(`^[\w_]{3,16}$`)
|
||||
// https://help.minecraft.net/hc/en-us/articles/4408950195341#h_01GE5JX1Z0CZ833A7S54Y195KV
|
||||
var allowedUsernamesRegex = regexp.MustCompile(`(?i)^[0-9a-z_]{3,16}$`)
|
||||
|
||||
type UUIDsProvider interface {
|
||||
GetUuid(username string) (*mojang.ProfileInfo, error)
|
||||
@@ -92,20 +91,24 @@ func (ctx *Provider) GetForUsername(username string) (*mojang.SignedTexturesResp
|
||||
})
|
||||
|
||||
if !allowedUsernamesRegex.MatchString(username) {
|
||||
return nil, errors.New("invalid username")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
username = strings.ToLower(username)
|
||||
ctx.Emit("mojang_textures:call", username)
|
||||
|
||||
uuid, err := ctx.getUuidFromCache(username)
|
||||
if err == nil && uuid == "" {
|
||||
uuid, found, err := ctx.getUuidFromCache(username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if found && uuid == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if uuid != "" {
|
||||
textures, err := ctx.getTexturesFromCache(uuid)
|
||||
if err == nil {
|
||||
if err == nil && textures != nil {
|
||||
return textures, nil
|
||||
}
|
||||
}
|
||||
@@ -131,7 +134,8 @@ func (ctx *Provider) getResultAndBroadcast(username string, uuid string) {
|
||||
ctx.broadcaster.BroadcastAndRemove(username, result)
|
||||
}
|
||||
|
||||
func (ctx *Provider) getResult(username string, uuid string) *broadcastResult {
|
||||
func (ctx *Provider) getResult(username string, cachedUuid string) *broadcastResult {
|
||||
uuid := cachedUuid
|
||||
if uuid == "" {
|
||||
profile, err := ctx.getUuid(username)
|
||||
if err != nil {
|
||||
@@ -152,6 +156,12 @@ func (ctx *Provider) getResult(username string, uuid string) *broadcastResult {
|
||||
|
||||
textures, err := ctx.getTextures(uuid)
|
||||
if err != nil {
|
||||
// Previously cached UUIDs may disappear
|
||||
// In this case we must invalidate UUID cache for given username
|
||||
if _, ok := err.(*mojang.EmptyResponse); ok && cachedUuid != "" {
|
||||
return ctx.getResult(username, "")
|
||||
}
|
||||
|
||||
return &broadcastResult{nil, err}
|
||||
}
|
||||
|
||||
@@ -162,12 +172,12 @@ func (ctx *Provider) getResult(username string, uuid string) *broadcastResult {
|
||||
return &broadcastResult{textures, nil}
|
||||
}
|
||||
|
||||
func (ctx *Provider) getUuidFromCache(username string) (string, error) {
|
||||
func (ctx *Provider) getUuidFromCache(username string) (string, bool, error) {
|
||||
ctx.Emit("mojang_textures:usernames:before_cache", username)
|
||||
uuid, err := ctx.Storage.GetUuid(username)
|
||||
ctx.Emit("mojang_textures:usernames:after_cache", username, uuid, err)
|
||||
uuid, found, err := ctx.Storage.GetUuid(username)
|
||||
ctx.Emit("mojang_textures:usernames:after_cache", username, uuid, found, err)
|
||||
|
||||
return uuid, err
|
||||
return uuid, found, err
|
||||
}
|
||||
|
||||
func (ctx *Provider) getTexturesFromCache(uuid string) (*mojang.SignedTexturesResponse, error) {
|
||||
|
||||
@@ -122,9 +122,9 @@ type mockStorage struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *mockStorage) GetUuid(username string) (string, error) {
|
||||
func (m *mockStorage) GetUuid(username string) (string, bool, error) {
|
||||
args := m.Called(username)
|
||||
return args.String(0), args.Error(1)
|
||||
return args.String(0), args.Bool(1), args.Error(2)
|
||||
}
|
||||
|
||||
func (m *mockStorage) StoreUuid(username string, uuid string) error {
|
||||
@@ -186,7 +186,7 @@ func (suite *providerTestSuite) TestGetForUsernameWithoutAnyCache() {
|
||||
|
||||
suite.Emitter.On("Emit", "mojang_textures:call", "username").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:before_cache", "username").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:after_cache", "username", "", &ValueNotFound{}).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:after_cache", "username", "", false, nil).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:before_result", "username", "").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:before_call", "username").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:after_call", "username", expectedProfile, nil).Once()
|
||||
@@ -194,7 +194,7 @@ func (suite *providerTestSuite) TestGetForUsernameWithoutAnyCache() {
|
||||
suite.Emitter.On("Emit", "mojang_textures:textures:after_call", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", expectedResult, nil).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:after_result", "username", expectedResult, nil).Once()
|
||||
|
||||
suite.Storage.On("GetUuid", "username").Once().Return("", &ValueNotFound{})
|
||||
suite.Storage.On("GetUuid", "username").Once().Return("", false, nil)
|
||||
suite.Storage.On("StoreUuid", "username", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").Once().Return(nil)
|
||||
suite.Storage.On("StoreTextures", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", expectedResult).Once()
|
||||
|
||||
@@ -213,16 +213,16 @@ func (suite *providerTestSuite) TestGetForUsernameWithCachedUuid() {
|
||||
|
||||
suite.Emitter.On("Emit", "mojang_textures:call", "username").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:before_cache", "username").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:after_cache", "username", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", nil).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:after_cache", "username", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", true, nil).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:textures:before_cache", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:textures:after_cache", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", expectedCachedTextures, &ValueNotFound{}).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:textures:after_cache", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", expectedCachedTextures, nil).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:before_result", "username", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:textures:before_call", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:textures:after_call", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", expectedResult, nil).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:after_result", "username", expectedResult, nil).Once()
|
||||
|
||||
suite.Storage.On("GetUuid", "username").Once().Return("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", nil)
|
||||
suite.Storage.On("GetTextures", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").Once().Return(nil, &ValueNotFound{})
|
||||
suite.Storage.On("GetUuid", "username").Once().Return("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", true, nil)
|
||||
suite.Storage.On("GetTextures", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").Once().Return(nil, nil)
|
||||
suite.Storage.On("StoreTextures", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", expectedResult).Once()
|
||||
|
||||
suite.TexturesProvider.On("GetTextures", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").Return(expectedResult, nil)
|
||||
@@ -238,11 +238,11 @@ func (suite *providerTestSuite) TestGetForUsernameWithFullyCachedResult() {
|
||||
|
||||
suite.Emitter.On("Emit", "mojang_textures:call", "username").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:before_cache", "username").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:after_cache", "username", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", nil).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:after_cache", "username", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", true, nil).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:textures:before_cache", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:textures:after_cache", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", expectedResult, nil).Once()
|
||||
|
||||
suite.Storage.On("GetUuid", "username").Once().Return("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", nil)
|
||||
suite.Storage.On("GetUuid", "username").Once().Return("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", true, nil)
|
||||
suite.Storage.On("GetTextures", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").Once().Return(expectedResult, nil)
|
||||
|
||||
result, err := suite.Provider.GetForUsername("username")
|
||||
@@ -254,9 +254,9 @@ func (suite *providerTestSuite) TestGetForUsernameWithFullyCachedResult() {
|
||||
func (suite *providerTestSuite) TestGetForUsernameWithCachedUnknownUuid() {
|
||||
suite.Emitter.On("Emit", "mojang_textures:call", "username").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:before_cache", "username").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:after_cache", "username", "", nil).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:after_cache", "username", "", true, nil).Once()
|
||||
|
||||
suite.Storage.On("GetUuid", "username").Once().Return("", nil)
|
||||
suite.Storage.On("GetUuid", "username").Once().Return("", true, nil)
|
||||
|
||||
result, err := suite.Provider.GetForUsername("username")
|
||||
|
||||
@@ -270,13 +270,13 @@ func (suite *providerTestSuite) TestGetForUsernameWhichHasNoMojangAccount() {
|
||||
|
||||
suite.Emitter.On("Emit", "mojang_textures:call", "username").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:before_cache", "username").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:after_cache", "username", "", &ValueNotFound{}).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:after_cache", "username", "", false, nil).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:before_result", "username", "").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:before_call", "username").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:after_call", "username", expectedProfile, nil).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:after_result", "username", expectedResult, nil).Once()
|
||||
|
||||
suite.Storage.On("GetUuid", "username").Once().Return("", &ValueNotFound{})
|
||||
suite.Storage.On("GetUuid", "username").Once().Return("", false, nil)
|
||||
suite.Storage.On("StoreUuid", "username", "").Once().Return(nil)
|
||||
|
||||
suite.UuidsProvider.On("GetUuid", "username").Once().Return(nil, nil)
|
||||
@@ -293,7 +293,7 @@ func (suite *providerTestSuite) TestGetForUsernameWhichHasMojangAccountButHasNoM
|
||||
|
||||
suite.Emitter.On("Emit", "mojang_textures:call", "username").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:before_cache", "username").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:after_cache", "username", "", &ValueNotFound{}).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:after_cache", "username", "", false, nil).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:before_result", "username", "").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:before_call", "username").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:after_call", "username", expectedProfile, nil).Once()
|
||||
@@ -301,7 +301,7 @@ func (suite *providerTestSuite) TestGetForUsernameWhichHasMojangAccountButHasNoM
|
||||
suite.Emitter.On("Emit", "mojang_textures:textures:after_call", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", expectedResult, nil).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:after_result", "username", expectedResult, nil).Once()
|
||||
|
||||
suite.Storage.On("GetUuid", "username").Once().Return("", &ValueNotFound{})
|
||||
suite.Storage.On("GetUuid", "username").Once().Return("", false, nil)
|
||||
suite.Storage.On("StoreUuid", "username", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").Once().Return(nil)
|
||||
suite.Storage.On("StoreTextures", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", expectedResult).Once()
|
||||
|
||||
@@ -314,13 +314,49 @@ func (suite *providerTestSuite) TestGetForUsernameWhichHasMojangAccountButHasNoM
|
||||
suite.Assert().Nil(err)
|
||||
}
|
||||
|
||||
// https://github.com/elyby/chrly/issues/29
|
||||
func (suite *providerTestSuite) TestGetForUsernameWithCachedUuidThatHasBeenDisappeared() {
|
||||
expectedErr := &mojang.EmptyResponse{}
|
||||
expectedProfile := &mojang.ProfileInfo{Id: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", Name: "username"}
|
||||
var nilTexturesResponse *mojang.SignedTexturesResponse
|
||||
expectedResult := &mojang.SignedTexturesResponse{Id: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", Name: "username"}
|
||||
|
||||
suite.Emitter.On("Emit", "mojang_textures:call", "username").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:before_cache", "username").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:after_cache", "username", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", true, nil).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:textures:before_cache", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:textures:after_cache", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", nilTexturesResponse, nil).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:before_result", "username", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:textures:before_call", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:textures:after_call", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", nilTexturesResponse, expectedErr).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:before_call", "username").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:after_call", "username", expectedProfile, nil).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:textures:before_call", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:textures:after_call", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", expectedResult, nil).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:after_result", "username", expectedResult, nil).Once()
|
||||
|
||||
suite.Storage.On("GetUuid", "username").Once().Return("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", true, nil)
|
||||
suite.Storage.On("GetTextures", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").Once().Return(nil, nil)
|
||||
suite.Storage.On("StoreUuid", "username", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb").Once().Return(nil)
|
||||
suite.Storage.On("StoreTextures", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", expectedResult).Once()
|
||||
|
||||
suite.UuidsProvider.On("GetUuid", "username").Return(expectedProfile, nil)
|
||||
suite.TexturesProvider.On("GetTextures", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").Return(nil, expectedErr)
|
||||
suite.TexturesProvider.On("GetTextures", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb").Return(expectedResult, nil)
|
||||
|
||||
result, err := suite.Provider.GetForUsername("username")
|
||||
|
||||
suite.Assert().Nil(err)
|
||||
suite.Assert().Equal(expectedResult, result)
|
||||
}
|
||||
|
||||
func (suite *providerTestSuite) TestGetForTheSameUsernames() {
|
||||
expectedProfile := &mojang.ProfileInfo{Id: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", Name: "username"}
|
||||
expectedResult := &mojang.SignedTexturesResponse{Id: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", Name: "username"}
|
||||
|
||||
suite.Emitter.On("Emit", "mojang_textures:call", "username").Twice()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:before_cache", "username").Twice()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:after_cache", "username", "", &ValueNotFound{}).Twice()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:after_cache", "username", "", false, nil).Twice()
|
||||
suite.Emitter.On("Emit", "mojang_textures:already_processing", "username").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:before_result", "username", "").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:before_call", "username").Once()
|
||||
@@ -329,7 +365,7 @@ func (suite *providerTestSuite) TestGetForTheSameUsernames() {
|
||||
suite.Emitter.On("Emit", "mojang_textures:textures:after_call", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", expectedResult, nil).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:after_result", "username", expectedResult, nil).Once()
|
||||
|
||||
suite.Storage.On("GetUuid", "username").Twice().Return("", &ValueNotFound{})
|
||||
suite.Storage.On("GetUuid", "username").Twice().Return("", false, nil)
|
||||
suite.Storage.On("StoreUuid", "username", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").Once().Return(nil)
|
||||
suite.Storage.On("StoreTextures", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", expectedResult).Once()
|
||||
|
||||
@@ -355,10 +391,25 @@ func (suite *providerTestSuite) TestGetForTheSameUsernames() {
|
||||
|
||||
func (suite *providerTestSuite) TestGetForNotAllowedMojangUsername() {
|
||||
result, err := suite.Provider.GetForUsername("Not allowed")
|
||||
suite.Assert().Error(err, "invalid username")
|
||||
suite.Assert().Nil(err)
|
||||
suite.Assert().Nil(result)
|
||||
}
|
||||
|
||||
func (suite *providerTestSuite) TestGetErrorFromUUIDsStorage() {
|
||||
expectedErr := errors.New("mock error")
|
||||
|
||||
suite.Emitter.On("Emit", "mojang_textures:call", "username").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:before_cache", "username").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:after_cache", "username", "", false, expectedErr).Once()
|
||||
|
||||
suite.Storage.On("GetUuid", "username").Once().Return("", false, expectedErr)
|
||||
|
||||
result, err := suite.Provider.GetForUsername("username")
|
||||
|
||||
suite.Assert().Nil(result)
|
||||
suite.Assert().Equal(expectedErr, err)
|
||||
}
|
||||
|
||||
func (suite *providerTestSuite) TestGetErrorFromUuidsProvider() {
|
||||
var expectedProfile *mojang.ProfileInfo
|
||||
var expectedResult *mojang.SignedTexturesResponse
|
||||
@@ -366,13 +417,13 @@ func (suite *providerTestSuite) TestGetErrorFromUuidsProvider() {
|
||||
|
||||
suite.Emitter.On("Emit", "mojang_textures:call", "username").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:before_cache", "username").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:after_cache", "username", "", &ValueNotFound{}).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:after_cache", "username", "", false, nil).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:before_result", "username", "").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:before_call", "username").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:after_call", "username", expectedProfile, err).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:after_result", "username", expectedResult, err).Once()
|
||||
|
||||
suite.Storage.On("GetUuid", "username").Once().Return("", &ValueNotFound{})
|
||||
suite.Storage.On("GetUuid", "username").Once().Return("", false, nil)
|
||||
suite.UuidsProvider.On("GetUuid", "username").Once().Return(nil, err)
|
||||
|
||||
result, resErr := suite.Provider.GetForUsername("username")
|
||||
@@ -387,7 +438,7 @@ func (suite *providerTestSuite) TestGetErrorFromTexturesProvider() {
|
||||
|
||||
suite.Emitter.On("Emit", "mojang_textures:call", "username").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:before_cache", "username").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:after_cache", "username", "", &ValueNotFound{}).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:after_cache", "username", "", false, nil).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:before_result", "username", "").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:before_call", "username").Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:usernames:after_call", "username", expectedProfile, nil).Once()
|
||||
@@ -395,7 +446,7 @@ func (suite *providerTestSuite) TestGetErrorFromTexturesProvider() {
|
||||
suite.Emitter.On("Emit", "mojang_textures:textures:after_call", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", expectedResult, err).Once()
|
||||
suite.Emitter.On("Emit", "mojang_textures:after_result", "username", expectedResult, err).Once()
|
||||
|
||||
suite.Storage.On("GetUuid", "username").Return("", &ValueNotFound{})
|
||||
suite.Storage.On("GetUuid", "username").Return("", false, nil)
|
||||
suite.Storage.On("StoreUuid", "username", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").Return(nil)
|
||||
suite.UuidsProvider.On("GetUuid", "username").Once().Return(expectedProfile, nil)
|
||||
suite.TexturesProvider.On("GetTextures", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").Once().Return(nil, err)
|
||||
|
||||
@@ -19,7 +19,7 @@ var HttpClient = &http.Client{
|
||||
|
||||
type RemoteApiUuidsProvider struct {
|
||||
Emitter
|
||||
Url URL
|
||||
Url URL
|
||||
}
|
||||
|
||||
func (ctx *RemoteApiUuidsProvider) GetUuid(username string) (*mojang.ProfileInfo, error) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/h2non/gock"
|
||||
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
@@ -4,12 +4,13 @@ import (
|
||||
"github.com/elyby/chrly/api/mojang"
|
||||
)
|
||||
|
||||
// UuidsStorage is a key-value storage of Mojang usernames pairs to its UUIDs,
|
||||
// UUIDsStorage is a key-value storage of Mojang usernames pairs to its UUIDs,
|
||||
// used to reduce the load on the account information queue
|
||||
type UuidsStorage interface {
|
||||
// Since only primitive types are used in this method, you should return a special error ValueNotFound
|
||||
// to return the information that no error has occurred and username does not have uuid
|
||||
GetUuid(username string) (string, error)
|
||||
type UUIDsStorage interface {
|
||||
// The second argument indicates whether a record was found in the storage,
|
||||
// since depending on it, the empty value must be interpreted as "no cached record"
|
||||
// or "value cached and has an empty value"
|
||||
GetUuid(username string) (uuid string, found bool, err error)
|
||||
// An empty uuid value can be passed if the corresponding account has not been found
|
||||
StoreUuid(username string, uuid string) error
|
||||
}
|
||||
@@ -24,23 +25,23 @@ type TexturesStorage interface {
|
||||
}
|
||||
|
||||
type Storage interface {
|
||||
UuidsStorage
|
||||
UUIDsStorage
|
||||
TexturesStorage
|
||||
}
|
||||
|
||||
// SeparatedStorage allows you to use separate storage engines to satisfy
|
||||
// the Storage interface
|
||||
type SeparatedStorage struct {
|
||||
UuidsStorage
|
||||
UUIDsStorage
|
||||
TexturesStorage
|
||||
}
|
||||
|
||||
func (s *SeparatedStorage) GetUuid(username string) (string, error) {
|
||||
return s.UuidsStorage.GetUuid(username)
|
||||
func (s *SeparatedStorage) GetUuid(username string) (string, bool, error) {
|
||||
return s.UUIDsStorage.GetUuid(username)
|
||||
}
|
||||
|
||||
func (s *SeparatedStorage) StoreUuid(username string, uuid string) error {
|
||||
return s.UuidsStorage.StoreUuid(username, uuid)
|
||||
return s.UUIDsStorage.StoreUuid(username, uuid)
|
||||
}
|
||||
|
||||
func (s *SeparatedStorage) GetTextures(uuid string) (*mojang.SignedTexturesResponse, error) {
|
||||
@@ -50,12 +51,3 @@ func (s *SeparatedStorage) GetTextures(uuid string) (*mojang.SignedTexturesRespo
|
||||
func (s *SeparatedStorage) StoreTextures(uuid string, textures *mojang.SignedTexturesResponse) {
|
||||
s.TexturesStorage.StoreTextures(uuid, textures)
|
||||
}
|
||||
|
||||
// This error can be used to indicate, that requested
|
||||
// value doesn't exists in the storage
|
||||
type ValueNotFound struct {
|
||||
}
|
||||
|
||||
func (*ValueNotFound) Error() string {
|
||||
return "value not found in the storage"
|
||||
}
|
||||
|
||||
@@ -12,9 +12,9 @@ type uuidsStorageMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *uuidsStorageMock) GetUuid(username string) (string, error) {
|
||||
func (m *uuidsStorageMock) GetUuid(username string) (string, bool, error) {
|
||||
args := m.Called(username)
|
||||
return args.String(0), args.Error(1)
|
||||
return args.String(0), args.Bool(1), args.Error(2)
|
||||
}
|
||||
|
||||
func (m *uuidsStorageMock) StoreUuid(username string, uuid string) error {
|
||||
@@ -50,9 +50,10 @@ func TestSplittedStorage(t *testing.T) {
|
||||
|
||||
t.Run("GetUuid", func(t *testing.T) {
|
||||
storage, uuidsMock, _ := createMockedStorage()
|
||||
uuidsMock.On("GetUuid", "username").Once().Return("find me", nil)
|
||||
result, err := storage.GetUuid("username")
|
||||
uuidsMock.On("GetUuid", "username").Once().Return("find me", true, nil)
|
||||
result, found, err := storage.GetUuid("username")
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, found)
|
||||
assert.Equal(t, "find me", result)
|
||||
uuidsMock.AssertExpectations(t)
|
||||
})
|
||||
@@ -82,8 +83,3 @@ func TestSplittedStorage(t *testing.T) {
|
||||
texturesMock.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
|
||||
func TestValueNotFound_Error(t *testing.T) {
|
||||
err := &ValueNotFound{}
|
||||
assert.Equal(t, "value not found in the storage", err.Error())
|
||||
}
|
||||
|
||||
42
signer/signer.go
Normal file
42
signer/signer.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package signer
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha1"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
)
|
||||
|
||||
var randomReader = rand.Reader
|
||||
|
||||
type Signer struct {
|
||||
Key *rsa.PrivateKey
|
||||
}
|
||||
|
||||
func (s *Signer) SignTextures(textures string) (string, error) {
|
||||
if s.Key == nil {
|
||||
return "", errors.New("Key is empty")
|
||||
}
|
||||
|
||||
message := []byte(textures)
|
||||
messageHash := sha1.New()
|
||||
_, _ = messageHash.Write(message)
|
||||
messageHashSum := messageHash.Sum(nil)
|
||||
|
||||
signature, err := rsa.SignPKCS1v15(randomReader, s.Key, crypto.SHA1, messageHashSum)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return base64.StdEncoding.EncodeToString(signature), nil
|
||||
}
|
||||
|
||||
func (s *Signer) GetPublicKey() (*rsa.PublicKey, error) {
|
||||
if s.Key == nil {
|
||||
return nil, errors.New("Key is empty")
|
||||
}
|
||||
|
||||
return &s.Key.PublicKey, nil
|
||||
}
|
||||
64
signer/signer_test.go
Normal file
64
signer/signer_test.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package signer
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
|
||||
"testing"
|
||||
|
||||
assert "github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type ConstantReader struct {
|
||||
}
|
||||
|
||||
func (c *ConstantReader) Read(p []byte) (int, error) {
|
||||
return 1, nil
|
||||
}
|
||||
|
||||
func TestSigner_SignTextures(t *testing.T) {
|
||||
randomReader = &ConstantReader{}
|
||||
|
||||
t.Run("sign textures", func(t *testing.T) {
|
||||
rawKey, _ := pem.Decode([]byte("-----BEGIN RSA PRIVATE KEY-----\nMIIBOwIBAAJBANbUpVCZkMKpfvYZ08W3lumdAaYxLBnmUDlzHBQH3DpYef5WCO32\nTDU6feIJ58A0lAywgtZ4wwi2dGHOz/1hAvcCAwEAAQJAItaxSHTe6PKbyEU/9pxj\nONdhYRYwVLLo56gnMYhkyoEqaaMsfov8hhoepkYZBMvZFB2bDOsQ2SaJ+E2eiBO4\nAQIhAPssS0+BR9w0bOdmjGqmdE9NrN5UJQcOW13s29+6QzUBAiEA2vWOepA5Apiu\npEA3pwoGdkVCrNSnnKjDQzDXBnpd3/cCIEFNd9sY4qUG4FWdXN6RnmXL7Sj0uZfH\nDMwzu8rEM5sBAiEAhvdoDNqLmbMdq3c+FsPSOeL1d21Zp/JK8kbPtFmHNf8CIQDV\n6FSZDwvWfuxaM7BsycQONkjDBTPNu+lqctJBGnBv3A==\n-----END RSA PRIVATE KEY-----\n"))
|
||||
key, _ := x509.ParsePKCS1PrivateKey(rawKey.Bytes)
|
||||
|
||||
signer := &Signer{key}
|
||||
|
||||
signature, err := signer.SignTextures("eyJ0aW1lc3RhbXAiOjE2MTQzMDcxMzQsInByb2ZpbGVJZCI6ImZmYzhmZGM5NTgyNDUwOWU4YTU3Yzk5Yjk0MGZiOTk2IiwicHJvZmlsZU5hbWUiOiJFcmlja1NrcmF1Y2giLCJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly9lbHkuYnkvc3RvcmFnZS9za2lucy82OWM2NzQwZDI5OTNlNWQ2ZjZhN2ZjOTI0MjBlZmMyOS5wbmcifX0sImVseSI6dHJ1ZX0")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "IyHCxTP5ITquEXTHcwCtLd08jWWy16JwlQeWg8naxhoAVQecHGRdzHRscuxtdq/446kmeox7h4EfRN2A2ZLL+A==", signature)
|
||||
})
|
||||
|
||||
t.Run("empty key", func(t *testing.T) {
|
||||
signer := &Signer{}
|
||||
|
||||
signature, err := signer.SignTextures("hello world")
|
||||
assert.Error(t, err, "Key is empty")
|
||||
assert.Empty(t, signature)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSigner_GetPublicKey(t *testing.T) {
|
||||
randomReader = &ConstantReader{}
|
||||
|
||||
t.Run("get public key", func(t *testing.T) {
|
||||
rawKey, _ := pem.Decode([]byte("-----BEGIN RSA PRIVATE KEY-----\nMIIBOwIBAAJBANbUpVCZkMKpfvYZ08W3lumdAaYxLBnmUDlzHBQH3DpYef5WCO32\nTDU6feIJ58A0lAywgtZ4wwi2dGHOz/1hAvcCAwEAAQJAItaxSHTe6PKbyEU/9pxj\nONdhYRYwVLLo56gnMYhkyoEqaaMsfov8hhoepkYZBMvZFB2bDOsQ2SaJ+E2eiBO4\nAQIhAPssS0+BR9w0bOdmjGqmdE9NrN5UJQcOW13s29+6QzUBAiEA2vWOepA5Apiu\npEA3pwoGdkVCrNSnnKjDQzDXBnpd3/cCIEFNd9sY4qUG4FWdXN6RnmXL7Sj0uZfH\nDMwzu8rEM5sBAiEAhvdoDNqLmbMdq3c+FsPSOeL1d21Zp/JK8kbPtFmHNf8CIQDV\n6FSZDwvWfuxaM7BsycQONkjDBTPNu+lqctJBGnBv3A==\n-----END RSA PRIVATE KEY-----\n"))
|
||||
key, _ := x509.ParsePKCS1PrivateKey(rawKey.Bytes)
|
||||
|
||||
signer := &Signer{key}
|
||||
|
||||
publicKey, err := signer.GetPublicKey()
|
||||
assert.NoError(t, err)
|
||||
assert.IsType(t, &rsa.PublicKey{}, publicKey)
|
||||
})
|
||||
|
||||
t.Run("empty key", func(t *testing.T) {
|
||||
signer := &Signer{}
|
||||
|
||||
publicKey, err := signer.GetPublicKey()
|
||||
assert.Error(t, err, "Key is empty")
|
||||
assert.Nil(t, publicKey)
|
||||
})
|
||||
}
|
||||
7
utils/time.go
Normal file
7
utils/time.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package utils
|
||||
|
||||
import "time"
|
||||
|
||||
func UnixMillisecond(t time.Time) int64 {
|
||||
return t.UnixNano() / int64(time.Millisecond)
|
||||
}
|
||||
16
utils/time_test.go
Normal file
16
utils/time_test.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"testing"
|
||||
|
||||
assert "github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUnixMillisecond(t *testing.T) {
|
||||
loc, _ := time.LoadLocation("CET")
|
||||
d := time.Date(2021, 02, 26, 00, 43, 57, 987654321, loc)
|
||||
|
||||
assert.Equal(t, int64(1614296637987), UnixMillisecond(d))
|
||||
}
|
||||
Reference in New Issue
Block a user