summaryrefslogtreecommitdiff
path: root/source
diff options
context:
space:
mode:
authorMichael Peter Christen <mc@yacy.net>2026-07-11 23:40:15 +0200
committerMichael Peter Christen <mc@yacy.net>2026-07-11 23:40:15 +0200
commitbfddfe5e7ed90a80b4acf9a71a567b61b77cfcc4 (patch)
treed1918d46d198606e867b9d83e0a64bff19c0891b /source
parentedeb6162c435e4ad9098233eed4f8c9c9b0312e2 (diff)
isolate Solr 9.0 Jetty client dependencies in a relocated bridge
Diffstat (limited to 'source')
-rw-r--r--source/net/yacy/http/AdminAccessPolicy.java60
-rw-r--r--source/net/yacy/http/AdminAuthenticationContext.java25
-rw-r--r--source/net/yacy/http/InetPathAccessHandler.java23
-rw-r--r--source/net/yacy/http/InetPathAccessRule.java42
-rw-r--r--source/net/yacy/http/YaCyDigestCredential.java45
-rw-r--r--source/net/yacy/http/YaCySecurityHandler.java38
-rw-r--r--source/net/yacy/http/servlets/Jetty9ServletResource.java99
-rw-r--r--source/net/yacy/http/servlets/ServletResource.java33
-rw-r--r--source/net/yacy/http/servlets/YaCyDefaultServlet.java285
9 files changed, 510 insertions, 140 deletions
diff --git a/source/net/yacy/http/AdminAccessPolicy.java b/source/net/yacy/http/AdminAccessPolicy.java
new file mode 100644
index 000000000..e4bad1350
--- /dev/null
+++ b/source/net/yacy/http/AdminAccessPolicy.java
@@ -0,0 +1,60 @@
+package net.yacy.http;
+
+import java.net.MalformedURLException;
+
+import net.yacy.cora.document.id.MultiProtocolURL;
+
+/** Container-neutral policy for administrator access to a request path. */
+public final class AdminAccessPolicy {
+
+ public enum Decision {
+ PUBLIC,
+ LOCAL_BYPASS,
+ ADMIN_REQUIRED
+ }
+
+ private final boolean protectAllPages;
+ private final boolean privateRobinsonMode;
+ private final boolean publicSearchPage;
+ private final boolean allowLocalhostWithoutLogin;
+ private final String adminUser;
+ private final String adminHash;
+
+ public AdminAccessPolicy(final boolean protectAllPages, final boolean privateRobinsonMode,
+ final boolean publicSearchPage, final boolean allowLocalhostWithoutLogin,
+ final String adminUser, final String adminHash) {
+ this.protectAllPages = protectAllPages;
+ this.privateRobinsonMode = privateRobinsonMode;
+ this.publicSearchPage = publicSearchPage;
+ this.allowLocalhostWithoutLogin = allowLocalhostWithoutLogin;
+ this.adminUser = adminUser;
+ this.adminHash = adminHash;
+ }
+
+ public Decision decide(final String path, final String socketPeerIp, final String referer,
+ final String authorizationHeader) {
+ if (!AdminSecurity.isProtectedPath(path, this.protectAllPages,
+ this.privateRobinsonMode, this.publicSearchPage)) {
+ return Decision.PUBLIC;
+ }
+
+ if (AdminSecurity.isLocalhostAccess(socketPeerIp, refererHost(referer))) {
+ if (this.allowLocalhostWithoutLogin || AdminSecurity.checkLocalhostLazyAuth(
+ authorizationHeader, this.adminUser, this.adminHash)) {
+ return Decision.LOCAL_BYPASS;
+ }
+ }
+ return Decision.ADMIN_REQUIRED;
+ }
+
+ private static String refererHost(final String referer) {
+ if (referer == null || referer.isEmpty()) {
+ return null;
+ }
+ try {
+ return new MultiProtocolURL(referer).getHost();
+ } catch (final MalformedURLException e) {
+ return null;
+ }
+ }
+}
diff --git a/source/net/yacy/http/AdminAuthenticationContext.java b/source/net/yacy/http/AdminAuthenticationContext.java
new file mode 100644
index 000000000..e276d4cbd
--- /dev/null
+++ b/source/net/yacy/http/AdminAuthenticationContext.java
@@ -0,0 +1,25 @@
+package net.yacy.http;
+
+import net.yacy.cora.protocol.Domains;
+
+/** Request-bound facts needed while the container verifies admin credentials. */
+public final class AdminAuthenticationContext {
+
+ private static final ThreadLocal<String> SOCKET_PEER_IP = new ThreadLocal<>();
+
+ private AdminAuthenticationContext() {
+ }
+
+ public static void setSocketPeerIp(final String ip) {
+ SOCKET_PEER_IP.set(ip);
+ }
+
+ public static void clear() {
+ SOCKET_PEER_IP.remove();
+ }
+
+ public static boolean isLocalhostRequest() {
+ final String ip = SOCKET_PEER_IP.get();
+ return ip != null && Domains.isLocalhost(ip);
+ }
+}
diff --git a/source/net/yacy/http/InetPathAccessHandler.java b/source/net/yacy/http/InetPathAccessHandler.java
index 84a3ddc4a..6d3651b83 100644
--- a/source/net/yacy/http/InetPathAccessHandler.java
+++ b/source/net/yacy/http/InetPathAccessHandler.java
@@ -98,23 +98,14 @@ public class InetPathAccessHandler extends InetAccessHandler {
*/
protected void addPattern(final String pattern, final PathMappings<InetAddressSet> pathMappings)
throws IllegalArgumentException {
- if (pattern != null && !pattern.isEmpty()) {
- final int idx = pattern.indexOf('|');
-
- final String addr = idx > 0 ? pattern.substring(0, idx) : pattern;
- final String path = (idx > 0 && (pattern.length() > idx + 1)) ? pattern.substring(idx + 1) : "/*";
-
- if (!addr.isEmpty()) {
- final PathSpec pathSpec = PathSpec.from(path);
- InetAddressSet addresses = pathMappings.get(pathSpec);
- if (addresses == null) {
- addresses = new InetAddressSet();
- pathMappings.put(pathSpec, addresses);
- }
- addresses.add(addr);
-
- }
+ final InetPathAccessRule rule = InetPathAccessRule.parse(pattern);
+ final PathSpec pathSpec = PathSpec.from(rule.pathPattern());
+ InetAddressSet addresses = pathMappings.get(pathSpec);
+ if (addresses == null) {
+ addresses = new InetAddressSet();
+ pathMappings.put(pathSpec, addresses);
}
+ addresses.add(rule.addressPattern());
}
/**
diff --git a/source/net/yacy/http/InetPathAccessRule.java b/source/net/yacy/http/InetPathAccessRule.java
new file mode 100644
index 000000000..f93f6336b
--- /dev/null
+++ b/source/net/yacy/http/InetPathAccessRule.java
@@ -0,0 +1,42 @@
+package net.yacy.http;
+
+/** Container-neutral representation of a server-client address/path rule. */
+public final class InetPathAccessRule {
+
+ private static final String DEFAULT_PATH = "/*";
+
+ private final String addressPattern;
+ private final String pathPattern;
+
+ private InetPathAccessRule(final String addressPattern, final String pathPattern) {
+ this.addressPattern = addressPattern;
+ this.pathPattern = pathPattern;
+ }
+
+ public static InetPathAccessRule parse(final String pattern) {
+ if (pattern == null || pattern.isEmpty()) {
+ throw new IllegalArgumentException("Access rule must not be empty");
+ }
+ final int separator = pattern.indexOf('|');
+ final String address = separator > 0 ? pattern.substring(0, separator) : pattern;
+ final String path = separator > 0 && pattern.length() > separator + 1
+ ? pattern.substring(separator + 1)
+ : DEFAULT_PATH;
+ if (address.isEmpty()) {
+ throw new IllegalArgumentException("Access rule has no address: " + pattern);
+ }
+ return new InetPathAccessRule(address, path);
+ }
+
+ public String addressPattern() {
+ return this.addressPattern;
+ }
+
+ public String pathPattern() {
+ return this.pathPattern;
+ }
+
+ public String asJettyPattern() {
+ return this.addressPattern + '|' + this.pathPattern;
+ }
+}
diff --git a/source/net/yacy/http/YaCyDigestCredential.java b/source/net/yacy/http/YaCyDigestCredential.java
index 450c12f1f..f11c676d6 100644
--- a/source/net/yacy/http/YaCyDigestCredential.java
+++ b/source/net/yacy/http/YaCyDigestCredential.java
@@ -24,7 +24,6 @@
package net.yacy.http;
-import net.yacy.cora.protocol.Domains;
import net.yacy.search.Switchboard;
import net.yacy.search.SwitchboardConstants;
@@ -46,43 +45,6 @@ public class YaCyDigestCredential extends Credential {
private static final long serialVersionUID = -3527894085562480001L;
- /**
- * True socket peer IP of the request currently being authenticated on this thread.
- * <p>
- * Jetty's {@link Credential#check(Object)} API does not hand over the request, so the
- * credential can not tell on its own whether a request comes from localhost. The
- * {@link YaCySecurityHandler} therefore publishes the request's socket peer IP here for
- * the duration of the request (set at the start of handling, cleared in a finally block).
- * This replaces the former global "recent localhost access" timestamp, which was a
- * process-wide value not bound to the request being checked.
- */
- private static final ThreadLocal<String> REQUEST_CLIENT_IP = new ThreadLocal<String>();
-
- /**
- * Publish the socket peer IP of the request being authenticated on the current thread.
- * Must be paired with {@link #clearRequestClientIP()} in a finally block.
- * @param ip the true socket peer IP (never an X-Real-IP derived address)
- */
- public static void setRequestClientIP(final String ip) {
- REQUEST_CLIENT_IP.set(ip);
- }
-
- /**
- * Remove the request client IP published for the current thread.
- */
- public static void clearRequestClientIP() {
- REQUEST_CLIENT_IP.remove();
- }
-
- /**
- * @return true when the request currently authenticated on this thread comes from
- * localhost. Fails closed (returns false) when no request IP was published.
- */
- private static boolean isRequestFromLocalhost() {
- final String ip = REQUEST_CLIENT_IP.get();
- return ip != null && Domains.isLocalhost(ip);
- }
-
private String hash; // remember password hash, either MD5(Base64(user:pwd)) or with encryption prefix "MD5:" + MD5(user:realm:pwd)
private String foruser; // remember the user as YaCy credential is username:pwd (not just pwd)
private Credential c;
@@ -108,14 +70,11 @@ public class YaCyDigestCredential extends Credential {
//
// We must therefore know whether THIS request comes from localhost. Jetty's
// Credential.check() is not given the request, so YaCySecurityHandler publishes
- // the request's true socket peer IP into REQUEST_CLIENT_IP for the duration of the
- // request (see the detailed rationale on YaCySecurityHandler.handle()); we read it
- // back here. Using the socket peer - not the spoofable X-Real-IP header - keeps
- // this exception restricted to genuine local callers.
+ // the true socket peer IP through AdminAuthenticationContext for this request.
return AdminSecurity.checkAdminPassword(this.foruser, this.hash,
sb.getConfig(SwitchboardConstants.ADMIN_REALM, ""),
sb.getConfig(SwitchboardConstants.ADMIN_ACCOUNT_USER_NAME, "admin"),
- isRequestFromLocalhost(),
+ AdminAuthenticationContext.isLocalhostRequest(),
(String) credentials);
}
throw new UnsupportedOperationException();
diff --git a/source/net/yacy/http/YaCySecurityHandler.java b/source/net/yacy/http/YaCySecurityHandler.java
index 436b167f1..9e6923d07 100644
--- a/source/net/yacy/http/YaCySecurityHandler.java
+++ b/source/net/yacy/http/YaCySecurityHandler.java
@@ -25,13 +25,11 @@
package net.yacy.http;
import java.io.IOException;
-import java.net.MalformedURLException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
-import net.yacy.cora.document.id.MultiProtocolURL;
import net.yacy.cora.protocol.RequestHeader;
import net.yacy.search.Switchboard;
import net.yacy.search.SwitchboardConstants;
@@ -47,7 +45,7 @@ import org.eclipse.jetty.server.Request;
* and updates AccessTracker
*
* This is a thin adapter to the servlet container: the decision logic is in
- * the container neutral {@link AdminSecurity}.
+ * the container-neutral {@link AdminAccessPolicy} and {@link AdminSecurity}.
*/
public class YaCySecurityHandler extends ConstraintSecurityHandler {
@@ -82,11 +80,11 @@ public class YaCySecurityHandler extends ConstraintSecurityHandler {
public void handle(final String pathInContext, final Request baseRequest,
final HttpServletRequest request, final HttpServletResponse response)
throws IOException, ServletException {
- YaCyDigestCredential.setRequestClientIP(baseRequest.getRemoteAddr());
+ AdminAuthenticationContext.setSocketPeerIp(baseRequest.getRemoteAddr());
try {
super.handle(pathInContext, baseRequest, request, response);
} finally {
- YaCyDigestCredential.clearRequestClientIP();
+ AdminAuthenticationContext.clear();
}
}
@@ -109,30 +107,20 @@ public class YaCySecurityHandler extends ConstraintSecurityHandler {
final String remoteip = request.getRemoteAddr();
serverAccessTracker.track(remoteip, pathInContext);
- final boolean protectedPage = AdminSecurity.isProtectedPath(pathInContext,
+ final AdminAccessPolicy policy = new AdminAccessPolicy(
sb.getConfigBool(SwitchboardConstants.ADMIN_ACCOUNT_All_PAGES, false),
sb.isRobinsonMode() && !sb.isPublicRobinson(),
- sb.getConfigBool(SwitchboardConstants.PUBLIC_SEARCHPAGE, true));
- if (!protectedPage) {
+ sb.getConfigBool(SwitchboardConstants.PUBLIC_SEARCHPAGE, true),
+ sb.getConfigBool(SwitchboardConstants.ADMIN_ACCOUNT_FOR_LOCALHOST, false),
+ sb.getConfig(SwitchboardConstants.ADMIN_ACCOUNT_USER_NAME, "admin"),
+ sb.getConfig(SwitchboardConstants.ADMIN_ACCOUNT_B64MD5, ""));
+ final AdminAccessPolicy.Decision decision = policy.decide(pathInContext, remoteip,
+ request.getHeader(RequestHeader.REFERER), request.getHeader(RequestHeader.AUTHORIZATION));
+ if (decision == AdminAccessPolicy.Decision.PUBLIC) {
return super.prepareConstraintInfo(pathInContext, request);
}
-
- String refererHost;
- try {
- refererHost = new MultiProtocolURL(request.getHeader(RequestHeader.REFERER)).getHost();
- } catch (MalformedURLException e) {
- refererHost = null;
- }
- if (AdminSecurity.isLocalhostAccess(remoteip, refererHost)) {
- if (sb.getConfigBool(SwitchboardConstants.ADMIN_ACCOUNT_FOR_LOCALHOST, false)) {
- return null;
- }
- // last chance to authorize using the admin from localhost
- if (AdminSecurity.checkLocalhostLazyAuth(request.getHeader(RequestHeader.AUTHORIZATION),
- sb.getConfig(SwitchboardConstants.ADMIN_ACCOUNT_USER_NAME, "admin"),
- sb.getConfig(SwitchboardConstants.ADMIN_ACCOUNT_B64MD5, ""))) {
- return null;
- }
+ if (decision == AdminAccessPolicy.Decision.LOCAL_BYPASS) {
+ return null;
}
RoleInfo roleinfo = new RoleInfo();
roleinfo.setChecked(true); // RoleInfo.setChecked() : in Jetty this means - marked to have any security constraint
diff --git a/source/net/yacy/http/servlets/Jetty9ServletResource.java b/source/net/yacy/http/servlets/Jetty9ServletResource.java
new file mode 100644
index 000000000..ba1d3d576
--- /dev/null
+++ b/source/net/yacy/http/servlets/Jetty9ServletResource.java
@@ -0,0 +1,99 @@
+package net.yacy.http.servlets;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URL;
+
+import org.eclipse.jetty.util.resource.Resource;
+
+/** Jetty 9 adapter for the static-resource operations used by YaCy. */
+final class Jetty9ServletResource implements ServletResource {
+
+ private final Resource delegate;
+
+ private Jetty9ServletResource(final Resource delegate) {
+ this.delegate = delegate;
+ }
+
+ static void disableDefaultCaches() {
+ Resource.setDefaultUseCaches(false);
+ }
+
+ static ServletResource from(final String location) throws IOException {
+ return wrap(Resource.newResource(location));
+ }
+
+ static ServletResource from(final File file) throws IOException {
+ return wrap(Resource.newResource(file));
+ }
+
+ static ServletResource from(final URL url) throws IOException {
+ return wrap(Resource.newResource(url));
+ }
+
+ private static ServletResource wrap(final Resource resource) {
+ return resource == null ? null : new Jetty9ServletResource(resource);
+ }
+
+ @Override
+ public ServletResource addPath(final String path) throws IOException {
+ return wrap(this.delegate.addPath(path));
+ }
+
+ @Override
+ public boolean exists() {
+ return this.delegate.exists();
+ }
+
+ @Override
+ public boolean isDirectory() {
+ return this.delegate.isDirectory();
+ }
+
+ @Override
+ public long lastModified() {
+ return this.delegate.lastModified();
+ }
+
+ @Override
+ public long length() {
+ return this.delegate.length();
+ }
+
+ @Override
+ public String getName() {
+ return this.delegate.getName();
+ }
+
+ @Override
+ public File getFile() throws IOException {
+ return this.delegate.getFile();
+ }
+
+ @Override
+ public InputStream getInputStream() throws IOException {
+ return this.delegate.getInputStream();
+ }
+
+ @Override
+ public String getListHTML(final String base, final boolean parent, final String query) throws IOException {
+ return this.delegate.getListHTML(base, parent, query);
+ }
+
+ @Override
+ public void writeTo(final OutputStream output, final long start, final long count) throws IOException {
+ this.delegate.writeTo(output, start, count);
+ }
+
+ @Override
+ public void close() {
+ this.delegate.close();
+ }
+
+ @Override
+ public String toString() {
+ return this.delegate.toString();
+ }
+}
diff --git a/source/net/yacy/http/servlets/ServletResource.java b/source/net/yacy/http/servlets/ServletResource.java
new file mode 100644
index 000000000..74e35113a
--- /dev/null
+++ b/source/net/yacy/http/servlets/ServletResource.java
@@ -0,0 +1,33 @@
+package net.yacy.http.servlets;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.File;
+
+/** Container-neutral view of a static resource served by YaCy. */
+public interface ServletResource extends AutoCloseable {
+
+ ServletResource addPath(String path) throws IOException;
+
+ boolean exists();
+
+ boolean isDirectory();
+
+ long lastModified();
+
+ long length();
+
+ String getName();
+
+ File getFile() throws IOException;
+
+ InputStream getInputStream() throws IOException;
+
+ String getListHTML(String base, boolean parent, String query) throws IOException;
+
+ void writeTo(OutputStream output, long start, long count) throws IOException;
+
+ @Override
+ void close();
+}
diff --git a/source/net/yacy/http/servlets/YaCyDefaultServlet.java b/source/net/yacy/http/servlets/YaCyDefaultServlet.java
index 54cb227a0..e90a7619f 100644
--- a/source/net/yacy/http/servlets/YaCyDefaultServlet.java
+++ b/source/net/yacy/http/servlets/YaCyDefaultServlet.java
@@ -28,7 +28,9 @@ import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
-import java.io.OutputStream;
+import java.io.OutputStream;
+import java.io.Writer;
+import java.io.FilterOutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@@ -57,16 +59,7 @@ import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileItemFactory;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
-import org.eclipse.jetty.http.HttpHeader;
-import org.eclipse.jetty.http.HttpMethod;
-import org.eclipse.jetty.http.MimeTypes;
-import org.eclipse.jetty.io.WriterOutputStream;
-import org.eclipse.jetty.server.InclusiveByteRange;
-import org.eclipse.jetty.util.MultiPartOutputStream;
-import org.eclipse.jetty.util.URIUtil;
-import org.eclipse.jetty.util.resource.Resource;
-
-import com.google.common.net.HttpHeaders;
+import com.google.common.net.HttpHeaders;
import net.yacy.cora.date.GenericFormatter;
import net.yacy.cora.document.analysis.Classification;
@@ -127,10 +120,18 @@ public class YaCyDefaultServlet extends HttpServlet {
private static final long serialVersionUID = 4900000000000001110L;
protected ServletContext _servletContext;
protected boolean _acceptRanges = true;
- protected boolean _dirAllowed = true;
- protected Resource _resourceBase;
- protected MimeTypes _mimeTypes;
- protected String[] _welcomes;
+ protected boolean _dirAllowed = true;
+ protected ServletResource _resourceBase;
+ protected String[] _welcomes;
+
+ private static final String METHOD_HEAD = "HEAD";
+ private static final String METHOD_POST = "POST";
+ private static final String HEADER_CONTENT_RANGE = "Content-Range";
+ private static final String HEADER_IF_MODIFIED_SINCE = "If-Modified-Since";
+ private static final String HEADER_IF_UNMODIFIED_SINCE = "If-Unmodified-Since";
+ private static final String HEADER_REQUEST_RANGE = "Request-Range";
+ private static final String MIME_TEXT_HTML = "text/html";
+ private static final String MIME_TEXT_HTML_UTF8 = "text/html;charset=utf-8";
protected File _htLocalePath;
protected File _htDocsPath;
@@ -149,8 +150,7 @@ public class YaCyDefaultServlet extends HttpServlet {
this._servletContext = this.getServletContext();
- this._mimeTypes = new MimeTypes();
- final String tmpstr = this.getServletContext().getInitParameter("welcomeFile");
+ final String tmpstr = this.getServletContext().getInitParameter("welcomeFile");
if (tmpstr == null) {
this._welcomes = HTTPDFileHandler.defaultFiles;
} else {
@@ -159,14 +159,14 @@ public class YaCyDefaultServlet extends HttpServlet {
this._acceptRanges = this.getInitBoolean("acceptRanges", this._acceptRanges);
this._dirAllowed = this.getInitBoolean("dirAllowed", this._dirAllowed);
- Resource.setDefaultUseCaches(false); // caching is handled internally (prevent double caching)
+ Jetty9ServletResource.disableDefaultCaches(); // caching is handled internally (prevent double caching)
final String rb = this.getInitParameter("resourceBase");
try {
if (rb != null) {
- this._resourceBase = Resource.newResource(rb);
+ this._resourceBase = Jetty9ServletResource.from(rb);
} else {
- this._resourceBase = Resource.newResource(sb.getConfig(SwitchboardConstants.HTROOT_PATH, SwitchboardConstants.HTROOT_PATH_DEFAULT)); //default
+ this._resourceBase = Jetty9ServletResource.from(sb.getConfig(SwitchboardConstants.HTROOT_PATH, SwitchboardConstants.HTROOT_PATH_DEFAULT)); //default
}
} catch (final IOException e) {
ConcurrentLog.severe("FILEHANDLER", "event=http.resource subsystem=http result=missing-resource-base reason=" + e.getMessage());
@@ -201,14 +201,14 @@ public class YaCyDefaultServlet extends HttpServlet {
* @param pathInContext The path to find a resource for.
* @return The resource to serve.
*/
- public Resource getResource(final String pathInContext) {
- Resource r = null;
+ public ServletResource getResource(final String pathInContext) {
+ ServletResource r = null;
try {
if (this._resourceBase != null) {
r = this._resourceBase.addPath(pathInContext);
} else {
final URL u = this._servletContext.getResource(pathInContext);
- r = Resource.newResource(u);
+ r = Jetty9ServletResource.from(u);
}
if (ConcurrentLog.isFine("FILEHANDLER")) {
@@ -249,10 +249,10 @@ public class YaCyDefaultServlet extends HttpServlet {
}
String pathInContext = pathInfo == null ? "/" : pathInfo; // this is the path of the resource in _resourceBase (= path within htroot respective htDocs)
- final boolean endsWithSlash = pathInContext.endsWith(URIUtil.SLASH);
+ final boolean endsWithSlash = pathInContext.endsWith("/");
// Find the resource
- Resource resource = null;
+ ServletResource resource = null;
try {
@@ -266,7 +266,7 @@ public class YaCyDefaultServlet extends HttpServlet {
hasClass = true;
} else {
final String pathofClass = pathInContext.substring(0, p) + ".class";
- final Resource classresource = this._resourceBase.addPath(pathofClass);
+ final ServletResource classresource = this._resourceBase.addPath(pathofClass);
// Does a class resource exist?
if (classresource != null && classresource.exists() && !classresource.isDirectory()) {
hasClass = true;
@@ -281,7 +281,7 @@ public class YaCyDefaultServlet extends HttpServlet {
if (!hasClass && (resource == null || !resource.exists()) && !pathInContext.contains("..")) {
// try to get this in the alternative htDocsPath
if (resource != null) resource.close();
- resource = Resource.newResource(new File(this._htDocsPath, pathInContext));
+ resource = Jetty9ServletResource.from(new File(this._htDocsPath, pathInContext));
}
if (ConcurrentLog.isFine("FILEHANDLER")) {
@@ -301,7 +301,7 @@ public class YaCyDefaultServlet extends HttpServlet {
if (q != null && q.length() != 0) {
pathInContext += "?" + q;
}
- response.sendRedirect(response.encodeRedirectURL(URIUtil.addPaths(this._servletContext.getContextPath(), pathInContext)));
+ response.sendRedirect(response.encodeRedirectURL(addPaths(this._servletContext.getContextPath(), pathInContext)));
} else {
if (hasClass) { // this is a YaCy servlet, handle the template
this.handleTemplate(pathInfo, request, response);
@@ -379,7 +379,7 @@ public class YaCyDefaultServlet extends HttpServlet {
private boolean shouldWrapBody(final HttpServletRequest request) {
final String method = request.getMethod();
- if (method == null || !HttpMethod.POST.asString().equalsIgnoreCase(method)) {
+ if (method == null || !METHOD_POST.equalsIgnoreCase(method)) {
return false;
}
final String contentType = request.getContentType();
@@ -498,8 +498,8 @@ public class YaCyDefaultServlet extends HttpServlet {
return null;
}
for (final String _welcome : this._welcomes) {
- final String welcome_in_context = URIUtil.addPaths(pathInContext, _welcome);
- final Resource welcome = this.getResource(welcome_in_context);
+ final String welcome_in_context = addPaths(pathInContext, _welcome);
+ final ServletResource welcome = this.getResource(welcome_in_context);
if (welcome != null && welcome.exists()) {
return _welcome;
}
@@ -510,15 +510,15 @@ public class YaCyDefaultServlet extends HttpServlet {
/* Check modification date headers.
* send a 304 response instead of content if not modified since
*/
- protected boolean passConditionalHeaders(final HttpServletRequest request, final HttpServletResponse response, final Resource resource)
+ protected boolean passConditionalHeaders(final HttpServletRequest request, final HttpServletResponse response, final ServletResource resource)
throws IOException {
try {
- if (!request.getMethod().equals(HttpMethod.HEAD.asString())) {
-
- final String ifms = request.getHeader(HttpHeader.IF_MODIFIED_SINCE.asString());
+ if (!request.getMethod().equals(METHOD_HEAD)) {
+
+ final String ifms = request.getHeader(HEADER_IF_MODIFIED_SINCE);
if (ifms != null) {
- final long ifmsl = request.getDateHeader(HttpHeader.IF_MODIFIED_SINCE.asString());
+ final long ifmsl = request.getDateHeader(HEADER_IF_MODIFIED_SINCE);
if (ifmsl != -1) {
if (resource.lastModified() / 1000 <= ifmsl / 1000) {
response.reset();
@@ -530,7 +530,7 @@ public class YaCyDefaultServlet extends HttpServlet {
}
// Parse the if[un]modified dates and compare to resource
- final long date = request.getDateHeader(HttpHeader.IF_UNMODIFIED_SINCE.asString());
+ final long date = request.getDateHeader(HEADER_IF_UNMODIFIED_SINCE);
if (date != -1) {
if (resource.lastModified() / 1000 > date / 1000) {
@@ -552,7 +552,7 @@ public class YaCyDefaultServlet extends HttpServlet {
/* ------------------------------------------------------------------- */
protected void sendDirectory(final HttpServletRequest request,
final HttpServletResponse response,
- final Resource resource,
+ final ServletResource resource,
final String pathInContext)
throws IOException {
if (!this._dirAllowed) {
@@ -560,7 +560,7 @@ public class YaCyDefaultServlet extends HttpServlet {
return;
}
- final String base = URIUtil.addEncodedPaths(request.getRequestURI(), URIUtil.SLASH);
+ final String base = addPaths(request.getRequestURI(), "/");
final String dir = resource.getListHTML(base, pathInContext.length() > 1, request.getQueryString());
if (dir == null) {
@@ -569,7 +569,7 @@ public class YaCyDefaultServlet extends HttpServlet {
}
final byte[] data = dir.getBytes(StandardCharsets.UTF_8);
- response.setContentType(MimeTypes.Type.TEXT_HTML_UTF_8.asString());
+ response.setContentType(MIME_TEXT_HTML_UTF8);
response.setContentLength(data.length);
response.setHeader(HeaderFramework.CACHE_CONTROL, "no-cache, no-store");
response.setDateHeader(HeaderFramework.EXPIRES, System.currentTimeMillis() + 10000); // consider that directories are not modified that often
@@ -591,7 +591,7 @@ public class YaCyDefaultServlet extends HttpServlet {
protected void sendData(final HttpServletRequest request,
final HttpServletResponse response,
final boolean include,
- final Resource resource,
+ final ServletResource resource,
final Enumeration<String> reqRanges)
throws IOException {
@@ -602,7 +602,7 @@ public class YaCyDefaultServlet extends HttpServlet {
try {
out = response.getOutputStream();
} catch (final IllegalStateException e) {
- out = new WriterOutputStream(response.getWriter());
+ out = new CharacterOutputStream(response.getWriter());
}
// remove the last-modified field since caching otherwise does not work
@@ -629,14 +629,14 @@ public class YaCyDefaultServlet extends HttpServlet {
}
} else {
// Parse the satisfiable ranges
- final List<InclusiveByteRange> ranges = InclusiveByteRange.satisfiableRanges(reqRanges, content_length);
+ final List<HttpByteRange> ranges = HttpByteRange.satisfiableRanges(reqRanges, content_length);
// if there are no satisfiable ranges, send 416 response
if (ranges == null || ranges.isEmpty()) {
this.writeHeaders(response, resource, content_length);
response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
- response.setHeader(HttpHeader.CONTENT_RANGE.asString(),
- InclusiveByteRange.to416HeaderRangeString(content_length));
+ response.setHeader(HEADER_CONTENT_RANGE,
+ HttpByteRange.to416HeaderRangeString(content_length));
resource.writeTo(out, 0, content_length);
out.close();
return;
@@ -645,11 +645,11 @@ public class YaCyDefaultServlet extends HttpServlet {
// if there is only a single valid range (must be satisfiable
// since were here now), send that range with a 216 response
if (ranges.size() == 1) {
- final InclusiveByteRange singleSatisfiableRange = ranges.iterator().next();
+ final HttpByteRange singleSatisfiableRange = ranges.iterator().next();
final long singleLength = singleSatisfiableRange.getSize();
this.writeHeaders(response, resource, singleLength);
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
- response.setHeader(HttpHeader.CONTENT_RANGE.asString(),
+ response.setHeader(HEADER_CONTENT_RANGE,
singleSatisfiableRange.toHeaderRangeString(content_length));
resource.writeTo(out, singleSatisfiableRange.getFirst(), singleLength);
out.close();
@@ -665,14 +665,14 @@ public class YaCyDefaultServlet extends HttpServlet {
if (mimetype == null) {
ConcurrentLog.warn("FILEHANDLER","YaCyDefaultServlet: Unknown mimetype for " + request.getRequestURI());
}
- final MultiPartOutputStream multi = new MultiPartOutputStream(out);
+ final MultipartByteRangeOutputStream multi = new MultipartByteRangeOutputStream(out);
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
// If the request has a "Request-Range" header then we need to
// send an old style multipart/x-byteranges Content-Type. This
// keeps Netscape and acrobat happy. This is what Apache does.
String ctp;
- if (request.getHeader(HttpHeader.REQUEST_RANGE.asString()) != null) {
+ if (request.getHeader(HEADER_REQUEST_RANGE) != null) {
ctp = "multipart/x-byteranges; boundary=";
} else {
ctp = "multipart/byteranges; boundary=";
@@ -686,7 +686,7 @@ public class YaCyDefaultServlet extends HttpServlet {
int length = 0;
final String[] header = new String[ranges.size()];
for (int i = 0; i < ranges.size(); i++) {
- final InclusiveByteRange ibr = ranges.get(i);
+ final HttpByteRange ibr = ranges.get(i);
header[i] = ibr.toHeaderRangeString(content_length);
length +=
((i > 0) ? 2 : 0)
@@ -700,7 +700,7 @@ public class YaCyDefaultServlet extends HttpServlet {
response.setContentLength(length);
for (int i = 0; i < ranges.size(); i++) {
- final InclusiveByteRange ibr = ranges.get(i);
+ final HttpByteRange ibr = ranges.get(i);
multi.startPart(mimetype, new String[]{HeaderFramework.CONTENT_RANGE + ": " + header[i]});
final long start = ibr.getFirst();
@@ -731,10 +731,10 @@ public class YaCyDefaultServlet extends HttpServlet {
}
/* ------------------------------------------------------------ */
- protected void writeHeaders(final HttpServletResponse response, final Resource resource, final long count) {
+ protected void writeHeaders(final HttpServletResponse response, final ServletResource resource, final long count) {
if (response.getContentType() == null) {
final String extensionmime;
- if ((extensionmime = this._mimeTypes.getMimeByExtension(resource.getName())) != null) {
+ if ((extensionmime = this._servletContext.getMimeType(resource.getName())) != null) {
response.setContentType(extensionmime);
}
}
@@ -1035,7 +1035,7 @@ public class YaCyDefaultServlet extends HttpServlet {
}
this.updateRespHeadersForImages(target, response);
- final String mimeType = Classification.ext2mime(targetExt, MimeTypes.Type.TEXT_HTML.asString());
+ final String mimeType = Classification.ext2mime(targetExt, MIME_TEXT_HTML);
response.setContentType(mimeType);
response.setContentLength(result.length());
response.setStatus(HttpServletResponse.SC_OK);
@@ -1150,7 +1150,7 @@ public class YaCyDefaultServlet extends HttpServlet {
templatePatterns.put(SwitchboardConstants.GREETING_IMAGE_ALT, sb.getConfig(SwitchboardConstants.GREETING_IMAGE_ALT, ""));
templatePatterns.put("clientlanguage", localeSelection);
- final String mimeType = Classification.ext2mime(targetExt, MimeTypes.Type.TEXT_HTML.asString());
+ final String mimeType = Classification.ext2mime(targetExt, MIME_TEXT_HTML);
InputStream fis;
final long fileSize = targetLocalizedFile.length();
@@ -1239,7 +1239,7 @@ public class YaCyDefaultServlet extends HttpServlet {
*/
private void writeInputStream(final HttpServletResponse response, final String targetExt, final InputStream inStream)
throws IOException {
- final String mimeType = Classification.ext2mime(targetExt, MimeTypes.Type.TEXT_HTML.asString());
+ final String mimeType = Classification.ext2mime(targetExt, MIME_TEXT_HTML);
response.setContentType(mimeType);
response.setStatus(HttpServletResponse.SC_OK);
final byte[] buffer = new byte[4096];
@@ -1372,4 +1372,177 @@ public class YaCyDefaultServlet extends HttpServlet {
}
}
+ /** Join two servlet paths without depending on a container URI utility. */
+ private static String addPaths(final String first, final String second) {
+ if (first == null || first.isEmpty()) {
+ return second == null ? "" : second;
+ }
+ if (second == null || second.isEmpty()) {
+ return first;
+ }
+ final boolean firstEndsWithSlash = first.charAt(first.length() - 1) == '/';
+ final boolean secondStartsWithSlash = second.charAt(0) == '/';
+ if (firstEndsWithSlash && secondStartsWithSlash) {
+ return first + second.substring(1);
+ }
+ if (!firstEndsWithSlash && !secondStartsWithSlash) {
+ return first + '/' + second;
+ }
+ return first + second;
+ }
+
+ /** Equivalent of Jetty's WriterOutputStream default-charset behavior. */
+ private static final class CharacterOutputStream extends OutputStream {
+
+ private final Writer writer;
+ private final byte[] singleByte = new byte[1];
+
+ private CharacterOutputStream(final Writer writer) {
+ this.writer = writer;
+ }
+
+ @Override
+ public void write(final int value) throws IOException {
+ this.singleByte[0] = (byte) value;
+ this.write(this.singleByte);
+ }
+
+ @Override
+ public void write(final byte[] data, final int offset, final int length) throws IOException {
+ this.writer.write(new String(data, offset, length));
+ }
+
+ @Override
+ public void flush() throws IOException {
+ this.writer.flush();
+ }
+
+ @Override
+ public void close() throws IOException {
+ this.writer.close();
+ }
+ }
+
+ /** Inclusive byte range parsed from an HTTP Range header. */
+ private static final class HttpByteRange {
+
+ private final long first;
+ private final long last;
+
+ private HttpByteRange(final long first, final long last) {
+ this.first = first;
+ this.last = last;
+ }
+
+ private long getFirst() {
+ return this.first;
+ }
+
+ private long getLast() {
+ return this.last;
+ }
+
+ private long getSize() {
+ return this.last - this.first + 1;
+ }
+
+ private String toHeaderRangeString(final long size) {
+ return "bytes " + this.first + '-' + this.last + '/' + size;
+ }
+
+ private static String to416HeaderRangeString(final long size) {
+ return "bytes */" + size;
+ }
+
+ private static List<HttpByteRange> satisfiableRanges(
+ final Enumeration<String> headers, final long size) {
+ final List<HttpByteRange> ranges = new java.util.ArrayList<>();
+ while (headers != null && headers.hasMoreElements()) {
+ final String header = headers.nextElement();
+ if (header == null || !header.regionMatches(true, 0, "bytes=", 0, 6)) {
+ continue;
+ }
+ final String[] specifications = header.substring(6).split(",");
+ for (final String specification : specifications) {
+ final HttpByteRange range = parse(specification.trim(), size);
+ if (range != null) {
+ ranges.add(range);
+ }
+ }
+ }
+ return ranges;
+ }
+
+ private static HttpByteRange parse(final String specification, final long size) {
+ final int separator = specification.indexOf('-');
+ if (separator < 0 || size <= 0) {
+ return null;
+ }
+ try {
+ if (separator == 0) {
+ final long suffixLength = Long.parseLong(specification.substring(1).trim());
+ if (suffixLength <= 0) {
+ return null;
+ }
+ return new HttpByteRange(Math.max(0, size - suffixLength), size - 1);
+ }
+ final long first = Long.parseLong(specification.substring(0, separator).trim());
+ if (first < 0 || first >= size) {
+ return null;
+ }
+ final String lastText = specification.substring(separator + 1).trim();
+ final long last = lastText.isEmpty()
+ ? size - 1
+ : Math.min(Long.parseLong(lastText), size - 1);
+ return last < first ? null : new HttpByteRange(first, last);
+ } catch (final NumberFormatException e) {
+ return null;
+ }
+ }
+ }
+
+ /** Minimal multipart writer for HTTP byte-range responses. */
+ private static final class MultipartByteRangeOutputStream extends FilterOutputStream {
+
+ private static final byte[] CRLF = {'\r', '\n'};
+ private final String boundary = "yacy-" + java.util.UUID.randomUUID().toString();
+ private boolean firstPart = true;
+
+ private MultipartByteRangeOutputStream(final OutputStream output) {
+ super(output);
+ }
+
+ private String getBoundary() {
+ return this.boundary;
+ }
+
+ private void startPart(final String contentType, final String[] headers) throws IOException {
+ if (!this.firstPart) {
+ this.out.write(CRLF);
+ }
+ this.firstPart = false;
+ this.writeAscii("--" + this.boundary + "\r\n");
+ if (contentType != null) {
+ this.writeAscii(HeaderFramework.CONTENT_TYPE + ": " + contentType + "\r\n");
+ }
+ if (headers != null) {
+ for (final String header : headers) {
+ this.writeAscii(header + "\r\n");
+ }
+ }
+ this.out.write(CRLF);
+ }
+
+ @Override
+ public void close() throws IOException {
+ this.out.write(CRLF);
+ this.writeAscii("--" + this.boundary + "--\r\n");
+ super.close();
+ }
+
+ private void writeAscii(final String value) throws IOException {
+ this.out.write(value.getBytes(StandardCharsets.ISO_8859_1));
+ }
+ }
+
}