Compare commits

...

21 Commits

Author SHA1 Message Date
ErickSkrauch
cfe8fea3f7 Add signature to the custom profile property when ?unsigned=false 2024-07-09 18:37:47 +02:00
ErickSkrauch
27c7b79b32 Added onUnknownProfileRespondWithUuid param to the /profile endpoint
Introducing profiles endpoint was a mistake, but we had to deal with that mistake until I'll remove it. The Accounts service needs textures with a signature. But it is possible that a user has a fresh account and Chrly has not yet received the profile information. In this case we have no way to get textures for the player. Adding the onUnknownProfileRespondWithUuid parameter solves this problem. This is a bad solution and nobody should use it except Ely.by infrastructure. In v5 version the texture signature on Chrly will be removed.
2024-06-11 02:31:47 +02:00
ErickSkrauch
ad31fdb709 Quick fix for production data inconsistency 2023-12-22 18:45:38 +01:00
ErickSkrauch
fa62d45d00 Update mojang api username filter 2023-12-22 02:00:31 +01:00
ErickSkrauch
cadb89f00a Fixes #40. Allow to upload profile information without a skin
Remove skin file uploading stubs
2023-12-22 01:56:02 +01:00
ErickSkrauch
20ba78953b Handle absence of the additional reporters 2023-12-14 03:01:55 +01:00
ErickSkrauch
883a7bda3c Fixes #8. Replace radix v2 with v4 2023-12-14 02:16:24 +01:00
ErickSkrauch
d678f61df7 Upgrade dependencies 2023-12-14 02:16:24 +01:00
ErickSkrauch
1543e98b87 Restore codecov export and update README badges 2023-12-13 17:09:46 +01:00
ErickSkrauch
0e0b41d6d7 Update LICENSE 2023-12-13 15:11:07 +01:00
ErickSkrauch
3cd12acc1b Export profile requests metrics to statsd 2023-12-13 01:56:40 +01:00
ErickSkrauch
dac4ed0ac6 Update CHANGELOG 2023-12-13 01:51:22 +01:00
ErickSkrauch
11a779c670 Another version CI fix 2023-12-13 01:34:15 +01:00
ErickSkrauch
6cb5e1eb42 Fix version generation 2023-12-13 01:23:53 +01:00
ErickSkrauch
8959e53270 Generate version and commit refs for built docker image 2023-12-13 01:09:21 +01:00
ErickSkrauch
3526570dd3 Run release only when previous step succeeded 2023-12-12 02:37:52 +01:00
ErickSkrauch
4b7f1346f5 Add an edge tag for master releases 2023-12-12 02:30:27 +01:00
ErickSkrauch
e7721f9e5a Enable pushing 2023-12-12 02:21:22 +01:00
ErickSkrauch
26bfbd1517 Read secrets from secrets 2023-12-12 02:13:19 +01:00
ErickSkrauch
ecbb06b83c Fix release pipeline and upgrade Dockerfile 2023-12-12 02:06:00 +01:00
ErickSkrauch
6accffed45 Replace dep with go mod, migrate from travis to github-actions 2023-12-12 01:35:08 +01:00
40 changed files with 779 additions and 886 deletions

View File

@@ -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
View 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
View 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 }}

View File

@@ -1,66 +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}}"
- export BUILD_TAGS=""
- if [ "$DOCKER_TAG" == "dev" ]; then export BUILD_TAGS="$BUILD_TAGS --tags profiling"; fi
- >
env CGO_ENABLED=0 GOOS=linux GOARCH=amd64
go build
$BUILD_TAGS
-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

View File

@@ -5,6 +5,24 @@ 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

View File

@@ -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"]

350
Gopkg.lock generated
View File

@@ -1,350 +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: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:cc4eb6813da8d08694e557fcafae8fcc24f47f61a0717f952da130ca9a486dfc"
name = "github.com/stretchr/testify"
packages = [
"assert",
"mock",
"require",
"suite",
]
pruneopts = ""
revision = "3ebf1ddaeb260c4b1ae502a01c7844fa8c1fa0e9"
version = "v1.5.1"
[[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]]
digest = "1:2a7fe9cf6ed8bca3f0ef319592db1621ce244e7ffefba3c2c21ee40d6898a9c8"
name = "gopkg.in/h2non/gock.v1"
packages = ["."]
pruneopts = ""
revision = "3ffff9b1aa8200275a5eb219c5f9c62bd27acb31"
version = "v1.0.15"
[[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/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/thedevsaddam/govalidator",
"gopkg.in/h2non/gock.v1",
]
solver-name = "gps-cdcl"
solver-version = 1

View File

@@ -1,56 +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/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 = "gopkg.in/h2non/gock.v1"
version = "^1.0.15"

View File

@@ -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.

View File

@@ -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
@@ -244,11 +243,12 @@ Response example:
"properties": [
{
"name": "textures",
"signature": "signature value",
"signature": "textures signature value",
"value": "base64 encoded value"
},
{
"name": "chrly",
"signature": "custom property signature value",
"value": "how do you tame a horse in Minecraft?"
}
]
@@ -345,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:**
@@ -362,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:
@@ -454,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
@@ -493,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

View File

@@ -4,7 +4,7 @@ import (
"net/http"
"testing"
"gopkg.in/h2non/gock.v1"
"github.com/h2non/gock"
testify "github.com/stretchr/testify/assert"
)

View File

@@ -6,7 +6,7 @@ import (
"os"
"strings"
. "github.com/goava/di"
. "github.com/defval/di"
"github.com/spf13/cobra"
"github.com/spf13/viper"

View File

@@ -1,3 +1,4 @@
//go:build profiling
// +build profiling
package cmd

View File

@@ -3,6 +3,7 @@ package redis
import (
"bytes"
"compress/zlib"
"context"
"encoding/json"
"fmt"
"io"
@@ -10,23 +11,22 @@ import (
"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"
)
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,45 +199,68 @@ func removeByUsername(username string, conn util.Cmder) error {
return nil
}
conn.Cmd("MULTI")
err = conn.Do(ctx, radix.Cmd(nil, "MULTI"))
if err != nil {
return err
}
conn.Cmd("DEL", buildUsernameKey(record.Username))
conn.Cmd("HDEL", accountIdToUsernameKey, record.UserId)
err = conn.Do(ctx, radix.Cmd(nil, "DEL", buildUsernameKey(record.Username)))
if err != nil {
return err
}
conn.Cmd("EXEC")
err = conn.Do(ctx, radix.FlatCmd(nil, "HDEL", accountIdToUsernameKey, record.UserId))
if err != nil {
return err
}
return nil
return conn.Do(ctx, radix.Cmd(nil, "EXEC"))
}
func (db *Redis) GetUuid(username string) (string, bool, error) {
conn, err := db.pool.Get()
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
}
defer db.pool.Put(conn)
return findMojangUuidByUsername(username, conn)
}
func findMojangUuidByUsername(username string, conn util.Cmder) (string, bool, error) {
key := strings.ToLower(username)
response := conn.Cmd("HGET", mojangUsernameToUuidKey, key)
if response.IsType(redis.Nil) {
if result == "" {
return "", false, nil
}
data, _ := response.Str()
parts := strings.Split(data, ":")
parts := strings.Split(result, ":")
// https://github.com/elyby/chrly/issues/28
if len(parts) < 2 {
conn.Cmd("HDEL", mojangUsernameToUuidKey, key)
return "", false, fmt.Errorf("Got unexpected response from the mojangUsernameToUuid hash: \"%s\"", data)
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()) {
conn.Cmd("HDEL", mojangUsernameToUuidKey, key)
err = conn.Do(ctx, radix.Cmd(nil, "HDEL", mojangUsernameToUuidKey, key))
if err != nil {
return "", false, err
}
return "", false, nil
}
@@ -222,36 +268,23 @@ func findMojangUuidByUsername(username string, conn util.Cmder) (string, bool, e
}
func (db *Redis) StoreUuid(username string, uuid string) error {
conn, err := db.pool.Get()
if err != nil {
return err
}
defer db.pool.Put(conn)
return storeMojangUuid(username, uuid, conn)
return db.client.Do(db.context, radix.WithConn("", func(ctx context.Context, conn radix.Conn) error {
return storeMojangUuid(ctx, conn, username, uuid)
}))
}
func storeMojangUuid(username string, uuid string, conn util.Cmder) error {
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
}
func (db *Redis) Avail() int {
return db.pool.Avail()
return db.client.Do(db.context, radix.Cmd(nil, "PING"))
}
func buildUsernameKey(username string) string {

View File

@@ -1,33 +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"
)
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)
})
@@ -38,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")
}
@@ -84,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,
@@ -95,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() {
@@ -181,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() {
@@ -210,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,
@@ -224,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)
})
}
@@ -245,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() {
@@ -258,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() {
@@ -279,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() {
@@ -292,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() {
@@ -355,7 +373,7 @@ func (suite *redisTestSuite) TestGetUuid() {
suite.Require().Nil(err)
resp := suite.cmd("HGET", "hash:mojang-username-to-uuid", "mock")
suite.Require().True(resp.IsType(redis.Nil), "should cleanup expired records")
suite.Require().Empty(resp, "should cleanup expired records")
})
suite.RunSubTest("exists, but corrupted record", func() {
@@ -371,7 +389,7 @@ func (suite *redisTestSuite) TestGetUuid() {
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().True(resp.IsType(redis.Nil), "should cleanup expired records")
suite.Require().Empty(resp, "should cleanup expired records")
})
}
@@ -385,9 +403,7 @@ func (suite *redisTestSuite) TestStoreUuid() {
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")
suite.Require().Equal(resp, "d3ca513eb3e14946b58047f2bd3530fd:1587435016")
})
suite.RunSubTest("store empty uuid", func() {
@@ -399,9 +415,7 @@ func (suite *redisTestSuite) TestStoreUuid() {
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, ":1587435016")
suite.Require().Equal(resp, ":1587435016")
})
}
@@ -409,8 +423,3 @@ func (suite *redisTestSuite) TestPing() {
err := suite.Redis.Ping()
suite.Require().Nil(err)
}
func (suite *redisTestSuite) TestAvail() {
avail := suite.Redis.Avail()
suite.Require().True(avail > 0)
}

View File

@@ -1,7 +1,7 @@
package di
import (
"github.com/goava/di"
"github.com/defval/di"
"github.com/spf13/viper"
)

View File

@@ -4,9 +4,8 @@ import (
"context"
"fmt"
"path"
"time"
"github.com/goava/di"
"github.com/defval/di"
"github.com/spf13/viper"
"github.com/elyby/chrly/db/fs"
@@ -38,6 +37,7 @@ func newRedis(container *di.Container, config *viper.Viper) (*redis.Redis, error
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"),
)
@@ -45,12 +45,6 @@ func newRedis(container *di.Container, config *viper.Viper) (*redis.Redis, error
return nil, err
}
if err := container.Provide(func() es.ReporterFunc {
return es.AvailableRedisPoolSizeReporter(conn, time.Second, context.Background())
}, di.As(new(es.Reporter))); err != nil {
return nil, err
}
if err := container.Provide(func() *namedHealthChecker {
return &namedHealthChecker{
Name: "redis",

View File

@@ -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(

View File

@@ -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)
}

View File

@@ -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"
@@ -75,14 +76,14 @@ func newHandlerFactory(
}
err := container.Invoke(enableReporters)
if err != nil {
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
}
@@ -105,19 +106,24 @@ func newSkinsystemHandler(
capesRepository CapesRepository,
mojangTexturesProvider MojangTexturesProvider,
texturesSigner TexturesSigner,
) *mux.Router {
) (*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,
TexturesSigner: texturesSigner,
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(skinsRepository SkinsRepository) *mux.Router {

View File

@@ -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"

View File

@@ -6,7 +6,7 @@ import (
"net/url"
"time"
"github.com/goava/di"
"github.com/defval/di"
"github.com/spf13/viper"
"github.com/elyby/chrly/api/mojang"

View File

@@ -7,8 +7,8 @@ import (
"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"

View File

@@ -9,7 +9,7 @@ import (
. "github.com/elyby/chrly/signer"
"strings"
"github.com/goava/di"
"github.com/defval/di"
"github.com/spf13/viper"
)

View File

@@ -6,7 +6,7 @@ import (
"sync"
"time"
"github.com/etherlabsio/healthcheck"
"github.com/etherlabsio/healthcheck/v2"
"github.com/elyby/chrly/api/mojang"
)

View File

@@ -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)

View File

@@ -1,7 +1,6 @@
package eventsubscribers
import (
"context"
"net/http"
"strings"
"sync"
@@ -30,7 +29,7 @@ func (f ReporterFunc) Enable(reporter slf.StatsReporter) {
f(reporter)
}
// TODO: rework all reporters in the same style as AvailableRedisPoolSizeReporter
// 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)
@@ -133,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/") {
@@ -187,24 +188,3 @@ func (s *StatsReporter) finalizeTimeRecording(timeKey string, statName string) {
s.RecordTimer(statName, time.Since(startedAt))
}
type RedisPoolCheckable interface {
Avail() int
}
func AvailableRedisPoolSizeReporter(pool RedisPoolCheckable, d time.Duration, stop context.Context) ReporterFunc {
return func(reporter slf.StatsReporter) {
go func() {
ticker := time.NewTicker(d)
for {
select {
case <-stop.Done():
ticker.Stop()
return
case <-ticker.C:
reporter.UpdateGauge("redis.pool.available", int64(pool.Avail()))
}
}
}()
}
}

View File

@@ -1,7 +1,6 @@
package eventsubscribers
import (
"context"
"errors"
"net/http/httptest"
"testing"
@@ -99,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)},
@@ -393,30 +400,3 @@ func TestStatsReporter(t *testing.T) {
})
}
}
type redisPoolCheckableMock struct {
mock.Mock
}
func (r *redisPoolCheckableMock) Avail() int {
return r.Called().Int(0)
}
func TestAvailableRedisPoolSizeReporter(t *testing.T) {
poolMock := &redisPoolCheckableMock{}
poolMock.On("Avail").Return(5).Times(3)
reporterMock := &StatsReporterMock{}
reporterMock.On("UpdateGauge", "redis.pool.available", int64(5)).Times(3)
ctx, cancel := context.WithCancel(context.Background())
creator := AvailableRedisPoolSizeReporter(poolMock, 10*time.Millisecond, ctx)
creator(reporterMock)
time.Sleep(35 * time.Millisecond)
cancel()
poolMock.AssertExpectations(t)
reporterMock.AssertExpectations(t)
}

55
go.mod Normal file
View 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
View 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=

View File

@@ -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)
@@ -163,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

View File

@@ -6,7 +6,6 @@ import (
"errors"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"net/http/httptest"
"net/url"
@@ -136,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{
@@ -271,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"
@@ -280,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))
})
}
/**************************************

View File

@@ -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)

View File

@@ -6,7 +6,7 @@ import (
"encoding/base64"
"encoding/json"
"encoding/pem"
"github.com/elyby/chrly/utils"
"fmt"
"io"
"net/http"
"strings"
@@ -16,6 +16,7 @@ import (
"github.com/elyby/chrly/api/mojang"
"github.com/elyby/chrly/model"
"github.com/elyby/chrly/utils"
)
var timeNow = time.Now
@@ -43,12 +44,39 @@ type TexturesSigner interface {
type Skinsystem struct {
Emitter
SkinsRepo SkinsRepository
CapesRepo CapesRepository
MojangTexturesProvider MojangTexturesProvider
TexturesSigner TexturesSigner
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 {
@@ -190,8 +218,15 @@ func (ctx *Skinsystem) profileHandler(response http.ResponseWriter, request *htt
}
if profile == nil {
response.WriteHeader(http.StatusNoContent)
return
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{
@@ -208,14 +243,20 @@ func (ctx *Skinsystem) profileHandler(response http.ResponseWriter, request *htt
Name: "textures",
Value: texturesPropEncodedValue,
}
customProp := &mojang.Property{
Name: ctx.TexturesExtraParamName,
Value: ctx.TexturesExtraParamValue,
}
if request.URL.Query().Get("unsigned") == "false" {
signature, err := ctx.TexturesSigner.SignTextures(texturesProp.Value)
customProp.Signature = ctx.texturesExtraParamSignature
texturesSignature, err := ctx.TexturesSigner.SignTextures(texturesProp.Value)
if err != nil {
panic(err)
}
texturesProp.Signature = signature
texturesProp.Signature = texturesSignature
}
profileResponse := &mojang.SignedTexturesResponse{
@@ -223,10 +264,7 @@ func (ctx *Skinsystem) profileHandler(response http.ResponseWriter, request *htt
Name: profile.Username,
Props: []*mojang.Property{
texturesProp,
{
Name: ctx.TexturesExtraParamName,
Value: ctx.TexturesExtraParamValue,
},
customProp,
},
}
@@ -264,7 +302,8 @@ func (ctx *Skinsystem) signatureVerificationKeyHandler(response http.ResponseWri
}
// TODO: in v5 should be extracted into some ProfileProvider interface,
// which will encapsulate all logics, declared in this method
//
// 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"])
@@ -273,21 +312,14 @@ func (ctx *Skinsystem) getProfile(request *http.Request, proxy bool) (*profile,
return nil, err
}
profile := &profile{
Id: "",
Username: "",
Textures: &mojang.TexturesResponse{}, // Field must be initialized to avoid "null" after json encoding
CapeFile: nil,
MojangTextures: "",
MojangSignature: "",
}
profile := createEmptyProfile()
if skin != nil {
profile.Id = strings.Replace(skin.Uuid, "-", "", -1)
profile.Id = formatUuid(skin.Uuid)
profile.Username = skin.Username
}
if skin != nil && skin.SkinId != 0 {
if skin != nil && skin.Url != "" {
profile.Textures.Skin = &mojang.SkinTexturesResponse{
Url: skin.Url,
}
@@ -349,6 +381,8 @@ func (ctx *Skinsystem) getProfile(request *http.Request, proxy bool) (*profile,
profile.Id = mojangProfile.Id
profile.Username = mojangProfile.Name
}
} else if profile.Id != "" {
return profile, nil
} else {
return nil, nil
}
@@ -356,6 +390,16 @@ func (ctx *Skinsystem) getProfile(request *http.Request, proxy bool) (*profile,
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")
}

View File

@@ -11,6 +11,7 @@ import (
"io/ioutil"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"time"
@@ -141,15 +142,17 @@ func (suite *skinsystemTestSuite) SetupTest() {
suite.TexturesSigner = &texturesSignerMock{}
suite.Emitter = &emitterMock{}
suite.App = &Skinsystem{
SkinsRepo: suite.SkinsRepository,
CapesRepo: suite.CapesRepository,
MojangTexturesProvider: suite.MojangTexturesProvider,
TexturesSigner: suite.TexturesSigner,
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() {
@@ -742,11 +745,12 @@ func (suite *skinsystemTestSuite) TestSignedTextures() {
***************************/
type profileTestCase struct {
Name string
Signed bool
BeforeTest func(suite *skinsystemTestSuite)
PanicErr string
AfterTest func(suite *skinsystemTestSuite, response *http.Response)
Name string
Signed bool
ForceResponse string
BeforeTest func(suite *skinsystemTestSuite)
PanicErr string
AfterTest func(suite *skinsystemTestSuite, response *http.Response)
}
var profileTestsCases = []*profileTestCase{
@@ -799,6 +803,7 @@ var profileTestsCases = []*profileTestCase{
},
{
"name": "texturesParamName",
"signature": "texturesParamSignature",
"value": "texturesParamValue"
}
]
@@ -828,6 +833,7 @@ var profileTestsCases = []*profileTestCase{
},
{
"name": "texturesParamName",
"signature": "texturesParamSignature",
"value": "texturesParamValue"
}
]
@@ -857,6 +863,7 @@ var profileTestsCases = []*profileTestCase{
},
{
"name": "texturesParamName",
"signature": "texturesParamSignature",
"value": "texturesParamValue"
}
]
@@ -890,6 +897,7 @@ var profileTestsCases = []*profileTestCase{
},
{
"name": "texturesParamName",
"signature": "texturesParamSignature",
"value": "texturesParamValue"
}
]
@@ -923,6 +931,7 @@ var profileTestsCases = []*profileTestCase{
},
{
"name": "texturesParamName",
"signature": "texturesParamSignature",
"value": "texturesParamValue"
}
]
@@ -952,6 +961,7 @@ var profileTestsCases = []*profileTestCase{
},
{
"name": "texturesParamName",
"signature": "texturesParamSignature",
"value": "texturesParamValue"
}
]
@@ -981,6 +991,7 @@ var profileTestsCases = []*profileTestCase{
},
{
"name": "texturesParamName",
"signature": "texturesParamSignature",
"value": "texturesParamValue"
}
]
@@ -1010,6 +1021,7 @@ var profileTestsCases = []*profileTestCase{
},
{
"name": "texturesParamName",
"signature": "texturesParamSignature",
"value": "texturesParamValue"
}
]
@@ -1028,6 +1040,37 @@ var profileTestsCases = []*profileTestCase{
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) {
@@ -1060,12 +1103,18 @@ func (suite *skinsystemTestSuite) TestProfile() {
suite.RunSubTest(testCase.Name, func() {
testCase.BeforeTest(suite)
url := "http://chrly/profile/mock_username"
u, _ := url.Parse("http://chrly/profile/mock_username")
q := make(url.Values)
if testCase.Signed {
url += "?unsigned=false"
q.Set("unsigned", "false")
}
req := httptest.NewRequest("GET", url, nil)
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 != "" {

View File

@@ -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"`

View File

@@ -412,7 +412,8 @@ func TestFullBusStrategy(t *testing.T) {
// Don't assert iteration.Queue length since it might be unstable
// Don't call iteration.Done()
case <-time.After(d):
t.Fatalf("iteration should be provided as soon as the bus is full")
t.Errorf("iteration should be provided as soon as the bus is full")
return
}
}

View File

@@ -60,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)

View File

@@ -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) {

View File

@@ -6,7 +6,7 @@ import (
. "net/url"
"testing"
"gopkg.in/h2non/gock.v1"
"github.com/h2non/gock"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite"