Compare commits

...

5 Commits

Author SHA1 Message Date
idk
49a0d65f85 add javadoc to magnet filter 2022-11-26 17:21:39 -05:00
idk
f97bfbff7a Start Magnet filter 2022-11-26 00:48:47 -05:00
idk
dc076c1601 Add javadoc comments 2022-11-26 00:42:43 -05:00
idk
4ff3210fd7 Create the X-I2P-Hostname filter 2022-11-26 00:08:14 -05:00
idk
a39c85b7a8 create abstract cache for adding headers to eepsite with filter, migrate XI2PLocationFilter to do it 2022-11-25 21:03:54 -05:00
4 changed files with 489 additions and 167 deletions

View File

@ -0,0 +1,257 @@
package net.i2p.servlet.filters;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Properties;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import net.i2p.I2PAppContext;
import net.i2p.I2PException;
import net.i2p.data.DataHelper;
import net.i2p.data.Destination;
import net.i2p.data.PrivateKeyFile;
import net.i2p.util.Log;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.handler.HandlerWrapper;
/**
* Abstract class that allows implementers to easily manipulate headers for I2P
* sites. It provides configuration, caching, and the ability to differentate
* between requests coming from I2P and requests coming from the clearweb.
*
* @String cachedHeader a value which can be either a whole header "Value" or a
* fragment of one
* @String headerKey a value which is the "Key" in the header applied to the
* response.
* @boolean applyToI2P set to true to apply the header to responses bound to
* I2P clients
* @boolean applyToClearnet set to true to apply the header tor responses bound
* to non-I2P clients
* @long cacheTimeout a value in milliseconds to wait before re-computing a
* cachedHeader value
*
* @since 0.9.57
*/
public abstract class XI2PHeaderFilter extends HandlerWrapper {
public String cachedHeader = null;
public String headerKey = null;
public boolean applyToI2P = false;
public boolean applyToClearnet = false;
private long lastFailure = -1;
public final long cacheTimeout;
public static final String encodeUTF = StandardCharsets.UTF_8.toString();
public XI2PHeaderFilter(final long ft) { cacheTimeout = ft; }
protected final Log _log =
I2PAppContext.getGlobalContext().logManager().getLog(
XI2PHeaderFilter.class);
private synchronized void setCachedHeader(String cacheableHeader) {
if (_log.shouldInfo()) {
_log.info("Checking cachedHeader header prefix" + cacheableHeader);
}
if (cachedHeader != null) {
return;
}
if (cacheableHeader == null) {
return;
}
if (cacheableHeader.equals("")) {
return;
}
cachedHeader = cacheableHeader;
if (_log.shouldInfo()) {
_log.info("Caching cachedHeader header prefix" + cachedHeader);
}
}
private synchronized boolean shouldRecheck() {
boolean settable = (cachedHeader == null);
if (!settable) {
return settable;
}
if (lastFailure == -1) {
lastFailure = System.currentTimeMillis();
if (_log.shouldDebug()) {
_log.debug(
"New instance, attempting to set cachedHeader header for the first time");
}
return settable;
}
if ((System.currentTimeMillis() - lastFailure) > cacheTimeout) {
lastFailure = System.currentTimeMillis();
if (_log.shouldDebug()) {
_log.debug(
"More than ten minutes since failing attempt to re-check cachedHeader header");
}
return settable;
}
if (_log.shouldDebug()) {
_log.debug("Not attempting to re-check cachedHeader header");
}
return false;
}
protected boolean isFromI2P(final HttpServletRequest httpRequest) {
final String hashHeader = httpRequest.getHeader("X-I2P-DestHash");
if (hashHeader == null) {
return true;
}
return false;
}
/**
* getCachableHeader obtains the "cacheable" part of a header and stores
* it in the cachedHeader variable.
*
* @param httpRequest the HttpServletRequest from the caller
* @param request the Request from the caller
* @return String the cachable part of the header
*
*/
public abstract String getCachableHeader(final HttpServletRequest httpRequest,
final Request request);
/**
* headerContents computes the final contents which will be used as the header
* "Key" in the key-value pair.
*
* @param httpRequest the HttpServletRequest from the caller
* @param request the Request from the caller
* @return the full header
*
*/
public abstract String headerContents(final HttpServletRequest httpRequest,
final Request request);
private void setHeader(final Request request,
final HttpServletRequest httpRequest,
HttpServletResponse httpResponse) {
if (shouldRecheck()) {
String cacheableHeader = getCachableHeader(httpRequest, request);
if (_log.shouldInfo()) {
_log.info("Checking cachedHeader header IP " + request.getLocalAddr() +
" port " + request.getLocalPort() + " prefix " +
cacheableHeader);
}
setCachedHeader(cacheableHeader);
}
String headerValue = headerContents(httpRequest, request);
if (headerValue != null) {
if (_log.shouldInfo()) {
_log.info("Checking cachedHeader header" + headerValue);
}
httpResponse.addHeader(headerKey, headerValue);
}
}
@Override
public void handle(final String target, final Request request,
final HttpServletRequest httpRequest,
HttpServletResponse httpResponse)
throws IOException, ServletException {
if (headerKey != null) {
// final String hashHeader = httpRequest.getHeader("X-I2P-DestHash");
if (isFromI2P(httpRequest) && applyToI2P) {
setHeader(request, httpRequest, httpResponse);
}
if (!isFromI2P(httpRequest) && applyToClearnet) {
setHeader(request, httpRequest, httpResponse);
}
}
_handler.handle(target, request, httpRequest, httpResponse);
}
protected synchronized Destination getTunnelDestination(String host,
String port) {
Properties tunnelProps = getTunnelProperties(host, port);
File configDir = I2PAppContext.getGlobalContext().getConfigDir();
String kf = tunnelProps.getProperty("privKeyFile");
if (kf != null) {
File keyFile = new File(kf);
if (!keyFile.isAbsolute()) {
keyFile = new File(configDir, kf);
}
if (keyFile.exists()) {
PrivateKeyFile pkf = new PrivateKeyFile(keyFile);
try {
return pkf.getDestination();
} catch (I2PException e) {
if (_log.shouldWarn()) {
_log.warn(
"I2PException Unable to get Destination value, keys arent ready. This is probably safe to ignore and will go away after the first run." +
e);
}
return null;
} catch (IOException e) {
if (_log.shouldWarn()) {
_log.warn(
"IOE Unable to get Destination value, location is uninitialized due file not found. This probably means the keys aren't ready. This is probably safe to ignore." +
e);
}
return null;
}
}
_log.warn(
"Unable to get Destination value, location is not a service tunnel.");
}
return null;
}
private synchronized Properties getTunnelProperties(String host,
String port) {
File configDir = I2PAppContext.getGlobalContext().getConfigDir();
File tunnelConfig = new File(configDir, "i2ptunnel.config");
boolean isSingleFile = tunnelConfig.exists();
if (!isSingleFile) {
File tunnelConfigD = new File(configDir, "i2ptunnel.config.d");
File[] configFiles =
tunnelConfigD.listFiles(new net.i2p.util.FileSuffixFilter(".config"));
if (configFiles == null) {
return null;
}
for (int fnum = 0; fnum < configFiles.length; fnum++) {
Properties tunnelProps = new Properties();
try {
DataHelper.loadProps(tunnelProps, configFiles[fnum]);
String targetHost = tunnelProps.getProperty("targetHost");
boolean hostmatch =
(host.equals(targetHost) || "0.0.0.0".equals(targetHost) ||
"::".equals(targetHost));
if (hostmatch && port.equals(tunnelProps.getProperty("targetPort"))) {
return tunnelProps;
}
} catch (IOException ioe) {
if (_log.shouldWarn()) {
_log.warn(
"IOE Unable to find a spoofed hosntame, location is uninitialized. This is probably safe to ignore. location='" +
ioe + "'");
}
return null;
}
}
} else {
// don't bother
}
return null;
}
protected synchronized String getSpoofedHostname(String host, String port) {
Properties tunnelProps = getTunnelProperties(host, port);
String sh = tunnelProps.getProperty("spoofedHost");
if (sh != null) {
if (sh.endsWith(".i2p")) {
return sh;
}
}
if (_log.shouldWarn()) {
_log.warn("Unable to find a spoofed hostname in any file");
}
return null;
}
}

View File

@ -0,0 +1,73 @@
package net.i2p.servlet.filters;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import net.i2p.data.Destination;
import org.eclipse.jetty.server.Request;
/**
* Adds a header, X-I2P-Hostname, to requests when they come from inside of
* I2P. This header contains a valid addresshelper and can be used to indicate
* the preferred human-readable hostname for the site without resorting to a
* jump service or adding the hostname to a subscription feed. Uses the
* spoofed hostname to configure the hostname, and the base64 destination as
* the bases of the addresshelper.
*
* @since 0.9.57
*/
public class XI2PHostnameFilter extends XI2PHeaderFilter {
private static final long failTimeout = 600000;
public XI2PHostnameFilter() {
super(failTimeout);
headerKey = "X-I2P-Hostname";
applyToI2P = true;
}
/**
* getCachableHeader obtains the base64 of the tunnel by loading the keys from
* the tunnel config file, and stores it in the cachedHeader
*
* @param httpRequest the HttpServletRequest from the caller
* @param request the Request from the caller
* @return the data to cache
*
*/
public synchronized String getCachableHeader(
final HttpServletRequest httpRequest, final Request request) {
if (isFromI2P(httpRequest)) {
Destination destination = getTunnelDestination(
request.getLocalAddr(), String.valueOf(request.getLocalPort()));
if (destination != null) {
return destination.toBase64();
}
}
return null;
}
/**
* headerContents computes the addresshelper which is the value to the
* X-I2P-Hostname key.
*
* @param httpRequest the HttpServletRequest from the caller
* @param request the Request from the caller
* @return the full header
*
*/
public synchronized String
headerContents(final HttpServletRequest httpRequest, final Request request) {
if (cachedHeader != null) {
String spoofedHostname = getSpoofedHostname(
request.getLocalAddr(), String.valueOf(request.getLocalPort()));
if (spoofedHostname == null) {
return null;
}
String scheme = httpRequest.getScheme();
if (scheme == null) {
scheme = "http://";
}
return scheme + spoofedHostname + "?i2paddresshelper=" + cachedHeader;
}
return null;
}
}

View File

@ -1,188 +1,99 @@
package net.i2p.servlet.filters;
import java.io.IOException;
import java.io.File;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URI;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Properties;
import javax.servlet.ServletException;
import java.net.URISyntaxException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.handler.HandlerWrapper;
import net.i2p.I2PAppContext;
import net.i2p.I2PException;
import net.i2p.data.DataHelper;
import net.i2p.data.Destination;
import net.i2p.data.PrivateKeyFile;
import net.i2p.util.Log;
import org.eclipse.jetty.server.Request;
/**
* Adds a header, X-I2P-Location, to requests when they do **not** come in on an I2P hostname.
* This header contains a URL that looks like: [scheme://][i2phostname.i2p][/path][?query]
* and expresses the I2P-Equivalent URL of the clearnet query. Clients can use this to prompt
* users to switch from a non-I2P host to an I2P host or to redirect them automatically. It
* automatically enabled on the default I2P site located on port 7658 by default.
* Adds a header, X-I2P-Location, to requests when they do **not** come in on an
* I2P hostname. This header contains a URL that looks like:
*
* [scheme://][i2phostname.i2p][/path][?query]
*
* This expresses the I2P-Equivalent URL of the clearnet query. Clients can use
* this to prompt users to switch from a non-I2P host to an I2P host or to
* redirect them automatically. It automatically enabled on the default I2P site
* located on port 7658 by default.
*
* @since 0.9.51
*/
public class XI2PLocationFilter extends HandlerWrapper {
private String X_I2P_Location = null;
private long lastFailure = -1;
private static final long failTimeout = 600000;
private static final String encodeUTF = StandardCharsets.UTF_8.toString();
private final Log _log = I2PAppContext.getGlobalContext().logManager().getLog(XI2PLocationFilter.class);
public class XI2PLocationFilter extends XI2PHeaderFilter {
private static final long failTimeout = 600000;
public XI2PLocationFilter() {
super(failTimeout);
headerKey = "X-I2P-Location";
applyToClearnet = true;
}
private synchronized void setLocation(String xi2plocation) {
if (_log.shouldInfo())
_log.info("Checking X-I2P-Location header prefix" + xi2plocation);
if (X_I2P_Location != null)
return ;
if (xi2plocation == null)
return ;
if (xi2plocation.equals(""))
return ;
X_I2P_Location = xi2plocation;
if (_log.shouldInfo())
_log.info("Caching X-I2P-Location header prefix" + X_I2P_Location);
private synchronized String getXI2PLocation(String host, String port) {
String sh = getSpoofedHostname(host, port);
if (sh != null) {
return sh;
}
private synchronized boolean shouldRecheck(){
boolean settable = (X_I2P_Location == null);
if (!settable) return settable;
if (lastFailure == -1) {
lastFailure = System.currentTimeMillis();
if (_log.shouldDebug())
_log.debug("New instance, attempting to set X-I2P-Location header for the first time");
return settable;
}
if ((System.currentTimeMillis() - lastFailure) > failTimeout){
lastFailure = System.currentTimeMillis();
if (_log.shouldDebug())
_log.debug("More than ten minutes since failing attempt to re-check X-I2P-Location header");
return settable;
}
if (_log.shouldDebug())
_log.debug("Not attempting to re-check X-I2P-Location header");
return false;
Destination destination = getTunnelDestination(host, port);
if (destination != null) {
return destination.toBase32();
}
return null;
}
private synchronized String getXI2PLocation(String host, String port) {
File configDir = I2PAppContext.getGlobalContext().getConfigDir();
File tunnelConfig = new File(configDir, "i2ptunnel.config");
boolean isSingleFile = tunnelConfig.exists();
if (!isSingleFile) {
File tunnelConfigD = new File(configDir, "i2ptunnel.config.d");
File[] configFiles = tunnelConfigD.listFiles(new net.i2p.util.FileSuffixFilter(".config"));
if (configFiles == null)
return null;
for (int fnum=0; fnum < configFiles.length; fnum++) {
Properties tunnelProps = new Properties();
try {
DataHelper.loadProps(tunnelProps, configFiles[fnum]);
String targetHost = tunnelProps.getProperty("targetHost");
boolean hostmatch = (host.equals(targetHost) || "0.0.0.0".equals(targetHost) || "::".equals(targetHost));
if ( hostmatch && port.equals(tunnelProps.getProperty("targetPort")) ) {
String sh = tunnelProps.getProperty("spoofedHost");
if (sh != null) {
if (sh.endsWith(".i2p"))
return sh;
}
String kf = tunnelProps.getProperty("privKeyFile");
if (kf != null) {
File keyFile = new File(kf);
if (!keyFile.isAbsolute())
keyFile = new File(configDir, kf);
if (keyFile.exists()) {
PrivateKeyFile pkf = new PrivateKeyFile(keyFile);
try {
Destination rv = pkf.getDestination();
if (rv != null)
return rv.toBase32();
} catch (I2PException e) {
if (_log.shouldWarn())
_log.warn("I2PException Unable to set X-I2P-Location, keys arent ready. This is probably safe to ignore and will go away after the first run." + e);
return null;
} catch (IOException e) {
if (_log.shouldWarn())
_log.warn("IOE Unable to set X-I2P-Location, location is uninitialized due file not found. This probably means the keys aren't ready. This is probably safe to ignore." + e);
return null;
}
}
}
if (_log.shouldWarn())
_log.warn("Unable to set X-I2P-Location, location is target not found in any I2PTunnel config file. This should never happen.");
return null;
}
} catch (IOException ioe) {
if (_log.shouldWarn())
_log.warn("IOE Unable to set X-I2P-Location, location is uninitialized. This is probably safe to ignore. location='" + ioe + "'");
return null;
}
}
/**
* getCachableHeader obtains the spoofed hostname or the base32 from
* the tunnel configuration and caches it.
*
* @param httpRequest the HttpServletRequest from the caller
* @param request the Request from the caller
* @return the data to cache
*
*/
public synchronized String getCachableHeader(
final HttpServletRequest httpRequest, final Request request) {
return getXI2PLocation(request.getLocalAddr(),
String.valueOf(request.getLocalPort()));
}
/**
* headerContents computes the addresshelper which is the value to the
* X-I2P-Hostname key.
*
* @param httpRequest the HttpServletRequest from the caller
* @param request the Request from the caller
* @return the full header
*
*/
public synchronized String
headerContents(final HttpServletRequest httpRequest, final Request request) {
if (cachedHeader != null) {
String scheme = httpRequest.getScheme();
if (scheme == null) {
scheme = "";
}
String path = httpRequest.getPathInfo();
if (path == null) {
path = "";
}
String query = httpRequest.getQueryString();
if (query == null) {
query = "";
}
try {
if (query.equals("")) {
URI uri = new URI(scheme, cachedHeader, path, null);
String encodedURL = uri.toASCIIString();
return encodedURL;
} else {
// don't bother
URI uri = new URI(scheme, cachedHeader, path, query, null);
String encodedURL = uri.toASCIIString();
return encodedURL;
}
} catch (URISyntaxException use) {
return null;
}
}
private synchronized String headerContents(final HttpServletRequest httpRequest) {
if (X_I2P_Location != null) {
String scheme = httpRequest.getScheme();
if (scheme == null)
scheme = "";
String path = httpRequest.getPathInfo();
if (path == null)
path = "";
String query = httpRequest.getQueryString();
if (query == null)
query = "";
try {
if (query.equals("")) {
URI uri = new URI(scheme, X_I2P_Location, path, null);
String encodedURL = uri.toASCIIString();
return encodedURL;
} else {
URI uri = new URI(scheme, X_I2P_Location, path, query, null);
String encodedURL = uri.toASCIIString();
return encodedURL;
}
}catch(URISyntaxException use){
return null;
}
}
return null;
}
@Override
public void handle(final String target, final Request request, final HttpServletRequest httpRequest, HttpServletResponse httpResponse)
throws IOException, ServletException {
final String hashHeader = httpRequest.getHeader("X-I2P-DestHash");
if (hashHeader == null) {
if (shouldRecheck()) {
String xi2plocation = getXI2PLocation(request.getLocalAddr(), String.valueOf(request.getLocalPort()));
if (_log.shouldInfo())
_log.info("Checking X-I2P-Location header IP " + request.getLocalAddr() + " port " + request.getLocalPort() + " prefix " + xi2plocation);
setLocation(xi2plocation);
}
String headerURL = headerContents(httpRequest);
if (headerURL != null) {
if (_log.shouldInfo())
_log.info("Checking X-I2P-Location header" + headerURL);
httpResponse.addHeader("X-I2P-Location", headerURL);
}
}
_handler.handle(target, request, httpRequest, httpResponse);
}
return null;
}
}

View File

@ -0,0 +1,81 @@
package net.i2p.servlet.filters;
// import java.net.URI;
// import java.net.URISyntaxException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import net.i2p.data.Destination;
import org.eclipse.jetty.server.Request;
/**
* Adds a header, X-I2P-Magnet, to requests when they come in on an I2P
* hostname. This header contains a link that looks like:
*
* [magnet:]?[xt=urn:btih:[BT_INFO_HASH]]&[tr=[BT_TRACKER_URL]]&[ws=[WEBSEED_PAYLOAD_URL]]
*
* This corresponds to a torrent where the content is exactly the file being
* served, and the webseed URL corresponds exactly to the I2P URL of the
* request. The torrent is structured thus:
*
* [hostname || base32]/[path/to/url]
*
* This can be useful for 2 things in particular:
*
* 1. A user who wishes to mirror the I2P site can use the header to keep the
* mirror up-to-date infohash changes, file has changed, delete the old one and
* participate in the new swarm
* 2. A user who uses an HTTP client which has insight into the torrent
* client(Such as I2P in Private Browsing) can optionally replace in-I2P
* resources with locally-cached resources from the corresponding I2P torrent.
*
* This allows sites to be more "Permanent" by spreading their files across
* the users, making the files themselves resistant to takedown, and if widely
* adopted, would reduce the bandwidth used for serving files over I2P. It may
* also cause the downloads to appear differently than HTTP downloads, being
* more loosely clustered and out-of-order.
*
* @since 0.9.57
*/
public class XI2PMagnetFilter extends XI2PHeaderFilter {
private static final long failTimeout = 600000;
public XI2PMagnetFilter() {
super(failTimeout);
headerKey = "X-I2P-Magnet";
applyToI2P = true;
}
/**
* getCachableHeader creates a torrent from a file to be served, generates a
* magnet link, and caches it.
*
* @param httpRequest the HttpServletRequest from the caller
* @param request the Request from the caller
* @return the data to cache
*
*/
public synchronized String getCachableHeader(
final HttpServletRequest httpRequest, final Request request) {
if (isFromI2P(httpRequest)) {
return null;
}
return null;
}
/**
* headerContents computes the magnet link and appends the webseed, which is
* the value to the X-I2P-Magnet key.
*
* @param httpRequest the HttpServletRequest from the caller
* @param request the Request from the caller
* @return the full header
*
*/
public synchronized String
headerContents(final HttpServletRequest httpRequest, final Request request) {
if (cachedHeader != null) {
return null;
}
return null;
}
}