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.
|
||||
* Configured with HTTP Signature interceptor for ActivityPub federation.
|
||||
*/
|
||||
@Bean
|
||||
public RestTemplate restTemplate(org.operaton.fitpub.config.ActivityPubHttpRequestInterceptor interceptor) {
|
||||
public RestTemplate restTemplate() {
|
||||
// Use Apache HttpClient with custom configuration
|
||||
HttpClientConnectionManager connectionManager = PoolingHttpClientConnectionManagerBuilder.create()
|
||||
.build();
|
||||
|
|
@ -48,11 +47,6 @@ public class FitPubApplication {
|
|||
|
||||
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
|
||||
|
||||
RestTemplate restTemplate = new RestTemplate(requestFactory);
|
||||
|
||||
// Add HTTP Signature interceptor
|
||||
restTemplate.getInterceptors().add(interceptor);
|
||||
|
||||
return restTemplate;
|
||||
return new RestTemplate(requestFactory);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
String activityJson = objectMapper.writeValueAsString(activity);
|
||||
|
||||
// Calculate Date and Digest headers
|
||||
java.time.ZonedDateTime now = java.time.ZonedDateTime.now(java.time.ZoneOffset.UTC);
|
||||
java.time.format.DateTimeFormatter formatter = java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME;
|
||||
String date = now.format(formatter);
|
||||
|
||||
java.security.MessageDigest digest = java.security.MessageDigest.getInstance("SHA-256");
|
||||
byte[] hash = digest.digest(activityJson.getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
||||
String digestValue = "SHA-256=" + java.util.Base64.getEncoder().encodeToString(hash);
|
||||
// Generate HTTP signature with all required headers
|
||||
// This calculates what the signature SHOULD be, including the host from the URL
|
||||
HttpSignatureValidator.SignatureHeaders signatureHeaders = signatureValidator.signRequest(
|
||||
HttpMethod.POST.name(),
|
||||
inboxUrl,
|
||||
activityJson,
|
||||
sender.getPrivateKey(),
|
||||
baseUrl + "/users/" + sender.getUsername() + "#main-key"
|
||||
);
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.set("Content-Type", "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
|
||||
// These will be removed by the interceptor before sending
|
||||
headers.set("X-ActivityPub-PrivateKey", sender.getPrivateKey());
|
||||
headers.set("X-ActivityPub-KeyId", baseUrl + "/users/" + sender.getUsername() + "#main-key");
|
||||
// Set the Date and Digest headers that were used in the signature
|
||||
headers.set("Date", signatureHeaders.date);
|
||||
headers.set("Digest", signatureHeaders.digest);
|
||||
|
||||
// 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);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue