Fix federation
This commit is contained in:
parent
4a55242ccb
commit
6aa3cbb5f6
3 changed files with 113 additions and 18 deletions
|
|
@ -33,12 +33,11 @@ public class FitPubApplication {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* REST template for making HTTP requests to remote ActivityPub servers.
|
* REST template for making HTTP requests to remote ActivityPub servers.
|
||||||
* Configured to not suppress HTTP headers, which is critical for HTTP Signature authentication.
|
* Configured with HTTP Signature interceptor for ActivityPub federation.
|
||||||
*/
|
*/
|
||||||
@Bean
|
@Bean
|
||||||
public RestTemplate restTemplate() {
|
public RestTemplate restTemplate(org.operaton.fitpub.config.ActivityPubHttpRequestInterceptor interceptor) {
|
||||||
// Use Apache HttpClient with custom configuration
|
// Use Apache HttpClient with custom configuration
|
||||||
// This prevents automatic Host header overwriting which breaks HTTP Signatures
|
|
||||||
HttpClientConnectionManager connectionManager = PoolingHttpClientConnectionManagerBuilder.create()
|
HttpClientConnectionManager connectionManager = PoolingHttpClientConnectionManagerBuilder.create()
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
|
@ -49,6 +48,11 @@ public class FitPubApplication {
|
||||||
|
|
||||||
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
|
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
|
||||||
|
|
||||||
return new RestTemplate(requestFactory);
|
RestTemplate restTemplate = new RestTemplate(requestFactory);
|
||||||
|
|
||||||
|
// Add HTTP Signature interceptor
|
||||||
|
restTemplate.getInterceptors().add(interceptor);
|
||||||
|
|
||||||
|
return restTemplate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
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,25 @@ public class FederationService {
|
||||||
try {
|
try {
|
||||||
String activityJson = objectMapper.writeValueAsString(activity);
|
String activityJson = objectMapper.writeValueAsString(activity);
|
||||||
|
|
||||||
// Generate HTTP signature with all required headers
|
// Calculate Date and Digest headers
|
||||||
HttpSignatureValidator.SignatureHeaders signatureHeaders = signatureValidator.signRequest(
|
java.time.ZonedDateTime now = java.time.ZonedDateTime.now(java.time.ZoneOffset.UTC);
|
||||||
HttpMethod.POST.name(),
|
java.time.format.DateTimeFormatter formatter = java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME;
|
||||||
inboxUrl,
|
String date = now.format(formatter);
|
||||||
activityJson,
|
|
||||||
sender.getPrivateKey(),
|
java.security.MessageDigest digest = java.security.MessageDigest.getInstance("SHA-256");
|
||||||
baseUrl + "/users/" + sender.getUsername() + "#main-key"
|
byte[] hash = digest.digest(activityJson.getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
||||||
);
|
String digestValue = "SHA-256=" + java.util.Base64.getEncoder().encodeToString(hash);
|
||||||
|
|
||||||
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);
|
||||||
|
|
||||||
// Add signature-related headers
|
// Set marker headers for the interceptor to use for signing
|
||||||
// NOTE: We do NOT set the Host header manually - RestTemplate/HttpClient sets it automatically
|
// These will be removed by the interceptor before sending
|
||||||
// The signature was calculated with the correct host from the URL, so it will match
|
headers.set("X-ActivityPub-PrivateKey", sender.getPrivateKey());
|
||||||
headers.set("Date", signatureHeaders.date);
|
headers.set("X-ActivityPub-KeyId", baseUrl + "/users/" + sender.getUsername() + "#main-key");
|
||||||
headers.set("Digest", signatureHeaders.digest);
|
|
||||||
headers.set("Signature", signatureHeaders.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