diff --git a/composer.json b/composer.json
index 9bc716f7a5d2cf3a197872de840853a4fbcfd4c4..fa55dde35f446ab8160bb4282a47111376a10a15 100644
--- a/composer.json
+++ b/composer.json
@@ -41,7 +41,8 @@
 		"symfony/finder": "2.6.9",
 		"doctrine/instantiator": "1.0.4",
 		"helhum/class-alias-loader": "1.1.9",
-		"typo3/cms-composer-installers": "1.2.1"
+		"typo3/cms-composer-installers": "1.2.1",
+		"psr/http-message": "1.0"
 	},
 	"require-dev": {
 		"mikey179/vfsStream": "1.4.*@dev",
diff --git a/typo3/index.php b/typo3/index.php
index 387119829f1914b67e5d1ae8f97e98e713f95070..69514c7b1a674b9f06f10293476740378d390d65 100644
--- a/typo3/index.php
+++ b/typo3/index.php
@@ -19,9 +19,5 @@
  */
 call_user_func(function() {
 	$classLoader = require __DIR__ . '/contrib/vendor/autoload.php';
-	(new \TYPO3\CMS\Backend\Http\Application($classLoader))->run(function() {
-		// currently implemented as a closure as there is no Request/Response implementation or routing in the backend
-		$loginController = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Controller\LoginController::class);
-		$loginController->main();
-	});
+	(new \TYPO3\CMS\Backend\Http\Application($classLoader))->run();
 });
diff --git a/typo3/sysext/backend/Classes/Console/CliRequestHandler.php b/typo3/sysext/backend/Classes/Console/CliRequestHandler.php
index 62d4a39a3879a70cc8d40fcde80257e4d1b2e60b..3e9c24f3f2606f303cb7bea8c79bea86227af732 100644
--- a/typo3/sysext/backend/Classes/Console/CliRequestHandler.php
+++ b/typo3/sysext/backend/Classes/Console/CliRequestHandler.php
@@ -14,6 +14,7 @@ namespace TYPO3\CMS\Backend\Console;
  * The TYPO3 project - inspiring people to share!
  */
 
+use Psr\Http\Message\ServerRequestInterface;
 use TYPO3\CMS\Core\Core\Bootstrap;
 use TYPO3\CMS\Core\Core\RequestHandlerInterface;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
@@ -44,9 +45,10 @@ class CliRequestHandler implements RequestHandlerInterface {
 	/**
 	 * Handles any commandline request
 	 *
+	 * @param ServerRequestInterface $request
 	 * @return void
 	 */
-	public function handleRequest() {
+	public function handleRequest(ServerRequestInterface $request) {
 		$commandLineKey = $this->getCommandLineKeyOrDie();
 		$commandLineScript = $this->getIncludeScriptByCommandLineKey($commandLineKey);
 
@@ -137,9 +139,10 @@ class CliRequestHandler implements RequestHandlerInterface {
 	/**
 	 * This request handler can handle any CLI request .
 	 *
+	 * @param ServerRequestInterface $request
 	 * @return bool If the request is a CLI request, TRUE otherwise FALSE
 	 */
-	public function canHandleRequest() {
+	public function canHandleRequest(ServerRequestInterface $request) {
 		return defined('TYPO3_cliMode') && (TYPO3_REQUESTTYPE & TYPO3_REQUESTTYPE_BE) && (TYPO3_REQUESTTYPE & TYPO3_REQUESTTYPE_CLI);
 	}
 
diff --git a/typo3/sysext/backend/Classes/Controller/LoginController.php b/typo3/sysext/backend/Classes/Controller/LoginController.php
index 460ab45936dd0795d87d4dcad8bfafcfdc3f928f..ee522e9a21d42fa7a5f24d46bfcee3a31529bf50 100644
--- a/typo3/sysext/backend/Classes/Controller/LoginController.php
+++ b/typo3/sysext/backend/Classes/Controller/LoginController.php
@@ -31,7 +31,7 @@ use TYPO3\CMS\Fluid\View\StandaloneView;
  * @author Kasper Skårhøj <kasperYYYY@typo3.com>
  * @author Frank Nägler <typo3@naegler.net>
  */
-class LoginController {
+class LoginController implements \TYPO3\CMS\Core\Http\ControllerInterface {
 
 	/**
 	 * The URL to redirect to after login.
@@ -121,11 +121,27 @@ class LoginController {
 		$this->view = $this->getFluidTemplateObject();
 	}
 
+	/**
+	 * Injects the request object for the current request or subrequest
+	 * As this controller goes only through the main() method, it is rather simple for now
+	 * This will be split up in an abstract controller once proper routing/dispatcher is in place.
+	 *
+	 * @param \Psr\Http\Message\RequestInterface $request
+	 * @return \Psr\Http\Message\ResponseInterface $response
+	 */
+	public function processRequest(\Psr\Http\Message\RequestInterface $request) {
+		$content = $this->main();
+		/** @var \TYPO3\CMS\Core\Http\Response $response */
+		$response = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Http\Response::class);
+		$response->getBody()->write($content);
+		return $response;
+	}
+
 	/**
 	 * Main function - creating the login/logout form
 	 *
 	 * @throws Exception
-	 * @return void
+	 * @return string The content to output
 	 */
 	public function main() {
 		/** @var $pageRenderer \TYPO3\CMS\Core\Page\PageRenderer */
@@ -221,7 +237,7 @@ class LoginController {
 		$content .= $this->view->render();
 		$content .= $this->getDocumentTemplate()->endPage();
 
-		echo $content;
+		return $content;
 	}
 
 	/**
diff --git a/typo3/sysext/backend/Classes/Http/AjaxRequestHandler.php b/typo3/sysext/backend/Classes/Http/AjaxRequestHandler.php
index ac11642e187faf627de227a924f4de9ea14e5eeb..306d7d2d501f5fae9bf5e63b14f60812d6ef5535 100644
--- a/typo3/sysext/backend/Classes/Http/AjaxRequestHandler.php
+++ b/typo3/sysext/backend/Classes/Http/AjaxRequestHandler.php
@@ -17,6 +17,7 @@ namespace TYPO3\CMS\Backend\Http;
 use TYPO3\CMS\Core\Core\Bootstrap;
 use TYPO3\CMS\Core\Core\RequestHandlerInterface;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use Psr\Http\Message\ServerRequestInterface;
 
 /**
  * Base class for all AJAX-related calls for the TYPO3 Backend run through typo3/ajax.php.
@@ -49,7 +50,7 @@ class AjaxRequestHandler implements RequestHandlerInterface {
 	);
 
 	/**
-	 * Constructor handing over the bootstrap
+	 * Constructor handing over the bootstrap and the original request
 	 *
 	 * @param Bootstrap $bootstrap
 	 */
@@ -60,15 +61,14 @@ class AjaxRequestHandler implements RequestHandlerInterface {
 	/**
 	 * Handles any AJAX request in the TYPO3 Backend
 	 *
-	 * @return void
+	 * @param ServerRequestInterface $request
+	 * @return NULL|\Psr\Http\Message\ResponseInterface
 	 */
-	public function handleRequest() {
+	public function handleRequest(ServerRequestInterface $request) {
 		// First get the ajaxID
-		$ajaxID = isset($_POST['ajaxID']) ? $_POST['ajaxID'] : $_GET['ajaxID'];
-		if (isset($ajaxID)) {
-			$ajaxID = (string)stripslashes($ajaxID);
-		}
+		$ajaxID = isset($request->getParsedBody()['ajaxID']) ? $request->getParsedBody()['ajaxID'] : $request->getQueryParams()['ajaxID'];
 
+		// used for backwards-compatibility
 		$GLOBALS['ajaxID'] = $ajaxID;
 		$this->boot($ajaxID);
 
@@ -94,7 +94,8 @@ class AjaxRequestHandler implements RequestHandlerInterface {
 			$success = TRUE;
 			$tokenIsValid = TRUE;
 			if ($csrfTokenCheck) {
-				$tokenIsValid = \TYPO3\CMS\Core\FormProtection\FormProtectionFactory::get()->validateToken(GeneralUtility::_GP('ajaxToken'), 'ajaxCall', $ajaxID);
+				$ajaxToken = $request->getParsedBody()['ajaxToken'] ?: $request->getQueryParams()['ajaxToken'];
+				$tokenIsValid = \TYPO3\CMS\Core\FormProtection\FormProtectionFactory::get()->validateToken($ajaxToken, 'ajaxCall', $ajaxID);
 			}
 			if ($tokenIsValid) {
 				// Cleanup global variable space
@@ -110,14 +111,17 @@ class AjaxRequestHandler implements RequestHandlerInterface {
 
 		// Outputting the content (and setting the X-JSON-Header)
 		$ajaxObj->render();
+
+		return NULL;
 	}
 
 	/**
 	 * This request handler can handle any backend request coming from ajax.php
 	 *
+	 * @param ServerRequestInterface $request
 	 * @return bool If the request is an AJAX backend request, TRUE otherwise FALSE
 	 */
-	public function canHandleRequest() {
+	public function canHandleRequest(ServerRequestInterface $request) {
 		return TYPO3_REQUESTTYPE & TYPO3_REQUESTTYPE_AJAX;
 	}
 
diff --git a/typo3/sysext/backend/Classes/Http/BackendModuleRequestHandler.php b/typo3/sysext/backend/Classes/Http/BackendModuleRequestHandler.php
index c06b092d094d9977949cc54856857be2fb9aec8b..e31d852aad95b6697a2fb4e80289ff14a42e6d27 100644
--- a/typo3/sysext/backend/Classes/Http/BackendModuleRequestHandler.php
+++ b/typo3/sysext/backend/Classes/Http/BackendModuleRequestHandler.php
@@ -20,6 +20,7 @@ use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
 use TYPO3\CMS\Core\Exception;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Extbase\Object\ObjectManager;
+use Psr\Http\Message\ServerRequestInterface;
 
 /**
  * Handles the request for backend modules and wizards
@@ -42,7 +43,15 @@ class BackendModuleRequestHandler implements \TYPO3\CMS\Core\Core\RequestHandler
 	protected $backendUserAuthentication;
 
 	/**
-	 * @param Bootstrap $bootstrap The TYPO3 core bootstrap
+	 * Instance of the current Http Request
+	 * @var ServerRequestInterface
+	 */
+	protected $request;
+
+	/**
+	 * Constructor handing over the bootstrap and the original request
+	 *
+	 * @param Bootstrap $bootstrap
 	 */
 	public function __construct(Bootstrap $bootstrap) {
 		$this->bootstrap = $bootstrap;
@@ -51,9 +60,12 @@ class BackendModuleRequestHandler implements \TYPO3\CMS\Core\Core\RequestHandler
 	/**
 	 * Handles the request, evaluating the configuration and executes the module accordingly
 	 *
+	 * @param ServerRequestInterface $request
+	 * @return NULL|\Psr\Http\Message\ResponseInterface
 	 * @throws Exception
 	 */
-	public function handleRequest() {
+	public function handleRequest(ServerRequestInterface $request) {
+		$this->request = $request;
 		$this->boot();
 
 		$this->moduleRegistry = $GLOBALS['TBE_MODULES'];
@@ -67,7 +79,7 @@ class BackendModuleRequestHandler implements \TYPO3\CMS\Core\Core\RequestHandler
 
 		$this->backendUserAuthentication = $GLOBALS['BE_USER'];
 
-		$moduleName = (string)GeneralUtility::_GET('M');
+		$moduleName = (string)$this->request->getQueryParams()['M'];
 		if ($this->isDispatchedModule($moduleName)) {
 			$isDispatched = $this->dispatchModule($moduleName);
 		} else {
@@ -107,10 +119,11 @@ class BackendModuleRequestHandler implements \TYPO3\CMS\Core\Core\RequestHandler
 	/**
 	 * This request handler can handle any backend request coming from mod.php
 	 *
+	 * @param ServerRequestInterface $request
 	 * @return bool
 	 */
-	public function canHandleRequest() {
-		return (TYPO3_REQUESTTYPE & TYPO3_REQUESTTYPE_BE) && !empty((string)GeneralUtility::_GET('M'));
+	public function canHandleRequest(ServerRequestInterface $request) {
+		return (TYPO3_REQUESTTYPE & TYPO3_REQUESTTYPE_BE) && !empty((string)$request->getQueryParams()['M']);
 	}
 
 	/**
@@ -119,7 +132,7 @@ class BackendModuleRequestHandler implements \TYPO3\CMS\Core\Core\RequestHandler
 	 * @return bool
 	 */
 	protected function isValidModuleRequest() {
-		return $this->getFormProtection()->validateToken((string)GeneralUtility::_GP('moduleToken'), 'moduleCall', (string)GeneralUtility::_GET('M'));
+		return $this->getFormProtection()->validateToken((string)$this->request->getQueryParams()['moduleToken'], 'moduleCall', (string)$this->request->getQueryParams()['M']);
 	}
 
 	/**
diff --git a/typo3/sysext/backend/Classes/Http/RequestHandler.php b/typo3/sysext/backend/Classes/Http/RequestHandler.php
index bf4ed837cbd0d01ee8ca8148c525083cf23ffe21..489616e4d63e1bc7735a27ffbf05855319a6b411 100644
--- a/typo3/sysext/backend/Classes/Http/RequestHandler.php
+++ b/typo3/sysext/backend/Classes/Http/RequestHandler.php
@@ -16,6 +16,7 @@ namespace TYPO3\CMS\Backend\Http;
 
 use TYPO3\CMS\Core\Core\Bootstrap;
 use TYPO3\CMS\Core\Core\RequestHandlerInterface;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
 
 /**
  * General RequestHandler for the TYPO3 Backend. This is used for all Backend requests except for CLI
@@ -33,7 +34,7 @@ class RequestHandler implements RequestHandlerInterface {
 	protected $bootstrap;
 
 	/**
-	 * Constructor handing over the bootstrap
+	 * Constructor handing over the bootstrap and the original request
 	 *
 	 * @param Bootstrap $bootstrap
 	 */
@@ -44,9 +45,14 @@ class RequestHandler implements RequestHandlerInterface {
 	/**
 	 * Handles any backend request
 	 *
-	 * @return void
+	 * @param \Psr\Http\Message\ServerRequestInterface $request
+	 * @return NULL|\Psr\Http\Message\ResponseInterface
 	 */
-	public function handleRequest() {
+	public function handleRequest(\Psr\Http\Message\ServerRequestInterface $request) {
+		// enable dispatching via Request/Response logic only for typo3/index.php currently
+		$path = substr($request->getUri()->getPath(), strlen(GeneralUtility::getIndpEnv('TYPO3_SITE_PATH')));
+		$routingEnabled = ($path === TYPO3_mainDir . 'index.php' || $path === TYPO3_mainDir);
+
 		// Evaluate the constant for skipping the BE user check for the bootstrap
 		if (defined('TYPO3_PROCEED_IF_NO_USER') && TYPO3_PROCEED_IF_NO_USER) {
 			$proceedIfNoUserIsLoggedIn = TRUE;
@@ -68,14 +74,20 @@ class RequestHandler implements RequestHandlerInterface {
 			->endOutputBufferingAndCleanPreviousOutput()
 			->initializeOutputCompression()
 			->sendHttpHeaders();
+
+		if ($routingEnabled) {
+			return $this->dispatch($request);
+		}
+		return NULL;
 	}
 
 	/**
 	 * This request handler can handle any backend request (but not CLI).
 	 *
+	 * @param \Psr\Http\Message\ServerRequestInterface $request
 	 * @return bool If the request is not a CLI script, TRUE otherwise FALSE
 	 */
-	public function canHandleRequest() {
+	public function canHandleRequest(\Psr\Http\Message\ServerRequestInterface $request) {
 		return (TYPO3_REQUESTTYPE & TYPO3_REQUESTTYPE_BE && !(TYPO3_REQUESTTYPE & TYPO3_REQUESTTYPE_CLI));
 	}
 
@@ -88,4 +100,19 @@ class RequestHandler implements RequestHandlerInterface {
 	public function getPriority() {
 		return 50;
 	}
+
+	/**
+	 * Dispatch the request to the appropriate controller, will go to a proper dispatcher/router class in the future
+	 *
+	 * @internal
+	 * @param \Psr\Http\Message\RequestInterface $request
+	 * @return NULL|\Psr\Http\Message\ResponseInterface
+	 */
+	protected function dispatch($request) {
+		$controller = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Controller\LoginController::class);
+		if ($controller instanceof \TYPO3\CMS\Core\Http\ControllerInterface) {
+			return $controller->processRequest($request);
+		}
+		return NULL;
+	}
 }
diff --git a/typo3/sysext/backend/Tests/Unit/BackendModuleRequestHandlerTest.php b/typo3/sysext/backend/Tests/Unit/Http/BackendModuleRequestHandlerTest.php
similarity index 74%
rename from typo3/sysext/backend/Tests/Unit/BackendModuleRequestHandlerTest.php
rename to typo3/sysext/backend/Tests/Unit/Http/BackendModuleRequestHandlerTest.php
index 654ef29c088fa7ed197ff1a623eb836c989e0f11..9c7d054411887a1eaf1f85c55c198156f485947a 100644
--- a/typo3/sysext/backend/Tests/Unit/BackendModuleRequestHandlerTest.php
+++ b/typo3/sysext/backend/Tests/Unit/Http/BackendModuleRequestHandlerTest.php
@@ -1,5 +1,5 @@
 <?php
-namespace TYPO3\CMS\Backend\Tests\Unit;
+namespace TYPO3\CMS\Backend\Tests\Unit\Http;
 
 /*
  * This file is part of the TYPO3 CMS project.
@@ -35,9 +35,15 @@ class BackendModuleRequestHandlerTest extends UnitTestCase {
 	 */
 	protected $formProtectionMock;
 
+	/**
+	 * @var \TYPO3\CMS\Core\Http\ServerRequest|PHPUnit_Framework_MockObject_MockObject
+	 */
+	protected $requestMock;
+
 	public function setUp() {
+		$this->requestMock = $this->getAccessibleMock(\TYPO3\CMS\Core\Http\ServerRequest::class, array(), array(), '', FALSE);
 		$this->formProtectionMock = $this->getMockForAbstractClass(AbstractFormProtection::class, array(), '', TRUE, TRUE, TRUE, array('validateToken'));
-		$this->subject = $this->getAccessibleMock(BackendModuleRequestHandler::class, array('boot', 'getFormProtection'), array(), '', FALSE);
+		$this->subject = $this->getAccessibleMock(BackendModuleRequestHandler::class, array('boot', 'getFormProtection'), array(\TYPO3\CMS\Core\Core\Bootstrap::getInstance()), '', TRUE);
 	}
 
 	/**
@@ -48,16 +54,16 @@ class BackendModuleRequestHandlerTest extends UnitTestCase {
 	public function moduleIndexIsCalled() {
 		$GLOBALS['TBE_MODULES'] = array(
 			'_PATHS' => array(
-				'module_fixture' => __DIR__ . '/Fixtures/ModuleFixture/'
+				'module_fixture' => __DIR__ . '/../Fixtures/ModuleFixture/'
 			)
 		);
-		$_GET['M'] = 'module_fixture';
 
+		$this->requestMock->expects($this->any())->method('getQueryParams')->will($this->returnValue(array('M' => 'module_fixture')));
 		$this->formProtectionMock->expects($this->once())->method('validateToken')->will($this->returnValue(TRUE));
 		$this->subject->expects($this->once())->method('boot');
 		$this->subject->expects($this->once())->method('getFormProtection')->will($this->returnValue($this->formProtectionMock));
 
-		$this->subject->handleRequest();
+		$this->subject->handleRequest($this->requestMock);
 	}
 
 	/**
@@ -70,7 +76,7 @@ class BackendModuleRequestHandlerTest extends UnitTestCase {
 		$this->subject->expects($this->once())->method('boot');
 		$this->subject->expects($this->once())->method('getFormProtection')->will($this->returnValue($this->formProtectionMock));
 
-		$this->subject->handleRequest();
+		$this->subject->handleRequest($this->requestMock);
 	}
 
 	/**
@@ -82,16 +88,15 @@ class BackendModuleRequestHandlerTest extends UnitTestCase {
 		$GLOBALS['TBE_MODULES'] = array(
 			'_PATHS' => array(
 				'_dispatcher' => array(),
-				'module_fixture' => __DIR__ . '/Fixtures/ModuleFixture/'
+				'module_fixture' => __DIR__ . '/../Fixtures/ModuleFixture/'
 			)
 		);
-		$_GET['M'] = 'module_fixture';
-
+		$this->requestMock->expects($this->any())->method('getQueryParams')->will($this->returnValue(array('M' => 'module_fixture')));
 		$this->formProtectionMock->expects($this->once())->method('validateToken')->will($this->returnValue(TRUE));
 		$this->subject->expects($this->once())->method('boot');
 		$this->subject->expects($this->once())->method('getFormProtection')->will($this->returnValue($this->formProtectionMock));
 
-		$this->subject->handleRequest();
+		$this->subject->handleRequest($this->requestMock);
 	}
 
 }
diff --git a/typo3/sysext/core/Classes/Core/Bootstrap.php b/typo3/sysext/core/Classes/Core/Bootstrap.php
index 0e4ec6d6413ea917ef6d28ecc26098f50a04a630..43c98e482e6f95e74d74fec22eec0afbf2b60212 100644
--- a/typo3/sysext/core/Classes/Core/Bootstrap.php
+++ b/typo3/sysext/core/Classes/Core/Bootstrap.php
@@ -73,11 +73,18 @@ class Bootstrap {
 	protected $activeErrorHandlerClassName;
 
 	/**
-	 * registered request handlers
+	 * A list of all registered request handlers, see the Application class / entry points for the registration
 	 * @var RequestHandlerInterface[]
 	 */
 	protected $availableRequestHandlers = array();
 
+	/**
+	 * The Response object when using Request/Response logic
+	 * @var \Psr\Http\Message\ResponseInterface
+	 * @see shutdown()
+	 */
+	protected $response;
+
 	/**
 	 * @var bool
 	 */
@@ -178,19 +185,6 @@ class Bootstrap {
 		return $this;
 	}
 
-	/**
-	 * Resolve the request handler that were registered based on the application
-	 * and execute the request
-	 *
-	 * @return Bootstrap
-	 * @throws \TYPO3\CMS\Core\Exception
-	 */
-	public function handleRequest() {
-		$requestHandler = $this->resolveRequestHandler();
-		$requestHandler->handleRequest();
-		return $this;
-	}
-
 	/**
 	 * Run the base setup that checks server environment, determines pathes,
 	 * populates base files and sets common configuration.
@@ -265,15 +259,16 @@ class Bootstrap {
 	 * Be sure to always have the constants that are defined in $this->defineTypo3RequestTypes() are set,
 	 * so most RequestHandlers can check if they can handle the request.
 	 *
+	 * @param \Psr\Http\Message\ServerRequestInterface $request
 	 * @return RequestHandlerInterface
 	 * @throws \TYPO3\CMS\Core\Exception
 	 * @internal This is not a public API method, do not use in own extensions
 	 */
-	public function resolveRequestHandler() {
+	protected function resolveRequestHandler(\Psr\Http\Message\ServerRequestInterface $request) {
 		$suitableRequestHandlers = array();
 		foreach ($this->availableRequestHandlers as $requestHandlerClassName) {
 			$requestHandler = GeneralUtility::makeInstance($requestHandlerClassName, $this);
-			if ($requestHandler->canHandleRequest()) {
+			if ($requestHandler->canHandleRequest($request)) {
 				$priority = $requestHandler->getPriority();
 				if (isset($suitableRequestHandlers[$priority])) {
 					throw new \TYPO3\CMS\Core\Exception('More than one request handler with the same priority can handle the request, but only one handler may be active at a time!', 1176471352);
@@ -288,6 +283,42 @@ class Bootstrap {
 		return array_pop($suitableRequestHandlers);
 	}
 
+	/**
+	 * Builds a Request instance from the current process, and then resolves the request
+	 * through the request handlers depending on Frontend, Backend, CLI etc.
+	 *
+	 * @return Bootstrap
+	 * @throws \TYPO3\CMS\Core\Exception
+	 */
+	protected function handleRequest() {
+		// Build the Request object
+		$request = \TYPO3\CMS\Core\Http\ServerRequestFactory::fromGlobals();
+
+		// Resolve request handler that were registered based on the Application
+		$requestHandler = $this->resolveRequestHandler($request);
+
+		// Execute the command which returns a Response object or NULL
+		$this->response = $requestHandler->handleRequest($request);
+		return $this;
+	}
+
+	/**
+	 * Outputs content if there is a proper Response object.
+	 *
+	 * @return Bootstrap
+	 */
+	protected function sendResponse() {
+		if ($this->response instanceof \Psr\Http\Message\ResponseInterface) {
+			if (!headers_sent()) {
+				foreach ($this->response->getHeaders() as $name => $values) {
+					header($name . ': ' . implode(', ', $values), FALSE);
+				}
+			}
+			echo $this->response->getBody()->__toString();
+		}
+		return $this;
+	}
+
 	/**
 	 * Registers the instance of the specified object for an early boot stage.
 	 * On finalizing the Object Manager initialization, all those instances will
@@ -1140,6 +1171,7 @@ class Bootstrap {
 	 * @internal This is not a public API method, do not use in own extensions
 	 */
 	public function shutdown() {
+		$this->sendResponse();
 		return $this;
 	}
 
diff --git a/typo3/sysext/core/Classes/Core/RequestHandlerInterface.php b/typo3/sysext/core/Classes/Core/RequestHandlerInterface.php
index cdddae390b09af1602afd3fee9da3ef858a4904a..7af6f4258c5542a909ebd4210bf4ad2abe30cee0 100644
--- a/typo3/sysext/core/Classes/Core/RequestHandlerInterface.php
+++ b/typo3/sysext/core/Classes/Core/RequestHandlerInterface.php
@@ -16,7 +16,7 @@ namespace TYPO3\CMS\Core\Core;
 
 /**
  * The interface for a request handler
- * see FrontendRequestHandler
+ * see RequestHandler in EXT:backend/Classes/Http/ and EXT:frontend/Classes/Http
  *
  * @api
  */
@@ -25,18 +25,20 @@ interface RequestHandlerInterface {
 	/**
 	 * Handles a raw request
 	 *
-	 * @return void
+	 * @param \Psr\Http\Message\ServerRequestInterface $request
+	 * @return NULL|\Psr\Http\Message\ResponseInterface
 	 * @api
 	 */
-	public function handleRequest();
+	public function handleRequest(\Psr\Http\Message\ServerRequestInterface $request);
 
 	/**
-	 * Checks if the request handler can handle the current request.
+	 * Checks if the request handler can handle the given request.
 	 *
+	 * @param \Psr\Http\Message\ServerRequestInterface $request
 	 * @return bool TRUE if it can handle the request, otherwise FALSE
 	 * @api
 	 */
-	public function canHandleRequest();
+	public function canHandleRequest(\Psr\Http\Message\ServerRequestInterface $request);
 
 	/**
 	 * Returns the priority - how eager the handler is to actually handle the
@@ -47,4 +49,5 @@ interface RequestHandlerInterface {
 	 * @api
 	 */
 	public function getPriority();
+
 }
diff --git a/typo3/sysext/core/Classes/Http/ControllerInterface.php b/typo3/sysext/core/Classes/Http/ControllerInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..e2d275e7c4911ea394e603549171c4e58257ba38
--- /dev/null
+++ b/typo3/sysext/core/Classes/Http/ControllerInterface.php
@@ -0,0 +1,37 @@
+<?php
+namespace TYPO3\CMS\Core\Http;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\RequestInterface;
+
+/**
+ * An interface every controller should implement
+ * in order to deal with PSR-7 standard.
+ *
+ * @internal please note that this API will be extended until TYPO3 CMS 7 LTS and is not public yet.
+ */
+interface ControllerInterface {
+
+	/**
+	 * Processes a typical request.
+	 *
+	 * @param RequestInterface $request The request object
+	 * @return ResponseInterface $response The response, created by the controller
+	 * @api
+	 */
+	public function processRequest(RequestInterface $request);
+
+}
diff --git a/typo3/sysext/core/Classes/Http/Message.php b/typo3/sysext/core/Classes/Http/Message.php
new file mode 100644
index 0000000000000000000000000000000000000000..e5938b840f3d2eb0d85bfb72720d31979b8f5e1a
--- /dev/null
+++ b/typo3/sysext/core/Classes/Http/Message.php
@@ -0,0 +1,472 @@
+<?php
+namespace TYPO3\CMS\Core\Http;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Psr\Http\Message\MessageInterface;
+use Psr\Http\Message\StreamInterface;
+
+/**
+ * Default implementation for the MessageInterface of the PSR-7 standard
+ * It is the base for any request or response for PSR-7.
+ *
+ * Highly inspired by https://github.com/phly/http/
+ *
+ * @internal Note that this is not public API yet.
+ */
+class Message implements MessageInterface {
+
+	/**
+	 * The HTTP Protocol version, defaults to 1.1
+	 * @var string
+	 */
+	protected $protocolVersion = '1.1';
+
+	/**
+	 * Associative array containing all headers of this Message
+	 * This is a mixed-case list of the headers (as due to the specification)
+	 * @var array
+	 */
+	protected $headers = array();
+
+	/**
+	 * Lowercased version of all headers, in order to check if a header is set or not
+	 * this way a lot of checks are easier to be set
+	 * @var array
+	 */
+	protected $lowercasedHeaderNames = array();
+
+	/**
+	 * The body as a Stream object
+	 * @var StreamInterface
+	 */
+	protected $body;
+
+	/**
+	 * Retrieves the HTTP protocol version as a string.
+	 *
+	 * The string MUST contain only the HTTP version number (e.g., "1.1", "1.0").
+	 *
+	 * @return string HTTP protocol version.
+	 */
+	public function getProtocolVersion() {
+		return $this->protocolVersion;
+	}
+
+	/**
+	 * Return an instance with the specified HTTP protocol version.
+	 *
+	 * The version string MUST contain only the HTTP version number (e.g.,
+	 * "1.1", "1.0").
+	 *
+	 * This method MUST be implemented in such a way as to retain the
+	 * immutability of the message, and MUST return an instance that has the
+	 * new protocol version.
+	 *
+	 * @param string $version HTTP protocol version
+	 * @return Message
+	 */
+	public function withProtocolVersion($version) {
+		$clonedObject = clone $this;
+		$clonedObject->protocolVersion = $version;
+		return $clonedObject;
+	}
+
+	/**
+	 * Retrieves all message header values.
+	 *
+	 * The keys represent the header name as it will be sent over the wire, and
+	 * each value is an array of strings associated with the header.
+	 *
+	 *     // Represent the headers as a string
+	 *     foreach ($message->getHeaders() as $name => $values) {
+	 *         echo $name . ": " . implode(", ", $values);
+	 *     }
+	 *
+	 *     // Emit headers iteratively:
+	 *     foreach ($message->getHeaders() as $name => $values) {
+	 *         foreach ($values as $value) {
+	 *             header(sprintf('%s: %s', $name, $value), false);
+	 *         }
+	 *     }
+	 *
+	 * While header names are not case-sensitive, getHeaders() will preserve the
+	 * exact case in which headers were originally specified.
+	 *
+	 * @return array Returns an associative array of the message's headers. Each
+	 *     key MUST be a header name, and each value MUST be an array of strings
+	 *     for that header.
+	 */
+	public function getHeaders() {
+		return $this->headers;
+	}
+
+	/**
+	 * Checks if a header exists by the given case-insensitive name.
+	 *
+	 * @param string $name Case-insensitive header field name.
+	 * @return bool Returns true if any header names match the given header
+	 *     name using a case-insensitive string comparison. Returns false if
+	 *     no matching header name is found in the message.
+	 */
+	public function hasHeader($name) {
+		return isset($this->lowercasedHeaderNames[strtolower($name)]);
+	}
+
+	/**
+	 * Retrieves a message header value by the given case-insensitive name.
+	 *
+	 * This method returns an array of all the header values of the given
+	 * case-insensitive header name.
+	 *
+	 * If the header does not appear in the message, this method MUST return an
+	 * empty array.
+	 *
+	 * @param string $name Case-insensitive header field name.
+	 * @return string[] An array of string values as provided for the given
+	 *    header. If the header does not appear in the message, this method MUST
+	 *    return an empty array.
+	 */
+	public function getHeader($name) {
+		if (!$this->hasHeader($name)) {
+			return array();
+		}
+		$header = $this->lowercasedHeaderNames[strtolower($name)];
+		$headerValue = $this->headers[$header];
+		if (is_array($headerValue)) {
+			return $headerValue;
+		} else {
+			return array($headerValue);
+		}
+	}
+
+	/**
+	 * Retrieves a comma-separated string of the values for a single header.
+	 *
+	 * This method returns all of the header values of the given
+	 * case-insensitive header name as a string concatenated together using
+	 * a comma.
+	 *
+	 * NOTE: Not all header values may be appropriately represented using
+	 * comma concatenation. For such headers, use getHeader() instead
+	 * and supply your own delimiter when concatenating.
+	 *
+	 * If the header does not appear in the message, this method MUST return
+	 * an empty string.
+	 *
+	 * @param string $name Case-insensitive header field name.
+	 * @return string A string of values as provided for the given header
+	 *    concatenated together using a comma. If the header does not appear in
+	 *    the message, this method MUST return an empty string.
+	 */
+	public function getHeaderLine($name) {
+		$headerValue = $this->getHeader($name);
+		if (empty($headerValue)) {
+			return '';
+		}
+		return implode(',', $headerValue);
+	}
+
+	/**
+	 * Return an instance with the provided value replacing the specified header.
+	 *
+	 * While header names are case-insensitive, the casing of the header will
+	 * be preserved by this function, and returned from getHeaders().
+	 *
+	 * This method MUST be implemented in such a way as to retain the
+	 * immutability of the message, and MUST return an instance that has the
+	 * new and/or updated header and value.
+	 *
+	 * @param string $name Case-insensitive header field name.
+	 * @param string|string[] $value Header value(s).
+	 * @return Message
+	 * @throws \InvalidArgumentException for invalid header names or values.
+	 */
+	public function withHeader($name, $value) {
+		if (is_string($value)) {
+			$value = array($value);
+		}
+
+		if (!is_array($value) || !$this->arrayContainsOnlyStrings($value)) {
+			throw new \InvalidArgumentException('Invalid header value for header "' . $name . '"". The value must be a string or an array of strings.', 1436717266);
+		}
+
+		$this->validateHeaderName($name);
+		$this->validateHeaderValues($value);
+		$lowercasedHeaderName = strtolower($name);
+
+		$clonedObject = clone $this;
+		$clonedObject->headers[$name] = $value;
+		$clonedObject->lowercasedHeaderNames[$lowercasedHeaderName] = $name;
+		return $clonedObject;
+	}
+
+	/**
+	 * Return an instance with the specified header appended with the given value.
+	 *
+	 * Existing values for the specified header will be maintained. The new
+	 * value(s) will be appended to the existing list. If the header did not
+	 * exist previously, it will be added.
+	 *
+	 * This method MUST be implemented in such a way as to retain the
+	 * immutability of the message, and MUST return an instance that has the
+	 * new header and/or value.
+	 *
+	 * @param string $name Case-insensitive header field name to add.
+	 * @param string|string[] $value Header value(s).
+	 * @return Message
+	 * @throws \InvalidArgumentException for invalid header names or values.
+	 */
+	public function withAddedHeader($name, $value) {
+		if (is_string($value)) {
+			$value = array($value);
+		}
+		if (!is_array($value) || !$this->arrayContainsOnlyStrings($value)) {
+			throw new \InvalidArgumentException('Invalid header value for header "' . $name . '". The header value must be a string or array of strings', 1436717267);
+		}
+		$this->validateHeaderName($name);
+		$this->validateHeaderValues($value);
+		if (!$this->hasHeader($name)) {
+			return $this->withHeader($name, $value);
+		}
+		$name = $this->lowercasedHeaderNames[strtolower($name)];
+		$clonedObject = clone $this;
+		$clonedObject->headers[$name] = array_merge($this->headers[$name], $value);
+		return $clonedObject;
+	}
+
+	/**
+	 * Return an instance without the specified header.
+	 *
+	 * Header resolution MUST be done without case-sensitivity.
+	 *
+	 * This method MUST be implemented in such a way as to retain the
+	 * immutability of the message, and MUST return an instance that removes
+	 * the named header.
+	 *
+	 * @param string $name Case-insensitive header field name to remove.
+	 * @return Message
+	 */
+	public function withoutHeader($name) {
+		if (!$this->hasHeader($name)) {
+			return clone $this;
+		}
+		// fetch the original header from the lowercased version
+		$lowercasedHeader = strtolower($name);
+		$name = $this->lowercasedHeaderNames[$lowercasedHeader];
+		$clonedObject = clone $this;
+		unset($clonedObject->headers[$name], $clonedObject->lowercasedHeaderNames[$lowercasedHeader]);
+		return $clonedObject;
+	}
+
+	/**
+	 * Gets the body of the message.
+	 *
+	 * @return \Psr\Http\Message\StreamInterface Returns the body as a stream.
+	 */
+	public function getBody() {
+		return $this->body;
+	}
+
+	/**
+	 * Return an instance with the specified message body.
+	 *
+	 * The body MUST be a StreamInterface object.
+	 *
+	 * This method MUST be implemented in such a way as to retain the
+	 * immutability of the message, and MUST return a new instance that has the
+	 * new body stream.
+	 *
+	 * @param \Psr\Http\Message\StreamInterface $body Body.
+	 * @return Message
+	 * @throws \InvalidArgumentException When the body is not valid.
+	 */
+	public function withBody(StreamInterface $body) {
+		$clonedObject = clone $this;
+		$clonedObject->body = $body;
+		return $clonedObject;
+	}
+
+	/**
+	 * Ensure header names and values are valid.
+	 *
+	 * @param array $headers
+	 * @throws \InvalidArgumentException
+	 */
+	protected function assertHeaders(array $headers) {
+		foreach ($headers as $name => $headerValues) {
+			$this->validateHeaderName($name);
+			// check if all values are correct
+			array_walk($headerValues, function($value, $key, Message $messageObject) {
+				if (!$messageObject->isValidHeaderValue($value)) {
+					throw new \InvalidArgumentException('Invalid header value for header "' . $key . '"', 1436717268);
+				}
+			}, $this);
+		}
+	}
+
+	/**
+	 * Filter a set of headers to ensure they are in the correct internal format.
+	 *
+	 * Used by message constructors to allow setting all initial headers at once.
+	 *
+	 * @param array $originalHeaders Headers to filter.
+	 * @return array Filtered headers and names.
+	 */
+	protected function filterHeaders(array $originalHeaders) {
+		$headerNames = $headers = array();
+		foreach ($originalHeaders as $header => $value) {
+			if (!is_string($header) || (!is_array($value) && !is_string($value))) {
+				continue;
+			}
+			if (!is_array($value)) {
+				$value = array($value);
+			}
+			$headerNames[strtolower($header)] = $header;
+			$headers[$header] = $value;
+		}
+		return array($headerNames, $headers);
+	}
+
+	/**
+	 * Helper function to test if an array contains only strings
+	 *
+	 * @param array $data
+	 * @return bool
+	 */
+	protected function arrayContainsOnlyStrings(array $data) {
+		return array_reduce($data, function($original, $item) {
+			return is_string($item) ? $original : FALSE;
+		}, TRUE);
+	}
+
+	/**
+	 * Assert that the provided header values are valid.
+	 *
+	 * @see http://tools.ietf.org/html/rfc7230#section-3.2
+	 * @param string[] $values
+	 * @throws \InvalidArgumentException
+	 */
+	protected function validateHeaderValues(array $values) {
+		array_walk($values, function($value, $key, Message $messageObject) {
+			if (!$messageObject->isValidHeaderValue($value)) {
+				throw new \InvalidArgumentException('Invalid header value for header "' . $key . '"', 1436717269);
+			}
+		}, $this);
+	}
+
+	/**
+	 * Filter a header value
+	 *
+	 * Ensures CRLF header injection vectors are filtered.
+	 *
+	 * Per RFC 7230, only VISIBLE ASCII characters, spaces, and horizontal
+	 * tabs are allowed in values; header continuations MUST consist of
+	 * a single CRLF sequence followed by a space or horizontal tab.
+	 *
+	 * This method filters any values not allowed from the string, and is
+	 * lossy.
+	 *
+	 * @see http://en.wikipedia.org/wiki/HTTP_response_splitting
+	 * @param string $value
+	 * @return string
+	 */
+	public function filter($value) {
+		$value  = (string)$value;
+		$length = strlen($value);
+		$string = '';
+		for ($i = 0; $i < $length; $i += 1) {
+			$ascii = ord($value[$i]);
+
+			// Detect continuation sequences
+			if ($ascii === 13) {
+				$lf = ord($value[$i + 1]);
+				$ws = ord($value[$i + 2]);
+				if ($lf === 10 && in_array($ws, [9, 32], TRUE)) {
+					$string .= $value[$i] . $value[$i + 1];
+					$i += 1;
+				}
+				continue;
+			}
+
+			// Non-visible, non-whitespace characters
+			// 9 === horizontal tab
+			// 32-126, 128-254 === visible
+			// 127 === DEL
+			// 255 === null byte
+			if (($ascii < 32 && $ascii !== 9) || $ascii === 127 || $ascii > 254) {
+				continue;
+			}
+
+			$string .= $value[$i];
+		}
+
+		return $string;
+	}
+
+	/**
+	 * Check whether or not a header name is valid and throw an exception.
+	 *
+	 * @see http://tools.ietf.org/html/rfc7230#section-3.2
+	 * @param string $name
+	 * @throws \InvalidArgumentException
+	 */
+	public function validateHeaderName($name) {
+		if (!preg_match('/^[a-zA-Z0-9\'`#$%&*+.^_|~!-]+$/', $name)) {
+			throw new \InvalidArgumentException('Invalid header name, given "' . $name . '"', 1436717270);
+		}
+	}
+
+	/**
+	 * Checks if a a HTTP header value is valid.
+	 *
+	 * Per RFC 7230, only VISIBLE ASCII characters, spaces, and horizontal
+	 * tabs are allowed in values; header continuations MUST consist of
+	 * a single CRLF sequence followed by a space or horizontal tab.
+	 *
+	 * @see http://en.wikipedia.org/wiki/HTTP_response_splitting
+	 * @param string $value
+	 * @return bool
+	 */
+	public function isValidHeaderValue($value) {
+		$value = (string)$value;
+
+		// Look for:
+		// \n not preceded by \r, OR
+		// \r not followed by \n, OR
+		// \r\n not followed by space or horizontal tab; these are all CRLF attacks
+		if (preg_match("#(?:(?:(?<!\r)\n)|(?:\r(?!\n))|(?:\r\n(?![ \t])))#", $value)) {
+			return FALSE;
+		}
+
+		$length = strlen($value);
+		for ($i = 0; $i < $length; $i += 1) {
+			$ascii = ord($value[$i]);
+
+			// Non-visible, non-whitespace characters
+			// 9 === horizontal tab
+			// 10 === line feed
+			// 13 === carriage return
+			// 32-126, 128-254 === visible
+			// 127 === DEL
+			// 255 === null byte
+			if (($ascii < 32 && ! in_array($ascii, [9, 10, 13], TRUE)) || $ascii === 127 || $ascii > 254) {
+				return FALSE;
+			}
+		}
+
+		return TRUE;
+	}
+
+}
diff --git a/typo3/sysext/core/Classes/Http/Request.php b/typo3/sysext/core/Classes/Http/Request.php
new file mode 100644
index 0000000000000000000000000000000000000000..9d7d05dc2c64334b56d983589f4bf81dde621406
--- /dev/null
+++ b/typo3/sysext/core/Classes/Http/Request.php
@@ -0,0 +1,349 @@
+<?php
+namespace TYPO3\CMS\Core\Http;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Psr\Http\Message\RequestInterface;
+use Psr\Http\Message\UriInterface;
+use Psr\Http\Message\StreamInterface;
+
+/**
+ * Default implementation for the RequestInterface of the PSR-7 standard
+ * It is the base for any request sent BY PHP.
+ *
+ * Please see ServerRequest for the typical use cases in the framework.
+ *
+ * Highly inspired by https://github.com/phly/http/
+ *
+ * @internal Note that this is not public API yet.
+ */
+class Request extends Message implements RequestInterface {
+
+	/**
+	 * The request-target, if it has been provided or calculated.
+	 * @var NULL|string
+	 */
+	protected $requestTarget;
+
+	/**
+	 * The HTTP method, defaults to GET
+	 *
+	 * @var string
+	 */
+	protected $method;
+
+	/**
+	 * Supported HTTP methods
+	 *
+	 * @var array
+	 */
+	protected $supportedMethods = array(
+		'CONNECT',
+		'DELETE',
+		'GET',
+		'HEAD',
+		'OPTIONS',
+		'PATCH',
+		'POST',
+		'PUT',
+		'TRACE'
+	);
+
+	/**
+	 * An instance of the Uri object
+	 * @var UriInterface
+	 */
+	protected $uri;
+
+	/**
+	 * Constructor, the only place to set all parameters of this Request
+	 *
+	 * @param NULL|string $uri URI for the request, if any.
+	 * @param NULL|string $method HTTP method for the request, if any.
+	 * @param string|resource|StreamInterface $body Message body, if any.
+	 * @param array $headers Headers for the message, if any.
+	 * @throws \InvalidArgumentException for any invalid value.
+	 */
+	public function __construct($uri = NULL, $method = NULL, $body = 'php://input', array $headers = array()) {
+
+		// Build a streamable object for the body
+		if (!is_string($body) && !is_resource($body) && !$body instanceof StreamInterface) {
+			throw new \InvalidArgumentException('Body must be a string stream resource identifier, a stream resource, or a StreamInterface instance', 1436717271);
+		}
+
+		if (!$body instanceof StreamInterface) {
+			$body = new Stream($body);
+		}
+
+		if (is_string($uri)) {
+			$uri = new Uri($uri);
+		}
+
+		if (!$uri instanceof UriInterface && $uri !== NULL) {
+			throw new \InvalidArgumentException('Invalid URI provided; must be null, a string, or a UriInterface instance', 1436717272);
+		}
+
+		$this->validateMethod($method);
+
+		$this->method = $method;
+		$this->uri    = $uri;
+		$this->body   = $body;
+		list($this->headerNames, $headers) = $this->filterHeaders($headers);
+		$this->assertHeaders($headers);
+		$this->headers = $headers;
+	}
+
+	/**
+	 * Retrieves all message header values.
+	 *
+	 * The keys represent the header name as it will be sent over the wire, and
+	 * each value is an array of strings associated with the header.
+	 *
+	 *     // Represent the headers as a string
+	 *     foreach ($message->getHeaders() as $name => $values) {
+	 *         echo $name . ": " . implode(", ", $values);
+	 *     }
+	 *
+	 *     // Emit headers iteratively:
+	 *     foreach ($message->getHeaders() as $name => $values) {
+	 *         foreach ($values as $value) {
+	 *             header(sprintf('%s: %s', $name, $value), false);
+	 *         }
+	 *     }
+	 *
+	 * While header names are not case-sensitive, getHeaders() will preserve the
+	 * exact case in which headers were originally specified.
+	 *
+	 * @return array Returns an associative array of the message's headers. Each
+	 *     key MUST be a header name, and each value MUST be an array of strings
+	 *     for that header.
+	 */
+	public function getHeaders() {
+		$headers = parent::getHeaders();
+		if (!$this->hasHeader('host') && ($this->uri && $this->uri->getHost())) {
+			$headers['host'] = [$this->getHostFromUri()];
+		}
+		return $headers;
+	}
+
+	/**
+	 * Retrieves a message header value by the given case-insensitive name.
+	 *
+	 * This method returns an array of all the header values of the given
+	 * case-insensitive header name.
+	 *
+	 * If the header does not appear in the message, this method MUST return an
+	 * empty array.
+	 *
+	 * @param string $name Case-insensitive header field name.
+	 * @return string[] An array of string values as provided for the given
+	 *    header. If the header does not appear in the message, this method MUST
+	 *    return an empty array.
+	 */
+	public function getHeader($header) {
+		if (!$this->hasHeader($header) && strtolower($header) === 'host' && ($this->uri && $this->uri->getHost())) {
+			return array($this->getHostFromUri());
+		}
+		return parent::getHeader($header);
+	}
+
+	/**
+	 * Retrieve the host from the URI instance
+	 *
+	 * @return string
+	 */
+	protected function getHostFromUri() {
+		$host  = $this->uri->getHost();
+		$host .= $this->uri->getPort() ? ':' . $this->uri->getPort() : '';
+		return $host;
+	}
+
+	/**
+	 * Retrieves the message's request target.
+	 *
+	 * Retrieves the message's request-target either as it will appear (for
+	 * clients), as it appeared at request (for servers), or as it was
+	 * specified for the instance (see withRequestTarget()).
+	 *
+	 * In most cases, this will be the origin-form of the composed URI,
+	 * unless a value was provided to the concrete implementation (see
+	 * withRequestTarget() below).
+	 *
+	 * If no URI is available, and no request-target has been specifically
+	 * provided, this method MUST return the string "/".
+	 *
+	 * @return string
+	 */
+	public function getRequestTarget() {
+		if ($this->requestTarget !== NULL) {
+			return $this->requestTarget;
+		}
+		if (!$this->uri) {
+			return '/';
+		}
+		$target = $this->uri->getPath();
+
+		if ($this->uri->getQuery()) {
+			$target .= '?' . $this->uri->getQuery();
+		}
+
+		if (empty($target)) {
+			$target = '/';
+		}
+		return $target;
+	}
+
+	/**
+	 * Return an instance with the specific request-target.
+	 *
+	 * If the request needs a non-origin-form request-target — e.g., for
+	 * specifying an absolute-form, authority-form, or asterisk-form —
+	 * this method may be used to create an instance with the specified
+	 * request-target, verbatim.
+	 *
+	 * This method MUST be implemented in such a way as to retain the
+	 * immutability of the message, and MUST return an instance that has the
+	 * changed request target.
+	 *
+	 * @link http://tools.ietf.org/html/rfc7230#section-2.7 (for the various
+	 *     request-target forms allowed in request messages)
+	 *
+	 * @param mixed $requestTarget
+	 * @return Request
+	 */
+	public function withRequestTarget($requestTarget) {
+		if (preg_match('#\s#', $requestTarget)) {
+			throw new \InvalidArgumentException('Invalid request target provided which contains whitespaces.', 1436717273);
+		}
+		$clonedObject = clone $this;
+		$clonedObject->requestTarget = $requestTarget;
+		return $clonedObject;
+	}
+
+	/**
+	 * Retrieves the HTTP method of the request, defaults to GET
+	 *
+	 * @return string Returns the request method.
+	 */
+	public function getMethod() {
+		return !empty($this->method) ? $this->method : 'GET';
+	}
+
+	/**
+	 * Return an instance with the provided HTTP method.
+	 *
+	 * While HTTP method names are typically all uppercase characters, HTTP
+	 * method names are case-sensitive and thus implementations SHOULD NOT
+	 * modify the given string.
+	 *
+	 * This method MUST be implemented in such a way as to retain the
+	 * immutability of the message, and MUST return an instance that has the
+	 * changed request method.
+	 *
+	 * @param string $method Case-sensitive method.
+	 * @return Request
+	 * @throws \InvalidArgumentException for invalid HTTP methods.
+	 */
+	public function withMethod($method) {
+		$clonedObject = clone $this;
+		$clonedObject->method = $method;
+		return $clonedObject;
+	}
+
+	/**
+	 * Retrieves the URI instance.
+	 *
+	 * This method MUST return a UriInterface instance.
+	 *
+	 * @link http://tools.ietf.org/html/rfc3986#section-4.3
+	 * @return \Psr\Http\Message\UriInterface Returns a UriInterface instance
+	 *     representing the URI of the request.
+	 */
+	public function getUri() {
+		return $this->uri;
+	}
+
+	/**
+	 * Returns an instance with the provided URI.
+	 *
+	 * This method MUST update the Host header of the returned request by
+	 * default if the URI contains a host component. If the URI does not
+	 * contain a host component, any pre-existing Host header MUST be carried
+	 * over to the returned request.
+	 *
+	 * You can opt-in to preserving the original state of the Host header by
+	 * setting `$preserveHost` to `true`. When `$preserveHost` is set to
+	 * `true`, this method interacts with the Host header in the following ways:
+	 *
+	 * - If the the Host header is missing or empty, and the new URI contains
+	 *   a host component, this method MUST update the Host header in the returned
+	 *   request.
+	 * - If the Host header is missing or empty, and the new URI does not contain a
+	 *   host component, this method MUST NOT update the Host header in the returned
+	 *   request.
+	 * - If a Host header is present and non-empty, this method MUST NOT update
+	 *   the Host header in the returned request.
+	 *
+	 * This method MUST be implemented in such a way as to retain the
+	 * immutability of the message, and MUST return an instance that has the
+	 * new UriInterface instance.
+	 *
+	 * @link http://tools.ietf.org/html/rfc3986#section-4.3
+	 *
+	 * @param \Psr\Http\Message\UriInterface $uri New request URI to use.
+	 * @param bool $preserveHost Preserve the original state of the Host header.
+	 * @return Request
+	 */
+	public function withUri(UriInterface $uri, $preserveHost = FALSE) {
+		$clonedObject = clone $this;
+		$clonedObject->uri = $uri;
+
+		if ($preserveHost) {
+			return $clonedObject;
+		}
+
+		if (!$uri->getHost()) {
+			return $clonedObject;
+		}
+
+		$host = $uri->getHost();
+
+		if ($uri->getPort()) {
+			$host .= ':' . $uri->getPort();
+		}
+
+		$clonedObject->headerNames['host'] = 'Host';
+		$clonedObject->headers['Host'] = array($host);
+		return $clonedObject;
+	}
+
+	/**
+	 * Validate the HTTP method, helper function.
+	 *
+	 * @param NULL|string $method
+	 * @throws \InvalidArgumentException on invalid HTTP method.
+	 */
+	protected function validateMethod($method) {
+		if ($method !== NULL) {
+			if (!is_string($method)) {
+				$methodAsString = is_object($method) ? get_class($method) : gettype($method);
+				throw new \InvalidArgumentException('Unsupported HTTP method "' . $methodAsString . '".', 1436717274);
+			}
+			$method = strtoupper($method);
+			if (!in_array($method, $this->supportedMethods, TRUE)) {
+				throw new \InvalidArgumentException('Unsupported HTTP method "' . $method. '".', 1436717275);
+			}
+		}
+	}
+}
diff --git a/typo3/sysext/core/Classes/Http/Response.php b/typo3/sysext/core/Classes/Http/Response.php
new file mode 100644
index 0000000000000000000000000000000000000000..14b4fb5e3d31e37bff65dbe2470601cc474149af
--- /dev/null
+++ b/typo3/sysext/core/Classes/Http/Response.php
@@ -0,0 +1,201 @@
+<?php
+namespace TYPO3\CMS\Core\Http;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\StreamInterface;
+
+/**
+ * Default implementation for the ResponseInterface of the PSR-7 standard.
+ *
+ * Highly inspired by https://github.com/phly/http/
+ *
+ * @internal Note that this is not public API yet.
+ */
+class Response extends Message implements ResponseInterface {
+
+	/**
+	 * The HTTP status code of the response
+	 * @var int $statusCode
+	 */
+	protected $statusCode;
+
+	/**
+	 * The reason phrase of the response
+	 * @var string $reasonPhrase
+	 */
+	protected $reasonPhrase = '';
+
+	/**
+	 * The standardized and other important HTTP Status Codes
+	 * @var array
+	 */
+	protected $availableStatusCodes = array(
+		// INFORMATIONAL CODES
+		100 => 'Continue',
+		101 => 'Switching Protocols',
+		102 => 'Processing',
+		// SUCCESS CODES
+		200 => 'OK',
+		201 => 'Created',
+		202 => 'Accepted',
+		203 => 'Non-Authoritative Information',
+		204 => 'No Content',
+		205 => 'Reset Content',
+		206 => 'Partial Content',
+		207 => 'Multi-status',
+		208 => 'Already Reported',
+		// REDIRECTION CODES
+		300 => 'Multiple Choices',
+		301 => 'Moved Permanently',
+		302 => 'Found',
+		303 => 'See Other',
+		304 => 'Not Modified',
+		305 => 'Use Proxy',
+		306 => 'Switch Proxy', // Deprecated
+		307 => 'Temporary Redirect',
+		// CLIENT ERROR
+		400 => 'Bad Request',
+		401 => 'Unauthorized',
+		402 => 'Payment Required',
+		403 => 'Forbidden',
+		404 => 'Not Found',
+		405 => 'Method Not Allowed',
+		406 => 'Not Acceptable',
+		407 => 'Proxy Authentication Required',
+		408 => 'Request Time-out',
+		409 => 'Conflict',
+		410 => 'Gone',
+		411 => 'Length Required',
+		412 => 'Precondition Failed',
+		413 => 'Request Entity Too Large',
+		414 => 'Request-URI Too Large',
+		415 => 'Unsupported Media Type',
+		416 => 'Requested range not satisfiable',
+		417 => 'Expectation Failed',
+		418 => 'I\'m a teapot',
+		422 => 'Unprocessable Entity',
+		423 => 'Locked',
+		424 => 'Failed Dependency',
+		425 => 'Unordered Collection',
+		426 => 'Upgrade Required',
+		428 => 'Precondition Required',
+		429 => 'Too Many Requests',
+		431 => 'Request Header Fields Too Large',
+		// SERVER ERROR
+		500 => 'Internal Server Error',
+		501 => 'Not Implemented',
+		502 => 'Bad Gateway',
+		503 => 'Service Unavailable',
+		504 => 'Gateway Time-out',
+		505 => 'HTTP Version not supported',
+		506 => 'Variant Also Negotiates',
+		507 => 'Insufficient Storage',
+		508 => 'Loop Detected',
+		509 => 'Bandwidth Limit Exceeded',
+		511 => 'Network Authentication Required'
+	);
+
+	/**
+	 * Constructor for generating new responses
+	 *
+	 * @param StreamInterface|string $body
+	 * @param int $statusCode
+	 * @param array $headers
+	 * @throws \InvalidArgumentException if any of the given arguments are given
+	 */
+	public function __construct($body = 'php://temp', $statusCode = 200, $headers = array()) {
+		// Build a streamable object for the body
+		if (!is_string($body) && !is_resource($body) && !$body instanceof StreamInterface) {
+			throw new \InvalidArgumentException('Body must be a string stream resource identifier, a stream resource, or a StreamInterface instance', 1436717277);
+		}
+
+		if (!$body instanceof StreamInterface) {
+			$body = new Stream($body, 'rw');
+		}
+		$this->body = $body;
+
+		if (\TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($statusCode) === FALSE || !array_key_exists((int)$statusCode, $this->availableStatusCodes)) {
+			throw new \InvalidArgumentException('The given status code is not a valid HTTP status code.', 1436717278);
+		}
+		$this->statusCode = (int)$statusCode;
+
+		$this->reasonPhrase = $this->availableStatusCodes[$this->statusCode];
+		list($this->headerNames, $headers) = $this->filterHeaders($headers);
+		$this->assertHeaders($headers);
+		$this->headers = $headers;
+	}
+
+	/**
+	 * Gets the response status code.
+	 *
+	 * The status code is a 3-digit integer result code of the server's attempt
+	 * to understand and satisfy the request.
+	 *
+	 * @return int Status code.
+	 */
+	public function getStatusCode() {
+		return $this->statusCode;
+	}
+
+	/**
+	 * Return an instance with the specified status code and, optionally, reason phrase.
+	 *
+	 * If no reason phrase is specified, implementations MAY choose to default
+	 * to the RFC 7231 or IANA recommended reason phrase for the response's
+	 * status code.
+	 *
+	 * This method MUST be implemented in such a way as to retain the
+	 * immutability of the message, and MUST return an instance that has the
+	 * updated status and reason phrase.
+	 *
+	 * @link http://tools.ietf.org/html/rfc7231#section-6
+	 * @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
+	 *
+	 * @param int $code The 3-digit integer result code to set.
+	 * @param string $reasonPhrase The reason phrase to use with the
+	 *     provided status code; if none is provided, implementations MAY
+	 *     use the defaults as suggested in the HTTP specification.
+	 * @return Response
+	 * @throws \InvalidArgumentException For invalid status code arguments.
+	 */
+	public function withStatus($code, $reasonPhrase = '') {
+		if (\TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($code) === FALSE || !array_key_exists((int)$code, $this->availableStatusCodes)) {
+			throw new \InvalidArgumentException('The given status code is not a valid HTTP status code', 1436717279);
+		}
+		$clonedObject = clone $this;
+		$clonedObject->statusCode = $code;
+		$clonedObject->reasonPhrase = $reasonPhrase !== '' ? $reasonPhrase : $this->availableStatusCodes[$code];
+		return $clonedObject;
+	}
+
+	/**
+	 * Gets the response reason phrase associated with the status code.
+	 *
+	 * Because a reason phrase is not a required element in a response
+	 * status line, the reason phrase value MAY be null. Implementations MAY
+	 * choose to return the default RFC 7231 recommended reason phrase (or those
+	 * listed in the IANA HTTP Status Code Registry) for the response's
+	 * status code.
+	 *
+	 * @link http://tools.ietf.org/html/rfc7231#section-6
+	 * @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
+	 * @return string Reason phrase; must return an empty string if none present.
+	 */
+	public function getReasonPhrase() {
+		return $this->reasonPhrase;
+	}
+
+}
diff --git a/typo3/sysext/core/Classes/Http/ServerRequest.php b/typo3/sysext/core/Classes/Http/ServerRequest.php
new file mode 100644
index 0000000000000000000000000000000000000000..019eebfccb1956e2c0f2da76a3b9cd196c9f631d
--- /dev/null
+++ b/typo3/sysext/core/Classes/Http/ServerRequest.php
@@ -0,0 +1,369 @@
+<?php
+namespace TYPO3\CMS\Core\Http;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Message\StreamInterface;
+use Psr\Http\Message\UploadedFileInterface;
+
+/**
+ * Represents a typical request incoming from the server to be processed
+ * by the TYPO3 Core. The original request is built from the ServerRequestFactory
+ * inside TYPO3's Bootstrap.
+ *
+ * Note that the PSR-7 standard works with immutable value objects, meaning that
+ * any modification to a Request object using the "with" methods will result
+ * in a new Request object.
+ *
+ * Highly inspired by https://github.com/phly/http/
+ *
+ * @internal Note that this is not public API yet.
+ */
+class ServerRequest extends Request implements ServerRequestInterface {
+
+	/**
+	 * @var array
+	 */
+	protected $attributes;
+
+	/**
+	 * @var array
+	 */
+	protected $cookieParams;
+
+	/**
+	 * @var array
+	 */
+	protected $parsedBody;
+
+	/**
+	 * @var array
+	 */
+	protected $queryParams;
+
+	/**
+	 * @var array
+	 */
+	protected $serverParams;
+
+	/**
+	 * @var array
+	 */
+	protected $uploadedFiles;
+
+	/**
+	 * Constructor, the only place to set all parameters of this Message/Request
+	 *
+	 * @param NULL|string $uri URI for the request, if any.
+	 * @param NULL|string $method HTTP method for the request, if any.
+	 * @param string|resource|StreamInterface $body Message body, if any.
+	 * @param array $headers Headers for the message, if any.
+	 * @param array $serverParams Server parameters, typically from $_SERVER
+	 * @param array $uploadedFiles Upload file information, a tree of UploadedFiles
+	 * @throws \InvalidArgumentException for any invalid value.
+	 */
+	public function __construct($uri = NULL, $method = NULL, $body = 'php://input', array $headers = array(), array $serverParams = array(), array $uploadedFiles = NULL) {
+		if ($uploadedFiles !== NULL) {
+			$this->validateUploadedFiles($uploadedFiles);
+		}
+
+		parent::__construct($uri, $method, $body, $headers);
+
+		$this->serverParams  = $serverParams;
+		$this->uploadedFiles = $uploadedFiles;
+	}
+
+	/**
+	 * Retrieve server parameters.
+	 *
+	 * Retrieves data related to the incoming request environment,
+	 * typically derived from PHP's $_SERVER superglobal. The data IS NOT
+	 * REQUIRED to originate from $_SERVER.
+	 *
+	 * @return array
+	 */
+	public function getServerParams() {
+		return $this->serverParams;
+	}
+
+	/**
+	 * Retrieve cookies.
+	 *
+	 * Retrieves cookies sent by the client to the server.
+	 *
+	 * The data MUST be compatible with the structure of the $_COOKIE
+	 * superglobal.
+	 *
+	 * @return array
+	 */
+	public function getCookieParams() {
+		return $this->cookieParams;
+	}
+
+	/**
+	 * Return an instance with the specified cookies.
+	 *
+	 * The data IS NOT REQUIRED to come from the $_COOKIE superglobal, but MUST
+	 * be compatible with the structure of $_COOKIE. Typically, this data will
+	 * be injected at instantiation.
+	 *
+	 * This method MUST NOT update the related Cookie header of the request
+	 * instance, nor related values in the server params.
+	 *
+	 * This method MUST be implemented in such a way as to retain the
+	 * immutability of the message, and MUST return an instance that has the
+	 * updated cookie values.
+	 *
+	 * @param array $cookies Array of key/value pairs representing cookies.
+	 * @return ServerRequest
+	 */
+	public function withCookieParams(array $cookies) {
+		$clonedObject = clone $this;
+		$clonedObject->cookieParams = $cookies;
+		return $clonedObject;
+	}
+
+	/**
+	 * Retrieve query string arguments.
+	 *
+	 * Retrieves the deserialized query string arguments, if any.
+	 *
+	 * Note: the query params might not be in sync with the URI or server
+	 * params. If you need to ensure you are only getting the original
+	 * values, you may need to parse the query string from `getUri()->getQuery()`
+	 * or from the `QUERY_STRING` server param.
+	 *
+	 * @return array
+	 */
+	public function getQueryParams() {
+		return $this->queryParams;
+	}
+
+	/**
+	 * Return an instance with the specified query string arguments.
+	 *
+	 * These values SHOULD remain immutable over the course of the incoming
+	 * request. They MAY be injected during instantiation, such as from PHP's
+	 * $_GET superglobal, or MAY be derived from some other value such as the
+	 * URI. In cases where the arguments are parsed from the URI, the data
+	 * MUST be compatible with what PHP's parse_str() would return for
+	 * purposes of how duplicate query parameters are handled, and how nested
+	 * sets are handled.
+	 *
+	 * Setting query string arguments MUST NOT change the URI stored by the
+	 * request, nor the values in the server params.
+	 *
+	 * This method MUST be implemented in such a way as to retain the
+	 * immutability of the message, and MUST return an instance that has the
+	 * updated query string arguments.
+	 *
+	 * @param array $query Array of query string arguments, typically from
+	 *     $_GET.
+	 * @return ServerRequest
+	 */
+	public function withQueryParams(array $query) {
+		$clonedObject = clone $this;
+		$clonedObject->queryParams = $query;
+		return $clonedObject;
+	}
+
+	/**
+	 * Retrieve normalized file upload data.
+	 *
+	 * This method returns upload metadata in a normalized tree, with each leaf
+	 * an instance of Psr\Http\Message\UploadedFileInterface.
+	 *
+	 * These values MAY be prepared from $_FILES or the message body during
+	 * instantiation, or MAY be injected via withUploadedFiles().
+	 *
+	 * @return array An array tree of UploadedFileInterface instances; an empty
+	 *     array MUST be returned if no data is present.
+	 */
+	public function getUploadedFiles() {
+		return $this->uploadedFiles;
+	}
+
+	/**
+	 * Create a new instance with the specified uploaded files.
+	 *
+	 * This method MUST be implemented in such a way as to retain the
+	 * immutability of the message, and MUST return an instance that has the
+	 * updated body parameters.
+	 *
+	 * @param array $uploadedFiles An array tree of UploadedFileInterface instances.
+	 * @return ServerRequest
+	 * @throws \InvalidArgumentException if an invalid structure is provided.
+	 */
+	public function withUploadedFiles(array $uploadedFiles) {
+		$this->validateUploadedFiles($uploadedFiles);
+		$clonedObject = clone $this;
+		$clonedObject->uploadedFiles = $uploadedFiles;
+		return $clonedObject;
+	}
+
+	/**
+	 * Retrieve any parameters provided in the request body.
+	 *
+	 * If the request Content-Type is either application/x-www-form-urlencoded
+	 * or multipart/form-data, and the request method is POST, this method MUST
+	 * return the contents of $_POST.
+	 *
+	 * Otherwise, this method may return any results of deserializing
+	 * the request body content; as parsing returns structured content, the
+	 * potential types MUST be arrays or objects only. A null value indicates
+	 * the absence of body content.
+	 *
+	 * @return null|array|object The deserialized body parameters, if any.
+	 *     These will typically be an array or object.
+	 */
+	public function getParsedBody() {
+		return $this->parsedBody;
+	}
+
+	/**
+	 * Return an instance with the specified body parameters.
+	 *
+	 * These MAY be injected during instantiation.
+	 *
+	 * If the request Content-Type is either application/x-www-form-urlencoded
+	 * or multipart/form-data, and the request method is POST, use this method
+	 * ONLY to inject the contents of $_POST.
+	 *
+	 * The data IS NOT REQUIRED to come from $_POST, but MUST be the results of
+	 * deserializing the request body content. Deserialization/parsing returns
+	 * structured data, and, as such, this method ONLY accepts arrays or objects,
+	 * or a null value if nothing was available to parse.
+	 *
+	 * As an example, if content negotiation determines that the request data
+	 * is a JSON payload, this method could be used to create a request
+	 * instance with the deserialized parameters.
+	 *
+	 * This method MUST be implemented in such a way as to retain the
+	 * immutability of the message, and MUST return an instance that has the
+	 * updated body parameters.
+	 *
+	 * @param null|array|object $data The deserialized body data. This will
+	 *     typically be in an array or object.
+	 * @return ServerRequest
+	 * @throws \InvalidArgumentException if an unsupported argument type is
+	 *     provided.
+	 */
+	public function withParsedBody($data) {
+		$clonedObject = clone $this;
+		$clonedObject->parsedBody = $data;
+		return $clonedObject;
+	}
+
+	/**
+	 * Retrieve attributes derived from the request.
+	 *
+	 * The request "attributes" may be used to allow injection of any
+	 * parameters derived from the request: e.g., the results of path
+	 * match operations; the results of decrypting cookies; the results of
+	 * deserializing non-form-encoded message bodies; etc. Attributes
+	 * will be application and request specific, and CAN be mutable.
+	 *
+	 * @return array Attributes derived from the request.
+	 */
+	public function getAttributes() {
+		return $this->attributes;
+	}
+
+	/**
+	 * Retrieve a single derived request attribute.
+	 *
+	 * Retrieves a single derived request attribute as described in
+	 * getAttributes(). If the attribute has not been previously set, returns
+	 * the default value as provided.
+	 *
+	 * This method obviates the need for a hasAttribute() method, as it allows
+	 * specifying a default value to return if the attribute is not found.
+	 *
+	 * @see getAttributes()
+	 *
+	 * @param string $name The attribute name.
+	 * @param mixed $default Default value to return if the attribute does not exist.
+	 * @return mixed
+	 */
+	public function getAttribute($name, $default = NULL) {
+		return isset($this->attributes[$name]) ? $this->attributes[$name] : $default;
+	}
+
+	/**
+	 * Return an instance with the specified derived request attribute.
+	 *
+	 * This method allows setting a single derived request attribute as
+	 * described in getAttributes().
+	 *
+	 * This method MUST be implemented in such a way as to retain the
+	 * immutability of the message, and MUST return an instance that has the
+	 * updated attribute.
+	 *
+	 * @see getAttributes()
+	 *
+	 * @param string $name The attribute name.
+	 * @param mixed $value The value of the attribute.
+	 * @return ServerRequest
+	 */
+	public function withAttribute($name, $value) {
+		$clonedObject = clone $this;
+		$clonedObject->attributes[$name] = $value;
+		return $clonedObject;
+	}
+
+	/**
+	 * Return an instance that removes the specified derived request attribute.
+	 *
+	 * This method allows removing a single derived request attribute as
+	 * described in getAttributes().
+	 *
+	 * This method MUST be implemented in such a way as to retain the
+	 * immutability of the message, and MUST return an instance that removes
+	 * the attribute.
+	 *
+	 * @see getAttributes()
+	 *
+	 * @param string $name The attribute name.
+	 * @return ServerRequest
+	 */
+	public function withoutAttribute($name) {
+		$clonedObject = clone $this;
+		if (!isset($clonedObject->attributes[$name])) {
+			return $clonedObject;
+		} else {
+			unset($clonedObject->attributes[$name]);
+			return $clonedObject;
+		}
+	}
+
+	/**
+	 * Recursively validate the structure in an uploaded files array.
+	 *
+	 * @param array $uploadedFiles
+	 * @throws \InvalidArgumentException if any leaf is not an UploadedFileInterface instance.
+	 */
+	protected function validateUploadedFiles(array $uploadedFiles) {
+		foreach ($uploadedFiles as $file) {
+			if (is_array($file)) {
+				$this->validateUploadedFiles($file);
+				continue;
+			}
+			if (!$file instanceof UploadedFileInterface) {
+				throw new \InvalidArgumentException('Invalid file in uploaded files structure.', 1436717281);
+			}
+		}
+	}
+
+}
diff --git a/typo3/sysext/core/Classes/Http/ServerRequestFactory.php b/typo3/sysext/core/Classes/Http/ServerRequestFactory.php
new file mode 100644
index 0000000000000000000000000000000000000000..73014f111d4161e915b9e76bda56462627fbbf42
--- /dev/null
+++ b/typo3/sysext/core/Classes/Http/ServerRequestFactory.php
@@ -0,0 +1,152 @@
+<?php
+namespace TYPO3\CMS\Core\Http;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Psr\Http\Message\UploadedFileInterface;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Class ServerRequestFactory to create ServerRequest objects
+ *
+ * Highly inspired by https://github.com/phly/http/
+ *
+ * @internal Note that this is not public API yet.
+ */
+class ServerRequestFactory {
+
+	/**
+	 * Create a request from the original superglobal variables.
+	 *
+	 * @return ServerRequest
+	 * @throws \InvalidArgumentException when invalid file values given
+	 * @internal Note that this is not public API yet.
+	 */
+	static public function fromGlobals() {
+		$serverParameters = $_SERVER;
+		$headers = static::prepareHeaders($serverParameters);
+
+		$method = isset($serverParameters['REQUEST_METHOD']) ? $serverParameters['REQUEST_METHOD'] : 'GET';
+		$uri = new Uri(GeneralUtility::getIndpEnv('TYPO3_REQUEST_URL'));
+
+		$request = new ServerRequest(
+			$uri,
+			$method,
+			'php://input',
+			$headers,
+			$serverParameters,
+			static::normalizeUploadedFiles($_FILES)
+		);
+
+		if (!empty($_COOKIE)) {
+			$request = $request->withCookieParams($_COOKIE);
+		}
+		$queryParameters = GeneralUtility::_GET();
+		if (!empty($queryParameters)) {
+			$request = $request->withQueryParams($queryParameters);
+		}
+		$parsedBody = GeneralUtility::_POST();
+		if (!empty($parsedBody)) {
+			$request = $request->withParsedBody($parsedBody);
+		}
+		return $request;
+	}
+
+	/**
+	 * Fetch headers from $_SERVER variables
+	 * which are only the ones starting with HTTP_* and CONTENT_*
+	 *
+	 * @param array $server
+	 * @return array
+	 */
+	protected static function prepareHeaders(array $server) {
+		$headers = array();
+		foreach ($server as $key => $value) {
+			if (strpos($key, 'HTTP_COOKIE') === 0) {
+				// Cookies are handled using the $_COOKIE superglobal
+				continue;
+			}
+			if (!empty($value)) {
+				if (strpos($key, 'HTTP_') === 0) {
+					$name = strtr(substr($key, 5), '_', ' ');
+					$name = strtr(ucwords(strtolower($name)), ' ', '-');
+					$name = strtolower($name);
+					$headers[$name] = $value;
+				} elseif (strpos($key, 'CONTENT_') === 0) {
+					$name = substr($key, 8); // Content-
+					$name = 'Content-' . (($name == 'MD5') ? $name : ucfirst(strtolower($name)));
+					$name = strtolower($name);
+					$headers[$name] = $value;
+				}
+			}
+		}
+		return $headers;
+	}
+
+	/**
+	 * Normalize uploaded files
+	 *
+	 * Transforms each value into an UploadedFileInterface instance, and ensures that nested arrays are normalized.
+	 *
+	 * @param array $files
+	 * @return array
+	 * @throws \InvalidArgumentException for unrecognized values
+	 */
+	protected static function normalizeUploadedFiles(array $files) {
+		$normalizedFileUploads = array();
+		foreach ($files as $key => $value) {
+			if ($value instanceof UploadedFileInterface) {
+				$normalizedFileUploads[$key] = $value;
+			} elseif (is_array($value)) {
+				if (isset($value['tmp_name'])) {
+					$normalizedFileUploads[$key] = self::createUploadedFile($value);
+				} else {
+					$normalizedFileUploads[$key] = self::normalizeUploadedFiles($value);
+				}
+			} else {
+				throw new \InvalidArgumentException('Invalid value in files specification.', 1436717282);
+			}
+		}
+		return $normalizedFileUploads;
+	}
+
+	/**
+	 * Create and return an UploadedFile instance from a $_FILES specification.
+	 *
+	 * If the specification represents an array of values, this method will
+	 * delegate to normalizeNestedFileSpec() and return that return value.
+	 *
+	 * @param array $value $_FILES structure
+	 * @return UploadedFileInterface[]|UploadedFileInterface
+	 */
+	protected static function createUploadedFile(array $value) {
+		if (is_array($value['tmp_name'])) {
+			$files = array();
+			foreach (array_keys($value['tmp_name']) as $key) {
+				$data = array(
+					'tmp_name' => $value['tmp_name'][$key],
+					'size'     => $value['size'][$key],
+					'error'    => $value['error'][$key],
+					'name'     => $value['name'][$key],
+					'type'     => $value['type'][$key]
+				);
+				$files[$key] = self::createUploadedFile($data);
+			}
+			return $files;
+		} else {
+			return new UploadedFile($value['tmp_name'], $value['size'], $value['error'], $value['name'], $value['type']);
+		}
+	}
+
+}
diff --git a/typo3/sysext/core/Classes/Http/Stream.php b/typo3/sysext/core/Classes/Http/Stream.php
new file mode 100644
index 0000000000000000000000000000000000000000..10df091ea7d2d5cb5a9e7e9704809ff62649f79b
--- /dev/null
+++ b/typo3/sysext/core/Classes/Http/Stream.php
@@ -0,0 +1,343 @@
+<?php
+namespace TYPO3\CMS\Core\Http;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Psr\Http\Message\StreamInterface;
+
+/**
+ * Default implementation for the StreamInterface of the PSR-7 standard
+ * Acts mainly as a decorator class for streams/resources.
+ *
+ * Highly inspired by https://github.com/phly/http/
+ *
+ * @internal Note that this is not public API yet.
+ */
+class Stream implements StreamInterface {
+
+	/**
+	 * The actual PHP resource
+	 * @var resource
+	 */
+	protected $resource;
+
+	/**
+	 * @var string|resource
+	 */
+	protected $stream;
+
+	/**
+	 * Constructor setting up the PHP resource
+	 *
+	 * @param string|resource $stream
+	 * @param string $mode Mode with which to open stream
+	 * @throws \InvalidArgumentException
+	 */
+	public function __construct($stream, $mode = 'r') {
+		$this->stream = $stream;
+		if (is_resource($stream)) {
+			$this->resource = $stream;
+		} elseif (is_string($stream)) {
+			$this->resource = fopen($stream, $mode);
+		} else {
+			throw new \InvalidArgumentException('Invalid stream provided; must be a string stream identifier or resource', 1436717284);
+		}
+	}
+
+	/**
+	 * Reads all data from the stream into a string, from the beginning to end.
+	 *
+	 * This method MUST attempt to seek to the beginning of the stream before
+	 * reading data and read the stream until the end is reached.
+	 *
+	 * Warning: This could attempt to load a large amount of data into memory.
+	 *
+	 * This method MUST NOT raise an exception in order to conform with PHP's
+	 * string casting operations.
+	 *
+	 * @see http://php.net/manual/en/language.oop5.magic.php#object.tostring
+	 * @return string
+	 */
+	public function __toString() {
+		if (!$this->isReadable()) {
+			return '';
+		}
+		try {
+			$this->rewind();
+			return $this->getContents();
+		} catch (\RuntimeException $e) {
+			return '';
+		}
+	}
+
+	/**
+	 * Closes the stream and any underlying resources.
+	 *
+	 * @return void
+	 */
+	public function close() {
+		if (!$this->resource) {
+			return;
+		}
+		$resource = $this->detach();
+		fclose($resource);
+	}
+
+	/**
+	 * Separates any underlying resources from the stream.
+	 *
+	 * After the stream has been detached, the stream is in an unusable state.
+	 *
+	 * @return resource|null Underlying PHP stream, if any
+	 */
+	public function detach() {
+		$resource = $this->resource;
+		$this->resource = NULL;
+		return $resource;
+	}
+
+	/**
+	 * Get the size of the stream if known.
+	 *
+	 * @return int|null Returns the size in bytes if known, or null if unknown.
+	 */
+	public function getSize() {
+		if ($this->resource === NULL) {
+			return NULL;
+		}
+		$stats = fstat($this->resource);
+		return $stats['size'];
+	}
+
+	/**
+	 * Returns the current position of the file read/write pointer
+	 *
+	 * @return int Position of the file pointer
+	 * @throws \RuntimeException on error.
+	 */
+	public function tell() {
+		if (!$this->resource) {
+			throw new \RuntimeException('No resource available; cannot tell position', 1436717285);
+		}
+		$result = ftell($this->resource);
+		if (!is_int($result)) {
+			throw new \RuntimeException('Error occurred during tell operation', 1436717286);
+		}
+		return $result;
+	}
+
+	/**
+	 * Returns true if the stream is at the end of the stream.
+	 *
+	 * @return bool
+	 */
+	public function eof() {
+		if (!$this->resource) {
+			return TRUE;
+		}
+		return feof($this->resource);
+	}
+
+	/**
+	 * Returns whether or not the stream is seekable.
+	 *
+	 * @return bool
+	 */
+	public function isSeekable() {
+		if (!$this->resource) {
+			return FALSE;
+		}
+		return (bool)$this->getMetadata('seekable');
+	}
+
+	/**
+	 * Seek to a position in the stream.
+	 *
+	 * @link http://www.php.net/manual/en/function.fseek.php
+	 *
+	 * @param int $offset Stream offset
+	 * @param int $whence Specifies how the cursor position will be calculated
+	 *     based on the seek offset. Valid values are identical to the built-in
+	 *     PHP $whence values for `fseek()`.  SEEK_SET: Set position equal to
+	 *     offset bytes SEEK_CUR: Set position to current location plus offset
+	 *     SEEK_END: Set position to end-of-stream plus offset.
+	 *
+	 * @throws \RuntimeException on failure.
+	 */
+	public function seek($offset, $whence = SEEK_SET) {
+		if (!$this->resource) {
+			throw new \RuntimeException('No resource available; cannot seek position', 1436717287);
+		}
+
+		if (!$this->isSeekable()) {
+			throw new \RuntimeException('Stream is not seekable', 1436717288);
+		}
+		$result = fseek($this->resource, $offset, $whence);
+		if ($result !== 0) {
+			throw new \RuntimeException('Error seeking within stream', 1436717289);
+		}
+	}
+
+	/**
+	 * Seek to the beginning of the stream.
+	 *
+	 * If the stream is not seekable, this method will raise an exception;
+	 * otherwise, it will perform a seek(0).
+	 *
+	 * @see seek()
+	 * @link http://www.php.net/manual/en/function.fseek.php
+	 * @throws \RuntimeException on failure.
+	 */
+	public function rewind() {
+		$this->seek(0);
+	}
+
+	/**
+	 * Returns whether or not the stream is writable.
+	 *
+	 * @return bool
+	 */
+	public function isWritable() {
+		if (!$this->resource) {
+			return FALSE;
+		}
+		$uri = $this->getMetadata('uri');
+		return is_writable($uri);
+	}
+
+	/**
+	 * Write data to the stream.
+	 *
+	 * @param string $string The string that is to be written.
+	 * @return int Returns the number of bytes written to the stream.
+	 * @throws \RuntimeException on failure.
+	 */
+	public function write($string) {
+		if (!$this->resource) {
+			throw new \RuntimeException('No resource available; cannot write', 1436717290);
+		}
+		$result = fwrite($this->resource, $string);
+		if ($result === FALSE) {
+			throw new \RuntimeException('Error writing to stream', 1436717291);
+		}
+		return $result;
+	}
+
+	/**
+	 * Returns whether or not the stream is readable.
+	 *
+	 * @return bool
+	 */
+	public function isReadable() {
+		if (!$this->resource) {
+			return FALSE;
+		}
+		$mode = $this->getMetadata('mode');
+		return (strpos($mode, 'r') !== FALSE || strpos($mode, '+') !== FALSE);
+	}
+
+	/**
+	 * Read data from the stream.
+	 *
+	 * @param int $length Read up to $length bytes from the object and return
+	 *     them. Fewer than $length bytes may be returned if underlying stream
+	 *     call returns fewer bytes.
+	 * @return string Returns the data read from the stream, or an empty string
+	 *     if no bytes are available.
+	 * @throws \RuntimeException if an error occurs.
+	 */
+	public function read($length) {
+		if (!$this->resource) {
+			throw new \RuntimeException('No resource available; cannot read', 1436717292);
+		}
+		if (!$this->isReadable()) {
+			throw new \RuntimeException('Stream is not readable', 1436717293);
+		}
+		$result = fread($this->resource, $length);
+		if ($result === FALSE) {
+			throw new \RuntimeException('Error reading stream', 1436717294);
+		}
+		return $result;
+	}
+
+	/**
+	 * Returns the remaining contents in a string
+	 *
+	 * @return string
+	 * @throws \RuntimeException if unable to read or an error occurs while
+	 *     reading.
+	 */
+	public function getContents() {
+		if (!$this->isReadable()) {
+			return '';
+		}
+		$result = stream_get_contents($this->resource);
+		if ($result === FALSE) {
+			throw new \RuntimeException('Error reading from stream', 1436717295);
+		}
+		return $result;
+	}
+
+	/**
+	 * Get stream metadata as an associative array or retrieve a specific key.
+	 *
+	 * The keys returned are identical to the keys returned from PHP's
+	 * stream_get_meta_data() function.
+	 *
+	 * @link http://php.net/manual/en/function.stream-get-meta-data.php
+	 *
+	 * @param string $key Specific metadata to retrieve.
+	 *
+	 * @return array|mixed|null Returns an associative array if no key is
+	 *     provided. Returns a specific key value if a key is provided and the
+	 *     value is found, or null if the key is not found.
+	 */
+	public function getMetadata($key = NULL) {
+		$metadata = stream_get_meta_data($this->resource);
+		if ($key === NULL) {
+			return $metadata;
+		}
+		if (!isset($metadata[$key])) {
+			return NULL;
+		}
+		return $metadata[$key];
+	}
+
+	/**
+	 * Attach a new stream/resource to the instance.
+	 *
+	 * @param string|resource $resource
+	 * @param string $mode
+	 * @throws \InvalidArgumentException for stream identifier that cannot be cast to a resource
+	 * @throws \InvalidArgumentException for non-resource stream
+	 */
+	public function attach($resource, $mode = 'r') {
+		$error = NULL;
+		if (!is_resource($resource) && is_string($resource)) {
+			set_error_handler(function ($e) use (&$error) {
+				$error = $e;
+			}, E_WARNING);
+			$resource = fopen($resource, $mode);
+			restore_error_handler();
+		}
+		if ($error) {
+			throw new \InvalidArgumentException('Invalid stream reference provided', 1436717296);
+		}
+		if (!is_resource($resource)) {
+			throw new \InvalidArgumentException('Invalid stream provided; must be a string stream identifier or resource', 1436717297);
+		}
+		$this->resource = $resource;
+	}
+
+}
diff --git a/typo3/sysext/core/Classes/Http/UploadedFile.php b/typo3/sysext/core/Classes/Http/UploadedFile.php
new file mode 100644
index 0000000000000000000000000000000000000000..efeaf4e19d6e7f08d13232a2b0d985db0ea6356a
--- /dev/null
+++ b/typo3/sysext/core/Classes/Http/UploadedFile.php
@@ -0,0 +1,265 @@
+<?php
+namespace TYPO3\CMS\Core\Http;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Psr\Http\Message\StreamInterface;
+use Psr\Http\Message\UploadedFileInterface;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Class UploadedFile which represents one uploaded file, usually coming
+ * from $_FILES, according to PSR-7 standard.
+ *
+ * Highly inspired by https://github.com/phly/http/
+ *
+ * @internal Note that this is not public API yet.
+ */
+class UploadedFile implements UploadedFileInterface {
+
+	/**
+	 * @var NULL|string
+	 */
+	protected $file;
+
+	/**
+	 * @var NULL|StreamInterface
+	 */
+	protected $stream;
+
+	/**
+	 * @var string
+	 */
+	protected $clientFilename;
+
+	/**
+	 * @var string
+	 */
+	protected $clientMediaType;
+
+	/**
+	 * @var int
+	 */
+	protected $error;
+
+	/**
+	 * @var bool
+	 */
+	protected $moved = false;
+
+	/**
+	 * @var int
+	 */
+	protected $size;
+
+	/**
+	 * Constructor method
+	 *
+	 * @param string|resource $input is either a stream or a filename
+	 * @param int $size see $_FILES['size'] from PHP
+	 * @param int $errorStatus see $_FILES['error']
+	 * @param string $clientFilename the original filename handed over from the client
+	 * @param string $clientMediaType the media type (optional)
+	 *
+	 * @throws \InvalidArgumentException
+	 */
+	public function __construct($input, $size, $errorStatus, $clientFilename = NULL, $clientMediaType = NULL) {
+
+		if (is_string($input)) {
+			$this->file = $input;
+		}
+
+		if (is_resource($input)) {
+			$this->stream = new Stream($input);
+		} elseif ($input instanceof StreamInterface) {
+			$this->stream = $input;
+		}
+
+		if (!$this->file && !$this->stream) {
+			throw new \InvalidArgumentException('The input given was not a valid stream or file.', 1436717301);
+		}
+
+		if (!is_int($size)) {
+			throw new \InvalidArgumentException('The size provided for an uploaded file must be an integer.', 1436717302);
+		}
+		$this->size = $size;
+
+		if (!is_int($errorStatus) || 0 > $errorStatus || 8 < $errorStatus) {
+			throw new \InvalidArgumentException('Invalid error status for an uploaded file. See UPLOAD_ERR_* constant in PHP.', 1436717303);
+		}
+		$this->error = $errorStatus;
+
+		if ($clientFilename !== NULL && !is_string($clientFilename)) {
+			throw new \InvalidArgumentException('Invalid client filename provided for an uploaded file.', 1436717304);
+		}
+		$this->clientFilename = $clientFilename;
+
+		if ($clientMediaType !== NULL && !is_string($clientMediaType)) {
+			throw new \InvalidArgumentException('Invalid client media type provided for an uploaded file.', 1436717305);
+		}
+		$this->clientMediaType = $clientMediaType;
+	}
+
+	/**
+	 * Retrieve a stream representing the uploaded file.
+	 * Returns a StreamInterface instance, representing the uploaded file. The purpose of this method
+	 * is to allow utilizing native PHP stream functionality to manipulate the file upload, such as
+	 * stream_copy_to_stream() (though the result will need to be decorated in a native PHP stream wrapper
+	 * to work with such functions).
+	 *
+	 * If the moveTo() method has been called previously, this method raises an exception.
+	 *
+	 * @return StreamInterface Stream representation of the uploaded file.
+	 * @throws \RuntimeException in cases when no stream is available or can be created.
+	 */
+	public function getStream() {
+		if ($this->moved) {
+			throw new \RuntimeException('Cannot retrieve stream as it was moved.', 1436717306);
+		}
+
+		if ($this->stream instanceof StreamInterface) {
+			return $this->stream;
+		}
+
+		$this->stream = new Stream($this->file);
+		return $this->stream;
+	}
+
+	 /**
+	 * Move the uploaded file to a new location.
+	 *
+	 * Use this method as an alternative to move_uploaded_file(). This method is
+	 * guaranteed to work in both SAPI and non-SAPI environments.
+	 * Implementations must determine which environment they are in, and use the
+	 * appropriate method (move_uploaded_file(), rename(), or a stream
+	 * operation) to perform the operation.
+	 *
+	 * $targetPath may be an absolute path, or a relative path. If it is a
+	 * relative path, resolution should be the same as used by PHP's rename()
+	 * function.
+	 *
+	 * The original file or stream MUST be removed on completion.
+	 *
+	 * If this method is called more than once, any subsequent calls MUST raise
+	 * an exception.
+	 *
+	 * When used in an SAPI environment where $_FILES is populated, when writing
+	 * files via moveTo(), is_uploaded_file() and move_uploaded_file() SHOULD be
+	 * used to ensure permissions and upload status are verified correctly.
+	 *
+	 * If you wish to move to a stream, use getStream(), as SAPI operations
+	 * cannot guarantee writing to stream destinations.
+	 *
+	 * @see http://php.net/is_uploaded_file
+	 * @see http://php.net/move_uploaded_file
+	 * @param string $targetPath Path to which to move the uploaded file.
+	 * @throws \InvalidArgumentException if the $path specified is invalid.
+	 * @throws \RuntimeException on any error during the move operation, or on the second or subsequent call to the method.
+	 */
+	public function moveTo($targetPath) {
+		if (!is_string($targetPath) || empty($targetPath)) {
+			throw new \InvalidArgumentException('Invalid path while moving an uploaded file.', 1436717307);
+		}
+
+		if ($this->moved) {
+			throw new \RuntimeException('Cannot move uploaded file, as it was already moved.', 1436717308);
+		}
+
+		// Check if the target path is inside the allowed paths of TYPO3, and make it absolute.
+		$targetPath = GeneralUtility::getFileAbsFileName($targetPath);
+		if (empty($targetPath)) {
+			throw new \RuntimeException('Cannot move uploaded file, as it was already moved.', 1436717309);
+		}
+
+		if (!empty($this->file) && is_uploaded_file($this->file)) {
+			if (GeneralUtility::upload_copy_move($this->file, $targetPath . basename($this->file)) === FALSE) {
+				throw new \RuntimeException('An error occurred while moving uploaded file', 1436717310);
+			}
+		} elseif ($this->stream) {
+			$handle = fopen($targetPath, 'wb+');
+			if ($handle === FALSE) {
+				throw new \RuntimeException('Unable to write to target path.', 1436717311);
+			}
+
+			$this->stream->rewind();
+			while (!$this->stream->eof()) {
+				fwrite($handle, $this->stream->read(4096));
+			}
+
+			fclose($handle);
+		}
+
+		$this->moved = TRUE;
+	}
+
+	/**
+	 * Retrieve the file size.
+	 * Usually returns the value stored in the "size" key of
+	 * the file in the $_FILES array if available, as PHP calculates this based
+	 * on the actual size transmitted.
+	 *
+	 * @return int|NULL The file size in bytes or null if unknown.
+	 */
+	public function getSize() {
+		return $this->size;
+	}
+
+	/**
+	 * Retrieve the error associated with the uploaded file.
+	 * Usually returns the value stored in the "error" key of
+	 * the file in the $_FILES array.
+	 *
+	 * The return value MUST be one of PHP's UPLOAD_ERR_XXX constants.
+	 *
+	 * If the file was uploaded successfully, this method MUST return
+	 * UPLOAD_ERR_OK.
+	 *
+	 * @see http://php.net/manual/en/features.file-upload.errors.php
+	 * @return int One of PHP's UPLOAD_ERR_XXX constants.
+	 */
+	public function getError() {
+		return $this->error;
+	}
+
+	/**
+	 * Retrieve the filename sent by the client.
+	 * Usually returns the value stored in the "name" key of
+	 * the file in the $_FILES array.
+	 *
+	 * Do not trust the value returned by this method. A client could send
+	 * a malicious filename with the intention to corrupt or hack your
+	 * application.
+	 *
+	 * @return string|NULL The filename sent by the client or null if none was provided.
+	 */
+	public function getClientFilename() {
+		return $this->clientFilename;
+	}
+
+	/**
+	 * Retrieve the media type sent by the client.
+	 * Usually returns the value stored in the "type" key of
+	 * the file in the $_FILES array.
+	 *
+	 * Do not trust the value returned by this method. A client could send
+	 * a malicious media type with the intention to corrupt or hack your
+	 * application.
+	 *
+	 * @return string|NULL The media type sent by the client or null if none was provided.
+	 */
+	public function getClientMediaType() {
+		return $this->clientMediaType;
+	}
+
+}
diff --git a/typo3/sysext/core/Classes/Http/Uri.php b/typo3/sysext/core/Classes/Http/Uri.php
new file mode 100644
index 0000000000000000000000000000000000000000..98cd63dd13e3aebc5ab2712554ed5667bfc63493
--- /dev/null
+++ b/typo3/sysext/core/Classes/Http/Uri.php
@@ -0,0 +1,716 @@
+<?php
+namespace TYPO3\CMS\Core\Http;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Psr\Http\Message\UriInterface;
+
+/**
+ * Represents a URI based on the PSR-7 Standard.
+ *
+ * Highly inspired by https://github.com/phly/http/
+ *
+ * @internal Note that this is not public API yet.
+ */
+class Uri implements UriInterface {
+
+	/**
+	 * Sub-delimiters used in query strings and fragments.
+	 *
+	 * @const string
+	 */
+	const SUBDELIMITER_CHARLIST = '!\$&\'\(\)\*\+,;=';
+
+	/**
+	 * Unreserved characters used in paths, query strings, and fragments.
+	 *
+	 * @const string
+	 */
+	const UNRESERVED_CHARLIST = 'a-zA-Z0-9_\-\.~';
+
+	/**
+	 * The default scheme for the URI
+	 * @var string
+	 */
+	protected $scheme;
+
+	/**
+	 * @var int[] Associative array containing schemes and their default ports.
+	 */
+	protected $supportedSchemes = array(
+		'http'  => 80,
+		'https' => 443
+	);
+
+	/**
+	 * The authority part of the URI
+	 * @var string
+	 */
+	protected $authority = '';
+
+	/**
+	 * The userInfo part of the URI
+	 * @var string
+	 */
+	protected $userInfo = '';
+
+	/**
+	 * The host part of the URI
+	 * @var string
+	 */
+	protected $host = '';
+
+	/**
+	 * The port of the URI (empty if it is the standard port for the scheme)
+	 * @var int|NULL
+	 */
+	protected $port = NULL;
+
+	/**
+	 * The path part of the URI (can be empty or /)
+	 * @var string
+	 */
+	protected $path = '';
+
+	/**
+	 * The query part of the URI without the ?
+	 * @var string
+	 */
+	protected $query;
+
+	/**
+	 * The fragment part of the URI without the # before
+	 * @var string
+	 */
+	protected $fragment;
+
+	/**
+	 * @param string|null $uri The full URI including query string and fragment
+	 * @throws \InvalidArgumentException when the URI is not a string
+	 */
+	public function __construct($uri = '') {
+	   if (!is_string($uri)) {
+		   $argumentType = is_object($uri) ? get_class($uri) : gettype($uri);
+		   throw new \InvalidArgumentException('URI passed must be a string, but is of type "' . $argumentType . '"', 1436717320);
+		}
+		if (!empty($uri)) {
+			$this->parseUri($uri);
+		}
+	}
+
+	/**
+	 * helper function for parsing the full URI string
+	 * @param string $uri
+	 * @throws \InvalidArgumentException if the URI is malformed.
+	 */
+	protected function parseUri($uri) {
+		$uriParts = parse_url($uri);
+
+		if ($uriParts === FALSE) {
+			throw new \InvalidArgumentException('The parsedUri string appears to be malformed', 1436717322);
+		}
+
+		if (isset($uriParts['scheme'])) {
+			$this->scheme = $this->sanitizeScheme($uriParts['scheme']);
+		}
+
+		if (isset($uriParts['user'])) {
+			$this->userInfo = $uriParts['user'];
+			if (isset($uriParts['pass'])) {
+				$this->userInfo .= ':' . $uriParts['pass'];
+			}
+		}
+
+		if (isset($uriParts['host'])) {
+			$this->host = $uriParts['host'];
+		}
+
+		if (isset($uriParts['port'])) {
+			$this->port = (int)$uriParts['port'];
+		}
+
+		if (isset($uriParts['path'])) {
+			$this->path = $this->sanitizePath($uriParts['path']);
+		}
+
+		if (isset($uriParts['query'])) {
+			$this->query = $this->sanitizeQuery($uriParts['query']);
+		}
+
+		if (isset($uriParts['fragment'])) {
+			$this->fragment = $this->sanitizeFragment($uriParts['fragment']);
+		}
+	}
+
+	/**
+	 * Retrieve the scheme component of the URI.
+	 *
+	 * If no scheme is present, this method MUST return an empty string.
+	 *
+	 * The value returned MUST be normalized to lowercase, per RFC 3986
+	 * Section 3.1.
+	 *
+	 * The trailing ":" character is not part of the scheme and MUST NOT be
+	 * added.
+	 *
+	 * @see https://tools.ietf.org/html/rfc3986#section-3.1
+	 * @return string The URI scheme.
+	 */
+	public function getScheme() {
+		return $this->scheme;
+	}
+
+	/**
+	 * Retrieve the authority component of the URI.
+	 *
+	 * If no authority information is present, this method MUST return an empty
+	 * string.
+	 *
+	 * The authority syntax of the URI is:
+	 *
+	 * <pre>
+	 * [user-info@]host[:port]
+	 * </pre>
+	 *
+	 * If the port component is not set or is the standard port for the current
+	 * scheme, it SHOULD NOT be included.
+	 *
+	 * @see https://tools.ietf.org/html/rfc3986#section-3.2
+	 * @return string The URI authority, in "[user-info@]host[:port]" format.
+	 */
+	public function getAuthority() {
+		if (empty($this->host)) {
+			return '';
+		}
+
+		$authority = $this->host;
+		if (!empty($this->userInfo)) {
+			$authority = $this->userInfo . '@' . $authority;
+		}
+
+		if ($this->isNonStandardPort($this->scheme, $this->host, $this->port)) {
+			$authority .= ':' . $this->port;
+		}
+
+		return $authority;
+	}
+
+	/**
+	 * Retrieve the user information component of the URI.
+	 *
+	 * If no user information is present, this method MUST return an empty
+	 * string.
+	 *
+	 * If a user is present in the URI, this will return that value;
+	 * additionally, if the password is also present, it will be appended to the
+	 * user value, with a colon (":") separating the values.
+	 *
+	 * The trailing "@" character is not part of the user information and MUST
+	 * NOT be added.
+	 *
+	 * @return string The URI user information, in "username[:password]" format.
+	 */
+	public function getUserInfo() {
+		return $this->userInfo;
+	}
+
+	/**
+	 * Retrieve the host component of the URI.
+	 *
+	 * If no host is present, this method MUST return an empty string.
+	 *
+	 * The value returned MUST be normalized to lowercase, per RFC 3986
+	 * Section 3.2.2.
+	 *
+	 * @see http://tools.ietf.org/html/rfc3986#section-3.2.2
+	 * @return string The URI host.
+	 */
+	public function getHost() {
+		return $this->host;
+	}
+
+	/**
+	 * Retrieve the port component of the URI.
+	 *
+	 * If a port is present, and it is non-standard for the current scheme,
+	 * this method MUST return it as an integer. If the port is the standard port
+	 * used with the current scheme, this method SHOULD return null.
+	 *
+	 * If no port is present, and no scheme is present, this method MUST return
+	 * a null value.
+	 *
+	 * If no port is present, but a scheme is present, this method MAY return
+	 * the standard port for that scheme, but SHOULD return null.
+	 *
+	 * @return null|int The URI port.
+	 */
+	public function getPort() {
+		return $this->isNonStandardPort($this->scheme, $this->host, $this->port) ? $this->port : NULL;
+	}
+
+	/**
+	 * Retrieve the path component of the URI.
+	 *
+	 * The path can either be empty or absolute (starting with a slash) or
+	 * rootless (not starting with a slash). Implementations MUST support all
+	 * three syntaxes.
+	 *
+	 * Normally, the empty path "" and absolute path "/" are considered equal as
+	 * defined in RFC 7230 Section 2.7.3. But this method MUST NOT automatically
+	 * do this normalization because in contexts with a trimmed base path, e.g.
+	 * the front controller, this difference becomes significant. It's the task
+	 * of the user to handle both "" and "/".
+	 *
+	 * The value returned MUST be percent-encoded, but MUST NOT double-encode
+	 * any characters. To determine what characters to encode, please refer to
+	 * RFC 3986, Sections 2 and 3.3.
+	 *
+	 * As an example, if the value should include a slash ("/") not intended as
+	 * delimiter between path segments, that value MUST be passed in encoded
+	 * form (e.g., "%2F") to the instance.
+	 *
+	 * @see https://tools.ietf.org/html/rfc3986#section-2
+	 * @see https://tools.ietf.org/html/rfc3986#section-3.3
+	 * @return string The URI path.
+	 */
+	public function getPath() {
+		return $this->path;
+	}
+
+	/**
+	 * Retrieve the query string of the URI.
+	 *
+	 * If no query string is present, this method MUST return an empty string.
+	 *
+	 * The leading "?" character is not part of the query and MUST NOT be
+	 * added.
+	 *
+	 * The value returned MUST be percent-encoded, but MUST NOT double-encode
+	 * any characters. To determine what characters to encode, please refer to
+	 * RFC 3986, Sections 2 and 3.4.
+	 *
+	 * As an example, if a value in a key/value pair of the query string should
+	 * include an ampersand ("&") not intended as a delimiter between values,
+	 * that value MUST be passed in encoded form (e.g., "%26") to the instance.
+	 *
+	 * @see https://tools.ietf.org/html/rfc3986#section-2
+	 * @see https://tools.ietf.org/html/rfc3986#section-3.4
+	 * @return string The URI query string.
+	 */
+	public function getQuery() {
+		return $this->query;
+	}
+
+	/**
+	 * Retrieve the fragment component of the URI.
+	 *
+	 * If no fragment is present, this method MUST return an empty string.
+	 *
+	 * The leading "#" character is not part of the fragment and MUST NOT be
+	 * added.
+	 *
+	 * The value returned MUST be percent-encoded, but MUST NOT double-encode
+	 * any characters. To determine what characters to encode, please refer to
+	 * RFC 3986, Sections 2 and 3.5.
+	 *
+	 * @see https://tools.ietf.org/html/rfc3986#section-2
+	 * @see https://tools.ietf.org/html/rfc3986#section-3.5
+	 * @return string The URI fragment.
+	 */
+	public function getFragment() {
+		return $this->fragment;
+	}
+
+	/**
+	 * Return an instance with the specified scheme.
+	 *
+	 * This method MUST retain the state of the current instance, and return
+	 * an instance that contains the specified scheme.
+	 *
+	 * Implementations MUST support the schemes "http" and "https" case
+	 * insensitively, and MAY accommodate other schemes if required.
+	 *
+	 * An empty scheme is equivalent to removing the scheme.
+	 *
+	 * @param string $scheme The scheme to use with the new instance.
+	 *
+	 * @return self A new instance with the specified scheme.
+	 * @throws \InvalidArgumentException for invalid or unsupported schemes.
+	 */
+	public function withScheme($scheme) {
+		$scheme = $this->sanitizeScheme($scheme);
+
+		$clonedObject = clone $this;
+		$clonedObject->scheme = $scheme;
+		return $clonedObject;
+	}
+
+	/**
+	 * Return an instance with the specified user information.
+	 *
+	 * This method MUST retain the state of the current instance, and return
+	 * an instance that contains the specified user information.
+	 *
+	 * Password is optional, but the user information MUST include the
+	 * user; an empty string for the user is equivalent to removing user
+	 * information.
+	 *
+	 * @param string $user The user name to use for authority.
+	 * @param null|string $password The password associated with $user.
+	 *
+	 * @return self A new instance with the specified user information.
+	 */
+	public function withUserInfo($user, $password = NULL) {
+
+		$userInfo = $user;
+		if (!empty($password)) {
+			$userInfo .= ':' . $password;
+		}
+
+		$clonedObject = clone $this;
+		$clonedObject->userInfo = $userInfo;
+		return $clonedObject;
+	}
+
+	/**
+	 * Return an instance with the specified host.
+	 *
+	 * This method MUST retain the state of the current instance, and return
+	 * an instance that contains the specified host.
+	 *
+	 * An empty host value is equivalent to removing the host.
+	 *
+	 * @param string $host The hostname to use with the new instance.
+	 *
+	 * @return self A new instance with the specified host.
+	 * @throws \InvalidArgumentException for invalid hostnames.
+	 */
+	public function withHost($host) {
+		$clonedObject = clone $this;
+		$clonedObject->host = $host;
+		return $clonedObject;
+	}
+
+	/**
+	 * Return an instance with the specified port.
+	 *
+	 * This method MUST retain the state of the current instance, and return
+	 * an instance that contains the specified port.
+	 *
+	 * Implementations MUST raise an exception for ports outside the
+	 * established TCP and UDP port ranges.
+	 *
+	 * A null value provided for the port is equivalent to removing the port
+	 * information.
+	 *
+	 * @param null|int $port The port to use with the new instance; a null value
+	 *     removes the port information.
+	 *
+	 * @return self A new instance with the specified port.
+	 * @throws \InvalidArgumentException for invalid ports.
+	 */
+	public function withPort($port) {
+		if (\TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($port) === FALSE) {
+			$argumentType = is_object($port) ? get_class($port) : gettype($port);
+			throw new \InvalidArgumentException('Invalid port "' . $argumentType . '" specified, must be an integer.', 1436717324);
+		}
+
+		$port = (int)$port;
+		if ($port < 1 || $port > 65535) {
+			throw new \InvalidArgumentException('Invalid port "' . $port . '" specified, must be a valid TCP/UDP port.', 1436717326);
+		}
+
+		$clonedObject = clone $this;
+		$clonedObject->port = $port;
+		return $clonedObject;
+	}
+
+	/**
+	 * Return an instance with the specified path.
+	 *
+	 * This method MUST retain the state of the current instance, and return
+	 * an instance that contains the specified path.
+	 *
+	 * The path can either be empty or absolute (starting with a slash) or
+	 * rootless (not starting with a slash). Implementations MUST support all
+	 * three syntaxes.
+	 *
+	 * If the path is intended to be domain-relative rather than path relative then
+	 * it must begin with a slash ("/"). Paths not starting with a slash ("/")
+	 * are assumed to be relative to some base path known to the application or
+	 * consumer.
+	 *
+	 * Users can provide both encoded and decoded path characters.
+	 * Implementations ensure the correct encoding as outlined in getPath().
+	 *
+	 * @param string $path The path to use with the new instance.
+	 *
+	 * @return self A new instance with the specified path.
+	 * @throws \InvalidArgumentException for invalid paths.
+	 */
+	public function withPath($path) {
+		if (!is_string($path)) {
+			throw new \InvalidArgumentException('Invalid path provided. Must be of type string.', 1436717328);
+		}
+
+		if (strpos($path, '?') !== FALSE) {
+			throw new \InvalidArgumentException('Invalid path provided. Must not contain a query string.', 1436717330);
+		}
+
+		if (strpos($path, '#') !== FALSE) {
+			throw new \InvalidArgumentException('Invalid path provided; must not contain a URI fragment', 1436717332);
+		}
+
+		$path = $this->sanitizePath($path);
+		$clonedObject = clone $this;
+		$clonedObject->path = $path;
+		return $clonedObject;
+	}
+
+	/**
+	 * Return an instance with the specified query string.
+	 *
+	 * This method MUST retain the state of the current instance, and return
+	 * an instance that contains the specified query string.
+	 *
+	 * Users can provide both encoded and decoded query characters.
+	 * Implementations ensure the correct encoding as outlined in getQuery().
+	 *
+	 * An empty query string value is equivalent to removing the query string.
+	 *
+	 * @param string $query The query string to use with the new instance.
+	 *
+	 * @return self A new instance with the specified query string.
+	 * @throws \InvalidArgumentException for invalid query strings.
+	 */
+	public function withQuery($query) {
+		if (!is_string($query)) {
+			throw new \InvalidArgumentException('Query string must be a string.', 1436717334);
+		}
+
+		if (strpos($query, '#') !== FALSE) {
+			throw new \InvalidArgumentException('Query string must not include a URI fragment.', 1436717336);
+		}
+
+		$query = $this->sanitizeQuery($query);
+		$clonedObject = clone $this;
+		$clonedObject->query = $query;
+		return $clonedObject;
+	}
+
+	/**
+	 * Return an instance with the specified URI fragment.
+	 *
+	 * This method MUST retain the state of the current instance, and return
+	 * an instance that contains the specified URI fragment.
+	 *
+	 * Users can provide both encoded and decoded fragment characters.
+	 * Implementations ensure the correct encoding as outlined in getFragment().
+	 *
+	 * An empty fragment value is equivalent to removing the fragment.
+	 *
+	 * @param string $fragment The fragment to use with the new instance.
+	 *
+	 * @return self A new instance with the specified fragment.
+	 */
+	public function withFragment($fragment) {
+		$fragment = $this->sanitizeFragment($fragment);
+		$clonedObject = clone $this;
+		$clonedObject->fragment = $fragment;
+		return $clonedObject;
+	}
+
+	/**
+	 * Return the string representation as a URI reference.
+	 *
+	 * Depending on which components of the URI are present, the resulting
+	 * string is either a full URI or relative reference according to RFC 3986,
+	 * Section 4.1. The method concatenates the various components of the URI,
+	 * using the appropriate delimiters:
+	 *
+	 * - If a scheme is present, it MUST be suffixed by ":".
+	 * - If an authority is present, it MUST be prefixed by "//".
+	 * - The path can be concatenated without delimiters. But there are two
+	 *   cases where the path has to be adjusted to make the URI reference
+	 *   valid as PHP does not allow to throw an exception in __toString():
+	 *     - If the path is rootless and an authority is present, the path MUST
+	 *       be prefixed by "/".
+	 *     - If the path is starting with more than one "/" and no authority is
+	 *       present, the starting slashes MUST be reduced to one.
+	 * - If a query is present, it MUST be prefixed by "?".
+	 * - If a fragment is present, it MUST be prefixed by "#".
+	 *
+	 * @see http://tools.ietf.org/html/rfc3986#section-4.1
+	 * @return string
+	 */
+	public function __toString() {
+		$uri = '';
+
+		if (!empty($this->scheme)) {
+			$uri .= $this->scheme . '://';
+		}
+
+		$authority = $this->getAuthority();
+		if (!empty($authority)) {
+			$uri .= $authority;
+		}
+
+		$path = $this->getPath();
+		if (!empty($path)) {
+			$uri .= '/' . ltrim($path, '/');
+		}
+
+		if ($this->query) {
+			$uri .= '?' . $this->query;
+		}
+		if ($this->fragment) {
+			$uri .= '#' . $this->fragment;
+		}
+		return $uri;
+	}
+
+	/**
+	 * Is a given port non-standard for the current scheme?
+	 *
+	 * @param string $scheme
+	 * @param string $host
+	 * @param int $port
+	 * @return bool
+	 */
+	protected function isNonStandardPort($scheme, $host, $port) {
+		if (empty($scheme)) {
+			return TRUE;
+		}
+
+		if (empty($host) || empty($port)) {
+			return FALSE;
+		}
+
+		return !isset($this->supportedSchemes[$scheme]) || $port !== $this->supportedSchemes[$scheme];
+	}
+
+	/**
+	 * Filters the scheme to ensure it is a valid scheme.
+	 *
+	 * @param string $scheme Scheme name.
+	 *
+	 * @return string Filtered scheme.
+	 * @throws \InvalidArgumentException when a scheme is given which is not supported
+	 */
+	protected function sanitizeScheme($scheme) {
+		$scheme = strtolower($scheme);
+		$scheme = preg_replace('#:(//)?$#', '', $scheme);
+
+		if (empty($scheme)) {
+			return '';
+		}
+
+		if (!array_key_exists($scheme, $this->supportedSchemes)) {
+			throw new \InvalidArgumentException('Unsupported scheme "' . $scheme . '"; must be any empty string or in the set (' . implode(', ', array_keys($this->supportedSchemes)) . ')', 1436717338);
+		}
+
+		return $scheme;
+	}
+
+	/**
+	 * Filters the path of a URI to ensure it is properly encoded.
+	 *
+	 * @param string $path
+	 * @return string
+	 */
+	protected function sanitizePath($path) {
+		return preg_replace_callback(
+			'/(?:[^' . self::UNRESERVED_CHARLIST . ':@&=\+\$,\/;%]+|%(?![A-Fa-f0-9]{2}))/',
+			function($matches) {
+				return rawurlencode($matches[0]);
+			},
+			$path
+		);
+	}
+
+	/**
+	 * Filter a query string to ensure it is propertly encoded.
+	 *
+	 * Ensures that the values in the query string are properly urlencoded.
+	 *
+	 * @param string $query
+	 * @return string
+	 */
+	protected function sanitizeQuery($query) {
+		if (!empty($query) && strpos($query, '?') === 0) {
+			$query = substr($query, 1);
+		}
+
+		$parts = explode('&', $query);
+		foreach ($parts as $index => $part) {
+			list($key, $value) = $this->splitQueryValue($part);
+			if ($value === NULL) {
+				$parts[$index] = $this->sanitizeQueryOrFragment($key);
+				continue;
+			}
+			$parts[$index] = $this->sanitizeQueryOrFragment($key) . '=' . $this->sanitizeQueryOrFragment($value);
+		}
+
+		return implode('&', $parts);
+	}
+
+	/**
+	 * Split a query value into a key/value tuple.
+	 *
+	 * @param string $value
+	 * @return array A value with exactly two elements, key and value
+	 */
+	protected function splitQueryValue($value) {
+		$data = explode('=', $value, 2);
+		if (count($data) === 1) {
+			$data[] = NULL;
+		}
+		return $data;
+	}
+
+	/**
+	 * Filter a fragment value to ensure it is properly encoded.
+	 *
+	 * @param null|string $fragment
+	 * @return string
+	 */
+	protected function sanitizeFragment($fragment) {
+		if ($fragment === NULL) {
+			$fragment = '';
+		}
+
+		if (!empty($fragment) && strpos($fragment, '#') === 0) {
+			$fragment = substr($fragment, 1);
+		}
+
+		return $this->sanitizeQueryOrFragment($fragment);
+	}
+
+	/**
+	 * Filter a query string key or value, or a fragment.
+	 *
+	 * @param string $value
+	 * @return string
+	 */
+	protected function sanitizeQueryOrFragment($value) {
+		return preg_replace_callback(
+			'/(?:[^' . self::UNRESERVED_CHARLIST . self::SUBDELIMITER_CHARLIST . '%:@\/\?]+|%(?![A-Fa-f0-9]{2}))/',
+			function($matches) {
+				return rawurlencode($matches[0]);
+			},
+			$value
+		);
+	}
+
+}
diff --git a/typo3/sysext/core/Tests/Unit/Http/MessageTest.php b/typo3/sysext/core/Tests/Unit/Http/MessageTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..75938b0cc63a252253d32f9bb8925947ce1ce927
--- /dev/null
+++ b/typo3/sysext/core/Tests/Unit/Http/MessageTest.php
@@ -0,0 +1,302 @@
+<?php
+namespace TYPO3\CMS\Core\Tests\Unit\Http;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Http\Stream;
+use TYPO3\CMS\Core\Http\Message;
+
+/**
+ * Testcase for \TYPO3\CMS\Core\Http\Message
+ *
+ * Adapted from https://github.com/phly/http/
+ */
+class MessageTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
+
+	/**
+	 * @var Stream
+	 */
+	protected $stream;
+
+	/**
+	 * @var Message
+	 */
+	protected $message;
+
+	public function setUp() {
+		$this->stream = new Stream('php://memory', 'wb+');
+		$this->message = (new Message())->withBody($this->stream);
+	}
+
+	/**
+	 * @test
+	 */
+	public function protocolHasAcceptableDefault() {
+		$this->assertEquals('1.1', $this->message->getProtocolVersion());
+	}
+
+	/**
+	 * @test
+	 */
+	public function protocolMutatorReturnsCloneWithChanges() {
+		$message = $this->message->withProtocolVersion('1.0');
+		$this->assertNotSame($this->message, $message);
+		$this->assertEquals('1.0', $message->getProtocolVersion());
+	}
+
+	/**
+	 * @test
+	 */
+	public function usesStreamProvidedInConstructorAsBody() {
+		$this->assertSame($this->stream, $this->message->getBody());
+	}
+
+	/**
+	 * @test
+	 */
+	public function bodyMutatorReturnsCloneWithChanges() {
+		$stream = new Stream('php://memory', 'wb+');
+		$message = $this->message->withBody($stream);
+		$this->assertNotSame($this->message, $message);
+		$this->assertSame($stream, $message->getBody());
+	}
+
+	/**
+	 * @test
+	 */
+	public function getHeaderReturnsHeaderValueAsArray() {
+		$message = $this->message->withHeader('X-Foo', ['Foo', 'Bar']);
+		$this->assertNotSame($this->message, $message);
+		$this->assertEquals(['Foo', 'Bar'], $message->getHeader('X-Foo'));
+	}
+
+	/**
+	 * @test
+	 */
+	public function getHeaderLineReturnsHeaderValueAsCommaConcatenatedString() {
+		$message = $this->message->withHeader('X-Foo', ['Foo', 'Bar']);
+		$this->assertNotSame($this->message, $message);
+		$this->assertEquals('Foo,Bar', $message->getHeaderLine('X-Foo'));
+	}
+
+	/**
+	 * @test
+	 */
+	public function getHeadersKeepsHeaderCaseSensitivity() {
+		$message = $this->message->withHeader('X-Foo', ['Foo', 'Bar']);
+		$this->assertNotSame($this->message, $message);
+		$this->assertEquals(['X-Foo' => ['Foo', 'Bar']], $message->getHeaders());
+	}
+
+	/**
+	 * @test
+	 */
+	public function getHeadersReturnsCaseWithWhichHeaderFirstRegistered() {
+		$message = $this->message
+			->withHeader('X-Foo', 'Foo')
+			->withAddedHeader('x-foo', 'Bar');
+		$this->assertNotSame($this->message, $message);
+		$this->assertEquals(['X-Foo' => ['Foo', 'Bar']], $message->getHeaders());
+	}
+
+	/**
+	 * @test
+	 */
+	public function hasHeaderReturnsFalseIfHeaderIsNotPresent() {
+		$this->assertFalse($this->message->hasHeader('X-Foo'));
+	}
+
+	/**
+	 * @test
+	 */
+	public function hasHeaderReturnsTrueIfHeaderIsPresent() {
+		$message = $this->message->withHeader('X-Foo', 'Foo');
+		$this->assertNotSame($this->message, $message);
+		$this->assertTrue($message->hasHeader('X-Foo'));
+	}
+
+	/**
+	 * @test
+	 */
+	public function addHeaderAppendsToExistingHeader() {
+		$message = $this->message->withHeader('X-Foo', 'Foo');
+		$this->assertNotSame($this->message, $message);
+		$message2 = $message->withAddedHeader('X-Foo', 'Bar');
+		$this->assertNotSame($message, $message2);
+		$this->assertEquals('Foo,Bar', $message2->getHeaderLine('X-Foo'));
+	}
+
+
+	/**
+	 * @test
+	 */
+	public function canRemoveHeaders() {
+		$message = $this->message->withHeader('X-Foo', 'Foo');
+		$this->assertNotSame($this->message, $message);
+		$this->assertTrue($message->hasHeader('x-foo'));
+		$message2 = $message->withoutHeader('x-foo');
+		$this->assertNotSame($this->message, $message2);
+		$this->assertNotSame($message, $message2);
+		$this->assertFalse($message2->hasHeader('X-Foo'));
+	}
+
+	/**
+	 * @test
+	 */
+	public function headerRemovalIsCaseInsensitive() {
+		$message = $this->message
+			->withHeader('X-Foo', 'Foo')
+			->withAddedHeader('x-foo', 'Bar')
+			->withAddedHeader('X-FOO', 'Baz');
+		$this->assertNotSame($this->message, $message);
+		$this->assertTrue($message->hasHeader('x-foo'));
+		$message2 = $message->withoutHeader('x-foo');
+		$this->assertNotSame($this->message, $message2);
+		$this->assertNotSame($message, $message2);
+		$this->assertFalse($message2->hasHeader('X-Foo'));
+		$headers = $message2->getHeaders();
+		$this->assertEquals(0, count($headers));
+	}
+
+	/**
+	 * @return array
+	 */
+	public function invalidGeneralHeaderValuesDataProvider() {
+		return [
+			'null'   => [NULL],
+			'true'   => [TRUE],
+			'false'  => [FALSE],
+			'int'    => [1],
+			'float'  => [1.1],
+			'array'  => [['foo' => ['bar']]],
+			'object' => [(object) ['foo' => 'bar']],
+		];
+	}
+
+	/**
+	 * @dataProvider invalidGeneralHeaderValuesDataProvider
+	 */
+	public function testWithHeaderRaisesExceptionForInvalidNestedHeaderValue($value) {
+		$this->setExpectedException('InvalidArgumentException', 'Invalid header value');
+		$message = $this->message->withHeader('X-Foo', [$value]);
+	}
+
+	/**
+	 * @return array
+	 */
+	public function invalidHeaderValuesDataProvider() {
+		return [
+			'null'   => [NULL],
+			'true'   => [TRUE],
+			'false'  => [FALSE],
+			'int'    => [1],
+			'float'  => [1.1],
+			'object' => [(object) ['foo' => 'bar']],
+		];
+	}
+
+	/**
+	 * @dataProvider invalidHeaderValuesDataProvider
+	 */
+	public function withHeaderRaisesExceptionForInvalidValueType($value) {
+		$this->setExpectedException('InvalidArgumentException', 'Invalid header value');
+		$message = $this->message->withHeader('X-Foo', $value);
+	}
+
+	/**
+	 * @dataProvider invalidHeaderValuesDataProvider
+	 */
+	public function withAddedHeaderRaisesExceptionForNonStringNonArrayValue($value) {
+		$this->setExpectedException('InvalidArgumentException', 'must be a string');
+		$message = $this->message->withAddedHeader('X-Foo', $value);
+	}
+
+	/**
+	 * @test
+	 */
+	public function withoutHeaderDoesNothingIfHeaderDoesNotExist() {
+		$this->assertFalse($this->message->hasHeader('X-Foo'));
+		$message = $this->message->withoutHeader('X-Foo');
+		$this->assertNotSame($this->message, $message);
+		$this->assertFalse($message->hasHeader('X-Foo'));
+	}
+
+	/**
+	 * @test
+	 */
+	public function getHeaderReturnsAnEmptyArrayWhenHeaderDoesNotExist() {
+		$this->assertSame([], $this->message->getHeader('X-Foo-Bar'));
+	}
+
+	/**
+	 * @test
+	 */
+	public function getHeaderLineReturnsEmptyStringWhenHeaderDoesNotExist() {
+		$this->assertSame('', $this->message->getHeaderLine('X-Foo-Bar'));
+	}
+
+	/**
+	 * @return array
+	 */
+	public function headersWithInjectionVectorsDataProvider() {
+		return [
+			'name-with-cr'           => ["X-Foo\r-Bar", 'value'],
+			'name-with-lf'           => ["X-Foo\n-Bar", 'value'],
+			'name-with-crlf'         => ["X-Foo\r\n-Bar", 'value'],
+			'name-with-2crlf'        => ["X-Foo\r\n\r\n-Bar", 'value'],
+			'value-with-cr'          => ['X-Foo-Bar', "value\rinjection"],
+			'value-with-lf'          => ['X-Foo-Bar', "value\ninjection"],
+			'value-with-crlf'        => ['X-Foo-Bar', "value\r\ninjection"],
+			'value-with-2crlf'       => ['X-Foo-Bar', "value\r\n\r\ninjection"],
+			'array-value-with-cr'    => ['X-Foo-Bar', ["value\rinjection"]],
+			'array-value-with-lf'    => ['X-Foo-Bar', ["value\ninjection"]],
+			'array-value-with-crlf'  => ['X-Foo-Bar', ["value\r\ninjection"]],
+			'array-value-with-2crlf' => ['X-Foo-Bar', ["value\r\n\r\ninjection"]],
+		];
+	}
+
+	/**
+	 * @dataProvider headersWithInjectionVectorsDataProvider
+	 * @test
+	 */
+	public function doesNotAllowCRLFInjectionWhenCallingWithHeader($name, $value) {
+		$this->setExpectedException('InvalidArgumentException');
+		$this->message->withHeader($name, $value);
+	}
+
+	/**
+	 * @dataProvider headersWithInjectionVectorsDataProvider
+	 * @test
+	 */
+	public function doesNotAllowCRLFInjectionWhenCallingWithAddedHeader($name, $value) {
+		$this->setExpectedException('InvalidArgumentException');
+		$this->message->withAddedHeader($name, $value);
+	}
+
+	/**
+	 * @test
+	 */
+	public function testWithHeaderAllowsHeaderContinuations() {
+		$message = $this->message->withHeader('X-Foo-Bar', "value,\r\n second value");
+		$this->assertEquals("value,\r\n second value", $message->getHeaderLine('X-Foo-Bar'));
+	}
+
+	/**
+	 * @test
+	 */
+	public function testWithAddedHeaderAllowsHeaderContinuations() {
+		$message = $this->message->withAddedHeader('X-Foo-Bar', "value,\r\n second value");
+		$this->assertEquals("value,\r\n second value", $message->getHeaderLine('X-Foo-Bar'));
+	}
+}
diff --git a/typo3/sysext/core/Tests/Unit/Http/RequestTest.php b/typo3/sysext/core/Tests/Unit/Http/RequestTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..7d9fd08311fd722bf5c0e37ee2dca3687b671a77
--- /dev/null
+++ b/typo3/sysext/core/Tests/Unit/Http/RequestTest.php
@@ -0,0 +1,453 @@
+<?php
+namespace TYPO3\CMS\Core\Tests\Unit\Http;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Http\Uri;
+use TYPO3\CMS\Core\Http\Request;
+use TYPO3\CMS\Core\Http\Stream;
+
+/**
+ * Testcase for \TYPO3\CMS\Core\Http\Request
+ *
+ * Adapted from https://github.com/phly/http/
+ */
+class RequestTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
+
+	/**
+	 * @var Request
+	 */
+	protected $request;
+
+	public function setUp() {
+		$this->request = new Request();
+	}
+
+	/**
+	 * @test
+	 */
+	public function getMethodIsGetByDefault() {
+		$this->assertEquals('GET', $this->request->getMethod());
+	}
+
+	/**
+	 * @test
+	 */
+	public function getMethodMutatorReturnsCloneWithChangedMethod() {
+		$request = $this->request->withMethod('GET');
+		$this->assertNotSame($this->request, $request);
+		$this->assertEquals('GET', $request->getMethod());
+	}
+
+	/**
+	 * @test
+	 */
+	public function getUriIsNullByDefault() {
+		$this->assertNull($this->request->getUri());
+	}
+
+	/**
+	 * @test
+	 */
+	public function constructorRaisesExceptionForInvalidStream() {
+		$this->setExpectedException('InvalidArgumentException');
+		new Request(['TOTALLY INVALID']);
+	}
+
+	/**
+	 * @test
+	 */
+	public function withUriReturnsNewInstanceWithNewUri() {
+		$request = $this->request->withUri(new Uri('https://example.com:10082/foo/bar?baz=bat'));
+		$this->assertNotSame($this->request, $request);
+		$request2 = $request->withUri(new Uri('/baz/bat?foo=bar'));
+		$this->assertNotSame($this->request, $request2);
+		$this->assertNotSame($request, $request2);
+		$this->assertEquals('/baz/bat?foo=bar', (string) $request2->getUri());
+	}
+
+	/**
+	 * @test
+	 */
+	public function constructorCanAcceptAllMessageParts() {
+		$uri = new Uri('http://example.com/');
+		$body = new Stream('php://memory');
+		$headers = [
+			'x-foo' => ['bar'],
+		];
+		$request = new Request(
+			$uri,
+			'POST',
+			$body,
+			$headers
+		);
+
+		$this->assertSame($uri, $request->getUri());
+		$this->assertEquals('POST', $request->getMethod());
+		$this->assertSame($body, $request->getBody());
+		$testHeaders = $request->getHeaders();
+		foreach ($headers as $key => $value) {
+			$this->assertArrayHasKey($key, $testHeaders);
+			$this->assertEquals($value, $testHeaders[$key]);
+		}
+	}
+
+	/**
+	 * @return array
+	 */
+	public function invalidRequestUriDataProvider() {
+		return [
+			'true'     => [TRUE],
+			'false'    => [FALSE],
+			'int'      => [1],
+			'float'    => [1.1],
+			'array'    => [['http://example.com']],
+			'stdClass' => [(object) ['href' => 'http://example.com']],
+		];
+	}
+
+	/**
+	 * @dataProvider invalidRequestUriDataProvider
+	 * @test
+	 */
+	public function constructorRaisesExceptionForInvalidUri($uri) {
+		$this->setExpectedException('InvalidArgumentException', 'Invalid URI');
+		new Request($uri);
+	}
+
+	/**
+	 * @return array
+	 */
+	public function invalidRequestMethodDataProvider() {
+		return [
+			'true'       => [TRUE],
+			'false'      => [FALSE],
+			'int'        => [1],
+			'float'      => [1.1],
+			'bad-string' => ['BOGUS-METHOD'],
+			'array'      => [['POST']],
+			'stdClass'   => [(object) ['method' => 'POST']],
+		];
+	}
+
+	/**
+	 * @dataProvider invalidRequestMethodDataProvider
+	 * @test
+	 */
+	public function constructorRaisesExceptionForInvalidMethod($method) {
+		$this->setExpectedException('InvalidArgumentException', 'Unsupported HTTP method');
+		new Request(NULL, $method);
+	}
+
+	/**
+	 * @return array
+	 */
+	public function invalidRequestBodyDataProvider() {
+		return [
+			'true'     => [TRUE],
+			'false'    => [FALSE],
+			'int'      => [1],
+			'float'    => [1.1],
+			'array'    => [['BODY']],
+			'stdClass' => [(object) ['body' => 'BODY']],
+		];
+	}
+
+	/**
+	 * @dataProvider invalidRequestBodyDataProvider
+	 * @test
+	 */
+	public function constructorRaisesExceptionForInvalidBody($body) {
+		$this->setExpectedException('InvalidArgumentException', 'stream');
+		new Request(NULL, NULL, $body);
+	}
+
+	/**
+	 * @test
+	 */
+	public function constructorIgnoresInvalidHeaders() {
+		$headers = [
+			['INVALID'],
+			'x-invalid-null'   => NULL,
+			'x-invalid-true'   => TRUE,
+			'x-invalid-false'  => FALSE,
+			'x-invalid-int'    => 1,
+			'x-invalid-object' => (object) ['INVALID'],
+			'x-valid-string'   => 'VALID',
+			'x-valid-array'    => ['VALID'],
+		];
+		$expected = [
+			'x-valid-string' => ['VALID'],
+			'x-valid-array'  => ['VALID'],
+		];
+		$request = new Request(NULL, NULL, 'php://memory', $headers);
+		$this->assertEquals($expected, $request->getHeaders());
+	}
+
+	/**
+	 * @test
+	 */
+	public function getRequestTargetIsSlashWhenNoUriPresent() {
+		$request = new Request();
+		$this->assertEquals('/', $request->getRequestTarget());
+	}
+
+	/**
+	 * @test
+	 */
+	public function getRequestTargetIsSlashWhenUriHasNoPathOrQuery() {
+		$request = (new Request())
+			->withUri(new Uri('http://example.com'));
+		$this->assertEquals('/', $request->getRequestTarget());
+	}
+
+	/**
+	 * @return array
+	 */
+	public function requestsWithUriDataProvider() {
+		return [
+			'absolute-uri'            => [
+				(new Request())
+					->withUri(new Uri('https://api.example.com/user'))
+					->withMethod('POST'),
+				'/user'
+			],
+			'absolute-uri-with-query' => [
+				(new Request())
+					->withUri(new Uri('https://api.example.com/user?foo=bar'))
+					->withMethod('POST'),
+				'/user?foo=bar'
+			],
+			'relative-uri'            => [
+				(new Request())
+					->withUri(new Uri('/user'))
+					->withMethod('GET'),
+				'/user'
+			],
+			'relative-uri-with-query' => [
+				(new Request())
+					->withUri(new Uri('/user?foo=bar'))
+					->withMethod('GET'),
+				'/user?foo=bar'
+			],
+		];
+	}
+
+	/**
+	 * @dataProvider requestsWithUriDataProvider
+	 * @test
+	 */
+	public function getRequestTargetWhenUriIsPresent($request, $expected) {
+		$this->assertEquals($expected, $request->getRequestTarget());
+	}
+
+	/**
+	 * @return array
+	 */
+	public function validRequestTargetsDataProvider() {
+		return [
+			'asterisk-form'         => ['*'],
+			'authority-form'        => ['api.example.com'],
+			'absolute-form'         => ['https://api.example.com/users'],
+			'absolute-form-query'   => ['https://api.example.com/users?foo=bar'],
+			'origin-form-path-only' => ['/users'],
+			'origin-form'           => ['/users?id=foo'],
+		];
+	}
+
+	/**
+	 * @dataProvider validRequestTargetsDataProvider
+	 * @test
+	 */
+	public function getRequestTargetCanProvideARequestTarget($requestTarget) {
+		$request = (new Request())->withRequestTarget($requestTarget);
+		$this->assertEquals($requestTarget, $request->getRequestTarget());
+	}
+
+	/**
+	 * @test
+	 */
+	public function withRequestTargetCannotContainWhitespace() {
+		$request = new Request();
+		$this->setExpectedException('InvalidArgumentException', 'Invalid request target');
+		$request->withRequestTarget('foo bar baz');
+	}
+
+	/**
+	 * @test
+	 */
+	public function getRequestTargetDoesNotCacheBetweenInstances() {
+		$request = (new Request())->withUri(new Uri('https://example.com/foo/bar'));
+		$original = $request->getRequestTarget();
+		$newRequest = $request->withUri(new Uri('http://mwop.net/bar/baz'));
+		$this->assertNotEquals($original, $newRequest->getRequestTarget());
+	}
+
+	/**
+	 * @test
+	 */
+	public function getRequestTargetIsResetWithNewUri() {
+		$request = (new Request())->withUri(new Uri('https://example.com/foo/bar'));
+		$original = $request->getRequestTarget();
+		$newRequest = $request->withUri(new Uri('http://mwop.net/bar/baz'));
+	}
+
+	/**
+	 * @test
+	 */
+	public function getHeadersContainsHostHeaderIfUriWithHostIsPresent() {
+		$request = new Request('http://example.com');
+		$headers = $request->getHeaders();
+		$this->assertArrayHasKey('host', $headers);
+		$this->assertContains('example.com', $headers['host']);
+	}
+
+	/**
+	 * @test
+	 */
+	public function getHeadersContainsNoHostHeaderIfNoUriPresent() {
+		$request = new Request();
+		$headers = $request->getHeaders();
+		$this->assertArrayNotHasKey('host', $headers);
+	}
+
+	/**
+	 * @test
+	 */
+	public function getHeadersContainsNoHostHeaderIfUriDoesNotContainHost() {
+		$request = new Request(new Uri());
+		$headers = $request->getHeaders();
+		$this->assertArrayNotHasKey('host', $headers);
+	}
+
+	/**
+	 * @test
+	 */
+	public function getHeaderWithHostReturnsUriHostWhenPresent() {
+		$request = new Request('http://example.com');
+		$header = $request->getHeader('host');
+		$this->assertEquals(array('example.com'), $header);
+	}
+
+	/**
+	 * @test
+	 */
+	public function getHeaderWithHostReturnsEmptyArrayIfNoUriPresent() {
+		$request = new Request();
+		$this->assertSame([], $request->getHeader('host'));
+	}
+
+	/**
+	 * @test
+	 */
+	public function getHeaderWithHostReturnsEmptyArrayIfUriDoesNotContainHost() {
+		$request = new Request(new Uri());
+		$this->assertSame([], $request->getHeader('host'));
+	}
+
+	/**
+	 * @test
+	 */
+	public function getHeaderLineWithHostReturnsUriHostWhenPresent() {
+		$request = new Request('http://example.com');
+		$header = $request->getHeaderLine('host');
+		$this->assertContains('example.com', $header);
+	}
+
+	/**
+	 * @test
+	 */
+	public function getHeaderLineWithHostReturnsEmptyStringIfNoUriPresent() {
+		$request = new Request();
+		$this->assertSame('', $request->getHeaderLine('host'));
+	}
+
+	/**
+	 * @test
+	 */
+	public function getHeaderLineWithHostReturnsEmptyStringIfUriDoesNotContainHost() {
+		$request = new Request(new Uri());
+		$this->assertSame('', $request->getHeaderLine('host'));
+	}
+
+	/**
+	 * @test
+	 */
+	public function getHeaderLineWithHostTakesPrecedenceOverModifiedUri() {
+		$request = (new Request())
+			->withAddedHeader('Host', 'example.com');
+
+		$uri = (new Uri())->withHost('www.example.com');
+		$new = $request->withUri($uri, TRUE);
+
+		$this->assertEquals('example.com', $new->getHeaderLine('Host'));
+	}
+
+	/**
+	 * @test
+	 */
+	public function getHeaderLineWithHostTakesPrecedenceOverEmptyUri() {
+		$request = (new Request())
+			->withAddedHeader('Host', 'example.com');
+
+		$uri = new Uri();
+		$new = $request->withUri($uri);
+
+		$this->assertEquals('example.com', $new->getHeaderLine('Host'));
+	}
+
+	/**
+	 * @test
+	 */
+	public function getHeaderLineWithHostDoesNotTakePrecedenceOverHostWithPortFromUri() {
+		$request = (new Request())
+			->withAddedHeader('Host', 'example.com');
+
+		$uri = (new Uri())
+			->withHost('www.example.com')
+			->withPort(10081);
+		$new = $request->withUri($uri);
+
+		$this->assertEquals('www.example.com:10081', $new->getHeaderLine('Host'));
+	}
+
+	/**
+	 * @return array
+	 */
+	public function headersWithInjectionVectorsDataProvider() {
+		return [
+			'name-with-cr'           => ["X-Foo\r-Bar", 'value'],
+			'name-with-lf'           => ["X-Foo\n-Bar", 'value'],
+			'name-with-crlf'         => ["X-Foo\r\n-Bar", 'value'],
+			'name-with-2crlf'        => ["X-Foo\r\n\r\n-Bar", 'value'],
+			'value-with-cr'          => ['X-Foo-Bar', "value\rinjection"],
+			'value-with-lf'          => ['X-Foo-Bar', "value\ninjection"],
+			'value-with-crlf'        => ['X-Foo-Bar', "value\r\ninjection"],
+			'value-with-2crlf'       => ['X-Foo-Bar', "value\r\n\r\ninjection"],
+			'array-value-with-cr'    => ['X-Foo-Bar', ["value\rinjection"]],
+			'array-value-with-lf'    => ['X-Foo-Bar', ["value\ninjection"]],
+			'array-value-with-crlf'  => ['X-Foo-Bar', ["value\r\ninjection"]],
+			'array-value-with-2crlf' => ['X-Foo-Bar', ["value\r\n\r\ninjection"]],
+		];
+	}
+
+	/**
+	 * @test
+	 * @dataProvider headersWithInjectionVectorsDataProvider
+	 */
+	public function constructorRaisesExceptionForHeadersWithCRLFVectors($name, $value) {
+		$this->setExpectedException('InvalidArgumentException');
+		$request = new Request(NULL, NULL, 'php://memory', [$name => $value]);
+	}
+}
diff --git a/typo3/sysext/core/Tests/Unit/Http/ResponseTest.php b/typo3/sysext/core/Tests/Unit/Http/ResponseTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..4e4d153afedb3cbfbad553abdfa1642d54538641
--- /dev/null
+++ b/typo3/sysext/core/Tests/Unit/Http/ResponseTest.php
@@ -0,0 +1,214 @@
+<?php
+namespace TYPO3\CMS\Core\Tests\Unit\Http;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Http\Response;
+use TYPO3\CMS\Core\Http\Stream;
+
+/**
+ * Testcase for \TYPO3\CMS\Core\Http\Response
+ *
+ * Adapted from https://github.com/phly/http/
+ */
+class ResponseTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
+
+	/**
+	 * @var Response
+	 */
+	protected $response;
+
+	public function setUp() {
+		$this->response = new Response();
+	}
+
+	/**
+	 * @test
+	 */
+	public function testStatusCodeIs200ByDefault() {
+		$this->assertEquals(200, $this->response->getStatusCode());
+	}
+
+	/**
+	 * @test
+	 */
+	public function testStatusCodeMutatorReturnsCloneWithChanges() {
+		$response = $this->response->withStatus(400);
+		$this->assertNotSame($this->response, $response);
+		$this->assertEquals(400, $response->getStatusCode());
+	}
+
+	/**
+	 * @return array
+	 */
+	public function invalidStatusCodesDataProvider() {
+		return [
+			'too-low'  => [99],
+			'too-high' => [600],
+			'null'     => [NULL],
+			'bool'     => [TRUE],
+			'string'   => ['foo'],
+			'array'    => [[200]],
+			'object'   => [(object) [200]],
+		];
+	}
+
+	/**
+	 * @dataProvider invalidStatusCodesDataProvider
+	 * @test
+	 */
+	public function testCannotSetInvalidStatusCode($code) {
+		$this->setExpectedException('InvalidArgumentException');
+		$response = $this->response->withStatus($code);
+	}
+
+	/**
+	 * @test
+	 */
+	public function testReasonPhraseDefaultsToStandards() {
+		$response = $this->response->withStatus(422);
+		$this->assertEquals('Unprocessable Entity', $response->getReasonPhrase());
+	}
+
+	/**
+	 * @test
+	 */
+	public function testCanSetCustomReasonPhrase() {
+		$response = $this->response->withStatus(422, 'Foo Bar!');
+		$this->assertEquals('Foo Bar!', $response->getReasonPhrase());
+	}
+
+	/**
+	 * @test
+	 */
+	public function testConstructorRaisesExceptionForInvalidStream() {
+		$this->setExpectedException('InvalidArgumentException');
+		new Response(['TOTALLY INVALID']);
+	}
+
+	/**
+	 * @test
+	 */
+	public function testConstructorCanAcceptAllMessageParts() {
+		$body = new Stream('php://memory');
+		$status = 302;
+		$headers = [
+			'location' => ['http://example.com/'],
+		];
+
+		$response = new Response($body, $status, $headers);
+		$this->assertSame($body, $response->getBody());
+		$this->assertEquals(302, $response->getStatusCode());
+		$this->assertEquals($headers, $response->getHeaders());
+	}
+
+	/**
+	 * @return array
+	 */
+	public function invalidStatusDataProvider() {
+		return [
+			'true'       => [TRUE],
+			'false'      => [FALSE],
+			'float'      => [100.1],
+			'bad-string' => ['Two hundred'],
+			'array'      => [[200]],
+			'object'     => [(object) ['statusCode' => 200]],
+			'too-small'  => [1],
+			'too-big'    => [600],
+		];
+	}
+
+	/**
+	 * @dataProvider invalidStatusDataProvider
+	 * @test
+	 */
+	public function testConstructorRaisesExceptionForInvalidStatus($code) {
+		$this->setExpectedException('InvalidArgumentException', 'The given status code is not a valid HTTP status code.');
+		new Response('php://memory', $code);
+	}
+
+	/**
+	 * @return array
+	 */
+	public function invalidResponseBodyDataProvider() {
+		return [
+			'true'     => [TRUE],
+			'false'    => [FALSE],
+			'int'      => [1],
+			'float'    => [1.1],
+			'array'    => [['BODY']],
+			'stdClass' => [(object) ['body' => 'BODY']],
+		];
+	}
+
+	/**
+	 * @dataProvider invalidResponseBodyDataProvider
+	 * @test
+	 */
+	public function testConstructorRaisesExceptionForInvalidBody($body) {
+		$this->setExpectedException('InvalidArgumentException', 'stream');
+		new Response($body);
+	}
+
+	/**
+	 * @test
+	 */
+	public function constructorIgonoresInvalidHeaders() {
+		$headers = [
+			['INVALID'],
+			'x-invalid-null'   => NULL,
+			'x-invalid-true'   => TRUE,
+			'x-invalid-false'  => FALSE,
+			'x-invalid-int'    => 1,
+			'x-invalid-object' => (object) ['INVALID'],
+			'x-valid-string'   => 'VALID',
+			'x-valid-array'    => ['VALID'],
+		];
+		$expected = [
+			'x-valid-string' => ['VALID'],
+			'x-valid-array'  => ['VALID'],
+		];
+		$response = new Response('php://memory', 200, $headers);
+		$this->assertEquals($expected, $response->getHeaders());
+	}
+
+	/**
+	 * @return array
+	 */
+	public function headersWithInjectionVectorsDataProvider() {
+		return [
+			'name-with-cr'           => ["X-Foo\r-Bar", 'value'],
+			'name-with-lf'           => ["X-Foo\n-Bar", 'value'],
+			'name-with-crlf'         => ["X-Foo\r\n-Bar", 'value'],
+			'name-with-2crlf'        => ["X-Foo\r\n\r\n-Bar", 'value'],
+			'value-with-cr'          => ['X-Foo-Bar', "value\rinjection"],
+			'value-with-lf'          => ['X-Foo-Bar', "value\ninjection"],
+			'value-with-crlf'        => ['X-Foo-Bar', "value\r\ninjection"],
+			'value-with-2crlf'       => ['X-Foo-Bar', "value\r\n\r\ninjection"],
+			'array-value-with-cr'    => ['X-Foo-Bar', ["value\rinjection"]],
+			'array-value-with-lf'    => ['X-Foo-Bar', ["value\ninjection"]],
+			'array-value-with-crlf'  => ['X-Foo-Bar', ["value\r\ninjection"]],
+			'array-value-with-2crlf' => ['X-Foo-Bar', ["value\r\n\r\ninjection"]],
+		];
+	}
+
+	/**
+	 * @test
+	 * @dataProvider headersWithInjectionVectorsDataProvider
+	 */
+	public function cnstructorRaisesExceptionForHeadersWithCRLFVectors($name, $value) {
+		$this->setExpectedException('InvalidArgumentException');
+		$request = new Response('php://memory', 200, [$name => $value]);
+	}
+}
diff --git a/typo3/sysext/core/Tests/Unit/Http/ServerRequestFactoryTest.php b/typo3/sysext/core/Tests/Unit/Http/ServerRequestFactoryTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..88a5213d29baf771cbfcff19d4a853a77d9580dc
--- /dev/null
+++ b/typo3/sysext/core/Tests/Unit/Http/ServerRequestFactoryTest.php
@@ -0,0 +1,84 @@
+<?php
+namespace TYPO3\CMS\Core\Tests\Unit\Http;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Http\ServerRequestFactory;
+use TYPO3\CMS\Core\Http\UploadedFile;
+
+/**
+ * Testcase for \TYPO3\CMS\Core\Http\ServerRequestFactory
+ */
+class ServerRequestFactoryTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
+
+	/**
+	 * @test
+	 */
+	public function uploadedFilesAreNormalizedFromFilesSuperGlobal() {
+		$_SERVER['HTTP_HOST'] = 'localhost';
+		$_SERVER['REQUEST_URI'] = '/index.php';
+		$_FILES = array(
+			'tx_uploadexample_piexample' => array(
+				'name' => array(
+					'newExample' => array(
+						'image' => 'o51pb.jpg',
+						'imageCollection' => array(
+							0 => 'composer.json',
+						),
+					),
+					),
+					'type' => array(
+						'newExample' => array(
+							'image' => 'image/jpeg',
+							'imageCollection' => array(
+								0 => 'application/json'
+							)
+						)
+					),
+					'tmp_name' => array(
+						'newExample' => array(
+							'image' => '/Applications/MAMP/tmp/php/phphXdbcd',
+							'imageCollection' => array(
+								0 => '/Applications/MAMP/tmp/php/phpgrZ4bb'
+							)
+						)
+					),
+					'error' => array(
+						'newExample' => array(
+								'image' => 0,
+								'imageCollection' => array(
+									0 => 0
+								)
+						)
+					),
+					'size' => array(
+						'newExample' => array(
+							'image' => 59065,
+							'imageCollection' => array(
+								0 => 683
+							)
+						)
+					)
+			)
+		);
+
+		$uploadedFiles = ServerRequestFactory::fromGlobals()->getUploadedFiles();
+
+		$this->assertNotEmpty($uploadedFiles['tx_uploadexample_piexample']['newExample']['image']);
+		$this->assertTrue($uploadedFiles['tx_uploadexample_piexample']['newExample']['image'] instanceof UploadedFile);
+		$this->assertNotEmpty($uploadedFiles['tx_uploadexample_piexample']['newExample']['imageCollection'][0]);
+		$this->assertTrue($uploadedFiles['tx_uploadexample_piexample']['newExample']['imageCollection'][0] instanceof UploadedFile);
+	}
+
+}
diff --git a/typo3/sysext/core/Tests/Unit/Http/ServerRequestTest.php b/typo3/sysext/core/Tests/Unit/Http/ServerRequestTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..2d729819e236d3ef39332b88b51abd0cc3b33673
--- /dev/null
+++ b/typo3/sysext/core/Tests/Unit/Http/ServerRequestTest.php
@@ -0,0 +1,175 @@
+<?php
+namespace TYPO3\CMS\Core\Tests\Unit\Http;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Http\Uri;
+use TYPO3\CMS\Core\Http\ServerRequest;
+use TYPO3\CMS\Core\Http\UploadedFile;
+
+/**
+ * Testcase for \TYPO3\CMS\Core\Http\ServerRequest
+ *
+ * Adapted from https://github.com/phly/http/
+ */
+class ServerRequestTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
+
+	/**
+	 * @var ServerRequest
+	 */
+	protected $request;
+
+	public function setUp() {
+		$this->request = new ServerRequest();
+	}
+
+	/**
+	 * @test
+	 */
+	public function getServerParamsAreEmptyByDefault() {
+		$this->assertEmpty($this->request->getServerParams());
+	}
+
+	/**
+	 * @test
+	 */
+	public function getQueryParamsAreEmptyByDefault() {
+		$this->assertEmpty($this->request->getQueryParams());
+	}
+
+	/**
+	 * @test
+	 */
+	public function withQueryParamsMutatorReturnsCloneWithChanges() {
+		$value = ['foo' => 'bar'];
+		$request = $this->request->withQueryParams($value);
+		$this->assertNotSame($this->request, $request);
+		$this->assertEquals($value, $request->getQueryParams());
+	}
+
+	/**
+	 * @test
+	 */
+	public function getCookieParamsAreEmptyByDefault() {
+		$this->assertEmpty($this->request->getCookieParams());
+	}
+
+	/**
+	 * @test
+	 */
+	public function withCookieParamsMutatorReturnsCloneWithChanges() {
+		$value = ['foo' => 'bar'];
+		$request = $this->request->withCookieParams($value);
+		$this->assertNotSame($this->request, $request);
+		$this->assertEquals($value, $request->getCookieParams());
+	}
+
+	/**
+	 * @test
+	 */
+	public function getUploadedFilesAreEmptyByDefault() {
+		$this->assertEmpty($this->request->getUploadedFiles());
+	}
+
+	/**
+	 * @test
+	 */
+	public function getParsedBodyIsEmptyByDefault() {
+		$this->assertEmpty($this->request->getParsedBody());
+	}
+
+	/**
+	 * @test
+	 */
+	public function withParsedBodyMutatorReturnsCloneWithChanges() {
+		$value = ['foo' => 'bar'];
+		$request = $this->request->withParsedBody($value);
+		$this->assertNotSame($this->request, $request);
+		$this->assertEquals($value, $request->getParsedBody());
+	}
+
+	/**
+	 * @test
+	 */
+	public function getAttributesAreEmptyByDefault() {
+		$this->assertEmpty($this->request->getAttributes());
+	}
+
+	/**
+	 * @depends testAttributesAreEmptyByDefault
+	 * @test
+	 */
+	public function withAttributeMutatorReturnsCloneWithChanges() {
+		$request = $this->request->withAttribute('foo', 'bar');
+		$this->assertNotSame($this->request, $request);
+		$this->assertEquals('bar', $request->getAttribute('foo'));
+
+		return $request;
+	}
+
+	/**
+	 * @depends testAttributeMutatorReturnsCloneWithChanges
+	 * @test
+	 */
+	public function withoutAttributeReturnsCloneWithoutAttribute($request) {
+		$new = $request->withoutAttribute('foo');
+		$this->assertNotSame($request, $new);
+		$this->assertNull($new->getAttribute('foo', NULL));
+	}
+
+	/**
+	 * @test
+	 */
+	public function constructorUsesProvidedArguments() {
+		$server = [
+			'foo' => 'bar',
+			'baz' => 'bat',
+		];
+
+		$server['server'] = TRUE;
+
+		$files = [
+			'files' => new UploadedFile('php://temp', 0, 0),
+		];
+
+		$uri = new Uri('http://example.com');
+		$method = 'POST';
+		$headers = [
+			'host' => ['example.com'],
+		];
+
+		$request = new ServerRequest(
+			$uri,
+			$method,
+			'php://memory',
+			$headers,
+			$server,
+			$files
+		);
+
+		$this->assertEquals($server, $request->getServerParams());
+		$this->assertEquals($files, $request->getUploadedFiles());
+
+		$this->assertSame($uri, $request->getUri());
+		$this->assertEquals($method, $request->getMethod());
+		$this->assertEquals($headers, $request->getHeaders());
+
+		$body = $request->getBody();
+		$r = new \ReflectionProperty($body, 'stream');
+		$r->setAccessible(TRUE);
+		$stream = $r->getValue($body);
+		$this->assertEquals('php://memory', $stream);
+	}
+
+}
diff --git a/typo3/sysext/core/Tests/Unit/Http/StreamTest.php b/typo3/sysext/core/Tests/Unit/Http/StreamTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..2ffd0a7fd8e08fb4b6ea2cc1d1fc7b7ca9de9091
--- /dev/null
+++ b/typo3/sysext/core/Tests/Unit/Http/StreamTest.php
@@ -0,0 +1,536 @@
+<?php
+namespace TYPO3\CMS\Core\Tests\Unit\Http;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Http\Stream;
+
+/**
+ * Testcase for \TYPO3\CMS\Core\Http\StreamTest
+ *
+ * Adapted from https://github.com/phly/http/
+ */
+class StreamTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
+
+	/**
+	 * @var Stream
+	 */
+	protected $stream;
+
+	public function setUp() {
+		$this->stream = new Stream('php://memory', 'wb+');
+	}
+
+	/**
+	 * @test
+	 */
+	public function canInstantiateWithStreamIdentifier() {
+		$this->assertInstanceOf(Stream::class, $this->stream);
+	}
+
+	/**
+	 * @test
+	 */
+	public function canInstantiteWithStreamResource() {
+		$resource = fopen('php://memory', 'wb+');
+		$stream = new Stream($resource);
+		$this->assertInstanceOf(Stream::class, $stream);
+	}
+
+	/**
+	 * @test
+	 */
+	public function isReadableReturnsFalseIfStreamIsNotReadable() {
+		$fileName = PATH_site . 'typo3temp/' . $this->getUniqueId('test_');
+		touch($fileName);
+		$this->testFilesToDelete[] = $fileName;
+		$stream = new Stream($fileName, 'w');
+		$this->assertFalse($stream->isReadable());
+	}
+
+	/**
+	 * @test
+	 */
+	public function isWritableReturnsFalseIfStreamIsNotWritable() {
+		$stream = new Stream('php://memory', 'r');
+		$this->assertFalse($stream->isWritable());
+	}
+
+	/**
+	 * @test
+	 */
+	public function toStringRetrievesFullContentsOfStream() {
+		$message = 'foo bar';
+		$this->stream->write($message);
+		$this->assertEquals($message, (string) $this->stream);
+	}
+
+	/**
+	 * @test
+	 */
+	public function detachReturnsResource() {
+		$resource = fopen('php://memory', 'wb+');
+		$stream = new Stream($resource);
+		$this->assertSame($resource, $stream->detach());
+	}
+
+	/**
+	 * @test
+	 */
+	public function constructorRaisesExceptionWhenPassingInvalidStreamResource() {
+		$this->setExpectedException('InvalidArgumentException');
+		$stream = new Stream(['  THIS WILL NOT WORK  ']);
+	}
+
+	/**
+	 * @test
+	 */
+	public function toStringSerializationReturnsEmptyStringWhenStreamIsNotReadable() {
+		$fileName = PATH_site . 'typo3temp/' . $this->getUniqueId('test_');
+		touch($fileName);
+		$this->testFilesToDelete[] = $fileName;
+		file_put_contents($fileName, 'FOO BAR');
+		$stream = new Stream($fileName, 'w');
+
+		$this->assertEquals('', $stream->__toString());
+	}
+
+	/**
+	 * @test
+	 */
+	public function closeClosesResource() {
+		$fileName = PATH_site . 'typo3temp/' . $this->getUniqueId('test_');
+		touch($fileName);
+		$this->testFilesToDelete[] = $fileName;
+		$resource = fopen($fileName, 'wb+');
+		$stream = new Stream($resource);
+		$stream->close();
+		$this->assertFalse(is_resource($resource));
+	}
+
+	/**
+	 * @test
+	 */
+	public function closeUnsetsResource() {
+		$fileName = PATH_site . 'typo3temp/' . $this->getUniqueId('test_');
+		touch($fileName);
+		$this->testFilesToDelete[] = $fileName;
+		$resource = fopen($fileName, 'wb+');
+		$stream = new Stream($resource);
+		$stream->close();
+
+		$this->assertNull($stream->detach());
+	}
+
+	/**
+	 * @test
+	 */
+	public function closeDoesNothingAfterDetach() {
+		$fileName = PATH_site . 'typo3temp/' . $this->getUniqueId('test_');
+		touch($fileName);
+		$this->testFilesToDelete[] = $fileName;
+		$resource = fopen($fileName, 'wb+');
+		$stream = new Stream($resource);
+		$detached = $stream->detach();
+
+		$stream->close();
+		$this->assertTrue(is_resource($detached));
+		$this->assertSame($resource, $detached);
+	}
+
+	/**
+	 * @test
+	 */
+	public function getSizeReportsNullWhenNoResourcePresent() {
+		$this->stream->detach();
+		$this->assertNull($this->stream->getSize());
+	}
+
+	/**
+	 * @test
+	 */
+	public function tellReportsCurrentPositionInResource() {
+		$fileName = PATH_site . 'typo3temp/' . $this->getUniqueId('test_');
+		$this->testFilesToDelete[] = $fileName;
+		file_put_contents($fileName, 'FOO BAR');
+		$resource = fopen($fileName, 'wb+');
+		$stream = new Stream($resource);
+
+		fseek($resource, 2);
+
+		$this->assertEquals(2, $stream->tell());
+	}
+
+	/**
+	 * @test
+	 */
+	public function tellRaisesExceptionIfResourceIsDetached() {
+		$fileName = PATH_site . 'typo3temp/' . $this->getUniqueId('test_');
+		$this->testFilesToDelete[] = $fileName;
+		file_put_contents($fileName, 'FOO BAR');
+		$resource = fopen($fileName, 'wb+');
+		$stream = new Stream($resource);
+
+		fseek($resource, 2);
+		$stream->detach();
+		$this->setExpectedException('RuntimeException', 'No resource');
+		$stream->tell();
+	}
+
+	/**
+	 * @test
+	 */
+	public function eofReportsFalseWhenNotAtEndOfStream() {
+		$fileName = PATH_site . 'typo3temp/' . $this->getUniqueId('test_');
+		$this->testFilesToDelete[] = $fileName;
+		file_put_contents($fileName, 'FOO BAR');
+		$resource = fopen($fileName, 'wb+');
+		$stream = new Stream($resource);
+
+		fseek($resource, 2);
+		$this->assertFalse($stream->eof());
+	}
+
+	/**
+	 * @test
+	 */
+	public function eofReportsTrueWhenAtEndOfStream() {
+		$fileName = PATH_site . 'typo3temp/' . $this->getUniqueId('test_');
+		$this->testFilesToDelete[] = $fileName;
+		file_put_contents($fileName, 'FOO BAR');
+		$resource = fopen($fileName, 'wb+');
+		$stream = new Stream($resource);
+
+		while (!feof($resource)) {
+			fread($resource, 4096);
+		}
+		$this->assertTrue($stream->eof());
+	}
+
+	/**
+	 * @test
+	 */
+	public function eofReportsTrueWhenStreamIsDetached() {
+		$fileName = PATH_site . 'typo3temp/' . $this->getUniqueId('test_');
+		$this->testFilesToDelete[] = $fileName;
+		file_put_contents($fileName, 'FOO BAR');
+		$resource = fopen($fileName, 'wb+');
+		$stream = new Stream($resource);
+
+		fseek($resource, 2);
+		$stream->detach();
+		$this->assertTrue($stream->eof());
+	}
+
+	/**
+	 * @test
+	 */
+	public function isSeekableReturnsTrueForReadableStreams() {
+		$fileName = PATH_site . 'typo3temp/' . $this->getUniqueId('test_');
+		$this->testFilesToDelete[] = $fileName;
+		file_put_contents($fileName, 'FOO BAR');
+		$resource = fopen($fileName, 'wb+');
+		$stream = new Stream($resource);
+		$this->assertTrue($stream->isSeekable());
+	}
+
+	/**
+	 * @test
+	 */
+	public function isSeekableReturnsFalseForDetachedStreams() {
+		$fileName = PATH_site . 'typo3temp/' . $this->getUniqueId('test_');
+		$this->testFilesToDelete[] = $fileName;
+		file_put_contents($fileName, 'FOO BAR');
+		$resource = fopen($fileName, 'wb+');
+		$stream = new Stream($resource);
+		$stream->detach();
+		$this->assertFalse($stream->isSeekable());
+	}
+
+	/**
+	 * @test
+	 */
+	public function seekAdvancesToGivenOffsetOfStream() {
+		$fileName = PATH_site . 'typo3temp/' . $this->getUniqueId('test_');
+		$this->testFilesToDelete[] = $fileName;
+		file_put_contents($fileName, 'FOO BAR');
+		$resource = fopen($fileName, 'wb+');
+		$stream = new Stream($resource);
+		$stream->seek(2);
+		$this->assertEquals(2, $stream->tell());
+	}
+
+	/**
+	 * @test
+	 */
+	public function rewindResetsToStartOfStream() {
+		$fileName = PATH_site . 'typo3temp/' . $this->getUniqueId('test_');
+		$this->testFilesToDelete[] = $fileName;
+		file_put_contents($fileName, 'FOO BAR');
+		$resource = fopen($fileName, 'wb+');
+		$stream = new Stream($resource);
+		$stream->seek(2);
+		$stream->rewind();
+		$this->assertEquals(0, $stream->tell());
+	}
+
+	/**
+	 * @test
+	 */
+	public function seekRaisesExceptionWhenStreamIsDetached() {
+		$fileName = PATH_site . 'typo3temp/' . $this->getUniqueId('test_');
+		$this->testFilesToDelete[] = $fileName;
+		file_put_contents($fileName, 'FOO BAR');
+		$resource = fopen($fileName, 'wb+');
+		$stream = new Stream($resource);
+		$stream->detach();
+		$this->setExpectedException('RuntimeException', 'No resource');
+		$stream->seek(2);
+	}
+
+	/**
+	 * @test
+	 */
+	public function isWritableReturnsFalseWhenStreamIsDetached() {
+		$fileName = PATH_site . 'typo3temp/' . $this->getUniqueId('test_');
+		$this->testFilesToDelete[] = $fileName;
+		file_put_contents($fileName, 'FOO BAR');
+		$resource = fopen($fileName, 'wb+');
+		$stream = new Stream($resource);
+		$stream->detach();
+		$this->assertFalse($stream->isWritable());
+	}
+
+	/**
+	 * @test
+	 */
+	public function writeRaisesExceptionWhenStreamIsDetached() {
+		$fileName = PATH_site . 'typo3temp/' . $this->getUniqueId('test_');
+		$this->testFilesToDelete[] = $fileName;
+		file_put_contents($fileName, 'FOO BAR');
+		$resource = fopen($fileName, 'wb+');
+		$stream = new Stream($resource);
+		$stream->detach();
+		$this->setExpectedException('RuntimeException', 'No resource');
+		$stream->write('bar');
+	}
+
+	/**
+	 * @test
+	 */
+	public function isReadableReturnsFalseWhenStreamIsDetached() {
+		$fileName = PATH_site . 'typo3temp/' . $this->getUniqueId('test_');
+		$this->testFilesToDelete[] = $fileName;
+		file_put_contents($fileName, 'FOO BAR');
+		$resource = fopen($fileName, 'wb+');
+		$stream = new Stream($resource);
+		$stream->detach();
+		$this->assertFalse($stream->isReadable());
+	}
+
+	/**
+	 * @test
+	 */
+	public function readRaisesExceptionWhenStreamIsDetached() {
+		$fileName = PATH_site . 'typo3temp/' . $this->getUniqueId('test_');
+		$this->testFilesToDelete[] = $fileName;
+		file_put_contents($fileName, 'FOO BAR');
+		$resource = fopen($fileName, 'r');
+		$stream = new Stream($resource);
+		$stream->detach();
+		$this->setExpectedException('RuntimeException', 'No resource');
+		$stream->read(4096);
+	}
+
+	/**
+	 * @test
+	 */
+	public function readReturnsEmptyStringWhenAtEndOfFile() {
+		$fileName = PATH_site . 'typo3temp/' . $this->getUniqueId('test_');
+		$this->testFilesToDelete[] = $fileName;
+		file_put_contents($fileName, 'FOO BAR');
+		$resource = fopen($fileName, 'r');
+		$stream = new Stream($resource);
+		while (!feof($resource)) {
+			fread($resource, 4096);
+		}
+		$this->assertEquals('', $stream->read(4096));
+	}
+
+	/**
+	 * @test
+	 */
+	public function getContentsReturnsEmptyStringIfStreamIsNotReadable() {
+		$fileName = PATH_site . 'typo3temp/' . $this->getUniqueId('test_');
+		$this->testFilesToDelete[] = $fileName;
+		file_put_contents($fileName, 'FOO BAR');
+		$resource = fopen($fileName, 'w');
+		$stream = new Stream($resource);
+		$this->assertEquals('', $stream->getContents());
+	}
+
+	/**
+	 * @return array
+	 */
+	public function invalidResourcesDataProvider() {
+		$fileName = tempnam(sys_get_temp_dir(), 'PHLY');
+		$this->testFilesToDelete[] = $fileName;
+
+		return [
+			'null'                => [NULL],
+			'false'               => [FALSE],
+			'true'                => [TRUE],
+			'int'                 => [1],
+			'float'               => [1.1],
+			'string-non-resource' => ['foo-bar-baz'],
+			'array'               => [[fopen($fileName, 'r+')]],
+			'object'              => [(object) ['resource' => fopen($fileName, 'r+')]],
+		];
+	}
+
+	/**
+	 * @dataProvider invalidResourcesDataProvider
+	 * @test
+	 */
+	public function attachWithNonStringNonResourceRaisesException($resource) {
+		$this->setExpectedException('InvalidArgumentException', 'Invalid stream');
+		$this->stream->attach($resource);
+	}
+
+	/**
+	 * @test
+	 */
+	public function attachWithResourceAttachesResource() {
+		$fileName = PATH_site . 'typo3temp/' . $this->getUniqueId('test_');
+		touch($fileName);
+		$this->testFilesToDelete[] = $fileName;
+		$resource = fopen($fileName, 'r+');
+		$this->stream->attach($resource);
+
+		$r = new \ReflectionProperty($this->stream, 'resource');
+		$r->setAccessible(TRUE);
+		$test = $r->getValue($this->stream);
+		$this->assertSame($resource, $test);
+	}
+
+	/**
+	 * @test
+	 */
+	public function attachWithStringRepresentingResourceCreatesAndAttachesResource() {
+		$fileName = PATH_site . 'typo3temp/' . $this->getUniqueId('test_');
+		touch($fileName);
+		$this->testFilesToDelete[] = $fileName;
+		$this->stream->attach($fileName);
+
+		$resource = fopen($fileName, 'r+');
+		fwrite($resource, 'FooBar');
+
+		$this->stream->rewind();
+		$test = (string) $this->stream;
+		$this->assertEquals('FooBar', $test);
+	}
+
+	/**
+	 * @test
+	 */
+	public function getContentsShouldGetFullStreamContents() {
+		$fileName = PATH_site . 'typo3temp/' . $this->getUniqueId('test_');
+		touch($fileName);
+		$this->testFilesToDelete[] = $fileName;
+		$resource = fopen($fileName, 'r+');
+		$this->stream->attach($resource);
+
+		fwrite($resource, 'FooBar');
+
+		// rewind, because current pointer is at end of stream!
+		$this->stream->rewind();
+		$test = $this->stream->getContents();
+		$this->assertEquals('FooBar', $test);
+	}
+
+	/**
+	 * @test
+	 */
+	public function getContentsShouldReturnStreamContentsFromCurrentPointer() {
+		$fileName = PATH_site . 'typo3temp/' . $this->getUniqueId('test_');
+		touch($fileName);
+		$this->testFilesToDelete[] = $fileName;
+		$resource = fopen($fileName, 'r+');
+		$this->stream->attach($resource);
+
+		fwrite($resource, 'FooBar');
+
+		// seek to position 3
+		$this->stream->seek(3);
+		$test = $this->stream->getContents();
+		$this->assertEquals('Bar', $test);
+	}
+
+	/**
+	 * @test
+	 */
+	public function getMetadataReturnsAllMetadataWhenNoKeyPresent() {
+		$fileName = PATH_site . 'typo3temp/' . $this->getUniqueId('test_');
+		touch($fileName);
+		$this->testFilesToDelete[] = $fileName;
+		$resource = fopen($fileName, 'r+');
+		$this->stream->attach($resource);
+
+		$expected = stream_get_meta_data($resource);
+		$test = $this->stream->getMetadata();
+
+		$this->assertEquals($expected, $test);
+	}
+
+	/**
+	 * @test
+	 */
+	public function getMetadataReturnsDataForSpecifiedKey() {
+		$fileName = PATH_site . 'typo3temp/' . $this->getUniqueId('test_');
+		touch($fileName);
+		$this->testFilesToDelete[] = $fileName;
+		$resource = fopen($fileName, 'r+');
+		$this->stream->attach($resource);
+
+		$metadata = stream_get_meta_data($resource);
+		$expected = $metadata['uri'];
+
+		$test = $this->stream->getMetadata('uri');
+
+		$this->assertEquals($expected, $test);
+	}
+
+	/**
+	 * @test
+	 */
+	public function getMetadataReturnsNullIfNoDataExistsForKey() {
+		$fileName = PATH_site . 'typo3temp/' . $this->getUniqueId('test_');
+		touch($fileName);
+		$this->testFilesToDelete[] = $fileName;
+		$resource = fopen($fileName, 'r+');
+		$this->stream->attach($resource);
+
+		$this->assertNull($this->stream->getMetadata('TOTALLY_MADE_UP'));
+	}
+
+	/**
+	 * @test
+	 */
+	public function getSizeReturnsStreamSize() {
+		$resource = fopen(__FILE__, 'r');
+		$expected = fstat($resource);
+		$stream = new Stream($resource);
+		$this->assertEquals($expected['size'], $stream->getSize());
+	}
+}
diff --git a/typo3/sysext/core/Tests/Unit/Http/UploadedFileTest.php b/typo3/sysext/core/Tests/Unit/Http/UploadedFileTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..a7106e01f57f33de545d63e32c4dbb88175c3d21
--- /dev/null
+++ b/typo3/sysext/core/Tests/Unit/Http/UploadedFileTest.php
@@ -0,0 +1,258 @@
+<?php
+namespace TYPO3\CMS\Core\Tests\Unit\Http;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Http\Stream;
+use TYPO3\CMS\Core\Http\UploadedFile;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Testcase for \TYPO3\CMS\Core\Http\UploadedFile
+ *
+ * Adapted from https://github.com/phly/http/
+ */
+class UploadedFileTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
+
+	protected $tmpFile;
+
+	public function setUp() {
+		$this->tmpfile = NULL;
+	}
+
+	public function tearDown() {
+		if (is_scalar($this->tmpFile) && file_exists($this->tmpFile)) {
+			unlink($this->tmpFile);
+		}
+	}
+
+	/**
+	 * @return array
+	 */
+	public function invalidStreamsDataProvider() {
+		return [
+			'null'   => [NULL],
+			'true'   => [TRUE],
+			'false'  => [FALSE],
+			'int'    => [1],
+			'float'  => [1.1],
+			/* Have not figured out a valid way to test an invalid path yet; null byte injection
+			 * appears to get caught by fopen()
+			'invalid-path' => [ ('WIN' === strtoupper(substr(PHP_OS, 0, 3))) ? '[:]' : 'foo' . chr(0) ],
+			 */
+			'array'  => [['filename']],
+			'object' => [(object) ['filename']],
+		];
+	}
+
+	/**
+	 * @dataProvider invalidStreamsDataProvider
+	 * @test
+	 */
+	public function constructorRaisesExceptionOnInvalidStreamOrFile($streamOrFile) {
+		$this->setExpectedException('InvalidArgumentException');
+		new UploadedFile($streamOrFile, 0, UPLOAD_ERR_OK);
+	}
+
+	/**
+	 * @return array
+	 */
+	public function invalidSizesDataProvider() {
+		return [
+			'null'   => [NULL],
+			'true'   => [TRUE],
+			'false'  => [FALSE],
+			'float'  => [1.1],
+			'string' => ['1'],
+			'array'  => [[1]],
+			'object' => [(object) [1]],
+		];
+	}
+
+	/**
+	 * @dataProvider invalidSizesDataProvider
+	 * @test
+	 */
+	public function constructorRaisesExceptionOnInvalidSize($size) {
+		$this->setExpectedException('InvalidArgumentException', 'size');
+		new UploadedFile(fopen('php://temp', 'wb+'), $size, UPLOAD_ERR_OK);
+	}
+
+	/**
+	 * @return array
+	 */
+	public function invalidErrorStatusesDataProvider() {
+		return [
+			'null'     => [NULL],
+			'true'     => [TRUE],
+			'false'    => [FALSE],
+			'float'    => [1.1],
+			'string'   => ['1'],
+			'array'    => [[1]],
+			'object'   => [(object) [1]],
+			'negative' => [-1],
+			'too-big'  => [9],
+		];
+	}
+
+	/**
+	 * @dataProvider invalidErrorStatusesDataProvider
+	 * @test
+	 */
+	public function constructorRaisesExceptionOnInvalidErrorStatus($status) {
+		$this->setExpectedException('InvalidArgumentException', 'status');
+		new UploadedFile(fopen('php://temp', 'wb+'), 0, $status);
+	}
+
+	/**
+	 * @return array
+	 */
+	public function invalidFilenamesAndMediaTypesDataProvider() {
+		return [
+			'true'   => [TRUE],
+			'false'  => [FALSE],
+			'int'    => [1],
+			'float'  => [1.1],
+			'array'  => [['string']],
+			'object' => [(object) ['string']],
+		];
+	}
+
+	/**
+	 * @dataProvider invalidFilenamesAndMediaTypesDataProvider
+	 * @test
+	 */
+	public function constructorRaisesExceptionOnInvalidClientFilename($filename) {
+		$this->setExpectedException('InvalidArgumentException', 'filename');
+		new UploadedFile(fopen('php://temp', 'wb+'), 0, UPLOAD_ERR_OK, $filename);
+	}
+
+	/**
+	 * @dataProvider invalidFilenamesAndMediaTypesDataProvider
+	 * @test
+	 */
+	public function constructorRaisesExceptionOnInvalidClientMediaType($mediaType) {
+		$this->setExpectedException('InvalidArgumentException', 'media type');
+		new UploadedFile(fopen('php://temp', 'wb+'), 0, UPLOAD_ERR_OK, 'foobar.baz', $mediaType);
+	}
+
+	/**
+	 * @test
+	 */
+	public function getStreamReturnsOriginalStreamObject() {
+		$stream = new Stream('php://temp');
+		$upload = new UploadedFile($stream, 0, UPLOAD_ERR_OK);
+		$this->assertSame($stream, $upload->getStream());
+	}
+
+	/**
+	 * @test
+	 */
+	public function getStreamReturnsWrappedPhpStream() {
+		$stream = fopen('php://temp', 'wb+');
+		$upload = new UploadedFile($stream, 0, UPLOAD_ERR_OK);
+		$uploadStream = $upload->getStream()->detach();
+		$this->assertSame($stream, $uploadStream);
+	}
+
+	/**
+	 * @test
+	 */
+	public function getStreamReturnsStreamForFile() {
+		$this->tmpFile = $stream = tempnam(sys_get_temp_dir(), 'phly');
+		$upload = new UploadedFile($stream, 0, UPLOAD_ERR_OK);
+		$uploadStream = $upload->getStream();
+		$r = new \ReflectionProperty($uploadStream, 'stream');
+		$r->setAccessible(TRUE);
+		$this->assertSame($stream, $r->getValue($uploadStream));
+	}
+
+	/**
+	 * @test
+	 */
+	public function moveToMovesFileToDesignatedPath() {
+		$stream = new Stream('php://temp', 'wb+');
+		$stream->write('Foo bar!');
+		$upload = new UploadedFile($stream, 0, UPLOAD_ERR_OK);
+
+		$this->tmpFile = $to = GeneralUtility::tempnam('psr7');
+		$upload->moveTo($to);
+		$this->assertTrue(file_exists($to));
+		$contents = file_get_contents($to);
+		$this->assertEquals($stream->__toString(), $contents);
+	}
+
+	/**
+	 * @return array
+	 */
+	public function invalidMovePathsDataProvider() {
+		return [
+			'null'   => [NULL],
+			'true'   => [TRUE],
+			'false'  => [FALSE],
+			'int'    => [1],
+			'float'  => [1.1],
+			'empty'  => [''],
+			'array'  => [['filename']],
+			'object' => [(object) ['filename']],
+		];
+	}
+
+	/**
+	 * @dataProvider invalidMovePathsDataProvider
+	 * @test
+	 */
+	public function moveToRaisesExceptionForInvalidPath($path) {
+		$stream = new Stream('php://temp', 'wb+');
+		$stream->write('Foo bar!');
+		$upload = new UploadedFile($stream, 0, UPLOAD_ERR_OK);
+
+		$this->tmpFile = $path;
+		$this->setExpectedException('InvalidArgumentException', 'path');
+		$upload->moveTo($path);
+	}
+
+	/**
+	 * @test
+	 */
+	public function moveToCannotBeCalledMoreThanOnce() {
+		$stream = new Stream('php://temp', 'wb+');
+		$stream->write('Foo bar!');
+		$upload = new UploadedFile($stream, 0, UPLOAD_ERR_OK);
+
+		$this->tmpFile = $to = GeneralUtility::tempnam('psr7');
+		$upload->moveTo($to);
+		$this->assertTrue(file_exists($to));
+
+		$this->setExpectedException('RuntimeException', 'moved');
+		$upload->moveTo($to);
+	}
+
+	/**
+	 * @test
+	 */
+	public function getGetStreamRaisesExceptionAfterMove() {
+		$stream = new Stream('php://temp', 'wb+');
+		$stream->write('Foo bar!');
+		$upload = new UploadedFile($stream, 0, UPLOAD_ERR_OK);
+
+		$this->tmpFile = $to = GeneralUtility::tempnam('psr7');
+		$upload->moveTo($to);
+		$this->assertTrue(file_exists($to));
+
+		$this->setExpectedException('RuntimeException', 'moved');
+		$upload->getStream();
+	}
+
+}
diff --git a/typo3/sysext/core/Tests/Unit/Http/UriTest.php b/typo3/sysext/core/Tests/Unit/Http/UriTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..5024f4c0dbe1b33eb7210fe8f0e9d04a0a44c450
--- /dev/null
+++ b/typo3/sysext/core/Tests/Unit/Http/UriTest.php
@@ -0,0 +1,486 @@
+<?php
+namespace TYPO3\CMS\Core\Tests\Unit\Http;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Http\Uri;
+
+/**
+ * Testcase for \TYPO3\CMS\Core\Http\Uri
+ *
+ * Adapted from https://github.com/phly/http/
+ */
+class UriTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
+
+	/**
+	 * @test
+	 */
+	public function constructorSetsAllProperties() {
+		$uri = new Uri('https://user:pass@local.example.com:3001/foo?bar=baz#quz');
+		$this->assertEquals('https', $uri->getScheme());
+		$this->assertEquals('user:pass', $uri->getUserInfo());
+		$this->assertEquals('local.example.com', $uri->getHost());
+		$this->assertEquals(3001, $uri->getPort());
+		$this->assertEquals('user:pass@local.example.com:3001', $uri->getAuthority());
+		$this->assertEquals('/foo', $uri->getPath());
+		$this->assertEquals('bar=baz', $uri->getQuery());
+		$this->assertEquals('quz', $uri->getFragment());
+	}
+
+	/**
+	 * @test
+	 */
+	public function canSerializeToString() {
+		$url = 'https://user:pass@local.example.com:3001/foo?bar=baz#quz';
+		$uri = new Uri($url);
+		$this->assertEquals($url, (string) $uri);
+	}
+
+	/**
+	 * @test
+	 */
+	public function withSchemeReturnsNewInstanceWithNewScheme() {
+		$uri = new Uri('https://user:pass@local.example.com:3001/foo?bar=baz#quz');
+		$new = $uri->withScheme('http');
+		$this->assertNotSame($uri, $new);
+		$this->assertEquals('http', $new->getScheme());
+		$this->assertEquals('http://user:pass@local.example.com:3001/foo?bar=baz#quz', (string) $new);
+	}
+
+	/**
+	 * @test
+	 */
+	public function withUserInfoReturnsNewInstanceWithProvidedUser() {
+		$uri = new Uri('https://user:pass@local.example.com:3001/foo?bar=baz#quz');
+		$new = $uri->withUserInfo('matthew');
+		$this->assertNotSame($uri, $new);
+		$this->assertEquals('matthew', $new->getUserInfo());
+		$this->assertEquals('https://matthew@local.example.com:3001/foo?bar=baz#quz', (string) $new);
+	}
+
+	/**
+	 * @test
+	 */
+	public function withUserInfoReturnsNewInstanceWithProvidedUserAndPassword() {
+		$uri = new Uri('https://user:pass@local.example.com:3001/foo?bar=baz#quz');
+		$new = $uri->withUserInfo('matthew', 'zf2');
+		$this->assertNotSame($uri, $new);
+		$this->assertEquals('matthew:zf2', $new->getUserInfo());
+		$this->assertEquals('https://matthew:zf2@local.example.com:3001/foo?bar=baz#quz', (string) $new);
+	}
+
+	/**
+	 * @test
+	 */
+	public function withHostReturnsNewInstanceWithProvidedHost() {
+		$uri = new Uri('https://user:pass@local.example.com:3001/foo?bar=baz#quz');
+		$new = $uri->withHost('framework.zend.com');
+		$this->assertNotSame($uri, $new);
+		$this->assertEquals('framework.zend.com', $new->getHost());
+		$this->assertEquals('https://user:pass@framework.zend.com:3001/foo?bar=baz#quz', (string) $new);
+	}
+
+	/**
+	 * @return array
+	 */
+	public function validPortsDataProvider() {
+		return [
+			'int'    => [3000],
+			'string' => ["3000"]
+		];
+	}
+
+	/**
+	 * @dataProvider validPortsDataProvider
+	 * @test
+	 */
+	public function withPortReturnsNewInstanceWithProvidedPort($port) {
+		$uri = new Uri('https://user:pass@local.example.com:3001/foo?bar=baz#quz');
+		$new = $uri->withPort($port);
+		$this->assertNotSame($uri, $new);
+		$this->assertEquals($port, $new->getPort());
+		$this->assertEquals(
+			sprintf('https://user:pass@local.example.com:%d/foo?bar=baz#quz', $port),
+			(string) $new
+		);
+	}
+
+	/**
+	 * @return array
+	 */
+	public function invalidPortsDataProvider() {
+		return [
+			'null'      => [NULL],
+			'true'      => [TRUE],
+			'false'     => [FALSE],
+			'string'    => ['string'],
+			'array'     => [[3000]],
+			'object'    => [(object) [3000]],
+			'zero'      => [0],
+			'too-small' => [-1],
+			'too-big'   => [65536],
+		];
+	}
+
+	/**
+	 * @dataProvider invalidPortsDataProvider
+	 */
+	public function withPortRaisesExceptionForInvalidPorts($port) {
+		$uri = new Uri('https://user:pass@local.example.com:3001/foo?bar=baz#quz');
+		$this->setExpectedException('InvalidArgumentException', 'Invalid port');
+		$new = $uri->withPort($port);
+	}
+
+	/**
+	 * @test
+	 */
+	public function withPathReturnsNewInstanceWithProvidedPath() {
+		$uri = new Uri('https://user:pass@local.example.com:3001/foo?bar=baz#quz');
+		$new = $uri->withPath('/bar/baz');
+		$this->assertNotSame($uri, $new);
+		$this->assertEquals('/bar/baz', $new->getPath());
+		$this->assertEquals('https://user:pass@local.example.com:3001/bar/baz?bar=baz#quz', (string) $new);
+	}
+
+	/**
+	 * @return array
+	 */
+	public function invalidPathsDataProvider() {
+		return [
+			'null'     => [NULL],
+			'true'     => [TRUE],
+			'false'    => [FALSE],
+			'array'    => [['/bar/baz']],
+			'object'   => [(object) ['/bar/baz']],
+			'query'    => ['/bar/baz?bat=quz'],
+			'fragment' => ['/bar/baz#bat'],
+		];
+	}
+
+	/**
+	 * @dataProvider invalidPathsDataProvider
+	 * @test
+	 */
+	public function withPathRaisesExceptionForInvalidPaths($path) {
+		$uri = new Uri('https://user:pass@local.example.com:3001/foo?bar=baz#quz');
+		$this->setExpectedException('InvalidArgumentException', 'Invalid path');
+		$new = $uri->withPath($path);
+	}
+
+	/**
+	 * @test
+	 */
+	public function withQueryReturnsNewInstanceWithProvidedQuery() {
+		$uri = new Uri('https://user:pass@local.example.com:3001/foo?bar=baz#quz');
+		$new = $uri->withQuery('baz=bat');
+		$this->assertNotSame($uri, $new);
+		$this->assertEquals('baz=bat', $new->getQuery());
+		$this->assertEquals('https://user:pass@local.example.com:3001/foo?baz=bat#quz', (string) $new);
+	}
+
+	/**
+	 * @return array
+	 */
+	public function invalidQueryStringsDataProvider() {
+		return [
+			'null'     => [NULL],
+			'true'     => [TRUE],
+			'false'    => [FALSE],
+			'array'    => [['baz=bat']],
+			'object'   => [(object) ['baz=bat']],
+			'fragment' => ['baz=bat#quz'],
+		];
+	}
+
+	/**
+	 * @dataProvider invalidQueryStringsDataProvider
+	 * @test
+	 */
+	public function withQueryRaisesExceptionForInvalidQueryStrings($query) {
+		$uri = new Uri('https://user:pass@local.example.com:3001/foo?bar=baz#quz');
+		$this->setExpectedException('InvalidArgumentException', 'Query string');
+		$new = $uri->withQuery($query);
+	}
+
+	/**
+	 * @test
+	 */
+	public function withFragmentReturnsNewInstanceWithProvidedFragment() {
+		$uri = new Uri('https://user:pass@local.example.com:3001/foo?bar=baz#quz');
+		$new = $uri->withFragment('qat');
+		$this->assertNotSame($uri, $new);
+		$this->assertEquals('qat', $new->getFragment());
+		$this->assertEquals('https://user:pass@local.example.com:3001/foo?bar=baz#qat', (string) $new);
+	}
+
+	/**
+	 * @return array
+	 */
+	public function authorityInfoDataProvider() {
+		return [
+			'host-only'      => ['http://foo.com/bar', 'foo.com'],
+			'host-port'      => ['http://foo.com:3000/bar', 'foo.com:3000'],
+			'user-host'      => ['http://me@foo.com/bar', 'me@foo.com'],
+			'user-host-port' => ['http://me@foo.com:3000/bar', 'me@foo.com:3000'],
+		];
+	}
+
+	/**
+	 * @dataProvider authorityInfoDataProvider
+	 * @test
+	 */
+	public function getAuthorityReturnsExpectedValues($url, $expected) {
+		$uri = new Uri($url);
+		$this->assertEquals($expected, $uri->getAuthority());
+	}
+
+	/**
+	 * @test
+	 */
+	public function canEmitOriginFormUrl() {
+		$url = '/foo/bar?baz=bat';
+		$uri = new Uri($url);
+		$this->assertEquals($url, (string) $uri);
+	}
+
+	/**
+	 * @test
+	 */
+	public function settingEmptyPathOnAbsoluteUriReturnsAnEmptyPath() {
+		$uri = new Uri('http://example.com/foo');
+		$new = $uri->withPath('');
+		$this->assertEquals('', $new->getPath());
+	}
+
+	/**
+	 * @test
+	 */
+	public function stringRepresentationOfAbsoluteUriWithNoPathSetsAnEmptyPath() {
+		$uri = new Uri('http://example.com');
+		$this->assertEquals('http://example.com', (string) $uri);
+	}
+
+	/**
+	 * @test
+	 */
+	public function getPathOnOriginFormRemainsAnEmptyPath() {
+		$uri = new Uri('?foo=bar');
+		$this->assertEquals('', $uri->getPath());
+	}
+
+	/**
+	 * @test
+	 */
+	public function stringRepresentationOfOriginFormWithNoPathRetainsEmptyPath() {
+		$uri = new Uri('?foo=bar');
+		$this->assertEquals('?foo=bar', (string) $uri);
+	}
+
+	/**
+	 * @return array
+	 */
+	public function invalidConstructorUrisDataProvider() {
+		return [
+			'null'   => [NULL],
+			'true'   => [TRUE],
+			'false'  => [FALSE],
+			'int'    => [1],
+			'float'  => [1.1],
+			'array'  => [['http://example.com/']],
+			'object' => [(object) ['uri' => 'http://example.com/']],
+		];
+	}
+
+	/**
+	 * @dataProvider invalidConstructorUrisDataProvider
+	 */
+	public function constructorRaisesExceptionForNonStringURI($uri) {
+		$this->setExpectedException('InvalidArgumentException');
+		new Uri($uri);
+	}
+
+	/**
+	 * @test
+	 */
+	public function constructorRaisesExceptionForSeriouslyMalformedURI() {
+		$this->setExpectedException('InvalidArgumentException');
+		new Uri('http:///www.php-fig.org/');
+	}
+
+	/**
+	 * @test
+	 */
+	public function withSchemeStripsOffDelimiter() {
+		$uri = new Uri('http://example.com');
+		$new = $uri->withScheme('https://');
+		$this->assertEquals('https', $new->getScheme());
+	}
+
+	/**
+	 * @return array
+	 */
+	public function invalidSchemesDataProvider() {
+		return [
+			'mailto' => ['mailto'],
+			'ftp'    => ['ftp'],
+			'telnet' => ['telnet'],
+			'ssh'    => ['ssh'],
+			'git'    => ['git'],
+		];
+	}
+
+	/**
+	 * @dataProvider invalidSchemesDataProvider
+	 * @test
+	 */
+	public function constructWithUnsupportedSchemeRaisesAnException($scheme) {
+		$this->setExpectedException('InvalidArgumentException', 'Unsupported scheme');
+		$uri = new Uri($scheme . '://example.com');
+	}
+
+	/**
+	 * @dataProvider invalidSchemesDataProvider
+	 * @test
+	 */
+	public function withSchemeUsingUnsupportedSchemeRaisesAnException($scheme) {
+		$uri = new Uri('http://example.com');
+		$this->setExpectedException('InvalidArgumentException', 'Unsupported scheme');
+		$uri->withScheme($scheme);
+	}
+
+	/**
+	 * @test
+	 */
+	public function withPathIsNotPrefixedWithSlashIfSetWithoutOne() {
+		$uri = new Uri('http://example.com');
+		$new = $uri->withPath('foo/bar');
+		$this->assertEquals('foo/bar', $new->getPath());
+	}
+
+	/**
+	 * @test
+	 */
+	public function withPathNotSlashPrefixedIsEmittedWithSlashDelimiterWhenUriIsCastToString() {
+		$uri = new Uri('http://example.com');
+		$new = $uri->withPath('foo/bar');
+		$this->assertEquals('http://example.com/foo/bar', $new->__toString());
+	}
+
+	/**
+	 * @test
+	 */
+	public function withQueryStripsQueryPrefixIfPresent() {
+		$uri = new Uri('http://example.com');
+		$new = $uri->withQuery('?foo=bar');
+		$this->assertEquals('foo=bar', $new->getQuery());
+	}
+
+	/**
+	 * @test
+	 */
+	public function withFragmentStripsFragmentPrefixIfPresent() {
+		$uri = new Uri('http://example.com');
+		$new = $uri->withFragment('#/foo/bar');
+		$this->assertEquals('/foo/bar', $new->getFragment());
+	}
+
+	/**
+	 * @return array
+	 */
+	public function standardSchemePortCombinationsDataProvider() {
+		return [
+			'http'  => ['http', 80],
+			'https' => ['https', 443],
+		];
+	}
+
+	/**
+	 * @dataProvider standardSchemePortCombinationsDataProvider
+	 * @test
+	 */
+	public function getAuthorityOmitsPortForStandardSchemePortCombinations($scheme, $port) {
+		$uri = (new Uri())
+			->withHost('example.com')
+			->withScheme($scheme)
+			->withPort($port);
+		$this->assertEquals('example.com', $uri->getAuthority());
+	}
+
+	/**
+	 * @test
+	 */
+	public function getPathIsProperlyEncoded() {
+		$uri = (new Uri())->withPath('/foo^bar');
+		$expected = '/foo%5Ebar';
+		$this->assertEquals($expected, $uri->getPath());
+	}
+
+	/**
+	 * @test
+	 */
+	public function getPathDoesNotBecomeDoubleEncoded() {
+		$uri = (new Uri())->withPath('/foo%5Ebar');
+		$expected = '/foo%5Ebar';
+		$this->assertEquals($expected, $uri->getPath());
+	}
+
+	/**
+	 * @return array
+	 */
+	public function queryStringsForEncodingDataProvider() {
+		return [
+			'key-only'        => ['k^ey', 'k%5Eey'],
+			'key-value'       => ['k^ey=valu`', 'k%5Eey=valu%60'],
+			'array-key-only'  => ['key[]', 'key%5B%5D'],
+			'array-key-value' => ['key[]=valu`', 'key%5B%5D=valu%60'],
+			'complex'         => ['k^ey&key[]=valu`&f<>=`bar', 'k%5Eey&key%5B%5D=valu%60&f%3C%3E=%60bar'],
+		];
+	}
+
+	/**
+	 * @dataProvider queryStringsForEncodingDataProvider
+	 * @test
+	 */
+	public function getQueryIsProperlyEncoded($query, $expected) {
+		$uri = (new Uri())->withQuery($query);
+		$this->assertEquals($expected, $uri->getQuery());
+	}
+
+	/**
+	 * @dataProvider queryStringsForEncodingDataProvider
+	 * @test
+	 */
+	public function getQueryIsNotDoubleEncoded($query, $expected) {
+		$uri = (new Uri())->withQuery($expected);
+		$this->assertEquals($expected, $uri->getQuery());
+	}
+
+	/**
+	 * @test
+	 */
+	public function getFragmentIsProperlyEncoded() {
+		$uri = (new Uri())->withFragment('/p^th?key^=`bar#b@z');
+		$expected = '/p%5Eth?key%5E=%60bar%23b@z';
+		$this->assertEquals($expected, $uri->getFragment());
+	}
+
+	/**
+	 * @test
+	 */
+	public function getFragmentIsNotDoubleEncoded() {
+		$expected = '/p%5Eth?key%5E=%60bar%23b@z';
+		$uri = (new Uri())->withFragment($expected);
+		$this->assertEquals($expected, $uri->getFragment());
+	}
+}
diff --git a/typo3/sysext/frontend/Classes/Http/EidRequestHandler.php b/typo3/sysext/frontend/Classes/Http/EidRequestHandler.php
index a2fbef86377e3f243031565f245d3e000e6e5d76..2fe2551f2a615f3d4facbca1a0110519dbf3b08b 100644
--- a/typo3/sysext/frontend/Classes/Http/EidRequestHandler.php
+++ b/typo3/sysext/frontend/Classes/Http/EidRequestHandler.php
@@ -20,6 +20,7 @@ use TYPO3\CMS\Core\TimeTracker\TimeTracker;
 use TYPO3\CMS\Frontend\Utility\EidUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Core\RequestHandlerInterface;
+use Psr\Http\Message\ServerRequestInterface;
 
 /**
  * Lightweight alternative to the regular RequestHandler used when $_GET[eID] is set.
@@ -34,7 +35,7 @@ class EidRequestHandler implements RequestHandlerInterface {
 	protected $bootstrap;
 
 	/**
-	 * Constructor handing over the bootstrap
+	 * Constructor handing over the bootstrap and the original request
 	 *
 	 * @param Bootstrap $bootstrap
 	 */
@@ -45,15 +46,17 @@ class EidRequestHandler implements RequestHandlerInterface {
 	/**
 	 * Handles a frontend request based on the _GP "eID" variable.
 	 *
-	 * @return void
+	 * @param ServerRequestInterface $request
+	 * @return NULL|\Psr\Http\Message\ResponseInterface
 	 */
-	public function handleRequest() {
+	public function handleRequest(ServerRequestInterface $request) {
+		$response = NULL;
 		// Timetracking started
 		$configuredCookieName = trim($GLOBALS['TYPO3_CONF_VARS']['BE']['cookieName']);
 		if (empty($configuredCookieName)) {
 			$configuredCookieName = 'be_typo_user';
 		}
-		if ($_COOKIE[$configuredCookieName]) {
+		if ($request->getCookieParams()[$configuredCookieName]) {
 			$GLOBALS['TT'] = new TimeTracker();
 		} else {
 			$GLOBALS['TT'] = new NullTimeTracker();
@@ -74,17 +77,18 @@ class EidRequestHandler implements RequestHandlerInterface {
 		// Remove any output produced until now
 		$this->bootstrap->endOutputBufferingAndCleanPreviousOutput();
 		require EidUtility::getEidScriptPath();
-		$this->bootstrap->shutdown();
-		exit;
+
+		return $response;
 	}
 
 	/**
 	 * This request handler can handle any frontend request.
 	 *
+	 * @param ServerRequestInterface $request The request to process
 	 * @return bool If the request is not an eID request, TRUE otherwise FALSE
 	 */
-	public function canHandleRequest() {
-		return GeneralUtility::_GP('eID') ? TRUE : FALSE;
+	public function canHandleRequest(ServerRequestInterface $request) {
+		return $request->getQueryParams()['eID'] || $request->getParsedBody()['eID'] ? TRUE : FALSE;
 	}
 
 	/**
@@ -96,4 +100,5 @@ class EidRequestHandler implements RequestHandlerInterface {
 	public function getPriority() {
 		return 80;
 	}
+
 }
diff --git a/typo3/sysext/frontend/Classes/Http/RequestHandler.php b/typo3/sysext/frontend/Classes/Http/RequestHandler.php
index d11a1d6c5eecb40eee53b0f8e807687209b151ed..b223440ee214e0930e4a9825d70410b934a4fd7b 100644
--- a/typo3/sysext/frontend/Classes/Http/RequestHandler.php
+++ b/typo3/sysext/frontend/Classes/Http/RequestHandler.php
@@ -58,7 +58,13 @@ class RequestHandler implements RequestHandlerInterface {
 	protected $controller;
 
 	/**
-	 * Constructor handing over the bootstrap
+	 * The request handed over
+	 * @var \Psr\Http\Message\ServerRequestInterface
+	 */
+	protected $request;
+
+	/**
+	 * Constructor handing over the bootstrap and the original request
 	 *
 	 * @param Bootstrap $bootstrap
 	 */
@@ -69,9 +75,12 @@ class RequestHandler implements RequestHandlerInterface {
 	/**
 	 * Handles a frontend request
 	 *
-	 * @return void
+	 * @param \Psr\Http\Message\ServerRequestInterface $request
+	 * @return NULL|\Psr\Http\Message\ResponseInterface
 	 */
-	public function handleRequest() {
+	public function handleRequest(\Psr\Http\Message\ServerRequestInterface $request) {
+		$response = NULL;
+		$this->request = $request;
 		$this->initializeTimeTracker();
 
 		// Hook to preprocess the current request:
@@ -266,7 +275,9 @@ class RequestHandler implements RequestHandlerInterface {
 		}
 
 		if ($sendTSFEContent) {
-			echo $this->controller->content;
+			/** @var \TYPO3\CMS\Core\Http\Response $response */
+			$response = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Http\Response::class);
+			$response->getBody()->write($this->controller->content);
 		}
 		// Debugging Output
 		if (isset($GLOBALS['error']) && is_object($GLOBALS['error']) && @is_callable(array($GLOBALS['error'], 'debugOutput'))) {
@@ -275,15 +286,17 @@ class RequestHandler implements RequestHandlerInterface {
 		if (TYPO3_DLOG) {
 			GeneralUtility::devLog('END of FRONTEND session', 'cms', 0, array('_FLUSH' => TRUE));
 		}
+		return $response;
 	}
 
 	/**
 	 * This request handler can handle any frontend request.
 	 *
+	 * @param \Psr\Http\Message\ServerRequestInterface $request
 	 * @return bool If the request is not an eID request, TRUE otherwise FALSE
 	 */
-	public function canHandleRequest() {
-		return GeneralUtility::_GP('eID') ? FALSE : TRUE;
+	public function canHandleRequest(\Psr\Http\Message\ServerRequestInterface $request) {
+		return $request->getQueryParams()['eID'] || $request->getParsedBody()['eID'] ? FALSE : TRUE;
 	}
 
 	/**
@@ -319,7 +332,7 @@ class RequestHandler implements RequestHandlerInterface {
 		if (empty($configuredCookieName)) {
 			$configuredCookieName = 'be_typo_user';
 		}
-		if ($_COOKIE[$configuredCookieName]) {
+		if ($this->request->getCookieParams()[$configuredCookieName]) {
 			$this->timeTracker = new TimeTracker();
 		} else {
 			$this->timeTracker = new NullTimeTracker();
@@ -357,4 +370,5 @@ class RequestHandler implements RequestHandlerInterface {
 		// This is a dirty workaround and bypasses the protected access modifier of the controller member.
 		$GLOBALS['TSFE'] = &$this->controller;
 	}
+
 }