diff --git a/core/core.services.yml b/core/core.services.yml
index 91a2c2b27e136d95fd9e600b858038e4915b29af..7abab67b453c03d4e12678e9262f967cc7a574e8 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -489,6 +489,9 @@ services:
     arguments: ['@router.route_provider', '@path_processor_manager', '@route_processor_manager', '@config.factory', '@settings', '@logger.channel.default', '@request_stack']
     calls:
       - [setContext, ['@?router.request_context']]
+  unrouted_url_assembler:
+    class: Drupal\Core\Utility\UnroutedUrlAssembler
+    arguments: ['@request_stack', '@config.factory' ]
   link_generator:
     class: Drupal\Core\Utility\LinkGenerator
     arguments: ['@url_generator', '@module_handler']
diff --git a/core/lib/Drupal/Core/Utility/UnroutedUrlAssembler.php b/core/lib/Drupal/Core/Utility/UnroutedUrlAssembler.php
new file mode 100644
index 0000000000000000000000000000000000000000..05fb19f224398a5201b77dd84bca03382923d8ce
--- /dev/null
+++ b/core/lib/Drupal/Core/Utility/UnroutedUrlAssembler.php
@@ -0,0 +1,152 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\Core\Utility\UnroutedUrlAssembler.
+ */
+
+namespace Drupal\Core\Utility;
+
+use Drupal\Component\Utility\UrlHelper;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Symfony\Component\HttpFoundation\RequestStack;
+
+/**
+ * Provides a way to build external or non Drupal local domain URLs.
+ *
+ * It takes into account configured safe HTTP protocols.
+ */
+class UnroutedUrlAssembler implements UnroutedUrlAssemblerInterface {
+
+  /**
+   * A request stack object.
+   *
+   * @var \Symfony\Component\HttpFoundation\RequestStack
+   */
+  protected $requestStack;
+
+  /**
+   *  Constructs a new unroutedUrlAssembler object.
+   *
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config
+   *    The config factory.
+   * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
+   *   A request stack object.
+   */
+  public function __construct(RequestStack $request_stack, ConfigFactoryInterface $config) {
+    $allowed_protocols = $config->get('system.filter')->get('protocols') ?: ['http', 'https'];
+    UrlHelper::setAllowedProtocols($allowed_protocols);
+    $this->requestStack = $request_stack;
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * This is a helper function that calls buildExternalUrl() or buildLocalUrl()
+   * based on a check of whether the path is a valid external URL.
+   */
+  public function assemble($uri, array $options = []) {
+    // Note that UrlHelper::isExternal will return FALSE if the $uri has a
+    // disallowed protocol.  This is later made safe since we always add at
+    // least a leading slash.
+    if (strpos($uri, 'base://') === 0) {
+      return $this->buildLocalUrl($uri, $options);
+    }
+    elseif (UrlHelper::isExternal($uri)) {
+      // UrlHelper::isExternal() only returns true for safe protocols.
+      return $this->buildExternalUrl($uri, $options);
+    }
+    throw new \InvalidArgumentException('You must use a valid URI scheme.  Use base:// for a path e.g. to a Drupal file that needs the base path.');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function buildExternalUrl($uri, array $options = []) {
+    $this->addOptionDefaults($options);
+    // Split off the fragment.
+    if (strpos($uri, '#') !== FALSE) {
+      list($uri, $old_fragment) = explode('#', $uri, 2);
+      // If $options contains no fragment, take it over from the path.
+      if (isset($old_fragment) && !$options['fragment']) {
+        $options['fragment'] = '#' . $old_fragment;
+      }
+    }
+
+    if (isset($options['https'])) {
+      if ($options['https'] === TRUE) {
+        $uri = str_replace('http://', 'https://', $uri);
+      }
+      elseif ($options['https'] === FALSE) {
+        $uri = str_replace('https://', 'http://', $uri);
+      }
+    }
+    // Append the query.
+    if ($options['query']) {
+      $uri .= (strpos($uri, '?') !== FALSE ? '&' : '?') . UrlHelper::buildQuery($options['query']);
+    }
+    // Reassemble.
+    return $uri . $options['fragment'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function buildLocalUrl($uri, array $options = []) {
+    $this->addOptionDefaults($options);
+    $request = $this->requestStack->getCurrentRequest();
+
+    // Remove the base:// scheme.
+    $uri = substr($uri, 7);
+    // Add any subdirectory where Drupal is installed.
+    $current_base_path = $request->getBasePath() . '/';
+
+    if ($options['absolute']) {
+      $current_base_url = $request->getSchemeAndHttpHost() . $current_base_path;
+      if (isset($options['https'])) {
+        if (!empty($options['https'])) {
+          $base = str_replace('http://', 'https://', $current_base_url);
+          $options['absolute'] = TRUE;
+        }
+        else {
+          $base = str_replace('https://', 'http://', $current_base_url);
+          $options['absolute'] = TRUE;
+        }
+      }
+      else {
+        $base = $current_base_url;
+      }
+    }
+    else {
+      $base = $current_base_path;
+    }
+
+    $prefix = empty($uri) ? rtrim($options['prefix'], '/') : $options['prefix'];
+
+    $uri = str_replace('%2F', '/', rawurlencode($prefix . $uri));
+    $query = $options['query'] ? ('?' . UrlHelper::buildQuery($options['query'])) : '';
+    return $base . $options['script'] . $uri . $query . $options['fragment'];
+  }
+
+  /**
+   * Merges in default defaults
+   *
+   * @param array $options
+   *   The options to merge in the defaults.
+   */
+  protected function addOptionDefaults(array &$options) {
+    // Merge in defaults.
+    $options += [
+      'fragment' => '',
+      'query' => [],
+      'absolute' => FALSE,
+      'prefix' => '',
+      'script' => '',
+    ];
+
+    if (isset($options['fragment']) && $options['fragment'] !== '') {
+      $options['fragment'] = '#' . $options['fragment'];
+    }
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Utility/UnroutedUrlAssemblerInterface.php b/core/lib/Drupal/Core/Utility/UnroutedUrlAssemblerInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..cba87febdbe9c1b91d84ef41fd66538d1131e9e7
--- /dev/null
+++ b/core/lib/Drupal/Core/Utility/UnroutedUrlAssemblerInterface.php
@@ -0,0 +1,54 @@
+<?php
+/**
+ * @file
+ * Contains Drupal\Core\Utility\UnroutedUrlAssemblerInterface.
+ */
+
+namespace Drupal\Core\Utility;
+
+/**
+ * Provides a way to build external or non Drupal local domain URLs.
+ */
+interface UnroutedUrlAssemblerInterface {
+
+  /**
+   * Builds a domain-local or external URL from a path or URL.
+   *
+   * For actual implementations the logic probably has to be split up between
+   * domain-local and external URLs.
+   *
+   * @param string $uri
+   *   A path on the same domain or external URL being linked to, such as "foo"
+   *    or "http://example.com/foo".
+   *   - If you provide a full URL, it will be considered an external URL as
+   *     long as it has an allowed protocol.
+   *   - If you provide only a path (e.g. "foo"), it will be
+   *     considered a URL local to the same domain. Additional query
+   *     arguments for local paths must be supplied in $options['query'], not
+   *     included in $path.
+   *   - If your external URL contains a query (e.g. http://example.com/foo?a=b),
+   *     then you can either URL encode the query keys and values yourself and
+   *     include them in $path, or use $options['query'] to let this method
+   *     URL encode them.
+   *
+   * @param array $options
+   *   (optional) An associative array of additional options, with the following
+   *   elements:
+   *   - 'query': An array of query key/value-pairs (without any URL-encoding) to
+   *     append to the URL.
+   *   - 'fragment': A fragment identifier (named anchor) to append to the URL.
+   *     Do not include the leading '#' character.
+   *   - 'absolute': Defaults to FALSE. Whether to force the output to be an
+   *     absolute link (beginning with http:). Useful for links that will be
+   *     displayed outside the site, such as in an RSS feed.
+   *   - 'https': Whether this URL should point to a secure location. If not
+   *     defined, the current scheme is used, so the user stays on HTTP or HTTPS
+   *     respectively. TRUE enforces HTTPS and FALSE enforces HTTP, but HTTPS can
+   *     only be enforced when the variable 'https' is set to TRUE.
+   *
+   * @return
+   *   A string containing a relative or absolute URL.
+   */
+  public function assemble($uri, array $options = array());
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Utility/UnroutedUrlAssemblerTest.php b/core/tests/Drupal/Tests/Core/Utility/UnroutedUrlAssemblerTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..517625f8e8c1c3e1ccc9327e06c3dd9bcd824532
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Utility/UnroutedUrlAssemblerTest.php
@@ -0,0 +1,129 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Utility\UnroutedUrlAssemblerTest.
+ */
+
+namespace Drupal\Tests\Core\Utility;
+
+use Drupal\Core\Utility\UnroutedUrlAssembler;
+use Drupal\Tests\UnitTestCase;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\RequestStack;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Utility\UnroutedUrlAssembler
+ * @group Utility
+ */
+class UnroutedUrlAssemblerTest extends UnitTestCase {
+
+  /**
+   * The request stack.
+   *
+   * @var \Symfony\Component\HttpFoundation\RequestStack
+   */
+  protected $requestStack;
+
+  /**
+   * The mocked config factory.
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $configFactory;
+
+  /**
+   * The tested unrouted url assembler.
+   *
+   * @var \Drupal\Core\Utility\UnroutedUrlAssembler
+   */
+  protected $unroutedUrlAssembler;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->requestStack = new RequestStack();
+    $this->configFactory = $this->getConfigFactoryStub(['system.filter' => []]);
+    $this->unroutedUrlAssembler = new UnroutedUrlAssembler($this->requestStack, $this->configFactory);
+  }
+
+  /**
+   * @covers ::assemble
+   * @expectedException \InvalidArgumentException
+   */
+  public function testAssembleWithNeitherExternalNorDomainLocalUri() {
+    $this->unroutedUrlAssembler->assemble('wrong-url');
+  }
+
+  /**
+   * @covers ::assemble
+   * @covers ::buildExternalUrl
+   *
+   * @dataProvider providerTestAssembleWithExternalUrl
+   */
+  public function testAssembleWithExternalUrl($uri, array $options, $expected) {
+   $this->assertEquals($expected, $this->unroutedUrlAssembler->assemble($uri, $options));
+  }
+
+  /**
+   * Provides test data for testAssembleWithExternalUrl
+   */
+  public function providerTestAssembleWithExternalUrl() {
+    return [
+      ['http://example.com/test', [], 'http://example.com/test'],
+      ['http://example.com/test', ['fragment' => 'example'], 'http://example.com/test#example'],
+      ['http://example.com/test', ['fragment' => 'example'], 'http://example.com/test#example'],
+      ['http://example.com/test', ['query' => ['foo' => 'bar']], 'http://example.com/test?foo=bar'],
+      ['http://example.com/test', ['https' => TRUE], 'https://example.com/test'],
+      ['https://example.com/test', ['https' => FALSE], 'http://example.com/test'],
+      ['https://example.com/test?foo=1#bar', [], 'https://example.com/test?foo=1#bar'],
+    ];
+  }
+
+  /**
+   * @covers ::assemble
+   * @covers::buildLocalUrl
+   *
+   * @dataProvider providerTestAssembleWithLocalUri
+   */
+  public function testAssembleWithLocalUri($uri, array $options, $subdir, $expected) {
+    $server = [];
+    if ($subdir) {
+      // Setup a fake request which looks like a Drupal installed under the
+      // subdir "subdir" on the domain www.example.com.
+      // To reproduce the values install Drupal like that and use a debugger.
+      $server = [
+        'SCRIPT_NAME' => '/subdir/index.php',
+        'SCRIPT_FILENAME' => DRUPAL_ROOT . '/index.php',
+        'SERVER_NAME' => 'http://www.example.com',
+      ];
+      $request = Request::create('/subdir');
+    }
+    else {
+      $request = Request::create('/');
+    }
+    $request->server->add($server);
+    $this->requestStack->push($request);
+
+    $this->assertEquals($expected, $this->unroutedUrlAssembler->assemble($uri, $options));
+  }
+
+  /**
+   * @return array
+   */
+  public function providerTestAssembleWithLocalUri() {
+    return [
+      ['base://example', [], FALSE, '/example'],
+      ['base://example', ['query' => ['foo' => 'bar']], FALSE, '/example?foo=bar'],
+      ['base://example', ['fragment' => 'example', ], FALSE, '/example#example'],
+      ['base://example', [], TRUE, '/subdir/example'],
+      ['base://example', ['query' => ['foo' => 'bar']], TRUE, '/subdir/example?foo=bar'],
+      ['base://example', ['fragment' => 'example', ], TRUE, '/subdir/example#example'],
+    ];
+  }
+
+}
+