Revert "Redirect Browsers from API to Client"
This reverts commit 447ae50497.
Change-Id: I972ff1ebd616ae3eb9eb375082393ff5ce1e942b
This commit is contained in:
@@ -37,12 +37,6 @@ lock_path = $state_path/lock
|
|||||||
# Port the bind the API server to
|
# Port the bind the API server to
|
||||||
# bind_port = 8080
|
# bind_port = 8080
|
||||||
|
|
||||||
# The default web client. This is the URL to which a client, presenting an
|
|
||||||
# Accepts: text/html header, will be redirected to when browsing the API. It
|
|
||||||
# is also used for email URL resolution, so we highly recommend that you set
|
|
||||||
# this to the host and protocol of your own storyboard server.
|
|
||||||
# default_client_url = https://storyboard.openstack.org/#!
|
|
||||||
|
|
||||||
# Enable notifications. This feature drives deferred processing, reporting,
|
# Enable notifications. This feature drives deferred processing, reporting,
|
||||||
# and subscriptions.
|
# and subscriptions.
|
||||||
# enable_notifications = True
|
# enable_notifications = True
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
# You may obtain a copy of the License at
|
# You may obtain a copy of the License at
|
||||||
#
|
#
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
#
|
#
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
@@ -23,8 +23,6 @@ from wsgiref import simple_server
|
|||||||
|
|
||||||
from storyboard.api import config as api_config
|
from storyboard.api import config as api_config
|
||||||
from storyboard.api.middleware.cors_middleware import CORSMiddleware
|
from storyboard.api.middleware.cors_middleware import CORSMiddleware
|
||||||
|
|
||||||
from storyboard.api.middleware import redirect_middleware
|
|
||||||
from storyboard.api.middleware import session_hook
|
from storyboard.api.middleware import session_hook
|
||||||
from storyboard.api.middleware import token_middleware
|
from storyboard.api.middleware import token_middleware
|
||||||
from storyboard.api.middleware import user_id_hook
|
from storyboard.api.middleware import user_id_hook
|
||||||
@@ -53,11 +51,8 @@ API_OPTS = [
|
|||||||
default=8080,
|
default=8080,
|
||||||
help='API port'),
|
help='API port'),
|
||||||
cfg.BoolOpt('enable_notifications',
|
cfg.BoolOpt('enable_notifications',
|
||||||
default=False,
|
default=False,
|
||||||
help='Enable Notifications'),
|
help='Enable Notifications')
|
||||||
cfg.StrOpt('default_client_url',
|
|
||||||
default='https://storyboard.openstack.org/#!',
|
|
||||||
help='The URL of the default web client.')
|
|
||||||
]
|
]
|
||||||
CORS_OPTS = [
|
CORS_OPTS = [
|
||||||
cfg.ListOpt('allowed_origins',
|
cfg.ListOpt('allowed_origins',
|
||||||
@@ -120,8 +115,6 @@ def setup_app(pecan_config=None):
|
|||||||
)
|
)
|
||||||
|
|
||||||
app = token_middleware.AuthTokenMiddleware(app)
|
app = token_middleware.AuthTokenMiddleware(app)
|
||||||
app = redirect_middleware. \
|
|
||||||
BrowserRedirectMiddleware(app, client_root_url=CONF.default_client_url)
|
|
||||||
|
|
||||||
# Setup CORS
|
# Setup CORS
|
||||||
if CONF.cors.allowed_origins:
|
if CONF.cors.allowed_origins:
|
||||||
@@ -157,7 +150,7 @@ def start():
|
|||||||
% ({'port': port}))
|
% ({'port': port}))
|
||||||
else:
|
else:
|
||||||
LOG.info(_LI("serving on http://%(host)s:%(port)s") % (
|
LOG.info(_LI("serving on http://%(host)s:%(port)s") % (
|
||||||
{'host': host, 'port': port}))
|
{'host': host, 'port': port}))
|
||||||
|
|
||||||
srv.serve_forever()
|
srv.serve_forever()
|
||||||
|
|
||||||
|
|||||||
@@ -1,70 +0,0 @@
|
|||||||
# Copyright (c) 2015 Hewlett-Packard Development Company, L.P.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
# implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
import re
|
|
||||||
|
|
||||||
from webob.acceptparse import Accept
|
|
||||||
|
|
||||||
|
|
||||||
class BrowserRedirectMiddleware(object):
|
|
||||||
# A list of HTML Headers that may come from browsers.
|
|
||||||
html_headers = [
|
|
||||||
'text/html',
|
|
||||||
'application/xhtml+xml'
|
|
||||||
]
|
|
||||||
|
|
||||||
def __init__(self, app, client_root_url='/'):
|
|
||||||
"""Build an HTTP redirector, with the initial assumption that the
|
|
||||||
client is installed on the same host as this wsgi app.
|
|
||||||
|
|
||||||
:param app The WSGI app to wrap.
|
|
||||||
:param client_root_url The root URL of the redirect target's path.
|
|
||||||
"""
|
|
||||||
self.app = app
|
|
||||||
self.client_root_url = client_root_url
|
|
||||||
|
|
||||||
def __call__(self, env, start_response):
|
|
||||||
# We only care about GET methods.
|
|
||||||
if env['REQUEST_METHOD'] == 'GET' and 'HTTP_ACCEPT' in env:
|
|
||||||
# Iterate over the headers.
|
|
||||||
for type, quality in Accept.parse(env['HTTP_ACCEPT']):
|
|
||||||
# Only accept quality 1 headers, anything less
|
|
||||||
# implies that the client prefers something else.
|
|
||||||
if quality == 1 and type in self.html_headers:
|
|
||||||
# Build the redirect URL and redirect if successful
|
|
||||||
redirect_to = self._build_redirect_url(env['PATH_INFO'])
|
|
||||||
if redirect_to:
|
|
||||||
start_response("303 See Other",
|
|
||||||
[('Location', redirect_to)])
|
|
||||||
return []
|
|
||||||
|
|
||||||
# Otherwise, break out of the whole loop and let the
|
|
||||||
# default handler deal with it.
|
|
||||||
break
|
|
||||||
|
|
||||||
return self.app(env, start_response)
|
|
||||||
|
|
||||||
def _build_redirect_url(self, path):
|
|
||||||
# To map to the client, we are assuming that the API adheres to a URL
|
|
||||||
# pattern of "/superfluous_prefix/v1/other_things. We strip out
|
|
||||||
# anything up to and including /v1, and use the rest as our redirect
|
|
||||||
# fragment. Note that this middleware makes no assumption about #!
|
|
||||||
# navigation, as it is feasible that true HTML5 history support is
|
|
||||||
# available on the client.
|
|
||||||
match = re.search('\/v1(\/.*$)', path)
|
|
||||||
if match:
|
|
||||||
return self.client_root_url + match.group(1)
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
# Copyright (c) 2015 Hewlett-Packard Development Company, L.P.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
from oslo_config import cfg
|
|
||||||
|
|
||||||
from storyboard.tests import base
|
|
||||||
|
|
||||||
|
|
||||||
CONF = cfg.CONF
|
|
||||||
|
|
||||||
|
|
||||||
class TestRedirectMiddleware(base.FunctionalTest):
|
|
||||||
# Map of API -> Client urls that we're expecting.
|
|
||||||
uri_mappings = {
|
|
||||||
'/v1/projects': 'https://storyboard.openstack.org/#!/projects',
|
|
||||||
'/v1/stories/22': 'https://storyboard.openstack.org/#!/stories/22',
|
|
||||||
'/v1/project_groups/2': 'https://storyboard.openstack.org/'
|
|
||||||
'#!/project_groups/2'
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_valid_results(self):
|
|
||||||
"""Assert that the expected URI mappings are returned."""
|
|
||||||
headers = {
|
|
||||||
'Accept': 'text/html;q=1'
|
|
||||||
}
|
|
||||||
|
|
||||||
for request_uri, redirect_uri in six.iteritems(self.uri_mappings):
|
|
||||||
response = self.app.get(request_uri,
|
|
||||||
headers=headers,
|
|
||||||
expect_errors=True)
|
|
||||||
|
|
||||||
self.assertEqual(303, response.status_code)
|
|
||||||
self.assertEqual(redirect_uri, response.headers['Location'])
|
|
||||||
|
|
||||||
def test_valid_results_as_post_put_delete(self):
|
|
||||||
"""Assert that POST, PUT, and DELETE methods are passed through to
|
|
||||||
the API.
|
|
||||||
"""
|
|
||||||
headers = {
|
|
||||||
'Accept': 'text/html;q=1'
|
|
||||||
}
|
|
||||||
|
|
||||||
for request_uri, redirect_uri in six.iteritems(self.uri_mappings):
|
|
||||||
response = self.app.post(request_uri, headers=headers,
|
|
||||||
expect_errors=True)
|
|
||||||
self.assertNotEqual(303, response.status_code)
|
|
||||||
self.assertNotIn('Location', response.headers)
|
|
||||||
|
|
||||||
response = self.app.put(request_uri, headers=headers,
|
|
||||||
expect_errors=True)
|
|
||||||
self.assertNotEqual(303, response.status_code)
|
|
||||||
self.assertNotIn('Location', response.headers)
|
|
||||||
|
|
||||||
response = self.app.delete(request_uri, headers=headers,
|
|
||||||
expect_errors=True)
|
|
||||||
self.assertNotEqual(303, response.status_code)
|
|
||||||
self.assertNotIn('Location', response.headers)
|
|
||||||
|
|
||||||
def test_graceful_accepts_header(self):
|
|
||||||
"""If the client prefers some other content type, make sure we
|
|
||||||
respect that.
|
|
||||||
"""
|
|
||||||
headers = {
|
|
||||||
'Accept': 'text/html;q=.9,application/json;q=1'
|
|
||||||
}
|
|
||||||
|
|
||||||
for request_uri, redirect_uri in six.iteritems(self.uri_mappings):
|
|
||||||
response = self.app.get(request_uri,
|
|
||||||
headers=headers,
|
|
||||||
expect_errors=True)
|
|
||||||
|
|
||||||
self.assertNotEqual(303, response.status_code)
|
|
||||||
self.assertNotIn('Location', response.headers)
|
|
||||||
|
|
||||||
def test_with_browser_useragent(self):
|
|
||||||
"""Future protection test. Make sure that no other code accidentally
|
|
||||||
gets in the way of browsers being redirected (such as search engine
|
|
||||||
bot response handling). This is intended to be a canary for
|
|
||||||
unexpected changes, rather than a comprehensive test for all possible
|
|
||||||
browsers.
|
|
||||||
"""
|
|
||||||
user_agents = [
|
|
||||||
# Chrome 41
|
|
||||||
'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML,'
|
|
||||||
' like Gecko) Chrome/41.0.2228.0 Safari/537.36',
|
|
||||||
# Firefox 36
|
|
||||||
'Mozilla/5.0 (Windows NT 6.3; rv:36.0) Gecko/20100101'
|
|
||||||
' Firefox/36.0',
|
|
||||||
# IE10
|
|
||||||
'Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; AS; rv:11.0)'
|
|
||||||
' like Gecko'
|
|
||||||
]
|
|
||||||
|
|
||||||
for request_uri, redirect_uri in six.iteritems(self.uri_mappings):
|
|
||||||
|
|
||||||
for user_agent in user_agents:
|
|
||||||
headers = {
|
|
||||||
'User-Agent': user_agent,
|
|
||||||
'Accept': 'text/html;q=1'
|
|
||||||
}
|
|
||||||
|
|
||||||
response = self.app.get(request_uri,
|
|
||||||
headers=headers,
|
|
||||||
expect_errors=True)
|
|
||||||
|
|
||||||
self.assertEqual(303, response.status_code)
|
|
||||||
self.assertEqual(redirect_uri, response.headers['Location'])
|
|
||||||
Reference in New Issue
Block a user