Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
0eaaf0c
Fix: open as text link on downloads now uses utf-8 encoding
ArneBab Nov 8, 2025
58077de
Fix: set utf-8 as fallback, as it should have been.
ArneBab Nov 9, 2025
36359ef
Merge remote-tracking branch 'torusrxxx/patch-3' into open-as-plain-t…
ArneBab Nov 23, 2025
827bec9
⬆️ Update Mockito to latest 2.x version
Bombe Sep 7, 2025
a72bc74
💚 Exclude ancient Hamcrest dependency from JUnit
Bombe Nov 25, 2025
71b771f
✅ Add matchers for verifying links in HTML documents
Bombe Nov 25, 2025
bdfd1e3
✅ Add possibility to use different translation for each test
Bombe Nov 25, 2025
322ff7f
✅ Add test implementation of ToadletContext
Bombe Nov 25, 2025
5763d0e
🐛 Set charset correctly
Bombe Nov 25, 2025
b2c13f4
✅ Add matchers for TestToadletContext
Bombe Nov 25, 2025
ec7959d
✅ Add second test for FProxyToadlet
Bombe Nov 25, 2025
ada6f7e
✅ Allow TestToadletContext to execute the request itself
Bombe Nov 26, 2025
68b73b0
✅ Add test for requesting an invalid key via parameter
Bombe Nov 26, 2025
ddf65d0
✅ Add test for requesting valid key via parameter
Bombe Nov 26, 2025
0fa9dc0
✅ Add matcher for the HTML document of a toadlet context
Bombe Nov 26, 2025
06758cb
✅ Add matchers for HTML
Bombe Nov 26, 2025
c167169
✅ Add test for / redirecting to welcome toadlet
Bombe Nov 26, 2025
1e67343
✅ Add test for /favicon.ico redirect
Bombe Nov 26, 2025
7c9e02a
✅ Add hasContentType matcher for TestToadletContext
Bombe Nov 26, 2025
cddc738
✅ Add test for feed generation
Bombe Nov 26, 2025
65944c6
✨ Allow activating robots file generation in context
Bombe Nov 26, 2025
1896e58
✅ Add tests for /robots.txt
Bombe Nov 26, 2025
c5bb0c5
✅ Add tests for some permanent redirects
Bombe Nov 26, 2025
b5f28eb
✨ Add Document matcher that can locate Elements
Bombe Nov 26, 2025
e8c58c3
✅ Add test for requesting a failed download
Bombe Nov 26, 2025
1728fed
✨ Add Document matcher that can locate and verify Elements
Bombe Nov 26, 2025
e467ab1
✨ Add Element matcher that can verify attributes
Bombe Nov 26, 2025
c8c48e7
♻️ Use improved matchers
Bombe Nov 26, 2025
f55d7f5
✅ Add test for the progress page
Bombe Nov 26, 2025
37ebe6b
✅ Add more tests for redirects
Bombe Nov 26, 2025
b567271
Merge remote-tracking branch 'Bombe/open-as-plain-text-utf8' into ope…
ArneBab May 1, 2026
09eb720
Add jsoup verification key
ArneBab May 1, 2026
556ca00
🧑‍💻 Make test (slightly) less confusing
Bombe May 1, 2026
cf65dd4
Merge remote-tracking branch 'Bombe/open-as-plain-text-utf8' into ope…
ArneBab May 1, 2026
695c144
⬆️ Use jakarta activation-api instead of relying on javax.activation
Bombe May 1, 2026
e56bad8
Merge remote-tracking branch 'Bombe/open-as-plain-text-utf8' into ope…
ArneBab May 1, 2026
3f0357a
Refresh the verification metadata
ArneBab May 1, 2026
ccf3694
Merge branch 'next' into open-as-plain-text-utf8
ArneBab May 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ task copyTestResourcesToClasses2 {
into layout.buildDirectory.dir("classes/java/test/")
include 'freenet/client/filter/*/**'
include 'freenet/crypt/ciphers/rijndael-gladman-test-data/**'
include 'freenet/l10n/*properties'
include 'freenet/**/*.properties'
include 'freenet/clients/http/templates/**'
}
}
Expand Down Expand Up @@ -263,10 +263,13 @@ dependencies {
implementation "org.unbescape:unbescape:1.1.6.RELEASE"
implementation "org.slf4j:slf4j-api:1.7.25"

testImplementation 'junit:junit:4.13.2'
testImplementation "org.mockito:mockito-core:1.9.5"
testImplementation('junit:junit:4.13.2') {
exclude(group: "org.hamcrest", module: "hamcrest-core")
}
testImplementation "org.mockito:mockito-inline:2.28.2"
testImplementation "org.hamcrest:hamcrest:3.0"
testImplementation "org.objenesis:objenesis:1.0"
testImplementation "org.jsoup:jsoup:1.21.2"
testImplementation "jakarta.activation:jakarta.activation-api:2.1.4"
}

task tar(type: Tar) {
Expand Down
Binary file modified gradle/verification-keyring.gpg
Binary file not shown.
1,377 changes: 551 additions & 826 deletions gradle/verification-keyring.keys

Large diffs are not rendered by default.

190 changes: 143 additions & 47 deletions gradle/verification-metadata.xml

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions src/freenet/clients/http/FProxyToadlet.java
Original file line number Diff line number Diff line change
Expand Up @@ -936,8 +936,9 @@ public void run() {
}
option = optionList.addChild("li");
// FIXME: is this safe? See bug #131
MediaType textMediaType = new MediaType("text/plain");
textMediaType.setParameter("charset", (e.getExpectedMimeType() != null) ? MediaType.getCharsetRobust(e.getExpectedMimeType()) : null);
String expectedCharset = (e.getExpectedMimeType() != null) ? MediaType.getCharsetRobust(e.getExpectedMimeType()) : null;
MediaType textMediaType = new MediaType("text/plain")
.setParameter("charset", expectedCharset != null ? expectedCharset : "utf-8");
NodeL10n.getBase().addL10nSubstitution(option, "FProxyToadlet.openAsText", new String[] { "link" }, new HTMLNode[] { HTMLNode.link(getLink(key, textMediaType.toString(), maxSize, null, false, maxRetries, overrideSize)) });
option = optionList.addChild("li");
NodeL10n.getBase().addL10nSubstitution(option, "FProxyToadlet.openForceDisk", new String[] { "link" }, new HTMLNode[] { HTMLNode.link(getLink(key, mime, maxSize, null, true, maxRetries, overrideSize)) });
Expand Down
7 changes: 7 additions & 0 deletions src/freenet/l10n/BaseL10n.java
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,13 @@ public void setLanguage(final LANGUAGE selectedLanguage) throws MissingResourceE
* @throws IOException
*/
private void loadOverrideFileOrBackup() throws IOException {
try (InputStream overrideInputStream = cl.getResourceAsStream(getL10nOverrideFileName(lang))) {
if (overrideInputStream != null) {
Logger.normal(this, "Override file found in classpath.");
this.translationOverride = SimpleFieldSet.readFrom(overrideInputStream, false, false);
return;
}
}
final File tmpFile = new File(this.getL10nOverrideFileName(this.lang));
if (tmpFile.exists() && tmpFile.canRead() && tmpFile.length() > 0) {
Logger.normal(this, "Override file detected : let's try to load it");
Expand Down
289 changes: 289 additions & 0 deletions test/freenet/clients/http/FProxyToadletTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,289 @@
package freenet.clients.http;

import freenet.client.ClientMetadata;
import freenet.client.FetchContext;
import freenet.client.FetchException;
import freenet.client.FetchResult;
import freenet.client.HighLevelSimpleClient;
import freenet.client.filter.ContentFilter;
import freenet.client.filter.KnownUnsafeContentTypeException;
import freenet.clients.http.TestToadletContext.TestToadletContextBuilder;
import freenet.keys.FreenetURI;
import freenet.l10n.BaseL10nTest;
import freenet.node.NodeClientCore;
import freenet.node.RequestClientBuilder;
import freenet.support.io.ArrayBucket;
import java.io.File;
import java.net.URI;
import java.util.function.Consumer;
import org.hamcrest.Matcher;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.internal.util.reflection.FieldSetter;

import static freenet.test.HtmlMatchers.hasAttribute;
import static freenet.test.HtmlMatchers.hasElement;
import static freenet.test.HtmlMatchers.hasTitle;
import static freenet.test.LinkMatchers.hasBaseType;
import static freenet.test.LinkMatchers.hasParameter;
import static freenet.test.LinkMatchers.hasQuery;
import static freenet.test.LinkMatchers.isKeyValuePairs;
import static freenet.test.LinkMatchers.isMimeType;
import static freenet.test.LinkMatchers.isURI;
import static freenet.test.ToadletContextMatchers.hasBodyText;
import static freenet.test.ToadletContextMatchers.hasContentType;
import static freenet.test.ToadletContextMatchers.hasHeader;
import static freenet.test.ToadletContextMatchers.hasStatus;
import static freenet.test.ToadletContextMatchers.isHtml;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.equalToIgnoringCase;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.hasEntry;
import static org.hamcrest.Matchers.lessThan;
import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

public class FProxyToadletTest {

@Test
public void whenOfferingFilterOptionsPlainTextMediaTypeHasCharsetUtf8() throws Exception {
BaseL10nTest.useTestTranslation("freenet/clients/http/FProxyToadletTest.properties");
FetchException fetchException = (FetchException) new FetchException(FetchException.FetchExceptionMode.UNKNOWN_METADATA, 4, true, "application/pdf")
.initCause(new KnownUnsafeContentTypeException(ContentFilter.getMIMEType("application/pdf")));
when(fetchTracker.makeFetcher(any(), anyLong(), any(), any())).thenThrow(fetchException);
createToadletContextExecuteRequestAndVerifyContext(builder -> builder.requesting("/KSK@test"), allOf(
hasStatus(equalTo(500)),
isHtml(
hasElement("li a:contains(open-as-text)",
hasAttribute("href",
isURI(hasQuery(isKeyValuePairs(hasEntry(equalTo("type"), contains(isMimeType(hasParameter("charset", equalToIgnoringCase("utf-8")))))))))))
));
}

@Test
public void downloadedFileIsDeliveredImmediately() throws Exception {
when(fetchTracker.makeFetcher(any(), anyLong(), any(), any())).then(invocation -> {
FProxyFetchInProgress fetchInProgress = new FProxyFetchInProgress(fetchTracker, invocation.getArgument(0, FreenetURI.class), invocation.getArgument(1, Long.class), 0, null, invocation.getArgument(2, FetchContext.class), new RequestClientBuilder().build(), invocation.getArgument(3, FProxyFetchInProgress.REFILTER_POLICY.class));
fetchInProgress.onSuccess(new FetchResult(new ClientMetadata("text/plain"), new ArrayBucket("test".getBytes(UTF_8))), null);
return new FProxyFetchWaiter(fetchInProgress);
});
createToadletContextExecuteRequestAndVerifyContext(builder -> builder.requesting("/KSK@test"), allOf(
hasStatus(equalTo(200)),
hasBodyText(equalTo("test")),
hasHeader("Content-Type", contains(isMimeType(hasBaseType("text/plain"))))
));
}

@Test
public void requestingARedirectResultsInPermanentRedirect() throws Exception {
when(fetchTracker.makeFetcher(any(), anyLong(), any(), any())).then(invocation -> {
FProxyFetchInProgress fetchInProgress = new FProxyFetchInProgress(fetchTracker, invocation.getArgument(0, FreenetURI.class), invocation.getArgument(1, Long.class), 0, null, invocation.getArgument(2, FetchContext.class), new RequestClientBuilder().build(), invocation.getArgument(3, FProxyFetchInProgress.REFILTER_POLICY.class));
fetchInProgress.onFailure(new FetchException(FetchException.FetchExceptionMode.PERMANENT_REDIRECT, new FreenetURI("KSK@test2")), null);
return new FProxyFetchWaiter(fetchInProgress);
});
verifyPermanentRedirect("/KSK@test", "/freenet:KSK@test2");
}

@Test
public void requestingARedirectWhenAcceptingImagesFollowsTheRedirect() throws Exception {
when(fetchTracker.makeFetcher(eq(new FreenetURI("KSK@test")), anyLong(), any(), any())).then(invocation -> {
FProxyFetchInProgress fetchInProgress = new FProxyFetchInProgress(fetchTracker, invocation.getArgument(0, FreenetURI.class), invocation.getArgument(1, Long.class), 0, null, invocation.getArgument(2, FetchContext.class), new RequestClientBuilder().build(), invocation.getArgument(3, FProxyFetchInProgress.REFILTER_POLICY.class));
fetchInProgress.onFailure(new FetchException(FetchException.FetchExceptionMode.PERMANENT_REDIRECT, new FreenetURI("KSK@test2")), null);
return new FProxyFetchWaiter(fetchInProgress);
});
when(fetchTracker.makeFetcher(eq(new FreenetURI("KSK@test2")), anyLong(), any(), any())).then(invocation -> {
FProxyFetchInProgress fetchInProgress = new FProxyFetchInProgress(fetchTracker, invocation.getArgument(0, FreenetURI.class), invocation.getArgument(1, Long.class), 0, null, invocation.getArgument(2, FetchContext.class), new RequestClientBuilder().build(), invocation.getArgument(3, FProxyFetchInProgress.REFILTER_POLICY.class));
fetchInProgress.onSuccess(new FetchResult(new ClientMetadata("text/css"), new ArrayBucket("test".getBytes(UTF_8))), null);
return new FProxyFetchWaiter(fetchInProgress);
});
createToadletContextExecuteRequestAndVerifyContext(builder -> builder.requesting("/KSK@test").withHeader("Accept", "text/css"), allOf(
hasStatus(equalTo(200)),
hasBodyText(equalTo("test"))
));
}

@Test
public void requestingInvalidKeyViaParameterResultsInErrorPage() throws Exception {
createToadletContextExecuteRequestAndVerifyContext(builder -> builder.requesting("/?key=INV@lid"), hasStatus(equalTo(404)));
}

@Test
public void requestingAValidKeyViaParameterResultsInRedirect() throws Exception {
verifyTemporaryRedirect("/?key=KSK@test", "/freenet:KSK@test");
}

@Test
public void requestingTheRootUrlRedirectsToTheWelcomeToadlet() {
verifyInternalRedirect("/", "/welcome/");
}

@Test
public void requestingFaviconIcoRedirectsToStaticToadlet() {
verifyInternalRedirect("/favicon.ico", "/static/favicon.ico");
}

@Test
public void requestingFeedSendsTheAtomFeed() throws Exception {
when(nodeClientCore.getAlerts().getAtom(any())).thenReturn("atom-feed");
createToadletContextExecuteRequestAndVerifyContext(builder -> builder.requesting("/feed"), allOf(
hasStatus(equalTo(200)),
hasContentType(hasBaseType("application/atom+xml")),
hasBodyText(equalTo("atom-feed"))
));
}

@Test
public void requestingFeedGeneratesUrlPrefixCorrectly() throws Exception {
when(nodeClientCore.getAlerts().getAtom(any())).thenReturn("atom-feed");
TestToadletContext toadletContext = TestToadletContext.builder()
.forToadlet(fProxyToadlet)
.requesting("/feed/")
.withNode(nodeClientCore.getNode())
.build();
toadletContext.handleRequest();
ArgumentCaptor<String> urlPrefixCaptor = ArgumentCaptor.forClass(String.class);
verify(nodeClientCore.getAlerts()).getAtom(urlPrefixCaptor.capture());
assertThat(urlPrefixCaptor.getValue(), equalTo("http://127.0.0.1:8888"));
}

@Test
public void requestingRobotsFileReturnsADisallowForEverything() throws Exception {
createToadletContextExecuteRequestAndVerifyContext(builder -> builder.requesting("/robots.txt").doRobots(), allOf(
hasStatus(equalTo(200)),
hasContentType(hasBaseType("text/plain")),
hasBodyText(equalToIgnoringCase("User-Agent: *\nDisallow: /"))
));
}

@Test
public void requestingRobotsFileWithRobotsFileDisabledReturnsClientError() throws Exception {
createToadletContextExecuteRequestAndVerifyContext(builder -> builder.requesting("/robots.txt"), hasStatus(allOf(greaterThanOrEqualTo(400), lessThan(500))));
}

@Test
public void requestDarknetPageWithoutSlashRedirectsToFriendsPage() throws Exception {
verifyPermanentRedirect("/darknet", "/friends/");
}

@Test
public void requestDarknetPageWithSlashRedirectsToFriendsPage() throws Exception {
verifyPermanentRedirect("/darknet/", "/friends/");
}

@Test
public void requestOpennetPageWithoutSlashRedirectsToStrangersPage() throws Exception {
verifyPermanentRedirect("/opennet", "/strangers/");
}

@Test
public void requestOpennetPageWithSlashRedirectsToStrangersPage() throws Exception {
verifyPermanentRedirect("/opennet/", "/strangers/");
}

@Test
public void requestQueuePageRedirectsToDownloadsPage() throws Exception {
verifyPermanentRedirect("/queue/", "/downloads/");
}

@Test
public void requestConfigPageRedirectsToConfigNodePage() throws Exception {
verifyPermanentRedirect("/config/", "/config/node");
}

@Test
public void requestingAFailedDownloadReturnsErrorPage() throws Exception {
BaseL10nTest.useTestTranslation();
when(fetchTracker.makeFetcher(any(), anyLong(), any(), any())).then(invocation -> {
FProxyFetchInProgress fetchInProgress = new FProxyFetchInProgress(fetchTracker, invocation.getArgument(0, FreenetURI.class), invocation.getArgument(1, Long.class), 0, null, invocation.getArgument(2, FetchContext.class), new RequestClientBuilder().build(), invocation.getArgument(3, FProxyFetchInProgress.REFILTER_POLICY.class));
fetchInProgress.onFailure(new FetchException(FetchException.FetchExceptionMode.ALL_DATA_NOT_FOUND), null);
return new FProxyFetchWaiter(fetchInProgress);
});
createToadletContextExecuteRequestAndVerifyContext(builder -> builder.requesting("/KSK@failed"), allOf(
hasStatus(equalTo(500)),
isHtml(allOf(
hasTitle(equalTo("FetchException.shortError.28 - Freenet")),
hasElement("p:contains(FProxyToadlet.unableToRetrieve)")
))
));
}

@Test
public void requestingAnInProgressKeyReturnsProgressPage() throws Exception {
BaseL10nTest.useTestTranslation();
when(fetchTracker.makeFetcher(any(), anyLong(), any(), any())).then(invocation -> {
FProxyFetchInProgress fetchInProgress = new FProxyFetchInProgress(fetchTracker, invocation.getArgument(0, FreenetURI.class), invocation.getArgument(1, Long.class), 0, null, invocation.getArgument(2, FetchContext.class), new RequestClientBuilder().build(), invocation.getArgument(3, FProxyFetchInProgress.REFILTER_POLICY.class));
fetchInProgress.setHasWaited();
return new FProxyFetchWaiter(fetchInProgress);
});
createToadletContextExecuteRequestAndVerifyContext(builder -> builder.requesting("/KSK@test").withHeader("User-Agent", "Mozilla/Test"), allOf(
isHtml(
hasTitle(equalTo("FProxyToadlet.fetchingPageTitle - Freenet"))
)
));
}

private void verifyTemporaryRedirect(String fromUri, String toUri) throws Exception {
createToadletContextExecuteRequestAndVerifyContext(builder -> builder.requesting(fromUri), allOf(
hasStatus(equalTo(302)),
hasHeader("Location", contains(equalTo(toUri)))
));
}

private void verifyPermanentRedirect(String fromUri, String toUri) throws Exception {
createToadletContextExecuteRequestAndVerifyContext(builder -> builder.requesting(fromUri), allOf(
hasStatus(equalTo(301)),
hasHeader("Location", contains(equalTo(toUri)))
));
}

private void verifyInternalRedirect(String fromUri, String toUri) {
TestToadletContext toadletContext = TestToadletContext.builder()
.forToadlet(fProxyToadlet)
.requesting(fromUri)
.withNode(nodeClientCore.getNode()).build();
RedirectException redirectException = assertThrows(RedirectException.class, toadletContext::handleRequest);
assertThat(redirectException.getTarget(), equalTo(URI.create(toUri)));
}

private void createToadletContextExecuteRequestAndVerifyContext(Consumer<TestToadletContextBuilder> builderCustomizer, Matcher<TestToadletContext> toadletContextMatcher) throws Exception {
TestToadletContextBuilder builder = TestToadletContext.builder()
.forToadlet(fProxyToadlet)
.withNode(nodeClientCore.getNode());
builderCustomizer.accept(builder);
TestToadletContext toadletContext = builder.build();
toadletContext.handleRequest();
assertThat(toadletContext, toadletContextMatcher);
}

private final HighLevelSimpleClient highLevelSimpleClient = mock(HighLevelSimpleClient.class, RETURNS_DEEP_STUBS);
private final NodeClientCore nodeClientCore = mock(NodeClientCore.class, RETURNS_DEEP_STUBS);
private final FProxyFetchTracker fetchTracker = mock(FProxyFetchTracker.class, RETURNS_DEEP_STUBS);

{
when(nodeClientCore.getNode().getClientCore()).thenReturn(nodeClientCore);
when(nodeClientCore.getNode().getConfig().get("fproxy").getOption("port").getValueString()).thenReturn("8888");
when(nodeClientCore.getNode().getConfig().get("fproxy").getOption("bindTo").getValueString()).thenReturn("127.0.0.1,0:0:0:0:0:0:0:1");
when(nodeClientCore.getAllowedDownloadDirs()).thenReturn(new File[] { new File("/test") });
}

private final FProxyToadlet fProxyToadlet = new FProxyToadlet(highLevelSimpleClient, nodeClientCore, fetchTracker);

{
try {
FieldSetter.setField(fProxyToadlet, FProxyToadlet.class.getDeclaredField("random"), new byte[0]);
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}

}
1 change: 1 addition & 0 deletions test/freenet/clients/http/FProxyToadletTest.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
FProxyToadlet.openAsText=${link}open-as-text${/link}
Loading