Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 104 additions & 8 deletions brouter-server/src/main/java/btools/server/RouteServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@

public class RouteServer extends Thread implements Comparable<RouteServer> {
public static final String PROFILE_UPLOAD_URL = "/brouter/profile";
public static final String PROFILES_URL = "/brouter/getprofiles";
public static final String ROUTING_URL = "/brouter";
static final String HTTP_STATUS_OK = "200 OK";
static final String HTTP_STATUS_BAD_REQUEST = "400 Bad Request";
static final String HTTP_STATUS_FORBIDDEN = "403 Forbidden";
Expand Down Expand Up @@ -83,6 +85,8 @@ public void run() {
String agent = null;
String encodings = null;
String xff = null; // X-Forwarded-For
String contentType = null;
int contentLength = -1;

// more headers until first empty line
for (; ; ) {
Expand All @@ -99,15 +103,21 @@ public void run() {
if (getline == null) {
getline = line;
}
line = line.toLowerCase();
if (line.startsWith("user-agent: ")) {
agent = line.substring("user-agent: ".length());
String lowerLine = line.toLowerCase(Locale.ROOT);
if (lowerLine.startsWith("user-agent: ")) {
agent = lowerLine.substring("user-agent: ".length());
}
if (line.startsWith("accept-encoding: ")) {
encodings = line.substring("accept-encoding: ".length());
if (lowerLine.startsWith("accept-encoding: ")) {
encodings = lowerLine.substring("accept-encoding: ".length());
}
if (line.startsWith("x-forwarded-for: ")) {
xff = line.substring("x-forwarded-for: ".length());
if (lowerLine.startsWith("x-forwarded-for: ")) {
xff = lowerLine.substring("x-forwarded-for: ".length());
}
if (lowerLine.startsWith("content-type: ")) {
contentType = line.substring("content-type: ".length()).trim();
}
if (lowerLine.startsWith("content-length: ")) {
contentLength = Integer.parseInt(lowerLine.substring("content-length: ".length()).trim());
}
}

Expand Down Expand Up @@ -147,10 +157,39 @@ public void run() {
return;
}

String url = getline.split(" ")[1];
String[] requestLineParts = getline.split(" ");
if (requestLineParts.length < 2) {
writeHttpHeader(bw, HTTP_STATUS_BAD_REQUEST);
bw.write("Malformed HTTP request line\n");
bw.flush();
return;
}

String method = requestLineParts[0];
String url = requestLineParts[1];
String path = getUrlPath(url);

if (ROUTING_URL.equals(path) && "OPTIONS".equals(method)) {
String corsHeaders = "Access-Control-Allow-Methods: GET, POST, PUT, OPTIONS\r\n"
+ "Access-Control-Allow-Headers: Content-Type\r\n";
writeHttpHeader(bw, "text/plain", null, corsHeaders, HTTP_STATUS_OK);
bw.flush();
return;
}

RoutingParamCollector routingParamCollector = new RoutingParamCollector();
Map<String, String> params = routingParamCollector.getUrlParams(url);
if (ROUTING_URL.equals(path) && isBodyRequest(method)) {
try {
params.putAll(readRoutingRequestParams(br, routingParamCollector, contentType, contentLength));
} catch (IllegalArgumentException e) {
writeHttpHeader(bw, HTTP_STATUS_BAD_REQUEST);
bw.write(e.getMessage());
bw.write("\n");
bw.flush();
return;
}
}

long maxRunningTime = getMaxRunningTime();

Expand Down Expand Up @@ -384,6 +423,54 @@ private static Map<String, String> getUrlParams(String url) throws UnsupportedEn
return params;
}

private static String getUrlPath(String url) {
int queryIdx = url.indexOf('?');
return queryIdx < 0 ? url : url.substring(0, queryIdx);
}

private static boolean isBodyRequest(String method) {
return "POST".equals(method) || "PUT".equals(method);
}

private static Map<String, String> readRoutingRequestParams(
BufferedReader br,
RoutingParamCollector routingParamCollector,
String contentType,
int contentLength) throws IOException {
Map<String, String> params = new HashMap<>();
if (contentLength == 0) {
return params;
}
if (contentLength < 0) {
return params;
}

if (contentType != null) {
String lowerContentType = contentType.toLowerCase(Locale.ROOT);
if (!lowerContentType.startsWith("application/x-www-form-urlencoded")
&& !lowerContentType.startsWith("text/plain")) {
throw new IllegalArgumentException("Unsupported Content-Type for routing request: " + contentType);
}
}

int maxRequestLength = getMaxRequestLength();
if (contentLength > maxRequestLength) {
throw new IllegalArgumentException("Routing request body too large (" + contentLength + " > " + maxRequestLength + ")");
}

char[] requestBody = new char[contentLength];
int offset = 0;
while (offset < contentLength) {
int read = br.read(requestBody, offset, contentLength - offset);
if (read < 0) {
throw new IOException("Unexpected end of routing request body");
}
offset += read;
}
params.putAll(routingParamCollector.getUrlParams(new String(requestBody)));
return params;
}

private static long getMaxRunningTime() {
long maxRunningTime = 60000;
String sMaxRunningTime = System.getProperty("maxRunningTime");
Expand All @@ -393,6 +480,15 @@ private static long getMaxRunningTime() {
return maxRunningTime;
}

private static int getMaxRequestLength() {
int maxRequestLength = 1000000;
String sMaxRequestLength = System.getProperty("maxRequestLength");
if (sMaxRequestLength != null) {
maxRequestLength = Integer.parseInt(sMaxRequestLength);
}
return maxRequestLength;
}

private static void writeHttpHeader(BufferedWriter bw, String status) throws IOException {
writeHttpHeader(bw, "text/plain", status);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,13 @@
import btools.server.ServiceContext;

/**
* URL query parameter handler for web and standalone server. Supports all
* Routing parameter handler for web and standalone server. Supports all
* BRouter features without restrictions.
* <p>
* Parameters can either be sent via the request URL (GET) or via a request
* body using {@code application/x-www-form-urlencoded} or {@code text/plain}
* (POST/PUT).
* <p>
* Parameters:
* <p>
* lonlats = lon,lat|... (unlimited list of lon,lat waypoints separated by |)
Expand All @@ -35,6 +39,10 @@
* Example URLs:
* {@code http://localhost:17777/brouter?lonlats=8.799297,49.565883|8.811764,49.563606&nogos=&profile=trekking&alternativeidx=0&format=gpx}
* {@code http://localhost:17777/brouter?lonlats=1.1,1.2|2.1,2.2|3.1,3.2|4.1,4.2&nogos=-1.1,-1.2,1|-2.1,-2.2,2&profile=shortest&alternativeidx=1&format=kml&trackname=Ride&pois=1.1,2.1,Barner Bar}
* {@code curl -X POST http://localhost:17777/brouter -H 'Content-Type: application/x-www-form-urlencoded' --data 'lonlats=8.799297,49.565883%7C8.811764,49.563606&profile=trekking&format=gpx'}
* {@code curl -X PUT http://localhost:17777/brouter -H 'Content-Type: application/x-www-form-urlencoded' --data-binary @request-body.txt}
* {@code http://localhost:17777/brouter/getprofiles}
* {@code http://localhost:17777/brouter/getprofiles/trekking.brf}
*/
public class ServerHandler extends RequestHandler {

Expand Down
54 changes: 54 additions & 0 deletions brouter-server/src/test/java/btools/server/RouteServerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,46 @@ public void defaultRouteTrekking() throws IOException, URISyntaxException {
Assert.assertEquals("4", geoJson.query("/features/0/properties/filtered ascend"));
}

@Test
public void routeRequestFromPostBody() throws IOException, URISyntaxException {
URL requestUrl = new URI(baseUrl + "brouter").toURL();
HttpURLConnection httpConnection = (HttpURLConnection) requestUrl.openConnection();

httpConnection.setRequestMethod("POST");
httpConnection.setDoOutput(true);
httpConnection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
String requestBody = "lonlats=8.723037,50.000491%7C8.712737,50.002899&profile=trekking&alternativeidx=0&format=geojson";
try (OutputStream outputStream = httpConnection.getOutputStream()) {
outputStream.write(requestBody.getBytes(StandardCharsets.UTF_8));
}

Assert.assertEquals(HttpURLConnection.HTTP_OK, httpConnection.getResponseCode());

InputStream inputStream = httpConnection.getInputStream();
JSONObject geoJson = new JSONObject(new String(inputStream.readAllBytes(), StandardCharsets.UTF_8));
Assert.assertEquals("1169", geoJson.query("/features/0/properties/track-length"));
}

@Test
public void largePolygonRequestFromPutBody() throws IOException, URISyntaxException {
URL requestUrl = new URI(baseUrl + "brouter").toURL();
HttpURLConnection httpConnection = (HttpURLConnection) requestUrl.openConnection();

httpConnection.setRequestMethod("PUT");
httpConnection.setDoOutput(true);
httpConnection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
String requestBody = "lonlats=8.723037,50.000491%7C8.712737,50.002899&profile=trekking&alternativeidx=0&format=geojson&polygons=" + buildLargePolygon();
try (OutputStream outputStream = httpConnection.getOutputStream()) {
outputStream.write(requestBody.getBytes(StandardCharsets.UTF_8));
}

Assert.assertEquals(HttpURLConnection.HTTP_OK, httpConnection.getResponseCode());

InputStream inputStream = httpConnection.getInputStream();
JSONObject geoJson = new JSONObject(new String(inputStream.readAllBytes(), StandardCharsets.UTF_8));
Assert.assertEquals("1169", geoJson.query("/features/0/properties/track-length"));
}

@Test
public void overrideParameter() throws IOException, URISyntaxException {
URL requestUrl = new URI(baseUrl + "brouter?lonlats=8.723037,50.000491%7C8.712737,50.002899&nogos=&profile=trekking&alternativeidx=0&format=geojson&profile:avoid_unsafe=1").toURL();
Expand Down Expand Up @@ -248,4 +288,18 @@ public void invalidUrl() throws IOException, URISyntaxException {

Assert.assertEquals(HttpURLConnection.HTTP_NOT_FOUND, httpConnection.getResponseCode());
}

private static String buildLargePolygon() {
StringBuilder polygon = new StringBuilder();
for (int i = 0; i < 600; i++) {
double angle = Math.PI * 2. * i / 600.;
double lon = 8.80 + Math.cos(angle) * 0.01;
double lat = 50.05 + Math.sin(angle) * 0.01;
if (i > 0) {
polygon.append(',');
}
polygon.append(lon).append(',').append(lat);
}
return polygon.toString();
}
}
41 changes: 41 additions & 0 deletions docs/developers/http_server.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,45 @@ BRouter HTTP server for various platforms.
The API endpoints exposed by this HTTP server are documented in the
`ServerHandler.java`

Routing requests to `/brouter` can use either:

* `GET` with the existing URL query parameters
* `POST` or `PUT` with the same parameter string in the request body

For request bodies, use `application/x-www-form-urlencoded` (preferred) or
`text/plain`. This is useful for large `nogos`, `polylines` or `polygons`
payloads that would otherwise hit browser, proxy or server URL-length limits.

The standalone startup script now defaults to request bodies slightly above
5 MiB:

* `BROUTER_MAX_REQUEST_LENGTH=6291456`
* `BROUTER_JAVA_XMX=256M`
* `BROUTER_JAVA_XMS=256M`
* `BROUTER_JAVA_XMN=16M`

You can override those values via environment variables, for example:

```sh
BROUTER_MAX_REQUEST_LENGTH=8388608 BROUTER_JAVA_XMX=512M ./misc/scripts/standalone/server.sh
```

Containerized deployments can pass the same environment variables through
their startup wrapper before invoking `misc/scripts/standalone/server.sh`.

For large polygon uploads it is usually easier to put the full parameter string
into a file and send it with `PUT`:

```sh
curl -X PUT http://localhost:17777/brouter \
-H 'Content-Type: application/x-www-form-urlencoded; charset=UTF-8' \
--data-binary @request-body.txt
```

Example `request-body.txt` content:

```text
lonlats=8.723037,50.000491|8.712737,50.002899&profile=trekking&alternativeidx=0&format=geojson&polygons=8.81,50.05,8.8101,50.0501,8.8102,50.0502
```

Please see also [IBRouterService.aidl](./android_service.md) for calling parameter.
16 changes: 13 additions & 3 deletions misc/scripts/standalone/server.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,18 @@ cd "$(dirname "$0")"
# BRouter standalone server
# java -cp brouter.jar btools.brouter.RouteServer <segmentdir> <profile-map> <customprofiledir> <port> <maxthreads> [bindaddress]

# maxRunningTime is the request timeout in seconds, set to 0 to disable timeout
JAVA_OPTS="-Xmx128M -Xms128M -Xmn8M -DmaxRunningTime=300 -DuseRFCMimeType=false"
# maxRunningTime is the request timeout in seconds, set to 0 to disable timeout.
# maxRequestLength is the maximum accepted request body size in bytes. The default
# is sized to allow PUT/POST bodies slightly above 5 MiB.
BROUTER_JAVA_XMX=${BROUTER_JAVA_XMX:-"256M"}
BROUTER_JAVA_XMS=${BROUTER_JAVA_XMS:-$BROUTER_JAVA_XMX}
BROUTER_JAVA_XMN=${BROUTER_JAVA_XMN:-"16M"}
BROUTER_MAX_RUNNING_TIME=${BROUTER_MAX_RUNNING_TIME:-"300"}
BROUTER_MAX_REQUEST_LENGTH=${BROUTER_MAX_REQUEST_LENGTH:-"6291456"}
BROUTER_USE_RFC_MIME_TYPE=${BROUTER_USE_RFC_MIME_TYPE:-"false"}

DEFAULT_JAVA_OPTS="-Xmx$BROUTER_JAVA_XMX -Xms$BROUTER_JAVA_XMS -Xmn$BROUTER_JAVA_XMN -DmaxRunningTime=$BROUTER_MAX_RUNNING_TIME -DmaxRequestLength=$BROUTER_MAX_REQUEST_LENGTH -DuseRFCMimeType=$BROUTER_USE_RFC_MIME_TYPE"
JAVA_OPTS=${JAVA_OPTS:-$DEFAULT_JAVA_OPTS}

# If paths are unset, first search in locations matching the directory structure
# as found in the official BRouter zip archive
Expand All @@ -28,4 +38,4 @@ if [ ! -e "$CUSTOMPROFILESPATH" ]; then
CUSTOMPROFILESPATH="../customprofiles"
fi

java $JAVA_OPTS -cp $CLASSPATH btools.server.RouteServer "$SEGMENTSPATH" "$PROFILESPATH" "$CUSTOMPROFILESPATH" 17777 1 $BINDADDRESS
exec java $JAVA_OPTS -cp $CLASSPATH btools.server.RouteServer "$SEGMENTSPATH" "$PROFILESPATH" "$CUSTOMPROFILESPATH" 17777 1 $BINDADDRESS
Loading