Repository: vert-x3/vertx-web
Issue: CORS handling fails with "Origin: null" header

### Version

Which version(s) did you encounter this bug ?

4.0.3

### Context

When issuing an HTTP request with the `Origin: null` header, the CorsHandler generates an HTTP 500 error (`CORS Rejected - Invalid origin`), even though it should allow the request to continue its normal processing.

This happens whenever one creates a CorsHandler with either https://vertx.io/docs/apidocs/io/vertx/ext/web/handler/CorsHandler.html#create-- (which should allow any origin), or with https://vertx.io/docs/apidocs/io/vertx/ext/web/handler/CorsHandler.html#create-java.lang.String- and passing the `"*"` pattern (which should do the same as `.create()` and allow any origin.)

The `Origin: null` header name and value is a valid origin, as per https://tools.ietf.org/html/rfc6454#section-7.1 ; the literal `null` is allowed; in that RFC it just happens to be phrased using its hex-encoded ascii values:

```
origin-list-or-null = %x6E %x75 %x6C %x6C / origin-list
```

This particular header value is sent automatically by browsers whenever they perform a cross-origin request, and the browser has to set an "opaque origin", like when the URL of the page is `file://` (see https://stackoverflow.com/questions/42239643/when-do-browsers-send-the-origin-header-when-do-browsers-set-the-origin-to-null )

### Do you have a reproducer?

Sort of; I noticed this issue in the https://github.com/yuzutech/kroki project; this project happens to have a publicly available instance which can demonstrate the issue; you can inspect how the CorsHandler is created, at https://github.com/yuzutech/kroki/blob/v0.12.1/server/src/main/java/io/kroki/server/Server.java#L92 

To reproduce, just issue what should be a valid HTTP request with `Origin: null`, and notice that the API reports back an HTTP 500 error:

* `curl https://kroki.io/graphviz/svg/eNpLyUwvSizIUHBXqPZIzcnJ17ULzy_KSanlAgB1EAjQ`, gives you HTTP 200, and a bunch of SVG, as expected.
* `curl -H 'Origin: http://localhost' https://kroki.io/graphviz/svg/eNpLyUwvSizIUHBXqPZIzcnJ17ULzy_KSanlAgB1EAjQ`, also gives you HTTP 200, and a bunch of SVG.
* `curl -H 'Origin: null' https://kroki.io/graphviz/svg/eNpLyUwvSizIUHBXqPZIzcnJ17ULzy_KSanlAgB1EAjQ` gives you an HTTP 500 with the error message `Error 500: CORS Rejected - Invalid origin` (which can be traced back to vertx-web).

For that case, the stacktrace is this (reproduced in local environment):

```
java.lang.IllegalStateException: CORS Rejected - Invalid origin
        at io.vertx.ext.web.handler.impl.CorsHandlerImpl.handle(CorsHandlerImpl.java:211)
        at io.vertx.ext.web.handler.impl.CorsHandlerImpl.handle(CorsHandlerImpl.java:41)                                                                                                                                                                                         
        at io.vertx.ext.web.impl.RouteState.handleContext(RouteState.java:1129)
        at io.vertx.ext.web.impl.RoutingContextImplBase.iterateNext(RoutingContextImplBase.java:151)
        at io.vertx.ext.web.impl.RoutingContextImpl.next(RoutingContextImpl.java:133)                                                                                                                                                                                            
        at io.vertx.ext.web.impl.RouterImpl.handle(RouterImpl.java:55)                                                                  
        at io.vertx.ext.web.impl.RouterImpl.handle(RouterImpl.java:37)                                                                                                                                                                                                           
        at io.vertx.core.http.impl.Http1xServerRequestHandler.handle(Http1xServerRequestHandler.java:67)                                
        at io.vertx.core.http.impl.Http1xServerRequestHandler.handle(Http1xServerRequestHandler.java:30)                                
        at io.vertx.core.impl.EventLoopContext.emit(EventLoopContext.java:52)                                                           
        at io.vertx.core.impl.DuplicatedContext.emit(DuplicatedContext.java:194)                                                        
        at io.vertx.core.http.impl.Http1xServerConnection.handleMessage(Http1xServerConnection.java:140)                                
        at io.vertx.core.net.impl.ConnectionBase.read(ConnectionBase.java:153)                                                          
        at io.vertx.core.net.impl.VertxHandler.channelRead(VertxHandler.java:154)                                                       
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)                     
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
        at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)                       
        at io.netty.channel.ChannelInboundHandlerAdapter.channelRead(ChannelInboundHandlerAdapter.java:93)
        at io.netty.handler.codec.http.websocketx.extensions.WebSocketServerExtensionHandler.channelRead(WebSocketServerExtensionHandler.java:102)
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)                     
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)                     
        at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
        at io.vertx.core.http.impl.Http1xUpgradeToH2CHandler.channelRead(Http1xUpgradeToH2CHandler.java:115)                            
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)                     
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)                     
        at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)                       
        at io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:324)                                   
        at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:296)
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)                     
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)                     
        at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)                       
        at io.vertx.core.http.impl.Http1xOrH2CHandler.end(Http1xOrH2CHandler.java:61)
        at io.vertx.core.http.impl.Http1xOrH2CHandler.channelRead(Http1xOrH2CHandler.java:38)
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
        at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
        at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410)
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
        at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919)
        at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:166)
        at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:719)
        at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:655)
        at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:581)
        at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:493)
        at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:989)
        at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
        at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
        at java.base/java.lang.Thread.run(Unknown Source)
```
```

Changed files:
vertx-web/src/test/java/io/vertx/ext/web/handler/CORSHandlerTest.java, vertx-web/src/test/java/io/vertx/ext/web/impl/OriginTest.java, vertx-web/src/main/java/io/vertx/ext/web/impl/Origin.java

diff --git a/vertx-web/src/main/java/io/vertx/ext/web/impl/Origin.java b/vertx-web/src/main/java/io/vertx/ext/web/impl/Origin.java
index f2aec90b5..5feada3f7 100644
--- a/vertx-web/src/main/java/io/vertx/ext/web/impl/Origin.java
+++ b/vertx-web/src/main/java/io/vertx/ext/web/impl/Origin.java
@@ -34,6 +34,7 @@ public final class Origin {
   private final String host;
   private final int port;
   private final String resource;
+  private final boolean isNull;
 
   // internal
   private final String base;
@@ -41,6 +42,21 @@ public final class Origin {
   private final String optional;
 
   private Origin(String protocol, String host, String port, String resource) {
+
+    if (protocol == null && host == null && port == null && resource == null) {
+      this.protocol = null;
+      this.host = null;
+      this.port = -1;
+      this.resource = null;
+      isNull = true;
+      this.base = null;
+      this.BASE = null;
+      this.optional = null;
+      return;
+    } else {
+      isNull = false;
+    }
+
     String defaultPort;
     switch (protocol.toLowerCase()) {
       case "ftp":
@@ -120,6 +136,12 @@ public final class Origin {
 
   public static Origin parse(String text) {
 
+    if (text.length() == 4) {
+      if ("null".equals(text)) {
+        return new Origin(null, null, null, null);
+      }
+    }
+
     int sep0 = text.indexOf("://");
 
     if (sep0 > 0) {
@@ -168,6 +190,12 @@ public final class Origin {
    * https://tools.ietf.org/html/rfc6454#section-7
    */
   public static boolean isValid(String text) {
+    if (text.length() == 4) {
+      if ("null".equals(text)) {
+        return true;
+      }
+    }
+
     int sep0 = text.indexOf("://");
 
     if (sep0 > 0) {
@@ -285,6 +313,11 @@ public final class Origin {
   }
 
   public boolean sameOrigin(String other) {
+
+    if (isNull) {
+      return "null".equals(other);
+    }
+
     // for each char of other
     // if any base chars != other abort
     // if more chars
@@ -332,6 +365,10 @@ public final class Origin {
   }
 
   public String encode() {
+    if (isNull) {
+      return "<null>";
+    }
+
     switch (protocol) {
       case "http":
         return protocol + "://" + host + (port == 80 ? "" : ":" + port);
@@ -346,6 +383,10 @@ public final class Origin {
 
   @Override
   public String toString() {
+    if (isNull) {
+      return "null";
+    }
+
     return base;
   }
 
@@ -353,6 +394,10 @@ public final class Origin {
    * An hyperlink representation of this origin. Like on web browsers.
    */
   public String href() {
+    if (isNull) {
+      return "null";
+    }
+
     return base + (resource == null ? "/" : resource);
   }
 }
diff --git a/vertx-web/src/test/java/io/vertx/ext/web/handler/CORSHandlerTest.java b/vertx-web/src/test/java/io/vertx/ext/web/handler/CORSHandlerTest.java
index a6daff696..96ee929c8 100644
--- a/vertx-web/src/test/java/io/vertx/ext/web/handler/CORSHandlerTest.java
+++ b/vertx-web/src/test/java/io/vertx/ext/web/handler/CORSHandlerTest.java
@@ -495,4 +495,12 @@ public class CORSHandlerTest extends WebTestBase {
     router.route().handler(context -> context.response().end());
     testRequest(HttpMethod.GET, "/", req -> req.headers().add("origin", "https://www.vertx.io"), resp -> checkHeaders(resp, "https://www.vertx.io", null, null, null, "true", null), 200, "OK", null);
   }
+
+  @Test
+  public void testAcceptNullOrigin() throws Exception {
+    router.route().handler(CorsHandler.create().addOrigin("*"));
+    router.route().handler(context -> context.response().end());
+    testRequest(HttpMethod.GET, "/", req -> req.headers().add("origin", "null"), resp -> checkHeaders(resp, "*", null, null, null), 200, "OK", null);
+  }
+
 }
diff --git a/vertx-web/src/test/java/io/vertx/ext/web/impl/OriginTest.java b/vertx-web/src/test/java/io/vertx/ext/web/impl/OriginTest.java
index 1f7929557..cb72dfb48 100644
--- a/vertx-web/src/test/java/io/vertx/ext/web/impl/OriginTest.java
+++ b/vertx-web/src/test/java/io/vertx/ext/web/impl/OriginTest.java
@@ -44,4 +44,11 @@ public class OriginTest {
       assertFalse(Origin.isValid(origin));
     }
   }
+
+  @Test
+  public void testNullOrigin() {
+    Origin.parse("null");
+    assertTrue(Origin.isValid("null"));
+  }
+
 }