示例#1
0
        public async Task <CharacterListResponse> GetCharacters()
        {
            int accountId = ClaimsReader.GetUserIdInt(User);

            //So to check characters we just need to query for the
            //characters with this account id
            int[] characterIds = await CharacterRepository.CharacterIdsForAccountId(accountId);

#warning This is just for the test build, we need to change this
            ProjectVersionStage.AssertInternalTesting();
            if (characterIds.Length == 0)
            {
                //We just create a new one for testing.
                bool result = await CharacterRepository.TryCreateAsync(new CharacterEntryModel(accountId, ClaimsReader.GetUserName(User)))
                              .ConfigureAwait(false);

                if (result)
                {
                    //Just return the get, a character should now exist.
                    return(await GetCharacters());
                }
                else
                {
                    return(new CharacterListResponse(CharacterListResponseCode.NoCharactersFoundError));
                }
            }

            /*if(characterIds.Length == 0)
             *      return new CharacterListResponse(CharacterListResponseCode.NoCharactersFoundError);*/

            //The reason we only provide the IDs is all other character data can be looked up
            //by the client when it needs it. Like name query, visible/character details/look stuff.
            //No reason to send all this data when they may only need names. Which can be queried through the known API
            return(new CharacterListResponse(characterIds));
        }
        public async Task <IActionResult> RequestWorldDownloadUrl(
            [FromRoute(Name = "id")] long worldId,
            [FromServices] IWorldEntryRepository worldEntryRepository,
            [FromServices] IStorageUrlBuilder urlBuilder,
            [FromServices] IContentDownloadAuthroizationValidator downloadAuthorizer)
        {
            if (worldEntryRepository == null)
            {
                throw new ArgumentNullException(nameof(worldEntryRepository));
            }

            //TODO: We want to rate limit access to this API
            //TODO: We should use both app logging but also another logging service that always gets hit

            //TODO: Consolidate this shared logic between controllers
            if (Logger.IsEnabled(LogLevel.Information))
            {
                Logger.LogInformation($"Recieved {nameof(RequestWorldDownloadUrl)} request from {ClaimsReader.GetUserName(User)}:{ClaimsReader.GetUserId(User)}.");
            }

            //TODO: We should probably check the flags of world to see if it's private (IE hidden from user). Or if it's unlisted or removed.
            //It's possible a user is requesting a world that doesn't exist
            //Could be malicious or it could have been deleted for whatever reason
            if (!await worldEntryRepository.ContainsAsync(worldId).ConfigureAwait(false))
            {
                return(Json(new ContentDownloadURLResponse(ContentDownloadURLResponseCode.NoContentId)));
            }

            //TODO: Refactor this into a validation dependency
            //Now we need to do some validation to determine if they should even be downloading this world
            //we do not want people downloading a world they have no business of going to
            int userId = ClaimsReader.GetUserIdInt(User);

            if (!await downloadAuthorizer.CanUserAccessWorldContet(userId, worldId))
            {
                return(Json(new ContentDownloadURLResponse(ContentDownloadURLResponseCode.AuthorizationFailed)));
            }

            //We can get the URL from the urlbuilder if we provide the world storage GUID
            string downloadUrl = await urlBuilder.BuildRetrivalUrl(UserContentType.World, (await worldEntryRepository.RetrieveAsync(worldId)).StorageGuid);

            //TODO: Should we be validating S3 availability?
            if (String.IsNullOrEmpty(downloadUrl))
            {
                if (Logger.IsEnabled(LogLevel.Error))
                {
                    Logger.LogError($"Failed to create world upload URL for {ClaimsReader.GetUserName(User)}:{ClaimsReader.GetUserId(User)} with ID: {worldId}.");
                }

                return(Json(new ContentDownloadURLResponse(ContentDownloadURLResponseCode.ContentDownloadServiceUnavailable)));
            }

            if (Logger.IsEnabled(LogLevel.Information))
            {
                Logger.LogInformation($"Success. Sending {ClaimsReader.GetUserName(User)} URL: {downloadUrl}");
            }

            return(Json(new ContentDownloadURLResponse(downloadUrl)));
        }
        public async Task <CharacterSessionDataResponse> GetCharacterSessionData([FromRoute(Name = "id")] int characterId)
        {
            int accountId = ClaimsReader.GetUserIdInt(User);

            //TODO: Do we want to expose this to non-controlers?
            //First we should validate that the account that is authorized owns the character it is requesting session data from
            return(await RetrieveSessionDataIfAvailable(characterId, accountId)
                   .ConfigureAwait(false));
        }
        /// <summary>
        /// Verifies that the provided <see cref="characterId"/>
        /// is owned by the current User claim.
        /// </summary>
        /// <param name="characterId"></param>
        /// <returns></returns>
        public async Task <bool> VerifyCharacterOwnedByAccount(int characterId)
        {
            int accountId = ClaimsReader.GetUserIdInt(User);

            //TODO: Do we want to expose this to non-controlers?
            //First we should validate that the account that is authorized owns the character it is requesting session data from

            return((await CharacterRepository.CharacterIdsForAccountId(accountId).ConfigureAwait(false))
                   .Contains(characterId));
        }
        public async Task <IActionResult> SetWorldAsUploaded([FromRoute(Name = "id")] long worldId, [FromServices] IWorldEntryRepository worldEntryRepository, [FromServices] IAmazonS3 storageClient)
        {
            //At this point, the user is telling us they finished uploading the world.
            //They could be lying so we should check that the resource exists AND
            //we should also check that it's an asset bundle and gather some information from the header.

            //First we verify a world exists with this id
            if (!await worldEntryRepository.ContainsAsync(worldId).ConfigureAwait(false))
            {
                //TODO: We should say something more specific
                return(BadRequest());
            }

            WorldEntryModel model = await worldEntryRepository.RetrieveAsync(worldId)
                                    .ConfigureAwait(false);

            //Check the model is associated with this account. Only 1 account can own a world resource
            if (model.AccountId != ClaimsReader.GetUserIdInt(User))
            {
                return(Unauthorized());
            }

            //Now that we know the world is in the database and the account making this authorized requests owns it
            //we can now actually check that the resource exists on the storeage system
            //TODO: This relies on some outdated API/deprecated stuff.
            bool resourceExists = await S3ResourceExists(storageClient, "projectvindictiveworlds-dev", model.StorageGuid)
                                  .ConfigureAwait(false); //TODO: Don't hardcore bucket name

            //TODO: Be more descriptive
            if (!resourceExists)
            {
                return(NotFound());
            }

            //Ok, so the user IS the resource owner AND he did upload something, so let's validate the assetbundle header.
            //TODO: Refactor this into an object that does the validation and generates readable data
            //TODO: Actually implement asset bundle validation
            //We haven't implemented this yet, we should do asset bundle parsing and validation
            //This REALLY important to prevent invalid bundles from being uploaded
            //or content that isn't even an asset bundle being uploaded
            //See: https://github.com/HearthSim/UnityPack/wiki/Format-Documentation

            //For now, since it's unimplemented let's just set it validated
            await worldEntryRepository.SetWorldValidated(model.WorldId)
            .ConfigureAwait(false);

            return(Ok());
        }
        public async Task <IActionResult> RequestWorldUploadUrl([FromServices] IWorldEntryRepository worldEntryRepository, [FromServices] IStorageUrlBuilder urlBuilder)
        {
            if (worldEntryRepository == null)
            {
                throw new ArgumentNullException(nameof(worldEntryRepository));
            }

            //TODO: We want to rate limit access to this API
            //TODO: We should use both app logging but also another logging service that always gets hit

            if (Logger.IsEnabled(LogLevel.Information))
            {
                Logger.LogInformation($"Recieved {nameof(RequestWorldUploadUrl)} request from {ClaimsReader.GetUserName(User)}:{ClaimsReader.GetUserId(User)}.");
            }

            int userId = ClaimsReader.GetUserIdInt(User);

            //TODO: We should send this if we can't get a user id
            //return new JsonResult(RequestedUrlResponseModel.CreateFailure("Failed to authorize action.", RequestedUrlResponseCode.AuthorizationFailed));
            //TODO: Abstract this behind an issuer
            Guid worldGuid = Guid.NewGuid();

            //TODO: Check if the result is valid? We should maybe return bool from this API (we do return bool from this API now)
            //The idea is to create an entry which will contain a GUID. From that GUID we can then generate the upload URL
            WorldEntryModel world  = new WorldEntryModel(userId, this.HttpContext.Connection.RemoteIpAddress.ToString(), worldGuid);
            bool            result = await worldEntryRepository.TryCreateAsync(world);  //TODO: Ok to just provide a guid right?

            //TODO: Check world's worldid has been set

            string uploadUrl = await urlBuilder.BuildUploadUrl(UserContentType.World, worldGuid);

            if (String.IsNullOrEmpty(uploadUrl))
            {
                if (Logger.IsEnabled(LogLevel.Error))
                {
                    Logger.LogError($"Failed to create world upload URL for {ClaimsReader.GetUserName(User)}:{ClaimsReader.GetUserId(User)} with GUID: {worldGuid}.");
                }

                return(new JsonResult(RequestedUrlResponseModel.CreateFailure("Upload service unavailable.", RequestedUrlResponseCode.ServiceUnavailable)));
            }

            if (Logger.IsEnabled(LogLevel.Information))
            {
                Logger.LogInformation($"Success. Sending {ClaimsReader.GetUserName(User)} URL: {uploadUrl}");
            }

            return(new JsonResult(RequestedUrlResponseModel.CreateSuccess(uploadUrl, world.WorldId)));
        }
        public async Task <CharacterSessionEnterResponse> EnterSession([FromRoute(Name = "id")] int characterId)
        {
            if (!await IsCharacterIdValidForUser(characterId, CharacterRepository))
            {
                return(new CharacterSessionEnterResponse(CharacterSessionEnterResponseCode.InvalidCharacterIdError));
            }

            int accountId = ClaimsReader.GetUserIdInt(User);

            //This checks to see if the account, not just the character, has an active session.
            //We do this before we check anything to reject quick even though the query behind this
            //may be abit more expensive
            //As a note, this checks (or should) CLAIMED SESSIONS. So, it won't prevent multiple session entries for an account
            //This is good because we actually use the left over session data to re-enter the instances on disconnect.
            if (await CharacterSessionRepository.AccountHasActiveSession(accountId))
            {
                return(new CharacterSessionEnterResponse(CharacterSessionEnterResponseCode.AccountAlreadyHasCharacterSession));
            }

            //They may have a session entry already, which is ok. So long as they don't have an active claimed session
            //which the above query checks for.
            bool hasSession = await CharacterSessionRepository.ContainsAsync(characterId);

            //We need to check active or not
            if (hasSession)
            {
                //If it's active we can just retrieve the data and send them off on their way
                CharacterSessionModel sessionModel = await CharacterSessionRepository.RetrieveAsync(characterId);

                //TODO: Handle case when we have an inactive session that can be claimed
                return(new CharacterSessionEnterResponse(sessionModel.ZoneId));
            }

            //If we've made it this far we'll need to create a session (because one does not exist) for the character
            //but we need player location data first (if they've never entered the world they won't have any
            //TODO: Handle location loading
            //TODO: Handle deafult
            if (!await CharacterSessionRepository.TryCreateAsync(new CharacterSessionModel(characterId, 1)))
            {
                return(new CharacterSessionEnterResponse(CharacterSessionEnterResponseCode.GeneralServerError));
            }

            //TODO: Better zone handling
            return(new CharacterSessionEnterResponse(1));
        }
        public async Task <CharacterSessionEnterResponse> SetCharacterSessionData([FromRoute(Name = "charid")] int characterId, [FromBody] int zoneId)
        {
            if (!await VerifyCharacterOwnedByAccount(characterId))
            {
                //TODO: Return not authed in JSON
                return(new CharacterSessionEnterResponse(CharacterSessionEnterResponseCode.InvalidCharacterIdError));
            }

            //This case is actually pretty likely, they will likely be trying to move to another server
            //before their active session is cleaned up. Retry logic will be required to get past this.
            if (await CharacterSessionRepository.AccountHasActiveSession(ClaimsReader.GetUserIdInt(User)))
            {
                //TODO: Return JSON that says active session
                return(new CharacterSessionEnterResponse(CharacterSessionEnterResponseCode.AccountAlreadyHasCharacterSession));
            }

            //We can't check if it contains and then update, because that will there is a data race with
            //zoneserver deregisteration and cascading delteing.
            //So we only check for removal, and then remove and create

            //Don't try to delete the claimed session, we should try to delete the current session data. Since they want to change zones
            //and we consider that a new session
            if (!await CharacterSessionRepository.TryDeleteAsync(characterId))
            {
                //TODO: This could fail, potentially removed during race. But it's ok. It's ok that it's gone but may want to log it
            }

            //TODO: Refactor
            if (!await CharacterSessionRepository.TryCreateAsync(new CharacterSessionModel(characterId, zoneId)))
            {
                //Not sure what it wrong, no way to know really.
                return(new CharacterSessionEnterResponse(CharacterSessionEnterResponseCode.GeneralServerError));
            }

            //It passed, they're allowed to join this zone.
            return(new CharacterSessionEnterResponse(zoneId));
        }
示例#9
0
        public async Task <IActionResult> CreateCharacter([FromRoute] string name)
        {
            if (string.IsNullOrWhiteSpace(name))
            {
                throw new ArgumentException("Value cannot be null or whitespace.", nameof(name));
            }

            int accountId = ClaimsReader.GetUserIdInt(User);

            bool nameIsAvailable = await ValidateNameAvailability(name);

            if (!nameIsAvailable)
            {
                return(BadRequest(new CharacterCreationResponse(CharacterCreationResponseCode.NameUnavailableError)));
            }

            //TODO: Don't expose the database table model
            //Otherwise we should try to create. There is a race condition here that can cause it to still fail
            //since others could create a character with this name before we finish after checking
            bool result = await CharacterRepository.TryCreateAsync(new CharacterEntryModel(accountId, name));

            //TODO: JSON
            return(Created("TODO", new CharacterCreationResponse(CharacterCreationResponseCode.Success)));
        }
        /// <inheritdoc />
        public async Task <HubOnConnectionState> OnConnected([JetBrains.Annotations.NotNull] Hub hubConnectedTo)
        {
            if (hubConnectedTo == null)
            {
                throw new ArgumentNullException(nameof(hubConnectedTo));
            }

            //We should never be here unless auth worked
            //so we can assume that and just try to request character session data
            //for the account.
            CharacterSessionDataResponse characterSessionDataResponse = await SocialToGameClient.GetCharacterSessionDataByAccount(ClaimsReader.GetUserIdInt(hubConnectedTo.Context.User))
                                                                        .ConfigureAwait(false);

            //TODO: To support website chat we shouldn't disconnect just because they don't have a zone session.
            //If the session data request fails we should just abort
            //and disconnect, the user shouldn't be connecting
            if (!characterSessionDataResponse.isSuccessful)
            {
                if (Logger.IsEnabled(LogLevel.Warning))
                {
                    Logger.LogWarning($"Failed to Query SessionData for AccountId: {ClaimsReader.GetUserId(hubConnectedTo.Context.User)} Reason: {characterSessionDataResponse.ResultCode}");
                }

                //TODO: Eventually we don't want to do this.
                return(HubOnConnectionState.Abort);
            }

            //This is ABSOLUTELY CRITICAL we need to validate that the character header they sent actually
            //is the character they have a session as
            //NOT CHECKING THIS IS EQUIVALENT TO LETTING USERS PRETEND THEY ARE ANYONE!
            if (hubConnectedTo.Context.UserIdentifier != characterSessionDataResponse.CharacterId.ToString())
            {
                //We can log account name and id here, because they were successfully authed.
                if (Logger.IsEnabled(LogLevel.Warning))
                {
                    Logger.LogWarning($"User with AccountId: {ClaimsReader.GetUserName(hubConnectedTo.Context.User)}:{ClaimsReader.GetUserId(hubConnectedTo.Context.User)} attempted to spoof as CharacterId: {hubConnectedTo.Context.UserIdentifier} but had session for CharacterID: {characterSessionDataResponse.CharacterId}.");
                }

                return(HubOnConnectionState.Abort);
            }

            if (Logger.IsEnabled(LogLevel.Information))
            {
                Logger.LogInformation($"Recieved SessionData: Id: {characterSessionDataResponse.CharacterId} ZoneId: {characterSessionDataResponse.ZoneId}");
            }

            //Registers for lookup so that we can tell where a connection is zone-wise.
            ZoneLookupService.Register(hubConnectedTo.Context.ConnectionId, characterSessionDataResponse.ZoneId);

            //TODO: We should have group name builders. Not hardcoded
            //Join the zoneserver's chat channel group
            await hubConnectedTo.Groups.AddToGroupAsync(hubConnectedTo.Context.ConnectionId, $"zone:{characterSessionDataResponse.ZoneId}", hubConnectedTo.Context.ConnectionAborted)
            .ConfigureAwait(false);

            return(HubOnConnectionState.Success);
        }
 /// <summary>
 /// Indicates if the provided character id is valid for the user in the message context.
 /// </summary>
 /// <param name="characterId">The id to check.</param>
 /// <param name="characterRepository">The character repository service.</param>
 /// <returns>True if the character id is valid/</returns>
 private async Task <bool> IsCharacterIdValidForUser(int characterId, ICharacterRepository characterRepository)
 {
     //We only support positive character ids so if they request a less than 0 it's invalid and likely spoofed
     //or if they request an id they don't own
     //or if it's an not a known character
     return(characterId >= 0 &&
            await characterRepository.ContainsAsync(characterId) &&
            (await characterRepository.RetrieveAsync(characterId)).AccountId == ClaimsReader.GetUserIdInt(User));
 }