From 37ff212acdf63e1a23de81087f81e7730c5ae64c Mon Sep 17 00:00:00 2001 From: Shingo Kitagawa Date: Wed, 15 Jul 2020 02:26:55 +0900 Subject: [PATCH 1/7] add basic and basic_yaml --- src/roswww/roswww_server.py | 68 +++++++++++++++++++++++++++++++++++-- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/src/roswww/roswww_server.py b/src/roswww/roswww_server.py index 094c3f9..65bfce3 100644 --- a/src/roswww/roswww_server.py +++ b/src/roswww/roswww_server.py @@ -36,15 +36,48 @@ import logging +import base64 +import functools import socket import tornado.ioloop # rosbridge installs tornado import tornado.web +import yaml + from .webrequest_handler import WebRequestHandler from .utils import run_shellcommand, split_words, get_packages + +def basic_auth(auth): + def decore(f): + def _request_auth(handler): + handler.set_header('WWW-Authenticate', 'Basic realm=JSL') + handler.set_status(401) + handler.finish() + return False + + @functools.wraps(f) + def new_f(*args): + handler = args[0] + auth_header = handler.request.headers.get('Authorization') + if auth_header is None: + return _request_auth(handler) + if not auth_header.startswith('Basic '): + return _request_auth(handler) + + auth_decoded = base64.decodestring(auth_header.split(' ', 1)[1]) + username, password = auth_decoded.split(':', 1) + + if auth(username, password): + f(*args) + else: + _request_auth(handler) + return new_f + return decore + + class ROSWWWServer(): - def __init__(self, name, webpath, ports, cached, single_package=None): + def __init__(self, name, webpath, ports, cached, single_package=None, basic=False, basic_yaml=None): ''' :param str name: webserver name :param str webpath: package relative path to web page source. @@ -54,20 +87,51 @@ def __init__(self, name, webpath, ports, cached, single_package=None): self._webpath = webpath self._ports = ports self._cached = cached + self._basic = basic self._logger = self._set_logger() self._packages = get_packages() self._application = self._create_webserver(self._packages, single_package=single_package) + if self._basic: + if basic_yaml: + with open(basic_yaml) as f: + self._keys = yaml.safe_load(f) + else: + self._keys = {'admin': 'admin'} def _create_webserver(self, packages, single_package=None): ''' @type packages: {str, str} @param packages: name and path of ROS packages. ''' + def _auth(username, password): + if username in self._keys: + return self._keys[username] == password + return False + class NoCacheStaticFileHandler(tornado.web.StaticFileHandler): def set_extra_headers(self, path): self.set_header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') - file_handler = tornado.web.StaticFileHandler if self._cached else NoCacheStaticFileHandler + class BasicStaticFileHandler(tornado.web.StaticFileHandler): + @basic_auth(_auth) + def get(self, path, include_body=True): + super(BasicStaticFileHandler, self).get(path, include_body) + + class BasicNoCacheStaticFileHandler(NoCacheStaticFileHandler): + @basic_auth(_auth) + def get(self, path, include_body=True): + super(BasicNoCacheStaticFileHandler, self).get(path, include_body) + + if self._cached: + if self._basic: + file_handler = BasicStaticFileHandler + else: + file_handler = tornado.web.StaticFileHandler + else: + if self._basic: + file_handler = BasicNoCacheStaticFileHandler + else: + file_handler = NoCacheStaticFileHandler if single_package: for package in packages: From ff3e4e75e41e86b3a883e04a7599ab056f53de4b Mon Sep 17 00:00:00 2001 From: Shingo Kitagawa Date: Wed, 15 Jul 2020 02:27:24 +0900 Subject: [PATCH 2/7] add basic and basic_yaml in webserver --- launch/roswww.launch | 2 ++ script/webserver.py | 11 ++++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/launch/roswww.launch b/launch/roswww.launch index 26d1009..5894e4d 100644 --- a/launch/roswww.launch +++ b/launch/roswww.launch @@ -3,6 +3,7 @@ + @@ -10,6 +11,7 @@ diff --git a/script/webserver.py b/script/webserver.py index 9b0ffa1..ea7842d 100755 --- a/script/webserver.py +++ b/script/webserver.py @@ -49,17 +49,22 @@ def parse_argument(argv): parser.add_argument('-w', '--webpath', default='www', help='package relative path to web pages') parser.add_argument('-s', '--single_package', default='', help='package name for single package mode') parser.add_argument('--cached', default='true', help='static file is cached') + parser.add_argument('--basic', default='false', help='enable basic authentication') + parser.add_argument('--basic-yaml', default=None, help='basic key yaml file path') parser.add_argument('--start_port', default=8000, type=int, help='setting up port scan range') parser.add_argument('--end_port', default=9000, type=int, help='setting up port scan range') parsed_args = parser.parse_args(argv) cached = False if parsed_args.cached in [0, False, 'false', 'False'] else True - return parsed_args.name, parsed_args.webpath, (parsed_args.port, parsed_args.start_port, parsed_args.end_port), cached, parsed_args.single_package + basic = True if parsed_args.basic in [1, True, 'true', 'True'] else False + basic_yaml = parsed_args.basic_yaml + return parsed_args.name, parsed_args.webpath, (parsed_args.port, parsed_args.start_port, parsed_args.end_port), cached, parsed_args.single_package, basic, basic_yaml + if __name__ == '__main__': rospy.init_node("webserver", disable_signals=True) - name, webpath, port, cached, single = parse_argument(rospy.myargv()[1:]) - webserver = roswww.ROSWWWServer(name, webpath, port, cached, single_package=single) + name, webpath, port, cached, single, basic, basic_yaml = parse_argument(rospy.myargv()[1:]) + webserver = roswww.ROSWWWServer(name, webpath, port, cached, single_package=single, basic=basic, basic_yaml=basic_yaml) webserver.loginfo("Initialised") webserver.spin() From 5889c02797bdc5bd5409f6fc9f71fc6c3b061d78 Mon Sep 17 00:00:00 2001 From: Shingo Kitagawa Date: Fri, 24 Jul 2020 20:03:00 +0900 Subject: [PATCH 3/7] update basic realm --- src/roswww/roswww_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/roswww/roswww_server.py b/src/roswww/roswww_server.py index 65bfce3..ec101ef 100644 --- a/src/roswww/roswww_server.py +++ b/src/roswww/roswww_server.py @@ -50,7 +50,7 @@ def basic_auth(auth): def decore(f): def _request_auth(handler): - handler.set_header('WWW-Authenticate', 'Basic realm=JSL') + handler.set_header('WWW-Authenticate', 'Basic realm=roswww') handler.set_status(401) handler.finish() return False From 432bef9f8fb740bba4b3e6fab46227630c7604b2 Mon Sep 17 00:00:00 2001 From: Kei Okada Date: Thu, 28 Oct 2021 19:16:50 +0900 Subject: [PATCH 4/7] use basic_flag to path both --basic and --basic-yaml to roswww.launch --- launch/roswww.launch | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/launch/roswww.launch b/launch/roswww.launch index 5894e4d..6805e07 100644 --- a/launch/roswww.launch +++ b/launch/roswww.launch @@ -4,6 +4,9 @@ + + + @@ -11,7 +14,7 @@ From 5e01bda94e87b3228fd4fd749ba152c47bca94e1 Mon Sep 17 00:00:00 2001 From: Kei Okada Date: Thu, 28 Oct 2021 19:17:19 +0900 Subject: [PATCH 5/7] add test code for basic authentication --- CMakeLists.txt | 2 +- test/basic.test | 29 +++++++++++++ test/keys.yaml | 2 + test/test_basic.py | 105 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 test/basic.test create mode 100644 test/keys.yaml create mode 100755 test/test_basic.py diff --git a/CMakeLists.txt b/CMakeLists.txt index ff52713..ebf5d16 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,7 +14,7 @@ install(DIRECTORY launch test www ) catkin_install_python( - PROGRAMS test/test_roswww.py test/test_single.py + PROGRAMS test/test_roswww.py test/test_single.py test/test_basic.py DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION}/test) if (CATKIN_ENABLE_TESTING) diff --git a/test/basic.test b/test/basic.test new file mode 100644 index 0000000..93b688b --- /dev/null +++ b/test/basic.test @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/keys.yaml b/test/keys.yaml new file mode 100644 index 0000000..c5516c5 --- /dev/null +++ b/test/keys.yaml @@ -0,0 +1,2 @@ +# example passwrod file +user: password \ No newline at end of file diff --git a/test/test_basic.py b/test/test_basic.py new file mode 100755 index 0000000..0d8e2f8 --- /dev/null +++ b/test/test_basic.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Software License Agreement (BSD License) +# +# Copyright (c) 2015, Tokyo Opensource Robotics Kyokai Association +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided +# with the distribution. +# * Neither the name of Tokyo Opensource Robotics Kyokai Association. nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# +# Author: Isaac I.Y. Saito +# Author: Yuki Furuta + +import argparse +import rospy +import rostest +import sys +import unittest + +from selenium import webdriver +from selenium.webdriver.common.keys import Keys +from selenium.webdriver.common.by import By +from selenium.webdriver.support import expected_conditions as EC + +class TestBasic(unittest.TestCase): + + def setUp(self): + parser = argparse.ArgumentParser() + parser.add_argument('--no-headless', action='store_true', + help='start webdriver with headless mode') + args, unknown = parser.parse_known_args() + + self.url_base = rospy.get_param("url_roswww_testserver") + + opts = webdriver.firefox.options.Options() + if not args.no_headless: + opts.add_argument('-headless') + self.browser = webdriver.Firefox(options=opts) + + self.wait = webdriver.support.ui.WebDriverWait(self.browser, 10) + # maximize screen + self.browser.find_element_by_tag_name("html").send_keys(Keys.F11) + + def tearDown(self): + try: + self.browser.close() + self.browser.quit() + except: + pass + + def _check_index(self, url): + rospy.logwarn("Accessing to %s" % url) + + self.browser.get(url) + self.wait.until(EC.presence_of_element_located((By.ID, "title"))) + + title = self.browser.find_element_by_id("title") + self.assertIsNotNone(title, "Object id=title not found") + + # check load other resouces + self.wait.until(EC.presence_of_element_located((By.ID, "relative-link-check"))) + check = self.browser.find_element_by_id("relative-link-check") + self.assertIsNotNone(check, "Object id=relative-link-check not found") + self.assertEqual(check.text, "Relative link is loaded", + "Loading 'css/index.css' from 'index.html' failed") + + def test_index_served(self): + url = '%s/roswww/' % (self.url_base) + self._check_index(url) + + def test_index_redirected(self): + url = '%s/roswww' % (self.url_base) + self._check_index(url) + + + + +if __name__ == '__main__': + rospy.init_node("test_basic") + exit(rostest.rosrun("roswww", "test_basic", TestBasic, sys.argv)) From 8b86b2d44f898279ddd4af501f7aa2a835d4acd7 Mon Sep 17 00:00:00 2001 From: Kei Okada Date: Thu, 28 Oct 2021 19:30:40 +0900 Subject: [PATCH 6/7] add info message on basic authenfication and user login --- src/roswww/roswww_server.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/roswww/roswww_server.py b/src/roswww/roswww_server.py index ec101ef..fe09b25 100644 --- a/src/roswww/roswww_server.py +++ b/src/roswww/roswww_server.py @@ -104,6 +104,7 @@ def _create_webserver(self, packages, single_package=None): @param packages: name and path of ROS packages. ''' def _auth(username, password): + self.loginfo("User %s attempt to login"%(username)) if username in self._keys: return self._keys[username] == password return False @@ -132,6 +133,7 @@ def get(self, path, include_body=True): file_handler = BasicNoCacheStaticFileHandler else: file_handler = NoCacheStaticFileHandler + self.loginfo("Configure webserver with cache:%s, basic:%s"%(self._cached, self._basic)) if single_package: for package in packages: From 42bb102fd75b2725db34f419aed902be006f9600 Mon Sep 17 00:00:00 2001 From: Kei Okada Date: Thu, 28 Oct 2021 19:39:54 +0900 Subject: [PATCH 7/7] add doc for basic authentication --- doc/index.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/doc/index.rst b/doc/index.rst index 4b62665..4dcd72e 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -125,6 +125,19 @@ Chat After launching start_bridge.launch, let's open http://localhost:%PORT_OF_YOURCHOICE%/roswww/chat.html with a browser in two windows. Once you send a message from one of the windows, the message will be shown in both windows. +Basic Authentication +==================== + +To enable basic authentication, start a launch file with a `basic` and `basic_yaml` arguments + + $ roslaunch roswww roswww.launch basic:=true basic_yaml:=%PATH_TO_YAML_FILE% + +The example of `%PATH_TO_YAML_FILE%` like below, which a dictionary of %USER_NAME% and %PASSWORD. + + # example passwrod file + user: password + + Support, communication ==========================