diff --git a/README.md b/README.md index 3d7e5010..36735237 100644 --- a/README.md +++ b/README.md @@ -269,6 +269,13 @@ Just define the parameters **username** and **password** in the start call: start(MyApp, username='myusername', password='mypassword') ``` +Protocol security +=== +In order to support login forms and data forms you can choose to use https protocol by giving a .pem certificate. +To enable https pass the **https** and **certfile** parameters in the start call +```py +start(MyApp, https=True, certfile="./server.pem") +``` Styling === diff --git a/examples/https_app.py b/examples/https_app.py new file mode 100644 index 00000000..cec1de7d --- /dev/null +++ b/examples/https_app.py @@ -0,0 +1,74 @@ + +import remi.gui as gui +from remi.gui import * +from remi import start, App + + +class untitled(App): + def __init__(self, *args, **kwargs): + if not 'editing_mode' in kwargs.keys(): + super(untitled, self).__init__(*args, static_file_path='./res/') + + def idle(self): + #idle function called every update cycle + pass + + def main(self): + mainContainer = Widget(width=706, height=445, margin='0px auto', style="position: relative") + subContainer = HBox(width=630, height=277, style='position: absolute; left: 40px; top: 150px; background-color: #b6b6b6') + vbox = VBox(width=300, height=250) + bt1 = Button('bt1', width=100, height=30) + vbox.append(bt1,'bt1') + bt3 = Button('bt3', width=100, height=30) + vbox.append(bt3,'bt3') + bt2 = Button('bt2', width=100, height=30) + vbox.append(bt2,'bt2') + subContainer.append(vbox,'vbox') + hbox = HBox(width=300, height=250) + lbl1 = Label('lbl1', width=50, height=50, style='background-color: #ffb509') + hbox.append(lbl1,'lbl1') + lbl2 = Label('lbl2', width=50, height=50, style='background-color: #40ff2b') + hbox.append(lbl2,'lbl2') + lbl3 = Label('lbl3', width=50, height=50, style='background-color: #e706ff') + hbox.append(lbl3,'lbl3') + subContainer.append(hbox,'hbox') + mainContainer.append(subContainer,'subContainer') + comboJustifyContent = gui.DropDown.new_from_list(('flex-start','flex-end','center','space-between','space-around'), + style='left: 160px; position: absolute; top: 60px; width: 148px; height: 30px') + mainContainer.append(comboJustifyContent,'comboJustifyContent') + lblJustifyContent = Label('justify-content', style='left: 40px; position: absolute; top: 60px; width: 100px; height: 30px') + mainContainer.append(lblJustifyContent,'lblJustifyContent') + comboAlignItems = gui.DropDown.new_from_list(('stretch','center','flex-start','flex-end','baseline'), + style='left:160px; position:absolute; top:100px; width:152px; height: 30px') + mainContainer.append(comboAlignItems,'comboAlignItems') + lblAlignItems = Label('align-items', style='left:40px; position:absolute; top:100px; width:100px; height:30px') + mainContainer.append(lblAlignItems,'lblAlignItems') + mainContainer.children['comboJustifyContent'].set_on_change_listener(self.onchange_comboJustifyContent,vbox,hbox) + mainContainer.children['comboAlignItems'].set_on_change_listener(self.onchange_comboAlignItems,vbox,hbox) + + lblTitle = gui.Label("The following example shows the two main layout style properties for the VBox and HBox containers. Change the value of the two combo boxes.", + style='position:absolute; left:0px; top:0px') + mainContainer.append(lblTitle) + + self.mainContainer = mainContainer + return self.mainContainer + + def onchange_comboJustifyContent(self,emitter,new_value,vbox,hbox): + vbox.style['justify-content'] = new_value + hbox.style['justify-content'] = new_value + + def onchange_comboAlignItems(self,emitter,new_value,vbox,hbox): + vbox.style['align-items'] = new_value + hbox.style['align-items'] = new_value + + + +#Configuration +configuration = {'config_enable_file_cache': True, 'config_multiple_instance': True, 'config_port': 8081, 'config_address': '0.0.0.0', 'config_start_browser': True, 'config_project_name': 'untitled', 'config_resourcepath': './res/'} + +if __name__ == "__main__": + + #on linux generate server.pen with + #openssl req -new -x509 -keyout server.pem -out server.pem -days 365 -nodes + + start(untitled, address=configuration['config_address'], port=configuration['config_port'], multiple_instance=configuration['config_multiple_instance'], enable_file_cache=configuration['config_enable_file_cache'],start_browser=configuration['config_start_browser'], https=True, certfile="./server.pem") diff --git a/genCertificate.sh b/genCertificate.sh new file mode 100644 index 00000000..0e4a7e50 --- /dev/null +++ b/genCertificate.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +openssl req -new -x509 -keyout server.pem -out server.pem -days 365 -nodes diff --git a/remi/server.py b/remi/server.py index cc215456..a7bc0829 100644 --- a/remi/server.py +++ b/remi/server.py @@ -34,6 +34,7 @@ import time import os import re +import ssl try: from urllib import unquote from urllib import quote @@ -916,8 +917,13 @@ class ThreadedHTTPServer(socketserver.ThreadingMixIn, HTTPServer): def __init__(self, server_address, RequestHandlerClass, websocket_address, auth, multiple_instance, enable_file_cache, update_interval, websocket_timeout_timer_ms, host_name, pending_messages_queue_length, - title, *userdata): + title, https, certfile, *userdata): HTTPServer.__init__(self, server_address, RequestHandlerClass) + if https: + try: + self.socket = ssl.wrap_socket(self.socket, certfile=certfile, server_side=True) + except Exception as e: + logging.warn("Cannot start https server %s", str(e)) self.websocket_address = websocket_address self.auth = auth self.multiple_instance = multiple_instance @@ -935,7 +941,7 @@ class Server(object): def __init__(self, gui_class, title='', start=True, address='127.0.0.1', port=8081, username=None, password=None, multiple_instance=False, enable_file_cache=True, update_interval=0.1, start_browser=True, websocket_timeout_timer_ms=1000, websocket_port=0, host_name=None, - pending_messages_queue_length=1000, userdata=()): + pending_messages_queue_length=1000, userdata=(), https=False, certfile=None, ignoreSIGINT=False): global http_server_instance http_server_instance = self @@ -955,6 +961,7 @@ def __init__(self, gui_class, title='', start=True, address='127.0.0.1', port=80 self._host_name = host_name self._pending_messages_queue_length = pending_messages_queue_length self._userdata = userdata + if username and password: self._auth = base64.b64encode(encode_text("%s:%s" % (username, password))) else: @@ -963,12 +970,18 @@ def __init__(self, gui_class, title='', start=True, address='127.0.0.1', port=80 if not isinstance(userdata, tuple): raise ValueError('userdata must be a tuple') + if https: + if certfile is None: + raise ValueError('server certificate must be used') + + + self._log = logging.getLogger('remi.server') self._alive = True if start: self._myid = threading.Thread.ident - self.start() - self.serve_forever() + self.start(https, certfile) + self.serve_forever(ignoreSIGINT) @property def title(self): @@ -978,7 +991,7 @@ def title(self): def address(self): return self._base_address - def start(self): + def start(self, https, certfile): # here the websocket is started on an ephemereal port self._wsserver = ThreadedWebsocketServer((self._address, self._websocket_port), WebSocketsHandler, self._multiple_instance) @@ -995,12 +1008,14 @@ def start(self): self._multiple_instance, self._enable_file_cache, self._update_interval, self._websocket_timeout_timer_ms, self._host_name, self._pending_messages_queue_length, - self._title, *self._userdata) + self._title, https, certfile, *self._userdata) shost, sport = self._sserver.socket.getsockname()[:2] # when listening on multiple net interfaces the browsers connects to localhost if shost == '0.0.0.0': shost = '127.0.0.1' self._base_address = 'http://%s:%s/' % (shost,sport) + if(https): + self._base_address = 'https://%s:%s/' % (shost,sport) self._log.info('Started httpserver %s' % self._base_address) if self._start_browser: try: @@ -1016,7 +1031,7 @@ def start(self): self._sth.daemon = False self._sth.start() - def serve_forever(self): + def serve_forever(self, ignoreSIGINT=False): # we could join on the threads, but join blocks all interupts (including # ctrl+c, so just spin here # noinspection PyBroadException @@ -1024,7 +1039,7 @@ def serve_forever(self): def sig_ignore(sig, _): self._log.info('*** signal %d ignored.' % sig) return signal.SIG_IGN - signal.signal(signal.SIGINT, sig_ignore) + if ignoreSIGINT: signal.signal(signal.SIGINT, sig_ignore) while self._alive: signal.pause() self._log.debug(' ** signal received') @@ -1085,6 +1100,7 @@ def start(main_gui_class, **kwargs): logging.getLogger('remi').setLevel( level=logging.DEBUG if debug else logging.INFO) + if standalone: s = StandaloneServer(main_gui_class, start=True, **kwargs) else: diff --git a/widgets_overview_app.py b/widgets_overview_app.py new file mode 100644 index 00000000..c7dfd167 --- /dev/null +++ b/widgets_overview_app.py @@ -0,0 +1,340 @@ +""" + 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 remi.gui as gui +from remi import start, App +from threading import Timer + + +class MyApp(App): + def __init__(self, *args): + super(MyApp, self).__init__(*args) + + def main(self): + # the margin 0px auto centers the main container + verticalContainer = gui.Widget(width=540, margin='0px auto', style={'display': 'block', 'overflow': 'hidden'}) + + horizontalContainer = gui.Widget(width='100%', layout_orientation=gui.Widget.LAYOUT_HORIZONTAL, margin='0px', style={'display': 'block', 'overflow': 'auto'}) + + subContainerLeft = gui.Widget(width=320, style={'display': 'block', 'overflow': 'auto', 'text-align': 'center'}) + self.img = gui.Image('/res/logo.png', width=100, height=100, margin='10px') + self.img.set_on_click_listener(self.on_img_clicked) + + self.table = gui.Table.new_from_list([('ID', 'First Name', 'Last Name'), + ('101', 'Danny', 'Young'), + ('102', 'Christine', 'Holand'), + ('103', 'Lars', 'Gordon'), + ('104', 'Roberto', 'Robitaille'), + ('105', 'Maria', 'Papadopoulos')], width=300, height=200, margin='10px') + self.table.set_on_table_row_click_listener(self.on_table_row_click) + + # the arguments are width - height - layoutOrientationOrizontal + subContainerRight = gui.Widget(style={'width': '220px', 'display': 'block', 'overflow': 'auto', 'text-align': 'center'}) + self.count = 0 + self.counter = gui.Label('', width=200, height=30, margin='10px') + + self.lbl = gui.Label('This is a LABEL!', width=200, height=30, margin='10px') + + self.bt = gui.Button('Press me!', width=200, height=30, margin='10px') + # setting the listener for the onclick event of the button + self.bt.set_on_click_listener(self.on_button_pressed) + + self.txt = gui.TextInput(width=200, height=30, margin='10px') + self.txt.set_text('This is a TEXTAREA') + self.txt.set_on_change_listener(self.on_text_area_change) + + self.spin = gui.SpinBox(-10, -100, 1000, width=200, height=30, margin='10px') + self.spin.set_on_change_listener(self.on_spin_change) + + self.check = gui.CheckBoxLabel('Label checkbox', True, width=200, height=30, margin='10px') + self.check.set_on_change_listener(self.on_check_change) + + self.btInputDiag = gui.Button('Open InputDialog', width=200, height=30, margin='10px') + self.btInputDiag.set_on_click_listener(self.open_input_dialog) + + self.btFileDiag = gui.Button('File Selection Dialog', width=200, height=30, margin='10px') + self.btFileDiag.set_on_click_listener(self.open_fileselection_dialog) + + self.btUploadFile = gui.FileUploader('./', width=200, height=30, margin='10px') + self.btUploadFile.set_on_success_listener(self.fileupload_on_success) + self.btUploadFile.set_on_failed_listener(self.fileupload_on_failed) + + items = ('Danny Young','Christine Holand','Lars Gordon','Roberto Robitaille') + self.listView = gui.ListView.new_from_list(items, width=300, height=120, margin='10px') + self.listView.set_on_selection_listener(self.list_view_on_selected) + + self.link = gui.Link("http://localhost:8081", "A link to here", width=200, height=30, margin='10px') + + self.dropDown = gui.DropDown.new_from_list(('DropDownItem 0', 'DropDownItem 1'), + width=200, height=20, margin='10px') + self.dropDown.set_on_change_listener(self.drop_down_changed) + self.dropDown.select_by_value('DropDownItem 0') + + self.slider = gui.Slider(10, 0, 100, 5, width=200, height=20, margin='10px') + self.slider.set_on_change_listener(self.slider_changed) + + self.colorPicker = gui.ColorPicker('#ffbb00', width=200, height=20, margin='10px') + self.colorPicker.set_on_change_listener(self.color_picker_changed) + + self.date = gui.Date('2015-04-13', width=200, height=20, margin='10px') + self.date.set_on_change_listener(self.date_changed) + + self.video = gui.VideoPlayer('http://www.w3schools.com/tags/movie.mp4', + 'http://www.oneparallel.com/wp-content/uploads/2011/01/placeholder.jpg', + width=300, height=270, margin='10px') + + self.tree = gui.TreeView(width='100%', height=300) + ti1 = gui.TreeItem("Item1") + ti2 = gui.TreeItem("Item2") + ti3 = gui.TreeItem("Item3") + subti1 = gui.TreeItem("Sub Item1") + subti2 = gui.TreeItem("Sub Item2") + subti3 = gui.TreeItem("Sub Item3") + subti4 = gui.TreeItem("Sub Item4") + subsubti1 = gui.TreeItem("Sub Sub Item1") + subsubti2 = gui.TreeItem("Sub Sub Item2") + subsubti3 = gui.TreeItem("Sub Sub Item3") + self.tree.append(ti1) + self.tree.append(ti2) + self.tree.append(ti3) + ti2.append(subti1) + ti2.append(subti2) + ti2.append(subti3) + ti2.append(subti4) + subti4.append(subsubti1) + subti4.append(subsubti2) + subti4.append(subsubti3) + + # appending a widget to another, the first argument is a string key + subContainerRight.append(self.counter) + subContainerRight.append(self.lbl) + subContainerRight.append(self.bt) + subContainerRight.append(self.txt) + subContainerRight.append(self.spin) + subContainerRight.append(self.check) + subContainerRight.append(self.btInputDiag) + subContainerRight.append(self.btFileDiag) + # use a defined key as we replace this widget later + fdownloader = gui.FileDownloader('download test', '../remi/res/logo.png', width=200, height=30, margin='10px') + subContainerRight.append(fdownloader, key='file_downloader') + subContainerRight.append(self.btUploadFile) + subContainerRight.append(self.dropDown) + subContainerRight.append(self.slider) + subContainerRight.append(self.colorPicker) + subContainerRight.append(self.date) + subContainerRight.append(self.tree) + self.subContainerRight = subContainerRight + + subContainerLeft.append(self.img) + subContainerLeft.append(self.table) + subContainerLeft.append(self.listView) + subContainerLeft.append(self.link) + subContainerLeft.append(self.video) + + horizontalContainer.append(subContainerLeft) + horizontalContainer.append(subContainerRight) + + menu = gui.Menu(width='100%', height='30px') + m1 = gui.MenuItem('File', width=100, height=30) + m2 = gui.MenuItem('View', width=100, height=30) + m2.set_on_click_listener(self.menu_view_clicked) + m11 = gui.MenuItem('Save', width=100, height=30) + m12 = gui.MenuItem('Open', width=100, height=30) + m12.set_on_click_listener(self.menu_open_clicked) + m111 = gui.MenuItem('Save', width=100, height=30) + m111.set_on_click_listener(self.menu_save_clicked) + m112 = gui.MenuItem('Save as', width=100, height=30) + m112.set_on_click_listener(self.menu_saveas_clicked) + m3 = gui.MenuItem('Dialog', width=100, height=30) + m3.set_on_click_listener(self.menu_dialog_clicked) + + menu.append(m1) + menu.append(m2) + menu.append(m3) + m1.append(m11) + m1.append(m12) + m11.append(m111) + m11.append(m112) + + menubar = gui.MenuBar(width='100%', height='30px') + menubar.append(menu) + + verticalContainer.append(menubar) + verticalContainer.append(horizontalContainer) + + # kick of regular display of counter + self.display_counter() + + # returning the root widget + return verticalContainer + + def display_counter(self): + self.counter.set_text('Running Time: ' + str(self.count)) + self.count += 1 + Timer(1, self.display_counter).start() + + def menu_dialog_clicked(self, widget): + self.dialog = gui.GenericDialog(title='Dialog Box', message='Click Ok to transfer content to main page', width='500px') + self.dtextinput = gui.TextInput(width=200, height=30) + self.dtextinput.set_value('Initial Text') + self.dialog.add_field_with_label('dtextinput', 'Text Input', self.dtextinput) + + self.dcheck = gui.CheckBox(False, width=200, height=30) + self.dialog.add_field_with_label('dcheck', 'Label Checkbox', self.dcheck) + values = ('Danny Young', 'Christine Holand', 'Lars Gordon', 'Roberto Robitaille') + self.dlistView = gui.ListView.new_from_list(values, width=200, height=120) + self.dialog.add_field_with_label('dlistView', 'Listview', self.dlistView) + + self.ddropdown = gui.DropDown.new_from_list(('DropDownItem 0', 'DropDownItem 1'), + width=200, height=20) + self.dialog.add_field_with_label('ddropdown', 'Dropdown', self.ddropdown) + + self.dspinbox = gui.SpinBox(min=0, max=5000, width=200, height=20) + self.dspinbox.set_value(50) + self.dialog.add_field_with_label('dspinbox', 'Spinbox', self.dspinbox) + + self.dslider = gui.Slider(10, 0, 100, 5, width=200, height=20) + self.dspinbox.set_value(50) + self.dialog.add_field_with_label('dslider', 'Slider', self.dslider) + + self.dcolor = gui.ColorPicker(width=200, height=20) + self.dcolor.set_value('#ffff00') + self.dialog.add_field_with_label('dcolor', 'Colour Picker', self.dcolor) + + self.ddate = gui.Date(width=200, height=20) + self.ddate.set_value('2000-01-01') + self.dialog.add_field_with_label('ddate', 'Date', self.ddate) + + self.dialog.set_on_confirm_dialog_listener(self.dialog_confirm) + self.dialog.show(self) + + def dialog_confirm(self, widget): + result = self.dialog.get_field('dtextinput').get_value() + self.txt.set_value(result) + + result = self.dialog.get_field('dcheck').get_value() + self.check.set_value(result) + + result = self.dialog.get_field('ddropdown').get_value() + self.dropDown.select_by_value(result) + + result = self.dialog.get_field('dspinbox').get_value() + self.spin.set_value(result) + + result = self.dialog.get_field('dslider').get_value() + self.slider.set_value(result) + + result = self.dialog.get_field('dcolor').get_value() + self.colorPicker.set_value(result) + + result = self.dialog.get_field('ddate').get_value() + self.date.set_value(result) + + result = self.dialog.get_field('dlistView').get_value() + self.listView.select_by_value(result) + + # listener function + def on_img_clicked(self, widget): + self.lbl.set_text('Image clicked!') + + def on_table_row_click(self, table, row, item): + self.lbl.set_text('Table Item clicked: ' + item.get_text()) + + def on_button_pressed(self, widget): + self.lbl.set_text('Button pressed! ') + self.bt.set_text('Hi!') + + def on_text_area_change(self, widget, newValue): + self.lbl.set_text('Text Area value changed!') + + def on_spin_change(self, widget, newValue): + self.lbl.set_text('SpinBox changed, new value: ' + str(newValue)) + + def on_check_change(self, widget, newValue): + self.lbl.set_text('CheckBox changed, new value: ' + str(newValue)) + + def open_input_dialog(self, widget): + self.inputDialog = gui.InputDialog('Input Dialog', 'Your name?', + initial_value='type here', + width=500, height=160) + self.inputDialog.set_on_confirm_value_listener( + self.on_input_dialog_confirm) + + # here is returned the Input Dialog widget, and it will be shown + self.inputDialog.show(self) + + def on_input_dialog_confirm(self, widget, value): + self.lbl.set_text('Hello ' + value) + + def open_fileselection_dialog(self, widget): + self.fileselectionDialog = gui.FileSelectionDialog('File Selection Dialog', 'Select files and folders', False, + '.') + self.fileselectionDialog.set_on_confirm_value_listener( + self.on_fileselection_dialog_confirm) + + # here is returned the Input Dialog widget, and it will be shown + self.fileselectionDialog.show(self) + + def on_fileselection_dialog_confirm(self, widget, filelist): + # a list() of filenames and folders is returned + self.lbl.set_text('Selected files: %s' % ','.join(filelist)) + if len(filelist): + f = filelist[0] + # replace the last download link + fdownloader = gui.FileDownloader("download selected", f, width=200, height=30) + self.subContainerRight.append(fdownloader, key='file_downloader') + + def list_view_on_selected(self, widget, selected_item_key): + """ The selection event of the listView, returns a key of the clicked event. + You can retrieve the item rapidly + """ + self.lbl.set_text('List selection: ' + self.listView.children[selected_item_key].get_text()) + + def drop_down_changed(self, widget, value): + self.lbl.set_text('New Combo value: ' + value) + + def slider_changed(self, widget, value): + self.lbl.set_text('New slider value: ' + str(value)) + + def color_picker_changed(self, widget, value): + self.lbl.set_text('New color value: ' + value) + + def date_changed(self, widget, value): + self.lbl.set_text('New date value: ' + value) + + def menu_save_clicked(self, widget): + self.lbl.set_text('Menu clicked: Save') + + def menu_saveas_clicked(self, widget): + self.lbl.set_text('Menu clicked: Save As') + + def menu_open_clicked(self, widget): + self.lbl.set_text('Menu clicked: Open') + + def menu_view_clicked(self, widget): + self.lbl.set_text('Menu clicked: View') + + def fileupload_on_success(self, widget, filename): + self.lbl.set_text('File upload success: ' + filename) + + def fileupload_on_failed(self, widget, filename): + self.lbl.set_text('File upload failed: ' + filename) + + +if __name__ == "__main__": + # starts the webserver + # optional parameters + # start(MyApp,address='127.0.0.1', port=8081, multiple_instance=False,enable_file_cache=True, update_interval=0.1, start_browser=True) + + start(MyApp, debug=True, address='127.0.0.1', start_browser=True, https=True, certfile='server.pem')