diff options
| author | Michael Peter Christen <mc@yacy.net> | 2026-07-11 23:40:15 +0200 |
|---|---|---|
| committer | Michael Peter Christen <mc@yacy.net> | 2026-07-11 23:40:15 +0200 |
| commit | bfddfe5e7ed90a80b4acf9a71a567b61b77cfcc4 (patch) | |
| tree | d1918d46d198606e867b9d83e0a64bff19c0891b /source/net | |
| parent | edeb6162c435e4ad9098233eed4f8c9c9b0312e2 (diff) | |
isolate Solr 9.0 Jetty client dependencies in a relocated bridge
Diffstat (limited to 'source/net')
| -rw-r--r-- | source/net/yacy/http/AdminAccessPolicy.java | 60 | ||||
| -rw-r--r-- | source/net/yacy/http/AdminAuthenticationContext.java | 25 | ||||
| -rw-r--r-- | source/net/yacy/http/InetPathAccessHandler.java | 23 | ||||
| -rw-r--r-- | source/net/yacy/http/InetPathAccessRule.java | 42 | ||||
| -rw-r--r-- | source/net/yacy/http/YaCyDigestCredential.java | 45 | ||||
| -rw-r--r-- | source/net/yacy/http/YaCySecurityHandler.java | 38 | ||||
| -rw-r--r-- | source/net/yacy/http/servlets/Jetty9ServletResource.java | 99 | ||||
| -rw-r--r-- | source/net/yacy/http/servlets/ServletResource.java | 33 | ||||
| -rw-r--r-- | source/net/yacy/http/servlets/YaCyDefaultServlet.java | 285 |
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)); + } + } + } |
