Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Feature/qrest cors #269

Merged
merged 3 commits into from
Nov 24, 2022
Merged
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
44 changes: 44 additions & 0 deletions doc/src/asciidoc/module_qrest.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ specified in the `queue` property, but you can override those with a route like
<2> Likewise, `POST` starting with `/v2` will get queued to the `TXNMGR.2` too.



The TransactionManager is configured like this:

[source,xml]
Expand Down Expand Up @@ -382,3 +383,46 @@ Processing transaction ${id} <1>
<1> The 'id' property is also provided by the `DynamicContent` participant using the
transaction id.

==== CORS configuration

QRest supports CORS that can be configured like this:

[source,xml]
------------
<qrest class='org.jpos.qrest.RestServer' logger='Q2'>
...
...
<cors path="/api/abc" <1>
max-age="600"
allow-null-origin="false"
allow-credentials="true">
<origin>http://jpos.org</origin> <2>
<origin>https://jpos.org</origin>
<allow-method>GET</allow-method> <3>
<allow-method>POST</allow-method>
<allow-method>PUT</allow-method>
<allow-method>REMOVE</allow-method>
<expose-header>Content-Type</expose-header> <4>
<expose-header>Authorization</expose-header>
<request-header>consumer-id</request-header> <5>
</cors>
<cors path="/api/xyz" ...>
...
...
</cors>
</qrest>
------------

<1> The optional `cors` element supports `max-age`, `allow-null-origin` and `allow-credentials` attributes.
<2> One or more `origin` elements can be added. If no `origin` element is specified, we assum _any_ origin.
<3> Multiple `allow-method` elements can be specified.
<4> Multiple `expose-header` elements can be specified.
<5> Multiple `request-header` elements can be specified.

[NOTE]
======
CORS can be configured on a system-wide basis by not providing a `path` attribute.

The last entry with no path is taken as the system's default.
======

2 changes: 1 addition & 1 deletion modules/qrest/src/dist/deploy/30_qrest_txnmgr.xml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
</group>
<group name="upload_file">
<participant class="org.jpos.qrest.ExtractFile" />
<participant class="org.jpos.qrest.test.participant.DumpFile" />
<participant class="org.jpos.qrest.test.participant.DumpFile" enabled="${test.enabled:false}" />
</group>
<group name="index">
<participant class="org.jpos.qrest.participant.StaticContent">
Expand Down
70 changes: 68 additions & 2 deletions modules/qrest/src/main/java/org/jpos/qrest/RestServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.*;
import io.netty.handler.codec.http.cors.CorsConfig;
import io.netty.handler.codec.http.cors.CorsConfigBuilder;
import io.netty.handler.ssl.SslHandler;
import io.netty.handler.timeout.IdleStateHandler;
import org.jdom2.Element;
Expand Down Expand Up @@ -76,6 +78,8 @@ public class RestServer extends QBeanSupport implements Runnable, XmlConfigurabl
private int maxInitialLineLength;
private int maxChunkSize;
private boolean validateHeaders;
private Map<String,CorsConfig> corsConfig = new LinkedHashMap<>();
private CorsConfig defaultCorsConfig;

public static final int DEFAULT_MAX_CONTENT_LENGTH = 512*1024;

Expand All @@ -97,8 +101,9 @@ public void initChannel(SocketChannel ch) throws Exception {
if (enableTLS) {
ch.pipeline().addLast(new SslHandler(getSSLEngine(sslContext), true));
}
ch.pipeline().addLast(new HttpServerCodec(maxInitialLineLength, maxHeaderSize, maxChunkSize, validateHeaders));
ch.pipeline().addLast(new HttpObjectAggregator(maxContentLength));
ch.pipeline()
.addLast(new HttpServerCodec(maxInitialLineLength, maxHeaderSize, maxChunkSize, validateHeaders))
.addLast(new HttpObjectAggregator(maxContentLength));
ch.pipeline().addLast(new RestSession(RestServer.this));
}
})
Expand Down Expand Up @@ -163,6 +168,16 @@ public void queue (FullHttpRequest request, Context ctx) {
sp.out(getQueue(request), ctx, 60000L);
}

public CorsConfig getCorsConfig (FullHttpRequest request) {
return corsConfig
.entrySet()
.stream()
.filter(e -> request.uri().startsWith(e.getKey()))
.map(Map.Entry::getValue)
.findFirst()
.orElse(defaultCorsConfig);
}

@Override
public void setConfiguration (Configuration cfg) throws ConfigurationException {
super.setConfiguration(cfg);
Expand All @@ -186,6 +201,14 @@ public void setConfiguration (Configuration cfg) throws ConfigurationException {
@Override
public void setConfiguration(Element e) throws ConfigurationException {
try {
for (Element c : e.getChildren("cors")) {
String path = c.getAttributeValue("path");
CorsConfig cc = getCorsConfig(c);
if (path != null)
corsConfig.put (path, cc);
else
defaultCorsConfig = cc;
}
for (Element r : e.getChildren("route")) {
routes.computeIfAbsent(
r.getAttributeValue("method"),
Expand Down Expand Up @@ -288,4 +311,47 @@ private String getQueue(FullHttpRequest request) {
}
return cfg.get("queue");
}

private CorsConfig getCorsConfig(Element e) {
String[] origins = e.getChildren("origin")
.stream()
.map(Element::getTextTrim)
.toArray(String[]::new);

CorsConfigBuilder ccb = origins.length > 0 ?
CorsConfigBuilder.forOrigins(origins) :
CorsConfigBuilder.forAnyOrigin();

if ("true".equalsIgnoreCase(e.getAttributeValue("allow-null-origin", "false")))
ccb.allowNullOrigin();

ccb.exposeHeaders(
e.getChildren("expose-header")
.stream()
.map(Element::getTextTrim)
.toArray(String[]::new)
);
if ("true".equalsIgnoreCase(e.getAttributeValue("allow-credentials", "false")))
ccb.allowCredentials();

long maxAge = Long.parseLong(e.getAttributeValue("max-age", "0"));
if (maxAge > 0)
ccb.maxAge(maxAge);

ccb.allowedRequestMethods(
e.getChildren("allow-method")
.stream()
.map(Element::getTextTrim)
.map(HttpMethod::valueOf)
.toArray(HttpMethod[]::new)
);

ccb.allowedRequestHeaders(
e.getChildren("request-header")
.stream()
.map(Element::getTextTrim)
.toArray(String[]::new)
);
return ccb.build();
}
}
24 changes: 22 additions & 2 deletions modules/qrest/src/main/java/org/jpos/qrest/RestSession.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,26 @@
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.http.*;
import io.netty.handler.codec.http.cors.CorsConfig;
import io.netty.handler.codec.http.cors.CorsHandler;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.util.AttributeKey;
import io.netty.util.CharsetUtil;
import org.jpos.transaction.Context;
import org.jpos.util.LogEvent;
import org.jpos.util.Logger;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import static io.netty.buffer.Unpooled.copiedBuffer;

public class RestSession extends ChannelInboundHandlerAdapter {
private RestServer server;
private String contentKey;
private AttributeKey<HttpVersion> httpVersion = AttributeKey.valueOf("httpVersion");

RestSession(RestServer server) {
this.server = server;
Expand All @@ -41,11 +49,20 @@ public class RestSession extends ChannelInboundHandlerAdapter {

@Override
public void channelRead(ChannelHandlerContext ch, Object msg) throws Exception {
Context ctx = new Context();
if (msg instanceof FullHttpRequest) {
final FullHttpRequest request = (FullHttpRequest) msg;
if (request.method().equals(HttpMethod.OPTIONS)) {
CorsConfig corsConfig = server.getCorsConfig(request);
if (corsConfig != null) {
new CorsHandler(corsConfig).channelRead(ch, msg);
return;
}
}
Context ctx = new Context();
ctx.put(Constants.SESSION, ch);
ctx.put(Constants.REQUEST, request);
ch.channel().attr(httpVersion).set(request.protocolVersion());

if (contentKey != null)
ctx.put(contentKey, request.content().toString(CharsetUtil.UTF_8));
server.queue(request, ctx);
Expand All @@ -71,8 +88,11 @@ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
}
}
Logger.log(evt);

HttpVersion version = ctx.channel().attr(httpVersion).get();

ctx.writeAndFlush(new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1,
version,
HttpResponseStatus.INTERNAL_SERVER_ERROR,
copiedBuffer(cause.getMessage().getBytes())
));
Expand Down
23 changes: 12 additions & 11 deletions modules/qrest/src/main/java/org/jpos/qrest/SendResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.*;
import io.netty.util.ReferenceCountUtil;
Expand All @@ -31,7 +32,6 @@
import org.jpos.transaction.Context;

import java.io.Serializable;
import java.util.Arrays;

import static io.netty.buffer.Unpooled.copiedBuffer;
import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE;
Expand All @@ -55,7 +55,7 @@ public void commit (long id, Serializable context) {
Context ctx = (Context) context;
ChannelHandlerContext ch = ctx.get(SESSION);
FullHttpRequest request = ctx.get(REQUEST);
FullHttpResponse response = getResponse(ctx);
FullHttpResponse response = getResponse(ctx, request.protocolVersion());
sendResponse(ctx, ch, request, response);
}

Expand All @@ -64,7 +64,7 @@ public void abort (long id, Serializable context) {
Context ctx = (Context) context;
ChannelHandlerContext ch = ctx.get(SESSION);
FullHttpRequest request = ctx.get(REQUEST);
FullHttpResponse response = getResponse(ctx);
FullHttpResponse response = getResponse(ctx, request.protocolVersion());
sendResponse(ctx, ch, request, response);
}

Expand All @@ -79,18 +79,19 @@ private void sendResponse (Context ctx, ChannelHandlerContext ch, FullHttpReques
headers.set(HttpHeaderNames.CONTENT_TYPE, contentType);
headers.set(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
ChannelFuture cf = ch.writeAndFlush(response);

if (!keepAlive)
ch.close();
cf.addListener(ChannelFutureListener.CLOSE);
} finally {
ReferenceCountUtil.release(request);
}
}

private FullHttpResponse error (HttpResponseStatus rc) {
return new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, rc);
private FullHttpResponse error (HttpResponseStatus rc, HttpVersion version) {
return new DefaultFullHttpResponse(version, rc);
}

private FullHttpResponse getResponse (Context ctx) {
private FullHttpResponse getResponse (Context ctx, HttpVersion version) {
Object r = ctx.get(RESPONSE);
FullHttpResponse httpResponse;

Expand All @@ -111,7 +112,7 @@ private FullHttpResponse getResponse (Context ctx) {
isJson = true;
}
httpResponse = new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1,
version,
response.status(),
copiedBuffer(responseBody));

Expand All @@ -122,14 +123,14 @@ private FullHttpResponse getResponse (Context ctx) {
httpHeaders.add("Access-Control-Allow-Origin", corsHeader);
} catch (JsonProcessingException e) {
ctx.log(e);
httpResponse = error(HttpResponseStatus.INTERNAL_SERVER_ERROR);
httpResponse = error(HttpResponseStatus.INTERNAL_SERVER_ERROR, version);
}
} else {
Result result = ctx.getResult();
if (result.hasFailures()) {
httpResponse = error(HttpResponseStatus.valueOf(result.failure().getIrc().irc()));
httpResponse = error(HttpResponseStatus.valueOf(result.failure().getIrc().irc()), version);
} else
httpResponse = error(HttpResponseStatus.NOT_FOUND);
httpResponse = error(HttpResponseStatus.NOT_FOUND, version);
}
return httpResponse;
}
Expand Down
8 changes: 8 additions & 0 deletions modules/qrest/src/test/java/org/jpos/qrest/RestTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import org.apache.http.entity.ContentType;
import org.jpos.q2.Q2;
import org.jpos.util.NameRegistrar;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

Expand All @@ -42,6 +43,7 @@ public static void setUp() throws NameRegistrar.NotFoundException {
RestAssured.baseURI = BASE_URL;
RestAssured.useRelaxedHTTPSValidation();
RestAssured.requestSpecification = new RequestSpecBuilder().build().contentType(APPLICATION_JSON.toString());
System.setProperty("test.enabled", "true");
if (q2 == null) {
q2 = new Q2();
q2.start();
Expand All @@ -50,6 +52,12 @@ public static void setUp() throws NameRegistrar.NotFoundException {
}
}

@AfterAll
public static void tearDown() {
if (q2 != null)
q2.stop();
}

@Test
public void test404() {
given()
Expand Down