Fix federation some more
This commit is contained in:
parent
6aa3cbb5f6
commit
8fc741e3d6
3 changed files with 22 additions and 113 deletions
|
|
@ -33,10 +33,9 @@ public class FitPubApplication {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* REST template for making HTTP requests to remote ActivityPub servers.
|
* REST template for making HTTP requests to remote ActivityPub servers.
|
||||||
* Configured with HTTP Signature interceptor for ActivityPub federation.
|
|
||||||
*/
|
*/
|
||||||
@Bean
|
@Bean
|
||||||
public RestTemplate restTemplate(org.operaton.fitpub.config.ActivityPubHttpRequestInterceptor interceptor) {
|
public RestTemplate restTemplate() {
|
||||||
// Use Apache HttpClient with custom configuration
|
// Use Apache HttpClient with custom configuration
|
||||||
HttpClientConnectionManager connectionManager = PoolingHttpClientConnectionManagerBuilder.create()
|
HttpClientConnectionManager connectionManager = PoolingHttpClientConnectionManagerBuilder.create()
|
||||||
.build();
|
.build();
|
||||||
|
|
@ -48,11 +47,6 @@ public class FitPubApplication {
|
||||||
|
|
||||||
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
|
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
|
||||||
|
|
||||||
RestTemplate restTemplate = new RestTemplate(requestFactory);
|
return new RestTemplate(requestFactory);
|
||||||
|
|
||||||
// Add HTTP Signature interceptor
|
|
||||||
restTemplate.getInterceptors().add(interceptor);
|
|
||||||
|
|
||||||
return restTemplate;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,91 +0,0 @@
|
||||||
package org.operaton.fitpub.config;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.operaton.fitpub.security.HttpSignatureValidator;
|
|
||||||
import org.springframework.http.HttpRequest;
|
|
||||||
import org.springframework.http.client.ClientHttpRequestExecution;
|
|
||||||
import org.springframework.http.client.ClientHttpRequestInterceptor;
|
|
||||||
import org.springframework.http.client.ClientHttpResponse;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Intercepts outgoing HTTP requests for ActivityPub federation to add HTTP Signatures.
|
|
||||||
* This interceptor is applied AFTER RestTemplate sets all headers (including Host),
|
|
||||||
* ensuring the signature matches the actual headers sent.
|
|
||||||
*/
|
|
||||||
@Component
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
@Slf4j
|
|
||||||
public class ActivityPubHttpRequestInterceptor implements ClientHttpRequestInterceptor {
|
|
||||||
|
|
||||||
private final HttpSignatureValidator signatureValidator;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
|
|
||||||
throws IOException {
|
|
||||||
|
|
||||||
// Check if this request needs HTTP Signature (look for a marker header)
|
|
||||||
String privateKey = request.getHeaders().getFirst("X-ActivityPub-PrivateKey");
|
|
||||||
String keyId = request.getHeaders().getFirst("X-ActivityPub-KeyId");
|
|
||||||
|
|
||||||
if (privateKey != null && keyId != null) {
|
|
||||||
// Remove marker headers (they shouldn't be sent)
|
|
||||||
request.getHeaders().remove("X-ActivityPub-PrivateKey");
|
|
||||||
request.getHeaders().remove("X-ActivityPub-KeyId");
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Now sign the request with the actual headers that will be sent
|
|
||||||
String method = request.getMethod().name();
|
|
||||||
String uri = request.getURI().toString();
|
|
||||||
String bodyString = new String(body, StandardCharsets.UTF_8);
|
|
||||||
|
|
||||||
// Get the actual Host header that RestTemplate set
|
|
||||||
String host = request.getHeaders().getFirst("Host");
|
|
||||||
if (host == null) {
|
|
||||||
host = request.getURI().getHost();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the actual Date header that was set
|
|
||||||
String date = request.getHeaders().getFirst("Date");
|
|
||||||
|
|
||||||
// Get the actual Digest header that was set
|
|
||||||
String digest = request.getHeaders().getFirst("Digest");
|
|
||||||
|
|
||||||
// Build the signing string with the ACTUAL header values
|
|
||||||
String signingString = String.format(
|
|
||||||
"(request-target): %s %s%s\nhost: %s\ndate: %s\ndigest: %s",
|
|
||||||
method.toLowerCase(),
|
|
||||||
request.getURI().getPath(),
|
|
||||||
request.getURI().getQuery() != null ? "?" + request.getURI().getQuery() : "",
|
|
||||||
host,
|
|
||||||
date,
|
|
||||||
digest
|
|
||||||
);
|
|
||||||
|
|
||||||
// Sign the string
|
|
||||||
String signatureBase64 = signatureValidator.sign(signingString, privateKey);
|
|
||||||
|
|
||||||
// Build signature header
|
|
||||||
String signatureHeader = String.format(
|
|
||||||
"keyId=\"%s\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest\",signature=\"%s\"",
|
|
||||||
keyId, signatureBase64
|
|
||||||
);
|
|
||||||
|
|
||||||
// Add signature header
|
|
||||||
request.getHeaders().set("Signature", signatureHeader);
|
|
||||||
|
|
||||||
log.debug("Added HTTP Signature to request: {}", request.getURI());
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Failed to sign request", e);
|
|
||||||
throw new IOException("Failed to sign ActivityPub request", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return execution.execute(request, body);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -166,25 +166,31 @@ public class FederationService {
|
||||||
try {
|
try {
|
||||||
String activityJson = objectMapper.writeValueAsString(activity);
|
String activityJson = objectMapper.writeValueAsString(activity);
|
||||||
|
|
||||||
// Calculate Date and Digest headers
|
// Generate HTTP signature with all required headers
|
||||||
java.time.ZonedDateTime now = java.time.ZonedDateTime.now(java.time.ZoneOffset.UTC);
|
// This calculates what the signature SHOULD be, including the host from the URL
|
||||||
java.time.format.DateTimeFormatter formatter = java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME;
|
HttpSignatureValidator.SignatureHeaders signatureHeaders = signatureValidator.signRequest(
|
||||||
String date = now.format(formatter);
|
HttpMethod.POST.name(),
|
||||||
|
inboxUrl,
|
||||||
java.security.MessageDigest digest = java.security.MessageDigest.getInstance("SHA-256");
|
activityJson,
|
||||||
byte[] hash = digest.digest(activityJson.getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
sender.getPrivateKey(),
|
||||||
String digestValue = "SHA-256=" + java.util.Base64.getEncoder().encodeToString(hash);
|
baseUrl + "/users/" + sender.getUsername() + "#main-key"
|
||||||
|
);
|
||||||
|
|
||||||
HttpHeaders headers = new HttpHeaders();
|
HttpHeaders headers = new HttpHeaders();
|
||||||
headers.set("Content-Type", "application/activity+json");
|
headers.set("Content-Type", "application/activity+json");
|
||||||
headers.set("Accept", "application/activity+json");
|
headers.set("Accept", "application/activity+json");
|
||||||
headers.set("Date", date);
|
|
||||||
headers.set("Digest", digestValue);
|
|
||||||
|
|
||||||
// Set marker headers for the interceptor to use for signing
|
// Set the Date and Digest headers that were used in the signature
|
||||||
// These will be removed by the interceptor before sending
|
headers.set("Date", signatureHeaders.date);
|
||||||
headers.set("X-ActivityPub-PrivateKey", sender.getPrivateKey());
|
headers.set("Digest", signatureHeaders.digest);
|
||||||
headers.set("X-ActivityPub-KeyId", baseUrl + "/users/" + sender.getUsername() + "#main-key");
|
|
||||||
|
// Set the Signature header
|
||||||
|
headers.set("Signature", signatureHeaders.signature);
|
||||||
|
|
||||||
|
// NOTE: We do NOT set the Host header here.
|
||||||
|
// RestTemplate/HttpClient will set it automatically to match the URL.
|
||||||
|
// The signature was calculated with the correct host (extracted from inboxUrl),
|
||||||
|
// so when the client sets the Host header, it will match the signature.
|
||||||
|
|
||||||
HttpEntity<String> entity = new HttpEntity<>(activityJson, headers);
|
HttpEntity<String> entity = new HttpEntity<>(activityJson, headers);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue