TimestampController.java
package io.mersel.dss.signer.api.controllers;
import io.mersel.dss.signer.api.dtos.TimestampResponseDto;
import io.mersel.dss.signer.api.dtos.TimestampStatusDto;
import io.mersel.dss.signer.api.dtos.TimestampValidationResponseDto;
import io.mersel.dss.signer.api.exceptions.TimestampException;
import io.mersel.dss.signer.api.models.ErrorModel;
import io.mersel.dss.signer.api.services.timestamp.TimestampConfigurationService;
import io.mersel.dss.signer.api.services.timestamp.TimestampService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
/**
* Zaman damgası (timestamp) işlemleri için REST controller.
* RFC 3161 standardına uygun TSQ, TSR ve validasyon endpoint'leri sağlar.
*/
@RestController
@CrossOrigin(origins = "*", allowedHeaders = "*", methods = {RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT, RequestMethod.DELETE, RequestMethod.OPTIONS})
@RequestMapping("/api/timestamp")
@Tag(name = "Timestamp", description = "Zaman damgası (RFC 3161) işlemleri - TSQ, TSR ve doğrulama")
public class TimestampController {
private static final Logger LOGGER = LoggerFactory.getLogger(TimestampController.class);
private final TimestampService timestampService;
private final TimestampConfigurationService timestampConfigurationService;
public TimestampController(
TimestampService timestampService,
TimestampConfigurationService timestampConfigurationService) {
this.timestampService = timestampService;
this.timestampConfigurationService = timestampConfigurationService;
}
@Operation(
summary = "Binary belge için zaman damgası al",
description = "Herhangi bir binary belge için RFC 3161 standardına uygun zaman damgası alır. " +
"Timestamp token'ı binary (application/octet-stream) olarak döner. " +
"Metadata bilgileri HTTP response header'larında gelir: " +
"X-Timestamp-Time, X-Timestamp-TSA, X-Timestamp-Serial, X-Timestamp-Hash-Algorithm"
)
@ApiResponses({
@ApiResponse(
responseCode = "200",
description = "Zaman damgası başarıyla alındı (binary .tst dosyası)",
content = @Content(mediaType = "application/octet-stream")
),
@ApiResponse(
responseCode = "400",
description = "Geçersiz istek veya timestamp servisi yapılandırılmamış",
content = @Content(schema = @Schema(implementation = ErrorModel.class))
),
@ApiResponse(
responseCode = "500",
description = "Zaman damgası alınamadı",
content = @Content(schema = @Schema(implementation = ErrorModel.class))
)
})
@PostMapping(
value = "/get",
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
produces = {MediaType.APPLICATION_OCTET_STREAM_VALUE, MediaType.APPLICATION_JSON_VALUE}
)
public ResponseEntity<?> getTimestamp(
@RequestParam("document")
@io.swagger.v3.oas.annotations.Parameter(
description = "Zaman damgası alınacak dosya",
required = true
)
MultipartFile document,
@RequestParam(value = "hashAlgorithm", defaultValue = "SHA256")
@io.swagger.v3.oas.annotations.Parameter(
description = "Hash algoritması",
example = "SHA256"
)
String hashAlgorithm) {
try {
LOGGER.info("Zaman damgası alma isteği alındı. Dosya: {}, Hash: {}",
document.getOriginalFilename(), hashAlgorithm);
// Timestamp servisinin yapılandırılmış olup olmadığını kontrol et
if (!timestampConfigurationService.isAvailable()) {
LOGGER.warn("Timestamp servisi yapılandırılmamış");
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.contentType(MediaType.APPLICATION_JSON)
.body(new ErrorModel(
"TIMESTAMP_NOT_CONFIGURED",
"Zaman damgası servisi yapılandırılmamış. TS_SERVER_HOST property'sini ayarlayın."
));
}
// Dosyayı byte array'e çevir
byte[] documentBytes = document.getBytes();
TimestampResponseDto response = timestampService.getTimestamp(documentBytes, hashAlgorithm);
LOGGER.info("Zaman damgası başarıyla alındı. Tarih: {}", response.getTimestamp());
// Timestamp token'ı binary olarak dön, metadata header'larda
byte[] timestampToken = java.util.Base64.getDecoder().decode(response.getTimestampToken());
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.header("Content-Disposition", "attachment; filename=timestamp.tst")
.header("X-Timestamp-Time", response.getTimestamp())
.header("X-Timestamp-TSA", response.getTsaName() != null ? response.getTsaName() : "")
.header("X-Timestamp-Serial", response.getSerialNumber())
.header("X-Timestamp-Hash-Algorithm", response.getHashAlgorithm())
.header("X-Timestamp-Nonce", response.getNonce() != null ? response.getNonce() : "")
.body(timestampToken);
} catch (TimestampException e) {
LOGGER.error("Zaman damgası alma hatası: {}", e.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.contentType(MediaType.APPLICATION_JSON)
.body(new ErrorModel("TIMESTAMP_ERROR", e.getMessage()));
} catch (Exception e) {
LOGGER.error("Beklenmeyen hata", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.contentType(MediaType.APPLICATION_JSON)
.body(new ErrorModel("INTERNAL_ERROR", "Zaman damgası alınamadı: " + e.getMessage()));
}
}
@Operation(
summary = "Zaman damgasını doğrula",
description = "RFC 3161 zaman damgasını doğrular. Timestamp token'ın imzasını, " +
"TSA sertifikasını ve isteğe bağlı olarak orijinal belgenin hash'ini doğrular. " +
"Detaylı doğrulama raporu döner."
)
@ApiResponses({
@ApiResponse(
responseCode = "200",
description = "Doğrulama tamamlandı (başarılı veya başarısız olabilir)",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = TimestampValidationResponseDto.class)
)
),
@ApiResponse(
responseCode = "400",
description = "Geçersiz istek",
content = @Content(schema = @Schema(implementation = ErrorModel.class))
),
@ApiResponse(
responseCode = "500",
description = "Doğrulama yapılamadı",
content = @Content(schema = @Schema(implementation = ErrorModel.class))
)
})
@PostMapping(
value = "/validate",
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE
)
public ResponseEntity<?> validateTimestamp(
@RequestParam("timestampToken")
@io.swagger.v3.oas.annotations.Parameter(
description = "Doğrulanacak timestamp token dosyası (.tst veya binary)",
required = true
)
MultipartFile timestampToken,
@RequestParam(value = "originalDocument", required = false)
@io.swagger.v3.oas.annotations.Parameter(
description = "Orijinal belge (hash doğrulaması için - opsiyonel)"
)
MultipartFile originalDocument) {
try {
LOGGER.info("Zaman damgası doğrulama isteği alındı. Token dosyası: {}",
timestampToken.getOriginalFilename());
// Timestamp token'ı byte array'e çevir
byte[] tokenBytes = timestampToken.getBytes();
LOGGER.debug("Timestamp token okundu - boyut: {} bytes", tokenBytes.length);
// Orijinal belge varsa byte array'e çevir
byte[] originalBytes = null;
if (originalDocument != null && !originalDocument.isEmpty()) {
originalBytes = originalDocument.getBytes();
LOGGER.info("Orijinal belge sağlandı, hash doğrulaması yapılacak: {}",
originalDocument.getOriginalFilename());
}
TimestampValidationResponseDto response = timestampService.validateTimestamp(
tokenBytes, originalBytes);
if (response.isValid()) {
LOGGER.info("Zaman damgası doğrulandı. Seri no: {}", response.getSerialNumber());
} else {
LOGGER.warn("Zaman damgası doğrulaması başarısız. Hatalar: {}", response.getErrors());
}
return ResponseEntity.ok(response);
} catch (Exception e) {
LOGGER.error("Zaman damgası doğrulama hatası", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorModel("VALIDATION_ERROR", "Doğrulama yapılamadı: " + e.getMessage()));
}
}
@Operation(
summary = "Timestamp servisi durumunu kontrol et",
description = "Timestamp servisinin yapılandırılmış ve kullanıma hazır olup olmadığını kontrol eder."
)
@ApiResponses({
@ApiResponse(
responseCode = "200",
description = "Servis durumu",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = TimestampStatusDto.class)
)
)
})
@GetMapping("/status")
public ResponseEntity<TimestampStatusDto> getStatus() {
boolean isAvailable = timestampConfigurationService.isAvailable();
TimestampStatusDto status = new TimestampStatusDto(
isAvailable,
isAvailable ? "Timestamp servisi aktif" : "Timestamp servisi yapılandırılmamış"
);
return ResponseEntity.ok(status);
}
}