/*
* This file is part of LanternServer, licensed under the MIT License (MIT).
*
* Copyright (c) LanternPowered <https://www.lanternpowered.org>
* Copyright (c) SpongePowered <https://www.spongepowered.org>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the Software), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.lanternpowered.server.config.user.ban;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.reflect.TypeToken;
import ninja.leaping.configurate.ConfigurationOptions;
import ninja.leaping.configurate.objectmapping.Setting;
import ninja.leaping.configurate.objectmapping.serialize.TypeSerializer;
import ninja.leaping.configurate.objectmapping.serialize.TypeSerializerCollection;
import org.lanternpowered.server.config.ConfigBase;
import org.lanternpowered.server.config.user.UserStorage;
import org.lanternpowered.server.game.Lantern;
import org.lanternpowered.server.util.collect.Lists2;
import org.spongepowered.api.Sponge;
import org.spongepowered.api.entity.living.player.Player;
import org.spongepowered.api.event.Event;
import org.spongepowered.api.event.SpongeEventFactory;
import org.spongepowered.api.event.cause.Cause;
import org.spongepowered.api.profile.GameProfile;
import org.spongepowered.api.service.ban.BanService;
import org.spongepowered.api.service.user.UserStorageService;
import org.spongepowered.api.util.GuavaCollectors;
import org.spongepowered.api.util.ban.Ban;
import org.spongepowered.api.util.ban.Ban.Ip;
import java.io.IOException;
import java.net.InetAddress;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.function.Supplier;
@SuppressWarnings({"unchecked", "rawtypes"})
public final class BanConfig extends ConfigBase implements UserStorage<BanEntry>, BanService {
public static final ConfigurationOptions OPTIONS;
static {
final TypeSerializerCollection typeSerializers = DEFAULT_OPTIONS.getSerializers();
final TypeSerializer banSerializer = new BanEntrySerializer();
final TypeSerializer ipTypeSerializer = typeSerializers.get(TypeToken.of(BanEntry.Ip.class));
final TypeSerializer userTypeSerializer = typeSerializers.get(TypeToken.of(BanEntry.Profile.class));
typeSerializers.registerType(TypeToken.of(Ban.class), banSerializer)
.registerType(TypeToken.of(BanEntry.class), banSerializer)
.registerType(TypeToken.of(BanEntry.Ip.class), ipTypeSerializer)
.registerType(TypeToken.of(Ban.Ip.class), ipTypeSerializer)
.registerType(TypeToken.of(BanEntry.Profile.class), userTypeSerializer)
.registerType(TypeToken.of(Ban.Profile.class), userTypeSerializer);
OPTIONS = ConfigurationOptions.defaults().setSerializers(typeSerializers);
}
@Setting(value = "entries")
private List<BanEntry> entries = Lists.newArrayList();
// A version of the entries list that allows concurrent operations
private final List<BanEntry> entries0 = Collections.synchronizedList(Lists2.createExpirableValueListWithPredicate(BanEntry::isExpired));
public BanConfig(Path path) throws IOException {
super(path, OPTIONS, false);
}
@Override
public void save() throws IOException {
synchronized (this) {
this.entries.clear();
this.entries.addAll(this.entries0);
super.save();
}
}
@Override
public void load() throws IOException {
synchronized (this) {
super.load();
this.entries0.clear();
this.entries0.addAll(this.entries);
}
}
@Override
public Optional<BanEntry> getEntryByUUID(UUID uniqueId) {
return this.entries0.stream().filter(e -> e instanceof BanEntry.Profile &&
((BanEntry.Profile) e).getProfile().getUniqueId().equals(uniqueId)).findFirst();
}
@Override
public Optional<BanEntry> getEntryByName(String username) {
return this.entries0.stream().filter(e -> {
if (!(e instanceof BanEntry.Profile)) {
return false;
}
final Optional<String> optName = ((BanEntry.Profile) e).getProfile().getName();
return optName.isPresent() && optName.get().equalsIgnoreCase(username);
}).findFirst();
}
@Override
public Optional<BanEntry> getEntryByProfile(GameProfile gameProfile) {
return this.getEntryByUUID(gameProfile.getUniqueId());
}
/**
* Gets a {@link BanEntry.Ip} for the specified {@link InetAddress}. May return
* {@link Optional#empty()} if not found.
*
* @param address The address
* @return The ban entry
*/
public Optional<BanEntry> getEntryByIp(InetAddress address) {
final String address0 = checkNotNull(address, "address").getHostAddress();
return (Optional) this.entries0.stream().filter(e -> e instanceof BanEntry.Ip &&
((BanEntry.Ip) e).getAddress().getHostAddress().equalsIgnoreCase(address0)).findFirst();
}
@Override
public void addEntry(BanEntry entry) {
this.addBan(entry);
}
@Override
public boolean removeEntry(UUID uniqueId) {
final Optional<BanEntry> ban = this.getEntryByUUID(checkNotNull(uniqueId, "uniqueId"));
return ban.isPresent() && this.removeBan(ban.get());
}
@Override
public Collection<? extends Ban> getBans() {
return ImmutableList.copyOf(this.entries0);
}
@Override
public Collection<Ban.Profile> getProfileBans() {
return (Collection) this.entries0.stream().filter(e -> e instanceof Ban.Profile).collect(GuavaCollectors.toImmutableList());
}
@Override
public Collection<Ban.Ip> getIpBans() {
return (Collection) this.entries0.stream().filter(e -> e instanceof Ban.Ip).collect(GuavaCollectors.toImmutableList());
}
@Override
public Optional<Ban.Profile> getBanFor(GameProfile profile) {
return (Optional) this.getEntryByProfile(profile);
}
@Override
public Optional<Ip> getBanFor(InetAddress address) {
return (Optional) this.getEntryByIp(address);
}
@Override
public boolean isBanned(GameProfile profile) {
return this.getBanFor(profile).isPresent();
}
@Override
public boolean isBanned(InetAddress address) {
return this.getBanFor(address).isPresent();
}
@Override
public boolean pardon(GameProfile profile) {
final Optional<Ban.Profile> ban = this.getBanFor(checkNotNull(profile, "profile"));
if (ban.isPresent()) {
return this.removeBan(ban.get());
}
return false;
}
@Override
public boolean pardon(InetAddress address) {
Optional<Ban.Ip> ban = this.getBanFor(checkNotNull(address, "address"));
return ban.isPresent() && this.removeBan(ban.get());
}
/**
* Removes the specified {@link Ban} and calls the proper event
* if needed with the specified {@link Cause}.
*
* @param ban The ban
* @param causeSupplier The cause supplier
* @return Whether a ban was removed
*/
public boolean removeBan(Ban ban, Supplier<Cause> causeSupplier) {
checkNotNull(ban, "ban");
checkNotNull(causeSupplier, "causeSupplier");
if (this.entries0.remove(ban)) {
// Post the pardon events
Event event;
if (ban instanceof Ban.Ip) {
event = SpongeEventFactory.createPardonIpEvent(causeSupplier.get(), (Ban.Ip) ban);
} else {
Ban.Profile profileBan = (Ban.Profile) ban;
Cause cause = causeSupplier.get();
// Check if the pardoned player is online (not yet been kicked)
Optional<Player> optTarget = Sponge.getServer().getPlayer(profileBan.getProfile().getUniqueId());
if (optTarget.isPresent()) {
event = SpongeEventFactory.createPardonUserEventTargetPlayer(cause, profileBan, optTarget.get(), optTarget.get());
} else {
event = SpongeEventFactory.createPardonUserEvent(cause, profileBan, Lantern.getGame().getServiceManager()
.provideUnchecked(UserStorageService.class).getOrCreate(profileBan.getProfile()));
}
}
// Just ignore for now the fact that they may be cancellable,
// only the PardonIpEvent seems to be cancellable
// TODO: Should they all be cancellable or none of them?
Sponge.getEventManager().post(event);
return true;
}
return false;
}
private static Supplier<Cause> getCauseSupplierFor(Ban ban) {
return () -> {
Object src = ban.getBanCommandSource().orElse(null);
if (src == null) {
src = ban.getBanSource().orElse(null);
}
return Cause.source(src == null ? ban : src).build();
};
}
@Override
public boolean removeBan(Ban ban) {
checkNotNull(ban, "ban");
return this.removeBan(ban, getCauseSupplierFor(ban));
}
/**
* Adds the specified {@link Ban} and calls the proper event
* if needed with the specified {@link Cause}.
*
* @param ban The ban
* @param causeSupplier The cause supplier
* @return The previous ban attached to new bans profile or ip address
*/
public Optional<? extends Ban> addBan(Ban ban, Supplier<Cause> causeSupplier) {
checkNotNull(ban, "ban");
checkNotNull(causeSupplier, "causeSupplier");
Optional<Ban> oldBan;
if (ban instanceof Ban.Ip) {
oldBan = (Optional) this.getBanFor(((Ban.Ip) ban).getAddress());
} else {
oldBan = (Optional) this.getBanFor(((Ban.Profile) ban).getProfile());
}
oldBan.ifPresent(this.entries0::remove);
this.entries0.add((BanEntry) ban);
if (!oldBan.isPresent() || !oldBan.get().equals(ban)) {
// Post the ban events
Event event;
if (ban instanceof Ban.Ip) {
event = SpongeEventFactory.createBanIpEvent(causeSupplier.get(), (Ban.Ip) ban);
} else {
Ban.Profile profileBan = (Ban.Profile) ban;
Cause cause = causeSupplier.get();
// Check if the pardoned player is online (not yet been kicked)
Optional<Player> optTarget = Sponge.getServer().getPlayer(profileBan.getProfile().getUniqueId());
if (optTarget.isPresent()) {
event = SpongeEventFactory.createBanUserEventTargetPlayer(cause, profileBan, optTarget.get(), optTarget.get());
} else {
event = SpongeEventFactory.createBanUserEvent(cause, profileBan, Lantern.getGame().getServiceManager()
.provideUnchecked(UserStorageService.class).getOrCreate(profileBan.getProfile()));
}
}
// Just ignore for now the fact that they may be cancellable,
// only the PardonIpEvent seems to be cancellable
// TODO: Should they all be cancellable or none of them?
Sponge.getEventManager().post(event);
}
return oldBan;
}
@Override
public Optional<? extends Ban> addBan(Ban ban) {
checkNotNull(ban, "ban");
return this.addBan(ban, getCauseSupplierFor(ban));
}
@Override
public boolean hasBan(Ban ban) {
return this.entries0.contains(checkNotNull(ban, "ban"));
}
@Override
public Collection<BanEntry> getEntries() {
return ImmutableList.copyOf(this.entries0);
}
}