This is an automated email from the ASF dual-hosted git repository.
markt-asf pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit b08b71e054c83ec3fbcec845ae062cad47fb5ac8
Author: Mark Thomas <[email protected]>
AuthorDate: Mon Aug 10 21:59:48 2026 +0100
Add test cases for the new URL / URL pattern encoded/decoded option
Generated with a combination of
- Claude + Opus 5
- CoPilot + ChatGPT-5.6
---
build.xml | 1 +
.../core/TestApplicationFilterRegistration.java | 52 ++++++++++++
.../core/TestApplicationServletRegistration.java | 48 +++++++++++
.../startup/TestContextConfigAnnotation.java | 27 +++++++
.../TestUrlPatternsProvidedInDecodedForm.java | 94 ++++++++++++++++++++++
.../apache/catalina/startup/UrlPatternFilter.java | 38 +++++++++
.../apache/catalina/startup/UrlPatternServlet.java | 26 ++++++
.../util/descriptor/web/TestWebXmlParser-web.xml | 63 +++++++++++++++
.../util/descriptor/web/TestWebXmlParser.java | 67 +++++++++++++++
test/webapp-url-patterns/WEB-INF/web.xml | 56 +++++++++++++
10 files changed, 472 insertions(+)
diff --git a/build.xml b/build.xml
index 2ad873f3e4..eeae5bdaca 100644
--- a/build.xml
+++ b/build.xml
@@ -1950,6 +1950,7 @@
<include name="**/service-config.txt"/>
<include name="**/logging-non-rotatable.properties"/>
<include name="**/TestStrings.properties"/>
+ <include name="**/*-web.xml"/>
</fileset>
</copy>
</target>
diff --git a/test/org/apache/catalina/core/TestApplicationFilterRegistration.java b/test/org/apache/catalina/core/TestApplicationFilterRegistration.java
new file mode 100644
index 0000000000..5f73fe6aff
--- /dev/null
+++ b/test/org/apache/catalina/core/TestApplicationFilterRegistration.java
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.catalina.core;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import org.apache.tomcat.util.descriptor.web.FilterDef;
+import org.apache.tomcat.util.descriptor.web.FilterMap;
+
+public class TestApplicationFilterRegistration {
+
+ @Test
+ public void testUrlPatternEncoded() {
+ doTestUrlPattern(false, "/filter%");
+ }
+
+ @Test
+ public void testUrlPatternDecoded() {
+ doTestUrlPattern(true, "/filter%25");
+ }
+
+ private void doTestUrlPattern(boolean urlPatternsProvidedInDecodedForm, String expectedPattern) {
+ StandardContext context = new StandardContext();
+ context.setUrlPatternsProvidedInDecodedForm(urlPatternsProvidedInDecodedForm);
+
+ FilterDef filterDef = new FilterDef();
+ filterDef.setFilterName("filter");
+ context.addFilterDef(filterDef);
+
+ ApplicationFilterRegistration registration = new ApplicationFilterRegistration(filterDef, context);
+ registration.addMappingForUrlPatterns(null, true, "/filter%25");
+
+ FilterMap[] filterMaps = context.findFilterMaps();
+ Assert.assertEquals(1, filterMaps.length);
+ Assert.assertArrayEquals(new String[] { expectedPattern }, filterMaps[0].getURLPatterns());
+ }
+}
diff --git a/test/org/apache/catalina/core/TestApplicationServletRegistration.java b/test/org/apache/catalina/core/TestApplicationServletRegistration.java
new file mode 100644
index 0000000000..4452a0baf9
--- /dev/null
+++ b/test/org/apache/catalina/core/TestApplicationServletRegistration.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.catalina.core;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import org.apache.catalina.Wrapper;
+
+public class TestApplicationServletRegistration {
+
+ @Test
+ public void testUrlPatternEncoded() {
+ doTestUrlPattern(false, "/servlet%");
+ }
+
+ @Test
+ public void testUrlPatternDecoded() {
+ doTestUrlPattern(true, "/servlet%25");
+ }
+
+ private void doTestUrlPattern(boolean urlPatternsProvidedInDecodedForm, String expectedPattern) {
+ StandardContext context = new StandardContext();
+ context.setUrlPatternsProvidedInDecodedForm(urlPatternsProvidedInDecodedForm);
+
+ Wrapper wrapper = context.createWrapper();
+ wrapper.setName("servlet");
+ context.addChild(wrapper);
+
+ ApplicationServletRegistration registration = new ApplicationServletRegistration(wrapper, context);
+ Assert.assertTrue(registration.addMapping("/servlet%25").isEmpty());
+ Assert.assertEquals("servlet", context.findServletMapping(expectedPattern));
+ }
+}
diff --git a/test/org/apache/catalina/startup/TestContextConfigAnnotation.java b/test/org/apache/catalina/startup/TestContextConfigAnnotation.java
index ba2b80ac09..585a19a4f8 100644
--- a/test/org/apache/catalina/startup/TestContextConfigAnnotation.java
+++ b/test/org/apache/catalina/startup/TestContextConfigAnnotation.java
@@ -268,6 +268,33 @@ public class TestContextConfigAnnotation {
Assert.assertNull(filterDef);
}
+ @Test
+ public void testUrlPatternsEncoded() throws Exception {
+ doTestUrlPatterns(false, "%");
+ }
+
+ @Test
+ public void testUrlPatternsDecoded() throws Exception {
+ doTestUrlPatterns(true, "%25");
+ }
+
+ @SuppressWarnings("deprecation")
+ private void doTestUrlPatterns(boolean urlPatternsProvidedInDecodedForm, String expectedSuffix) throws Exception {
+ WebXml webXml = new WebXml(urlPatternsProvidedInDecodedForm);
+ Map<String,JavaClassCacheEntry> javaClassCache = new HashMap<>();
+ ContextConfig config = new ContextConfig();
+
+ File servletFile = paramClassResource("org/apache/catalina/startup/UrlPatternServlet");
+ config.processAnnotationsFile(servletFile, webXml, false, javaClassCache);
+ Assert.assertEquals("urlPatternServlet",
+ webXml.getServletMappings().get("/servlet" + expectedSuffix));
+
+ File filterFile = paramClassResource("org/apache/catalina/startup/UrlPatternFilter");
+ config.processAnnotationsFile(filterFile, webXml, false, javaClassCache);
+ FilterMap filterMap = webXml.getFilterMappings().iterator().next();
+ Assert.assertArrayEquals(new String[] { "/filter" + expectedSuffix }, filterMap.getURLPatterns());
+ }
+
@Test
public void testCheckHandleTypes() throws Exception {
Map<String,JavaClassCacheEntry> javaClassCache = new HashMap<>();
diff --git a/test/org/apache/catalina/startup/TestUrlPatternsProvidedInDecodedForm.java b/test/org/apache/catalina/startup/TestUrlPatternsProvidedInDecodedForm.java
new file mode 100644
index 0000000000..ff3a2c5d7e
--- /dev/null
+++ b/test/org/apache/catalina/startup/TestUrlPatternsProvidedInDecodedForm.java
@@ -0,0 +1,94 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.catalina.startup;
+
+import java.io.File;
+
+import jakarta.servlet.http.HttpServletResponse;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import org.apache.catalina.Context;
+import org.apache.tomcat.util.buf.ByteChunk;
+
+/**
+ * End to end tests for the <code>urlPatternsProvidedInDecodedForm</code> Context attribute. The web application maps a
+ * Servlet at <code>/servlet%25</code> and protects <code>/secure%25</code> with a security constraint that denies all
+ * access. Whether those patterns are stored as <code>%</code> or as <code>%25</code> determines which request URI
+ * reaches them once the URI has been decoded.
+ */
+public class TestUrlPatternsProvidedInDecodedForm extends TomcatBaseTest {
+
+ /*
+ * The URI that decodes to "%".
+ */
+ private static final String ENCODED_FORM_URI_SUFFIX = "%25";
+
+ /*
+ * The URI that decodes to "%25".
+ */
+ private static final String DECODED_FORM_URI_SUFFIX = "%2525";
+
+
+ @Test
+ public void testEncodedForm() throws Exception {
+ // Patterns are decoded when read so the Servlet is mapped at "/servlet%"
+ doTestUrlPatterns(false, ENCODED_FORM_URI_SUFFIX, DECODED_FORM_URI_SUFFIX);
+ }
+
+
+ @Test
+ public void testDecodedForm() throws Exception {
+ // Patterns are used as provided so the Servlet is mapped at "/servlet%25"
+ doTestUrlPatterns(true, DECODED_FORM_URI_SUFFIX, ENCODED_FORM_URI_SUFFIX);
+ }
+
+
+ @SuppressWarnings("deprecation")
+ private void doTestUrlPatterns(boolean urlPatternsProvidedInDecodedForm, String matchingSuffix,
+ String nonMatchingSuffix) throws Exception {
+
+ Tomcat tomcat = getTomcatInstance();
+
+ File appDir = new File("test/webapp-url-patterns");
+ Context ctx = tomcat.addWebapp(null, "/test", appDir.getAbsolutePath());
+ ctx.setUrlPatternsProvidedInDecodedForm(urlPatternsProvidedInDecodedForm);
+
+ tomcat.start();
+
+ // The Servlet mapping is only reachable via the URI that decodes to the configured pattern
+ ByteChunk body = new ByteChunk();
+ Assert.assertEquals(HttpServletResponse.SC_OK, getUrl(uri("/test/servlet", matchingSuffix), body, null));
+ Assert.assertEquals("OK", body.toString());
+
+ Assert.assertEquals(HttpServletResponse.SC_NOT_FOUND,
+ getUrl(uri("/test/servlet", nonMatchingSuffix), new ByteChunk(), null));
+
+ // The security constraint follows the same pattern so it only protects the matching URI
+ Assert.assertEquals(HttpServletResponse.SC_FORBIDDEN,
+ getUrl(uri("/test/secure", matchingSuffix), new ByteChunk(), null));
+
+ Assert.assertEquals(HttpServletResponse.SC_NOT_FOUND,
+ getUrl(uri("/test/secure", nonMatchingSuffix), new ByteChunk(), null));
+ }
+
+
+ private String uri(String path, String suffix) {
+ return "http://localhost:" + getPort() + path + suffix;
+ }
+}
diff --git a/test/org/apache/catalina/startup/UrlPatternFilter.java b/test/org/apache/catalina/startup/UrlPatternFilter.java
new file mode 100644
index 0000000000..4327dc035c
--- /dev/null
+++ b/test/org/apache/catalina/startup/UrlPatternFilter.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.catalina.startup;
+
+import java.io.IOException;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.GenericFilter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+import jakarta.servlet.annotation.WebFilter;
+
+@WebFilter(value = "/filter%25", filterName = "urlPatternFilter")
+public class UrlPatternFilter extends GenericFilter {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+ throws IOException, ServletException {
+ chain.doFilter(request, response);
+ }
+}
diff --git a/test/org/apache/catalina/startup/UrlPatternServlet.java b/test/org/apache/catalina/startup/UrlPatternServlet.java
new file mode 100644
index 0000000000..a656628754
--- /dev/null
+++ b/test/org/apache/catalina/startup/UrlPatternServlet.java
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.catalina.startup;
+
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+
+@WebServlet(value = "/servlet%25", name = "urlPatternServlet")
+public class UrlPatternServlet extends HttpServlet {
+
+ private static final long serialVersionUID = 1L;
+}
diff --git a/test/org/apache/tomcat/util/descriptor/web/TestWebXmlParser-web.xml b/test/org/apache/tomcat/util/descriptor/web/TestWebXmlParser-web.xml
new file mode 100644
index 0000000000..e9be914953
--- /dev/null
+++ b/test/org/apache/tomcat/util/descriptor/web/TestWebXmlParser-web.xml
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!--
+ Every URL and URL pattern in this file uses %25 so that the two forms are
+ easy to tell apart. If the patterns are treated as being provided in encoded
+ form they will be decoded to "%". If they are treated as being provided in
+ decoded form they will be left as "%25".
+-->
+<web-app>
+ <error-page>
+ <error-code>404</error-code>
+ <location>/error%25</location>
+ </error-page>
+ <filter>
+ <filter-name>filter</filter-name>
+ <filter-class>org.apache.tomcat.TestFilter</filter-class>
+ </filter>
+ <filter-mapping>
+ <filter-name>filter</filter-name>
+ <url-pattern>/filter%25</url-pattern>
+ </filter-mapping>
+ <servlet>
+ <servlet-name>servlet</servlet-name>
+ <servlet-class>org.apache.tomcat.TestServlet</servlet-class>
+ </servlet>
+ <servlet-mapping>
+ <servlet-name>servlet</servlet-name>
+ <url-pattern>/servlet%25</url-pattern>
+ </servlet-mapping>
+ <jsp-config>
+ <jsp-property-group>
+ <url-pattern>/jsp%25</url-pattern>
+ </jsp-property-group>
+ </jsp-config>
+ <login-config>
+ <auth-method>FORM</auth-method>
+ <form-login-config>
+ <form-login-page>/login%25</form-login-page>
+ <form-error-page>/login-error%25</form-error-page>
+ </form-login-config>
+ </login-config>
+ <security-constraint>
+ <web-resource-collection>
+ <web-resource-name>resource</web-resource-name>
+ <url-pattern>/secure%25</url-pattern>
+ </web-resource-collection>
+ </security-constraint>
+</web-app>
diff --git a/test/org/apache/tomcat/util/descriptor/web/TestWebXmlParser.java b/test/org/apache/tomcat/util/descriptor/web/TestWebXmlParser.java
new file mode 100644
index 0000000000..5bcf23a10a
--- /dev/null
+++ b/test/org/apache/tomcat/util/descriptor/web/TestWebXmlParser.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.util.descriptor.web;
+
+import java.net.URL;
+import java.util.Set;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class TestWebXmlParser {
+
+ private static final String WEB_XML = "TestWebXmlParser-web.xml";
+
+ @Test
+ public void testUrlPatternsEncoded() throws Exception {
+ doTestUrlPatterns(false, "%");
+ }
+
+ @Test
+ public void testUrlPatternsDecoded() throws Exception {
+ doTestUrlPatterns(true, "%25");
+ }
+
+ @SuppressWarnings("deprecation")
+ private void doTestUrlPatterns(boolean urlPatternsProvidedInDecodedForm, String expectedSuffix) throws Exception {
+ URL webXmlUrl = TestWebXmlParser.class.getResource(WEB_XML);
+ Assert.assertNotNull("Could not locate " + WEB_XML, webXmlUrl);
+
+ WebXml webXml = new WebXml(urlPatternsProvidedInDecodedForm);
+ WebXmlParser parser = new WebXmlParser(false, false, true, urlPatternsProvidedInDecodedForm);
+ Assert.assertTrue(parser.parseWebXml(webXmlUrl, webXml, false));
+
+ Assert.assertEquals("servlet", webXml.getServletMappings().get("/servlet" + expectedSuffix));
+
+ FilterMap filterMap = webXml.getFilterMappings().iterator().next();
+ Assert.assertArrayEquals(new String[] { "/filter" + expectedSuffix }, filterMap.getURLPatterns());
+
+ JspPropertyGroup jspPropertyGroup = webXml.getJspPropertyGroups().iterator().next();
+ Assert.assertEquals(Set.of("/jsp" + expectedSuffix), jspPropertyGroup.getUrlPatterns());
+
+ LoginConfig loginConfig = webXml.getLoginConfig();
+ Assert.assertEquals("/login" + expectedSuffix, loginConfig.getLoginPage());
+ Assert.assertEquals("/login-error" + expectedSuffix, loginConfig.getErrorPage());
+
+ ErrorPage errorPage = webXml.getErrorPages().values().iterator().next();
+ Assert.assertEquals("/error" + expectedSuffix, errorPage.getLocation());
+
+ SecurityConstraint securityConstraint = webXml.getSecurityConstraints().iterator().next();
+ SecurityCollection securityCollection = securityConstraint.findCollection("resource");
+ Assert.assertArrayEquals(new String[] { "/secure" + expectedSuffix }, securityCollection.findPatterns());
+ }
+}
diff --git a/test/webapp-url-patterns/WEB-INF/web.xml b/test/webapp-url-patterns/WEB-INF/web.xml
new file mode 100644
index 0000000000..f1a64bcdab
--- /dev/null
+++ b/test/webapp-url-patterns/WEB-INF/web.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee
+ https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
+ version="6.0"
+ metadata-complete="true">
+
+ <!--
+ Used by TestUrlPatternsProvidedInDecodedForm. The %25 in the URL patterns
+ is decoded to "%" if the patterns are treated as being provided in encoded
+ form and left as "%25" if they are treated as being provided in decoded
+ form. The request URI required to reach each mapping therefore differs
+ between the two modes.
+ -->
+
+ <servlet>
+ <servlet-name>tester</servlet-name>
+ <servlet-class>org.apache.catalina.startup.TesterServlet</servlet-class>
+ </servlet>
+
+ <servlet-mapping>
+ <servlet-name>tester</servlet-name>
+ <url-pattern>/servlet%25</url-pattern>
+ </servlet-mapping>
+
+ <servlet-mapping>
+ <servlet-name>tester</servlet-name>
+ <url-pattern>/secure%25</url-pattern>
+ </servlet-mapping>
+
+ <security-constraint>
+ <web-resource-collection>
+ <web-resource-name>secure</web-resource-name>
+ <url-pattern>/secure%25</url-pattern>
+ </web-resource-collection>
+ <auth-constraint/>
+ </security-constraint>
+
+</web-app>
lmpx.com only provides a reader for public news (NNTP) servers. It is not
affiliated with the servers or forums shown here and is not responsible for
the content of articles, which is written by their respective authors.