mirror of
https://github.com/imgfloat/server.git
synced 2026-02-05 11:49:25 +00:00
Remove server validation
This commit is contained in:
@@ -7,6 +7,7 @@ import static org.springframework.http.HttpStatus.NOT_FOUND;
|
||||
import dev.kruhlmann.imgfloat.model.AdminRequest;
|
||||
import dev.kruhlmann.imgfloat.model.AssetView;
|
||||
import dev.kruhlmann.imgfloat.model.CanvasSettingsRequest;
|
||||
import dev.kruhlmann.imgfloat.model.CodeAssetRequest;
|
||||
import dev.kruhlmann.imgfloat.model.OauthSessionUser;
|
||||
import dev.kruhlmann.imgfloat.model.PlaybackRequest;
|
||||
import dev.kruhlmann.imgfloat.model.TransformRequest;
|
||||
@@ -236,6 +237,48 @@ public class ChannelApiController {
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/assets/code")
|
||||
public ResponseEntity<AssetView> createCodeAsset(
|
||||
@PathVariable("broadcaster") String broadcaster,
|
||||
@Valid @RequestBody CodeAssetRequest request,
|
||||
OAuth2AuthenticationToken oauthToken
|
||||
) {
|
||||
String sessionUsername = OauthSessionUser.from(oauthToken).login();
|
||||
String logBroadcaster = LogSanitizer.sanitize(broadcaster);
|
||||
String logSessionUsername = LogSanitizer.sanitize(sessionUsername);
|
||||
authorizationService.userIsBroadcasterOrChannelAdminForBroadcasterOrThrowHttpError(
|
||||
broadcaster,
|
||||
sessionUsername
|
||||
);
|
||||
LOG.info("Creating custom script for {} by {}", logBroadcaster, logSessionUsername);
|
||||
return channelDirectoryService
|
||||
.createCodeAsset(broadcaster, request)
|
||||
.map(ResponseEntity::ok)
|
||||
.orElseThrow(() -> new ResponseStatusException(BAD_REQUEST, "Unable to save custom script"));
|
||||
}
|
||||
|
||||
@PutMapping("/assets/{assetId}/code")
|
||||
public ResponseEntity<AssetView> updateCodeAsset(
|
||||
@PathVariable("broadcaster") String broadcaster,
|
||||
@PathVariable("assetId") String assetId,
|
||||
@Valid @RequestBody CodeAssetRequest request,
|
||||
OAuth2AuthenticationToken oauthToken
|
||||
) {
|
||||
String sessionUsername = OauthSessionUser.from(oauthToken).login();
|
||||
String logBroadcaster = LogSanitizer.sanitize(broadcaster);
|
||||
String logSessionUsername = LogSanitizer.sanitize(sessionUsername);
|
||||
String logAssetId = LogSanitizer.sanitize(assetId);
|
||||
authorizationService.userIsBroadcasterOrChannelAdminForBroadcasterOrThrowHttpError(
|
||||
broadcaster,
|
||||
sessionUsername
|
||||
);
|
||||
LOG.info("Updating custom script {} for {} by {}", logAssetId, logBroadcaster, logSessionUsername);
|
||||
return channelDirectoryService
|
||||
.updateCodeAsset(broadcaster, assetId, request)
|
||||
.map(ResponseEntity::ok)
|
||||
.orElseThrow(() -> new ResponseStatusException(NOT_FOUND, "Asset not found"));
|
||||
}
|
||||
|
||||
@PutMapping("/assets/{assetId}/transform")
|
||||
public ResponseEntity<AssetView> transform(
|
||||
@PathVariable("broadcaster") String broadcaster,
|
||||
|
||||
@@ -38,6 +38,15 @@ public class AssetEvent {
|
||||
return event;
|
||||
}
|
||||
|
||||
public static AssetEvent updated(String channel, AssetView asset) {
|
||||
AssetEvent event = new AssetEvent();
|
||||
event.type = Type.UPDATED;
|
||||
event.channel = channel;
|
||||
event.payload = asset;
|
||||
event.assetId = asset.id();
|
||||
return event;
|
||||
}
|
||||
|
||||
public static AssetEvent play(String channel, AssetView asset, boolean play) {
|
||||
AssetEvent event = new AssetEvent();
|
||||
event.type = Type.PLAY;
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package dev.kruhlmann.imgfloat.model;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
public class CodeAssetRequest {
|
||||
|
||||
@NotBlank
|
||||
private String name;
|
||||
|
||||
@NotBlank
|
||||
private String source;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getSource() {
|
||||
return source;
|
||||
}
|
||||
|
||||
public void setSource(String source) {
|
||||
this.source = source;
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import dev.kruhlmann.imgfloat.model.AssetView;
|
||||
import dev.kruhlmann.imgfloat.model.CanvasEvent;
|
||||
import dev.kruhlmann.imgfloat.model.CanvasSettingsRequest;
|
||||
import dev.kruhlmann.imgfloat.model.Channel;
|
||||
import dev.kruhlmann.imgfloat.model.CodeAssetRequest;
|
||||
import dev.kruhlmann.imgfloat.model.PlaybackRequest;
|
||||
import dev.kruhlmann.imgfloat.model.Settings;
|
||||
import dev.kruhlmann.imgfloat.model.TransformRequest;
|
||||
@@ -21,6 +22,7 @@ import dev.kruhlmann.imgfloat.service.media.MediaDetectionService;
|
||||
import dev.kruhlmann.imgfloat.service.media.MediaOptimizationService;
|
||||
import dev.kruhlmann.imgfloat.service.media.OptimizedAsset;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.regex.Pattern;
|
||||
import org.slf4j.Logger;
|
||||
@@ -36,6 +38,9 @@ public class ChannelDirectoryService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ChannelDirectoryService.class);
|
||||
private static final Pattern SAFE_FILENAME = Pattern.compile("[^a-zA-Z0-9._ -]");
|
||||
private static final Pattern INIT_FUNCTION = Pattern.compile("\\bfunction\\s+init\\b");
|
||||
private static final Pattern TICK_FUNCTION = Pattern.compile("\\bfunction\\s+tick\\b");
|
||||
private static final String DEFAULT_CODE_MEDIA_TYPE = "application/javascript";
|
||||
|
||||
private final ChannelRepository channelRepository;
|
||||
private final AssetRepository assetRepository;
|
||||
@@ -192,6 +197,65 @@ public class ChannelDirectoryService {
|
||||
return Optional.of(view);
|
||||
}
|
||||
|
||||
public Optional<AssetView> createCodeAsset(String broadcaster, CodeAssetRequest request) {
|
||||
validateCodeAssetSource(request.getSource());
|
||||
Channel channel = getOrCreateChannel(broadcaster);
|
||||
byte[] bytes = request.getSource().getBytes(StandardCharsets.UTF_8);
|
||||
enforceUploadLimit(bytes.length);
|
||||
|
||||
Asset asset = new Asset(channel.getBroadcaster(), request.getName().trim(), "", 480, 270);
|
||||
asset.setOriginalMediaType(DEFAULT_CODE_MEDIA_TYPE);
|
||||
asset.setMediaType(DEFAULT_CODE_MEDIA_TYPE);
|
||||
asset.setPreview("");
|
||||
asset.setSpeed(1.0);
|
||||
asset.setMuted(false);
|
||||
asset.setAudioLoop(false);
|
||||
asset.setAudioDelayMillis(0);
|
||||
asset.setAudioSpeed(1.0);
|
||||
asset.setAudioPitch(1.0);
|
||||
asset.setAudioVolume(1.0);
|
||||
asset.setZIndex(nextZIndex(channel.getBroadcaster()));
|
||||
|
||||
try {
|
||||
assetStorageService.storeAsset(channel.getBroadcaster(), asset.getId(), bytes, DEFAULT_CODE_MEDIA_TYPE);
|
||||
} catch (IOException e) {
|
||||
throw new ResponseStatusException(BAD_REQUEST, "Unable to store custom script", e);
|
||||
}
|
||||
|
||||
assetRepository.save(asset);
|
||||
AssetView view = AssetView.from(channel.getBroadcaster(), asset);
|
||||
messagingTemplate.convertAndSend(topicFor(broadcaster), AssetEvent.created(broadcaster, view));
|
||||
return Optional.of(view);
|
||||
}
|
||||
|
||||
public Optional<AssetView> updateCodeAsset(String broadcaster, String assetId, CodeAssetRequest request) {
|
||||
validateCodeAssetSource(request.getSource());
|
||||
String normalized = normalize(broadcaster);
|
||||
byte[] bytes = request.getSource().getBytes(StandardCharsets.UTF_8);
|
||||
enforceUploadLimit(bytes.length);
|
||||
|
||||
return assetRepository
|
||||
.findById(assetId)
|
||||
.filter((asset) -> normalized.equals(asset.getBroadcaster()))
|
||||
.map((asset) -> {
|
||||
if (!isCodeMediaType(asset.getMediaType()) && !isCodeMediaType(asset.getOriginalMediaType())) {
|
||||
throw new ResponseStatusException(BAD_REQUEST, "Asset is not a script");
|
||||
}
|
||||
asset.setName(request.getName().trim());
|
||||
asset.setOriginalMediaType(DEFAULT_CODE_MEDIA_TYPE);
|
||||
asset.setMediaType(DEFAULT_CODE_MEDIA_TYPE);
|
||||
try {
|
||||
assetStorageService.storeAsset(broadcaster, asset.getId(), bytes, DEFAULT_CODE_MEDIA_TYPE);
|
||||
} catch (IOException e) {
|
||||
throw new ResponseStatusException(BAD_REQUEST, "Unable to store custom script", e);
|
||||
}
|
||||
assetRepository.save(asset);
|
||||
AssetView view = AssetView.from(normalized, asset);
|
||||
messagingTemplate.convertAndSend(topicFor(broadcaster), AssetEvent.updated(broadcaster, view));
|
||||
return view;
|
||||
});
|
||||
}
|
||||
|
||||
private String sanitizeFilename(String original) {
|
||||
String stripped = original.replaceAll("^.*[/\\\\]", "");
|
||||
return SAFE_FILENAME.matcher(stripped).replaceAll("_");
|
||||
@@ -371,6 +435,31 @@ public class ChannelDirectoryService {
|
||||
return normalized.startsWith("application/javascript") || normalized.startsWith("text/javascript");
|
||||
}
|
||||
|
||||
private void validateCodeAssetSource(String source) {
|
||||
if (source == null || source.isBlank()) {
|
||||
throw new ResponseStatusException(BAD_REQUEST, "Script source is required");
|
||||
}
|
||||
if (!INIT_FUNCTION.matcher(source).find()) {
|
||||
throw new ResponseStatusException(BAD_REQUEST, "Missing function: init");
|
||||
}
|
||||
if (!TICK_FUNCTION.matcher(source).find()) {
|
||||
throw new ResponseStatusException(BAD_REQUEST, "Missing function: tick");
|
||||
}
|
||||
}
|
||||
|
||||
private void enforceUploadLimit(long sizeBytes) {
|
||||
if (sizeBytes > uploadLimitBytes) {
|
||||
throw new ResponseStatusException(
|
||||
PAYLOAD_TOO_LARGE,
|
||||
String.format(
|
||||
"Uploaded file is too large (%d bytes). Maximum allowed is %d bytes.",
|
||||
sizeBytes,
|
||||
uploadLimitBytes
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private String topicFor(String broadcaster) {
|
||||
return "/topic/channel/" + broadcaster.toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package dev.kruhlmann.imgfloat.service.media;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.URLConnection;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
@@ -33,15 +34,17 @@ public class MediaDetectionService {
|
||||
private static final Set<String> ALLOWED_MEDIA_TYPES = Set.copyOf(EXTENSION_TYPES.values());
|
||||
|
||||
public Optional<String> detectAllowedMediaType(MultipartFile file, byte[] bytes) {
|
||||
Optional<String> detected = detectMediaType(bytes).filter(MediaDetectionService::isAllowedMediaType);
|
||||
Optional<String> detected = detectMediaType(bytes)
|
||||
.map(MediaDetectionService::normalizeJavaScriptMediaType)
|
||||
.filter(MediaDetectionService::isAllowedMediaType);
|
||||
|
||||
if (detected.isPresent()) {
|
||||
return detected;
|
||||
}
|
||||
|
||||
Optional<String> declared = Optional.ofNullable(file.getContentType()).filter(
|
||||
MediaDetectionService::isAllowedMediaType
|
||||
);
|
||||
Optional<String> declared = Optional.ofNullable(file.getContentType())
|
||||
.map(MediaDetectionService::normalizeJavaScriptMediaType)
|
||||
.filter(MediaDetectionService::isAllowedMediaType);
|
||||
if (declared.isPresent()) {
|
||||
return declared;
|
||||
}
|
||||
@@ -66,7 +69,8 @@ public class MediaDetectionService {
|
||||
}
|
||||
|
||||
public static boolean isAllowedMediaType(String mediaType) {
|
||||
return mediaType != null && ALLOWED_MEDIA_TYPES.contains(mediaType.toLowerCase());
|
||||
String normalized = normalizeJavaScriptMediaType(mediaType);
|
||||
return normalized != null && ALLOWED_MEDIA_TYPES.contains(normalized.toLowerCase());
|
||||
}
|
||||
|
||||
public static boolean isInlineDisplayType(String mediaType) {
|
||||
@@ -75,4 +79,15 @@ public class MediaDetectionService {
|
||||
(mediaType.startsWith("image/") || mediaType.startsWith("video/") || mediaType.startsWith("audio/"))
|
||||
);
|
||||
}
|
||||
|
||||
private static String normalizeJavaScriptMediaType(String mediaType) {
|
||||
if (mediaType == null) {
|
||||
return null;
|
||||
}
|
||||
String normalized = mediaType.toLowerCase(Locale.ROOT);
|
||||
if (normalized.contains("javascript") || normalized.contains("ecmascript")) {
|
||||
return "application/javascript";
|
||||
}
|
||||
return mediaType;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user